Skip to content

Commit

Permalink
support suggested fixes by analyzing a diff
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`.

Signed-off-by: Sergey Vilgelm <sergey@vilgelm.com>
  • Loading branch information
SVilgelm committed Mar 8, 2023
1 parent 4b78992 commit d5a859f
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 28 deletions.
40 changes: 12 additions & 28 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package analyzer

import (
"bytes"
"fmt"
"go/token"
"strings"
Expand All @@ -13,6 +12,7 @@ import (
"github.com/daixiang0/gci/pkg/gci"
"github.com/daixiang0/gci/pkg/io"
"github.com/daixiang0/gci/pkg/log"
"github.com/daixiang0/gci/pkg/utils"
)

const (
Expand Down Expand Up @@ -77,39 +77,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 := utils.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
78 changes: 78 additions & 0 deletions pkg/utils/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package utils

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
first = true
edit analysis.TextEdit
buf bytes.Buffer
)
for _, line := range strings.Split(diff, "\n") {
if line == "" {
continue
}
hunk := hunkRE.FindStringSubmatch(line)
switch {
case len(hunk) > 0:
if !first {
edit.NewText = buf.Bytes()
buf = bytes.Buffer{}
fix.TextEdits = append(fix.TextEdits, edit)
edit = analysis.TextEdit{}
}
first = false
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)
}
case line[0] == '+':
buf.WriteString(line[1:])
buf.WriteRune('\n')
case line[0] == '-':
// just skip
default:
buf.WriteString(line)
buf.WriteRune('\n')
}
}
edit.NewText = buf.Bytes()
fix.TextEdits = append(fix.TextEdits, edit)

return &fix, nil
}

0 comments on commit d5a859f

Please sign in to comment.