@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
JavaScript
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