Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/brief/enrich.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func cmdEnrich(args []string) {
keep := fs.Bool("keep", false, "Keep downloaded remote source")
depth := fs.Int("depth", -1, "Git clone depth (0 = full clone, default shallow)")
dir := fs.String("dir", "", "Directory to clone remote source into")
scanDepth := fs.Int("scan-depth", 0, "Max directory depth for language detection (default 4)")
scanDepth := fs.Int("scan-depth", 0, "Max directory depth for language detection (0 = unlimited)")
skip := fs.String("skip", "", "Additional directories to skip, comma-separated")
_ = fs.Parse(args)

Expand Down
2 changes: 1 addition & 1 deletion cmd/brief/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func cmdScan(args []string) {
keep := fs.Bool("keep", false, "Keep downloaded remote source")
depth := fs.Int("depth", -1, "Git clone depth (0 = full clone, default shallow)")
dir := fs.String("dir", "", "Directory to clone remote source into")
scanDepth := fs.Int("scan-depth", 0, "Max directory depth for language detection (default 4)")
scanDepth := fs.Int("scan-depth", 0, "Max directory depth for language detection (0 = unlimited)")
skip := fs.String("skip", "", "Additional directories to skip, comma-separated")
tracked := fs.Bool("tracked", false, "Only consider files tracked by git")
version := fs.Bool("version", false, "Print version and exit")
Expand Down
2 changes: 1 addition & 1 deletion cmd/brief/threat.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func runDetection(name string, args []string) (*detect.Engine, *brief.Report, ou
jsonFlag := fs.Bool("json", false, "Force JSON output")
humanFlag := fs.Bool("human", false, "Force human-readable output")
markdownFlag := fs.Bool("markdown", false, "Force markdown output")
scanDepth := fs.Int("scan-depth", 0, "Max directory depth for language detection (default 4)")
scanDepth := fs.Int("scan-depth", 0, "Max directory depth for language detection (0 = unlimited)")
skip := fs.String("skip", "", "Additional directories to skip, comma-separated")
_ = fs.Parse(args)

Expand Down
32 changes: 18 additions & 14 deletions detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
)

const (
defaultScanDepth = 4 // max directory depth for language detection
extScanFileLimit = 10000 // max files to visit when collecting extensions
microsPerMS = 1000.0 // microseconds per millisecond
globSplitParts = 2 // expected parts when splitting "**/" patterns

Expand All @@ -41,7 +41,7 @@ const (
type Engine struct {
KB *kb.KnowledgeBase
Root string
ScanDepth int // max directory depth for recursive detection (0 = default 4)
ScanDepth int // optional max directory depth for recursive detection (0 = unlimited)
SkipDirs []string // additional directories to skip during walks
TrackedOnly bool // only consider files tracked by git
filesChecked int
Expand Down Expand Up @@ -109,6 +109,7 @@ var skipDirs = map[string]bool{
"third_party": true,
"thirdparty": true,
"external": true,
"testdata": true,
"tmp": true,
"temp": true,
"cache": true,
Expand Down Expand Up @@ -564,49 +565,52 @@ func (e *Engine) recursiveGlob(pattern string) bool {
return found
}

// loadFileExts walks the project to a bounded depth to collect file extensions.
// Cached for the lifetime of the engine. Default depth of 4 covers most layouts
// (src/main/java/*.java, lib/something/*.rb).
// loadFileExts walks the project to collect file extension counts. Cached for
// the lifetime of the engine. The walk is bounded by extScanFileLimit rather
// than directory depth so that deep source layouts such as
// app/src/main/java/<package>/ are reached; skipDirs already prunes the
// expensive vendor/build directories.
// Uses WalkDir instead of Walk to avoid following symlinks into directories.
func (e *Engine) loadFileExts() {
if e.fileExts != nil {
return
}
e.fileExts = make(map[string]int)
maxDepth := e.ScanDepth
if maxDepth == 0 {
maxDepth = defaultScanDepth
}
rootLen := len(e.Root)
seen := 0
errDone := errors.New("done")
_ = filepath.WalkDir(e.Root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
rel := strings.TrimPrefix(path[rootLen:], string(filepath.Separator))
if d.IsDir() {
name := d.Name()
if name != "." && e.shouldSkipDir(name) {
return filepath.SkipDir
}
rel := path[rootLen:]
depth := strings.Count(rel, string(filepath.Separator))
if depth > maxDepth {
if e.ScanDepth > 0 && strings.Count(rel, string(filepath.Separator))+1 > e.ScanDepth {
return filepath.SkipDir
}
if !e.isTracked(strings.TrimPrefix(rel, string(filepath.Separator))) {
if !e.isTracked(rel) {
return filepath.SkipDir
}
return nil
}
if d.Type()&os.ModeSymlink != 0 {
return nil
}
if !e.isTracked(strings.TrimPrefix(path[rootLen:], string(filepath.Separator))) {
if !e.isTracked(rel) {
return nil
}
ext := filepath.Ext(d.Name())
if ext != "" {
e.fileExts[ext]++
}
seen++
if seen >= extScanFileLimit {
return errDone
}
return nil
})
}
Expand Down
53 changes: 53 additions & 0 deletions detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,59 @@ func TestPythonProject(t *testing.T) {
}
}

func TestGradleJavaKotlinDSL(t *testing.T) {
// Regression for #84: a Java project that uses the Kotlin DSL for its
// Gradle build scripts must be reported as Java, not Kotlin.
r := runOn(t, "../testdata/gradle-java-kotlin-dsl")

if len(r.Languages) != 1 || r.Languages[0].Name != "Java" {
t.Fatalf("expected only Java language, got %v", languageNames(r))
}
if !slices.Contains(packageManagerNames(r), "Gradle") {
t.Errorf("expected Gradle package manager, got %v", packageManagerNames(r))
}
}

func TestGradleJavaGroovyDSL(t *testing.T) {
// Regression for #84: with build.gradle in app/ and source under
// app/src/main/java/<pkg>/, both Java and Gradle must be detected.
r := runOn(t, "../testdata/gradle-java-groovy-dsl")

if len(r.Languages) == 0 || r.Languages[0].Name != "Java" {
t.Fatalf("expected Java language, got %v", languageNames(r))
}
for _, l := range r.Languages {
if l.Name == "Groovy" {
t.Errorf("did not expect Groovy language for build script only, got %v", languageNames(r))
}
}
if !slices.Contains(packageManagerNames(r), "Gradle") {
t.Errorf("expected Gradle package manager, got %v", packageManagerNames(r))
}
}

func TestScanDepthOverride(t *testing.T) {
engine := New(loadKB(t), "../testdata/gradle-java-groovy-dsl")
engine.ScanDepth = 2
r, err := engine.Run()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, l := range r.Languages {
if l.Name == "Java" {
t.Errorf("expected ScanDepth=2 to miss app/src/main/java/..., got %v", languageNames(r))
}
}
}

func languageNames(r *brief.Report) []string {
names := make([]string, 0, len(r.Languages))
for _, l := range r.Languages {
names = append(names, l.Name)
}
return names
}

func writeProjectFile(t *testing.T, dir, path, content string) {
t.Helper()
full := filepath.Join(dir, path)
Expand Down
2 changes: 1 addition & 1 deletion knowledge/java/language.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ docs = "https://docs.oracle.com/en/java/"
description = "General-purpose, class-based, object-oriented programming language"

[detect]
files = ["pom.xml", "build.gradle", "build.gradle.kts", "*.java", "**/*.java"]
files = ["pom.xml", "*.java", "**/*.java"]
ecosystems = ["java"]

[taxonomy]
Expand Down
2 changes: 1 addition & 1 deletion knowledge/kotlin/language.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repo = "https://github.com/JetBrains/kotlin"
description = "Modern, concise programming language for the JVM"

[detect]
files = ["*.kt", "**/*.kt", "*.kts", "build.gradle.kts"]
files = ["*.kt", "**/*.kt"]
ecosystems = ["kotlin"]

[taxonomy]
Expand Down
7 changes: 7 additions & 0 deletions testdata/gradle-java-groovy-dsl/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
id 'application'
}

application {
mainClass = 'org.example.App'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example;

public class App {
public static void main(String[] args) {
System.out.println("hello");
}
}
2 changes: 2 additions & 0 deletions testdata/gradle-java-groovy-dsl/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rootProject.name = 'test-gradle'
include('app')
7 changes: 7 additions & 0 deletions testdata/gradle-java-kotlin-dsl/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
application
}

application {
mainClass = "org.example.App"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example;

public class App {
public static void main(String[] args) {
System.out.println("hello");
}
}
2 changes: 2 additions & 0 deletions testdata/gradle-java-kotlin-dsl/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rootProject.name = "test-gradle"
include("app")
Loading