package main import ( "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "sort" "strings" ) // Analyzer parses Go source code and extracts structural information. type Analyzer struct { sourceDir string fset *token.FileSet } // NewAnalyzer creates a new Analyzer for the given source directory. func NewAnalyzer(sourceDir string) *Analyzer { return &Analyzer{ sourceDir: sourceDir, fset: token.NewFileSet(), } } // Analyze runs the full analysis pipeline. func (a *Analyzer) Analyze() (*AnalysisResult, error) { serverDir := filepath.Join(a.sourceDir, "server") // 1. Discover all Go files grouped by directory fileGroups, err := a.discoverFiles(serverDir) if err != nil { return nil, fmt.Errorf("discovering files: %w", err) } // 2. Parse each group into modules result := &AnalysisResult{} allImports := make(map[string]*ImportInfo) for dir, files := range fileGroups { module, imports, err := a.parseModule(dir, files) if err != nil { return nil, fmt.Errorf("parsing module %s: %w", dir, err) } result.Modules = append(result.Modules, *module) for _, imp := range imports { if existing, ok := allImports[imp.ImportPath]; ok { existing.UsedInFiles = append(existing.UsedInFiles, imp.UsedInFiles...) } else { allImports[imp.ImportPath] = &imp } } } // 3. Build module-level dependencies from import analysis result.Dependencies = a.buildDependencies(result.Modules) // 4. Collect imports for _, imp := range allImports { result.Imports = append(result.Imports, *imp) } sort.Slice(result.Imports, func(i, j int) bool { return result.Imports[i].ImportPath < result.Imports[j].ImportPath }) // Sort modules by name sort.Slice(result.Modules, func(i, j int) bool { return result.Modules[i].Name < result.Modules[j].Name }) return result, nil } // discoverFiles walks the source tree and groups .go files by directory. func (a *Analyzer) discoverFiles(root string) (map[string][]string, error) { groups := make(map[string][]string) err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { if info.Name() == "configs" || info.Name() == "testdata" { return filepath.SkipDir } return nil } if !strings.HasSuffix(info.Name(), ".go") { return nil } dir := filepath.Dir(path) groups[dir] = append(groups[dir], path) return nil }) return groups, err } // parseModule parses all Go files in a directory into a Module. func (a *Analyzer) parseModule(dir string, files []string) (*Module, []ImportInfo, error) { moduleName := a.moduleNameFromDir(dir) module := &Module{ Name: moduleName, GoPackage: moduleName, GoFile: dir, } var sourceFiles []string var testFiles []string for _, f := range files { if strings.HasSuffix(f, "_test.go") { testFiles = append(testFiles, f) } else { sourceFiles = append(sourceFiles, f) } } var allImports []ImportInfo totalLines := 0 for _, f := range sourceFiles { features, imports, lines, err := a.parseSourceFile(f) if err != nil { fmt.Fprintf(os.Stderr, "Warning: skipping %s: %v\n", f, err) continue } module.Features = append(module.Features, features...) allImports = append(allImports, imports...) totalLines += lines } for _, f := range testFiles { tests, _, lines, err := a.parseTestFile(f) if err != nil { fmt.Fprintf(os.Stderr, "Warning: skipping test %s: %v\n", f, err) continue } module.Tests = append(module.Tests, tests...) totalLines += lines } module.GoLineCount = totalLines return module, allImports, nil } // parseSourceFile extracts functions, methods, and imports from a Go source file. func (a *Analyzer) parseSourceFile(filePath string) ([]Feature, []ImportInfo, int, error) { src, err := os.ReadFile(filePath) if err != nil { return nil, nil, 0, err } file, err := parser.ParseFile(a.fset, filePath, src, parser.ParseComments) if err != nil { return nil, nil, 0, err } lines := strings.Count(string(src), "\n") + 1 relPath := a.relPath(filePath) var features []Feature var imports []ImportInfo for _, imp := range file.Imports { path := strings.Trim(imp.Path.Value, "\"") imports = append(imports, ImportInfo{ ImportPath: path, IsStdlib: isStdlib(path), UsedInFiles: []string{relPath}, }) } for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } feature := Feature{ Name: fn.Name.Name, GoFile: relPath, GoMethod: fn.Name.Name, GoLineNumber: a.fset.Position(fn.Pos()).Line, } startLine := a.fset.Position(fn.Pos()).Line endLine := a.fset.Position(fn.End()).Line feature.GoLineCount = endLine - startLine + 1 if fn.Recv != nil && len(fn.Recv.List) > 0 { feature.GoClass = a.receiverTypeName(fn.Recv.List[0].Type) feature.Name = feature.GoClass + "." + fn.Name.Name } if fn.Doc != nil { feature.Description = strings.TrimSpace(fn.Doc.Text()) } features = append(features, feature) } return features, imports, lines, nil } // parseTestFile extracts test functions from a Go test file. func (a *Analyzer) parseTestFile(filePath string) ([]TestFunc, []ImportInfo, int, error) { src, err := os.ReadFile(filePath) if err != nil { return nil, nil, 0, err } file, err := parser.ParseFile(a.fset, filePath, src, parser.ParseComments) if err != nil { return nil, nil, 0, err } lines := strings.Count(string(src), "\n") + 1 relPath := a.relPath(filePath) var tests []TestFunc var imports []ImportInfo for _, imp := range file.Imports { path := strings.Trim(imp.Path.Value, "\"") imports = append(imports, ImportInfo{ ImportPath: path, IsStdlib: isStdlib(path), UsedInFiles: []string{relPath}, }) } for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } name := fn.Name.Name if !strings.HasPrefix(name, "Test") && !strings.HasPrefix(name, "Benchmark") { continue } startLine := a.fset.Position(fn.Pos()).Line endLine := a.fset.Position(fn.End()).Line test := TestFunc{ Name: name, GoFile: relPath, GoMethod: name, GoLineNumber: startLine, GoLineCount: endLine - startLine + 1, } if fn.Doc != nil { test.Description = strings.TrimSpace(fn.Doc.Text()) } test.FeatureName = a.inferFeatureName(name) tests = append(tests, test) } return tests, imports, lines, nil } // buildDependencies creates module-level dependencies based on cross-package imports. func (a *Analyzer) buildDependencies(modules []Module) []Dependency { pkgToModule := make(map[string]string) for _, m := range modules { pkgToModule[m.GoPackage] = m.Name } var deps []Dependency for _, m := range modules { if m.Name != "server" && m.GoPackage != "server" { deps = append(deps, Dependency{ SourceModule: "server", TargetModule: m.Name, DependencyKind: "calls", }) } } return deps } // moduleNameFromDir converts a directory path to a module name. func (a *Analyzer) moduleNameFromDir(dir string) string { base := filepath.Base(dir) if base == "server" { return "server" } return base } // relPath returns a path relative to the analyzer's source directory. func (a *Analyzer) relPath(absPath string) string { rel, err := filepath.Rel(a.sourceDir, absPath) if err != nil { return absPath } return rel } // receiverTypeName extracts the type name from a method receiver. func (a *Analyzer) receiverTypeName(expr ast.Expr) string { switch t := expr.(type) { case *ast.StarExpr: return a.receiverTypeName(t.X) case *ast.Ident: return t.Name default: return "" } } // inferFeatureName attempts to derive a feature name from a test name. func (a *Analyzer) inferFeatureName(testName string) string { name := testName for _, prefix := range []string{"Test", "Benchmark"} { if strings.HasPrefix(name, prefix) { name = strings.TrimPrefix(name, prefix) break } } if name == "" { return "" } if idx := strings.Index(name, "_"); idx > 0 { name = name[:idx] + "." + name[idx+1:] } return name } // isStdlib checks if an import path is a Go standard library package. func isStdlib(importPath string) bool { firstSlash := strings.Index(importPath, "/") var first string if firstSlash < 0 { first = importPath } else { first = importPath[:firstSlash] } return !strings.Contains(first, ".") }