Adds expression index on lower(name) for memory-efficient case-insensitive searches, replacing in-memory caches that caused OOM on large codebases. Includes batched reference resolution, enhanced error reporting with detailed breakdown by error type, and improved CLI progress display for scanning phases.
200 lines
6.0 KiB
TypeScript
200 lines
6.0 KiB
TypeScript
import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference, Language } from '../types';
|
|
import { generateNodeId } from './tree-sitter-helpers';
|
|
import { TreeSitterExtractor } from './tree-sitter';
|
|
import { isLanguageSupported } from './grammars';
|
|
|
|
/**
|
|
* 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 <script> block content
|
|
* and delegate it to the TypeScript/JavaScript TreeSitterExtractor.
|
|
*
|
|
* Every .svelte file produces a component node (Svelte components are always importable).
|
|
*/
|
|
export class SvelteExtractor {
|
|
private filePath: string;
|
|
private source: string;
|
|
private nodes: Node[] = [];
|
|
private edges: Edge[] = [];
|
|
private unresolvedReferences: UnresolvedReference[] = [];
|
|
private errors: ExtractionError[] = [];
|
|
|
|
constructor(filePath: string, source: string) {
|
|
this.filePath = filePath;
|
|
this.source = source;
|
|
}
|
|
|
|
/**
|
|
* Extract from Svelte source
|
|
*/
|
|
extract(): ExtractionResult {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Create component node for the .svelte file itself
|
|
const componentNode = this.createComponentNode();
|
|
|
|
// Extract and process script blocks
|
|
const scriptBlocks = this.extractScriptBlocks();
|
|
|
|
for (const block of scriptBlocks) {
|
|
this.processScriptBlock(block, componentNode.id);
|
|
}
|
|
} catch (error) {
|
|
this.errors.push({
|
|
message: `Svelte extraction error: ${error instanceof Error ? error.message : String(error)}`,
|
|
severity: 'error',
|
|
code: 'parse_error',
|
|
});
|
|
}
|
|
|
|
return {
|
|
nodes: this.nodes,
|
|
edges: this.edges,
|
|
unresolvedReferences: this.unresolvedReferences,
|
|
errors: this.errors,
|
|
durationMs: Date.now() - startTime,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a component node for the .svelte file
|
|
*/
|
|
private createComponentNode(): Node {
|
|
const lines = this.source.split('\n');
|
|
const fileName = this.filePath.split(/[/\\]/).pop() || this.filePath;
|
|
const componentName = fileName.replace(/\.svelte$/, '');
|
|
const id = generateNodeId(this.filePath, 'component', componentName, 1);
|
|
|
|
const node: Node = {
|
|
id,
|
|
kind: 'component',
|
|
name: componentName,
|
|
qualifiedName: `${this.filePath}::${componentName}`,
|
|
filePath: this.filePath,
|
|
language: 'svelte',
|
|
startLine: 1,
|
|
endLine: lines.length,
|
|
startColumn: 0,
|
|
endColumn: lines[lines.length - 1]?.length || 0,
|
|
isExported: true, // Svelte components are always importable
|
|
updatedAt: Date.now(),
|
|
};
|
|
|
|
this.nodes.push(node);
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Extract <script> blocks from the Svelte source
|
|
*/
|
|
private extractScriptBlocks(): Array<{
|
|
content: string;
|
|
startLine: number;
|
|
isModule: boolean;
|
|
isTypeScript: boolean;
|
|
}> {
|
|
const blocks: Array<{
|
|
content: string;
|
|
startLine: number;
|
|
isModule: boolean;
|
|
isTypeScript: boolean;
|
|
}> = [];
|
|
|
|
const scriptRegex = /<script(\s[^>]*)?>(?<content>[\s\S]*?)<\/script>/g;
|
|
let match;
|
|
|
|
while ((match = scriptRegex.exec(this.source)) !== null) {
|
|
const attrs = match[1] || '';
|
|
const content = match.groups?.content || match[2] || '';
|
|
|
|
// Detect TypeScript from lang attribute
|
|
const isTypeScript = /lang\s*=\s*["'](ts|typescript)["']/.test(attrs);
|
|
|
|
// Detect module script
|
|
const isModule = /context\s*=\s*["']module["']/.test(attrs);
|
|
|
|
// Calculate start line of the script content (line after <script>)
|
|
const beforeScript = this.source.substring(0, match.index);
|
|
const scriptTagLine = (beforeScript.match(/\n/g) || []).length;
|
|
// The content starts on the line after the opening <script> tag
|
|
const openingTag = match[0].substring(0, match[0].indexOf('>') + 1);
|
|
const openingTagLines = (openingTag.match(/\n/g) || []).length;
|
|
const contentStartLine = scriptTagLine + openingTagLines + 1; // 0-indexed line
|
|
|
|
blocks.push({
|
|
content,
|
|
startLine: contentStartLine,
|
|
isModule,
|
|
isTypeScript,
|
|
});
|
|
}
|
|
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* Process a script block by delegating to TreeSitterExtractor
|
|
*/
|
|
private processScriptBlock(
|
|
block: { content: string; startLine: number; isModule: boolean; isTypeScript: boolean },
|
|
componentNodeId: string
|
|
): void {
|
|
const scriptLanguage: Language = block.isTypeScript ? 'typescript' : 'javascript';
|
|
|
|
// Check if the script language parser is available
|
|
if (!isLanguageSupported(scriptLanguage)) {
|
|
this.errors.push({
|
|
message: `Parser for ${scriptLanguage} not available, cannot parse Svelte script block`,
|
|
severity: 'warning',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Delegate to TreeSitterExtractor
|
|
const extractor = new TreeSitterExtractor(this.filePath, block.content, scriptLanguage);
|
|
const result = extractor.extract();
|
|
|
|
// Offset line numbers from script block back to .svelte file positions
|
|
for (const node of result.nodes) {
|
|
node.startLine += block.startLine;
|
|
node.endLine += block.startLine;
|
|
node.language = 'svelte'; // Mark as svelte, not TS/JS
|
|
|
|
this.nodes.push(node);
|
|
|
|
// Add containment edge from component to this node
|
|
this.edges.push({
|
|
source: componentNodeId,
|
|
target: node.id,
|
|
kind: 'contains',
|
|
});
|
|
}
|
|
|
|
// Offset edges (they reference line numbers)
|
|
for (const edge of result.edges) {
|
|
if (edge.line) {
|
|
edge.line += block.startLine;
|
|
}
|
|
this.edges.push(edge);
|
|
}
|
|
|
|
// Offset unresolved references
|
|
for (const ref of result.unresolvedReferences) {
|
|
ref.line += block.startLine;
|
|
ref.filePath = this.filePath;
|
|
ref.language = 'svelte';
|
|
this.unresolvedReferences.push(ref);
|
|
}
|
|
|
|
// Carry over errors
|
|
for (const error of result.errors) {
|
|
if (error.line) {
|
|
error.line += block.startLine;
|
|
}
|
|
this.errors.push(error);
|
|
}
|
|
}
|
|
}
|