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