UNPKG

@felixgeelhaar/cclint

Version:

A comprehensive linter for CLAUDE.md files with multi-language code block validation

347 lines 14.9 kB
import { ContextFile } from '../domain/ContextFile.js'; import { Violation } from '../domain/Violation.js'; import { Location } from '../domain/Location.js'; import { Severity } from '../domain/Severity.js'; import { CodeBlockExtractor } from '../infrastructure/CodeBlockExtractor.js'; import { CodeBlock } from '../domain/CodeBlock.js'; /** * Rule that validates code blocks within CLAUDE.md files * Checks for syntax, completeness, and best practices */ export class CodeBlockRule { id = 'code-blocks'; description = 'Validates code blocks for syntax and best practices'; extractor; enabledLanguages; strictMode; constructor(options) { this.extractor = new CodeBlockExtractor(); this.enabledLanguages = new Set(options?.languages || [ 'javascript', 'typescript', 'python', 'go', 'bash', 'sql', 'yaml', 'json', ]); this.strictMode = options?.strict ?? true; } lint(file) { const violations = []; const codeBlocks = this.extractor.extractCodeBlocks(file); // Check for code blocks without language specification for (const block of codeBlocks) { if (block.language === 'text' || block.language === '') { violations.push(new Violation(this.id, `Code block at line ${block.location.line} should specify a language`, Severity.WARNING, block.location)); } } // Validate each code block based on its language for (const block of codeBlocks) { if (!this.enabledLanguages.has(block.language)) { continue; } // Check for incomplete code blocks if (!block.isComplete) { violations.push(new Violation(this.id, `Incomplete ${block.getLanguageDisplayName()} code block at line ${block.location.line} contains placeholder content`, Severity.WARNING, block.location)); } // Language-specific validation switch (block.language) { case 'javascript': case 'typescript': violations.push(...this.validateJavaScriptTypeScript(block)); break; case 'python': violations.push(...this.validatePython(block)); break; case 'go': violations.push(...this.validateGo(block)); break; case 'bash': violations.push(...this.validateBash(block)); break; case 'sql': violations.push(...this.validateSQL(block)); break; case 'json': violations.push(...this.validateJSON(block)); break; case 'yaml': violations.push(...this.validateYAML(block)); break; } // Check for anti-patterns marked as examples if (block.metadata.isAntiPattern && !block.context.toLowerCase().includes('bad') && !block.context.toLowerCase().includes('wrong') && !block.context.toLowerCase().includes('anti-pattern')) { violations.push(new Violation(this.id, `Code block at line ${block.location.line} appears to be an anti-pattern but is not clearly marked as such`, Severity.WARNING, block.location)); } } // Check for consistent code style across blocks const jsBlocks = codeBlocks.filter(b => b.language === 'javascript' || b.language === 'typescript'); if (jsBlocks.length > 1) { violations.push(...this.checkConsistentStyle(jsBlocks)); } return violations; } /** * Validate JavaScript/TypeScript code blocks */ validateJavaScriptTypeScript(block) { const violations = []; const lines = block.getLines(); // Check for common bad practices for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = block.location.line + i; // Check for console.log in production examples if (!block.metadata.isAntiPattern && /console\.(log|error|warn)/.test(line)) { violations.push(new Violation(this.id, `Avoid console statements in example code (line ${lineNumber})`, Severity.INFO, new Location(lineNumber, 1))); } // Check for var usage (prefer const/let) if (/\bvar\s+\w+/.test(line)) { violations.push(new Violation(this.id, `Use 'const' or 'let' instead of 'var' (line ${lineNumber})`, this.strictMode ? Severity.ERROR : Severity.WARNING, new Location(lineNumber, 1))); } // Check for == instead of === if (/[^=!]==[^=]/.test(line) && !line.includes('null')) { violations.push(new Violation(this.id, `Use '===' instead of '==' for strict equality (line ${lineNumber})`, Severity.WARNING, new Location(lineNumber, 1))); } // Check for async without error handling if (/async\s+function|\basync\s*\(/.test(line)) { // Check if there's try-catch in the function const functionEnd = this.findFunctionEnd(lines, i); const functionBody = lines.slice(i, functionEnd + 1).join('\n'); if (!functionBody.includes('try') && !functionBody.includes('catch')) { violations.push(new Violation(this.id, `Async function should include error handling (line ${lineNumber})`, Severity.WARNING, new Location(lineNumber, 1))); } } // Check for missing semicolons (if in strict mode) if (this.strictMode && this.shouldHaveSemicolon(line) && !line.trim().endsWith(';')) { violations.push(new Violation(this.id, `Missing semicolon (line ${lineNumber})`, Severity.INFO, new Location(lineNumber, line.length))); } } // Check for missing imports when using external dependencies if (this.usesExternalDependencies(block) && !block.hasImports) { violations.push(new Violation(this.id, `Code block uses external dependencies but doesn't show imports`, Severity.WARNING, block.location)); } return violations; } /** * Validate Python code blocks */ validatePython(block) { const violations = []; const lines = block.getLines(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = block.location.line + i; // Check for print statements in non-example code if (!block.metadata.isExample && /\bprint\s*\(/.test(line)) { violations.push(new Violation(this.id, `Consider using logging instead of print() (line ${lineNumber})`, Severity.INFO, new Location(lineNumber, 1))); } // Check for bare except clauses if (/except\s*:/.test(line)) { violations.push(new Violation(this.id, `Avoid bare 'except:' clauses, specify exception type (line ${lineNumber})`, Severity.WARNING, new Location(lineNumber, 1))); } // Check for type hints in function definitions if (/def\s+\w+\s*\([^)]*\)\s*:/.test(line) && !line.includes('->')) { violations.push(new Violation(this.id, `Consider adding type hints to function (line ${lineNumber})`, Severity.INFO, new Location(lineNumber, 1))); } } return violations; } /** * Validate Go code blocks */ validateGo(block) { const violations = []; const lines = block.getLines(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = block.location.line + i; // Check for error handling if (/err\s*:=/.test(line)) { // Check if error is handled in next few lines const nextLines = lines.slice(i + 1, i + 5).join('\n'); if (!nextLines.includes('if err != nil')) { violations.push(new Violation(this.id, `Error not handled after assignment (line ${lineNumber})`, Severity.ERROR, new Location(lineNumber, 1))); } } // Check for panic in example code if (!block.metadata.isAntiPattern && /\bpanic\s*\(/.test(line)) { violations.push(new Violation(this.id, `Avoid using panic() in example code (line ${lineNumber})`, Severity.WARNING, new Location(lineNumber, 1))); } } return violations; } /** * Validate Bash/Shell code blocks */ validateBash(block) { const violations = []; const lines = block.getLines(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = block.location.line + i; // Check for unquoted variables if (/\$\w+(?!["\w])/.test(line) && !line.includes('$@') && !line.includes('$*')) { violations.push(new Violation(this.id, `Quote variables to prevent word splitting (line ${lineNumber})`, Severity.WARNING, new Location(lineNumber, 1))); } // Check for rm -rf without safeguards if (/rm\s+-rf/.test(line) && !block.metadata.isAntiPattern) { violations.push(new Violation(this.id, `Dangerous 'rm -rf' command in example (line ${lineNumber})`, Severity.ERROR, new Location(lineNumber, 1))); } // Check for missing error handling if (/^\s*cd\s+/.test(line) && !lines[i + 1]?.includes('||')) { violations.push(new Violation(this.id, `Check for cd command success (line ${lineNumber})`, Severity.INFO, new Location(lineNumber, 1))); } } return violations; } /** * Validate SQL code blocks */ validateSQL(block) { const violations = []; const content = block.content.toUpperCase(); // Check for SELECT * in examples if (!block.metadata.isAntiPattern && content.includes('SELECT *')) { violations.push(new Violation(this.id, `Avoid SELECT * in examples, specify columns explicitly`, Severity.WARNING, block.location)); } // Check for potential SQL injection patterns if (/\$\{.*\}/.test(block.content) || /\+\s*["']/.test(block.content)) { violations.push(new Violation(this.id, `Potential SQL injection vulnerability - use parameterized queries`, Severity.ERROR, block.location)); } return violations; } /** * Validate JSON code blocks */ validateJSON(block) { const violations = []; try { JSON.parse(block.content); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Invalid JSON'; violations.push(new Violation(this.id, `Invalid JSON syntax: ${errorMessage}`, Severity.ERROR, block.location)); } return violations; } /** * Validate YAML code blocks */ validateYAML(block) { const violations = []; const lines = block.getLines(); // Basic YAML validation for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = block.location.line + i; // Check for tabs (YAML requires spaces) if (line.includes('\t')) { violations.push(new Violation(this.id, `YAML files should use spaces, not tabs (line ${lineNumber})`, Severity.ERROR, new Location(lineNumber, 1))); } } return violations; } /** * Check for consistent code style across blocks */ checkConsistentStyle(blocks) { const violations = []; // Check for consistent semicolon usage let withSemicolons = 0; let withoutSemicolons = 0; for (const block of blocks) { const hasSemicolons = block.content.includes(';'); if (hasSemicolons) { withSemicolons++; } else { withoutSemicolons++; } } if (withSemicolons > 0 && withoutSemicolons > 0) { violations.push(new Violation(this.id, 'Inconsistent semicolon usage across JavaScript/TypeScript code blocks', Severity.INFO, new Location(1, 1))); } return violations; } /** * Helper: Find the end of a function */ findFunctionEnd(lines, startIndex) { let braceCount = 0; let inFunction = false; for (let i = startIndex; i < lines.length; i++) { const line = lines[i]; for (const char of line) { if (char === '{') { braceCount++; inFunction = true; } else if (char === '}') { braceCount--; if (inFunction && braceCount === 0) { return i; } } } } return lines.length - 1; } /** * Helper: Check if a line should have a semicolon */ shouldHaveSemicolon(line) { const trimmed = line.trim(); // Skip empty lines, comments, and block statements if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.endsWith('{') || trimmed.endsWith('}') || trimmed.startsWith('if') || trimmed.startsWith('for') || trimmed.startsWith('while') || trimmed.startsWith('function') || trimmed.startsWith('class') || trimmed.startsWith('interface')) { return false; } // Check if it's a statement that should end with semicolon return /^(const|let|var|return|throw|import|export)\s+/.test(trimmed); } /** * Helper: Check if code uses external dependencies */ usesExternalDependencies(block) { const commonLibraries = [ 'axios', 'lodash', 'express', 'react', 'vue', 'angular', 'moment', 'date-fns', 'rxjs', 'redux', 'mongoose', ]; for (const lib of commonLibraries) { if (block.content.includes(lib)) { return true; } } return false; } } //# sourceMappingURL=CodeBlockRule.js.map