| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package gofix |
| |
| import ( |
| "fmt" |
| "go/ast" |
| "go/token" |
| "go/types" |
| "slices" |
| "strings" |
| |
| _ "embed" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/analysis/passes/inspect" |
| "golang.org/x/tools/go/ast/inspector" |
| "golang.org/x/tools/go/types/typeutil" |
| "golang.org/x/tools/gopls/internal/util/moreiters" |
| "golang.org/x/tools/internal/analysisinternal" |
| internalastutil "golang.org/x/tools/internal/astutil" |
| "golang.org/x/tools/internal/astutil/cursor" |
| "golang.org/x/tools/internal/astutil/edge" |
| "golang.org/x/tools/internal/diff" |
| "golang.org/x/tools/internal/refactor/inline" |
| "golang.org/x/tools/internal/typesinternal" |
| ) |
| |
| //go:embed doc.go |
| var doc string |
| |
| var Analyzer = &analysis.Analyzer{ |
| Name: "gofix", |
| Doc: analysisinternal.MustExtractDoc(doc, "gofix"), |
| URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix", |
| Run: func(pass *analysis.Pass) (any, error) { return run(pass, true) }, |
| FactTypes: []analysis.Fact{ |
| (*goFixInlineFuncFact)(nil), |
| (*goFixInlineConstFact)(nil), |
| (*goFixInlineAliasFact)(nil), |
| }, |
| Requires: []*analysis.Analyzer{inspect.Analyzer}, |
| } |
| |
| var DirectiveAnalyzer = &analysis.Analyzer{ |
| Name: "gofixdirective", |
| Doc: analysisinternal.MustExtractDoc(doc, "gofixdirective"), |
| URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/gofix", |
| Run: func(pass *analysis.Pass) (any, error) { return run(pass, false) }, |
| FactTypes: []analysis.Fact{ |
| (*goFixInlineFuncFact)(nil), |
| (*goFixInlineConstFact)(nil), |
| (*goFixInlineAliasFact)(nil), |
| }, |
| Requires: []*analysis.Analyzer{inspect.Analyzer}, |
| } |
| |
| // analyzer holds the state for this analysis. |
| type analyzer struct { |
| pass *analysis.Pass |
| fix bool // only suggest fixes if true; else, just check directives |
| root cursor.Cursor |
| // memoization of repeated calls for same file. |
| fileContent map[string][]byte |
| // memoization of fact imports (nil => no fact) |
| inlinableFuncs map[*types.Func]*inline.Callee |
| inlinableConsts map[*types.Const]*goFixInlineConstFact |
| inlinableAliases map[*types.TypeName]*goFixInlineAliasFact |
| } |
| |
| func run(pass *analysis.Pass, fix bool) (any, error) { |
| a := &analyzer{ |
| pass: pass, |
| fix: fix, |
| root: cursor.Root(pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)), |
| fileContent: make(map[string][]byte), |
| inlinableFuncs: make(map[*types.Func]*inline.Callee), |
| inlinableConsts: make(map[*types.Const]*goFixInlineConstFact), |
| inlinableAliases: make(map[*types.TypeName]*goFixInlineAliasFact), |
| } |
| a.find() |
| a.inline() |
| return nil, nil |
| } |
| |
| // find finds functions and constants annotated with an appropriate "//go:fix" |
| // comment (the syntax proposed by #32816), |
| // and exports a fact for each one. |
| func (a *analyzer) find() { |
| for cur := range a.root.Preorder((*ast.FuncDecl)(nil), (*ast.GenDecl)(nil)) { |
| switch decl := cur.Node().(type) { |
| case *ast.FuncDecl: |
| a.findFunc(decl) |
| |
| case *ast.GenDecl: |
| if decl.Tok != token.CONST && decl.Tok != token.TYPE { |
| continue |
| } |
| declInline := hasFixInline(decl.Doc) |
| // Accept inline directives on the entire decl as well as individual specs. |
| for _, spec := range decl.Specs { |
| switch spec := spec.(type) { |
| case *ast.TypeSpec: // Tok == TYPE |
| a.findAlias(spec, declInline) |
| |
| case *ast.ValueSpec: // Tok == CONST |
| a.findConst(spec, declInline) |
| } |
| } |
| } |
| } |
| } |
| |
| func (a *analyzer) findFunc(decl *ast.FuncDecl) { |
| if !hasFixInline(decl.Doc) { |
| return |
| } |
| content, err := a.readFile(decl) |
| if err != nil { |
| a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err) |
| return |
| } |
| callee, err := inline.AnalyzeCallee(discard, a.pass.Fset, a.pass.Pkg, a.pass.TypesInfo, decl, content) |
| if err != nil { |
| a.pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err) |
| return |
| } |
| fn := a.pass.TypesInfo.Defs[decl.Name].(*types.Func) |
| a.pass.ExportObjectFact(fn, &goFixInlineFuncFact{callee}) |
| a.inlinableFuncs[fn] = callee |
| } |
| |
| func (a *analyzer) findAlias(spec *ast.TypeSpec, declInline bool) { |
| if !declInline && !hasFixInline(spec.Doc) { |
| return |
| } |
| if !spec.Assign.IsValid() { |
| a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias") |
| return |
| } |
| |
| // Disallow inlines of type expressions containing array types. |
| // Given an array type like [N]int where N is a named constant, go/types provides |
| // only the value of the constant as an int64. So inlining A in this code: |
| // |
| // const N = 5 |
| // type A = [N]int |
| // |
| // would result in [5]int, breaking the connection with N. |
| for n := range ast.Preorder(spec.Type) { |
| if ar, ok := n.(*ast.ArrayType); ok && ar.Len != nil { |
| // Make an exception when the array length is a literal int. |
| if lit, ok := ast.Unparen(ar.Len).(*ast.BasicLit); ok && lit.Kind == token.INT { |
| continue |
| } |
| a.pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: array types not supported") |
| return |
| } |
| } |
| |
| // Remember that this is an inlinable alias. |
| typ := &goFixInlineAliasFact{} |
| lhs := a.pass.TypesInfo.Defs[spec.Name].(*types.TypeName) |
| a.inlinableAliases[lhs] = typ |
| // Create a fact only if the LHS is exported and defined at top level. |
| // We create a fact even if the RHS is non-exported, |
| // so we can warn about uses in other packages. |
| if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { |
| a.pass.ExportObjectFact(lhs, typ) |
| } |
| } |
| |
| func (a *analyzer) findConst(spec *ast.ValueSpec, declInline bool) { |
| info := a.pass.TypesInfo |
| specInline := hasFixInline(spec.Doc) |
| if declInline || specInline { |
| for i, name := range spec.Names { |
| if i >= len(spec.Values) { |
| // Possible following an iota. |
| break |
| } |
| val := spec.Values[i] |
| var rhsID *ast.Ident |
| switch e := val.(type) { |
| case *ast.Ident: |
| // Constants defined with the predeclared iota cannot be inlined. |
| if info.Uses[e] == builtinIota { |
| a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota") |
| return |
| } |
| rhsID = e |
| case *ast.SelectorExpr: |
| rhsID = e.Sel |
| default: |
| a.pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant") |
| return |
| } |
| lhs := info.Defs[name].(*types.Const) |
| rhs := info.Uses[rhsID].(*types.Const) // must be so in a well-typed program |
| con := &goFixInlineConstFact{ |
| RHSName: rhs.Name(), |
| RHSPkgName: rhs.Pkg().Name(), |
| RHSPkgPath: rhs.Pkg().Path(), |
| } |
| if rhs.Pkg() == a.pass.Pkg { |
| con.rhsObj = rhs |
| } |
| a.inlinableConsts[lhs] = con |
| // Create a fact only if the LHS is exported and defined at top level. |
| // We create a fact even if the RHS is non-exported, |
| // so we can warn about uses in other packages. |
| if lhs.Exported() && typesinternal.IsPackageLevel(lhs) { |
| a.pass.ExportObjectFact(lhs, con) |
| } |
| } |
| } |
| } |
| |
| // inline inlines each static call to an inlinable function |
| // and each reference to an inlinable constant or type alias. |
| // |
| // TODO(adonovan): handle multiple diffs that each add the same import. |
| func (a *analyzer) inline() { |
| for cur := range a.root.Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) { |
| switch n := cur.Node().(type) { |
| case *ast.CallExpr: |
| a.inlineCall(n, cur) |
| |
| case *ast.Ident: |
| switch t := a.pass.TypesInfo.Uses[n].(type) { |
| case *types.TypeName: |
| a.inlineAlias(t, cur) |
| case *types.Const: |
| a.inlineConst(t, cur) |
| } |
| } |
| } |
| } |
| |
| // If call is a call to an inlinable func, suggest inlining its use at cur. |
| func (a *analyzer) inlineCall(call *ast.CallExpr, cur cursor.Cursor) { |
| if fn := typeutil.StaticCallee(a.pass.TypesInfo, call); fn != nil { |
| // Inlinable? |
| callee, ok := a.inlinableFuncs[fn] |
| if !ok { |
| var fact goFixInlineFuncFact |
| if a.pass.ImportObjectFact(fn, &fact) { |
| callee = fact.Callee |
| a.inlinableFuncs[fn] = callee |
| } |
| } |
| if callee == nil { |
| return // nope |
| } |
| |
| // Inline the call. |
| content, err := a.readFile(call) |
| if err != nil { |
| a.pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err) |
| return |
| } |
| curFile := currentFile(cur) |
| caller := &inline.Caller{ |
| Fset: a.pass.Fset, |
| Types: a.pass.Pkg, |
| Info: a.pass.TypesInfo, |
| File: curFile, |
| Call: call, |
| Content: content, |
| } |
| res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard}) |
| if err != nil { |
| a.pass.Reportf(call.Lparen, "%v", err) |
| return |
| } |
| if !a.fix { |
| return |
| } |
| |
| if res.Literalized { |
| // Users are not fond of inlinings that literalize |
| // f(x) to func() { ... }(), so avoid them. |
| // |
| // (Unfortunately the inliner is very timid, |
| // and often literalizes when it cannot prove that |
| // reducing the call is safe; the user of this tool |
| // has no indication of what the problem is.) |
| return |
| } |
| got := res.Content |
| |
| // Suggest the "fix". |
| var textEdits []analysis.TextEdit |
| for _, edit := range diff.Bytes(content, got) { |
| textEdits = append(textEdits, analysis.TextEdit{ |
| Pos: curFile.FileStart + token.Pos(edit.Start), |
| End: curFile.FileStart + token.Pos(edit.End), |
| NewText: []byte(edit.New), |
| }) |
| } |
| a.pass.Report(analysis.Diagnostic{ |
| Pos: call.Pos(), |
| End: call.End(), |
| Message: fmt.Sprintf("Call of %v should be inlined", callee), |
| SuggestedFixes: []analysis.SuggestedFix{{ |
| Message: fmt.Sprintf("Inline call of %v", callee), |
| TextEdits: textEdits, |
| }}, |
| }) |
| } |
| } |
| |
| // If tn is the TypeName of an inlinable alias, suggest inlining its use at cur. |
| func (a *analyzer) inlineAlias(tn *types.TypeName, curId cursor.Cursor) { |
| inalias, ok := a.inlinableAliases[tn] |
| if !ok { |
| var fact goFixInlineAliasFact |
| if a.pass.ImportObjectFact(tn, &fact) { |
| inalias = &fact |
| a.inlinableAliases[tn] = inalias |
| } |
| } |
| if inalias == nil { |
| return // nope |
| } |
| |
| alias := tn.Type().(*types.Alias) |
| // Remember the names of the alias's type params. When we check for shadowing |
| // later, we'll ignore these because they won't appear in the replacement text. |
| typeParamNames := map[*types.TypeName]bool{} |
| for tp := range alias.TypeParams().TypeParams() { |
| typeParamNames[tp.Obj()] = true |
| } |
| rhs := alias.Rhs() |
| curPath := a.pass.Pkg.Path() |
| curFile := currentFile(curId) |
| id := curId.Node().(*ast.Ident) |
| // We have an identifier A here (n), possibly qualified by a package |
| // identifier (sel.n), and an inlinable "type A = rhs" elsewhere. |
| // |
| // We can replace A with rhs if no name in rhs is shadowed at n's position, |
| // and every package in rhs is importable by the current package. |
| |
| var ( |
| importPrefixes = map[string]string{curPath: ""} // from pkg path to prefix |
| edits []analysis.TextEdit |
| ) |
| for _, tn := range typenames(rhs) { |
| // Ignore the type parameters of the alias: they won't appear in the result. |
| if typeParamNames[tn] { |
| continue |
| } |
| var pkgPath, pkgName string |
| if pkg := tn.Pkg(); pkg != nil { |
| pkgPath = pkg.Path() |
| pkgName = pkg.Name() |
| } |
| if pkgPath == "" || pkgPath == curPath { |
| // The name is in the current package or the universe scope, so no import |
| // is required. Check that it is not shadowed (that is, that the type |
| // it refers to in rhs is the same one it refers to at n). |
| scope := a.pass.TypesInfo.Scopes[curFile].Innermost(id.Pos()) // n's scope |
| _, obj := scope.LookupParent(tn.Name(), id.Pos()) // what qn.name means in n's scope |
| if obj != tn { |
| return |
| } |
| } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), pkgPath) { |
| // If this package can't see the package of this part of rhs, we can't inline. |
| return |
| } else if _, ok := importPrefixes[pkgPath]; !ok { |
| // Use AddImport to add pkgPath if it's not there already. Associate the prefix it assigns |
| // with the package path for use by the TypeString qualifier below. |
| _, prefix, eds := analysisinternal.AddImport( |
| a.pass.TypesInfo, curFile, pkgName, pkgPath, tn.Name(), id.Pos()) |
| importPrefixes[pkgPath] = strings.TrimSuffix(prefix, ".") |
| edits = append(edits, eds...) |
| } |
| } |
| // Find the complete identifier, which may take any of these forms: |
| // Id |
| // Id[T] |
| // Id[K, V] |
| // pkg.Id |
| // pkg.Id[T] |
| // pkg.Id[K, V] |
| var expr ast.Expr = id |
| if e, _ := curId.Edge(); e == edge.SelectorExpr_Sel { |
| curId = curId.Parent() |
| expr = curId.Node().(ast.Expr) |
| } |
| // If expr is part of an IndexExpr or IndexListExpr, we'll need that node. |
| // Given C[int], TypeOf(C) is generic but TypeOf(C[int]) is instantiated. |
| switch ek, _ := curId.Edge(); ek { |
| case edge.IndexExpr_X: |
| expr = curId.Parent().Node().(*ast.IndexExpr) |
| case edge.IndexListExpr_X: |
| expr = curId.Parent().Node().(*ast.IndexListExpr) |
| } |
| t := a.pass.TypesInfo.TypeOf(expr).(*types.Alias) // type of entire identifier |
| if targs := t.TypeArgs(); targs.Len() > 0 { |
| // Instantiate the alias with the type args from this use. |
| // For example, given type A = M[K, V], compute the type of the use |
| // A[int, Foo] as M[int, Foo]. |
| // Don't validate instantiation: it can't panic unless we have a bug, |
| // in which case seeing the stack trace via telemetry would be helpful. |
| instAlias, _ := types.Instantiate(nil, alias, slices.Collect(targs.Types()), false) |
| rhs = instAlias.(*types.Alias).Rhs() |
| } |
| // To get the replacement text, render the alias RHS using the package prefixes |
| // we assigned above. |
| newText := types.TypeString(rhs, func(p *types.Package) string { |
| if p == a.pass.Pkg { |
| return "" |
| } |
| if prefix, ok := importPrefixes[p.Path()]; ok { |
| return prefix |
| } |
| panic(fmt.Sprintf("in %q, package path %q has no import prefix", rhs, p.Path())) |
| }) |
| a.reportInline("type alias", "Type alias", expr, edits, newText) |
| } |
| |
| // typenames returns the TypeNames for types within t (including t itself) that have |
| // them: basic types, named types and alias types. |
| // The same name may appear more than once. |
| func typenames(t types.Type) []*types.TypeName { |
| var tns []*types.TypeName |
| |
| var visit func(types.Type) |
| visit = func(t types.Type) { |
| if hasName, ok := t.(interface{ Obj() *types.TypeName }); ok { |
| tns = append(tns, hasName.Obj()) |
| } |
| switch t := t.(type) { |
| case *types.Basic: |
| tns = append(tns, types.Universe.Lookup(t.Name()).(*types.TypeName)) |
| case *types.Named: |
| for t := range t.TypeArgs().Types() { |
| visit(t) |
| } |
| case *types.Alias: |
| for t := range t.TypeArgs().Types() { |
| visit(t) |
| } |
| case *types.TypeParam: |
| tns = append(tns, t.Obj()) |
| case *types.Pointer: |
| visit(t.Elem()) |
| case *types.Slice: |
| visit(t.Elem()) |
| case *types.Array: |
| visit(t.Elem()) |
| case *types.Chan: |
| visit(t.Elem()) |
| case *types.Map: |
| visit(t.Key()) |
| visit(t.Elem()) |
| case *types.Struct: |
| for f := range t.Fields() { |
| visit(f.Type()) |
| } |
| case *types.Signature: |
| // Ignore the receiver: although it may be present, it has no meaning |
| // in a type expression. |
| // Ditto for receiver type params. |
| // Also, function type params cannot appear in a type expression. |
| if t.TypeParams() != nil { |
| panic("Signature.TypeParams in type expression") |
| } |
| visit(t.Params()) |
| visit(t.Results()) |
| case *types.Interface: |
| for i := range t.NumEmbeddeds() { |
| visit(t.EmbeddedType(i)) |
| } |
| for i := range t.NumExplicitMethods() { |
| visit(t.ExplicitMethod(i).Type()) |
| } |
| case *types.Tuple: |
| for v := range t.Variables() { |
| visit(v.Type()) |
| } |
| case *types.Union: |
| panic("Union in type expression") |
| default: |
| panic(fmt.Sprintf("unknown type %T", t)) |
| } |
| } |
| |
| visit(t) |
| |
| return tns |
| } |
| |
| // If con is an inlinable constant, suggest inlining its use at cur. |
| func (a *analyzer) inlineConst(con *types.Const, cur cursor.Cursor) { |
| incon, ok := a.inlinableConsts[con] |
| if !ok { |
| var fact goFixInlineConstFact |
| if a.pass.ImportObjectFact(con, &fact) { |
| incon = &fact |
| a.inlinableConsts[con] = incon |
| } |
| } |
| if incon == nil { |
| return // nope |
| } |
| |
| // If n is qualified by a package identifier, we'll need the full selector expression. |
| curFile := currentFile(cur) |
| n := cur.Node().(*ast.Ident) |
| |
| // We have an identifier A here (n), possibly qualified by a package identifier (sel.X, |
| // where sel is the parent of n), // and an inlinable "const A = B" elsewhere (incon). |
| // Consider replacing A with B. |
| |
| // Check that the expression we are inlining (B) means the same thing |
| // (refers to the same object) in n's scope as it does in A's scope. |
| // If the RHS is not in the current package, AddImport will handle |
| // shadowing, so we only need to worry about when both expressions |
| // are in the current package. |
| if a.pass.Pkg.Path() == incon.RHSPkgPath { |
| // incon.rhsObj is the object referred to by B in the definition of A. |
| scope := a.pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope |
| _, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope |
| if obj == nil { |
| // Should be impossible: if code at n can refer to the LHS, |
| // it can refer to the RHS. |
| panic(fmt.Sprintf("no object for inlinable const %s RHS %s", n.Name, incon.RHSName)) |
| } |
| if obj != incon.rhsObj { |
| // "B" means something different here than at the inlinable const's scope. |
| return |
| } |
| } else if !analysisinternal.CanImport(a.pass.Pkg.Path(), incon.RHSPkgPath) { |
| // If this package can't see the RHS's package, we can't inline. |
| return |
| } |
| var ( |
| importPrefix string |
| edits []analysis.TextEdit |
| ) |
| if incon.RHSPkgPath != a.pass.Pkg.Path() { |
| _, importPrefix, edits = analysisinternal.AddImport( |
| a.pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos()) |
| } |
| // If n is qualified by a package identifier, we'll need the full selector expression. |
| var expr ast.Expr = n |
| if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel { |
| expr = cur.Parent().Node().(ast.Expr) |
| } |
| a.reportInline("constant", "Constant", expr, edits, importPrefix+incon.RHSName) |
| } |
| |
| // reportInline reports a diagnostic for fixing an inlinable name. |
| func (a *analyzer) reportInline(kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) { |
| if !a.fix { |
| return |
| } |
| edits = append(edits, analysis.TextEdit{ |
| Pos: ident.Pos(), |
| End: ident.End(), |
| NewText: []byte(newText), |
| }) |
| name := analysisinternal.Format(a.pass.Fset, ident) |
| a.pass.Report(analysis.Diagnostic{ |
| Pos: ident.Pos(), |
| End: ident.End(), |
| Message: fmt.Sprintf("%s %s should be inlined", capKind, name), |
| SuggestedFixes: []analysis.SuggestedFix{{ |
| Message: fmt.Sprintf("Inline %s %s", kind, name), |
| TextEdits: edits, |
| }}, |
| }) |
| } |
| |
| func (a *analyzer) readFile(node ast.Node) ([]byte, error) { |
| filename := a.pass.Fset.File(node.Pos()).Name() |
| content, ok := a.fileContent[filename] |
| if !ok { |
| var err error |
| content, err = a.pass.ReadFile(filename) |
| if err != nil { |
| return nil, err |
| } |
| a.fileContent[filename] = content |
| } |
| return content, nil |
| } |
| |
| // currentFile returns the unique ast.File for a cursor. |
| func currentFile(c cursor.Cursor) *ast.File { |
| cf, _ := moreiters.First(c.Ancestors((*ast.File)(nil))) |
| return cf.Node().(*ast.File) |
| } |
| |
| // hasFixInline reports the presence of a "//go:fix inline" directive |
| // in the comments. |
| func hasFixInline(cg *ast.CommentGroup) bool { |
| for _, d := range internalastutil.Directives(cg) { |
| if d.Tool == "go" && d.Name == "fix" && d.Args == "inline" { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // A goFixInlineFuncFact is exported for each function marked "//go:fix inline". |
| // It holds information about the callee to support inlining. |
| type goFixInlineFuncFact struct{ Callee *inline.Callee } |
| |
| func (f *goFixInlineFuncFact) String() string { return "goFixInline " + f.Callee.String() } |
| func (*goFixInlineFuncFact) AFact() {} |
| |
| // A goFixInlineConstFact is exported for each constant marked "//go:fix inline". |
| // It holds information about an inlinable constant. Gob-serializable. |
| type goFixInlineConstFact struct { |
| // Information about "const LHSName = RHSName". |
| RHSName string |
| RHSPkgPath string |
| RHSPkgName string |
| rhsObj types.Object // for current package |
| } |
| |
| func (c *goFixInlineConstFact) String() string { |
| return fmt.Sprintf("goFixInline const %q.%s", c.RHSPkgPath, c.RHSName) |
| } |
| |
| func (*goFixInlineConstFact) AFact() {} |
| |
| // A goFixInlineAliasFact is exported for each type alias marked "//go:fix inline". |
| // It holds no information; its mere existence demonstrates that an alias is inlinable. |
| type goFixInlineAliasFact struct{} |
| |
| func (c *goFixInlineAliasFact) String() string { return "goFixInline alias" } |
| func (*goFixInlineAliasFact) AFact() {} |
| |
| func discard(string, ...any) {} |
| |
| var builtinIota = types.Universe.Lookup("iota") |