gopls/internal/lsp/source: multiline errors from rename

This changes restores the full glory of the multi-line
error messages produced by the gorename tool, which
cite up to three source locations in the explanation
of a renaming conflict. LSP provides no way to preserve
the structure of the error, so we do our best to format
the filename concisely but informatively.

Change-Id: I4e56a818e48f94a19f732f3142917e67869212f1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/466495
Run-TryBot: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go
index f56ed40..7b00f88 100644
--- a/gopls/internal/lsp/source/rename.go
+++ b/gopls/internal/lsp/source/rename.go
@@ -35,8 +35,7 @@
 	snapshot           Snapshot
 	refs               []*ReferenceInfo
 	objsToUpdate       map[types.Object]bool
-	hadConflicts       bool
-	errors             string
+	conflicts          []string
 	from, to           string
 	satisfyConstraints map[satisfy.Constraint]bool
 	packages           map[*types.Package]Package // may include additional packages that are a dep of pkg.
@@ -647,13 +646,11 @@
 	// Check that the renaming of the identifier is ok.
 	for _, ref := range refs {
 		r.check(ref.obj)
-		if r.hadConflicts { // one error is enough.
-			break
+		if len(r.conflicts) > 0 {
+			// Stop at first error.
+			return nil, fmt.Errorf("%s", strings.Join(r.conflicts, "\n"))
 		}
 	}
-	if r.hadConflicts {
-		return nil, fmt.Errorf("%s", r.errors)
-	}
 
 	changes, err := r.update()
 	if err != nil {
diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go
index d01d228..69b0c4a 100644
--- a/gopls/internal/lsp/source/rename_check.go
+++ b/gopls/internal/lsp/source/rename_check.go
@@ -12,18 +12,59 @@
 	"go/ast"
 	"go/token"
 	"go/types"
+	"path/filepath"
 	"reflect"
 	"strings"
 	"unicode"
 
 	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/refactor/satisfy"
 )
 
 // errorf reports an error (e.g. conflict) and prevents file modification.
 func (r *renamer) errorf(pos token.Pos, format string, args ...interface{}) {
-	r.hadConflicts = true
-	r.errors += fmt.Sprintf(format, args...)
+	// Conflict error messages in the old gorename tool (whence this
+	// logic originated) contain rich information associated with
+	// multiple source lines, such as:
+	//
+	//   p/a.go:1:2: renaming "x" to "y" here
+	//   p/b.go:3:4: \t would cause this reference to "y"
+	//   p/c.go:5:5: \t to become shadowed by this intervening declaration.
+	//
+	// Unfortunately LSP provides no means to transmit the
+	// structure of this error, so we format the positions briefly
+	// using dir/file.go where dir is the base name of the parent
+	// directory.
+
+	var conflict strings.Builder
+
+	// Add prefix of (truncated) position.
+	if pos != token.NoPos {
+		var fset *token.FileSet
+		for _, pkg := range r.packages {
+			fset = pkg.FileSet()
+			break
+		}
+		if fset != nil {
+			// TODO(adonovan): skip position of first error if it is
+			// on the same line as the renaming itself.
+			posn := safetoken.StartPosition(fset, pos).String()
+			segments := strings.Split(filepath.ToSlash(posn), "/")
+			if n := len(segments); n > 2 {
+				segments = segments[n-2:]
+			}
+			posn = strings.Join(segments, "/")
+			fmt.Fprintf(&conflict, "%s:", posn)
+
+			if !strings.HasPrefix(format, "\t") {
+				conflict.WriteByte(' ')
+			}
+		}
+	}
+
+	fmt.Fprintf(&conflict, format, args...)
+	r.conflicts = append(r.conflicts, conflict.String())
 }
 
 // check performs safety checks of the renaming of the 'from' object to r.to.
diff --git a/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden b/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden
index 6281bcd..a34b5c0 100644
--- a/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden
+++ b/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden
@@ -1,5 +1,7 @@
 -- a-rename --
-renaming this func "A" to "a"	would cause this reference to become shadowed	by this intervening var definition
+shadow/shadow.go:10:6: renaming this func "A" to "a"
+shadow/shadow.go:5:13:	would cause this reference to become shadowed
+shadow/shadow.go:4:2:	by this intervening var definition
 -- b-rename --
 package shadow
 
@@ -23,7 +25,8 @@
 }
 
 -- c-rename --
-renaming this var "b" to "c"	conflicts with var in same block
+shadow/shadow.go:5:2: renaming this var "b" to "c"
+shadow/shadow.go:5:5:	conflicts with var in same block
 -- d-rename --
 package shadow