Skip to content

Commit

Permalink
support suggested fixes by analyzing a diff (#148)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
SVilgelm authored Mar 9, 2023
1 parent 4b78992 commit d6e755b
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 34 deletions.
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},
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)
})
}
}

0 comments on commit d6e755b

Please sign in to comment.