import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference, Language } from '../types'; import { generateNodeId } from './tree-sitter-helpers'; import { TreeSitterExtractor } from './tree-sitter'; import { isLanguageSupported } from './grammars'; /** Svelte 5 rune names — compiler builtins, not real functions */ const SVELTE_RUNES = new Set([ '$props', '$state', '$derived', '$effect', '$bindable', '$inspect', '$host', '$snippet', ]); /** * SvelteExtractor - Extracts code relationships from Svelte component files * * Svelte files are multi-language (script + template + style). Rather than * parsing the full Svelte grammar, we extract the and ranges const tagRegex = /<(script|style)(\s[^>]*)?>[\s\S]*?<\/\1>/g; let tagMatch; while ((tagMatch = tagRegex.exec(this.source)) !== null) { const startLine = (this.source.substring(0, tagMatch.index).match(/\n/g) || []).length; const endLine = startLine + (tagMatch[0].match(/\n/g) || []).length; coveredRanges.push([startLine, endLine]); } // Find template expressions: {...} outside of script/style blocks // Matches curly-brace expressions, excluding Svelte block syntax ({#if}, {:else}, {/if}, {@html}, {@render}) const lines = this.source.split('\n'); const exprRegex = /\{([^}#/:@][^}]*)\}/g; for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { // Skip lines inside script/style blocks if (coveredRanges.some(([start, end]) => lineIdx >= start && lineIdx <= end)) continue; const line = lines[lineIdx]!; let exprMatch; while ((exprMatch = exprRegex.exec(line)) !== null) { const expr = exprMatch[1]!; // Extract function calls: identifiers followed by ( // Matches: cn(...), buttonVariants(...), obj.method(...) const callRegex = /\b([a-zA-Z_$][\w$.]*)\s*\(/g; let callMatch; while ((callMatch = callRegex.exec(expr)) !== null) { const calleeName = callMatch[1]!; // Skip Svelte runes, control flow keywords, and common non-function patterns if (SVELTE_RUNES.has(calleeName)) continue; if (calleeName === 'if' || calleeName === 'else' || calleeName === 'each' || calleeName === 'await') continue; this.unresolvedReferences.push({ fromNodeId: componentNodeId, referenceName: calleeName, referenceKind: 'calls', line: lineIdx + 1, // 1-indexed column: exprMatch.index + callMatch.index, filePath: this.filePath, language: 'svelte', }); } } } } /** * Extract component usages from the Svelte template. * * PascalCase tags like ,