go/types/internal/play: show Cursor path

(e.g. File.Decls[0].(*ast.GenDecl) etc)

Also, add Cursor.GoString method that shows the complete path.

Updates golang/go#80024

Change-Id: I437b7314ef19b5d3077256c25a4990a17ced53b6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/790900
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alex Putman <aputman@golang.org>
diff --git a/go/ast/inspector/cursor.go b/go/ast/inspector/cursor.go
index 239b10c..1c48225 100644
--- a/go/ast/inspector/cursor.go
+++ b/go/ast/inspector/cursor.go
@@ -10,6 +10,7 @@
 	"go/token"
 	"iter"
 	"reflect"
+	"strings"
 
 	"golang.org/x/tools/go/ast/edge"
 )
@@ -110,6 +111,46 @@
 	return reflect.TypeOf(c.Node()).String()
 }
 
+// GoString returns a string describing the cursor's path from the
+// root, if any.
+func (c Cursor) GoString() string {
+	if !c.Valid() {
+		return "(invalid)"
+	}
+	if c.index < 0 {
+		return "(root)"
+	}
+	// e.g "File.Decls[1].(*ast.GenDecl).Specs[0].(*ast.TypeSpec)"
+	//
+	// In hindsight even the File node should have reported a
+	// virtual ParentEdge of (Root_Files, i) where i is the index
+	// among the files passed to NewInspector. Then the path would
+	// read "(root).Files[i]", etc; but we missed the boat.
+	var buf strings.Builder
+	buf.WriteString("File")
+	var visit func(Cursor)
+	visit = func(c Cursor) {
+		ek, idx := c.ParentEdge()
+		if ek == edge.Invalid {
+			return // File
+		}
+		visit(c.Parent())
+		fmt.Fprintf(&buf, ".%s", ek.FieldName())
+		if idx >= 0 {
+			fmt.Fprintf(&buf, "[%d]", idx)
+		}
+		ftype := ek.FieldType()
+		if idx >= 0 {
+			ftype = ftype.Elem() // []T -> T
+		}
+		if ftype.Kind() == reflect.Interface {
+			fmt.Fprintf(&buf, ".(%T)", c.Node())
+		}
+	}
+	visit(c)
+	return buf.String()
+}
+
 // indices return the [start, end) half-open interval of event indices.
 func (c Cursor) indices() (int32, int32) {
 	if c.index < 0 {
diff --git a/go/ast/inspector/cursor_test.go b/go/ast/inspector/cursor_test.go
index c216492..42ba57b 100644
--- a/go/ast/inspector/cursor_test.go
+++ b/go/ast/inspector/cursor_test.go
@@ -542,6 +542,33 @@
 	}
 }
 
+func TestCursor_GoString(t *testing.T) {
+	const src = `package a
+func f()
+func g() {
+	print("goodbye")
+}
+`
+	fset := token.NewFileSet()
+	file, _ := parser.ParseFile(fset, "a.go", src, 0)
+	root := inspector.New([]*ast.File{file}).Root()
+
+	// Find sole (string) literal.
+	var curLit inspector.Cursor
+	for cur := range root.Preorder((*ast.BasicLit)(nil)) {
+		curLit = cur
+	}
+	got := curLit.GoString()
+	want := "File.Decls[1].(*ast.FuncDecl).Body.List[0].(*ast.ExprStmt).X.(*ast.CallExpr).Args[0].(*ast.BasicLit)"
+	if got != want {
+		t.Errorf("GoString = %q, want %s", got, want)
+	}
+
+	if file.Decls[1].(*ast.FuncDecl).Body.List[0].(*ast.ExprStmt).X.(*ast.CallExpr).Args[0].(*ast.BasicLit) != curLit.Node() {
+		t.Errorf("GoString path was inaccurate")
+	}
+}
+
 func is[T any](x any) bool {
 	_, ok := x.(T)
 	return ok
diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go
index f48394c..a682dc2 100644
--- a/go/types/internal/play/play.go
+++ b/go/types/internal/play/play.go
@@ -169,10 +169,10 @@
 	// cases (e.g. around FuncType.Func).
 	curFile, _ := inspector.New([]*ast.File{file}).Root().FirstChild()
 	if cur, ok := curFile.FindByPos(startPos, endPos); ok {
-		fmt.Fprintf(out, "Cursor.FindByPos().Enclosing() = %v\n",
-			slices.Collect(cur.Enclosing()))
+		fmt.Fprintf(out, "Cursor.GoString = %#v\n", cur)
+		fmt.Fprintf(out, "Cursor.Enclosing = %v\n", slices.Collect(cur.Enclosing()))
 	} else {
-		fmt.Fprintf(out, "Cursor.FindPos() failed\n")
+		fmt.Fprintf(out, "Cursor.FindPos failed\n")
 	}
 	// And show the astutil.Select result (enclosing, leftmost & rightmost enclosed).
 	if curEnclosing, curStart, curEnd, err := astutil.Select(curFile, startPos, endPos); err == nil {