assessment/collectors/project_analyzer/file_dependency_analyzer.go (180 lines of code) (raw):

/* Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.*/ package assessment import ( "context" "fmt" "os" "strings" "github.com/GoogleCloudPlatform/spanner-migration-tool/logger" "github.com/dominikbraun/graph" "go.uber.org/zap" "golang.org/x/tools/go/packages" ) // DependencyAnalyzer defines the interface for dependency analysis type DependencyAnalyzer interface { getDependencyGraph(directory string) map[string]map[string]struct{} IsDAO(filePath string, fileContent string) bool GetFrameworkFromFileContent(fileContent string) string GetExecutionOrder(projectDir string) (map[string]map[string]struct{}, [][]string) LogDependencyGraph(dependencyGraphmap map[string]map[string]struct{}, projectDir string) LogExecutionOrder(groupedTasks [][]string) } // BaseAnalyzer provides default implementation for execution order type BaseAnalyzer struct{} // GoDependencyAnalyzer implements DependencyAnalyzer for Go projects type GoDependencyAnalyzer struct { BaseAnalyzer } func validateGoroot() error { goroot := os.Getenv("GOROOT") if len(goroot) == 0 { return fmt.Errorf("please set GOROOT path to GO version 1.22.7 or higher to ensure that app assessment works") } return nil } // packagesLoadLogger: debug logger for packages.Load function func packagesLoadLogger(format string, args ...interface{}) { logger.Log.Debug(fmt.Sprintf(format, args...)) } func (b *BaseAnalyzer) RemoveCycle(fileDependenciesMapWithCycle map[string]map[string]struct{}) map[string]map[string]struct{} { dependencyGraphCycleCheck := graph.New(graph.StringHash, graph.Directed(), graph.PreventCycles()) // Dependency graph: key = file, value = list of files it depends on dependencyGraph := make(map[string]map[string]struct{}) for file, dependencies := range fileDependenciesMapWithCycle { if _, ok := dependencyGraph[file]; !ok { dependencyGraph[file] = make(map[string]struct{}) dependencyGraphCycleCheck.AddVertex(file) } for dependency := range dependencies { if _, ok := dependencyGraph[dependency]; !ok { dependencyGraph[dependency] = make(map[string]struct{}) dependencyGraphCycleCheck.AddVertex(dependency) } if dependencyGraphCycleCheck.AddEdge(file, dependency) != nil { logger.Log.Debug("Cycle detected: ", zap.String("file", file), zap.String("dependency", dependency)) } else if _, exists := dependencyGraph[file][dependency]; !exists { dependencyGraph[file][dependency] = struct{}{} } } } return dependencyGraph } func (g *GoDependencyAnalyzer) getDependencyGraph(directory string) map[string]map[string]struct{} { err := validateGoroot() if err != nil { logger.Log.Warn("Error validating GOROOT: ", zap.Error(err)) } cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo, Dir: (directory), Logf: packagesLoadLogger, } logger.Log.Debug(fmt.Sprintf("loading packages from directory: %s", directory)) pkgs, err := packages.Load(cfg, "./...") if err != nil { logger.Log.Fatal("Error loading packages: ", zap.Error(err)) } // Dependency graph: key = file, value = list of files it depends on dependencyGraphWithCycles := make(map[string]map[string]struct{}) // Iterate through all packages and process their files for _, pkg := range pkgs { if pkg.TypesInfo == nil { continue } // Process symbol usages (functions, variables, structs) for ident, obj := range pkg.TypesInfo.Uses { if obj != nil && obj.Pos().IsValid() { useFile := pkg.Fset.Position(ident.Pos()).Filename // Only process files inside the project directory if strings.HasPrefix(useFile, directory) { // Get the file where the symbol is defined defFile := pkg.Fset.Position(obj.Pos()).Filename // Only add if the file is inside the project directory and avoid redundant edges if strings.HasPrefix(defFile, directory) && useFile != defFile { // Initialize the map for the useFile if not present if _, ok := dependencyGraphWithCycles[useFile]; !ok { dependencyGraphWithCycles[useFile] = make(map[string]struct{}) } if _, ok := dependencyGraphWithCycles[defFile]; !ok { dependencyGraphWithCycles[defFile] = make(map[string]struct{}) } dependencyGraphWithCycles[useFile][defFile] = struct{}{} } } } } } return g.RemoveCycle(dependencyGraphWithCycles) } func (g *GoDependencyAnalyzer) IsDAO(filePath string, fileContent string) bool { filePath = strings.ToLower(filePath) if strings.Contains(filePath, "/dao/") { return true } if strings.Contains(fileContent, "database/sql") || strings.Contains(fileContent, "github.com/go-sql-driver/mysql") { return true } if strings.Contains(fileContent, "*sql.DB") || strings.Contains(fileContent, "*sql.Tx") { return true } if strings.Contains(fileContent, "`gorm:\"") { return true } return false } func (g *GoDependencyAnalyzer) GetFrameworkFromFileContent(fileContent string) string { if strings.Contains(fileContent, "database/sql") || strings.Contains(fileContent, "github.com/go-sql-driver/mysql") { return "database/sql" } if strings.Contains(fileContent, "*sql.DB") || strings.Contains(fileContent, "*sql.Tx") { return "database/sql" } if strings.Contains(fileContent, "`gorm:\"") { return "gorm" } return "" } func (g *GoDependencyAnalyzer) GetExecutionOrder(projectDir string) (map[string]map[string]struct{}, [][]string) { G := g.getDependencyGraph(projectDir) sortedTasks, err := g.TopologicalSort(G) if err != nil { logger.Log.Debug("Graph still has cycles after relaxation. Sorting not possible: ", zap.Error(err)) return nil, nil } logger.Log.Debug("Execution order determined successfully.") return G, sortedTasks } // AnalyzerFactory creates DependencyAnalyzer instances func AnalyzerFactory(language string, ctx context.Context) DependencyAnalyzer { switch language { case "go": return &GoDependencyAnalyzer{} case "java": return &JavaDependencyAnalyzer{ctx: ctx} default: panic("Unsupported language") } } func (b *BaseAnalyzer) TopologicalSort(G map[string]map[string]struct{}) ([][]string, error) { inDegree := make(map[string]int) for node := range G { inDegree[node] = 0 } var maxDegree int for node := range G { for neighbor := range G[node] { inDegree[neighbor]++ if inDegree[neighbor] > maxDegree { maxDegree = inDegree[neighbor] } } } taskLevels := make([][]string, maxDegree+1) for node, degree := range inDegree { degree = maxDegree - degree taskLevels[degree] = append(taskLevels[degree], node) } return taskLevels, nil } func (b *BaseAnalyzer) LogDependencyGraph(dependencyGraph map[string]map[string]struct{}, projectDir string) { logger.Log.Debug("Dependency Graph:") for file, dependencies := range dependencyGraph { logger.Log.Debug("depends on: ", zap.String("filepath: ", strings.TrimPrefix(file, projectDir))) for dep := range dependencies { logger.Log.Debug("Dependency: ", zap.String("filepath", strings.TrimPrefix(dep, projectDir))) } } } func (b *BaseAnalyzer) LogExecutionOrder(groupedTasks [][]string) { logger.Log.Debug("Execution Order Groups:") for i, group := range groupedTasks { logger.Log.Debug("Level: ", zap.Int("level", i), zap.String("group", strings.Join(group, ", "))) } }