1
2
3
4
5 package inline
6
7 import (
8 "fmt"
9 "go/ast"
10 "go/types"
11 "slices"
12 "strings"
13
14 _ "embed"
15
16 "golang.org/x/tools/go/analysis"
17 "golang.org/x/tools/go/analysis/passes/inspect"
18 "golang.org/x/tools/go/analysis/passes/internal/gofixdirective"
19 "golang.org/x/tools/go/ast/edge"
20 "golang.org/x/tools/go/ast/inspector"
21 "golang.org/x/tools/go/types/typeutil"
22 "golang.org/x/tools/internal/analysis/analyzerutil"
23 typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
24 "golang.org/x/tools/internal/astutil"
25 "golang.org/x/tools/internal/moreiters"
26 "golang.org/x/tools/internal/packagepath"
27 "golang.org/x/tools/internal/refactor"
28 "golang.org/x/tools/internal/refactor/inline"
29 "golang.org/x/tools/internal/typesinternal"
30 "golang.org/x/tools/internal/typesinternal/typeindex"
31 )
32
33
34 var doc string
35
36 var Analyzer = &analysis.Analyzer{
37 Name: "inline",
38 Doc: analyzerutil.MustExtractDoc(doc, "inline"),
39 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/inline",
40 Run: run,
41 FactTypes: []analysis.Fact{
42 (*goFixInlineFuncFact)(nil),
43 (*goFixInlineConstFact)(nil),
44 (*goFixInlineAliasFact)(nil),
45 },
46 Requires: []*analysis.Analyzer{
47 inspect.Analyzer,
48 typeindexanalyzer.Analyzer,
49 },
50 }
51
52 var (
53 allowBindingDecl bool
54 lazyEdits bool
55 )
56
57 func init() {
58 Analyzer.Flags.BoolVar(&allowBindingDecl, "allow_binding_decl", false,
59 "permit inlinings that require a 'var params = args' declaration")
60 Analyzer.Flags.BoolVar(&lazyEdits, "lazy_edits", false,
61 "compute edits lazily (only meaningful to gopls driver)")
62 }
63
64
65 type analyzer struct {
66 pass *analysis.Pass
67 root inspector.Cursor
68 index *typeindex.Index
69
70 fileContent map[string][]byte
71
72 inlinableFuncs map[*types.Func]*inline.Callee
73 inlinableConsts map[*types.Const]*goFixInlineConstFact
74 inlinableAliases map[*types.TypeName]*goFixInlineAliasFact
75 }
76
77 func run(pass *analysis.Pass) (any, error) {
78 a := &analyzer{
79 pass: pass,
80 root: pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Root(),
81 index: pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index),
82 fileContent: make(map[string][]byte),
83 inlinableFuncs: make(map[*types.Func]*inline.Callee),
84 inlinableConsts: make(map[*types.Const]*goFixInlineConstFact),
85 inlinableAliases: make(map[*types.TypeName]*goFixInlineAliasFact),
86 }
87 gofixdirective.Find(pass, a.root, a)
88 a.inline()
89 return nil, nil
90 }
91
92
93 func (a *analyzer) HandleFunc(decl *ast.FuncDecl) {
94 content, err := a.readFile(decl)
95 if err != nil {
96 a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err)
97 return
98 }
99 callee, err := inline.AnalyzeCallee(discard, a.pass.Fset, a.pass.Pkg, a.pass.TypesInfo, decl, content)
100 if err != nil {
101 a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err)
102 return
103 }
104 fn := a.pass.TypesInfo.Defs[decl.Name].(*types.Func)
105 a.pass.ExportObjectFact(fn, &goFixInlineFuncFact{callee})
106 a.inlinableFuncs[fn] = callee
107 }
108
109
110 func (a *analyzer) HandleAlias(spec *ast.TypeSpec) {
111
112 typ := &goFixInlineAliasFact{}
113 lhs := a.pass.TypesInfo.Defs[spec.Name].(*types.TypeName)
114 a.inlinableAliases[lhs] = typ
115
116
117
118 if lhs.Exported() && typesinternal.IsPackageLevel(lhs) {
119 a.pass.ExportObjectFact(lhs, typ)
120 }
121 }
122
123
124 func (a *analyzer) HandleConst(nameIdent, rhsIdent *ast.Ident) {
125 lhs := a.pass.TypesInfo.Defs[nameIdent].(*types.Const)
126 rhs := a.pass.TypesInfo.Uses[rhsIdent].(*types.Const)
127 con := &goFixInlineConstFact{
128 RHSName: rhs.Name(),
129 RHSPkgName: rhs.Pkg().Name(),
130 RHSPkgPath: rhs.Pkg().Path(),
131 }
132 if rhs.Pkg() == a.pass.Pkg {
133 con.rhsObj = rhs
134 }
135 a.inlinableConsts[lhs] = con
136
137
138
139 if lhs.Exported() && typesinternal.IsPackageLevel(lhs) {
140 a.pass.ExportObjectFact(lhs, con)
141 }
142 }
143
144
145
146 func (a *analyzer) inline() {
147 for cur := range a.root.Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) {
148 switch n := cur.Node().(type) {
149 case *ast.CallExpr:
150 a.inlineCall(n, cur)
151
152 case *ast.Ident:
153 switch t := a.pass.TypesInfo.Uses[n].(type) {
154 case *types.TypeName:
155 a.inlineAlias(t, cur)
156 case *types.Const:
157 a.inlineConst(t, cur)
158 }
159 }
160 }
161 }
162
163
164 func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) {
165 if fn := typeutil.StaticCallee(a.pass.TypesInfo, call); fn != nil {
166
167 callee, ok := a.inlinableFuncs[fn]
168 if !ok {
169 var fact goFixInlineFuncFact
170 if a.pass.ImportObjectFact(fn, &fact) {
171 callee = fact.Callee
172 a.inlinableFuncs[fn] = callee
173 }
174 }
175 if callee == nil {
176 return
177 }
178
179 if a.withinTestOf(cur, fn) {
180 return
181 }
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202 var edits []analysis.TextEdit
203 if !lazyEdits {
204
205 caller := &inline.Caller{
206 Fset: a.pass.Fset,
207 Types: a.pass.Pkg,
208 Info: a.pass.TypesInfo,
209 File: astutil.EnclosingFile(cur),
210 Call: call,
211 CountUses: func(pkgname *types.PkgName) int {
212 return moreiters.Len(a.index.Uses(pkgname))
213 },
214 }
215 res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard})
216 if err != nil {
217 a.pass.Reportf(call.Lparen, "%v", err)
218 return
219 }
220
221 if res.Literalized {
222
223
224
225
226
227
228
229 return
230 }
231 if res.BindingDecl && !allowBindingDecl {
232
233
234
235
236
237 return
238 }
239 edits = res.Edits
240 }
241
242 a.pass.Report(analysis.Diagnostic{
243 Pos: call.Pos(),
244 End: call.End(),
245 Message: fmt.Sprintf("Call of %v should be inlined", callee),
246 Category: "inline_call",
247 SuggestedFixes: []analysis.SuggestedFix{{
248 Message: fmt.Sprintf("Inline call of %v", callee),
249 TextEdits: edits,
250 }},
251 })
252 }
253 }
254
255
256
257
258 func (a *analyzer) withinTestOf(cur inspector.Cursor, target *types.Func) bool {
259 curFuncDecl, ok := moreiters.First(cur.Enclosing((*ast.FuncDecl)(nil)))
260 if !ok {
261 return false
262 }
263 funcDecl := curFuncDecl.Node().(*ast.FuncDecl)
264 if funcDecl.Recv != nil {
265 return false
266 }
267 if strings.TrimSuffix(a.pass.Pkg.Path(), "_test") != target.Pkg().Path() {
268 return false
269 }
270 if !strings.HasSuffix(a.pass.Fset.File(funcDecl.Pos()).Name(), "_test.go") {
271 return false
272 }
273
274
275
276 symbol := target.Name()
277 if recv := target.Signature().Recv(); recv != nil {
278 _, named := typesinternal.ReceiverNamed(recv)
279 symbol = named.Obj().Name() + "_" + symbol
280 }
281
282
283 fname := funcDecl.Name.Name
284 for _, pre := range []string{"Test", "Example", "Bench"} {
285 if fname == pre+symbol || strings.HasPrefix(fname, pre+symbol+"_") {
286 return true
287 }
288 }
289
290 return false
291 }
292
293
294 func (a *analyzer) inlineAlias(tn *types.TypeName, curId inspector.Cursor) {
295 inalias, ok := a.inlinableAliases[tn]
296 if !ok {
297 var fact goFixInlineAliasFact
298 if a.pass.ImportObjectFact(tn, &fact) {
299 inalias = &fact
300 a.inlinableAliases[tn] = inalias
301 }
302 }
303 if inalias == nil {
304 return
305 }
306
307 alias := tn.Type().(*types.Alias)
308
309
310 typeParamNames := map[*types.TypeName]bool{}
311 for tp := range alias.TypeParams().TypeParams() {
312 typeParamNames[tp.Obj()] = true
313 }
314 rhs := alias.Rhs()
315 curPath := a.pass.Pkg.Path()
316 curFile := astutil.EnclosingFile(curId)
317 id := curId.Node().(*ast.Ident)
318
319
320
321
322
323
324 var (
325 importPrefixes = map[string]string{curPath: ""}
326 edits []analysis.TextEdit
327 )
328 for _, tn := range typenames(rhs) {
329
330 if typeParamNames[tn] {
331 continue
332 }
333 var pkgPath, pkgName string
334 if pkg := tn.Pkg(); pkg != nil {
335 pkgPath = pkg.Path()
336 pkgName = pkg.Name()
337 }
338 if pkgPath == "" || pkgPath == curPath {
339
340
341
342 scope := a.pass.TypesInfo.Scopes[curFile].Innermost(id.Pos())
343 _, obj := scope.LookupParent(tn.Name(), id.Pos())
344 if obj != tn {
345 return
346 }
347 } else if !packagepath.CanImport(a.pass.Pkg.Path(), pkgPath) {
348
349 return
350 } else if _, ok := importPrefixes[pkgPath]; !ok {
351
352
353 prefix, eds := refactor.AddImport(
354 a.pass.TypesInfo, curFile, pkgName, pkgPath, tn.Name(), id.Pos())
355 importPrefixes[pkgPath] = strings.TrimSuffix(prefix, ".")
356 edits = append(edits, eds...)
357 }
358 }
359
360
361
362
363
364
365
366 var expr ast.Expr = id
367 if astutil.IsChildOf(curId, edge.SelectorExpr_Sel) {
368 curId = curId.Parent()
369 expr = curId.Node().(ast.Expr)
370 }
371
372
373 switch ek, _ := curId.ParentEdge(); ek {
374 case edge.IndexExpr_X:
375 expr = curId.Parent().Node().(*ast.IndexExpr)
376 case edge.IndexListExpr_X:
377 expr = curId.Parent().Node().(*ast.IndexListExpr)
378 }
379 t := a.pass.TypesInfo.TypeOf(expr).(*types.Alias)
380 if targs := t.TypeArgs(); targs.Len() > 0 {
381
382
383
384
385
386 instAlias, _ := types.Instantiate(nil, alias, slices.Collect(targs.Types()), false)
387 rhs = instAlias.(*types.Alias).Rhs()
388 }
389
390
391 newText := types.TypeString(rhs, func(p *types.Package) string {
392 if p == a.pass.Pkg {
393 return ""
394 }
395 if prefix, ok := importPrefixes[p.Path()]; ok {
396 return prefix
397 }
398 panic(fmt.Sprintf("in %q, package path %q has no import prefix", rhs, p.Path()))
399 })
400 a.reportInline("type alias", "Type alias", expr, edits, newText)
401 }
402
403
404
405
406 func typenames(t types.Type) []*types.TypeName {
407 var tns []*types.TypeName
408
409 var visit func(types.Type)
410 visit = func(t types.Type) {
411 if hasName, ok := t.(interface{ Obj() *types.TypeName }); ok {
412 tns = append(tns, hasName.Obj())
413 }
414 switch t := t.(type) {
415 case *types.Basic:
416 tns = append(tns, types.Universe.Lookup(t.Name()).(*types.TypeName))
417 case *types.Named:
418 for t := range t.TypeArgs().Types() {
419 visit(t)
420 }
421 case *types.Alias:
422 for t := range t.TypeArgs().Types() {
423 visit(t)
424 }
425 case *types.TypeParam:
426 tns = append(tns, t.Obj())
427 case *types.Pointer:
428 visit(t.Elem())
429 case *types.Slice:
430 visit(t.Elem())
431 case *types.Array:
432 visit(t.Elem())
433 case *types.Chan:
434 visit(t.Elem())
435 case *types.Map:
436 visit(t.Key())
437 visit(t.Elem())
438 case *types.Struct:
439 for field := range t.Fields() {
440 visit(field.Type())
441 }
442 case *types.Signature:
443
444
445
446
447 if t.TypeParams() != nil {
448 panic("Signature.TypeParams in type expression")
449 }
450 visit(t.Params())
451 visit(t.Results())
452 case *types.Interface:
453 for etyp := range t.EmbeddedTypes() {
454 visit(etyp)
455 }
456 for method := range t.ExplicitMethods() {
457 visit(method.Type())
458 }
459 case *types.Tuple:
460 for v := range t.Variables() {
461 visit(v.Type())
462 }
463 case *types.Union:
464 panic("Union in type expression")
465 default:
466 panic(fmt.Sprintf("unknown type %T", t))
467 }
468 }
469
470 visit(t)
471
472 return tns
473 }
474
475
476 func (a *analyzer) inlineConst(con *types.Const, cur inspector.Cursor) {
477 incon, ok := a.inlinableConsts[con]
478 if !ok {
479 var fact goFixInlineConstFact
480 if a.pass.ImportObjectFact(con, &fact) {
481 incon = &fact
482 a.inlinableConsts[con] = incon
483 }
484 }
485 if incon == nil {
486 return
487 }
488
489
490 curFile := astutil.EnclosingFile(cur)
491 n := cur.Node().(*ast.Ident)
492
493
494
495
496
497
498
499
500
501
502 if a.pass.Pkg.Path() == incon.RHSPkgPath {
503
504 scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos())
505 _, obj := scope.LookupParent(incon.RHSName, n.Pos())
506 if obj == nil {
507
508
509 panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, incon.RHSName))
510 }
511 if obj != incon.rhsObj {
512
513 return
514 }
515 } else if !packagepath.CanImport(a.pass.Pkg.Path(), incon.RHSPkgPath) {
516
517 return
518 }
519 var (
520 importPrefix string
521 edits []analysis.TextEdit
522 )
523 if incon.RHSPkgPath != a.pass.Pkg.Path() {
524 importPrefix, edits = refactor.AddImport(
525 a.pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos())
526 }
527
528 var expr ast.Expr = n
529 if astutil.IsChildOf(cur, edge.SelectorExpr_Sel) {
530 expr = cur.Parent().Node().(ast.Expr)
531 }
532 a.reportInline("constant", "Constant", expr, edits, importPrefix+incon.RHSName)
533 }
534
535
536 func (a *analyzer) reportInline(kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) {
537 edits = append(edits, analysis.TextEdit{
538 Pos: ident.Pos(),
539 End: ident.End(),
540 NewText: []byte(newText),
541 })
542 name := astutil.Format(a.pass.Fset, ident)
543 a.pass.Report(analysis.Diagnostic{
544 Pos: ident.Pos(),
545 End: ident.End(),
546 Message: fmt.Sprintf("%s %s should be inlined", capKind, name),
547 SuggestedFixes: []analysis.SuggestedFix{{
548 Message: fmt.Sprintf("Inline %s %s", kind, name),
549 TextEdits: edits,
550 }},
551 })
552 }
553
554 func (a *analyzer) readFile(node ast.Node) ([]byte, error) {
555 filename := a.pass.Fset.File(node.Pos()).Name()
556 content, ok := a.fileContent[filename]
557 if !ok {
558 var err error
559 content, err = a.pass.ReadFile(filename)
560 if err != nil {
561 return nil, err
562 }
563 a.fileContent[filename] = content
564 }
565 return content, nil
566 }
567
568
569
570 type goFixInlineFuncFact struct{ Callee *inline.Callee }
571
572 func (f *goFixInlineFuncFact) String() string { return "goFixInline " + f.Callee.String() }
573 func (*goFixInlineFuncFact) AFact() {}
574
575
576
577 type goFixInlineConstFact struct {
578
579 RHSName string
580 RHSPkgPath string
581 RHSPkgName string
582 rhsObj types.Object
583 }
584
585 func (c *goFixInlineConstFact) String() string {
586 return fmt.Sprintf("goFixInline const %q.%s", c.RHSPkgPath, c.RHSName)
587 }
588
589 func (*goFixInlineConstFact) AFact() {}
590
591
592
593 type goFixInlineAliasFact struct{}
594
595 func (c *goFixInlineAliasFact) String() string { return "goFixInline alias" }
596 func (*goFixInlineAliasFact) AFact() {}
597
598 func discard(string, ...any) {}
599
View as plain text