blob: 9de3b2eaa6c7d486fa5286696d5be52ba546d602 [file] [log] [blame]
// 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"
"iter"
"slices"
"strings"
_ "embed"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/gofix/findgofix"
"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/internal/gofix",
Run: run,
FactTypes: []analysis.Fact{
(*goFixInlineFuncFact)(nil),
(*goFixInlineConstFact)(nil),
(*goFixInlineAliasFact)(nil),
},
Requires: []*analysis.Analyzer{inspect.Analyzer},
}
var allowBindingDecl bool
func init() {
Analyzer.Flags.BoolVar(&allowBindingDecl, "allow_binding_decl", false,
"permit inlinings that require a 'var params = args' declaration")
}
// analyzer holds the state for this analysis.
type analyzer struct {
pass *analysis.Pass
root inspector.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) (any, error) {
a := &analyzer{
pass: pass,
root: pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Root(),
fileContent: make(map[string][]byte),
inlinableFuncs: make(map[*types.Func]*inline.Callee),
inlinableConsts: make(map[*types.Const]*goFixInlineConstFact),
inlinableAliases: make(map[*types.TypeName]*goFixInlineAliasFact),
}
findgofix.Find(pass, a.root, a)
a.inline()
return nil, nil
}
// HandleFunc exports a fact for functions marked with go:fix.
func (a *analyzer) HandleFunc(decl *ast.FuncDecl) {
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
}
// HandleAlias exports a fact for aliases marked with go:fix.
func (a *analyzer) HandleAlias(spec *ast.TypeSpec) {
// 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)
}
}
// HandleConst exports a fact for constants marked with go:fix.
func (a *analyzer) HandleConst(nameIdent, rhsIdent *ast.Ident) {
lhs := a.pass.TypesInfo.Defs[nameIdent].(*types.Const)
rhs := a.pass.TypesInfo.Uses[rhsIdent].(*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 inspector.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 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
}
if res.BindingDecl && !allowBindingDecl {
// When applying fix en masse, users are similarly
// unenthusiastic about inlinings that cannot
// entirely eliminate the parameters and
// insert a 'var params = args' declaration.
// The flag allows them to decline such fixes.
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 inspector.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 listIter(alias.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 ek, _ := curId.ParentEdge(); ek == 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.ParentEdge(); 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(listIter(targs)), 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 listIter(t.TypeArgs()) {
visit(t)
}
case *types.Alias:
for t := range listIter(t.TypeArgs()) {
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 i := range t.NumFields() {
visit(t.Field(i).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 listIter(t) {
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 inspector.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 ek, _ := cur.ParentEdge(); ek == 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) {
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 inspector.Cursor) *ast.File {
for cf := range c.Enclosing((*ast.File)(nil)) {
return cf.Node().(*ast.File)
}
panic("no *ast.File enclosing a cursor: impossible")
}
// 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) {}
type list[T any] interface {
Len() int
At(int) T
}
// TODO(adonovan): eliminate in favor of go/types@go1.24 iterators.
func listIter[L list[T], T any](lst L) iter.Seq[T] {
return func(yield func(T) bool) {
for i := range lst.Len() {
if !yield(lst.At(i)) {
return
}
}
}
}