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
177 lines
4.6 KiB
Go
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 ""
|
|
}
|
|
}
|