Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support suggested fixes by analyzing a diff #148

Merged
merged 1 commit into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/hexops/gotextdiff v1.0.3
github.com/pmezard/go-difflib v1.0.0
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
go.uber.org/zap v1.24.0
Expand All @@ -15,7 +16,6 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
Expand Down
47 changes: 14 additions & 33 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package analyzer

import (
"bytes"
"fmt"
"go/token"
"strings"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"

"github.com/daixiang0/gci/pkg/config"
"github.com/daixiang0/gci/pkg/gci"
Expand Down Expand Up @@ -44,10 +42,9 @@ func init() {
}

var Analyzer = &analysis.Analyzer{
Name: "gci",
Doc: "A tool that control golang package import order and make it always deterministic.",
Requires: []*analysis.Analyzer{inspect.Analyzer},
SVilgelm marked this conversation as resolved.
Show resolved Hide resolved
Run: runAnalysis,
Name: "gci",
Doc: "A tool that control golang package import order and make it always deterministic.",
Run: runAnalysis,
}

func runAnalysis(pass *analysis.Pass) (interface{}, error) {
Expand Down Expand Up @@ -77,39 +74,23 @@ func runAnalysis(pass *analysis.Pass) (interface{}, error) {
if err != nil {
return nil, err
}
// search for a difference
fileRunes := bytes.Runes(unmodifiedFile)
formattedRunes := bytes.Runes(formattedFile)
diffIdx := compareRunes(fileRunes, formattedRunes)
switch diffIdx {
case -1:
fix, err := GetSuggestedFix(file, unmodifiedFile, formattedFile)
if err != nil {
return nil, err
}
if fix == nil {
// no difference
default:
pass.Reportf(file.Pos(diffIdx), "fix by `%s %s`", generateCmdLine(*gciCfg), filePath)
continue
}
pass.Report(analysis.Diagnostic{
Pos: fix.TextEdits[0].Pos,
Message: fmt.Sprintf("fix by `%s %s`", generateCmdLine(*gciCfg), filePath),
SuggestedFixes: []analysis.SuggestedFix{*fix},
})
}
return nil, nil
}

func compareRunes(a, b []rune) (differencePos int) {
// check shorter rune slice first to prevent invalid array access
shorterRune := a
if len(b) < len(a) {
shorterRune = b
}
// check for differences up to where the length is identical
for idx := 0; idx < len(shorterRune); idx++ {
if a[idx] != b[idx] {
return idx
}
}
// check that we have compared two equally long rune arrays
if len(a) != len(b) {
return len(shorterRune) + 1
}
return -1
}

func parseGciConfiguration() (*config.Config, error) {
fmtCfg := config.BoolConfig{
NoInlineComments: noInlineComments,
Expand Down
83 changes: 83 additions & 0 deletions pkg/analyzer/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package analyzer

import (
"bytes"
"go/token"
"regexp"
"strconv"
"strings"

"github.com/pmezard/go-difflib/difflib"
"golang.org/x/tools/go/analysis"
)

var hunkRE = regexp.MustCompile(`@@ -(\d+),(\d+) \+\d+,\d+ @@`)

func GetSuggestedFix(file *token.File, a, b []byte) (*analysis.SuggestedFix, error) {
d := difflib.UnifiedDiff{
A: difflib.SplitLines(string(a)),
B: difflib.SplitLines(string(b)),
Context: 1,
}
diff, err := difflib.GetUnifiedDiffString(d)
if err != nil {
return nil, err
}
if diff == "" {
return nil, nil
}
var (
fix analysis.SuggestedFix
found = false
edit analysis.TextEdit
buf bytes.Buffer
)
for _, line := range strings.Split(diff, "\n") {
if hunk := hunkRE.FindStringSubmatch(line); len(hunk) > 0 {
if found {
edit.NewText = buf.Bytes()
buf = bytes.Buffer{}
fix.TextEdits = append(fix.TextEdits, edit)
edit = analysis.TextEdit{}
}
found = true
start, err := strconv.Atoi(hunk[1])
if err != nil {
return nil, err
}
lines, err := strconv.Atoi(hunk[2])
if err != nil {
return nil, err
}
edit.Pos = file.LineStart(start)
end := start + lines
if end > file.LineCount() {
edit.End = token.Pos(file.Size())
} else {
edit.End = file.LineStart(end)
}
continue
}
// skip any lines until first hunk found
if !found {
continue
}
if line == "" {
continue
}
switch line[0] {
case '+':
buf.WriteString(line[1:])
buf.WriteRune('\n')
case '-':
// just skip
default:
buf.WriteString(line)
buf.WriteRune('\n')
}
}
edit.NewText = buf.Bytes()
fix.TextEdits = append(fix.TextEdits, edit)

return &fix, nil
}
127 changes: 127 additions & 0 deletions pkg/analyzer/diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package analyzer_test

import (
"go/parser"
"go/token"
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/tools/go/analysis"

"github.com/daixiang0/gci/pkg/analyzer"
)

const formattedFile = `package analyzer

import (
"fmt"
"go/token"
"strings"

"golang.org/x/tools/go/analysis"

"github.com/daixiang0/gci/pkg/config"
"github.com/daixiang0/gci/pkg/gci"
"github.com/daixiang0/gci/pkg/io"
"github.com/daixiang0/gci/pkg/log"
)
`

func TestGetSuggestedFix(t *testing.T) {
for _, tt := range []struct {
name string
unformattedFile string
expectedFix *analysis.SuggestedFix
expectedErr string
}{
{
name: "same files",
unformattedFile: formattedFile,
},
{
name: "one change",
unformattedFile: `package analyzer

import (
"fmt"
"go/token"
"strings"

"golang.org/x/tools/go/analysis"

"github.com/daixiang0/gci/pkg/config"
"github.com/daixiang0/gci/pkg/gci"

"github.com/daixiang0/gci/pkg/io"
"github.com/daixiang0/gci/pkg/log"
)
`,
expectedFix: &analysis.SuggestedFix{
TextEdits: []analysis.TextEdit{
{
Pos: 133,
End: 205,
NewText: []byte(` "github.com/daixiang0/gci/pkg/gci"
"github.com/daixiang0/gci/pkg/io"
`,
),
},
},
},
},
{
name: "multiple changes",
unformattedFile: `package analyzer

import (
"fmt"
"go/token"

"strings"

"golang.org/x/tools/go/analysis"

"github.com/daixiang0/gci/pkg/config"
"github.com/daixiang0/gci/pkg/gci"

"github.com/daixiang0/gci/pkg/io"
"github.com/daixiang0/gci/pkg/log"
)
`,
expectedFix: &analysis.SuggestedFix{
TextEdits: []analysis.TextEdit{
{
Pos: 35,
End: 59,
NewText: []byte(` "go/token"
"strings"
`,
),
},
{
Pos: 134,
End: 206,
NewText: []byte(` "github.com/daixiang0/gci/pkg/gci"
"github.com/daixiang0/gci/pkg/io"
`,
),
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "analyzer.go", tt.unformattedFile, 0)
assert.NoError(t, err)

actualFix, err := analyzer.GetSuggestedFix(fset.File(f.Pos()), []byte(tt.unformattedFile), []byte(formattedFile))
if tt.expectedErr != "" {
assert.ErrorContains(t, err, tt.expectedErr)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expectedFix, actualFix)
})
}
}