Skip to content

Instantly share code, notes, and snippets.

@uhthomas
Created June 27, 2022 10:34
Show Gist options
  • Save uhthomas/fb66757f0e38e143bc465189a3b3d2ad to your computer and use it in GitHub Desktop.
Save uhthomas/fb66757f0e38e143bc465189a3b3d2ad to your computer and use it in GitHub Desktop.
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,
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