Comprehensive guide for developers working on or extending CodeGuard
- Architecture Overview
- Core Components
- API Reference
- Creating Custom Analyzers
- Performance Optimization
- Testing Strategy
- Debugging Guide
- Extension Points
CodeGuard follows a modular, plugin-based architecture designed for performance, extensibility, and maintainability.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β VSCode Extension Host β
β ββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
β β Extension β β Diagnostic β β Providers β β
β β Entry Pointββββ Manager ββββ (Actions/Hover) β β
β ββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
β β β β
β β Worker Thread Communication β β
β βΌ βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Analysis Engine (Worker Thread) β β
β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β
β β β Security β βPerformanceβ β Code Smells β β β
β β β Analyzer β β Analyzer β β Detector β β β
β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β
β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β
β β β Secret β βIncrementalβ β Cache Manager β β β
β β β Scanner β β Analyzer β β (LRU + Disk) β β β
β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β Optional AI Integration β
βΌ βΌ
ββββββββββββββββ ββββββββββββββββ
β AI Service β β Local Cache β
β (OpenAI/ β β (SQLite) β
β Claude/ β ββββββββββββββββ
β Ollama) β
ββββββββββββββββ
- Local-First: All static analysis runs locally without network calls
- Non-Blocking: Analysis runs in worker threads to keep UI responsive
- Incremental: Only analyze changed regions in large files
- Cached: Aggressive caching with content-based hashing
- Modular: Pluggable analyzer architecture for easy extension
- User Types β VSCode fires
onDidChangeTextDocumentevent - Debouncer β Waits 500ms (configurable) after typing stops
- Analysis Engine β Receives analysis request with file content
- Cache Check β Computes SHA-256 hash, checks memory/disk cache
- Parallel Analysis β Executes all enabled analyzers concurrently
- Result Aggregation β Collects diagnostics from all analyzers
- Diagnostic Display β Updates VSCode diagnostic collection
- UI Update β Status bar and hover providers reflect new state
Main Thread (VSCode UI)
βββ Extension activation
βββ Event listeners (document changes)
βββ Diagnostic display
βββ Code action handling
βββ Worker thread communication
Worker Thread (Analysis)
βββ Analysis engine coordination
βββ All analyzer execution
βββ AST parsing
βββ Pattern matching
βββ Cache management
The main entry point that initializes all components and registers VSCode providers.
Key Responsibilities:
- Extension activation and deactivation
- Document change listener registration with debouncing
- Diagnostic collection initialization
- Configuration loading
- Worker thread initialization
Important Functions:
export function activate(context: vscode.ExtensionContext): void
export function deactivate(): Promise<void>Activation Events:
onLanguage:javascriptonLanguage:typescriptonLanguage:pythononLanguage:java
Coordinates analyzer execution, manages caching, and handles timeouts.
Key Responsibilities:
- Parallel analyzer execution using
Promise.all - Content hash computation (SHA-256) for cache keys
- Timeout handling (5 second limit per analyzer)
- Result aggregation from multiple analyzers
- Cache integration
Core Interface:
interface AnalysisEngine {
analyze(request: AnalysisRequest): Promise<AnalysisResult>;
analyzeIncremental(request: IncrementalAnalysisRequest): Promise<AnalysisResult>;
invalidateCache(fileUri: string): void;
}
interface AnalysisRequest {
fileUri: string;
content: string;
languageId: string;
version: number;
}
interface AnalysisResult {
diagnostics: Diagnostic[];
analysisTimeMs: number;
cacheHit: boolean;
analyzersRun: string[];
}Performance Characteristics:
- <100ms for files under 1000 lines
- Parallel execution of all analyzers
- Automatic timeout after 5 seconds
- Cache hit returns results in <10ms
Defines the contract that all analyzers must implement.
Interface Definition:
interface Analyzer {
readonly name: string;
readonly supportedLanguages: string[];
analyze(context: AnalysisContext): Promise<Diagnostic[]>;
supportsIncremental(): boolean;
analyzeIncremental?(context: IncrementalAnalysisContext): Promise<Diagnostic[]>;
}
interface AnalysisContext {
fileUri: string;
content: string;
languageId: string;
syntaxTree?: SyntaxTree;
config: AnalyzerConfig;
}Implementation Requirements:
- Must return diagnostics array (can be empty)
- Must handle errors gracefully (catch and log)
- Should complete within 5 seconds
- Should support incremental analysis for large files
- Must respect configuration settings
Implements two-level caching (memory + disk) with LRU eviction.
Architecture:
class TwoLevelCache implements CacheManager {
private memoryCache: LRUCache<string, AnalysisResult>; // 50MB default
private diskCache: SQLiteCache; // 500MB default
async get(key: string): Promise<AnalysisResult | null>;
async set(key: string, value: AnalysisResult): Promise<void>;
async invalidate(key: string): Promise<void>;
async clear(): Promise<void>;
getStats(): CacheStats;
}Caching Strategy:
- Check memory cache first (fastest)
- If miss, check disk cache
- If disk hit, promote to memory cache
- On set, write to both memory and disk
- LRU eviction when limits exceeded
Cache Key Generation:
function computeCacheKey(content: string, languageId: string): string {
const hash = crypto.createHash('sha256')
.update(content)
.update(languageId)
.digest('hex');
return `${languageId}:${hash}`;
}Manages VSCode diagnostic collection and updates.
Key Features:
- Debounced diagnostic updates
- Diagnostic persistence during analysis
- Automatic cleanup on file close
- Severity-based grouping
Interface:
interface DiagnosticManager {
updateDiagnostics(uri: Uri, diagnostics: Diagnostic[]): void;
clearDiagnostics(uri: Uri): void;
getDiagnostics(uri: Uri): Diagnostic[];
getAllDiagnostics(): [Uri, Diagnostic[]][];
}Provides quick fixes and actions for diagnostics.
Available Actions:
- Fix with AI: AI-powered fix suggestions (when enabled)
- Ignore Issue: Add to
.codeguardignorefile - Explain Issue: Show detailed documentation
Implementation:
class CodeGuardActionProvider implements vscode.CodeActionProvider {
provideCodeActions(
document: TextDocument,
range: Range,
context: CodeActionContext
): Promise<CodeAction[]>;
}Abstracts AI provider integration for fix suggestions.
Supported Providers:
- OpenAI (GPT-4, GPT-3.5)
- Anthropic Claude (Opus, Sonnet, Haiku)
- Ollama (local models)
Architecture:
interface AIService {
generateFix(request: FixRequest): Promise<FixSuggestion>;
explainIssue(diagnostic: Diagnostic, context: string): Promise<string>;
isAvailable(): boolean;
}
interface AIClient {
complete(prompt: string, options: CompletionOptions): Promise<string>;
getRateLimitInfo(): RateLimitInfo;
}Privacy Features:
- Only sends minimal context (snippet + 10 lines)
- Never sends entire files
- Caches responses to reduce API calls
- Optional - can be completely disabled
Error Handling:
- Timeout after 3 seconds
- Fallback to rule-based fixes
- Rate limit handling with exponential backoff
- Graceful degradation on network errors
import { Analyzer, AnalysisContext, Diagnostic } from '../types';
import * as vscode from 'vscode';
export class MyCustomAnalyzer implements Analyzer {
readonly name = 'my-custom-analyzer';
readonly supportedLanguages = ['javascript', 'typescript'];
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
const diagnostics: Diagnostic[] = [];
// Your analysis logic here
const issues = this.detectIssues(context.content);
for (const issue of issues) {
diagnostics.push({
range: new vscode.Range(
issue.line, issue.column,
issue.line, issue.column + issue.length
),
severity: vscode.DiagnosticSeverity.Warning,
message: issue.message,
source: 'codeguard',
code: 'my-rule-id'
});
}
return diagnostics;
}
supportsIncremental(): boolean {
return false;
}
private detectIssues(content: string): Issue[] {
// Implementation
return [];
}
}Add your analyzer to the analysis engine:
// In src/engine/AnalysisEngine.ts
import { MyCustomAnalyzer } from '../analyzers/MyCustomAnalyzer';
constructor() {
this.analyzers = [
new SecurityAnalyzer(),
new PerformanceAnalyzer(),
new SecretScanner(),
new CodeSmellDetector(),
new MyCustomAnalyzer() // Add your analyzer
];
}Add configuration options in package.json:
{
"contributes": {
"configuration": {
"properties": {
"codeguard.analyzers.myCustomAnalyzer.enabled": {
"type": "boolean",
"default": true,
"description": "Enable my custom analyzer"
},
"codeguard.analyzers.myCustomAnalyzer.severity": {
"type": "object",
"default": {
"my-rule-id": "warning"
},
"description": "Severity levels for custom analyzer rules"
}
}
}
}
}import { ConfigurationManager } from './config/ConfigurationManager';
const configManager = new ConfigurationManager();
const config = configManager.getConfig();
// Access analyzer settings
if (config.analyzers.security.enabled) {
// Run security analysis
}
// Access custom rules
const maxComplexity = config.analyzers.codeSmells.customRules?.maxComplexity || 10;const disposable = configManager.watchConfig((newConfig) => {
console.log('Configuration changed:', newConfig);
// React to configuration changes
});
// Clean up when done
disposable.dispose();import { CacheManager } from './cache/CacheManager';
const cache = new CacheManager({
maxMemoryMB: 50,
maxDiskMB: 500,
ttlSeconds: 3600
});
// Get cached result
const cached = await cache.get(cacheKey);
if (cached) {
return cached;
}
// Perform analysis
const result = await performAnalysis();
// Store in cache
await cache.set(cacheKey, result);
// Invalidate cache entry
await cache.invalidate(cacheKey);
// Get cache statistics
const stats = cache.getStats();
console.log(`Hit rate: ${stats.hitRate}%`);import * as crypto from 'crypto';
function computeCacheKey(
content: string,
languageId: string,
analyzerName: string
): string {
const hash = crypto.createHash('sha256')
.update(content)
.update(languageId)
.update(analyzerName)
.digest('hex');
return `${analyzerName}:${languageId}:${hash}`;
}import { AIService } from './ai/AIService';
const aiService = new AIService({
provider: 'openai',
apiKey: 'sk-...',
model: 'gpt-4'
});
// Generate fix
const fix = await aiService.generateFix({
diagnostic: diagnostic,
codeSnippet: problematicCode,
contextBefore: beforeCode,
contextAfter: afterCode,
languageId: 'typescript'
});
console.log('Fixed code:', fix.fixedCode);
console.log('Explanation:', fix.explanation);
console.log('Confidence:', fix.confidence);import { AIClient, CompletionOptions } from './ai/AIService';
export class MyAIClient implements AIClient {
constructor(private apiKey: string, private model: string) {}
async complete(prompt: string, options: CompletionOptions): Promise<string> {
const response = await fetch('https://api.myai.com/v1/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: this.model,
prompt,
max_tokens: options.maxTokens,
temperature: options.temperature
})
});
const data = await response.json();
return data.choices[0].text;
}
getRateLimitInfo(): RateLimitInfo {
return {
remaining: 100,
reset: Date.now() + 3600000
};
}
}Create a new file src/analyzers/MyAnalyzer.ts:
import { Analyzer, AnalysisContext, Diagnostic } from '../types';
import * as vscode from 'vscode';
export class MyAnalyzer implements Analyzer {
readonly name = 'my-analyzer';
readonly supportedLanguages = ['javascript', 'typescript'];
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
const diagnostics: Diagnostic[] = [];
// Parse the code
const lines = context.content.split('\n');
// Analyze each line
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for issues
if (this.hasIssue(line)) {
diagnostics.push(this.createDiagnostic(i, line));
}
}
return diagnostics;
}
supportsIncremental(): boolean {
return false;
}
private hasIssue(line: string): boolean {
// Your detection logic
return line.includes('problematic-pattern');
}
private createDiagnostic(lineNumber: number, line: string): Diagnostic {
const startColumn = line.indexOf('problematic-pattern');
const endColumn = startColumn + 'problematic-pattern'.length;
return {
range: new vscode.Range(lineNumber, startColumn, lineNumber, endColumn),
severity: vscode.DiagnosticSeverity.Warning,
message: 'Problematic pattern detected',
source: 'codeguard',
code: 'my-rule-id',
relatedInformation: [
{
location: new vscode.Location(
vscode.Uri.file(context.fileUri),
new vscode.Range(lineNumber, startColumn, lineNumber, endColumn)
),
message: 'Consider using alternative pattern'
}
]
};
}
}For more sophisticated analysis, use regex patterns:
export class PatternBasedAnalyzer implements Analyzer {
private patterns: Pattern[] = [
{
id: 'unsafe-eval',
regex: /\beval\s*\(/g,
severity: vscode.DiagnosticSeverity.Error,
message: 'Avoid using eval() - it can execute arbitrary code'
},
{
id: 'console-log',
regex: /console\.log\(/g,
severity: vscode.DiagnosticSeverity.Information,
message: 'Remove console.log before production'
}
];
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
const diagnostics: Diagnostic[] = [];
for (const pattern of this.patterns) {
const matches = context.content.matchAll(pattern.regex);
for (const match of matches) {
const position = this.getPosition(context.content, match.index!);
diagnostics.push({
range: new vscode.Range(
position.line,
position.character,
position.line,
position.character + match[0].length
),
severity: pattern.severity,
message: pattern.message,
source: 'codeguard',
code: pattern.id
});
}
}
return diagnostics;
}
private getPosition(content: string, index: number): vscode.Position {
const lines = content.substring(0, index).split('\n');
const line = lines.length - 1;
const character = lines[lines.length - 1].length;
return new vscode.Position(line, character);
}
}For deeper analysis, parse the code into an AST:
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
export class ASTBasedAnalyzer implements Analyzer {
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
const diagnostics: Diagnostic[] = [];
try {
// Parse code to AST
const ast = parser.parse(context.content, {
sourceType: 'module',
plugins: ['typescript', 'jsx']
});
// Traverse AST
traverse(ast, {
FunctionDeclaration: (path) => {
// Check function length
const loc = path.node.loc;
if (loc) {
const lines = loc.end.line - loc.start.line;
if (lines > 50) {
diagnostics.push({
range: new vscode.Range(
loc.start.line - 1,
loc.start.column,
loc.end.line - 1,
loc.end.column
),
severity: vscode.DiagnosticSeverity.Information,
message: `Function is ${lines} lines long (max: 50)`,
source: 'codeguard',
code: 'long-function'
});
}
}
},
CallExpression: (path) => {
// Check for dangerous function calls
if (path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'eval') {
const loc = path.node.loc;
if (loc) {
diagnostics.push({
range: new vscode.Range(
loc.start.line - 1,
loc.start.column,
loc.end.line - 1,
loc.end.column
),
severity: vscode.DiagnosticSeverity.Error,
message: 'Avoid using eval() - security risk',
source: 'codeguard',
code: 'unsafe-eval'
});
}
}
}
});
} catch (error) {
// Handle parse errors gracefully
console.error('Parse error:', error);
}
return diagnostics;
}
}For large files, support incremental analysis:
export class IncrementalAnalyzer implements Analyzer {
private syntaxTreeCache = new Map<string, SyntaxTree>();
supportsIncremental(): boolean {
return true;
}
async analyzeIncremental(
context: IncrementalAnalysisContext
): Promise<Diagnostic[]> {
// Get cached syntax tree
let syntaxTree = this.syntaxTreeCache.get(context.fileUri);
if (!syntaxTree) {
// First analysis - parse entire file
syntaxTree = this.parseFile(context.content);
this.syntaxTreeCache.set(context.fileUri, syntaxTree);
return this.analyze(context);
}
// Incremental analysis - only analyze affected regions
const affectedNodes = this.getAffectedNodes(
syntaxTree,
context.affectedRanges
);
const diagnostics: Diagnostic[] = [];
for (const node of affectedNodes) {
diagnostics.push(...this.analyzeNode(node));
}
return diagnostics;
}
private getAffectedNodes(
tree: SyntaxTree,
ranges: vscode.Range[]
): ASTNode[] {
const affected: ASTNode[] = [];
for (const range of ranges) {
// Find nodes that overlap with changed ranges
const nodes = this.findNodesInRange(tree.root, range);
affected.push(...nodes);
}
return affected;
}
}Make your analyzer configurable:
export class ConfigurableAnalyzer implements Analyzer {
private config: AnalyzerConfig;
constructor(config: AnalyzerConfig) {
this.config = config;
}
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
// Check if analyzer is enabled
if (!this.config.enabled) {
return [];
}
const diagnostics: Diagnostic[] = [];
// Use custom rules from config
const maxLength = this.config.customRules?.maxLength || 50;
const severity = this.config.severity['long-function'] ||
vscode.DiagnosticSeverity.Information;
// Analyze with configured settings
const issues = this.detectIssues(context.content, maxLength);
for (const issue of issues) {
diagnostics.push({
...issue,
severity: severity
});
}
return diagnostics;
}
}Create comprehensive tests for your analyzer:
// src/analyzers/MyAnalyzer.test.ts
import { describe, it, expect } from 'vitest';
import { MyAnalyzer } from './MyAnalyzer';
describe('MyAnalyzer', () => {
const analyzer = new MyAnalyzer();
describe('Pattern Detection', () => {
it('should detect problematic pattern', async () => {
const code = 'const x = problematic-pattern();';
const diagnostics = await analyzer.analyze({
fileUri: 'test.js',
content: code,
languageId: 'javascript',
config: { enabled: true, severity: {} }
});
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].code).toBe('my-rule-id');
expect(diagnostics[0].severity).toBe(vscode.DiagnosticSeverity.Warning);
});
it('should not flag safe patterns', async () => {
const code = 'const x = safePattern();';
const diagnostics = await analyzer.analyze({
fileUri: 'test.js',
content: code,
languageId: 'javascript',
config: { enabled: true, severity: {} }
});
expect(diagnostics).toHaveLength(0);
});
});
describe('Edge Cases', () => {
it('should handle empty input', async () => {
const diagnostics = await analyzer.analyze({
fileUri: 'test.js',
content: '',
languageId: 'javascript',
config: { enabled: true, severity: {} }
});
expect(diagnostics).toHaveLength(0);
});
it('should handle malformed code gracefully', async () => {
const code = 'const x = {{{';
const diagnostics = await analyzer.analyze({
fileUri: 'test.js',
content: code,
languageId: 'javascript',
config: { enabled: true, severity: {} }
});
// Should not throw, may return empty or partial results
expect(Array.isArray(diagnostics)).toBe(true);
});
});
});- Keep analysis fast: Target <100ms for typical files
- Use incremental analysis: For files >5000 lines
- Cache expensive operations: AST parsing, regex compilation
- Avoid blocking operations: Use async/await
- Limit regex backtracking: Use atomic groups, possessive quantifiers
// Good: Fast regex with atomic group
const pattern = /(?:const|let|var)\s+(\w+)/g;
// Bad: Slow regex with backtracking
const pattern = /(const|let|var)\s+(\w+)*/g;- Minimize false positives: Use data flow analysis when possible
- Provide clear messages: Explain what's wrong and why
- Include fix suggestions: Help users resolve issues
- Test thoroughly: Unit tests + property-based tests
- Handle edge cases: Empty files, malformed code, comments
// Good: Clear, actionable message
message: 'SQL injection risk: Use parameterized queries instead of string concatenation'
// Bad: Vague message
message: 'Security issue detected'- Document patterns: Explain what each pattern detects
- Use descriptive names: For rules, functions, variables
- Separate concerns: Detection logic vs diagnostic creation
- Make it configurable: Allow users to customize behavior
- Version your rules: Track changes to detection logic
/**
* Detects SQL injection vulnerabilities by identifying string concatenation
* in SQL query contexts. Uses data flow analysis to track user input.
*
* @example
* // Detected:
* const query = "SELECT * FROM users WHERE id = " + userId;
*
* // Safe:
* const query = db.prepare("SELECT * FROM users WHERE id = ?").bind(userId);
*/
private detectSQLInjection(ast: AST): Diagnostic[] {
// Implementation
}# Run all benchmarks
npm run benchmark
# Run specific benchmark
npm run benchmark -- --grep "Cache performance"
# Run with profiling
node --prof dist/benchmarks/performance.bench.js// src/benchmarks/my-feature.bench.ts
import { describe, bench } from 'vitest';
import { MyAnalyzer } from '../analyzers/MyAnalyzer';
describe('MyAnalyzer Performance', () => {
const analyzer = new MyAnalyzer();
const smallFile = generateCode(100); // 100 lines
const mediumFile = generateCode(500); // 500 lines
const largeFile = generateCode(1000); // 1000 lines
bench('analyze small file (100 lines)', async () => {
await analyzer.analyze({
fileUri: 'test.js',
content: smallFile,
languageId: 'javascript'
});
});
bench('analyze medium file (500 lines)', async () => {
await analyzer.analyze({
fileUri: 'test.js',
content: mediumFile,
languageId: 'javascript'
});
});
bench('analyze large file (1000 lines)', async () => {
await analyzer.analyze({
fileUri: 'test.js',
content: largeFile,
languageId: 'javascript'
});
});
});Cache expensive computations:
export class OptimizedAnalyzer implements Analyzer {
private regexCache = new Map<string, RegExp>();
private astCache = new Map<string, AST>();
private getRegex(pattern: string): RegExp {
if (!this.regexCache.has(pattern)) {
this.regexCache.set(pattern, new RegExp(pattern, 'g'));
}
return this.regexCache.get(pattern)!;
}
private getAST(content: string, hash: string): AST {
if (!this.astCache.has(hash)) {
this.astCache.set(hash, this.parseCode(content));
}
return this.astCache.get(hash)!;
}
}Defer expensive operations until needed:
export class LazyAnalyzer implements Analyzer {
private _ast?: AST;
private get ast(): AST {
if (!this._ast) {
this._ast = this.parseCode(this.content);
}
return this._ast;
}
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
// Only parse if we need AST-based analysis
if (this.needsAST(context)) {
return this.analyzeWithAST(this.ast);
}
return this.analyzeWithRegex(context.content);
}
}Process independent tasks concurrently:
export class ParallelAnalyzer implements Analyzer {
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
// Run multiple checks in parallel
const [
securityIssues,
performanceIssues,
styleIssues
] = await Promise.all([
this.checkSecurity(context),
this.checkPerformance(context),
this.checkStyle(context)
]);
return [
...securityIssues,
...performanceIssues,
...styleIssues
];
}
}Stop processing when possible:
export class EarlyExitAnalyzer implements Analyzer {
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
// Check if file is too large
if (context.content.length > 1000000) {
return [{
range: new vscode.Range(0, 0, 0, 0),
severity: vscode.DiagnosticSeverity.Information,
message: 'File too large for analysis',
source: 'codeguard',
code: 'file-too-large'
}];
}
// Check if language is supported
if (!this.supportedLanguages.includes(context.languageId)) {
return [];
}
// Proceed with analysis
return this.performAnalysis(context);
}
}Only reprocess changed regions:
export class IncrementalOptimizer implements Analyzer {
private previousResults = new Map<string, Diagnostic[]>();
async analyzeIncremental(
context: IncrementalAnalysisContext
): Promise<Diagnostic[]> {
const previous = this.previousResults.get(context.fileUri) || [];
// Filter out diagnostics in affected ranges
const unaffected = previous.filter(d =>
!context.affectedRanges.some(range => range.contains(d.range))
);
// Analyze only affected ranges
const newDiagnostics = await this.analyzeRanges(
context,
context.affectedRanges
);
// Merge results
const merged = [...unaffected, ...newDiagnostics];
this.previousResults.set(context.fileUri, merged);
return merged;
}
}export class MemoryEfficientCache {
private cache = new LRUCache<string, AnalysisResult>({
max: 100, // Maximum entries
maxSize: 50 * 1024 * 1024, // 50MB
sizeCalculation: (value) => {
return JSON.stringify(value).length;
},
dispose: (value, key) => {
console.log(`Evicting cache entry: ${key}`);
}
});
}export class ResourceAwareAnalyzer implements Analyzer {
private workers: Worker[] = [];
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
const worker = new Worker('./analyzer-worker.js');
this.workers.push(worker);
try {
return await this.runInWorker(worker, context);
} finally {
// Clean up worker
worker.terminate();
this.workers = this.workers.filter(w => w !== worker);
}
}
dispose(): void {
// Terminate all workers
for (const worker of this.workers) {
worker.terminate();
}
this.workers = [];
}
}| Metric | Target | Measurement |
|---|---|---|
| Analysis time (< 1000 lines) | < 100ms | 95th percentile |
| Analysis time (1000-5000 lines) | < 500ms | 95th percentile |
| Analysis time (> 5000 lines) | < 1000ms | 95th percentile (incremental) |
| Cache hit latency | < 10ms | Average |
| Memory usage (idle) | < 5MB | Maximum |
| Memory usage (active) | < 100MB | 95th percentile |
| Extension activation | < 500ms | Maximum |
# Start VSCode with profiling
code --prof-startup
# Analyze profile
node --prof-process isolate-*.log > profile.txt- Open VSCode
- Help β Toggle Developer Tools
- Performance tab β Record
- Trigger analysis
- Stop recording and analyze
# Run with profiler
node --prof dist/extension.js
# Generate report
node --prof-process isolate-*.log > profile.txtTest individual functions and components:
describe('SecurityAnalyzer', () => {
it('should detect SQL injection', () => {
const code = 'const query = "SELECT * FROM users WHERE id = " + userId;';
const diagnostics = analyzer.analyze({ content: code });
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].code).toBe('sql-injection');
});
});Test universal properties with randomized inputs:
import fc from 'fast-check';
// Feature: codeguard, Property 17: Cache Round-Trip Consistency
it('Property 17: cached results match fresh analysis', async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 10, maxLength: 1000 }),
async (content) => {
const result1 = await engine.analyze({ content });
const result2 = await engine.analyze({ content });
expect(result2.cacheHit).toBe(true);
expect(result2.diagnostics).toEqual(result1.diagnostics);
}
),
{ numRuns: 100 }
);
});Test component interactions:
describe('Analysis Engine Integration', () => {
it('should coordinate multiple analyzers', async () => {
const engine = new AnalysisEngine();
const result = await engine.analyze({
fileUri: 'test.js',
content: 'const x = eval("code");',
languageId: 'javascript'
});
expect(result.analyzersRun).toContain('security');
expect(result.diagnostics.length).toBeGreaterThan(0);
});
});Test complete user workflows:
describe('E2E: User Workflow', () => {
it('should analyze file on open', async () => {
// Open file
const doc = await vscode.workspace.openTextDocument({
content: 'const x = eval("code");',
language: 'javascript'
});
// Wait for analysis
await waitForDiagnostics(doc.uri);
// Verify diagnostics
const diagnostics = vscode.languages.getDiagnostics(doc.uri);
expect(diagnostics.length).toBeGreaterThan(0);
});
});- Unit tests: >80% code coverage
- Property tests: All 39 design properties
- Integration tests: All component interactions
- E2E tests: All major user workflows
# All tests
npm test
# Unit tests only
npm test -- --grep -v "Property"
# Property tests only
npm test -- --grep "Property"
# With coverage
npm run test:coverage
# Watch mode
npm test -- --watch
# Specific file
npm test -- src/analyzers/SecurityAnalyzer.test.ts.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "npm: compile"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/dist/test"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "npm: compile"
}
]
}- Set breakpoints: Click in gutter or press F9
- Start debugging: Press F5
- Extension Development Host opens: New VSCode window
- Trigger functionality: Open file, type code, etc.
- Debugger pauses: At breakpoints
- Inspect variables: Hover or use Debug panel
- Step through code: F10 (step over), F11 (step into)
Use the Debug Console for interactive debugging:
// In your code
debugger; // Pauses execution
// In Debug Console
> context.content
> diagnostics.length
> JSON.stringify(result, null, 2)import * as vscode from 'vscode';
const outputChannel = vscode.window.createOutputChannel('CodeGuard');
// Log messages
outputChannel.appendLine('[INFO] Analysis started');
outputChannel.appendLine(`[DEBUG] Found ${diagnostics.length} issues`);
outputChannel.appendLine('[ERROR] Analysis failed: ' + error.message);
// Show output panel
outputChannel.show();class Logger {
private output: vscode.OutputChannel;
private logLevel: 'error' | 'warn' | 'info' | 'debug';
constructor(name: string, level: string = 'info') {
this.output = vscode.window.createOutputChannel(name);
this.logLevel = level as any;
}
error(message: string, ...args: any[]): void {
this.log('ERROR', message, args);
}
warn(message: string, ...args: any[]): void {
if (this.shouldLog('warn')) {
this.log('WARN', message, args);
}
}
info(message: string, ...args: any[]): void {
if (this.shouldLog('info')) {
this.log('INFO', message, args);
}
}
debug(message: string, ...args: any[]): void {
if (this.shouldLog('debug')) {
this.log('DEBUG', message, args);
}
}
private log(level: string, message: string, args: any[]): void {
const timestamp = new Date().toISOString();
const formatted = `[${timestamp}] [${level}] ${message}`;
this.output.appendLine(formatted);
if (args.length > 0) {
this.output.appendLine(JSON.stringify(args, null, 2));
}
}
private shouldLog(level: string): boolean {
const levels = ['error', 'warn', 'info', 'debug'];
return levels.indexOf(level) <= levels.indexOf(this.logLevel);
}
}
// Usage
const logger = new Logger('CodeGuard', 'debug');
logger.info('Analysis started', { fileUri, languageId });
logger.debug('Cache hit', { key, hitRate });
logger.error('Analysis failed', { error: error.message });Symptoms: Extension doesn't load, no diagnostics appear
Debug steps:
- Check Output panel (View β Output β Extension Host)
- Verify activation events in
package.json - Check for compilation errors:
npm run compile - Verify
mainfield inpackage.jsonpoints todist/extension.js
Solution:
# Clean and rebuild
rm -rf dist/
npm run compile
# Check for errors
npm run lintSymptoms: Code has issues but no squiggly lines appear
Debug steps:
- Check if analyzer is enabled in settings
- Verify file language is supported
- Check Output panel for errors
- Add logging to analyzer:
console.log('[Analyzer] Running:', this.name); console.log('[Analyzer] Found issues:', diagnostics.length);
Solution:
// Verify analyzer is registered
const analyzers = engine.getAnalyzers();
console.log('Registered analyzers:', analyzers.map(a => a.name));
// Check if diagnostics are created
const diagnostics = await analyzer.analyze(context);
console.log('Diagnostics:', diagnostics);Symptoms: Analysis takes >1 second, UI feels sluggish
Debug steps:
- Run performance benchmarks:
npm run benchmark - Profile with Chrome DevTools
- Check cache hit rate:
const stats = cache.getStats(); console.log('Cache hit rate:', stats.hitRate);
- Measure analyzer execution time:
const start = Date.now(); const result = await analyzer.analyze(context); console.log('Analysis time:', Date.now() - start, 'ms');
Solution:
- Enable caching
- Implement incremental analysis
- Optimize regex patterns
- Use worker threads
Symptoms: Memory usage grows over time, VSCode becomes slow
Debug steps:
- Take heap snapshots in Chrome DevTools
- Check for unclosed resources:
// Bad: Worker not terminated const worker = new Worker('./worker.js'); // Good: Worker cleaned up try { await runInWorker(worker); } finally { worker.terminate(); }
- Check cache size:
const stats = cache.getStats(); console.log('Cache size:', stats.size, 'MB');
Solution:
- Implement proper cleanup in
dispose() - Limit cache size
- Terminate workers after use
- Clear event listeners
// src/analyzers/MyAnalyzer.ts
export class MyAnalyzer implements Analyzer {
readonly supportedLanguages = [
'javascript',
'typescript',
'python',
'java',
'go' // Add new language
];
}export class LanguageAwareAnalyzer implements Analyzer {
private patterns: Record<string, Pattern[]> = {
javascript: [
{ regex: /eval\(/g, message: 'Avoid eval()' }
],
python: [
{ regex: /exec\(/g, message: 'Avoid exec()' }
],
go: [
{ regex: /unsafe\./g, message: 'Avoid unsafe package' }
]
};
async analyze(context: AnalysisContext): Promise<Diagnostic[]> {
const patterns = this.patterns[context.languageId] || [];
return this.analyzeWithPatterns(context.content, patterns);
}
}// package.json
{
"activationEvents": [
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:python",
"onLanguage:java",
"onLanguage:go"
]
}export class CustomActionProvider implements vscode.CodeActionProvider {
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range,
context: vscode.CodeActionContext
): vscode.CodeAction[] {
const actions: vscode.CodeAction[] = [];
for (const diagnostic of context.diagnostics) {
if (diagnostic.source !== 'codeguard') continue;
// Add custom action
const action = new vscode.CodeAction(
'My Custom Fix',
vscode.CodeActionKind.QuickFix
);
action.diagnostics = [diagnostic];
action.command = {
command: 'codeguard.myCustomFix',
title: 'My Custom Fix',
arguments: [document, diagnostic]
};
actions.push(action);
}
return actions;
}
}// src/extension.ts
export function activate(context: vscode.ExtensionContext): void {
const actionProvider = new CustomActionProvider();
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
['javascript', 'typescript'],
actionProvider,
{
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
}
)
);
}// src/extension.ts
context.subscriptions.push(
vscode.commands.registerCommand(
'codeguard.myCustomFix',
async (document: vscode.TextDocument, diagnostic: vscode.Diagnostic) => {
const edit = new vscode.WorkspaceEdit();
// Create fix
const fixedText = await generateFix(document, diagnostic);
edit.replace(document.uri, diagnostic.range, fixedText);
// Apply fix
await vscode.workspace.applyEdit(edit);
// Show message
vscode.window.showInformationMessage('Fix applied!');
}
)
);{
"contributes": {
"configuration": {
"title": "CodeGuard",
"properties": {
"codeguard.myFeature.enabled": {
"type": "boolean",
"default": true,
"description": "Enable my custom feature"
},
"codeguard.myFeature.threshold": {
"type": "number",
"default": 10,
"minimum": 1,
"maximum": 100,
"description": "Threshold for my feature"
},
"codeguard.myFeature.patterns": {
"type": "array",
"default": ["pattern1", "pattern2"],
"description": "Custom patterns for my feature"
}
}
}
}
}import * as vscode from 'vscode';
function getMyFeatureConfig() {
const config = vscode.workspace.getConfiguration('codeguard.myFeature');
return {
enabled: config.get<boolean>('enabled', true),
threshold: config.get<number>('threshold', 10),
patterns: config.get<string[]>('patterns', [])
};
}export function activate(context: vscode.ExtensionContext): void {
// Initial config
let config = getMyFeatureConfig();
// Watch for changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('codeguard.myFeature')) {
config = getMyFeatureConfig();
console.log('Configuration changed:', config);
// React to changes
updateFeature(config);
}
})
);
}// src/extension.ts
export function activate(context: vscode.ExtensionContext): void {
context.subscriptions.push(
vscode.commands.registerCommand(
'codeguard.myCustomCommand',
async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
// Command logic
const result = await performCustomAction(editor.document);
// Show result
vscode.window.showInformationMessage(
`Custom command completed: ${result}`
);
}
)
);
}{
"contributes": {
"commands": [
{
"command": "codeguard.myCustomCommand",
"title": "CodeGuard: My Custom Command",
"category": "CodeGuard"
}
],
"keybindings": [
{
"command": "codeguard.myCustomCommand",
"key": "ctrl+shift+alt+c",
"mac": "cmd+shift+alt+c",
"when": "editorTextFocus"
}
],
"menus": {
"editor/context": [
{
"command": "codeguard.myCustomCommand",
"when": "editorTextFocus",
"group": "codeguard"
}
]
}
}
}// src/ai/MyAIClient.ts
import { AIClient, CompletionOptions, RateLimitInfo } from './AIService';
export class MyAIClient implements AIClient {
constructor(
private apiKey: string,
private model: string,
private baseUrl: string = 'https://api.myai.com'
) {}
async complete(prompt: string, options: CompletionOptions): Promise<string> {
const response = await fetch(`${this.baseUrl}/v1/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: this.model,
prompt,
max_tokens: options.maxTokens,
temperature: options.temperature,
stop: options.stop
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`AI API error: ${response.status} ${error}`);
}
const data = await response.json();
return data.choices[0].text;
}
getRateLimitInfo(): RateLimitInfo {
// Parse from response headers or API
return {
remaining: 100,
reset: Date.now() + 3600000,
limit: 1000
};
}
}// src/ai/AIService.ts
export class AIService {
private client: AIClient;
constructor(config: AIConfig) {
switch (config.provider) {
case 'openai':
this.client = new OpenAIClient(config.apiKey, config.model);
break;
case 'claude':
this.client = new ClaudeClient(config.apiKey, config.model);
break;
case 'myai':
this.client = new MyAIClient(config.apiKey, config.model);
break;
default:
throw new Error(`Unknown AI provider: ${config.provider}`);
}
}
}{
"contributes": {
"configuration": {
"properties": {
"codeguard.ai.provider": {
"type": "string",
"enum": ["openai", "claude", "ollama", "myai", "none"],
"default": "none",
"description": "AI service provider"
}
}
}
}
}// src/engine/WorkerThreadManager.ts
import { Worker } from 'worker_threads';
export class WorkerThreadManager {
private workers: Worker[] = [];
private nextWorker = 0;
constructor(private workerCount: number = 4) {
for (let i = 0; i < workerCount; i++) {
const worker = new Worker('./AnalysisWorker.js');
this.workers.push(worker);
}
}
async analyze(request: AnalysisRequest): Promise<AnalysisResult> {
const worker = this.getNextWorker();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Worker timeout'));
}, 5000);
worker.once('message', (result: AnalysisResult) => {
clearTimeout(timeout);
resolve(result);
});
worker.once('error', (error) => {
clearTimeout(timeout);
reject(error);
});
worker.postMessage(request);
});
}
private getNextWorker(): Worker {
const worker = this.workers[this.nextWorker];
this.nextWorker = (this.nextWorker + 1) % this.workers.length;
return worker;
}
dispose(): void {
for (const worker of this.workers) {
worker.terminate();
}
this.workers = [];
}
}// src/engine/AnalysisWorker.ts
import { parentPort } from 'worker_threads';
import { AnalysisEngine } from './AnalysisEngine';
const engine = new AnalysisEngine();
parentPort?.on('message', async (request: AnalysisRequest) => {
try {
const result = await engine.analyze(request);
parentPort?.postMessage(result);
} catch (error) {
parentPort?.postMessage({
error: error.message,
stack: error.stack
});
}
});Implement taint tracking for security analysis:
export class DataFlowAnalyzer {
private taintedVariables = new Set<string>();
analyzeDataFlow(ast: AST): Map<string, TaintInfo> {
const taintMap = new Map<string, TaintInfo>();
// Find sources (user input)
const sources = this.findTaintSources(ast);
for (const source of sources) {
this.taintedVariables.add(source.name);
taintMap.set(source.name, {
source: source.location,
tainted: true
});
}
// Track data flow
this.trackDataFlow(ast, taintMap);
return taintMap;
}
private findTaintSources(ast: AST): Source[] {
const sources: Source[] = [];
traverse(ast, {
// User input from request parameters
MemberExpression: (path) => {
if (this.isUserInput(path.node)) {
sources.push({
name: this.getVariableName(path.node),
location: path.node.loc
});
}
}
});
return sources;
}
private trackDataFlow(ast: AST, taintMap: Map<string, TaintInfo>): void {
traverse(ast, {
// Track variable assignments
VariableDeclarator: (path) => {
if (path.node.init && this.isTainted(path.node.init, taintMap)) {
const name = path.node.id.name;
taintMap.set(name, {
source: path.node.loc,
tainted: true
});
}
},
// Track function calls
CallExpression: (path) => {
if (this.isSink(path.node)) {
// Check if any argument is tainted
for (const arg of path.node.arguments) {
if (this.isTainted(arg, taintMap)) {
// Found tainted data flowing to sink
this.reportVulnerability(path.node, taintMap);
}
}
}
}
});
}
private isTainted(node: ASTNode, taintMap: Map<string, TaintInfo>): boolean {
if (node.type === 'Identifier') {
return taintMap.has(node.name) && taintMap.get(node.name)!.tainted;
}
if (node.type === 'BinaryExpression') {
return this.isTainted(node.left, taintMap) ||
this.isTainted(node.right, taintMap);
}
return false;
}
private isSink(node: CallExpression): boolean {
// Check if this is a dangerous sink (SQL, exec, etc.)
const sinks = ['execute', 'query', 'exec', 'eval'];
return node.callee.type === 'Identifier' &&
sinks.includes(node.callee.name);
}
}Implement Shannon entropy for secret detection:
export class EntropyCalculator {
/**
* Calculate Shannon entropy of a string.
* Returns value between 0 (no randomness) and 8 (maximum randomness for bytes).
*
* @param str - String to analyze
* @returns Entropy value (0-8)
*/
static calculate(str: string): number {
if (str.length === 0) return 0;
// Count character frequencies
const freq = new Map<string, number>();
for (const char of str) {
freq.set(char, (freq.get(char) || 0) + 1);
}
// Calculate entropy
let entropy = 0;
const len = str.length;
for (const count of freq.values()) {
const probability = count / len;
entropy -= probability * Math.log2(probability);
}
return entropy;
}
/**
* Check if string has high enough entropy to be a secret.
*
* @param str - String to check
* @param minEntropy - Minimum entropy threshold (default: 4.0)
* @returns True if entropy exceeds threshold
*/
static isHighEntropy(str: string, minEntropy: number = 4.0): boolean {
return this.calculate(str) >= minEntropy;
}
/**
* Calculate entropy for different character sets.
* Useful for detecting base64, hex, etc.
*/
static analyzeCharacterSet(str: string): EntropyAnalysis {
const hasLower = /[a-z]/.test(str);
const hasUpper = /[A-Z]/.test(str);
const hasDigit = /[0-9]/.test(str);
const hasSpecial = /[^a-zA-Z0-9]/.test(str);
let charsetSize = 0;
if (hasLower) charsetSize += 26;
if (hasUpper) charsetSize += 26;
if (hasDigit) charsetSize += 10;
if (hasSpecial) charsetSize += 32; // Approximate
const maxEntropy = Math.log2(charsetSize);
const actualEntropy = this.calculate(str);
return {
entropy: actualEntropy,
maxEntropy,
ratio: actualEntropy / maxEntropy,
charsetSize,
isHighEntropy: actualEntropy >= 4.0
};
}
}
interface EntropyAnalysis {
entropy: number;
maxEntropy: number;
ratio: number;
charsetSize: number;
isHighEntropy: boolean;
}Implement cyclomatic complexity calculation:
export class ComplexityCalculator {
/**
* Calculate cyclomatic complexity of a function.
* Complexity = decision points + 1
*
* Decision points: if, else, for, while, case, catch, &&, ||, ?:
*/
static calculate(ast: FunctionNode): number {
let complexity = 1; // Base complexity
traverse(ast, {
// Conditional statements
IfStatement: () => complexity++,
ConditionalExpression: () => complexity++,
// Loops
ForStatement: () => complexity++,
ForInStatement: () => complexity++,
ForOfStatement: () => complexity++,
WhileStatement: () => complexity++,
DoWhileStatement: () => complexity++,
// Switch cases
SwitchCase: (path) => {
if (path.node.test !== null) { // Exclude default case
complexity++;
}
},
// Exception handling
CatchClause: () => complexity++,
// Logical operators
LogicalExpression: (path) => {
if (path.node.operator === '&&' || path.node.operator === '||') {
complexity++;
}
}
});
return complexity;
}
/**
* Get complexity rating based on value.
*/
static getRating(complexity: number): ComplexityRating {
if (complexity <= 5) return 'simple';
if (complexity <= 10) return 'moderate';
if (complexity <= 20) return 'complex';
return 'very-complex';
}
/**
* Get recommended action based on complexity.
*/
static getRecommendation(complexity: number): string {
if (complexity <= 10) {
return 'Complexity is acceptable';
} else if (complexity <= 20) {
return 'Consider refactoring to reduce complexity';
} else {
return 'High complexity - refactoring strongly recommended';
}
}
}
type ComplexityRating = 'simple' | 'moderate' | 'complex' | 'very-complex';- VSCode Extension API - Official VSCode extension documentation
- TypeScript Handbook - TypeScript language reference
- Vitest Documentation - Testing framework documentation
- fast-check Documentation - Property-based testing library
- AST Explorer - Visualize and explore ASTs
- Regex101 - Test and debug regular expressions
- TypeScript Playground - Experiment with TypeScript
- VSCode Extension Samples - Official extension examples
- OWASP Top 10 - Common security vulnerabilities
- CWE List - Common weakness enumeration
- SANS Top 25 - Most dangerous software errors
- Semgrep Rules - Security analysis patterns
- V8 Performance Tips - JavaScript optimization
- Node.js Performance - Profiling Node.js apps
- Web Performance - General performance best practices
We welcome contributions! Please see CONTRIBUTING.md for:
- Development setup
- Code style guidelines
- Testing requirements
- Pull request process
- Adding new analyzers
- Adding new AI providers
- π User Documentation
- π§ Configuration Reference
- π Troubleshooting Guide
- π¬ GitHub Discussions
- π Issue Tracker
When reporting issues, please include:
-
Environment:
- VSCode version
- CodeGuard version
- Operating system
- Node.js version
-
Steps to reproduce:
- Minimal code example
- Configuration settings
- Expected vs actual behavior
-
Logs:
- Output panel logs (View β Output β CodeGuard)
- Extension Host logs
- Console errors (if any)
We love hearing your ideas! When requesting features:
- Describe the problem: What are you trying to solve?
- Propose a solution: How would you like it to work?
- Consider alternatives: Are there other ways to solve this?
- Provide examples: Show use cases and code examples
Happy coding! π
CodeGuard - Catch issues before they reach production