Skip to content

Commit d405635

Browse files
committed
internal/refactor/inline/analyzer: use "//go:fix inline" annotation
This CL changes the proof-of-concept "inliner" analyzer to use "//go:fix inline" as the marker of a function that should be inlined. This seems to be the syntax emerging from the proposal golang/go#32816. Also, skip inlinings that cause literalization of the callee. Users of batch tools only want to see reductions. Updates golang/go#32816 Updates golang/go#70717 Change-Id: I6d70118f69b7c35f51e1d51863a0312b5dcc3b63 Reviewed-on: https://go-review.googlesource.com/c/tools/+/634919 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 47df42f commit d405635

File tree

4 files changed

+141
-45
lines changed

4 files changed

+141
-45
lines changed

internal/refactor/inline/analyzer/analyzer.go

+43-37
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import (
99
"go/ast"
1010
"go/token"
1111
"go/types"
12-
"os"
13-
"strings"
12+
"slices"
1413

1514
"golang.org/x/tools/go/analysis"
1615
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -20,28 +19,26 @@ import (
2019
"golang.org/x/tools/internal/refactor/inline"
2120
)
2221

23-
const Doc = `inline calls to functions with "inlineme" doc comment`
22+
const Doc = `inline calls to functions with "//go:fix inline" doc comment`
2423

2524
var Analyzer = &analysis.Analyzer{
2625
Name: "inline",
2726
Doc: Doc,
2827
URL: "https://pkg.go.dev/golang.org/x/tools/internal/refactor/inline/analyzer",
2928
Run: run,
30-
FactTypes: []analysis.Fact{new(inlineMeFact)},
29+
FactTypes: []analysis.Fact{new(goFixInlineFact)},
3130
Requires: []*analysis.Analyzer{inspect.Analyzer},
3231
}
3332

34-
func run(pass *analysis.Pass) (interface{}, error) {
33+
func run(pass *analysis.Pass) (any, error) {
3534
// Memoize repeated calls for same file.
36-
// TODO(adonovan): the analysis.Pass should abstract this (#62292)
37-
// as the driver may not be reading directly from the file system.
3835
fileContent := make(map[string][]byte)
3936
readFile := func(node ast.Node) ([]byte, error) {
4037
filename := pass.Fset.File(node.Pos()).Name()
4138
content, ok := fileContent[filename]
4239
if !ok {
4340
var err error
44-
content, err = os.ReadFile(filename)
41+
content, err = pass.ReadFile(filename)
4542
if err != nil {
4643
return nil, err
4744
}
@@ -50,40 +47,37 @@ func run(pass *analysis.Pass) (interface{}, error) {
5047
return content, nil
5148
}
5249

53-
// Pass 1: find functions annotated with an "inlineme"
54-
// comment, and export a fact for each one.
50+
// Pass 1: find functions annotated with a "//go:fix inline"
51+
// comment (the syntax proposed by #32816),
52+
// and export a fact for each one.
5553
inlinable := make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact)
5654
for _, file := range pass.Files {
5755
for _, decl := range file.Decls {
58-
if decl, ok := decl.(*ast.FuncDecl); ok {
59-
// TODO(adonovan): this is just a placeholder.
60-
// Use the precise go:fix syntax in the proposal.
61-
// Beware that //go: comments are treated specially
62-
// by (*ast.CommentGroup).Text().
63-
// TODO(adonovan): alternatively, consider using
64-
// the universal annotation mechanism sketched in
65-
// https://go.dev/cl/489835 (which doesn't yet have
66-
// a proper proposal).
67-
if strings.Contains(decl.Doc.Text(), "inlineme") {
68-
content, err := readFile(file)
69-
if err != nil {
70-
pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err)
71-
continue
72-
}
73-
callee, err := inline.AnalyzeCallee(discard, pass.Fset, pass.Pkg, pass.TypesInfo, decl, content)
74-
if err != nil {
75-
pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err)
76-
continue
77-
}
78-
fn := pass.TypesInfo.Defs[decl.Name].(*types.Func)
79-
pass.ExportObjectFact(fn, &inlineMeFact{callee})
80-
inlinable[fn] = callee
56+
if decl, ok := decl.(*ast.FuncDecl); ok &&
57+
slices.ContainsFunc(directives(decl.Doc), func(d *directive) bool {
58+
return d.Tool == "go" && d.Name == "fix" && d.Args == "inline"
59+
}) {
60+
61+
content, err := readFile(decl)
62+
if err != nil {
63+
pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err)
64+
continue
65+
}
66+
callee, err := inline.AnalyzeCallee(discard, pass.Fset, pass.Pkg, pass.TypesInfo, decl, content)
67+
if err != nil {
68+
pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err)
69+
continue
8170
}
71+
fn := pass.TypesInfo.Defs[decl.Name].(*types.Func)
72+
pass.ExportObjectFact(fn, &goFixInlineFact{callee})
73+
inlinable[fn] = callee
8274
}
8375
}
8476
}
8577

8678
// Pass 2. Inline each static call to an inlinable function.
79+
//
80+
// TODO(adonovan): handle multiple diffs that each add the same import.
8781
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
8882
nodeFilter := []ast.Node{
8983
(*ast.File)(nil),
@@ -100,7 +94,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
10094
// Inlinable?
10195
callee, ok := inlinable[fn]
10296
if !ok {
103-
var fact inlineMeFact
97+
var fact goFixInlineFact
10498
if pass.ImportObjectFact(fn, &fact) {
10599
callee = fact.Callee
106100
inlinable[fn] = callee
@@ -129,6 +123,16 @@ func run(pass *analysis.Pass) (interface{}, error) {
129123
pass.Reportf(call.Lparen, "%v", err)
130124
return
131125
}
126+
if res.Literalized {
127+
// Users are not fond of inlinings that literalize
128+
// f(x) to func() { ... }(), so avoid them.
129+
//
130+
// (Unfortunately the inliner is very timid,
131+
// and often literalizes when it cannot prove that
132+
// reducing the call is safe; the user of this tool
133+
// has no indication of what the problem is.)
134+
return
135+
}
132136
got := res.Content
133137

134138
// Suggest the "fix".
@@ -156,9 +160,11 @@ func run(pass *analysis.Pass) (interface{}, error) {
156160
return nil, nil
157161
}
158162

159-
type inlineMeFact struct{ Callee *inline.Callee }
163+
// A goFixInlineFact is exported for each function marked "//go:fix inline".
164+
// It holds information about the callee to support inlining.
165+
type goFixInlineFact struct{ Callee *inline.Callee }
160166

161-
func (f *inlineMeFact) String() string { return "inlineme " + f.Callee.String() }
162-
func (*inlineMeFact) AFact() {}
167+
func (f *goFixInlineFact) String() string { return "goFixInline " + f.Callee.String() }
168+
func (*goFixInlineFact) AFact() {}
163169

164170
func discard(string, ...any) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package analyzer
6+
7+
import (
8+
"go/ast"
9+
"go/token"
10+
"strings"
11+
)
12+
13+
// -- plundered from the future (CL 605517, issue #68021) --
14+
15+
// TODO(adonovan): replace with ast.Directive after go1.24 (#68021).
16+
17+
// A directive is a comment line with special meaning to the Go
18+
// toolchain or another tool. It has the form:
19+
//
20+
// //tool:name args
21+
//
22+
// The "tool:" portion is missing for the three directives named
23+
// line, extern, and export.
24+
//
25+
// See https://go.dev/doc/comment#Syntax for details of Go comment
26+
// syntax and https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
27+
// for details of directives used by the Go compiler.
28+
type directive struct {
29+
Pos token.Pos // of preceding "//"
30+
Tool string
31+
Name string
32+
Args string // may contain internal spaces
33+
}
34+
35+
// directives returns the directives within the comment.
36+
func directives(g *ast.CommentGroup) (res []*directive) {
37+
if g != nil {
38+
// Avoid (*ast.CommentGroup).Text() as it swallows directives.
39+
for _, c := range g.List {
40+
if len(c.Text) > 2 &&
41+
c.Text[1] == '/' &&
42+
c.Text[2] != ' ' &&
43+
isDirective(c.Text[2:]) {
44+
45+
tool, nameargs, ok := strings.Cut(c.Text[2:], ":")
46+
if !ok {
47+
// Must be one of {line,extern,export}.
48+
tool, nameargs = "", tool
49+
}
50+
name, args, _ := strings.Cut(nameargs, " ") // tab??
51+
res = append(res, &directive{
52+
Pos: c.Slash,
53+
Tool: tool,
54+
Name: name,
55+
Args: strings.TrimSpace(args),
56+
})
57+
}
58+
}
59+
}
60+
return
61+
}
62+
63+
// isDirective reports whether c is a comment directive.
64+
// This code is also in go/printer.
65+
func isDirective(c string) bool {
66+
// "//line " is a line directive.
67+
// "//extern " is for gccgo.
68+
// "//export " is for cgo.
69+
// (The // has been removed.)
70+
if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") {
71+
return true
72+
}
73+
74+
// "//[a-z0-9]+:[a-z0-9]"
75+
// (The // has been removed.)
76+
colon := strings.Index(c, ":")
77+
if colon <= 0 || colon+1 >= len(c) {
78+
return false
79+
}
80+
for i := 0; i <= colon+1; i++ {
81+
if i == colon {
82+
continue
83+
}
84+
b := c[i]
85+
if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') {
86+
return false
87+
}
88+
}
89+
return true
90+
}

internal/refactor/inline/analyzer/testdata/src/a/a.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ func f() {
88

99
type T struct{}
1010

11-
// inlineme
12-
func One() int { return one } // want One:`inlineme a.One`
11+
//go:fix inline
12+
func One() int { return one } // want One:`goFixInline a.One`
1313

1414
const one = 1
1515

16-
// inlineme
17-
func (T) Two() int { return 2 } // want Two:`inlineme \(a.T\).Two`
16+
//go:fix inline
17+
func (T) Two() int { return 2 } // want Two:`goFixInline \(a.T\).Two`

internal/refactor/inline/analyzer/testdata/src/a/a.go.golden

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ func f() {
88

99
type T struct{}
1010

11-
// inlineme
12-
func One() int { return one } // want One:`inlineme a.One`
11+
//go:fix inline
12+
func One() int { return one } // want One:`goFixInline a.One`
1313

1414
const one = 1
1515

16-
// inlineme
17-
func (T) Two() int { return 2 } // want Two:`inlineme \(a.T\).Two`
16+
//go:fix inline
17+
func (T) Two() int { return 2 } // want Two:`goFixInline \(a.T\).Two`

0 commit comments

Comments
 (0)