Skip to content

Commit 314ead1

Browse files
authored
feat: Add auto-fix with import management (#179)
## Description - enhance `format-with-verb` rule with context-specific suggestions and auto-fix support - Add automatic import management (add missing, remove unused) after applying fixes - Add `RequiredImports` field to `Issue` type to track imports needed for fixes ### Import Processing - Add `ProcessImports` function - Automatically add required imports (e.g. `errors` package when replacing `ufmt.Errorf`) - Support `*.gno` files by treating them as `*.go` for import solution
1 parent 9a1e8a1 commit 314ead1

7 files changed

Lines changed: 519 additions & 47 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@
2020
# Go workspace file
2121
go.work
2222
go.work.sum
23+
24+
.gocache

internal/fixer/fixer.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ package fixer
33
import (
44
"bytes"
55
"fmt"
6-
"go/format"
7-
"go/parser"
8-
"go/token"
96
"os"
107
"sort"
118
"strings"
@@ -114,18 +111,15 @@ func (f *Fixer) writeFixedContent(filename string, lines []string) error {
114111
}
115112
}
116113

117-
fset := token.NewFileSet()
118-
astFile, err := parser.ParseFile(fset, filename, f.buffer.Bytes(), parser.ParseComments)
114+
// Process imports: adds missing imports and removes unused ones
115+
content, err := ProcessImports(filename, f.buffer.Bytes())
119116
if err != nil {
120-
return fmt.Errorf("failed to parse file: %w", err)
117+
// If import processing fails, fall back to basic formatting
118+
fmt.Printf("Warning: failed to process imports: %v\n", err)
119+
content = f.buffer.Bytes()
121120
}
122121

123-
f.buffer.Reset()
124-
if err := format.Node(&f.buffer, fset, astFile); err != nil {
125-
return fmt.Errorf("failed to format file: %w", err)
126-
}
127-
128-
if err := os.WriteFile(filename, f.buffer.Bytes(), defaultFilePermissions); err != nil {
122+
if err := os.WriteFile(filename, content, defaultFilePermissions); err != nil {
129123
return fmt.Errorf("failed to write file: %w", err)
130124
}
131125

internal/fixer/fixer_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,136 @@ func main() {
176176
"oldOwner", oldOwner,
177177
)
178178
}
179+
`,
180+
},
181+
{
182+
name: "Fix - format-without-verb Errorf with return",
183+
input: `package main
184+
185+
func example() error {
186+
return ufmt.Errorf("handler error")
187+
}
188+
`,
189+
issues: []tt.Issue{
190+
{
191+
Rule: "format-without-verb",
192+
Message: "format string has no verbs; use errors.New() instead",
193+
Start: token.Position{Line: 4, Column: 9},
194+
End: token.Position{Line: 4, Column: 38},
195+
Suggestion: `return errors.New("handler error")`,
196+
},
197+
},
198+
expected: `package main
199+
200+
import "errors"
201+
202+
func example() error {
203+
return errors.New("handler error")
204+
}
205+
`,
206+
},
207+
{
208+
name: "Fix - format-without-verb Sprintf to literal",
209+
input: `package main
210+
211+
func main() {
212+
msg := ufmt.Sprintf("hello world")
213+
}
214+
`,
215+
issues: []tt.Issue{
216+
{
217+
Rule: "format-without-verb",
218+
Message: "format string has no verbs; use a string literal directly",
219+
Start: token.Position{Line: 4, Column: 9},
220+
End: token.Position{Line: 4, Column: 37},
221+
Suggestion: `msg := "hello world"`,
222+
},
223+
},
224+
expected: `package main
225+
226+
func main() {
227+
msg := "hello world"
228+
}
229+
`,
230+
},
231+
{
232+
name: "Fix - format-without-verb Printf to print",
233+
input: `package main
234+
235+
func main() {
236+
ufmt.Printf("status ok")
237+
}
238+
`,
239+
issues: []tt.Issue{
240+
{
241+
Rule: "format-without-verb",
242+
Message: "format string has no verbs; use print() instead",
243+
Start: token.Position{Line: 4, Column: 2},
244+
End: token.Position{Line: 4, Column: 26},
245+
Suggestion: `print("status ok")`,
246+
},
247+
},
248+
expected: `package main
249+
250+
func main() {
251+
print("status ok")
252+
}
253+
`,
254+
},
255+
{
256+
name: "Fix - format-without-verb Errorf adds errors import",
257+
input: `package main
258+
259+
func example() error {
260+
return ufmt.Errorf("handler error")
261+
}
262+
`,
263+
issues: []tt.Issue{
264+
{
265+
Rule: "format-without-verb",
266+
Message: "format string has no verbs; use errors.New() instead",
267+
Start: token.Position{Line: 4, Column: 9},
268+
End: token.Position{Line: 4, Column: 38},
269+
Suggestion: `return errors.New("handler error")`,
270+
RequiredImports: []string{"errors"},
271+
},
272+
},
273+
expected: `package main
274+
275+
import "errors"
276+
277+
func example() error {
278+
return errors.New("handler error")
279+
}
280+
`,
281+
},
282+
{
283+
name: "Fix - does not duplicate existing import",
284+
input: `package main
285+
286+
import "errors"
287+
288+
func example() error {
289+
return ufmt.Errorf("handler error")
290+
}
291+
`,
292+
issues: []tt.Issue{
293+
{
294+
Rule: "format-without-verb",
295+
Message: "format string has no verbs; use errors.New() instead",
296+
Start: token.Position{Line: 6, Column: 9},
297+
End: token.Position{Line: 6, Column: 38},
298+
Suggestion: `return errors.New("handler error")`,
299+
RequiredImports: []string{"errors"},
300+
},
301+
},
302+
expected: `package main
303+
304+
import "errors"
305+
306+
func example() error {
307+
return errors.New("handler error")
308+
}
179309
`,
180310
},
181311
}

internal/fixer/imports.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package fixer
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
7+
"golang.org/x/tools/imports"
8+
)
9+
10+
// ProcessImports uses goimports-style processing to:
11+
// 1. Add missing imports (for standard library packages)
12+
// 2. Remove unused imports
13+
// 3. Format the code
14+
func ProcessImports(filename string, src []byte) ([]byte, error) {
15+
// For .gno files, use .go extension so imports package recognizes it
16+
processName := filename
17+
if strings.HasSuffix(filename, ".gno") {
18+
processName = strings.TrimSuffix(filepath.Base(filename), ".gno") + ".go"
19+
}
20+
21+
opts := &imports.Options{
22+
Comments: true,
23+
TabIndent: true,
24+
TabWidth: 8,
25+
FormatOnly: false, // Process imports, not just format
26+
}
27+
28+
result, err := imports.Process(processName, src, opts)
29+
if err != nil {
30+
return src, err
31+
}
32+
33+
return result, nil
34+
}

0 commit comments

Comments
 (0)