first commit

This commit is contained in:
Tomas Dvorak
2026-02-22 10:42:17 +01:00
commit 55885a0e8f
239 changed files with 103690 additions and 0 deletions
@@ -0,0 +1,276 @@
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
}
@@ -0,0 +1,124 @@
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 UnusedImportFixer struct{}
func NewUnusedImportFixer() *UnusedImportFixer {
return &UnusedImportFixer{}
}
func (f *UnusedImportFixer) Name() string {
return "unused_import"
}
func (f *UnusedImportFixer) Description() string {
return "Removes unused import statements"
}
func (f *UnusedImportFixer) CanFix(finding quality.Finding) bool {
return finding.Type == "unused_import"
}
func (f *UnusedImportFixer) Fix(ctx context.Context, finding quality.Finding, dryRun bool) (*plugins.FixResult, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, finding.File, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("parse error: %w", err)
}
importToRemove := finding.Metadata["import_path"]
if importToRemove == "" {
return nil, fmt.Errorf("no import_path in finding metadata")
}
var newImports []*ast.ImportSpec
for _, imp := range node.Imports {
path := strings.Trim(imp.Path.Value, `"`)
if path != importToRemove {
newImports = append(newImports, imp)
}
}
node.Imports = newImports
if dryRun {
return &plugins.FixResult{
Success: true,
Message: fmt.Sprintf("Would remove import '%s' from %s", importToRemove, finding.File),
}, 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("Removed unused import '%s' from %s", importToRemove, finding.File),
}, nil
}
type FormattingFixer struct{}
func NewFormattingFixer() *FormattingFixer {
return &FormattingFixer{}
}
func (f *FormattingFixer) Name() string {
return "format"
}
func (f *FormattingFixer) Description() string {
return "Formats Go source files using gofmt style"
}
func (f *FormattingFixer) CanFix(finding quality.Finding) bool {
return finding.Type == "formatting" || finding.Type == "style"
}
func (f *FormattingFixer) Fix(ctx context.Context, finding quality.Finding, dryRun bool) (*plugins.FixResult, error) {
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 format %s", finding.File),
}, 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("Formatted %s", finding.File),
}, nil
}