package fixers import ( "context" "fmt" "go/ast" "go/format" "go/parser" "go/token" "os" "strings" "github.com/yourorg/devour/internal/quality" "github.com/yourorg/devour/internal/quality/plugins" ) type DeadCodeFixer struct{} func NewDeadCodeFixer() *DeadCodeFixer { return &DeadCodeFixer{} } func (f *DeadCodeFixer) Name() string { return "dead_code" } func (f *DeadCodeFixer) Description() string { return "Comments out or removes unused exported functions/types" } func (f *DeadCodeFixer) CanFix(finding quality.Finding) bool { return finding.Type == "dead_code" && finding.Severity == quality.SeverityT1 } func (f *DeadCodeFixer) Fix(ctx context.Context, finding quality.Finding, dryRun bool) (*plugins.FixResult, error) { name := finding.Metadata["name"] if name == "" { return nil, fmt.Errorf("no function/type name in metadata") } fset := token.NewFileSet() node, err := parser.ParseFile(fset, finding.File, nil, parser.ParseComments) if err != nil { return nil, fmt.Errorf("parse error: %w", err) } if dryRun { return &plugins.FixResult{ Success: true, Message: fmt.Sprintf("Would comment out unused '%s' in %s", name, finding.File), }, nil } var targetDecl ast.Decl for _, decl := range node.Decls { switch d := decl.(type) { case *ast.FuncDecl: if d.Name.Name == name { targetDecl = d } case *ast.GenDecl: for _, spec := range d.Specs { if ts, ok := spec.(*ast.TypeSpec); ok && ts.Name.Name == name { targetDecl = d } } } if targetDecl != nil { comment := &ast.CommentGroup{ List: []*ast.Comment{ {Text: "// DEPRECATED: This code is unused and should be removed"}, }, } if targetDecl.(*ast.FuncDecl) != nil { targetDecl.(*ast.FuncDecl).Doc = comment } else if targetDecl.(*ast.GenDecl) != nil { targetDecl.(*ast.GenDecl).Doc = comment } break } } if targetDecl == nil { return &plugins.FixResult{ Success: false, Message: fmt.Sprintf("Could not find '%s' in file", name), }, nil } var output strings.Builder if err := format.Node(&output, fset, node); err != nil { return nil, fmt.Errorf("format error: %w", err) } if err := os.WriteFile(finding.File, []byte(output.String()), 0644); err != nil { return nil, fmt.Errorf("write error: %w", err) } return &plugins.FixResult{ Success: true, Message: fmt.Sprintf("Marked '%s' as deprecated in %s", name, finding.File), }, nil } type ComplexityHintFixer struct{} func NewComplexityHintFixer() *ComplexityHintFixer { return &ComplexityHintFixer{} } func (f *ComplexityHintFixer) Name() string { return "complexity_hint" } func (f *ComplexityHintFixer) Description() string { return "Adds complexity warning comments to complex functions" } func (f *ComplexityHintFixer) CanFix(finding quality.Finding) bool { return finding.Type == "complexity" || finding.Type == "complexity_ast" } func (f *ComplexityHintFixer) Fix(ctx context.Context, finding quality.Finding, dryRun bool) (*plugins.FixResult, error) { funcName := finding.Metadata["function"] if funcName == "" { return nil, fmt.Errorf("no function name in metadata") } fset := token.NewFileSet() node, err := parser.ParseFile(fset, finding.File, nil, parser.ParseComments) if err != nil { return nil, fmt.Errorf("parse error: %w", err) } if dryRun { return &plugins.FixResult{ Success: true, Message: fmt.Sprintf("Would add complexity warning to '%s' in %s", funcName, finding.File), }, nil } for _, decl := range node.Decls { if fn, ok := decl.(*ast.FuncDecl); ok && fn.Name.Name == funcName { complexity := finding.Metadata["complexity"] warning := fmt.Sprintf("// FIXME: High complexity (%s). Consider breaking into smaller functions.", complexity) comment := &ast.CommentGroup{ List: []*ast.Comment{ {Text: warning}, }, } fn.Doc = comment break } } var output strings.Builder if err := format.Node(&output, fset, node); err != nil { return nil, fmt.Errorf("format error: %w", err) } if err := os.WriteFile(finding.File, []byte(output.String()), 0644); err != nil { return nil, fmt.Errorf("write error: %w", err) } return &plugins.FixResult{ Success: true, Message: fmt.Sprintf("Added complexity warning to '%s' in %s", funcName, finding.File), }, nil } type IoutilFixer struct{} func NewIoutilFixer() *IoutilFixer { return &IoutilFixer{} } func (f *IoutilFixer) Name() string { return "ioutil" } func (f *IoutilFixer) Description() string { return "Replaces deprecated io/ioutil with modern equivalents" } func (f *IoutilFixer) CanFix(finding quality.Finding) bool { return finding.Type == "deprecated" && strings.Contains(finding.Title, "io/ioutil") } func (f *IoutilFixer) Fix(ctx context.Context, finding quality.Finding, dryRun bool) (*plugins.FixResult, error) { data, err := os.ReadFile(finding.File) if err != nil { return nil, fmt.Errorf("read error: %w", err) } content := string(data) replacements := map[string]string{ `"io/ioutil"`: "", `ioutil.ReadFile`: `os.ReadFile`, `ioutil.WriteFile`: `os.WriteFile`, `ioutil.ReadDir`: `os.ReadDir`, `ioutil.TempDir`: `os.MkdirTemp`, `ioutil.TempFile`: `os.CreateTemp`, `ioutil.NopCloser`: `io.NopCloser`, `ioutil.ReadAll`: `io.ReadAll`, `ioutil.Discard`: `io.Discard`, } if dryRun { return &plugins.FixResult{ Success: true, Message: fmt.Sprintf("Would replace io/ioutil usage in %s", finding.File), }, nil } for old, new := range replacements { content = strings.ReplaceAll(content, old, new) } if strings.Contains(content, "os.ReadFile") || strings.Contains(content, "os.WriteFile") || strings.Contains(content, "os.ReadDir") || strings.Contains(content, "os.MkdirTemp") || strings.Contains(content, "os.CreateTemp") { if !strings.Contains(content, `"os"`) { content = strings.Replace(content, "package ", "import \"os\"\n\npackage ", 1) } } if strings.Contains(content, "io.NopCloser") || strings.Contains(content, "io.ReadAll") || strings.Contains(content, "io.Discard") { if !strings.Contains(content, `"io"`) { content = strings.Replace(content, "package ", "import \"io\"\n\npackage ", 1) } } if err := os.WriteFile(finding.File, []byte(content), 0644); err != nil { return nil, fmt.Errorf("write error: %w", err) } return &plugins.FixResult{ Success: true, Message: fmt.Sprintf("Replaced io/ioutil in %s", finding.File), }, nil } type DocCommentFixer struct{} func NewDocCommentFixer() *DocCommentFixer { return &DocCommentFixer{} } func (f *DocCommentFixer) Name() string { return "doc_comment" } func (f *DocCommentFixer) Description() string { return "Adds TODO comments for missing documentation on exported items" } func (f *DocCommentFixer) CanFix(finding quality.Finding) bool { return finding.Type == "naming" || finding.Type == "god_struct" || finding.Type == "god_function" } func (f *DocCommentFixer) Fix(ctx context.Context, finding quality.Finding, dryRun bool) (*plugins.FixResult, error) { return &plugins.FixResult{ Success: false, Message: "Documentation fixer requires manual intervention", Warnings: []string{ fmt.Sprintf("Add documentation for: %s", finding.Title), fmt.Sprintf("Location: %s:%d", finding.File, finding.Line), }, }, nil }