gopls/internal/golang: simplify "rewrite" code actions

Two of these actions used the Inspector for AST traversal,
even though in typical use they inspect only a small selection,
and at most twice, which is not enough to break even.
(This should be a slight performance improvement too.)

Also, unexport various functions.

(More poking around trying to enumerate gopls' feature set...)

Change-Id: Ia08d79329c84c750e525074dad4c0e2e029b9557
Reviewed-on: https://go-review.googlesource.com/c/tools/+/582938
Auto-Submit: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go
index fd8d04e..55f2cec 100644
--- a/gopls/internal/analysis/fillstruct/fillstruct.go
+++ b/gopls/internal/analysis/fillstruct/fillstruct.go
@@ -24,7 +24,6 @@
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/ast/astutil"
-	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
 	"golang.org/x/tools/internal/aliases"
 	"golang.org/x/tools/internal/analysisinternal"
@@ -33,32 +32,35 @@
 )
 
 // Diagnose computes diagnostics for fillable struct literals overlapping with
-// the provided start and end position.
+// the provided start and end position of file f.
 //
 // The diagnostic contains a lazy fix; the actual patch is computed
 // (via the ApplyFix command) by a call to [SuggestedFix].
 //
-// If either start or end is invalid, the entire package is inspected.
-func Diagnose(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic {
+// If either start or end is invalid, the entire file is inspected.
+func Diagnose(f *ast.File, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic {
 	var diags []analysis.Diagnostic
-	nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)}
-	inspect.Preorder(nodeFilter, func(n ast.Node) {
-		expr := n.(*ast.CompositeLit)
-
-		if (start.IsValid() && expr.End() < start) || (end.IsValid() && expr.Pos() > end) {
-			return // non-overlapping
+	ast.Inspect(f, func(n ast.Node) bool {
+		if n == nil {
+			return true // pop
 		}
-
+		if start.IsValid() && n.End() < start || end.IsValid() && n.Pos() > end {
+			return false // skip non-overlapping subtree
+		}
+		expr, ok := n.(*ast.CompositeLit)
+		if !ok {
+			return true
+		}
 		typ := info.TypeOf(expr)
 		if typ == nil {
-			return
+			return true
 		}
 
 		// Find reference to the type declaration of the struct being initialized.
 		typ = typeparams.Deref(typ)
 		tStruct, ok := typeparams.CoreType(typ).(*types.Struct)
 		if !ok {
-			return
+			return true
 		}
 		// Inv: typ is the possibly-named struct type.
 
@@ -66,7 +68,7 @@
 
 		// Skip any struct that is already populated or that has no fields.
 		if fieldCount == 0 || fieldCount == len(expr.Elts) {
-			return
+			return true
 		}
 
 		// Are any fields in need of filling?
@@ -80,7 +82,7 @@
 			fillableFields = append(fillableFields, fmt.Sprintf("%s: %s", field.Name(), field.Type().String()))
 		}
 		if len(fillableFields) == 0 {
-			return
+			return true
 		}
 
 		// Derive a name for the struct type.
@@ -116,6 +118,7 @@
 				// No TextEdits => computed later by gopls.
 			}},
 		})
+		return true
 	})
 
 	return diags
diff --git a/gopls/internal/analysis/fillstruct/fillstruct_test.go b/gopls/internal/analysis/fillstruct/fillstruct_test.go
index f90998f..e0ad83d 100644
--- a/gopls/internal/analysis/fillstruct/fillstruct_test.go
+++ b/gopls/internal/analysis/fillstruct/fillstruct_test.go
@@ -10,21 +10,19 @@
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/analysistest"
-	"golang.org/x/tools/go/analysis/passes/inspect"
-	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
 )
 
 // analyzer allows us to test the fillstruct code action using the analysistest
 // harness. (fillstruct used to be a gopls analyzer.)
 var analyzer = &analysis.Analyzer{
-	Name:     "fillstruct",
-	Doc:      "test only",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Name: "fillstruct",
+	Doc:  "test only",
 	Run: func(pass *analysis.Pass) (any, error) {
-		inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
-		for _, d := range fillstruct.Diagnose(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) {
-			pass.Report(d)
+		for _, f := range pass.Files {
+			for _, diag := range fillstruct.Diagnose(f, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) {
+				pass.Report(diag)
+			}
 		}
 		return nil, nil
 	},
diff --git a/gopls/internal/analysis/fillswitch/fillswitch.go b/gopls/internal/analysis/fillswitch/fillswitch.go
index b93ade0..12f116e 100644
--- a/gopls/internal/analysis/fillswitch/fillswitch.go
+++ b/gopls/internal/analysis/fillswitch/fillswitch.go
@@ -12,22 +12,22 @@
 	"go/types"
 
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/go/ast/inspector"
 )
 
 // Diagnose computes diagnostics for switch statements with missing cases
-// overlapping with the provided start and end position.
+// overlapping with the provided start and end position of file f.
 //
-// If either start or end is invalid, the entire package is inspected.
-func Diagnose(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic {
+// If either start or end is invalid, the entire file is inspected.
+func Diagnose(f *ast.File, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic {
 	var diags []analysis.Diagnostic
-	nodeFilter := []ast.Node{(*ast.SwitchStmt)(nil), (*ast.TypeSwitchStmt)(nil)}
-	inspect.Preorder(nodeFilter, func(n ast.Node) {
+	ast.Inspect(f, func(n ast.Node) bool {
+		if n == nil {
+			return true // pop
+		}
 		if start.IsValid() && n.End() < start ||
 			end.IsValid() && n.Pos() > end {
-			return // non-overlapping
+			return false // skip non-overlapping subtree
 		}
-
 		var fix *analysis.SuggestedFix
 		switch n := n.(type) {
 		case *ast.SwitchStmt:
@@ -35,17 +35,15 @@
 		case *ast.TypeSwitchStmt:
 			fix = suggestedFixTypeSwitch(n, pkg, info)
 		}
-
-		if fix == nil {
-			return
+		if fix != nil {
+			diags = append(diags, analysis.Diagnostic{
+				Message:        fix.Message,
+				Pos:            n.Pos(),
+				End:            n.Pos() + token.Pos(len("switch")),
+				SuggestedFixes: []analysis.SuggestedFix{*fix},
+			})
 		}
-
-		diags = append(diags, analysis.Diagnostic{
-			Message:        fix.Message,
-			Pos:            n.Pos(),
-			End:            n.Pos() + token.Pos(len("switch")),
-			SuggestedFixes: []analysis.SuggestedFix{*fix},
-		})
+		return true
 	})
 
 	return diags
diff --git a/gopls/internal/analysis/fillswitch/fillswitch_test.go b/gopls/internal/analysis/fillswitch/fillswitch_test.go
index 15d3ef1..bf70aa3 100644
--- a/gopls/internal/analysis/fillswitch/fillswitch_test.go
+++ b/gopls/internal/analysis/fillswitch/fillswitch_test.go
@@ -10,21 +10,19 @@
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/analysistest"
-	"golang.org/x/tools/go/analysis/passes/inspect"
-	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/analysis/fillswitch"
 )
 
 // analyzer allows us to test the fillswitch code action using the analysistest
 // harness.
 var analyzer = &analysis.Analyzer{
-	Name:     "fillswitch",
-	Doc:      "test only",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Name: "fillswitch",
+	Doc:  "test only",
 	Run: func(pass *analysis.Pass) (any, error) {
-		inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
-		for _, d := range fillswitch.Diagnose(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) {
-			pass.Report(d)
+		for _, f := range pass.Files {
+			for _, diag := range fillswitch.Diagnose(f, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) {
+				pass.Report(diag)
+			}
 		}
 		return nil, nil
 	},
diff --git a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go
index 919b935..761a836 100644
--- a/gopls/internal/golang/change_quote.go
+++ b/gopls/internal/golang/change_quote.go
@@ -19,17 +19,13 @@
 	"golang.org/x/tools/internal/diff"
 )
 
-// ConvertStringLiteral reports whether we can convert between raw and interpreted
-// string literals in the [start, end), along with a CodeAction containing the edits.
+// convertStringLiteral reports whether we can convert between raw and interpreted
+// string literals in the [start, end) range, along with a CodeAction containing the edits.
 //
 // Only the following conditions are true, the action in result is valid
 //   - [start, end) is enclosed by a string literal
 //   - if the string is interpreted string, need check whether the convert is allowed
-func ConvertStringLiteral(pgf *parsego.File, fh file.Handle, rng protocol.Range) (protocol.CodeAction, bool) {
-	startPos, endPos, err := pgf.RangePos(rng)
-	if err != nil {
-		return protocol.CodeAction{}, false // e.g. invalid range
-	}
+func convertStringLiteral(pgf *parsego.File, fh file.Handle, startPos, endPos token.Pos) (protocol.CodeAction, bool) {
 	path, _ := astutil.PathEnclosingInterval(pgf.File, startPos, endPos)
 	lit, ok := path[0].(*ast.BasicLit)
 	if !ok || lit.Kind != token.STRING {
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index eb4e28b..29ae363 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -11,7 +11,6 @@
 	"go/ast"
 	"strings"
 
-	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
 	"golang.org/x/tools/gopls/internal/analysis/fillswitch"
 	"golang.org/x/tools/gopls/internal/cache"
@@ -299,17 +298,17 @@
 		actions = append(actions, newCodeAction("Refactor: remove unused parameter", protocol.RefactorRewrite, &cmd, nil, options))
 	}
 
-	if action, ok := ConvertStringLiteral(pgf, fh, rng); ok {
-		actions = append(actions, action)
-	}
-
 	start, end, err := pgf.RangePos(rng)
 	if err != nil {
 		return nil, err
 	}
 
+	if action, ok := convertStringLiteral(pgf, fh, start, end); ok {
+		actions = append(actions, action)
+	}
+
 	var commands []protocol.Command
-	if _, ok, _ := CanInvertIfCondition(pgf.File, start, end); ok {
+	if _, ok, _ := canInvertIfCondition(pgf.File, start, end); ok {
 		cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{
 			Fix:          fixInvertIfCondition,
 			URI:          pgf.URI,
@@ -322,7 +321,7 @@
 		commands = append(commands, cmd)
 	}
 
-	if msg, ok, _ := CanSplitLines(pgf.File, pkg.FileSet(), start, end); ok {
+	if msg, ok, _ := canSplitLines(pgf.File, pkg.FileSet(), start, end); ok {
 		cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{
 			Fix:          fixSplitLines,
 			URI:          pgf.URI,
@@ -335,7 +334,7 @@
 		commands = append(commands, cmd)
 	}
 
-	if msg, ok, _ := CanJoinLines(pgf.File, pkg.FileSet(), start, end); ok {
+	if msg, ok, _ := canJoinLines(pgf.File, pkg.FileSet(), start, end); ok {
 		cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{
 			Fix:          fixJoinLines,
 			URI:          pgf.URI,
@@ -348,12 +347,10 @@
 		commands = append(commands, cmd)
 	}
 
-	// N.B.: an inspector only pays for itself after ~5 passes, which means we're
-	// currently not getting a good deal on this inspection.
-	//
-	// TODO: Consider removing the inspection after convenienceAnalyzers are removed.
-	inspect := inspector.New([]*ast.File{pgf.File})
-	for _, diag := range fillstruct.Diagnose(inspect, start, end, pkg.Types(), pkg.TypesInfo()) {
+	// fillstruct.Diagnose is a lazy analyzer: all it gives us is
+	// the (start, end, message) of each SuggestedFix; the actual
+	// edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix.
+	for _, diag := range fillstruct.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) {
 		rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End)
 		if err != nil {
 			return nil, err
@@ -372,7 +369,7 @@
 		}
 	}
 
-	for _, diag := range fillswitch.Diagnose(inspect, start, end, pkg.Types(), pkg.TypesInfo()) {
+	for _, diag := range fillswitch.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) {
 		edits, err := suggestedFixToEdits(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0])
 		if err != nil {
 			return nil, err
diff --git a/gopls/internal/golang/invertifcondition.go b/gopls/internal/golang/invertifcondition.go
index 377e1ce..16eaaa3 100644
--- a/gopls/internal/golang/invertifcondition.go
+++ b/gopls/internal/golang/invertifcondition.go
@@ -18,7 +18,7 @@
 
 // invertIfCondition is a singleFileFixFunc that inverts an if/else statement
 func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
-	ifStatement, _, err := CanInvertIfCondition(file, start, end)
+	ifStatement, _, err := canInvertIfCondition(file, start, end)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -239,9 +239,9 @@
 	return []byte(string(invertedBefore) + string(whitespaceAfterBefore) + newOpWithTrailingWhitespace + string(invertedAfter)), nil
 }
 
-// CanInvertIfCondition reports whether we can do invert-if-condition on the
+// canInvertIfCondition reports whether we can do invert-if-condition on the
 // code in the given range
-func CanInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) {
+func canInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) {
 	path, _ := astutil.PathEnclosingInterval(file, start, end)
 	for _, node := range path {
 		stmt, isIfStatement := node.(*ast.IfStmt)
diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go
index 1c4b562..761fee9 100644
--- a/gopls/internal/golang/lines.go
+++ b/gopls/internal/golang/lines.go
@@ -22,9 +22,9 @@
 	"golang.org/x/tools/gopls/internal/util/slices"
 )
 
-// CanSplitLines checks whether we can split lists of elements inside an enclosing curly bracket/parens into separate
-// lines.
-func CanSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
+// canSplitLines checks whether we can split lists of elements inside
+// an enclosing curly bracket/parens into separate lines.
+func canSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
 	itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end)
 	if itemType == "" {
 		return "", false, nil
@@ -45,8 +45,9 @@
 	return "", false, nil
 }
 
-// CanJoinLines checks whether we can join lists of elements inside an enclosing curly bracket/parens into a single line.
-func CanJoinLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
+// canJoinLines checks whether we can join lists of elements inside an
+// enclosing curly bracket/parens into a single line.
+func canJoinLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
 	itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end)
 	if itemType == "" {
 		return "", false, nil