Created
June 27, 2022 10:34
-
-
Save uhthomas/fb66757f0e38e143bc465189a3b3d2ad to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/go/tools/builders/nogo_main.go b/go/tools/builders/nogo_main.go | |
index 631cf0e5..29722f35 100644 | |
--- a/go/tools/builders/nogo_main.go | |
+++ b/go/tools/builders/nogo_main.go | |
@@ -26,18 +26,23 @@ import ( | |
"flag" | |
"fmt" | |
"go/ast" | |
+ "go/format" | |
"go/parser" | |
"go/token" | |
"go/types" | |
+ "io" | |
"io/ioutil" | |
"log" | |
"os" | |
"reflect" | |
"regexp" | |
"sort" | |
+ "strconv" | |
"strings" | |
"sync" | |
+ "unicode" | |
+ "github.com/sergi/go-diff/diffmatchpatch" | |
"golang.org/x/tools/go/analysis" | |
"golang.org/x/tools/go/analysis/internal/facts" | |
"golang.org/x/tools/go/gcexportdata" | |
@@ -98,7 +103,7 @@ func run(args []string) error { | |
} | |
// Adapted from go/src/cmd/compile/internal/gc/main.go. Keep in sync. | |
-func readImportCfg(file string) (packageFile map[string]string, importMap map[string]string, err error) { | |
+func readImportCfg(file string) (packageFile, importMap map[string]string, err error) { | |
packageFile, importMap = make(map[string]string), make(map[string]string) | |
data, err := ioutil.ReadFile(file) | |
if err != nil { | |
@@ -145,7 +150,7 @@ func readImportCfg(file string) (packageFile map[string]string, importMap map[st | |
// It returns an empty string if no source code diagnostics need to be printed. | |
// | |
// This implementation was adapted from that of golang.org/x/tools/go/checker/internal/checker. | |
-func checkPackage(analyzers []*analysis.Analyzer, packagePath string, packageFile, importMap map[string]string, factMap map[string]string, filenames []string) (string, []byte, error) { | |
+func checkPackage(analyzers []*analysis.Analyzer, packagePath string, packageFile, importMap, factMap map[string]string, filenames []string) (string, []byte, error) { | |
// Register fact types and establish dependencies between analyzers. | |
actions := make(map[*analysis.Analyzer]*action) | |
var visit func(a *analysis.Analyzer) *action | |
@@ -362,14 +367,15 @@ func (g *goPackage) String() string { | |
return g.types.Path() | |
} | |
+type entry struct { | |
+ analysis.Diagnostic | |
+ *analysis.Analyzer | |
+} | |
+ | |
// checkAnalysisResults checks the analysis diagnostics in the given actions | |
// and returns a string containing all the diagnostics that should be printed | |
// to the build log. | |
func checkAnalysisResults(actions []*action, pkg *goPackage) string { | |
- type entry struct { | |
- analysis.Diagnostic | |
- *analysis.Analyzer | |
- } | |
var diagnostics []entry | |
var errs []error | |
for _, act := range actions { | |
@@ -430,21 +436,352 @@ func checkAnalysisResults(actions []*action, pkg *goPackage) string { | |
sort.Slice(diagnostics, func(i, j int) bool { | |
return diagnostics[i].Pos < diagnostics[j].Pos | |
}) | |
- errMsg := &bytes.Buffer{} | |
+ sort.Slice(pkg.syntax, func(i, j int) bool { | |
+ return pkg.syntax[i].Pos() < pkg.syntax[j].Pos() | |
+ }) | |
+ | |
+ var b strings.Builder | |
sep := "" | |
for _, err := range errs { | |
- errMsg.WriteString(sep) | |
+ b.WriteString(sep) | |
sep = "\n" | |
- errMsg.WriteString(err.Error()) | |
+ b.WriteString(err.Error()) | |
} | |
+ | |
+ lastBase := -1 | |
for _, d := range diagnostics { | |
- errMsg.WriteString(sep) | |
- sep = "\n" | |
- fmt.Fprintf(errMsg, "%s: %s (%s)", pkg.fset.Position(d.Pos), d.Message, d.Name) | |
+ if strings.Contains(pkg.fset.File(d.Pos).Name(), "$GOROOT") { | |
+ continue | |
+ } | |
+ fd := newFileDiagnostics(pkg, &b, d) | |
+ if base := pkg.fset.File(d.Pos).Base(); base != lastBase { | |
+ // Write the header if this is a new file. | |
+ fd.writeHeader() | |
+ lastBase = base | |
+ } | |
+ fd.writeDiagnostic() | |
+ } | |
+ return b.String() | |
+} | |
+ | |
+const ( | |
+ escapeCode = '\x1b' | |
+ csi = '[' | |
+ graphicsMode = 'm' | |
+ | |
+ normal = "0" | |
+ bold = "1" | |
+ colourCyan = "36" | |
+ colourGreen = "32" | |
+ colourRed = "31" | |
+ colourYellow = "33" | |
+) | |
+ | |
+func ansiEscapeSequence(args ...string) string { | |
+ var b strings.Builder | |
+ b.WriteRune(escapeCode) | |
+ b.WriteRune(csi) | |
+ for i, a := range args { | |
+ if i != 0 { | |
+ b.WriteRune(';') | |
+ } | |
+ b.WriteString(a) | |
+ } | |
+ b.WriteRune(graphicsMode) | |
+ return b.String() | |
+} | |
+ | |
+type posRange [2]token.Pos | |
+ | |
+var _ analysis.Range = posRange{} | |
+ | |
+func (r posRange) Pos() token.Pos { return r[0] } | |
+func (r posRange) End() token.Pos { return r[1] } | |
+ | |
+type fileDiagnostics struct { | |
+ pkg *goPackage | |
+ b *strings.Builder | |
+ prefixLength int | |
+ d entry | |
+ dmp *diffmatchpatch.DiffMatchPatch | |
+ f *token.File | |
+} | |
+ | |
+func newFileDiagnostics( | |
+ pkg *goPackage, | |
+ b *strings.Builder, | |
+ d entry, | |
+) *fileDiagnostics { | |
+ fd := fileDiagnostics{ | |
+ pkg: pkg, | |
+ b: b, | |
+ d: d, | |
+ dmp: diffmatchpatch.New(), | |
+ f: pkg.fset.File(d.Pos), | |
+ } | |
+ // setPrefixLength sets the prefix length if the length of the textual | |
+ // representation of the line associated with pos is greater. Essentially | |
+ // just max(pos1, pos2). | |
+ setPrefixLength := func(p token.Pos) { | |
+ if !p.IsValid() { | |
+ return | |
+ } | |
+ if l := len(strconv.Itoa(fd.f.Line(p))); l > fd.prefixLength { | |
+ fd.prefixLength = l | |
+ } | |
+ } | |
+ // this is probably not necessary given we now only use one value ever. | |
+ setPrefixLength(d.Pos) | |
+ setPrefixLength(d.End) | |
+ for _, sf := range d.SuggestedFixes { | |
+ for _, edit := range sf.TextEdits { | |
+ setPrefixLength(edit.Pos) | |
+ setPrefixLength(edit.End) | |
+ } | |
+ } | |
+ return &fd | |
+} | |
+ | |
+func (fd *fileDiagnostics) writeHeader() { | |
+ fd.b.WriteRune('\n') | |
+ fd.b.WriteString(fd.pkg.fset.Position(fd.d.Pos).String()) | |
+ fd.b.WriteString(":\n") | |
+} | |
+ | |
+func (fd *fileDiagnostics) writeDiagnostic() { | |
+ fd.b.WriteRune('\n') | |
+ fd.b.WriteString(ansiEscapeSequence( /*bold,*/ colourRed)) | |
+ fd.b.WriteString(fd.d.String()) | |
+ fd.b.WriteRune(':') | |
+ fd.b.WriteString(ansiEscapeSequence(normal)) | |
+ fd.b.WriteRune(' ') | |
+ fd.b.WriteString(fd.d.Message) | |
+ fd.b.WriteString("\n\n") | |
+ | |
+ f := fd.pkg.fset.File(fd.d.Pos) | |
+ | |
+ end := fd.d.End | |
+ if !end.IsValid() { | |
+ if l := f.Line(fd.d.Pos); l < f.LineCount() { | |
+ // Get the position for the end of the line. | |
+ end = f.LineStart(l+1) - 1 | |
+ } else { | |
+ // This is the last line of the file, just read it all. | |
+ end = f.Pos(f.Size()) | |
+ } | |
+ } | |
+ | |
+ fd.writeEdits([]analysis.TextEdit{{ | |
+ Pos: f.LineStart(f.Line(fd.d.Pos)), | |
+ End: end, | |
+ }}, false) | |
+ | |
+ fd.writeLine(-1) | |
+ | |
+ fd.writeSuggestedFixes() | |
+} | |
+ | |
+func (fd *fileDiagnostics) writeSuggestedFixes() { | |
+ for _, sf := range fd.d.SuggestedFixes { | |
+ fd.writeSuggestedFix(sf) | |
+ } | |
+} | |
+ | |
+func (fd *fileDiagnostics) writeSuggestedFix(sf analysis.SuggestedFix) { | |
+ fd.writeLine(-1, | |
+ ansiEscapeSequence( /*bold,*/ colourYellow), | |
+ "suggested fix:", | |
+ ansiEscapeSequence(normal), | |
+ " ", | |
+ sf.Message, | |
+ ) | |
+ fd.writeLine(-1) | |
+ fd.writeEdits(sf.TextEdits, true) | |
+} | |
+ | |
+func (fd *fileDiagnostics) writeEdits(edits []analysis.TextEdit, format bool) { | |
+ if len(edits) == 0 { | |
+ return | |
+ } | |
+ | |
+ l := fd.f.Line(fd.d.Pos) | |
+ for _, d := range fd.calculateDiffs(edits, format) { | |
+ for _, line := range strings.Split( | |
+ strings.TrimRightFunc(d.Text, unicode.IsSpace), | |
+ "\n", | |
+ ) { | |
+ switch d.Type { | |
+ case diffmatchpatch.DiffInsert: | |
+ fd.writeLine( | |
+ l, | |
+ ansiEscapeSequence(bold, colourGreen), | |
+ line, | |
+ ansiEscapeSequence(normal), | |
+ ) | |
+ case diffmatchpatch.DiffDelete: | |
+ fd.writeLine( | |
+ l, | |
+ ansiEscapeSequence(colourRed), | |
+ line, | |
+ ansiEscapeSequence(normal), | |
+ ) | |
+ case diffmatchpatch.DiffEqual: | |
+ fd.writeLine(l, line) | |
+ } | |
+ l = -1 | |
+ } | |
+ } | |
+} | |
+ | |
+func (fd *fileDiagnostics) writeLines(start int, lines []string) { | |
+ for i, l := range lines { | |
+ if i == 0 { | |
+ fd.writeLine(start+i, l) | |
+ } else { | |
+ fd.writeLine(-1, l) | |
+ } | |
} | |
- return errMsg.String() | |
} | |
+// writeLine writes a line to the string builder with the given prefix. If line | |
+// is -1, it is omitted. | |
+func (fd *fileDiagnostics) writeLine(line int, a ...string) { | |
+ fd.writePrefix(line) | |
+ if len(a) != 0 { | |
+ fd.b.WriteRune(' ') | |
+ for _, s := range a { | |
+ fd.b.WriteString(s) | |
+ } | |
+ } | |
+ fd.b.WriteRune('\n') | |
+} | |
+ | |
+func (fd *fileDiagnostics) writePrefix(line int) { | |
+ cyan := ansiEscapeSequence(colourCyan) | |
+ normal := ansiEscapeSequence(normal) | |
+ prefix := make([]byte, len(cyan)+len(normal)+fd.prefixLength+2) | |
+ n := copy(prefix, []byte(cyan)) | |
+ n += copy(prefix[n:], bytes.Repeat([]byte{' '}, fd.prefixLength+1)) | |
+ n += copy(prefix[n:], []byte{'|'}) | |
+ if line != -1 { | |
+ strconv.AppendInt(prefix[:len(cyan)], int64(line), 10) | |
+ } | |
+ copy(prefix[n:], []byte(normal)) | |
+ fd.b.Write(prefix) | |
+} | |
+ | |
+func (fd *fileDiagnostics) calculateDiffs(edits []analysis.TextEdit, format bool) []diffmatchpatch.Diff { | |
+ sort.SliceStable(edits, func(i, j int) bool { | |
+ return edits[i].Pos < edits[j].Pos || edits[i].End < edits[j].End | |
+ // Some edits are purely insertion. They should be prioritised. | |
+ }) | |
+ | |
+ var before strings.Builder | |
+ if err := writeFileTo(fd.f.Name(), &before); err != nil { | |
+ panic(err) | |
+ } | |
+ | |
+ after := fd.applyEdits(edits, before.String()) | |
+ | |
+ if format { | |
+ fd.formatSource(fd.f.Name(), after) | |
+ } | |
+ | |
+ ad, bd, lines := fd.dmp.DiffLinesToRunes(before.String(), after.String()) | |
+ return truncateDiffs(fd.dmp.DiffCleanupSemantic(fd.dmp.DiffCharsToLines( | |
+ fd.dmp.DiffMainRunes(ad, bd, false), | |
+ lines, | |
+ ))) | |
+} | |
+ | |
+func truncateDiffs(diffs []diffmatchpatch.Diff) []diffmatchpatch.Diff { | |
+ if len(diffs) == 0 { | |
+ return diffs | |
+ } | |
+ startLen := len(diffs) | |
+ for i := 0; i < len(diffs); i++ { | |
+ if diffs[i].Type != diffmatchpatch.DiffEqual && strings.TrimSpace(diffs[i].Text) != "" { | |
+ diffs = diffs[i:] | |
+ break | |
+ } | |
+ } | |
+ for i := len(diffs) - 1; i >= 0; i-- { | |
+ if diffs[i].Type != diffmatchpatch.DiffEqual && strings.TrimSpace(diffs[i].Text) != "" { | |
+ diffs = diffs[:i+1] | |
+ break | |
+ } | |
+ } | |
+ if len(diffs) == startLen { | |
+ // The diffs haven't been trimmed. This could cause the entire | |
+ // file to be printed. Instead, trim the space from the diff | |
+ // text and limit to just the first line. | |
+ diffs = diffs[:1] | |
+ diffs[0].Text = strings.SplitN(strings.TrimSpace(diffs[0].Text), "\n", 2)[0] | |
+ } | |
+ return diffs | |
+} | |
+ | |
+func (fd *fileDiagnostics) applyEdits(edits []analysis.TextEdit, before string) *strings.Builder { | |
+ var ( | |
+ b strings.Builder | |
+ currentOffset int | |
+ ) | |
+ for _, edit := range edits { | |
+ off := fd.f.Offset(edit.Pos) | |
+ b.WriteString(before[currentOffset:off]) | |
+ if len(edit.NewText) > 0 { | |
+ b.Write(edit.NewText) | |
+ } | |
+ | |
+ end := off | |
+ if edit.End.IsValid() { | |
+ // The end pos for text edits may be token.NoPos to represent pure | |
+ // insertion. | |
+ end = fd.f.Offset(edit.End) | |
+ } | |
+ currentOffset = end | |
+ | |
+ // fmt.Printf("edit (off %d, end %d): %s -> %s\n", edit.Pos, edit.End, before[off:end], string(edit.NewText)) | |
+ } | |
+ b.WriteString(before[currentOffset:]) | |
+ return &b | |
+} | |
+ | |
+func (fd *fileDiagnostics) formatSource(name string, b *strings.Builder) { | |
+ fset := token.NewFileSet() | |
+ f, err := parser.ParseFile(fset, name, b.String(), parser.ParseComments) | |
+ if err != nil { | |
+ // Formatting is best effort. Silently fail. | |
+ fmt.Println(fmt.Errorf("parse file: %w", err).Error()) | |
+ return | |
+ } | |
+ b.Reset() | |
+ if err := format.Node(b, fset, f); err != nil { | |
+ fmt.Println(fmt.Errorf("format node: %w", err).Error()) | |
+ return | |
+ } | |
+} | |
+ | |
+func writeFileTo(name string, w io.Writer) error { | |
+ ff, err := os.Open(name) | |
+ if err != nil { | |
+ return fmt.Errorf("open: %w", err) | |
+ } | |
+ defer ff.Close() | |
+ _, err = io.Copy(w, ff) | |
+ return err | |
+} | |
+ | |
+// func (fd *fileDiagnostics) findAST(pos token.Pos) *ast.File { | |
+// base := fd.pkg.fset.File(pos).Base() | |
+// if i := sort.Search(len(fd.pkg.syntax), func(i int) bool { | |
+// return fd.pkg.fset.File(fd.pkg.syntax[i].Pos()).Base() >= base | |
+// }); i < len(fd.pkg.syntax) && fd.pkg.fset.File(fd.pkg.syntax[i].Pos()).Base() == base { | |
+// return fd.pkg.syntax[i] | |
+// } | |
+// return nil | |
+// } | |
+ | |
// config determines which source files an analyzer will emit diagnostics for. | |
// config values are generated in another file that is compiled with | |
// nogo_main.go by the nogo rule. | |
@@ -469,7 +806,7 @@ type importer struct { | |
factMap map[string]string // map import path in source code to file containing serialized facts | |
} | |
-func newImporter(importMap, packageFile map[string]string, factMap map[string]string) *importer { | |
+func newImporter(importMap, packageFile, factMap map[string]string) *importer { | |
return &importer{ | |
fset: token.NewFileSet(), | |
importMap: importMap, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff -urN a/go/tools/builders/BUILD.bazel b/go/tools/builders/BUILD.bazel | |
--- a/go/tools/builders/BUILD.bazel | |
+++ b/go/tools/builders/BUILD.bazel | |
@@ -77,6 +77,7 @@ go_source( | |
tags = ["manual"], | |
visibility = ["//visibility:public"], | |
deps = [ | |
+ "@com_github_sergi_go_diff//diffmatchpatch", | |
"@org_golang_x_tools//go/analysis", | |
"@org_golang_x_tools//go/analysis/internal/facts:go_default_library", | |
"@org_golang_x_tools//go/gcexportdata", |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment