package goplugin import ( "context" "fmt" "go/ast" "go/parser" "go/token" "go/types" "os" "path/filepath" "strings" "github.com/yourorg/devour/internal/quality" "github.com/yourorg/devour/internal/quality/plugins" "github.com/yourorg/devour/internal/quality/plugins/go/analyzers" "github.com/yourorg/devour/internal/quality/plugins/go/fixers" "golang.org/x/tools/go/packages" ) type GoPlugin struct{} func New() *GoPlugin { return &GoPlugin{} } func (p *GoPlugin) Name() string { return "go" } func (p *GoPlugin) Extensions() []string { return []string{".go"} } func (p *GoPlugin) MarkerFiles() []string { return []string{"go.mod", "go.sum"} } func (p *GoPlugin) DefaultSrcDir() string { return "." } func (p *GoPlugin) CreateDetectors(finder quality.FileFinder) []quality.Detector { return []quality.Detector{ analyzers.NewEnhancedDeadCodeDetector(finder), analyzers.NewUnusedImportDetector(finder), analyzers.NewCycleDetector(finder), analyzers.NewSecurityDetector(finder), analyzers.NewComplexityASTDetector(finder), analyzers.NewLargeFileDetector(finder), analyzers.NewGodStructDetector(finder), analyzers.NewGodFunctionDetector(finder), analyzers.NewDebugLogDetector(finder), analyzers.NewSingleUseDetector(finder), analyzers.NewCouplingDetector(finder), analyzers.NewTestCoverageDetector(finder), analyzers.NewUntestedFuncDetector(finder), analyzers.NewOrphanedFileDetector(finder), analyzers.NewDeprecatedUsageDetector(finder), } } func (p *GoPlugin) CreateFixers() []plugins.Fixer { return []plugins.Fixer{ fixers.NewUnusedImportFixer(), fixers.NewFormattingFixer(), fixers.NewDeadCodeFixer(), fixers.NewComplexityHintFixer(), fixers.NewIoutilFixer(), fixers.NewDocCommentFixer(), } } func (p *GoPlugin) AnalyzeFile(ctx context.Context, path string, config *quality.Config) (*plugins.FileAnalysis, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, path, nil, parser.ParseComments|parser.AllErrors) if err != nil { return nil, fmt.Errorf("parse error: %w", err) } analysis := &plugins.FileAnalysis{ Path: path, Package: node.Name.Name, } loc, err := countLOC(path) if err != nil { return nil, fmt.Errorf("count loc for %s: %w", path, err) } analysis.LOC = loc analysis.Imports = p.extractImports(node, fset) analysis.Functions = p.extractFunctions(node, path, fset) analysis.Types = p.extractTypes(node, path, fset) analysis.Variables = p.extractVariables(node, path, fset) analysis.Comments = p.extractComments(node, path, fset) return analysis, nil } func (p *GoPlugin) BuildDependencyGraph(ctx context.Context, rootPath string) (*plugins.DependencyGraph, error) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedImports | packages.NeedFiles, Dir: rootPath, } pkgs, err := packages.Load(cfg, "./...") if err != nil { return nil, fmt.Errorf("failed to load packages: %w", err) } graph := &plugins.DependencyGraph{ Packages: make(map[string]*plugins.PackageNode), Edges: []plugins.DependencyEdge{}, } for _, pkg := range pkgs { node := &plugins.PackageNode{ Name: pkg.Name, Path: pkg.PkgPath, Files: pkg.GoFiles, IsLocal: true, } for _, imp := range pkg.Imports { node.Imports = append(node.Imports, imp.PkgPath) graph.Edges = append(graph.Edges, plugins.DependencyEdge{ From: pkg.PkgPath, To: imp.PkgPath, Type: plugins.EdgeTypeImport, }) } graph.Packages[pkg.PkgPath] = node } graph.Cycles = p.detectCycles(graph) return graph, nil } func (p *GoPlugin) extractImports(node *ast.File, fset *token.FileSet) []plugins.ImportInfo { var imports []plugins.ImportInfo for _, imp := range node.Imports { info := plugins.ImportInfo{ Path: strings.Trim(imp.Path.Value, `"`), Line: fset.Position(imp.Pos()).Line, } if imp.Name != nil { info.Alias = imp.Name.Name } imports = append(imports, info) } return imports } func (p *GoPlugin) extractFunctions(node *ast.File, path string, fset *token.FileSet) []quality.FunctionInfo { var functions []quality.FunctionInfo for _, decl := range node.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } info := quality.FunctionInfo{ Name: fn.Name.Name, File: path, Line: fset.Position(fn.Pos()).Line, EndLine: fset.Position(fn.End()).Line, } info.LOC = info.EndLine - info.Line + 1 var params []string if fn.Type.Params != nil { for _, field := range fn.Type.Params.List { for _, name := range field.Names { params = append(params, name.Name) } } } info.Params = params if fn.Type.Results != nil { info.ReturnAnnotation = fmt.Sprintf("%v", fn.Type.Results) } functions = append(functions, info) } return functions } func (p *GoPlugin) extractTypes(node *ast.File, path string, fset *token.FileSet) []plugins.TypeInfo { var typeInfos []plugins.TypeInfo for _, decl := range node.Decls { gen, ok := decl.(*ast.GenDecl) if !ok || gen.Tok != token.TYPE { continue } for _, spec := range gen.Specs { typeSpec, ok := spec.(*ast.TypeSpec) if !ok { continue } info := plugins.TypeInfo{ Name: typeSpec.Name.Name, File: path, Line: fset.Position(typeSpec.Pos()).Line, IsExported: ast.IsExported(typeSpec.Name.Name), } switch t := typeSpec.Type.(type) { case *ast.StructType: info.Underlying = "struct" case *ast.InterfaceType: info.Underlying = "interface" default: info.Underlying = fmt.Sprintf("%T", t) } typeInfos = append(typeInfos, info) } } return typeInfos } func (p *GoPlugin) extractVariables(node *ast.File, path string, fset *token.FileSet) []plugins.VariableInfo { var variables []plugins.VariableInfo for _, decl := range node.Decls { gen, ok := decl.(*ast.GenDecl) if !ok || (gen.Tok != token.VAR && gen.Tok != token.CONST) { continue } for _, spec := range gen.Specs { valueSpec, ok := spec.(*ast.ValueSpec) if !ok { continue } for _, name := range valueSpec.Names { info := plugins.VariableInfo{ Name: name.Name, File: path, Line: fset.Position(name.Pos()).Line, IsExported: ast.IsExported(name.Name), } if valueSpec.Type != nil { info.Type = fmt.Sprintf("%v", valueSpec.Type) } variables = append(variables, info) } } } return variables } func (p *GoPlugin) extractComments(node *ast.File, path string, fset *token.FileSet) []plugins.CommentInfo { var comments []plugins.CommentInfo for _, group := range node.Comments { for _, comment := range group.List { info := plugins.CommentInfo{ Text: comment.Text, File: path, Line: fset.Position(comment.Pos()).Line, IsDoc: strings.HasPrefix(comment.Text, "//"), } comments = append(comments, info) } } return comments } func (p *GoPlugin) detectCycles(graph *plugins.DependencyGraph) [][]string { var cycles [][]string visited := make(map[string]bool) recStack := make(map[string]bool) path := []string{} var dfs func(pkg string) bool dfs = func(pkg string) bool { visited[pkg] = true recStack[pkg] = true path = append(path, pkg) node, exists := graph.Packages[pkg] if !exists { return false } for _, imp := range node.Imports { if !visited[imp] { if dfs(imp) { return true } } else if recStack[imp] { cycleStart := -1 for i, p := range path { if p == imp { cycleStart = i break } } if cycleStart >= 0 { cycle := make([]string, len(path)-cycleStart) copy(cycle, path[cycleStart:]) cycles = append(cycles, cycle) } } } path = path[:len(path)-1] recStack[pkg] = false return false } for pkg := range graph.Packages { if !visited[pkg] { dfs(pkg) } } return cycles } func (p *GoPlugin) LoadTypesInfo(ctx context.Context, path string) (*types.Info, *token.FileSet, error) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo, Dir: filepath.Dir(path), } pkgs, err := packages.Load(cfg, filepath.Base(path)) if err != nil { return nil, nil, err } if len(pkgs) == 0 { return nil, nil, fmt.Errorf("no packages found") } return pkgs[0].TypesInfo, pkgs[0].Fset, nil } func countLOC(path string) (int, error) { data, err := os.ReadFile(path) if err != nil { return 0, fmt.Errorf("read file for loc %q: %w", path, err) } return strings.Count(string(data), "\n") + 1, nil } var pluginRegistrationErr error // RegistrationError returns a plugin registration error captured during init, if any. func RegistrationError() error { return pluginRegistrationErr } func init() { if err := plugins.Register(New()); err != nil { pluginRegistrationErr = fmt.Errorf("register go quality plugin: %w", err) _, _ = fmt.Fprintf(os.Stderr, "warning: %v\n", pluginRegistrationErr) } }