mirror of https://github.com/grafana/grafana.git
382 lines
11 KiB
Go
382 lines
11 KiB
Go
package wirecheck
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
|
|
"github.com/golangci/plugin-module-register/register"
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/inspect"
|
|
"golang.org/x/tools/go/ast/inspector"
|
|
)
|
|
|
|
// Settings represents the configuration for the wire-checker plugin
|
|
type Settings struct {
|
|
WireGen string `json:"wire-gen"` // Path to wire_gen.go file
|
|
Recursive bool `json:"recursive"` // Enable recursive analysis
|
|
}
|
|
|
|
// methodCall represents a method call found in a provider function
|
|
type methodCall struct {
|
|
receiver string
|
|
method string
|
|
line int
|
|
pos token.Pos
|
|
callPath []string // Track the call path for recursive calls
|
|
}
|
|
|
|
// functionCall represents a function call that needs to be analyzed recursively
|
|
type functionCall struct {
|
|
funcName string
|
|
pos token.Pos
|
|
pkg string
|
|
}
|
|
|
|
// DependencyParam represents a parameter that is a wire dependency
|
|
type DependencyParam struct {
|
|
Name string
|
|
Type string
|
|
Position int
|
|
}
|
|
|
|
// ParameterFlow tracks how wire dependencies flow through function calls
|
|
type ParameterFlow struct {
|
|
SourceParam DependencyParam
|
|
CallSite token.Pos
|
|
TargetParam DependencyParam
|
|
}
|
|
|
|
// WireChecker represents the wire-checker plugin
|
|
type WireChecker struct {
|
|
settings Settings
|
|
wireParser *WireParser
|
|
}
|
|
|
|
// SetSettings allows setting the configuration for standalone usage
|
|
func (w *WireChecker) SetSettings(settings Settings) {
|
|
w.settings = settings
|
|
}
|
|
|
|
// New creates a new instance of the wire-checker plugin for golangci-lint
|
|
func New(settings any) (register.LinterPlugin, error) {
|
|
s, err := register.DecodeSettings[Settings](settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &WireChecker{
|
|
settings: s,
|
|
wireParser: NewWireParser(),
|
|
}, nil
|
|
}
|
|
|
|
// BuildAnalyzers returns the analyzers for the wire-checker plugin
|
|
func (w *WireChecker) BuildAnalyzers() ([]*analysis.Analyzer, error) {
|
|
fs := flag.NewFlagSet("wirechecker", flag.ExitOnError)
|
|
fs.StringVar(&w.settings.WireGen, "wire-gen", w.settings.WireGen, "path to wire_gen.go file to analyze provider functions from")
|
|
fs.BoolVar(&w.settings.Recursive, "recursive", w.settings.Recursive, "enable recursive analysis of function calls")
|
|
return []*analysis.Analyzer{
|
|
{
|
|
Name: "wirecheck",
|
|
Doc: "check for direct dependency method calls in wire provider functions",
|
|
Run: w.run,
|
|
Flags: *fs,
|
|
Requires: []*analysis.Analyzer{inspect.Analyzer},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// GetLoadMode returns the load mode for the analyzer
|
|
func (w *WireChecker) GetLoadMode() string {
|
|
return register.LoadModeTypesInfo
|
|
}
|
|
|
|
// run is the main analysis function for the wire-checker
|
|
func (w *WireChecker) run(pass *analysis.Pass) (interface{}, error) {
|
|
|
|
// Require wire-gen setting to be specified
|
|
if w.settings.WireGen == "" {
|
|
return nil, fmt.Errorf("wire-gen setting is required")
|
|
}
|
|
|
|
// Get the inspector from the required analyzer
|
|
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
|
|
|
|
err := w.wireParser.Parse(w.settings.WireGen)
|
|
if err != nil {
|
|
// Report error and skip analysis if wire_gen.go parsing fails
|
|
pass.Report(analysis.Diagnostic{
|
|
Pos: 0,
|
|
Message: "failed to parse wire-gen file " + w.settings.WireGen + ": " + err.Error(),
|
|
})
|
|
return nil, nil
|
|
}
|
|
|
|
// Analyze only functions that are referenced in the wire_gen.go file
|
|
nodeFilter := []ast.Node{
|
|
(*ast.FuncDecl)(nil),
|
|
}
|
|
|
|
insp.Preorder(nodeFilter, func(n ast.Node) {
|
|
funcDecl := n.(*ast.FuncDecl)
|
|
|
|
// Only analyze functions that are actually used in wire dependency injection
|
|
funcName := funcDecl.Name.Name
|
|
|
|
// Check if this function should be analyzed using the wire parser helper
|
|
// Use the package's import path for consistent lookup
|
|
pkgImportPath := pass.Pkg.Path()
|
|
if w.wireParser.ShouldAnalyzeFunction(pkgImportPath, funcName) {
|
|
w.analyzeProviderFunctionForTightCoupling(pass, funcDecl, []analysis.RelatedInformation{})
|
|
}
|
|
})
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (w *WireChecker) analyzeProviderFunctionForTightCoupling(pass *analysis.Pass, funcDecl *ast.FuncDecl, related []analysis.RelatedInformation) {
|
|
// For wire provider functions, all parameters are wire dependencies
|
|
wireDependencies := w.extractParameterNames(funcDecl)
|
|
|
|
// Analyze this function with parameter flow tracking
|
|
w.analyzeWithParameterFlow(pass, funcDecl, related, wireDependencies)
|
|
}
|
|
|
|
func (w *WireChecker) analyzeWithParameterFlow(pass *analysis.Pass, funcDecl *ast.FuncDecl, related []analysis.RelatedInformation, wireDependencies map[string]bool) {
|
|
// Find method calls on wire dependencies in this function
|
|
calls := w.findMethodCallsOnTrackedDependencies(funcDecl, pass.Fset, wireDependencies)
|
|
|
|
for _, call := range calls {
|
|
if !call.pos.IsValid() {
|
|
continue
|
|
}
|
|
pass.Report(analysis.Diagnostic{
|
|
Pos: call.pos,
|
|
Message: "Wire provider dependency method called",
|
|
Related: related,
|
|
})
|
|
}
|
|
|
|
// If recursive analysis is enabled, find function calls and track parameter flow
|
|
if w.settings.Recursive {
|
|
functionCalls := w.findFunctionCallsInFunction(funcDecl)
|
|
for _, funcCall := range functionCalls {
|
|
if w.containsFunction(related, funcCall) {
|
|
continue
|
|
}
|
|
|
|
// Find the target function declaration
|
|
targetFunc := w.findFunctionDeclaration(pass, funcCall.funcName, funcCall.pkg)
|
|
if targetFunc != nil {
|
|
// Track which parameters flow from this call to the target function
|
|
flows := w.extractParameterFlowFromCall(funcCall, funcDecl, targetFunc, wireDependencies)
|
|
|
|
// Create new wire dependencies map for the target function
|
|
targetWireDeps := make(map[string]bool)
|
|
for _, flow := range flows {
|
|
targetWireDeps[flow.TargetParam.Name] = true
|
|
}
|
|
|
|
if len(targetWireDeps) > 0 {
|
|
newRelated := append(related, analysis.RelatedInformation{
|
|
Pos: funcCall.pos,
|
|
Message: "This function call results in dependency method call",
|
|
})
|
|
w.analyzeWithParameterFlow(pass, targetFunc, newRelated, targetWireDeps)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// findMethodCallsOnTrackedDependencies finds method calls on specific tracked wire dependencies
|
|
func (w *WireChecker) findMethodCallsOnTrackedDependencies(funcDecl *ast.FuncDecl, fset *token.FileSet, wireDependencies map[string]bool) []methodCall {
|
|
var calls []methodCall
|
|
w.findMethodCallsInScope(funcDecl, fset, &calls, wireDependencies)
|
|
return calls
|
|
}
|
|
|
|
// findMethodCallsInScope recursively finds method calls on tracked wire dependencies
|
|
func (w *WireChecker) findMethodCallsInScope(node ast.Node, fset *token.FileSet, calls *[]methodCall, wireDependencies map[string]bool) {
|
|
ast.Inspect(node, func(n ast.Node) bool {
|
|
// When we encounter a function literal, recursively check it but keep tracking the same wire dependencies
|
|
if funcLit, ok := n.(*ast.FuncLit); ok {
|
|
w.findMethodCallsInScope(funcLit.Body, fset, calls, wireDependencies)
|
|
return false // Don't continue inspecting this node's children
|
|
}
|
|
|
|
if callExpr, ok := n.(*ast.CallExpr); ok {
|
|
if selExpr, ok := callExpr.Fun.(*ast.SelectorExpr); ok {
|
|
if ident, ok := selExpr.X.(*ast.Ident); ok {
|
|
// Only check method calls on tracked wire dependencies
|
|
if wireDependencies[ident.Name] {
|
|
*calls = append(*calls, methodCall{
|
|
receiver: ident.Name,
|
|
method: selExpr.Sel.Name,
|
|
line: fset.Position(callExpr.Pos()).Line,
|
|
pos: callExpr.Pos(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// extractParameterNames returns a map of parameter names for the given function
|
|
func (w *WireChecker) extractParameterNames(funcDecl *ast.FuncDecl) map[string]bool {
|
|
params := make(map[string]bool)
|
|
if funcDecl.Type.Params != nil {
|
|
for _, field := range funcDecl.Type.Params.List {
|
|
for _, name := range field.Names {
|
|
params[name.Name] = true
|
|
}
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
// extractParameterFlowFromCall analyzes a function call to determine which wire dependencies
|
|
// flow from the calling function to the called function
|
|
func (w *WireChecker) extractParameterFlowFromCall(funcCall functionCall, callingFunc *ast.FuncDecl, targetFunc *ast.FuncDecl, wireDependencies map[string]bool) []ParameterFlow {
|
|
var flows []ParameterFlow
|
|
|
|
// Find the actual call expression in the calling function
|
|
var callExpr *ast.CallExpr
|
|
ast.Inspect(callingFunc, func(n ast.Node) bool {
|
|
if ce, ok := n.(*ast.CallExpr); ok && ce.Pos() == funcCall.pos {
|
|
callExpr = ce
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
if callExpr == nil {
|
|
return flows
|
|
}
|
|
|
|
// Get target function parameters
|
|
targetParams := w.extractDependencyParams(targetFunc)
|
|
|
|
// Match arguments to parameters
|
|
for i, arg := range callExpr.Args {
|
|
if i >= len(targetParams) {
|
|
break
|
|
}
|
|
|
|
// Check if this argument is a wire dependency
|
|
if ident, ok := arg.(*ast.Ident); ok {
|
|
if wireDependencies[ident.Name] {
|
|
flows = append(flows, ParameterFlow{
|
|
SourceParam: DependencyParam{
|
|
Name: ident.Name,
|
|
Position: i,
|
|
},
|
|
CallSite: funcCall.pos,
|
|
TargetParam: targetParams[i],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return flows
|
|
}
|
|
|
|
// extractDependencyParams returns a slice of DependencyParam for the given function
|
|
func (w *WireChecker) extractDependencyParams(funcDecl *ast.FuncDecl) []DependencyParam {
|
|
var params []DependencyParam
|
|
if funcDecl.Type.Params != nil {
|
|
pos := 0
|
|
for _, field := range funcDecl.Type.Params.List {
|
|
for _, name := range field.Names {
|
|
params = append(params, DependencyParam{
|
|
Name: name.Name,
|
|
Type: w.typeToString(field.Type),
|
|
Position: pos,
|
|
})
|
|
pos++
|
|
}
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
// typeToString converts an AST type expression to a string representation
|
|
func (w *WireChecker) typeToString(expr ast.Expr) string {
|
|
switch t := expr.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.SelectorExpr:
|
|
if pkg, ok := t.X.(*ast.Ident); ok {
|
|
return pkg.Name + "." + t.Sel.Name
|
|
}
|
|
return t.Sel.Name
|
|
case *ast.StarExpr:
|
|
return "*" + w.typeToString(t.X)
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// findFunctionCallsInFunction finds all function calls within a provider function
|
|
func (w *WireChecker) findFunctionCallsInFunction(funcDecl *ast.FuncDecl) []functionCall {
|
|
var calls []functionCall
|
|
|
|
ast.Inspect(funcDecl, func(n ast.Node) bool {
|
|
if callExpr, ok := n.(*ast.CallExpr); ok {
|
|
switch fun := callExpr.Fun.(type) {
|
|
case *ast.Ident:
|
|
// Direct function call: SomeFunction()
|
|
calls = append(calls, functionCall{
|
|
funcName: fun.Name,
|
|
pos: callExpr.Pos(),
|
|
pkg: "", // Same package
|
|
})
|
|
case *ast.SelectorExpr:
|
|
// Package function call: package.SomeFunction()
|
|
if pkgIdent, ok := fun.X.(*ast.Ident); ok {
|
|
calls = append(calls, functionCall{
|
|
funcName: fun.Sel.Name,
|
|
pos: callExpr.Pos(),
|
|
pkg: pkgIdent.Name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
return calls
|
|
}
|
|
|
|
// containsFunction checks if a function name is already in the call path
|
|
func (w *WireChecker) containsFunction(callPath []analysis.RelatedInformation, funcCall functionCall) bool {
|
|
for _, related := range callPath {
|
|
if related.Pos == funcCall.pos {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// findFunctionDeclaration finds a function declaration by name in the current analysis pass
|
|
func (w *WireChecker) findFunctionDeclaration(pass *analysis.Pass, funcName, pkg string) *ast.FuncDecl {
|
|
for _, file := range pass.Files {
|
|
for _, decl := range file.Decls {
|
|
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
|
|
if funcDecl.Name.Name == funcName {
|
|
// If pkg is specified, we need to check if this is from the right package
|
|
// For now, we'll assume same package if pkg is empty
|
|
if pkg == "" || pkg == pass.Pkg.Name() {
|
|
return funcDecl
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|