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)
}