diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 9500202..b575fa1 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -1,7 +1,6 @@ package analyzer import ( - "bytes" "fmt" "go/token" "strings" @@ -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 ( @@ -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, diff --git a/pkg/utils/diff.go b/pkg/utils/diff.go new file mode 100644 index 0000000..7695a7b --- /dev/null +++ b/pkg/utils/diff.go @@ -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 +}