Skip to content

Commit

Permalink
feat: Auto Fix (#58)
Browse files Browse the repository at this point in the history
* auto fixer

* add test

* confidence level
  • Loading branch information
notJoon authored Aug 31, 2024
1 parent 8768047 commit 1b57a65
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 4 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,7 @@ install-linter:
lint:
golangci-lint run

fmt:
go fmt ./...

.PHONY: all build test clean run deps build-linux build-windows build-mac build-all install-linter lint
52 changes: 50 additions & 2 deletions cmd/tlin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import (
"github.com/gnoswap-labs/tlin/formatter"
"github.com/gnoswap-labs/tlin/internal"
"github.com/gnoswap-labs/tlin/internal/analysis/cfg"
"github.com/gnoswap-labs/tlin/internal/fixer"
"github.com/gnoswap-labs/tlin/internal/lints"
tt "github.com/gnoswap-labs/tlin/internal/types"
"go.uber.org/zap"
)

const defaultTimeout = 5 * time.Minute
const (
defaultTimeout = 5 * time.Minute
defaultConfidenceThreshold = 0.75
)

type Config struct {
Timeout time.Duration
Expand All @@ -31,6 +35,9 @@ type Config struct {
Paths []string
CFGAnalysis bool
FuncName string
AutoFix bool
DryRun bool
ConfidenceThreshold float64
}

type LintEngine interface {
Expand Down Expand Up @@ -72,6 +79,18 @@ func main() {
runNormalLintProcess(ctx, logger, engine, config.Paths)
})
}

if config.AutoFix {
if config.ConfidenceThreshold < 0 || config.ConfidenceThreshold > 1 {
fmt.Println("error: confidence threshold must be between 0 and 1")
os.Exit(1)
}
runAutoFix(ctx, logger, engine, config.Paths, config.DryRun, config.ConfidenceThreshold)
} else {
runWithTimeout(ctx, func() {
runNormalLintProcess(ctx, logger, engine, config.Paths)
})
}
}

func parseFlags() Config {
Expand All @@ -83,6 +102,10 @@ func parseFlags() Config {
flag.BoolVar(&config.CFGAnalysis, "cfg", false, "Run control flow graph analysis")
flag.StringVar(&config.FuncName, "func", "", "Function name for CFG analysis")

flag.BoolVar(&config.AutoFix, "fix", false, "Automatically fix issues")
flag.BoolVar(&config.DryRun, "dry-run", false, "Show what would be fixed without actually fixing")
flag.Float64Var(&config.ConfidenceThreshold, "confidence", defaultConfidenceThreshold, "Minimum confidence threshold for fixing issues")

flag.Parse()

config.Paths = flag.Args()
Expand Down Expand Up @@ -124,6 +147,31 @@ func runNormalLintProcess(ctx context.Context, logger *zap.Logger, engine LintEn
}
}

func runAutoFix(ctx context.Context, logger *zap.Logger, engine LintEngine, paths []string, dryRun bool, confidenceThreshold float64) {
fix := fixer.New(dryRun, confidenceThreshold)

for _, path := range paths {
issues, err := processPath(ctx, logger, engine, path, processFile)
if err != nil {
logger.Error(
"error processing path",
zap.String("path", path),
zap.Error(err),
)
continue
}

err = fix.Fix(path, issues)
if err != nil {
logger.Error(
"error fixing issues",
zap.String("path", path),
zap.Error(err),
)
}
}
}

func runCyclomaticComplexityAnalysis(ctx context.Context, logger *zap.Logger, paths []string, threshold int) {
issues, err := processFiles(ctx, logger, nil, paths, func(_ LintEngine, path string) ([]tt.Issue, error) {
return processCyclomaticComplexity(path, threshold)
Expand Down Expand Up @@ -183,7 +231,7 @@ func processFiles(ctx context.Context, logger *zap.Logger, engine LintEngine, pa
return allIssues, nil
}

func processPath(ctx context.Context, logger *zap.Logger, engine LintEngine, path string, processor func(LintEngine, string) ([]tt.Issue, error)) ([]tt.Issue, error) {
func processPath(_ context.Context, logger *zap.Logger, engine LintEngine, path string, processor func(LintEngine, string) ([]tt.Issue, error)) ([]tt.Issue, error) {
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("error accessing %s: %w", path, err)
Expand Down
121 changes: 121 additions & 0 deletions internal/fixer/fixer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package fixer

import (
"bufio"
"bytes"
"fmt"
"go/format"
"go/parser"
"go/token"
"os"
"sort"
"strings"

tt "github.com/gnoswap-labs/tlin/internal/types"
)

type Fixer struct {
DryRun bool
autoConfirm bool // testing purposes
MinConfidence float64 // threshold for fixing issues
}

func New(dryRun bool, threshold float64) *Fixer {
return &Fixer{
DryRun: dryRun,
autoConfirm: false,
MinConfidence: threshold,
}
}

func (f *Fixer) Fix(filename string, issues []tt.Issue) error {
content, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}

sort.Slice(issues, func(i, j int) bool {
return issues[i].End.Offset > issues[j].End.Offset
})

lines := strings.Split(string(content), "\n")

for _, issue := range issues {
if issue.Confidence < f.MinConfidence {
continue
}

if f.DryRun {
fmt.Printf("Would fix issue in %s at line %d: %s\n", filename, issue.Start.Line, issue.Message)
fmt.Printf("Suggestion:\n%s\n", issue.Suggestion)
continue
}

if !f.confirmFix(issue) && !f.autoConfirm {
continue
}

startLine := issue.Start.Line - 1
endLine := issue.End.Line - 1

indent := f.extractIndent(lines[startLine])
suggestion := f.applyIndent(issue.Suggestion, indent)

lines = append(lines[:startLine], append([]string{suggestion}, lines[endLine+1:]...)...)
}

if !f.DryRun {
newContent := strings.Join(lines, "\n")

fset := token.NewFileSet()
astFile, err := parser.ParseFile(fset, filename, newContent, parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse file: %w", err)
}

var buf bytes.Buffer
if err := format.Node(&buf, fset, astFile); err != nil {
return fmt.Errorf("failed to format file: %w", err)
}

err = os.WriteFile(filename, buf.Bytes(), 0644)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}

fmt.Printf("Fixed issues in %s\n", filename)
}

return nil
}

func (f *Fixer) confirmFix(issue tt.Issue) bool {
if f.autoConfirm {
return true
}

fmt.Printf(
"Fix issue in %s at line %d? (confidence: %.2f)\n",
issue.Filename, issue.Start.Line, issue.Confidence,
)
fmt.Printf("Message: %s\n", issue.Message)
fmt.Printf("Suggestion:\n%s\n", issue.Suggestion)
fmt.Print("Apply this fix? (y/N): ")

reader := bufio.NewReader(os.Stdin)
resp, _ := reader.ReadString('\n')
return strings.ToLower(strings.TrimSpace(resp)) == "y"
}

func (c *Fixer) extractIndent(line string) string {
return line[:len(line)-len(strings.TrimLeft(line, " \t"))]
}

// TODO: better indentation handling
func (f *Fixer) applyIndent(code, indent string) string {
lines := strings.Split(code, "\n")
for i, line := range lines {
lines[i] = indent + line
}
return strings.Join(lines, "\n")
}
Loading

0 comments on commit 1b57a65

Please sign in to comment.