Skip to content

Commit d6e755b

Browse files
authored
support suggested fixes by analyzing a diff (#148)
Added `GetSuggestedFix` function creates unified diff for `unmodifiedFile` and `formattedFile`. Then analyzes the diff and creates `analysis.SuggestedFix` if needed. The Analyzer checks the result of `GetSuggestedFix` function and reports as `analysis.Diagnostic`. Fix #146 Signed-off-by: Sergey Vilgelm <[email protected]>
1 parent 4b78992 commit d6e755b

File tree

4 files changed

+225
-34
lines changed

4 files changed

+225
-34
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.20
44

55
require (
66
github.com/hexops/gotextdiff v1.0.3
7+
github.com/pmezard/go-difflib v1.0.0
78
github.com/spf13/cobra v1.6.1
89
github.com/stretchr/testify v1.8.1
910
go.uber.org/zap v1.24.0
@@ -15,7 +16,6 @@ require (
1516
require (
1617
github.com/davecgh/go-spew v1.1.1 // indirect
1718
github.com/inconshreveable/mousetrap v1.0.1 // indirect
18-
github.com/pmezard/go-difflib v1.0.0 // indirect
1919
github.com/spf13/pflag v1.0.5 // indirect
2020
go.uber.org/atomic v1.7.0 // indirect
2121
go.uber.org/multierr v1.6.0 // indirect

pkg/analyzer/analyzer.go

+14-33
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package analyzer
22

33
import (
4-
"bytes"
54
"fmt"
65
"go/token"
76
"strings"
87

98
"golang.org/x/tools/go/analysis"
10-
"golang.org/x/tools/go/analysis/passes/inspect"
119

1210
"github.com/daixiang0/gci/pkg/config"
1311
"github.com/daixiang0/gci/pkg/gci"
@@ -44,10 +42,9 @@ func init() {
4442
}
4543

4644
var Analyzer = &analysis.Analyzer{
47-
Name: "gci",
48-
Doc: "A tool that control golang package import order and make it always deterministic.",
49-
Requires: []*analysis.Analyzer{inspect.Analyzer},
50-
Run: runAnalysis,
45+
Name: "gci",
46+
Doc: "A tool that control golang package import order and make it always deterministic.",
47+
Run: runAnalysis,
5148
}
5249

5350
func runAnalysis(pass *analysis.Pass) (interface{}, error) {
@@ -77,39 +74,23 @@ func runAnalysis(pass *analysis.Pass) (interface{}, error) {
7774
if err != nil {
7875
return nil, err
7976
}
80-
// search for a difference
81-
fileRunes := bytes.Runes(unmodifiedFile)
82-
formattedRunes := bytes.Runes(formattedFile)
83-
diffIdx := compareRunes(fileRunes, formattedRunes)
84-
switch diffIdx {
85-
case -1:
77+
fix, err := GetSuggestedFix(file, unmodifiedFile, formattedFile)
78+
if err != nil {
79+
return nil, err
80+
}
81+
if fix == nil {
8682
// no difference
87-
default:
88-
pass.Reportf(file.Pos(diffIdx), "fix by `%s %s`", generateCmdLine(*gciCfg), filePath)
83+
continue
8984
}
85+
pass.Report(analysis.Diagnostic{
86+
Pos: fix.TextEdits[0].Pos,
87+
Message: fmt.Sprintf("fix by `%s %s`", generateCmdLine(*gciCfg), filePath),
88+
SuggestedFixes: []analysis.SuggestedFix{*fix},
89+
})
9090
}
9191
return nil, nil
9292
}
9393

94-
func compareRunes(a, b []rune) (differencePos int) {
95-
// check shorter rune slice first to prevent invalid array access
96-
shorterRune := a
97-
if len(b) < len(a) {
98-
shorterRune = b
99-
}
100-
// check for differences up to where the length is identical
101-
for idx := 0; idx < len(shorterRune); idx++ {
102-
if a[idx] != b[idx] {
103-
return idx
104-
}
105-
}
106-
// check that we have compared two equally long rune arrays
107-
if len(a) != len(b) {
108-
return len(shorterRune) + 1
109-
}
110-
return -1
111-
}
112-
11394
func parseGciConfiguration() (*config.Config, error) {
11495
fmtCfg := config.BoolConfig{
11596
NoInlineComments: noInlineComments,

pkg/analyzer/diff.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package analyzer
2+
3+
import (
4+
"bytes"
5+
"go/token"
6+
"regexp"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/pmezard/go-difflib/difflib"
11+
"golang.org/x/tools/go/analysis"
12+
)
13+
14+
var hunkRE = regexp.MustCompile(`@@ -(\d+),(\d+) \+\d+,\d+ @@`)
15+
16+
func GetSuggestedFix(file *token.File, a, b []byte) (*analysis.SuggestedFix, error) {
17+
d := difflib.UnifiedDiff{
18+
A: difflib.SplitLines(string(a)),
19+
B: difflib.SplitLines(string(b)),
20+
Context: 1,
21+
}
22+
diff, err := difflib.GetUnifiedDiffString(d)
23+
if err != nil {
24+
return nil, err
25+
}
26+
if diff == "" {
27+
return nil, nil
28+
}
29+
var (
30+
fix analysis.SuggestedFix
31+
found = false
32+
edit analysis.TextEdit
33+
buf bytes.Buffer
34+
)
35+
for _, line := range strings.Split(diff, "\n") {
36+
if hunk := hunkRE.FindStringSubmatch(line); len(hunk) > 0 {
37+
if found {
38+
edit.NewText = buf.Bytes()
39+
buf = bytes.Buffer{}
40+
fix.TextEdits = append(fix.TextEdits, edit)
41+
edit = analysis.TextEdit{}
42+
}
43+
found = true
44+
start, err := strconv.Atoi(hunk[1])
45+
if err != nil {
46+
return nil, err
47+
}
48+
lines, err := strconv.Atoi(hunk[2])
49+
if err != nil {
50+
return nil, err
51+
}
52+
edit.Pos = file.LineStart(start)
53+
end := start + lines
54+
if end > file.LineCount() {
55+
edit.End = token.Pos(file.Size())
56+
} else {
57+
edit.End = file.LineStart(end)
58+
}
59+
continue
60+
}
61+
// skip any lines until first hunk found
62+
if !found {
63+
continue
64+
}
65+
if line == "" {
66+
continue
67+
}
68+
switch line[0] {
69+
case '+':
70+
buf.WriteString(line[1:])
71+
buf.WriteRune('\n')
72+
case '-':
73+
// just skip
74+
default:
75+
buf.WriteString(line)
76+
buf.WriteRune('\n')
77+
}
78+
}
79+
edit.NewText = buf.Bytes()
80+
fix.TextEdits = append(fix.TextEdits, edit)
81+
82+
return &fix, nil
83+
}

pkg/analyzer/diff_test.go

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package analyzer_test
2+
3+
import (
4+
"go/parser"
5+
"go/token"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"golang.org/x/tools/go/analysis"
10+
11+
"github.com/daixiang0/gci/pkg/analyzer"
12+
)
13+
14+
const formattedFile = `package analyzer
15+
16+
import (
17+
"fmt"
18+
"go/token"
19+
"strings"
20+
21+
"golang.org/x/tools/go/analysis"
22+
23+
"github.com/daixiang0/gci/pkg/config"
24+
"github.com/daixiang0/gci/pkg/gci"
25+
"github.com/daixiang0/gci/pkg/io"
26+
"github.com/daixiang0/gci/pkg/log"
27+
)
28+
`
29+
30+
func TestGetSuggestedFix(t *testing.T) {
31+
for _, tt := range []struct {
32+
name string
33+
unformattedFile string
34+
expectedFix *analysis.SuggestedFix
35+
expectedErr string
36+
}{
37+
{
38+
name: "same files",
39+
unformattedFile: formattedFile,
40+
},
41+
{
42+
name: "one change",
43+
unformattedFile: `package analyzer
44+
45+
import (
46+
"fmt"
47+
"go/token"
48+
"strings"
49+
50+
"golang.org/x/tools/go/analysis"
51+
52+
"github.com/daixiang0/gci/pkg/config"
53+
"github.com/daixiang0/gci/pkg/gci"
54+
55+
"github.com/daixiang0/gci/pkg/io"
56+
"github.com/daixiang0/gci/pkg/log"
57+
)
58+
`,
59+
expectedFix: &analysis.SuggestedFix{
60+
TextEdits: []analysis.TextEdit{
61+
{
62+
Pos: 133,
63+
End: 205,
64+
NewText: []byte(` "github.com/daixiang0/gci/pkg/gci"
65+
"github.com/daixiang0/gci/pkg/io"
66+
`,
67+
),
68+
},
69+
},
70+
},
71+
},
72+
{
73+
name: "multiple changes",
74+
unformattedFile: `package analyzer
75+
76+
import (
77+
"fmt"
78+
"go/token"
79+
80+
"strings"
81+
82+
"golang.org/x/tools/go/analysis"
83+
84+
"github.com/daixiang0/gci/pkg/config"
85+
"github.com/daixiang0/gci/pkg/gci"
86+
87+
"github.com/daixiang0/gci/pkg/io"
88+
"github.com/daixiang0/gci/pkg/log"
89+
)
90+
`,
91+
expectedFix: &analysis.SuggestedFix{
92+
TextEdits: []analysis.TextEdit{
93+
{
94+
Pos: 35,
95+
End: 59,
96+
NewText: []byte(` "go/token"
97+
"strings"
98+
`,
99+
),
100+
},
101+
{
102+
Pos: 134,
103+
End: 206,
104+
NewText: []byte(` "github.com/daixiang0/gci/pkg/gci"
105+
"github.com/daixiang0/gci/pkg/io"
106+
`,
107+
),
108+
},
109+
},
110+
},
111+
},
112+
} {
113+
t.Run(tt.name, func(t *testing.T) {
114+
fset := token.NewFileSet()
115+
f, err := parser.ParseFile(fset, "analyzer.go", tt.unformattedFile, 0)
116+
assert.NoError(t, err)
117+
118+
actualFix, err := analyzer.GetSuggestedFix(fset.File(f.Pos()), []byte(tt.unformattedFile), []byte(formattedFile))
119+
if tt.expectedErr != "" {
120+
assert.ErrorContains(t, err, tt.expectedErr)
121+
return
122+
}
123+
assert.NoError(t, err)
124+
assert.Equal(t, tt.expectedFix, actualFix)
125+
})
126+
}
127+
}

0 commit comments

Comments
 (0)