UNPKG

@felixgeelhaar/cclint

Version:

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

220 lines 8.11 kB
import { ContextFile } from '../domain/ContextFile.js'; import { CodeBlock } from '../domain/CodeBlock.js'; import { Location } from '../domain/Location.js'; /** * Extracts code blocks from markdown files */ export class CodeBlockExtractor { /** * Extract all code blocks from a ContextFile * @param file The file to extract code blocks from * @returns Array of CodeBlock instances */ extractCodeBlocks(file) { const codeBlocks = []; const lines = file.lines; let inCodeBlock = false; let currentLanguage = ''; let currentContent = []; let codeBlockStartLine = 0; let indentLevel = 0; let context = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = i + 1; // Check for code block start const codeBlockStart = this.parseCodeBlockStart(line); if (codeBlockStart && !inCodeBlock) { inCodeBlock = true; currentLanguage = codeBlockStart.language; codeBlockStartLine = lineNumber; indentLevel = codeBlockStart.indentLevel; currentContent = []; // Get context from previous lines context = this.getContext(lines, i); continue; } // Check for code block end if (inCodeBlock && this.isCodeBlockEnd(line, indentLevel)) { // Create the code block const location = new Location(codeBlockStartLine, 1); const codeBlock = new CodeBlock(currentLanguage, currentContent.join('\n'), location, context, { indentLevel, isExample: !context.toLowerCase().includes('anti-pattern'), isAntiPattern: context.toLowerCase().includes('anti-pattern') || context.toLowerCase().includes('bad') || context.toLowerCase().includes('wrong'), description: this.extractDescription(context), }); codeBlocks.push(codeBlock); // Reset state inCodeBlock = false; currentLanguage = ''; currentContent = []; context = ''; continue; } // Collect code block content if (inCodeBlock) { currentContent.push(line); } } // Handle unclosed code block if (inCodeBlock && currentContent.length > 0) { const location = new Location(codeBlockStartLine, 1); const codeBlock = new CodeBlock(currentLanguage, currentContent.join('\n'), location, context, { indentLevel, isExample: true, isAntiPattern: false, description: this.extractDescription(context), }); codeBlocks.push(codeBlock); } return codeBlocks; } /** * Parse the start of a code block * @param line The line to parse * @returns Language and indent information if this is a code block start */ parseCodeBlockStart(line) { // Match ``` or ~~~ with optional language identifier const match = line.match(/^(\s*)(```|~~~)\s*(\w+)?/); if (!match) { return null; } const indent = match[1] ?? ''; const language = match[3] ?? ''; return { language: language || 'text', indentLevel: indent.length, }; } /** * Check if a line ends a code block * @param line The line to check * @param expectedIndent The expected indentation level * @returns True if this ends a code block */ isCodeBlockEnd(line, expectedIndent) { const match = line.match(/^(\s*)(```|~~~)\s*$/); if (!match) { return false; } const indent = match[1] ?? ''; // Allow some flexibility in indentation return Math.abs(indent.length - expectedIndent) <= 2; } /** * Get context for a code block from surrounding text * @param lines All lines in the file * @param codeBlockIndex Index where the code block starts * @returns Context string */ getContext(lines, codeBlockIndex) { const contextLines = []; const maxContextLines = 5; // Look backwards for context for (let i = codeBlockIndex - 1; i >= 0 && contextLines.length < maxContextLines; i--) { const line = lines[i].trim(); // Skip empty lines if (line === '') { continue; } // Stop at headers or other code blocks if (line.startsWith('#') || line.startsWith('```') || line.startsWith('~~~')) { if (line.startsWith('#')) { contextLines.unshift(line); } break; } contextLines.unshift(line); } return contextLines.join(' '); } /** * Extract a description from context * @param context The context string * @returns A clean description */ extractDescription(context) { // Remove markdown formatting let description = context .replace(/^#+\s+/, '') // Remove headers .replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold .replace(/\*(.*?)\*/g, '$1') // Remove italic .replace(/`(.*?)`/g, '$1') // Remove inline code .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links .trim(); // Limit length if (description.length > 100) { description = description.substring(0, 97) + '...'; } return description; } /** * Extract code blocks of a specific language * @param file The file to extract from * @param language The language to filter by * @returns Array of CodeBlock instances for the specified language */ extractCodeBlocksByLanguage(file, language) { const allBlocks = this.extractCodeBlocks(file); const normalizedLanguage = this.normalizeLanguage(language); return allBlocks.filter(block => this.normalizeLanguage(block.language) === normalizedLanguage); } /** * Normalize language identifier * @param language The language string to normalize * @returns Normalized language identifier */ normalizeLanguage(language) { const normalized = language.toLowerCase().trim(); const languageMap = { js: 'javascript', ts: 'typescript', tsx: 'typescriptreact', jsx: 'javascriptreact', py: 'python', rb: 'ruby', yml: 'yaml', shell: 'bash', sh: 'bash', }; return languageMap[normalized] ?? normalized; } /** * Get statistics about code blocks in a file * @param file The file to analyze * @returns Statistics object */ getCodeBlockStats(file) { const blocks = this.extractCodeBlocks(file); const languageCounts = new Map(); let totalLines = 0; let completeBlocks = 0; let blocksWithImports = 0; for (const block of blocks) { const lang = block.language; languageCounts.set(lang, (languageCounts.get(lang) ?? 0) + 1); totalLines += block.getLineCount(); if (block.isComplete) { completeBlocks++; } if (block.hasImports) { blocksWithImports++; } } return { totalBlocks: blocks.length, totalLines, completeBlocks, blocksWithImports, languageCounts: Object.fromEntries(languageCounts), averageLinesPerBlock: blocks.length > 0 ? totalLines / blocks.length : 0, }; } } //# sourceMappingURL=CodeBlockExtractor.js.map