blob: f0519469b5e7974f301e0ca4cdf19b7ae5e3b49e [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 analyzer
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"os"
"strings"
"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/internal/diff"
"golang.org/x/tools/internal/refactor/inline"
)
const Doc = `inline calls to functions with "inlineme" doc comment`
var Analyzer = &analysis.Analyzer{
Name: "inline",
Doc: Doc,
URL: "https://pkg.go.dev/golang.org/x/tools/internal/refactor/inline/analyzer",
Run: run,
FactTypes: []analysis.Fact{new(inlineMeFact)},
Requires: []*analysis.Analyzer{inspect.Analyzer},
}
func run(pass *analysis.Pass) (interface{}, error) {
// Memoize repeated calls for same file.
// TODO(adonovan): the analysis.Pass should abstract this (#62292)
// as the driver may not be reading directly from the file system.
fileContent := make(map[string][]byte)
readFile := func(node ast.Node) ([]byte, error) {
filename := pass.Fset.File(node.Pos()).Name()
content, ok := fileContent[filename]
if !ok {
var err error
content, err = os.ReadFile(filename)
if err != nil {
return nil, err
}
fileContent[filename] = content
}
return content, nil
}
// Pass 1: find functions annotated with an "inlineme"
// comment, and export a fact for each one.
inlinable := make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact)
for _, file := range pass.Files {
for _, decl := range file.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok {
// TODO(adonovan): this is just a placeholder.
// Use the precise go:fix syntax in the proposal.
// Beware that //go: comments are treated specially
// by (*ast.CommentGroup).Text().
// TODO(adonovan): alternatively, consider using
// the universal annotation mechanism sketched in
// https://go.dev/cl/489835 (which doesn't yet have
// a proper proposal).
if strings.Contains(decl.Doc.Text(), "inlineme") {
content, err := readFile(file)
if err != nil {
pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err)
continue
}
callee, err := inline.AnalyzeCallee(discard, pass.Fset, pass.Pkg, pass.TypesInfo, decl, content)
if err != nil {
pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err)
continue
}
fn := pass.TypesInfo.Defs[decl.Name].(*types.Func)
pass.ExportObjectFact(fn, &inlineMeFact{callee})
inlinable[fn] = callee
}
}
}
}
// Pass 2. Inline each static call to an inlinable function.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.File)(nil),
(*ast.CallExpr)(nil),
}
var currentFile *ast.File
inspect.Preorder(nodeFilter, func(n ast.Node) {
if file, ok := n.(*ast.File); ok {
currentFile = file
return
}
call := n.(*ast.CallExpr)
if fn := typeutil.StaticCallee(pass.TypesInfo, call); fn != nil {
// Inlinable?
callee, ok := inlinable[fn]
if !ok {
var fact inlineMeFact
if pass.ImportObjectFact(fn, &fact) {
callee = fact.Callee
inlinable[fn] = callee
}
}
if callee == nil {
return // nope
}
// Inline the call.
content, err := readFile(call)
if err != nil {
pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err)
return
}
caller := &inline.Caller{
Fset: pass.Fset,
Types: pass.Pkg,
Info: pass.TypesInfo,
File: currentFile,
Call: call,
Content: content,
}
res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard})
if err != nil {
pass.Reportf(call.Lparen, "%v", err)
return
}
got := res.Content
// Suggest the "fix".
var textEdits []analysis.TextEdit
for _, edit := range diff.Bytes(content, got) {
textEdits = append(textEdits, analysis.TextEdit{
Pos: currentFile.FileStart + token.Pos(edit.Start),
End: currentFile.FileStart + token.Pos(edit.End),
NewText: []byte(edit.New),
})
}
msg := fmt.Sprintf("inline call of %v", callee)
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Message: msg,
SuggestedFixes: []analysis.SuggestedFix{{
Message: msg,
TextEdits: textEdits,
}},
})
}
})
return nil, nil
}
type inlineMeFact struct{ Callee *inline.Callee }
func (f *inlineMeFact) String() string { return "inlineme " + f.Callee.String() }
func (*inlineMeFact) AFact() {}
func discard(string, ...any) {}