Skip to content

Commit 72b42f2

Browse files
adonovangopherbot
authored andcommitted
go/analysis/passes/inline: add lazy-fix mode for gopls
Computation of inliner fixes can be expensive due to its (unnecessary) strategy of formatting and parsing the file repeatedly. Rather than fix that right now, this CL makes the computation of fixes lazy when the analyzer is running within gopls. An undocumented analyzer flag, set by gopls' init, causes the analyzer to return an empty list of edits, which triggers gopls to compute the edits lazily based on the Diagnostic.Category string. As luck would have it, we already have a lazy fix in place for the refactor.inline.call Code Action, so we simply use that name again for the Category. The change broke an MCP test that assumed that the inline analyzer's fixes carry edits. There is a TODO in the MCP code not to rely on this assumption, but rather than do it, I rewrote the test to use the printf analyzer instead. For golang/go#75773 Change-Id: I5388e472e841cb23351fb9e90d8d7721dae3df07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/721180 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>
1 parent 53f4100 commit 72b42f2

File tree

5 files changed

+132
-98
lines changed

5 files changed

+132
-98
lines changed

go/analysis/passes/inline/inline.go

Lines changed: 83 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,16 @@ var Analyzer = &analysis.Analyzer{
5151
},
5252
}
5353

54-
var allowBindingDecl bool
54+
var (
55+
allowBindingDecl bool
56+
lazyEdits bool
57+
)
5558

5659
func init() {
5760
Analyzer.Flags.BoolVar(&allowBindingDecl, "allow_binding_decl", false,
5861
"permit inlinings that require a 'var params = args' declaration")
62+
Analyzer.Flags.BoolVar(&lazyEdits, "lazy_edits", false,
63+
"compute edits lazily (only meaningful to gopls driver)")
5964
}
6065

6166
// analyzer holds the state for this analysis.
@@ -177,66 +182,88 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) {
177182
return // don't inline a function from within its own test
178183
}
179184

180-
// Inline the call.
181-
content, err := a.readFile(call)
182-
if err != nil {
183-
a.pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err)
184-
return
185-
}
186-
curFile := astutil.EnclosingFile(cur)
187-
caller := &inline.Caller{
188-
Fset: a.pass.Fset,
189-
Types: a.pass.Pkg,
190-
Info: a.pass.TypesInfo,
191-
File: curFile,
192-
Call: call,
193-
Content: content,
194-
CountUses: func(pkgname *types.PkgName) int {
195-
return moreiters.Len(a.index.Uses(pkgname))
196-
},
197-
}
198-
res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard})
199-
if err != nil {
200-
a.pass.Reportf(call.Lparen, "%v", err)
201-
return
202-
}
185+
// Compute the edits.
186+
//
187+
// Ordinarily the analyzer reports a fix containing
188+
// edits. However, the algorithm is somewhat expensive
189+
// (unnecessarily so: see go.dev/issue/75773) so
190+
// to reduce costs in gopls, we omit the edits,
191+
// meaning that gopls must compute them on demand
192+
// (based on the Diagnostic.Category) when they are
193+
// requested via a code action.
194+
//
195+
// This does mean that the following categories of
196+
// caller-dependent obstacles to inlining will be
197+
// reported when the gopls user requests the fix,
198+
// rather than by quietly suppressing the diagnostic:
199+
// - shadowing problems
200+
// - callee imports inaccessible "internal" packages
201+
// - callee refers to nonexported symbols
202+
// - callee uses too-new Go features
203+
// - inlining call from a cgo file
204+
var edits []analysis.TextEdit
205+
if !lazyEdits {
206+
// Inline the call.
207+
content, err := a.readFile(call)
208+
if err != nil {
209+
a.pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err)
210+
return
211+
}
212+
curFile := astutil.EnclosingFile(cur)
213+
caller := &inline.Caller{
214+
Fset: a.pass.Fset,
215+
Types: a.pass.Pkg,
216+
Info: a.pass.TypesInfo,
217+
File: curFile,
218+
Call: call,
219+
Content: content,
220+
CountUses: func(pkgname *types.PkgName) int {
221+
return moreiters.Len(a.index.Uses(pkgname))
222+
},
223+
}
224+
res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard})
225+
if err != nil {
226+
a.pass.Reportf(call.Lparen, "%v", err)
227+
return
228+
}
203229

204-
if res.Literalized {
205-
// Users are not fond of inlinings that literalize
206-
// f(x) to func() { ... }(), so avoid them.
207-
//
208-
// (Unfortunately the inliner is very timid,
209-
// and often literalizes when it cannot prove that
210-
// reducing the call is safe; the user of this tool
211-
// has no indication of what the problem is.)
212-
return
213-
}
214-
if res.BindingDecl && !allowBindingDecl {
215-
// When applying fix en masse, users are similarly
216-
// unenthusiastic about inlinings that cannot
217-
// entirely eliminate the parameters and
218-
// insert a 'var params = args' declaration.
219-
// The flag allows them to decline such fixes.
220-
return
221-
}
222-
got := res.Content
223-
224-
// Suggest the "fix".
225-
var textEdits []analysis.TextEdit
226-
for _, edit := range diff.Bytes(content, got) {
227-
textEdits = append(textEdits, analysis.TextEdit{
228-
Pos: curFile.FileStart + token.Pos(edit.Start),
229-
End: curFile.FileStart + token.Pos(edit.End),
230-
NewText: []byte(edit.New),
231-
})
230+
if res.Literalized {
231+
// Users are not fond of inlinings that literalize
232+
// f(x) to func() { ... }(), so avoid them.
233+
//
234+
// (Unfortunately the inliner is very timid,
235+
// and often literalizes when it cannot prove that
236+
// reducing the call is safe; the user of this tool
237+
// has no indication of what the problem is.)
238+
return
239+
}
240+
if res.BindingDecl && !allowBindingDecl {
241+
// When applying fix en masse, users are similarly
242+
// unenthusiastic about inlinings that cannot
243+
// entirely eliminate the parameters and
244+
// insert a 'var params = args' declaration.
245+
// The flag allows them to decline such fixes.
246+
return
247+
}
248+
got := res.Content
249+
250+
for _, edit := range diff.Bytes(content, got) {
251+
edits = append(edits, analysis.TextEdit{
252+
Pos: curFile.FileStart + token.Pos(edit.Start),
253+
End: curFile.FileStart + token.Pos(edit.End),
254+
NewText: []byte(edit.New),
255+
})
256+
}
232257
}
258+
233259
a.pass.Report(analysis.Diagnostic{
234-
Pos: call.Pos(),
235-
End: call.End(),
236-
Message: fmt.Sprintf("Call of %v should be inlined", callee),
260+
Pos: call.Pos(),
261+
End: call.End(),
262+
Message: fmt.Sprintf("Call of %v should be inlined", callee),
263+
Category: "inline_call", // keep consistent with gopls/internal/golang.fixInlineCall
237264
SuggestedFixes: []analysis.SuggestedFix{{
238265
Message: fmt.Sprintf("Inline call of %v", callee),
239-
TextEdits: textEdits,
266+
TextEdits: edits, // within gopls, this is nil => compute fix's edits lazily
240267
}},
241268
})
242269
}

gopls/internal/golang/fix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const (
5454
fixExtractVariableAll = "extract_variable_all"
5555
fixExtractFunction = "extract_function"
5656
fixExtractMethod = "extract_method"
57-
fixInlineCall = "inline_call"
57+
fixInlineCall = "inline_call" // keep consistent with go/analysis/passes/inline Diagnostic.Category
5858
fixInlineVariable = "inline_variable"
5959
fixInvertIfCondition = "invert_if_condition"
6060
fixSplitLines = "split_lines"

gopls/internal/settings/analysis.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package settings
66

77
import (
8+
"log"
89
"slices"
910

1011
"golang.org/x/tools/go/analysis"
@@ -241,7 +242,7 @@ var DefaultAnalyzers = []*Analyzer{
241242
severity: protocol.SeverityInformation,
242243
},
243244
// other simplifiers
244-
{analyzer: inline.Analyzer, severity: protocol.SeverityHint},
245+
{analyzer: inline.Analyzer, severity: protocol.SeverityHint}, // (in -lazy_edit mode)
245246
{analyzer: infertypeargs.Analyzer, severity: protocol.SeverityInformation},
246247
{analyzer: maprange.Analyzer, severity: protocol.SeverityHint},
247248
{analyzer: unusedparams.Analyzer, severity: protocol.SeverityInformation},
@@ -281,3 +282,9 @@ var DefaultAnalyzers = []*Analyzer{
281282
{analyzer: noresultvalues.Analyzer},
282283
{analyzer: unusedvariable.Analyzer},
283284
}
285+
286+
func init() {
287+
if err := inline.Analyzer.Flags.Set("lazy_edits", "true"); err != nil {
288+
log.Fatalf("setting inline -lazy_edits flag: %v", err)
289+
}
290+
}
Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
This test exercises the "go_file_diagnostics" MCP tool.
22

3+
Some diagnostics from the "printf" analyzer carry fixes, and those fixes have edits.
4+
(Not all fixes have edits; some analyzers rely on gopls to compute them lazily.)
5+
6+
The printf analyzer only reports a fix in files using >= go1.24.
7+
38
-- flags --
49
-mcp
10+
-min_go_command=go1.24
511

612
-- settings.json --
713
{
@@ -12,6 +18,7 @@ This test exercises the "go_file_diagnostics" MCP tool.
1218

1319
-- go.mod --
1420
module example.com
21+
go 1.24
1522

1623
-- a/main.go --
1724
package main
@@ -38,52 +45,33 @@ Fix:
3845
-- b/main.go --
3946
package main
4047

41-
func _() {
42-
_ = deprecated([]string{"a"}, "a") //@loc(inline, "deprecated")
43-
44-
_ = deprecated([]string{"a"}, "a") //@loc(inline2, "deprecated")
45-
}
48+
import "fmt"
4649

47-
//go:fix inline
48-
func deprecated(slice []string, s string) bool {
49-
return proposed(slice, s, true)
50-
}
51-
52-
func proposed(_ []string, _ string, _ bool) bool {
53-
return false // fake
54-
}
50+
// (diagnostic with fix)
51+
var (
52+
msg string
53+
_ = fmt.Sprintf(msg) //@ diag("msg", re"non-constant format string")
54+
)
5555

56-
//@mcptool("go_file_diagnostics", `{"file":"$WORKDIR/b/main.go"}`, output=diagnoseInline)
57-
//@diag(inline, re"inline")
58-
//@diag(inline2, re"inline")
59-
-- @diagnoseInline --
60-
3:5-3:35: [Hint] Call of main.deprecated should be inlined
61-
Fix:
62-
--- $WORKDIR/b/main.go
63-
+++ $WORKDIR/b/main.go
64-
@@ -1,7 +1,7 @@
65-
package main
66-
67-
func _() {
68-
- _ = deprecated([]string{"a"}, "a") //@loc(inline, "deprecated")
69-
+ _ = proposed([]string{"a"}, "a", true) //@loc(inline, "deprecated")
70-
71-
_ = deprecated([]string{"a"}, "a") //@loc(inline2, "deprecated")
72-
}
56+
// (diagnostic without fix)
57+
var _ = fmt.Sprintf("%d", "hello") //@ diag("%d", re"%d .* wrong type string")
7358

59+
//@ mcptool("go_file_diagnostics", `{"file":"$WORKDIR/b/main.go"}`, output=diagnosePrintf)
7460

75-
5:5-5:35: [Hint] Call of main.deprecated should be inlined
61+
-- @diagnosePrintf --
62+
7:17-7:20: [Warning] non-constant format string in call to fmt.Sprintf
7663
Fix:
7764
--- $WORKDIR/b/main.go
7865
+++ $WORKDIR/b/main.go
79-
@@ -3,7 +3,7 @@
80-
func _() {
81-
_ = deprecated([]string{"a"}, "a") //@loc(inline, "deprecated")
82-
83-
- _ = deprecated([]string{"a"}, "a") //@loc(inline2, "deprecated")
84-
+ _ = proposed([]string{"a"}, "a", true) //@loc(inline2, "deprecated")
85-
}
66+
@@ -5,7 +5,7 @@
67+
// (diagnostic with fix)
68+
var (
69+
msg string
70+
- _ = fmt.Sprintf(msg) //@ diag("msg", re"non-constant format string")
71+
+ _ = fmt.Sprintf("%s", msg) //@ diag("msg", re"non-constant format string")
72+
)
8673

87-
//go:fix inline
74+
// (diagnostic without fix)
8875

8976

77+
11:21-11:23: [Warning] fmt.Sprintf format %d has arg "hello" of wrong type string

internal/refactor/inline/inline.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type Caller struct {
4040
Info *types.Info
4141
File *ast.File
4242
Call *ast.CallExpr
43-
Content []byte // source of file containing
43+
Content []byte // source of file containing (TODO(adonovan): see comment at Result.Content)
4444

4545
// CountUses is an optional optimized computation of
4646
// the number of times pkgname appears in Info.Uses.
@@ -61,6 +61,18 @@ type Options struct {
6161

6262
// Result holds the result of code transformation.
6363
type Result struct {
64+
// TODO(adonovan): the only textual results that should be
65+
// needed are (1) an edit in the vicinity of the call (either
66+
// to the CallExpr or one of its ancestors), and optionally
67+
// (2) an edit to the import declaration.
68+
// Change the inliner API to return a list of edits,
69+
// and not to accept a Caller.Content, as it is only
70+
// temptation to use such algorithmically expensive
71+
// operations as reformatting the entire file, which is
72+
// a significant source of non-linear dynamic behavior;
73+
// see https://go.dev/issue/75773.
74+
// This will require a sequence of changes to the tests
75+
// and the inliner algorithm itself.
6476
Content []byte // formatted, transformed content of caller file
6577
Literalized bool // chosen strategy replaced callee() with func(){...}()
6678
BindingDecl bool // transformation added "var params = args" declaration

0 commit comments

Comments
 (0)