// Package analyzers provides custom Go analysis rules for FetchML code quality and security package analyzers import ( "go/ast" "go/token" "go/types" "strings" "golang.org/x/tools/go/analysis" ) // NoBareDetectorAnalyzer flags calls to GPUDetectorFactory.CreateDetector() that // silently discard DetectionInfo needed for manifest and audit log. // Use CreateDetectorWithInfo() instead to capture both the detector and its metadata. var NoBareDetectorAnalyzer = &analysis.Analyzer{ Name: "nobaredetector", Doc: "flags bare CreateDetector calls that discard DetectionInfo", Run: runNoBareDetector, } func runNoBareDetector(pass *analysis.Pass) (interface{}, error) { for _, file := range pass.Files { ast.Inspect(file, func(n ast.Node) bool { // Look for call expressions call, ok := n.(*ast.CallExpr) if !ok { return true } // Check if it's a method call like obj.CreateDetector(...) sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return true } // Check if the method name is CreateDetector if sel.Sel.Name != "CreateDetector" { return true } // Check if the result is assigned (which is okay) // If the parent is an assignment or short var decl, it's being captured if isAssigned(pass, call) { return true } // Check if the call is part of a selector chain that includes CreateDetectorWithInfo // This is a heuristic - if the same object has CreateDetectorWithInfo called on it, // we assume they're doing the right thing if hasCreateDetectorWithInfo(pass, call, sel) { return true } pass.Reportf(call.Pos(), "bare CreateDetector() call detected - use CreateDetectorWithInfo() to capture DetectionInfo for manifest/audit") return true }) } return nil, nil } // isAssigned checks if a call expression's result is being assigned to a variable func isAssigned(pass *analysis.Pass, call *ast.CallExpr) bool { // Walk up the AST to see if this call is the RHS of an assignment // This is a simplified check - in a full implementation, we'd use pass.TypesInfo // to track the parent node pos := call.Pos() for _, file := range pass.Files { if file.Pos() <= pos && pos <= file.End() { var found bool ast.Inspect(file, func(n ast.Node) bool { if n == nil { return true } if n.Pos() == pos { // Check if parent is an assignment or short var decl return false // Stop searching } // Check for assignment statement containing this call switch node := n.(type) { case *ast.AssignStmt: for _, rhs := range node.Rhs { if rhs.Pos() == pos { found = true return false } } case *ast.ValueSpec: for _, val := range node.Values { if val.Pos() == pos { found = true return false } } } return true }) if found { return true } } } return false } // hasCreateDetectorWithInfo checks if the same object that has CreateDetector called // also has CreateDetectorWithInfo called somewhere in the function func hasCreateDetectorWithInfo(pass *analysis.Pass, call *ast.CallExpr, sel *ast.SelectorExpr) bool { // Get the object being called on (the receiver) receiverObj := sel.X // Find the containing function var containingFunc *ast.FuncDecl for _, file := range pass.Files { if file.Pos() <= call.Pos() && call.Pos() <= file.End() { ast.Inspect(file, func(n ast.Node) bool { if fn, ok := n.(*ast.FuncDecl); ok { if fn.Pos() <= call.Pos() && call.Pos() <= fn.End() { containingFunc = fn return false } } return true }) break } } if containingFunc == nil { return false } // Check if CreateDetectorWithInfo is called on the same object in the same function found := false ast.Inspect(containingFunc, func(n ast.Node) bool { call2, ok := n.(*ast.CallExpr) if !ok { return true } sel2, ok := call2.Fun.(*ast.SelectorExpr) if !ok { return true } if sel2.Sel.Name != "CreateDetectorWithInfo" { return true } // Check if it's the same receiver object if isSameObject(receiverObj, sel2.X) { found = true return false } return true }) return found } // isSameObject checks if two AST expressions refer to the same object func isSameObject(a, b ast.Expr) bool { // Simple string comparison for identifiers and selectors // In a full implementation, we'd use types.Object from type checking aStr := exprToString(a) bStr := exprToString(b) return aStr == bStr } // exprToString converts an expression to a simple string representation func exprToString(expr ast.Expr) string { switch e := expr.(type) { case *ast.Ident: return e.Name case *ast.SelectorExpr: return exprToString(e.X) + "." + e.Sel.Name default: return "" } } // getTypeName returns a simple type name string func getTypeName(t types.Type) string { if t == nil { return "" } return strings.TrimPrefix(t.String(), "github.com/jfraeys/fetch_ml/") } // isWithinFunction checks if a position is within a function declaration func isWithinFunction(fset *token.FileSet, pos token.Pos, fn *ast.FuncDecl) bool { if fn == nil { return false } return fn.Pos() <= pos && pos <= fn.End() }