Implement suggested replacements for problems.
This adds the infrastructure for plumbing suggestions through,
and implements a suggested fix for range-loop.
diff --git a/lint.go b/lint.go
index 8182c0b..1a8c8d4 100644
--- a/lint.go
+++ b/lint.go
@@ -39,6 +39,10 @@
Confidence float64 // a value in (0,1] estimating the confidence in this problem's correctness
LineText string // the source line
Category string // a short name for the general category of the problem
+
+ // If the problem has a suggested fix (the minority case),
+ // ReplacementLine is a full replacement for the relevant line of the source file.
+ ReplacementLine string
}
func (p *Problem) String() string {
@@ -190,15 +194,16 @@
// The variadic arguments may start with link and category types,
// and must end with a format string and any arguments.
-func (f *file) errorf(n ast.Node, confidence float64, args ...interface{}) {
+// It returns the new Problem.
+func (f *file) errorf(n ast.Node, confidence float64, args ...interface{}) *Problem {
pos := f.fset.Position(n.Pos())
if pos.Filename == "" {
pos.Filename = f.filename
}
- f.pkg.errorfAt(pos, confidence, args...)
+ return f.pkg.errorfAt(pos, confidence, args...)
}
-func (p *pkg) errorfAt(pos token.Position, confidence float64, args ...interface{}) {
+func (p *pkg) errorfAt(pos token.Position, confidence float64, args ...interface{}) *Problem {
problem := Problem{
Position: pos,
Confidence: confidence,
@@ -226,6 +231,7 @@
problem.Text = fmt.Sprintf(args[0].(string), args[1:]...)
p.problems = append(p.problems, problem)
+ return &p.problems[len(p.problems)-1]
}
var gcImporter = gcimporter.Import
@@ -1033,7 +1039,17 @@
return true
}
- f.errorf(rs.Value, 1, category("range-loop"), "should omit 2nd value from range; this loop is equivalent to `for %s %s range ...`", f.render(rs.Key), rs.Tok)
+ p := f.errorf(rs.Value, 1, category("range-loop"), "should omit 2nd value from range; this loop is equivalent to `for %s %s range ...`", f.render(rs.Key), rs.Tok)
+
+ newRS := *rs // shallow copy
+ newRS.Value = nil
+ line := f.render(&newRS)
+ if i := strings.Index(line, "\n"); i >= 0 {
+ line = line[:i]
+ }
+ line = f.indentOf(rs) + line
+ p.ReplacementLine = line
+
return true
})
}
@@ -1477,6 +1493,18 @@
return "", false
}
+func (f *file) indentOf(node ast.Node) string {
+ line := srcLine(f.src, f.fset.Position(node.Pos()))
+ for i, r := range line {
+ switch r {
+ case ' ', '\t':
+ default:
+ return line[:i]
+ }
+ }
+ return line // unusual or empty line
+}
+
// srcLine returns the complete line at p, including the terminating newline.
func srcLine(src []byte, p token.Position) string {
// Run to end of line in both directions if not at line start/end.
diff --git a/lint_test.go b/lint_test.go
index 90da9b7..91a981e 100644
--- a/lint_test.go
+++ b/lint_test.go
@@ -70,6 +70,11 @@
continue
}
if in.Match.MatchString(p.Text) {
+ // check replacement if we are expecting one
+ if in.Replacement != "" && p.ReplacementLine != in.Replacement {
+ t.Errorf("Lint failed at %s:%d; got replacement %q, want %q", fi.Name(), in.Line, p.ReplacementLine, in.Replacement)
+ }
+
// remove this problem from ps
copy(ps[i:], ps[i+1:])
ps = ps[:len(ps)-1]
@@ -90,8 +95,9 @@
}
type instruction struct {
- Line int // the line number this applies to
- Match *regexp.Regexp // what pattern to match
+ Line int // the line number this applies to
+ Match *regexp.Regexp // what pattern to match
+ Replacement string // what the suggested replacement line should be
}
// parseInstructions parses instructions from the comments in a Go source file.
@@ -130,9 +136,14 @@
t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err)
}
}
+ var repl string
+ if r, ok := extractReplacement(line); ok {
+ repl = r
+ }
ins = append(ins, instruction{
- Line: matchLine,
- Match: rx,
+ Line: matchLine,
+ Match: rx,
+ Replacement: repl,
})
}
}
@@ -153,6 +164,18 @@
return rx, nil
}
+func extractReplacement(line string) (string, bool) {
+ // Look for this: / -> `
+ // (the end of a match and start of a backtick string),
+ // and then the closing backtick.
+ const start = "/ -> `"
+ a, b := strings.Index(line, start), strings.LastIndex(line, "`")
+ if a < 0 || a > b {
+ return "", false
+ }
+ return line[a+len(start) : b], true
+}
+
func render(fset *token.FileSet, x interface{}) string {
var buf bytes.Buffer
if err := printer.Fprint(&buf, fset, x); err != nil {
diff --git a/testdata/range.go b/testdata/range.go
index a01c2bf..af36a82 100644
--- a/testdata/range.go
+++ b/testdata/range.go
@@ -7,7 +7,7 @@
var m map[string]int
// with :=
- for x, _ := range m { // MATCH /should omit 2nd value.*range.*equivalent.*for x := range/
+ for x, _ := range m { // MATCH /should omit 2nd value.*range.*equivalent.*for x := range/ -> ` for x := range m {`
_ = x
}
// with =