mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
first commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user