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
325
326 var expr ast.Expr = id
327 if astutil.IsChildOf(curId, edge.SelectorExpr_Sel) {
328 curId = curId.Parent()
329 expr = curId.Node().(ast.Expr)
330 }
331
332
333 switch ek, _ := curId.ParentEdge(); ek {
334 case edge.IndexExpr_X:
335 expr = curId.Parent().Node().(*ast.IndexExpr)
336 case edge.IndexListExpr_X:
337 expr = curId.Parent().Node().(*ast.IndexListExpr)
338 }
339 t := a.pass.TypesInfo.TypeOf(expr).(*types.Alias)
340 if targs := t.TypeArgs(); targs.Len() > 0 {
341
342
343
344
345
346 instAlias, _ := types.Instantiate(nil, alias, slices.Collect(targs.Types()), false)
347 rhs = instAlias.(*types.Alias).Rhs()
348 }
349
350
351
352
353
354
355 var (
356 importPrefixes = map[string]string{curPath: ""}
357 edits []analysis.TextEdit
358 )
359 for _, tn := range typenames(rhs) {
360
361 if typeParamNames[tn] {
362 continue
363 }
364 var pkgPath, pkgName string
365 if pkg := tn.Pkg(); pkg != nil {
366 pkgPath = pkg.Path()
367 pkgName = pkg.Name()
368 }
369 if pkgPath == "" || pkgPath == curPath {
370
371
372
373 scope := a.pass.TypesInfo.Scopes[curFile].Innermost(id.Pos())
374 _, obj := scope.LookupParent(tn.Name(), id.Pos())
375 if obj != tn {
376 return
377 }
378 } else if !packagepath.CanImport(a.pass.Pkg.Path(), pkgPath) {
379
380 return
381 } else if _, ok := importPrefixes[pkgPath]; !ok {
382
383
384
385 prefix, eds := refactor.AddImport(
386 a.pass.TypesInfo, curFile, pkgName, pkgPath, tn.Name(), id.Pos())
387 importPrefixes[pkgPath] = strings.TrimSuffix(prefix, ".")
388 edits = append(edits, eds...)
389 }
390 }
391
392
393
394 newText := types.TypeString(rhs, func(p *types.Package) string {
395 if p == a.pass.Pkg {
396 return ""
397 }
398 if prefix, ok := importPrefixes[p.Path()]; ok {
399 return prefix
400 }
401 panic(fmt.Sprintf("in %q, package path %q has no import prefix", rhs, p.Path()))
402 })
403 a.reportInline("type alias", "Type alias", expr, edits, newText)
404 }
405
406
407
408
409 func typenames(t types.Type) []*types.TypeName {
410 var tns []*types.TypeName
411
412 var visit func(types.Type)
413 visit = func(t types.Type) {
414 if hasName, ok := t.(interface{ Obj() *types.TypeName }); ok {
415 tns = append(tns, hasName.Obj())
416 }
417 switch t := t.(type) {
418 case *types.Basic:
419 tns = append(tns, types.Universe.Lookup(t.Name()).(*types.TypeName))
420 case *types.Named:
421 for t := range t.TypeArgs().Types() {
422 visit(t)
423 }
424 case *types.Alias:
425 for t := range t.TypeArgs().Types() {
426 visit(t)
427 }
428 case *types.TypeParam:
429 tns = append(tns, t.Obj())
430 case *types.Pointer:
431 visit(t.Elem())
432 case *types.Slice:
433 visit(t.Elem())
434 case *types.Array:
435 visit(t.Elem())
436 case *types.Chan:
437 visit(t.Elem())
438 case *types.Map:
439 visit(t.Key())
440 visit(t.Elem())
441 case *types.Struct:
442 for field := range t.Fields() {
443 visit(field.Type())
444 }
445 case *types.Signature:
446
447
448
449
450 if t.TypeParams() != nil {
451 panic("Signature.TypeParams in type expression")
452 }
453 visit(t.Params())
454 visit(t.Results())
455 case *types.Interface:
456 for etyp := range t.EmbeddedTypes() {
457 visit(etyp)
458 }
459 for method := range t.ExplicitMethods() {
460 visit(method.Type())
461 }
462 case *types.Tuple:
463 for v := range t.Variables() {
464 visit(v.Type())
465 }
466 case *types.Union:
467 panic("Union in type expression")
468 default:
469 panic(fmt.Sprintf("unknown type %T", t))
470 }
471 }
472
473 visit(t)
474
475 return tns
476 }
477
478
479 func (a *analyzer) inlineConst(con *types.Const, cur inspector.Cursor) {
480 incon, ok := a.inlinableConsts[con]
481 if !ok {
482 var fact goFixInlineConstFact
483 if a.pass.ImportObjectFact(con, &fact) {
484 incon = &fact
485 a.inlinableConsts[con] = incon
486 }
487 }
488 if incon == nil {
489 return
490 }
491
492
493 curFile := astutil.EnclosingFile(cur)
494 n := cur.Node().(*ast.Ident)
495
496
497
498
499
500
501
502
503
504
505 if a.pass.Pkg.Path() == incon.RHSPkgPath {
506
507 scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos())
508 _, obj := scope.LookupParent(incon.RHSName, n.Pos())
509 if obj == nil {
510
511
512 panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, incon.RHSName))
513 }
514 if obj != incon.rhsObj {
515
516 return
517 }
518 } else if !packagepath.CanImport(a.pass.Pkg.Path(), incon.RHSPkgPath) {
519
520 return
521 }
522 var (
523 importPrefix string
524 edits []analysis.TextEdit
525 )
526 if incon.RHSPkgPath != a.pass.Pkg.Path() {
527 importPrefix, edits = refactor.AddImport(
528 a.pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos())
529 }
530
531 var expr ast.Expr = n
532 if astutil.IsChildOf(cur, edge.SelectorExpr_Sel) {
533 expr = cur.Parent().Node().(ast.Expr)
534 }
535 a.reportInline("constant", "Constant", expr, edits, importPrefix+incon.RHSName)
536 }
537
538
539 func (a *analyzer) reportInline(kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) {
540 edits = append(edits, analysis.TextEdit{
541 Pos: ident.Pos(),
542 End: ident.End(),
543 NewText: []byte(newText),
544 })
545 name := astutil.Format(a.pass.Fset, ident)
546 a.pass.Report(analysis.Diagnostic{
547 Pos: ident.Pos(),
548 End: ident.End(),
549 Message: fmt.Sprintf("%s %s should be inlined", capKind, name),
550 SuggestedFixes: []analysis.SuggestedFix{{
551 Message: fmt.Sprintf("Inline %s %s", kind, name),
552 TextEdits: edits,
553 }},
554 })
555 }
556
557 func (a *analyzer) readFile(node ast.Node) ([]byte, error) {
558 filename := a.pass.Fset.File(node.Pos()).Name()
559 content, ok := a.fileContent[filename]
560 if !ok {
561 var err error
562 content, err = a.pass.ReadFile(filename)
563 if err != nil {
564 return nil, err
565 }
566 a.fileContent[filename] = content
567 }
568 return content, nil
569 }
570
571
572
573 type goFixInlineFuncFact struct{ Callee *inline.Callee }
574
575 func (f *goFixInlineFuncFact) String() string { return "goFixInline " + f.Callee.String() }
576 func (*goFixInlineFuncFact) AFact() {}
577
578
579
580 type goFixInlineConstFact struct {
581
582 RHSName string
583 RHSPkgPath string
584 RHSPkgName string
585 rhsObj types.Object
586 }
587
588 func (c *goFixInlineConstFact) String() string {
589 return fmt.Sprintf("goFixInline const %q.%s", c.RHSPkgPath, c.RHSName)
590 }
591
592 func (*goFixInlineConstFact) AFact() {}
593
594
595
596 type goFixInlineAliasFact struct{}
597
598 func (c *goFixInlineAliasFact) String() string { return "goFixInline alias" }
599 func (*goFixInlineAliasFact) AFact() {}
600
601 func discard(string, ...any) {}
602
View as plain text