fetch_ml/tools/fetchml-vet/analyzers/nobaredetector.go
Jeremie Fraeys 6fc2e373c1
fix: resolve IDE warnings and test errors
Bug fixes and cleanup for test infrastructure:

- schema_test.go: Fix SchemaVersion reference with proper manifest import
- schema_test.go: Update all schema.json paths to internal/manifest location
- manifestenv.go: Remove unused helper functions (isArtifactsType, getPackagePath)
- nobaredetector.go: Fix exprToString syntax error, remove unused functions

All tests now pass without errors or warnings
2026-02-23 20:26:20 -05:00

177 lines
4.6 KiB
Go

// Package analyzers provides custom Go analysis rules for FetchML code quality and security
package analyzers
import (
"go/ast"
"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 ""
}
}