@felixgeelhaar/cclint
Version:
Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.
139 lines • 5.98 kB
JavaScript
import { Violation } from '../../../domain/Violation.js';
import { Location } from '../../../domain/Location.js';
import { Severity } from '../../../domain/Severity.js';
/** Third-party libraries whose presence implies imports should be shown. */
const COMMON_LIBRARIES = [
'axios',
'lodash',
'express',
'react',
'vue',
'angular',
'moment',
'date-fns',
'rxjs',
'redux',
'mongoose',
];
/**
* Validates JavaScript and TypeScript code blocks for common best-practice
* issues (console usage, `var`, loose equality, unhandled async, missing
* semicolons, undisclosed dependencies) and cross-block style consistency.
*/
export class JavaScriptValidator {
validate(block, context) {
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 console.log in production examples
if (!block.metadata.isAntiPattern &&
/console\.(log|error|warn)/.test(line)) {
violations.push(new Violation(context.ruleId, `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(context.ruleId, `Use 'const' or 'let' instead of 'var' (line ${lineNumber})`, context.strict ? Severity.ERROR : Severity.WARNING, new Location(lineNumber, 1)));
}
// Check for == instead of ===
if (/[^=!]==[^=]/.test(line) && !line.includes('null')) {
violations.push(new Violation(context.ruleId, `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(context.ruleId, `Async function should include error handling (line ${lineNumber})`, Severity.WARNING, new Location(lineNumber, 1)));
}
}
// Check for missing semicolons (if in strict mode)
if (context.strict &&
this.shouldHaveSemicolon(line) &&
!line.trim().endsWith(';')) {
violations.push(new Violation(context.ruleId, `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(context.ruleId, `Code block uses external dependencies but doesn't show imports`, Severity.WARNING, block.location));
}
return violations;
}
/**
* Check for consistent code style across all JavaScript/TypeScript blocks in
* a file.
*/
validateConsistency(blocks, context) {
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(context.ruleId, 'Inconsistent semicolon usage across JavaScript/TypeScript code blocks', Severity.INFO, new Location(1, 1)));
}
return violations;
}
/** Find the end (closing brace) of a function starting at `startIndex`. */
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;
}
/** Whether a line is a statement that should end with 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);
}
/** Whether the block references a well-known external library. */
usesExternalDependencies(block) {
for (const lib of COMMON_LIBRARIES) {
if (block.content.includes(lib)) {
return true;
}
}
return false;
}
}
//# sourceMappingURL=JavaScriptValidator.js.map