| // 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) {} |