diff --git a/config/build_tools.yml b/config/build_tools.yml index 05c61bc..e465f25 100644 --- a/config/build_tools.yml +++ b/config/build_tools.yml @@ -1,5 +1,9 @@ build_tools: rules: + bazel: + when: fs.fileExists("BUILD.bazel") || fs.fileExists("WORKSPACE.bazel") || fs.fileExists("MODULE.bazel") + then: bazel + deno: when: fs.fileExists("deno.json") || fs.fileExists("deno.lock") then: deno diff --git a/expr.cache b/expr.cache index 4c787d8..6958b5d 100644 --- a/expr.cache +++ b/expr.cache @@ -156,6 +156,7 @@ fs.depVersion("rust", "yew") EggIARIECgJmcxITCAISDxoNZnMuZGVwVmVyc2lvbhoGCAQSAhg fs.fileExists(".eleventy.js") || fs.glob("eleventy.config.*s").size() > 0 || fs.depExists("js", "@11ty/eleventy") EggIBBIECgJmcxIPCAcSCxoJbGlzdF9zaXplEhAIDxIMGgpsb2dpY2FsX29yEg0IBRIJGgdmcy5nbG9iEhMICBIPGg1ncmVhdGVyX2ludDY0EggICxIECgJmcxISCAwSDhoMZnMuZGVwRXhpc3RzEggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxIQCAoSDBoKbG9naWNhbF9vchoGCAoSAhgBGgYIDxICGAEaBggEEgIKABoGCAMSAhgFGgYICRICGAIaBggCEgIYARoGCAsSAgoAGgYIDBICGAEaBggGEgIYBRoKCAUSBjIECgIYBRoGCAcSAhgCGgYIDRICGAUaBggBEgIKABoGCAgSAhgBGgYIDhICGAUi0wEQDzLOARIEX3x8XxqKARAKMoUBEgRffHxfGi4QAjIqCggQASIECgJmcxIKZmlsZUV4aXN0cxoSEAMaDjIMLmVsZXZlbnR5LmpzGk0QCDJJEgNfPl8aOhAHMjYKLhAFMioKCBAEIgQKAmZzEgRnbG9iGhgQBhoUMhJlbGV2ZW50eS5jb25maWcuKnMSBHNpemUaBhAJGgIYABo5EAwyNQoIEAsiBAoCZnMSCWRlcEV4aXN0cxoIEA0aBDICanMaFBAOGhAyDkAxMXR5L2VsZXZlbnR5KmYSBzxpbnB1dD4aAXIiBAgKEB4iBAgMEFkiBAgJEEgiBAgPEEoiBAgDEA4iBAgLEE0iBAgBEAAiBAgFECgiBAgIEEYiBAgNEFoiBAgOEGAiBAgCEA0iBAgGECkiBAgEECEiBAgHEEM= fs.fileExists(".meteor/packages") EggIARIECgJmcxITCAISDxoNZnNfZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIyEAIyLgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaFhADGhIyEC5tZXRlb3IvcGFja2FnZXMqHhIHPGlucHV0PhoBIiIECAEQACIECAIQDSIECAMQDg== fs.fileExists(".platform.app.yaml") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASI0EAIyMAoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaGBADGhQyEi5wbGF0Zm9ybS5hcHAueWFtbCoeEgc8aW5wdXQ+GgEkIgQIARAAIgQIAhANIgQIAxAO +fs.fileExists("BUILD.bazel") || fs.fileExists("WORKSPACE.bazel") || fs.fileExists("MODULE.bazel") EhMICRIPGg1mcy5maWxlRXhpc3RzEhAICxIMGgpsb2dpY2FsX29yEggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxIICAQSBAoCZnMSEwgFEg8aDWZzLmZpbGVFeGlzdHMSEAgHEgwaCmxvZ2ljYWxfb3ISCAgIEgQKAmZzGgYIBhICGAUaBggIEgIKABoGCAMSAhgFGgYIAhICGAEaBggFEgIYARoGCAoSAhgFGgYIARICCgAaBggJEgIYARoGCAsSAhgBGgYIBBICCgAaBggHEgIYASKpARALMqQBEgRffHxfGmwQBzJoEgRffHxfGi0QAjIpCggQASIECgJmcxIKZmlsZUV4aXN0cxoREAMaDTILQlVJTEQuYmF6ZWwaMRAFMi0KCBAEIgQKAmZzEgpmaWxlRXhpc3RzGhUQBhoRMg9XT1JLU1BBQ0UuYmF6ZWwaLhAJMioKCBAIIgQKAmZzEgpmaWxlRXhpc3RzGhIQChoOMgxNT0RVTEUuYmF6ZWwqThIHPGlucHV0PhoBYiIECAQQICIECAYQLiIECAUQLSIECAsQQSIECAcQHSIECAgQRCIECAEQACIECAIQDSIECAkQUSIECAoQUiIECAMQDg== fs.fileExists("Cargo.toml") EhMIAhIPGg1mc19maWxlRXhpc3RzEggIARIECgJmcxoGCAESAgoAGgYIAhICGAEaBggDEgIYBSIsEAIyKAoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaEBADGgwyCkNhcmdvLnRvbWwqHhIHPGlucHV0PhoBHCIECAEQACIECAIQDSIECAMQDg== fs.fileExists("Gruntfile.js") || fs.fileExists("Gruntfile.coffee") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxIICAQSBAoCZnMSEwgFEg8aDWZzLmZpbGVFeGlzdHMSEAgHEgwaCmxvZ2ljYWxfb3IaBggDEgIYBRoGCAESAgoAGgYIAhICGAEaBggGEgIYBRoGCAQSAgoAGgYIBRICGAEaBggHEgIYASJuEAcyahIEX3x8XxouEAIyKgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaEhADGg4yDEdydW50ZmlsZS5qcxoyEAUyLgoIEAQiBAoCZnMSCmZpbGVFeGlzdHMaFhAGGhIyEEdydW50ZmlsZS5jb2ZmZWUqNhIHPGlucHV0PhoBQyIECAcQHiIECAEQACIECAIQDSIECAMQDiIECAQQISIECAUQLiIECAYQLw== fs.fileExists("Makefile") EggIARIECgJmcxITCAISDxoNZnMuZmlsZUV4aXN0cxoGCAMSAhgFGgYIARICCgAaBggCEgIYASIqEAIyJgoIEAEiBAoCZnMSCmZpbGVFeGlzdHMaDhADGgoyCE1ha2VmaWxlKh4SBzxpbnB1dD4aARoiBAgBEAAiBAgCEA0iBAgDEA4= diff --git a/pkg/dep/bazel.go b/pkg/dep/bazel.go new file mode 100644 index 0000000..243ab48 --- /dev/null +++ b/pkg/dep/bazel.go @@ -0,0 +1,736 @@ +package dep + +import ( + "bufio" + "errors" + "io/fs" + "path/filepath" + "regexp" + "strings" + "sync" + + "github.com/IGLOU-EU/go-wildcard/v2" +) + +// Global caches for performance optimization +var ( + // Cache for Maven coordinate parsing results (most expensive operation) + mavenCoordCache = sync.Map{} // thread-safe map[string]string +) + +// bazelParser handles parsing of Bazel build files to extract dependencies +type bazelParser struct { + fsys fs.FS + path string + deps map[string][]Dependency // Keyed by language type (java, python, etc) +} + +// BazelDependency represents a Bazel-specific dependency +type BazelDependency struct { + Target string // e.g., "//lib:mylib" or "@maven//:com_google_guava" + Rule string // e.g., "java_library", "py_library" + External bool // true for external dependencies like @maven// +} + +// newBazelParser creates a new Bazel dependency parser +func newBazelParser(fsys fs.FS, path string) *bazelParser { + return &bazelParser{ + fsys: fsys, + path: path, + deps: make(map[string][]Dependency), + } +} + +// HasBazelFiles checks if the given path contains Bazel build files +func HasBazelFiles(fsys fs.FS, path string) bool { + bazelFiles := []string{ + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + "MODULE.bazel", + } + + for _, filename := range bazelFiles { + if _, err := fsys.Open(filepath.Join(path, filename)); err == nil { + return true + } + } + return false +} + +// ParseBazelDependencies parses Bazel dependencies and returns categorized results +func ParseBazelDependencies(fsys fs.FS, path string) (*bazelParser, error) { + parser := newBazelParser(fsys, path) + if err := parser.parse(); err != nil { + return nil, err + } + return parser, nil +} + +// GetJavaDeps returns Java dependencies found in Bazel files +func (b *bazelParser) GetJavaDeps() []Dependency { + return b.deps["java"] +} + +// GetPythonDeps returns Python dependencies found in Bazel files +func (b *bazelParser) GetPythonDeps() []Dependency { + return b.deps["python"] +} + +// GetGoDeps returns Go dependencies found in Bazel files +func (b *bazelParser) GetGoDeps() []Dependency { + return b.deps["go"] +} + +// GetJSDeps returns JavaScript dependencies found in Bazel files +func (b *bazelParser) GetJSDeps() []Dependency { + return b.deps["js"] +} + +// GetWorkspaceDeps returns WORKSPACE dependencies found in Bazel files +func (b *bazelParser) GetWorkspaceDeps() []Dependency { + return b.deps["workspace"] +} + +// GetAllDeps returns all dependencies regardless of language +func (b *bazelParser) GetAllDeps() []Dependency { + var allDeps []Dependency + for _, langDeps := range b.deps { + allDeps = append(allDeps, langDeps...) + } + return allDeps +} + +// FindDeps finds dependencies matching a pattern across all languages +func (b *bazelParser) FindDeps(pattern string) []Dependency { + var deps []Dependency + for _, dep := range b.GetAllDeps() { + if wildcard.Match(pattern, dep.Name) { + deps = append(deps, dep) + } + } + return deps +} + +// parse orchestrates parsing of all Bazel files +func (b *bazelParser) parse() error { + // Parse BUILD files for target dependencies + if err := b.parseBuildFiles(); err != nil { + return err + } + + // Parse MODULE.bazel for modern Bazel dependencies + if err := b.parseModuleBazel(); err != nil { + return err + } + + // Parse WORKSPACE for legacy external dependencies + if err := b.parseWorkspace(); err != nil { + return err + } + + return nil +} + +// Regular expressions for parsing Bazel dependencies +var ( + // Match deps = ["//path:target", "@external//path:target"] + depsPattern = regexp.MustCompile(`deps\s*=\s*\[(.*?)\]`) + + // Match individual dependency strings + depStringPattern = regexp.MustCompile(`"([^"]+)"`) + + // Match Java rules + javaRulePattern = regexp.MustCompile(`(java_library|java_binary|java_test)\s*\(`) + + // Match Python rules + pythonRulePattern = regexp.MustCompile(`(py_library|py_binary|py_test)\s*\(`) + + // Match Go rules + goRulePattern = regexp.MustCompile(`(go_library|go_binary|go_test)\s*\(`) + + // Match JavaScript/Node.js rules + jsRulePattern = regexp.MustCompile(`(js_library|js_binary|js_test|nodejs_binary|nodejs_test)\s*\(`) + + // Match external Maven dependencies + mavenDepPattern = regexp.MustCompile(`@maven//:(.+)`) + + // Match external pip dependencies + pipDepPattern = regexp.MustCompile(`@pip//(.+)`) + + // Match external dependencies with @repo// format + goDepPattern = regexp.MustCompile(`@([^/]+)//.*`) + + // Match external npm dependencies + npmDepPattern = regexp.MustCompile(`@npm//(.+)`) + + // Match bazel_dep declarations in MODULE.bazel + bazelDepPattern = regexp.MustCompile(`bazel_dep\s*\(\s*name\s*=\s*"([^"]+)"\s*,\s*version\s*=\s*"([^"]+)"`) + + // Match WORKSPACE dependency declarations + mavenInstallPattern = regexp.MustCompile(`maven_install\s*\(`) + httpArchivePattern = regexp.MustCompile(`http_archive\s*\(`) + gitRepositoryPattern = regexp.MustCompile(`git_repository\s*\(`) + + // Match name and version in WORKSPACE declarations + namePattern = regexp.MustCompile(`name\s*=\s*"([^"]+)"`) + versionPattern = regexp.MustCompile(`version\s*=\s*"([^"]+)"`) + tagPattern = regexp.MustCompile(`tag\s*=\s*"([^"]+)"`) + commitPattern = regexp.MustCompile(`commit\s*=\s*"([^"]+)"`) +) + +// parseBuildFiles parses BUILD and BUILD.bazel files for dependencies +func (b *bazelParser) parseBuildFiles() error { + buildFiles := []string{"BUILD", "BUILD.bazel"} + + // Optimize by checking file existence first to avoid unnecessary I/O + existingFiles := make([]string, 0, len(buildFiles)) + for _, filename := range buildFiles { + if _, err := b.fsys.Open(filepath.Join(b.path, filename)); err == nil { + existingFiles = append(existingFiles, filename) + } else if !errors.Is(err, fs.ErrNotExist) { + return err + } + } + + // Parse only existing files + for _, filename := range existingFiles { + if err := b.parseBuildFile(filename); err != nil { + return err + } + } + + return nil +} + +// parseBuildFile parses a single BUILD file +func (b *bazelParser) parseBuildFile(filename string) error { + f, err := b.fsys.Open(filepath.Join(b.path, filename)) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + var currentRule string + var inRule bool + var ruleContent strings.Builder + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip comments and empty lines + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + // Check for start of language-specific rules + switch { + case javaRulePattern.MatchString(line): + currentRule = "java" + inRule = true + ruleContent.Reset() + case pythonRulePattern.MatchString(line): + currentRule = "python" + inRule = true + ruleContent.Reset() + case goRulePattern.MatchString(line): + currentRule = "go" + inRule = true + ruleContent.Reset() + case jsRulePattern.MatchString(line): + currentRule = "js" + inRule = true + ruleContent.Reset() + } + + if inRule { + ruleContent.WriteString(line + " ") + + // Check for end of rule (closing parenthesis) + if strings.Contains(line, ")") { + deps := b.extractDepsFromRule(ruleContent.String(), currentRule) + b.deps[currentRule] = append(b.deps[currentRule], deps...) + inRule = false + } + } + } + + return scanner.Err() +} + +// extractDepsFromRule extracts dependencies from a rule declaration +func (b *bazelParser) extractDepsFromRule(ruleContent, language string) []Dependency { + var deps []Dependency + + // Find deps = [...] pattern + depsMatches := depsPattern.FindStringSubmatch(ruleContent) + if len(depsMatches) < 2 { + return deps + } + + // Extract individual dependency strings + depStrings := depStringPattern.FindAllStringSubmatch(depsMatches[1], 1000) + + // Pre-allocate slice for better performance + deps = make([]Dependency, 0, len(depStrings)) + + for _, match := range depStrings { + if len(match) < 2 { + continue + } + + depTarget := match[1] + dep := b.parseDependencyTarget(depTarget, language) + if dep.Name != "" { + deps = append(deps, dep) + } + } + + return deps +} + +// parseDependencyTarget parses a dependency target string into a Dependency +func (b *bazelParser) parseDependencyTarget(target, language string) Dependency { + var dep Dependency + + // Handle Maven dependencies + if mavenMatches := mavenDepPattern.FindStringSubmatch(target); len(mavenMatches) > 1 { + mavenCoord := mavenMatches[1] + dep.Name = b.parseMavenCoordinate(mavenCoord) + if dep.Name != "" { + // Extract vendor from coordinate if possible + if colonIdx := strings.Index(dep.Name, ":"); colonIdx > 0 { + dep.Vendor = dep.Name[:colonIdx] + } + } + return dep + } + + // Handle pip dependencies + if pipMatches := pipDepPattern.FindStringSubmatch(target); len(pipMatches) > 1 { + pipPackage := pipMatches[1] + // Convert pip package format to standard Python package name + // Common patterns: @pip//package_name, @pip//package_name_extra + dep.Name = strings.ReplaceAll(pipPackage, "_", "-") + return dep + } + + // Handle npm dependencies + if npmMatches := npmDepPattern.FindStringSubmatch(target); len(npmMatches) > 1 { + npmPackage := npmMatches[1] + // Convert npm package format to standard package name + // Common patterns: @npm//package_name, @npm//@scope/package_name + if strings.HasPrefix(npmPackage, "@") { + // Handle scoped packages like @npm//@angular/core -> @angular/core + dep.Name = npmPackage + } else { + // Handle regular packages like @npm//lodash -> lodash + dep.Name = strings.ReplaceAll(npmPackage, "_", "-") + } + return dep + } + + // Handle Go dependencies + if language == "go" { + if goMatches := goDepPattern.FindStringSubmatch(target); len(goMatches) > 1 { + // For Go, external dependencies are typically like @com_github_gorilla_mux// + // Convert to Go module format: github.com/gorilla/mux + repoName := goMatches[1] + // Convert underscores to slashes and dots appropriately + switch { + case strings.HasPrefix(repoName, "com_github_"): + // Handle github.com repositories + parts := strings.Split(repoName, "_") + if len(parts) >= 3 { + dep.Name = "github.com/" + strings.Join(parts[2:], "/") + } else { + dep.Name = repoName + } + case strings.HasPrefix(repoName, "org_golang_x_"): + // Handle golang.org/x repositories + parts := strings.Split(repoName, "_") + if len(parts) >= 4 { + dep.Name = "golang.org/x/" + strings.Join(parts[3:], "/") + } else { + dep.Name = repoName + } + default: + // Generic conversion: replace underscores with dots + dep.Name = strings.ReplaceAll(repoName, "_", ".") + } + return dep + } + } + + // Handle internal dependencies (//path:target) + if strings.HasPrefix(target, "//") { + dep.Name = target + return dep + } + + // Handle other external dependencies (@repo//path:target) + if strings.HasPrefix(target, "@") { + dep.Name = target + return dep + } + + // Handle simple target names + dep.Name = target + return dep +} + +// parseModuleBazel parses MODULE.bazel for modern Bazel dependencies +func (b *bazelParser) parseModuleBazel() error { + f, err := b.fsys.Open(filepath.Join(b.path, "MODULE.bazel")) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip comments and empty lines + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + // Parse bazel_dep declarations + if matches := bazelDepPattern.FindStringSubmatch(line); len(matches) > 2 { + dep := Dependency{ + Name: matches[1], + Version: matches[2], + Constraint: matches[2], + } + + // Add to general category for now - could be categorized better with more context + b.deps["bazel"] = append(b.deps["bazel"], dep) + } + } + + return scanner.Err() +} + +// parseWorkspace parses WORKSPACE files for legacy external dependencies +func (b *bazelParser) parseWorkspace() error { + workspaceFiles := []string{"WORKSPACE", "WORKSPACE.bazel"} + + for _, filename := range workspaceFiles { + if err := b.parseWorkspaceFile(filename); err != nil { + if errors.Is(err, fs.ErrNotExist) { + continue + } + return err + } + } + + return nil +} + +// parseWorkspaceFile parses a single WORKSPACE file +func (b *bazelParser) parseWorkspaceFile(filename string) error { + f, err := b.fsys.Open(filepath.Join(b.path, filename)) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + var currentDeclaration string + var inDeclaration bool + var declarationContent strings.Builder + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip comments and empty lines + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + // Check for start of dependency declarations + switch { + case mavenInstallPattern.MatchString(line): + currentDeclaration = "maven_install" + inDeclaration = true + declarationContent.Reset() + case httpArchivePattern.MatchString(line): + currentDeclaration = "http_archive" + inDeclaration = true + declarationContent.Reset() + case gitRepositoryPattern.MatchString(line): + currentDeclaration = "git_repository" + inDeclaration = true + declarationContent.Reset() + } + + if inDeclaration { + declarationContent.WriteString(line + " ") + + // Check for end of declaration (closing parenthesis) + if strings.Contains(line, ")") { + dep := b.parseWorkspaceDeclaration(declarationContent.String(), currentDeclaration) + if dep.Name != "" { + // Add to workspace category + b.deps["workspace"] = append(b.deps["workspace"], dep) + } + inDeclaration = false + } + } + } + + return scanner.Err() +} + +// parseWorkspaceDeclaration parses a WORKSPACE dependency declaration +func (b *bazelParser) parseWorkspaceDeclaration(content, declarationType string) Dependency { + var dep Dependency + + // Extract name + if nameMatches := namePattern.FindStringSubmatch(content); len(nameMatches) > 1 { + dep.Name = nameMatches[1] + } + + // Extract version information based on declaration type + switch declarationType { + case "maven_install": + // For maven_install, we don't get individual dependency info easily + // This would need more sophisticated parsing of the artifacts list + dep.Name = "maven_install_" + dep.Name + case "http_archive": + // Look for version, tag, or other version indicators + if versionMatches := versionPattern.FindStringSubmatch(content); len(versionMatches) > 1 { + dep.Version = versionMatches[1] + dep.Constraint = versionMatches[1] + } else if tagMatches := tagPattern.FindStringSubmatch(content); len(tagMatches) > 1 { + dep.Version = tagMatches[1] + dep.Constraint = tagMatches[1] + } + case "git_repository": + // Look for tag or commit + if tagMatches := tagPattern.FindStringSubmatch(content); len(tagMatches) > 1 { + dep.Version = tagMatches[1] + dep.Constraint = tagMatches[1] + } else if commitMatches := commitPattern.FindStringSubmatch(content); len(commitMatches) > 1 { + dep.Version = commitMatches[1][:8] // Short commit hash + dep.Constraint = commitMatches[1][:8] + } + } + + return dep +} + +// parseMavenCoordinate converts Bazel Maven coordinate format to standard Maven coordinate +// with sophisticated heuristics for various patterns +func (b *bazelParser) parseMavenCoordinate(mavenCoord string) string { + // Check cache first for performance + if cached, ok := mavenCoordCache.Load(mavenCoord); ok { + if result, ok := cached.(string); ok { + return result + } + } + + result := b.parseMavenCoordinateUncached(mavenCoord) + + // Cache the result for future use + mavenCoordCache.Store(mavenCoord, result) + + return result +} + +// parseMavenCoordinateUncached performs the actual parsing without caching +func (b *bazelParser) parseMavenCoordinateUncached(mavenCoord string) string { + // Handle empty or invalid coordinates + if mavenCoord == "" { + return "" + } + + // Split by underscore - this is the standard Bazel convention + parts := strings.Split(mavenCoord, "_") + if len(parts) < 2 { + return mavenCoord // Return as-is if we can't parse it + } + + // Enhanced pattern recognition for Maven coordinates + // Common patterns in real-world usage: + // 1. Simple: group_artifact (junit_junit) + // 2. Multi-part group: org_springframework_spring_core + // 3. Repeated components: com_google_guava_guava + // 4. Complex artifacts: org_slf4j_slf4j_api, io_grpc_grpc_netty_shaded + // 5. Deep hierarchies: org_apache_commons_commons_lang3 + + var groupId, artifactId string + + switch len(parts) { + case 2: + // Simple case: group_artifact + groupId = parts[0] + artifactId = parts[1] + + case 3: + // Three parts - need to determine the split + // Common patterns: + // - org_junit_jupiter -> org.junit:jupiter + // - com_fasterxml_jackson -> com.fasterxml:jackson + groupId = strings.Join(parts[:2], ".") + artifactId = parts[2] + + case 4: + // Four parts - most complex cases + switch { + case parts[0] == parts[1] && parts[1] == parts[2]: + // Pattern: com_google_guava_guava -> com.google.guava:guava + groupId = strings.Join(parts[:3], ".") + artifactId = parts[3] + case parts[1] == parts[2]: + // Pattern: org_slf4j_slf4j_api -> org.slf4j:slf4j-api + groupId = strings.Join(parts[:2], ".") + artifactId = strings.Join(parts[2:], "-") + case b.isKnownGroupPattern(parts): + // Use known patterns for common libraries + groupId, artifactId = b.parseKnownPattern(parts) + default: + // Default: assume first 3 parts are group, last is artifact + groupId = strings.Join(parts[:3], ".") + artifactId = parts[3] + } + + case 5: + // Five parts - very complex hierarchies + switch { + case b.isKnownGroupPattern(parts): + groupId, artifactId = b.parseKnownPattern(parts) + case parts[2] == parts[3]: + // Pattern like: io_grpc_grpc_netty_shaded -> io.grpc:grpc-netty-shaded + groupId = strings.Join(parts[:2], ".") + artifactId = strings.Join(parts[2:], "-") + default: + // Default: assume first 4 parts are group, last is artifact + groupId = strings.Join(parts[:4], ".") + artifactId = parts[4] + } + + default: + // Six or more parts - handle known patterns or default strategy + if len(parts) >= 6 && b.isKnownGroupPattern(parts) { + groupId, artifactId = b.parseKnownPattern(parts) + } else { + // Conservative default: assume last part is artifact, rest is group + groupId = strings.Join(parts[:len(parts)-1], ".") + artifactId = parts[len(parts)-1] + } + } + + // Post-processing: normalize common naming conventions + artifactId = b.normalizeArtifactId(artifactId, groupId) + + return groupId + ":" + artifactId +} + +// isKnownGroupPattern checks if the coordinate matches known library patterns +func (b *bazelParser) isKnownGroupPattern(parts []string) bool { + if len(parts) < 3 { + return false + } + + // Check for well-known library patterns + coordinate := strings.Join(parts, "_") + + // Spring Framework patterns + if strings.HasPrefix(coordinate, "org_springframework_") { + return true + } + + // Apache Commons patterns + if strings.HasPrefix(coordinate, "org_apache_commons_") { + return true + } + + // Jackson patterns + if strings.HasPrefix(coordinate, "com_fasterxml_jackson_") { + return true + } + + // gRPC patterns + if strings.HasPrefix(coordinate, "io_grpc_") { + return true + } + + // Netty patterns + if strings.HasPrefix(coordinate, "io_netty_") { + return true + } + + return false +} + +// parseKnownPattern handles specific known library patterns +func (b *bazelParser) parseKnownPattern(parts []string) (string, string) { + coordinate := strings.Join(parts, "_") + + // Spring Framework: org_springframework_spring_* -> org.springframework:spring-* + if strings.HasPrefix(coordinate, "org_springframework_spring_") { + return "org.springframework", strings.Join(parts[2:], "-") + } + + // Apache Commons: org_apache_commons_commons_* -> org.apache.commons:commons-* + if strings.HasPrefix(coordinate, "org_apache_commons_commons_") { + return "org.apache.commons", strings.Join(parts[3:], "-") + } + + // Jackson: com_fasterxml_jackson_* -> com.fasterxml.jackson.*:jackson-* + if strings.HasPrefix(coordinate, "com_fasterxml_jackson_") { + if len(parts) >= 4 { + groupId := strings.Join(parts[:4], ".") + artifactId := strings.Join(parts[2:], "-") + return groupId, artifactId + } + } + + // gRPC: io_grpc_grpc_* -> io.grpc:grpc-* + if strings.HasPrefix(coordinate, "io_grpc_grpc_") { + return "io.grpc", strings.Join(parts[2:], "-") + } + + // Netty: io_netty_netty_* -> io.netty:netty-* + if strings.HasPrefix(coordinate, "io_netty_netty_") { + return "io.netty", strings.Join(parts[2:], "-") + } + + // Default fallback + return strings.Join(parts[:len(parts)-1], "."), parts[len(parts)-1] +} + +// normalizeArtifactId applies common normalization rules to artifact IDs +func (b *bazelParser) normalizeArtifactId(artifactId, groupId string) string { + // Replace underscores with hyphens, as hyphens are more standard in artifact names + // This is a conservative normalization step commonly used in Maven coordinates + return strings.ReplaceAll(artifactId, "_", "-") +} + +// ClearBazelCaches clears all Bazel-related caches to free memory +// This can be called periodically in long-running applications +func ClearBazelCaches() { + mavenCoordCache = sync.Map{} +} + +// GetBazelCacheStats returns statistics about cache usage for monitoring +func GetBazelCacheStats() map[string]int { + stats := make(map[string]int) + + // Count Maven coordinate cache entries + mavenCount := 0 + mavenCoordCache.Range(func(_, _ any) bool { + mavenCount++ + return true + }) + stats["maven_coordinates"] = mavenCount + + return stats +} diff --git a/pkg/dep/bazel_test.go b/pkg/dep/bazel_test.go new file mode 100644 index 0000000..88bb366 --- /dev/null +++ b/pkg/dep/bazel_test.go @@ -0,0 +1,569 @@ +package dep_test + +import ( + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/upsun/whatsun/pkg/dep" +) + +func TestBazelHasFiles(t *testing.T) { + cases := []struct { + name string + files map[string][]byte + expected bool + }{ + { + name: "has BUILD file", + files: map[string][]byte{ + "BUILD": []byte("java_library(name = 'lib')"), + }, + expected: true, + }, + { + name: "has BUILD.bazel file", + files: map[string][]byte{ + "BUILD.bazel": []byte("java_library(name = 'lib')"), + }, + expected: true, + }, + { + name: "has MODULE.bazel file", + files: map[string][]byte{ + "MODULE.bazel": []byte("module(name = 'test')"), + }, + expected: true, + }, + { + name: "has WORKSPACE file", + files: map[string][]byte{ + "WORKSPACE": []byte("workspace(name = 'test')"), + }, + expected: true, + }, + { + name: "no Bazel files", + files: map[string][]byte{ + "build.gradle": []byte("plugins { id 'java' }"), + }, + expected: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + fsys := fstest.MapFS{} + for filename, content := range c.files { + fsys[filename] = &fstest.MapFile{Data: content} + } + + result := dep.HasBazelFiles(fsys, ".") + assert.Equal(t, c.expected, result) + }) + } +} + +func TestBazelJavaParsingSimple(t *testing.T) { + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +java_library( + name = "lib", + deps = [ + "//internal:common", + "@maven//:com_google_guava_guava", + "@maven//:junit_junit", + ], +) + +java_binary( + name = "main", + deps = [ + ":lib", + "@maven//:org_slf4j_slf4j_api", + ], +) + `)}, + } + + parser, err := dep.ParseBazelDependencies(fsys, ".") + require.NoError(t, err) + + javaDeps := parser.GetJavaDeps() + + expectedDeps := []dep.Dependency{ + {Name: "//internal:common"}, + {Name: "com.google.guava:guava"}, + {Name: "junit:junit"}, + {Name: ":lib"}, + {Name: "org.slf4j:slf4j-api"}, + } + + assert.Len(t, javaDeps, len(expectedDeps)) + + // Check that all expected dependencies are found + for _, expected := range expectedDeps { + found := false + for _, actual := range javaDeps { + if actual.Name == expected.Name { + found = true + break + } + } + assert.True(t, found, "Expected dependency %s not found", expected.Name) + } +} + +func TestBazelModuleFile(t *testing.T) { + fsys := fstest.MapFS{ + "MODULE.bazel": {Data: []byte(` +module(name = "my-module", version = "1.0") + +bazel_dep(name = "rules_java", version = "7.1.0") +bazel_dep(name = "rules_cc", version = "0.1.1") +bazel_dep(name = "platforms", version = "0.0.11") + `)}, + } + + parser, err := dep.ParseBazelDependencies(fsys, ".") + require.NoError(t, err) + + allDeps := parser.GetAllDeps() + + expectedNames := []string{"rules_java", "rules_cc", "platforms"} + + assert.Len(t, allDeps, len(expectedNames)) + + for _, expectedName := range expectedNames { + found := false + for _, dep := range allDeps { + if dep.Name == expectedName { + assert.NotEmpty(t, dep.Version) + found = true + break + } + } + assert.True(t, found, "Expected dependency %s not found", expectedName) + } +} + +func TestBazelJavaIntegration(t *testing.T) { + // Test that Java manager properly integrates Bazel dependencies + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +java_library( + name = "lib", + deps = [ + "@maven//:com_google_guava_guava", + ], +) + `)}, + "pom.xml": {Data: []byte(` + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + `)}, + } + + m, err := dep.GetManager(dep.ManagerTypeJava, fsys, ".") + require.NoError(t, err) + require.NoError(t, m.Init()) + + // Should have dependencies from both Maven (pom.xml) and Bazel (BUILD) + allDeps := m.Find("*") + + // Check that we have dependencies from both sources + hasMaven := false + hasBazel := false + + for _, dep := range allDeps { + if dep.Name == "org.apache.commons:commons-lang3" { + hasMaven = true + } + if dep.Name == "com.google.guava:guava" { + hasBazel = true + } + } + + assert.True(t, hasMaven, "Should have Maven dependency from pom.xml") + assert.True(t, hasBazel, "Should have Bazel dependency from BUILD file") +} + +func TestBazelPythonParsingSimple(t *testing.T) { + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +py_library( + name = "mylib", + deps = [ + "//internal:utils", + "@pip//requests", + "@pip//flask_cors", + ], +) + +py_binary( + name = "main", + deps = [ + ":mylib", + "@pip//click", + ], +) + `)}, + } + + parser, err := dep.ParseBazelDependencies(fsys, ".") + require.NoError(t, err) + + pythonDeps := parser.GetPythonDeps() + + expectedDeps := []dep.Dependency{ + {Name: "//internal:utils"}, + {Name: "requests"}, + {Name: "flask-cors"}, + {Name: ":mylib"}, + {Name: "click"}, + } + + assert.Len(t, pythonDeps, len(expectedDeps)) + + // Check that all expected dependencies are found + for _, expected := range expectedDeps { + found := false + for _, actual := range pythonDeps { + if actual.Name == expected.Name { + found = true + break + } + } + assert.True(t, found, "Expected dependency %s not found", expected.Name) + } +} + +func TestBazelPythonIntegration(t *testing.T) { + // Test that Python manager properly integrates Bazel dependencies + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +py_library( + name = "lib", + deps = [ + "@pip//requests", + ], +) + `)}, + "requirements.txt": {Data: []byte(` +flask==2.0.1 +pytest>=6.0 + `)}, + } + + m, err := dep.GetManager(dep.ManagerTypePython, fsys, ".") + require.NoError(t, err) + require.NoError(t, m.Init()) + + // Should have dependencies from both requirements.txt and Bazel (BUILD) + allDeps := m.Find("*") + + // Check that we have dependencies from both sources + hasRequirements := false + hasBazel := false + + for _, dep := range allDeps { + if dep.Name == "flask" { + hasRequirements = true + } + if dep.Name == "requests" { + hasBazel = true + } + } + + assert.True(t, hasRequirements, "Should have dependency from requirements.txt") + assert.True(t, hasBazel, "Should have Bazel dependency from BUILD file") +} + +func TestBazelGoParsingSimple(t *testing.T) { + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +go_library( + name = "mylib", + deps = [ + "//internal:utils", + "@com_github_gorilla_mux//:mux", + "@org_golang_x_time//rate", + ], +) + +go_binary( + name = "main", + deps = [ + ":mylib", + "@com_github_sirupsen_logrus//:logrus", + ], +) + `)}, + } + + parser, err := dep.ParseBazelDependencies(fsys, ".") + require.NoError(t, err) + + goDeps := parser.GetGoDeps() + + expectedDeps := []dep.Dependency{ + {Name: "//internal:utils"}, + {Name: "github.com/gorilla/mux"}, + {Name: "golang.org/x/time"}, + {Name: ":mylib"}, + {Name: "github.com/sirupsen/logrus"}, + } + + assert.Len(t, goDeps, len(expectedDeps)) + + // Check that all expected dependencies are found + for _, expected := range expectedDeps { + found := false + for _, actual := range goDeps { + if actual.Name == expected.Name { + found = true + break + } + } + assert.True(t, found, "Expected dependency %s not found", expected.Name) + } +} + +func TestBazelGoIntegration(t *testing.T) { + // Test that Go manager properly integrates Bazel dependencies + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +go_library( + name = "lib", + deps = [ + "@com_github_gorilla_mux//:mux", + ], +) + `)}, + "go.mod": {Data: []byte(` +module example.com/myproject + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 +) + `)}, + } + + m, err := dep.GetManager(dep.ManagerTypeGo, fsys, ".") + require.NoError(t, err) + require.NoError(t, m.Init()) + + // Should have dependencies from both go.mod and Bazel (BUILD) + allDeps := m.Find("*") + + // Check that we have dependencies from both sources + hasGoMod := false + hasBazel := false + + for _, dep := range allDeps { + if dep.Name == "github.com/gin-gonic/gin" { + hasGoMod = true + } + if dep.Name == "github.com/gorilla/mux" { + hasBazel = true + } + } + + assert.True(t, hasGoMod, "Should have dependency from go.mod") + assert.True(t, hasBazel, "Should have Bazel dependency from BUILD file") +} + +func TestBazelWorkspaceParsingSimple(t *testing.T) { + fsys := fstest.MapFS{ + "WORKSPACE": {Data: []byte(` +http_archive( + name = "rules_go", + sha256 = "abc123", + urls = ["https://github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip"], + strip_prefix = "rules_go-0.39.1", +) + +git_repository( + name = "com_google_protobuf", + remote = "https://github.com/protocolbuffers/protobuf", + tag = "v3.21.12", +) + +maven_install( + name = "maven", + artifacts = [ + "com.google.guava:guava:31.1-jre", + "junit:junit:4.13.2", + ], + repositories = [ + "https://repo1.maven.org/maven2", + ], +) + `)}, + } + + parser, err := dep.ParseBazelDependencies(fsys, ".") + require.NoError(t, err) + + workspaceDeps := parser.GetWorkspaceDeps() + + expectedDeps := []dep.Dependency{ + {Name: "rules_go"}, + {Name: "com_google_protobuf", Version: "v3.21.12", Constraint: "v3.21.12"}, + {Name: "maven_install_maven"}, + } + + assert.Len(t, workspaceDeps, len(expectedDeps)) + + // Check that all expected dependencies are found + for _, expected := range expectedDeps { + found := false + for _, actual := range workspaceDeps { + if actual.Name == expected.Name { + if expected.Version != "" { + assert.Equal(t, expected.Version, actual.Version, "Version mismatch for %s", expected.Name) + } + found = true + break + } + } + assert.True(t, found, "Expected dependency %s not found", expected.Name) + } +} + +func TestBazelJavaScriptParsingSimple(t *testing.T) { + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +js_library( + name = "mylib", + deps = [ + "//internal:utils", + "@npm//lodash", + "@npm//@angular/core", + "@npm//react", + ], +) + +nodejs_binary( + name = "server", + deps = [ + ":mylib", + "@npm//express", + ], +) + `)}, + } + + parser, err := dep.ParseBazelDependencies(fsys, ".") + require.NoError(t, err) + + jsDeps := parser.GetJSDeps() + + expectedDeps := []dep.Dependency{ + {Name: "//internal:utils"}, + {Name: "lodash"}, + {Name: "@angular/core"}, + {Name: "react"}, + {Name: ":mylib"}, + {Name: "express"}, + } + + assert.Len(t, jsDeps, len(expectedDeps)) + + // Check that all expected dependencies are found + for _, expected := range expectedDeps { + found := false + for _, actual := range jsDeps { + if actual.Name == expected.Name { + found = true + break + } + } + assert.True(t, found, "Expected dependency %s not found", expected.Name) + } +} + +func TestBazelJavaScriptIntegration(t *testing.T) { + // Test that JavaScript manager properly integrates Bazel dependencies + fsys := fstest.MapFS{ + "BUILD.bazel": {Data: []byte(` +js_library( + name = "lib", + deps = [ + "@npm//lodash", + "@npm//@types/node", + ], +) + `)}, + "package.json": {Data: []byte(`{ + "dependencies": { + "react": "^18.0.0", + "express": "^4.18.0" + } + }`)}, + } + + m, err := dep.GetManager(dep.ManagerTypeJavaScript, fsys, ".") + require.NoError(t, err) + require.NoError(t, m.Init()) + + // Should have dependencies from both package.json and Bazel (BUILD) + allDeps := m.Find("*") + + // Check that we have dependencies from both sources + hasPackageJson := false + hasBazel := false + + for _, dep := range allDeps { + if dep.Name == "react" { + hasPackageJson = true + } + if dep.Name == "lodash" { + hasBazel = true + } + } + + assert.True(t, hasPackageJson, "Should have dependency from package.json") + assert.True(t, hasBazel, "Should have Bazel dependency from BUILD file") +} + +func TestBazelFindPattern(t *testing.T) { + fsys := fstest.MapFS{ + "BUILD": {Data: []byte(` +java_library( + name = "lib", + deps = [ + "@maven//:com_google_guava_guava", + "@maven//:com_google_inject_guice", + "@maven//:junit_junit", + ], +) + `)}, + } + + parser, err := dep.ParseBazelDependencies(fsys, ".") + require.NoError(t, err) + + // Test wildcard pattern matching + googleDeps := parser.FindDeps("com.google*") + + expectedCount := 2 // guava and inject + assert.Len(t, googleDeps, expectedCount) + + for _, dep := range googleDeps { + assert.True(t, + dep.Name == "com.google.guava:guava" || dep.Name == "com.google.inject:guice", + "Unexpected dependency: %s", dep.Name) + } +} diff --git a/pkg/dep/go.go b/pkg/dep/go.go index df50eb2..004eff1 100644 --- a/pkg/dep/go.go +++ b/pkg/dep/go.go @@ -14,8 +14,9 @@ type goManager struct { fsys fs.FS path string - initOnce sync.Once - file *modfile.File + initOnce sync.Once + file *modfile.File + bazelDeps []Dependency } func newGoManager(fsys fs.FS, path string) Manager { @@ -43,34 +44,69 @@ func (m *goManager) init() error { return err } m.file = f + + // Parse Bazel dependencies if Bazel files are present + if HasBazelFiles(m.fsys, m.path) { + bazelParser, err := ParseBazelDependencies(m.fsys, m.path) + if err != nil { + return err + } + m.bazelDeps = bazelParser.GetGoDeps() + } + return nil } func (m *goManager) Get(name string) (Dependency, bool) { - for _, v := range m.file.Require { - if v.Mod.Path == name && !v.Indirect { - return Dependency{ - Name: v.Mod.Path, - Version: v.Mod.Version, - IsDirect: !v.Indirect, - ToolName: "go", - }, true + if m.file != nil { + // Check go.mod dependencies first + for _, v := range m.file.Require { + if v.Mod.Path == name && !v.Indirect { + return Dependency{ + Name: v.Mod.Path, + Version: v.Mod.Version, + IsDirect: !v.Indirect, + ToolName: "go", + }, true + } + } + } + + // Check Bazel dependencies + for _, dep := range m.bazelDeps { + if dep.Name == name { + return dep, true } } + return Dependency{}, false } func (m *goManager) Find(pattern string) []Dependency { var deps []Dependency - for _, v := range m.file.Require { - if wildcard.Match(pattern, v.Mod.Path) { - deps = append(deps, Dependency{ - Name: v.Mod.Path, - Version: v.Mod.Version, - IsDirect: !v.Indirect, - ToolName: "go", - }) + seen := make(map[string]struct{}) + + // Add go.mod dependencies + if m.file != nil { + for _, v := range m.file.Require { + if !v.Indirect && wildcard.Match(pattern, v.Mod.Path) { + deps = append(deps, Dependency{ + Name: v.Mod.Path, + Version: v.Mod.Version, + IsDirect: !v.Indirect, + ToolName: "go", + }) + seen[v.Mod.Path] = struct{}{} + } + } + } + + // Add Bazel dependencies (avoid duplicates) + for _, dep := range m.bazelDeps { + if _, exists := seen[dep.Name]; !exists && wildcard.Match(pattern, dep.Name) { + deps = append(deps, dep) } } + return deps } diff --git a/pkg/dep/java.go b/pkg/dep/java.go index 5331abe..1d4eb0c 100644 --- a/pkg/dep/java.go +++ b/pkg/dep/java.go @@ -57,6 +57,16 @@ func (m *javaManager) parse() error { return err } m.deps = append(m.deps, deps...) + + // Parse Bazel dependencies if Bazel files are present + if HasBazelFiles(m.fsys, m.path) { + bazelParser, err := ParseBazelDependencies(m.fsys, m.path) + if err != nil { + return err + } + m.deps = append(m.deps, bazelParser.GetJavaDeps()...) + } + return nil } diff --git a/pkg/dep/js.go b/pkg/dep/js.go index d3ad323..86dc85a 100644 --- a/pkg/dep/js.go +++ b/pkg/dep/js.go @@ -136,6 +136,21 @@ func (m *jsManager) parse() error { } } + // Parse Bazel dependencies if Bazel files are present + if HasBazelFiles(m.fsys, m.path) { + bazelParser, err := ParseBazelDependencies(m.fsys, m.path) + if err != nil { + return err + } + // Merge Bazel JavaScript dependencies + for _, dep := range bazelParser.GetJSDeps() { + // Only add if not already present (Bazel dependencies are supplementary) + if _, exists := m.deps[dep.Name]; !exists { + m.deps[dep.Name] = dep + } + } + } + return nil } diff --git a/pkg/dep/python.go b/pkg/dep/python.go index a63f709..fb16066 100644 --- a/pkg/dep/python.go +++ b/pkg/dep/python.go @@ -177,6 +177,17 @@ func (m *pythonManager) parse() error { } } + // Parse Bazel dependencies if Bazel files are present + if HasBazelFiles(m.fsys, m.path) { + bazelParser, err := ParseBazelDependencies(m.fsys, m.path) + if err != nil { + return err + } + // Add Bazel Python dependencies to the dependencies list + pythonDeps := bazelParser.GetPythonDeps() + m.dependencies = append(m.dependencies, pythonDeps...) + } + return nil } diff --git a/pkg/rules/analyze_testfs_test.go b/pkg/rules/analyze_testfs_test.go index 155e7ee..86efc5b 100644 --- a/pkg/rules/analyze_testfs_test.go +++ b/pkg/rules/analyze_testfs_test.go @@ -87,6 +87,30 @@ var testFs = fstest.MapFS{ "blazor-app/BlazorApp.csproj": &fstest.MapFile{Data: blazorCsproj}, "blazor-app/packages.lock.json": &fstest.MapFile{Data: blazorLock}, + // Spring Boot with Bazel. + "spring-bazel/BUILD.bazel": &fstest.MapFile{Data: []byte(` +java_library( + name = "spring-app", + deps = [ + "@maven//:org_springframework_boot_spring_boot_starter_web", + "@maven//:org_springframework_boot_spring_boot_starter_data_jpa", + ], +)`)}, + "spring-bazel/pom.xml": &fstest.MapFile{Data: []byte(` + + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + + org.springframework.boot + spring-boot-starter-web + + +`)}, + // Additional directories to increase time taken. "deep/1/2/3/4/5/composer.json": &fstest.MapFile{Data: []byte("{}")}, "deep/a/b/c/d/e/package.json": &fstest.MapFile{Data: []byte("{}")}, @@ -123,6 +147,7 @@ func TestAnalyze_TestFS_ActualRules(t *testing.T) { {Ruleset: "build_tools", Path: "configured-app", Result: "platformsh-app", Rules: []string{"platformsh-app"}, With: map[string]rules.ReportValue{"name": {Value: "app"}}, Groups: []string{"cloud"}}, {Ruleset: "build_tools", Path: "rake", Result: "rake", Rules: []string{"rake"}, Groups: []string{"ruby"}}, + {Ruleset: "build_tools", Path: "spring-bazel", Result: "bazel", Rules: []string{"bazel"}}, // Framework results. {Ruleset: "frameworks", Path: ".", Result: "symfony", Rules: []string{"symfony-framework"}, @@ -138,6 +163,8 @@ func TestAnalyze_TestFS_ActualRules(t *testing.T) { With: map[string]rules.ReportValue{"version": {Value: "1.5.1"}}, Groups: []string{"js"}}, {Ruleset: "frameworks", Path: "python", Result: "django", Rules: []string{"django"}, With: map[string]rules.ReportValue{"version": {Value: "5.2.3"}}, Groups: []string{"django", "python"}}, + {Ruleset: "frameworks", Path: "spring-bazel", Result: "spring-boot", Rules: []string{"spring-boot"}, + With: map[string]rules.ReportValue{"version": {Value: "3.2.1"}}, Groups: []string{"java"}}, // Package manager results. {Ruleset: "package_managers", Path: ".", Result: "composer", Rules: []string{"composer"}, Groups: []string{"php"}, @@ -174,6 +201,8 @@ func TestAnalyze_TestFS_ActualRules(t *testing.T) { Rules: []string{"npm-lockfile"}, Groups: []string{"js"}}, {Ruleset: "package_managers", Path: "python", Result: "uv", Rules: []string{"uv"}, Groups: []string{"python"}}, + {Ruleset: "package_managers", Path: "spring-bazel", Result: "maven", + Rules: []string{"maven"}, Groups: []string{"java"}}, }, reports) }