Guide to creating and registering custom code issue detectors.

Overview

Detectors analyze AST (Abstract Syntax Tree) to find specific code patterns that indicate performance issues. Each detector focuses on a particular type of problem.

Detector Architecture

┌─────────────────────────────────────────────────────────┐
│                    BaseDetector                          │
├─────────────────────────────────────────────────────────┤
│ + name: string                                           │
│ # issues: Issue[]                                        │
│                                                          │
│ + detect(ast, context): Promise<DetectorResult>          │
│ # createIssue(...): Issue                                │
│ # createImpact(...): EstimatedImpact                     │
│ # getCode(node, sourceCode): string                      │
│ # reset(): void                                          │
└─────────────────────────────────────────────────────────┘

                          │ extends
          ┌───────────────┼───────────────┐
          │               │               │
┌─────────┴─────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ N1QueryDetector│ │MemoryLeak  │ │ YourCustom  │
│               │ │ Detector    │ │ Detector    │
└───────────────┘ └─────────────┘ └─────────────┘

Step 1: Create Detector Class

Create a new file in backend/src/detectors/:

// backend/src/detectors/my-custom-detector.ts
import { BaseDetector } from './base-detector';
import { AnalysisContext, DetectorResult, Issue } from '../types';
import traverse from '@babel/traverse';
import * as t from '@babel/types';

export class MyCustomDetector extends BaseDetector {
  name = 'My Custom Detector';

  async detect(ast: any, context: AnalysisContext): Promise<DetectorResult> {
    this.reset();
    const issues: Issue[] = [];

    traverse(ast, {
      // Define which AST nodes to visit
      CallExpression: (path) => {
        if (this.isProblematicPattern(path, context)) {
          issues.push(this.createIssueFromNode(path, context));
        }
      },
      
      // Can visit multiple node types
      ForStatement: (path) => {
        // Check for loops
      },
      
      AwaitExpression: (path) => {
        // Check for async patterns
      }
    });

    return {
      issues,
      detectorName: this.name
    };
  }

  private isProblematicPattern(
    path: any, 
    context: AnalysisContext
  ): boolean {
    const node = path.node;
    
    // Example: Detect console.log in production code
    if (t.isMemberExpression(node.callee)) {
      const obj = node.callee.object;
      const prop = node.callee.property;
      
      if (t.isIdentifier(obj, { name: 'console' }) &&
          t.isIdentifier(prop, { name: 'log' })) {
        return true;
      }
    }
    
    return false;
  }

  private createIssueFromNode(
    path: any,
    context: AnalysisContext
  ): Issue {
    const node = path.node;
    const lineNumber = node.loc?.start.line || 0;
    const code = this.getCode(node, context.sourceCode);
    
    return this.createIssue(
      'console_log_detected',           // type
      'low',                             // severity
      context,
      lineNumber,
      'Console.log Detected',            // title
      'Console.log statements should be removed in production code.',
      code,                              // codeBefore
      '// Removed console.log',          // codeAfter (suggested fix)
      this.createImpact(
        20,                              // severityScore
        'Minor performance impact from logging',
        0.95,                            // confidenceScore
        'performance',                   // category
        'trivial',                       // fixDifficulty
        { logsRemoved: 1 }               // metrics
      )
    );
  }
}

Step 2: Register Detector

Add your detector to the CodeAnalyzer in backend/src/analyzer/code-analyzer.ts. Detectors are keyed by slug in initializeDetectors():

import { MyCustomDetector } from '../detectors/my-custom-detector';

// Inside initializeDetectors()
const all: Record<string, any> = {
  // ... existing detectors
  'my-custom': new MyCustomDetector(),
};

Add a matching entry to DETECTOR_REGISTRY so the slug can be selected per scan. All detectors run on every tier — there is no basic/advanced split.

Or add dynamically to an existing instance:

const analyzer = new CodeAnalyzer();
analyzer.addDetector(new MyCustomDetector());

Step 3: Add Solution Generator (Optional)

If your detector should have auto-generated solutions:

// In CodeAnalyzer constructor
this.generators.set('console_log_detected', new MyCustomSolutionGenerator());

Common AST Patterns

Detecting Function Calls

traverse(ast, {
  CallExpression: (path) => {
    const callee = path.node.callee;
    
    // Direct call: myFunction()
    if (t.isIdentifier(callee, { name: 'myFunction' })) {
      // Found it
    }
    
    // Method call: obj.method()
    if (t.isMemberExpression(callee)) {
      const objName = t.isIdentifier(callee.object) ? callee.object.name : null;
      const methodName = t.isIdentifier(callee.property) ? callee.property.name : null;
    }
  }
});

Detecting Loops

traverse(ast, {
  // for (let i = 0; i < n; i++)
  ForStatement: (path) => {
    const body = path.node.body;
    // Analyze loop body
  },
  
  // for (const item of items)
  ForOfStatement: (path) => {
    const right = path.node.right;  // The iterable
  },
  
  // for (const key in obj)
  ForInStatement: (path) => {},
  
  // while (condition)
  WhileStatement: (path) => {},
  
  // array.forEach()
  CallExpression: (path) => {
    if (t.isMemberExpression(path.node.callee) &&
        t.isIdentifier(path.node.callee.property, { name: 'forEach' })) {
      // Found forEach
    }
  }
});

Detecting Async Patterns

traverse(ast, {
  // async function
  FunctionDeclaration: (path) => {
    if (path.node.async) {
      // Async function
    }
  },
  
  // await expression
  AwaitExpression: (path) => {
    const argument = path.node.argument;
    // What's being awaited
  },
  
  // Check if inside a loop
  AwaitExpression: (path) => {
    const loopParent = path.findParent(p => 
      t.isForStatement(p) || 
      t.isForOfStatement(p) || 
      t.isWhileStatement(p)
    );
    
    if (loopParent) {
      // await inside loop - potential issue
    }
  }
});

Detecting Variable Declarations

traverse(ast, {
  VariableDeclaration: (path) => {
    path.node.declarations.forEach(decl => {
      if (t.isIdentifier(decl.id)) {
        const varName = decl.id.name;
        const init = decl.init;  // Initial value
      }
    });
  }
});

Using Context (ORM Detection)

async detect(ast: any, context: AnalysisContext): Promise<DetectorResult> {
  const { ormContext } = context;
  
  traverse(ast, {
    CallExpression: (path) => {
      // Check if this is an ORM call
      if (ormContext?.detectedORMs.has('prisma')) {
        // Prisma-specific detection
      }
      
      if (ormContext?.detectedORMs.has('sequelize')) {
        // Sequelize-specific detection
      }
    }
  });
}

Testing Your Detector

Create a test file:

// src/__tests__/my-custom-detector.test.ts
import { MyCustomDetector } from '../detectors/my-custom-detector';
import { CodeParser } from '../analyzer/parser';

describe('MyCustomDetector', () => {
  const detector = new MyCustomDetector();
  const parser = new CodeParser();

  test('detects console.log', async () => {
    const code = `
      function test() {
        console.log('debug');
      }
    `;
    
    const ast = parser.parse(code);
    const context = {
      sourceCode: code,
      filePath: 'test.js',
      ast
    };
    
    const result = await detector.detect(ast, context);
    
    expect(result.issues).toHaveLength(1);
    expect(result.issues[0].type).toBe('console_log_detected');
    expect(result.issues[0].severity).toBe('low');
  });

  test('ignores code without console.log', async () => {
    const code = `
      function test() {
        return 42;
      }
    `;
    
    const ast = parser.parse(code);
    const context = {
      sourceCode: code,
      filePath: 'test.js',
      ast
    };
    
    const result = await detector.detect(ast, context);
    
    expect(result.issues).toHaveLength(0);
  });
});

Run tests:

npm test -- my-custom-detector

Best Practices

1. Use Specific Node Types

// Good - specific
traverse(ast, {
  CallExpression: (path) => { /* ... */ }
});

// Avoid - too broad
traverse(ast, {
  enter: (path) => { /* checks every node */ }
});

2. Provide Confidence Scores

this.createImpact(
  severityScore,
  description,
  0.85,  // 85% confident
  // ...
);

3. Include Helpful Metrics

metrics: {
  queriesInLoop: 3,
  estimatedQueryReduction: '300%',
  affectedLines: [5, 12, 18]
}

4. Give Actionable Suggestions

codeBefore: 'for (const id of ids) { await User.findByPk(id); }',
codeAfter: 'const users = await User.findAll({ where: { id: ids } });'

5. Handle Edge Cases

private isProblematicPattern(path: any): boolean {
  // Guard against missing nodes
  if (!path.node || !path.node.callee) {
    return false;
  }
  
  // Check node type before accessing properties
  if (!t.isMemberExpression(path.node.callee)) {
    return false;
  }
  
  // ...
}

Example: Complete Custom Detector

// src/detectors/expensive-computation-detector.ts
import { BaseDetector } from './base-detector';
import { AnalysisContext, DetectorResult, Issue } from '../types';
import traverse from '@babel/traverse';
import * as t from '@babel/types';

export class ExpensiveComputationDetector extends BaseDetector {
  name = 'Expensive Computation Detector';

  private expensiveMethods = new Set([
    'sort', 'reverse', 'filter', 'map', 'reduce',
    'find', 'findIndex', 'some', 'every', 'flat', 'flatMap'
  ]);

  async detect(ast: any, context: AnalysisContext): Promise<DetectorResult> {
    this.reset();
    const issues: Issue[] = [];
    const chainedCalls: Map<number, string[]> = new Map();

    traverse(ast, {
      CallExpression: (path) => {
        // Detect chained array methods
        const chain = this.getMethodChain(path);
        
        if (chain.length >= 3) {
          const lineNumber = path.node.loc?.start.line || 0;
          
          if (!chainedCalls.has(lineNumber)) {
            chainedCalls.set(lineNumber, chain);
            
            issues.push(this.createIssue(
              'expensive_computation_chain',
              chain.length >= 4 ? 'high' : 'medium',
              context,
              lineNumber,
              `Chained Array Methods (${chain.length} operations)`,
              `Multiple array method calls (${chain.join(' → ')}) create intermediate arrays. Consider combining into a single reduce() or for loop.`,
              this.getCode(path.node, context.sourceCode),
              this.generateSuggestion(chain),
              this.createImpact(
                chain.length * 15,
                `${chain.length} array iterations instead of 1`,
                0.9,
                'performance',
                'moderate',
                {
                  chainLength: chain.length,
                  methods: chain,
                  estimatedIterations: `${chain.length}n`
                }
              )
            ));
          }
        }
      }
    });

    return { issues, detectorName: this.name };
  }

  private getMethodChain(path: any): string[] {
    const chain: string[] = [];
    let current = path.node;

    while (t.isCallExpression(current)) {
      if (t.isMemberExpression(current.callee) &&
          t.isIdentifier(current.callee.property)) {
        const methodName = current.callee.property.name;
        
        if (this.expensiveMethods.has(methodName)) {
          chain.unshift(methodName);
          current = current.callee.object;
        } else {
          break;
        }
      } else {
        break;
      }
    }

    return chain;
  }

  private generateSuggestion(chain: string[]): string {
    if (chain.includes('filter') && chain.includes('map')) {
      return `// Consider using reduce() to combine operations:
// array.reduce((acc, item) => {
//   if (/* filter condition */) {
//     acc.push(/* mapped value */);
//   }
//   return acc;
// }, []);`;
    }
    return '// Consider combining into a single iteration';
  }
}

Next Steps