UNPKG

@felixgeelhaar/cclint

Version:

Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.

90 lines 3.92 kB
import { ContextFile } from '../domain/ContextFile.js'; import { Violation } from '../domain/Violation.js'; import { Severity } from '../domain/Severity.js'; import { CodeBlockExtractor } from '../infrastructure/CodeBlockExtractor.js'; import { ValidatorRegistry, createDefaultValidatorRegistry, } from './codeblock/ValidatorRegistry.js'; /** * Rule that validates code blocks within CLAUDE.md files. * * @remarks * This is a thin orchestrator: it extracts code blocks, applies the * language-agnostic checks (untyped fences, incompleteness, unmarked * anti-patterns), and delegates language-specific validation to per-language * {@link LanguageValidator} strategies resolved through a * {@link ValidatorRegistry}. */ export class CodeBlockRule { id = 'code-blocks'; description = 'Validates code blocks for syntax and best practices'; extractor; enabledLanguages; strictMode; registry; 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; this.registry = createDefaultValidatorRegistry(); } appliesTo(file) { return file.isMarkdown(); } lint(file) { const violations = []; const codeBlocks = this.extractor.extractCodeBlocks(file); const context = { ruleId: this.id, strict: this.strictMode, }; // 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, delegated to the registered validator const validator = this.registry.get(block.language); if (validator) { violations.push(...validator.validate(block, context)); } // 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)); } } // Cross-block checks (e.g. consistent JavaScript/TypeScript style) for (const group of this.registry.groups()) { const { validator, languages } = group; if (typeof validator.validateConsistency !== 'function') { continue; } const groupBlocks = codeBlocks.filter(b => languages.includes(b.language)); if (groupBlocks.length > 1) { violations.push(...validator.validateConsistency(groupBlocks, context)); } } return violations; } } //# sourceMappingURL=CodeBlockRule.js.map