UNPKG

@felixgeelhaar/cclint

Version:

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

162 lines 6.08 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'; /** * Rule that validates CLAUDE.md import syntax * * @remarks * Validates the @path/to/file import syntax introduced by Anthropic. * - Imports should not appear in code blocks or code spans * - Warns about potential circular dependencies * - Validates path formats (relative, absolute, home directory) * * @see {@link https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports | CLAUDE.md imports documentation} * * @category Rules */ export class ImportSyntaxRule { id = 'import-syntax'; description = 'Validates CLAUDE.md import syntax (@path/to/file)'; maxDepth; constructor(maxDepth = 5) { this.maxDepth = maxDepth; } appliesTo(file) { return file.isMarkdown(); } lint(file) { const violations = []; const imports = []; let inCodeBlock = false; let lineNumber = 0; for (const line of file.lines) { lineNumber++; // Track code block state if (line.trim().startsWith('```')) { inCodeBlock = !inCodeBlock; continue; } // Skip lines inside code blocks if (inCodeBlock) { continue; } // Find all imports in this line (can have multiple) const importMatches = this.findImports(line); for (const match of importMatches) { // Check if import is inside a code span (backticks) if (this.isInCodeSpan(line, match.index)) { continue; // Valid - imports in code spans are ignored per Anthropic docs } // Validate import path const pathViolations = this.validateImportPath(match.path, lineNumber, match.index); violations.push(...pathViolations); // Track import for circular dependency detection imports.push({ path: match.path, line: lineNumber, column: match.index, }); } } // Check for potential issues with imports violations.push(...this.checkImportPatterns(imports)); return violations; } /** * Find all import patterns in a line */ findImports(line) { const imports = []; // Match @path patterns that are not in backticks const regex = /@([\w\-~/.]+)/g; let match; while ((match = regex.exec(line)) !== null) { const path = match[1]; if (path) { imports.push({ path, index: match.index, }); } } return imports; } /** * Check if import is inside a code span (backticks) */ isInCodeSpan(line, position) { let inSpan = false; let escapeNext = false; for (let i = 0; i < position; i++) { const char = line[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '`') { inSpan = !inSpan; } } return inSpan; } /** * Validate import path format and provide helpful suggestions. * * The findImports regex char class ([\w\-~/.]+) cannot capture `@`, * `\`, or whitespace, so package-name, Windows-backslash, and space * checks are unreachable here — they belong upstream in findImports * if those signals are ever needed. We keep the path-shape checks * for documentation value and to guard the `//` double-slash edge. */ validateImportPath(path, _line, _column) { const violations = []; // Absolute path: starts with / but not //. Valid. if (path.startsWith('/') && !path.startsWith('//')) { return violations; } // Home directory path. Valid. if (path.startsWith('~/')) { return violations; } // Relative path or bare file name. Valid. if (path.startsWith('./') || path.startsWith('../') || !path.includes('/')) { return violations; } // Anything else (e.g. `//double-slash` or paths with multiple // segments that don't start with ./, ../, /, or ~/) is not a // recognized form, but we deliberately don't flag it here — // import-resolution will surface real "file not found" errors // with a sharper message than a path-shape heuristic could. return violations; } /** * Check for patterns and potential issues across all imports */ checkImportPatterns(imports) { const violations = []; // Check for duplicate imports const pathCounts = new Map(); for (const imp of imports) { const count = pathCounts.get(imp.path) ?? 0; pathCounts.set(imp.path, count + 1); } for (const [path, count] of pathCounts) { if (count > 1) { violations.push(new Violation(this.id, `Import "@${path}" is referenced ${count} times. Consider consolidating duplicate imports.`, Severity.INFO, new Location(1, 1))); } } // Warn if approaching max depth limit (can't fully validate without file system access) if (imports.length > 10) { violations.push(new Violation(this.id, `File contains ${imports.length} imports. Claude supports recursive imports with max depth of ${this.maxDepth} hops. Ensure import chains don't exceed this limit.`, Severity.WARNING, new Location(1, 1))); } return violations; } } //# sourceMappingURL=ImportSyntaxRule.js.map