Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ Know exactly what breaks before you change it. Trace callers, callees, and the f
<tr>
<td width="33%" valign="top">

### 🌍 15+ Languages
TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin—all with the same API.
### 🌍 17+ Languages
TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte—all with the same API.

</td>
<td width="33%" valign="top">
Expand Down Expand Up @@ -598,6 +598,8 @@ The `.codegraph/config.json` file controls indexing behavior:
| C++ | `.cpp`, `.hpp`, `.cc` | Full support |
| Swift | `.swift` | Basic support |
| Kotlin | `.kt` | Basic support |
| Dart | `.dart` | Full support |
| Svelte | `.svelte` | Full support (script extraction, Svelte 5 runes, SvelteKit routes) |

## 🔧 Troubleshooting

Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function validateConfig(config: unknown): config is CodeGraphConfig {
'go',
'rust',
'java',
'svelte',
'unknown',
];
if (!c.languages.every((l) => validLanguages.includes(l as Language))) return false;
Expand Down
7 changes: 5 additions & 2 deletions src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Parser from 'tree-sitter';
import { Language } from '../types';

type GrammarLoader = () => unknown;
type GrammarLanguage = Exclude<Language, 'liquid' | 'unknown'>;
type GrammarLanguage = Exclude<Language, 'svelte' | 'liquid' | 'unknown'>;

/**
* Lazy grammar loaders — each language's native binding is only loaded
Expand Down Expand Up @@ -115,6 +115,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.kts': 'kotlin',
'.dart': 'dart',
'.liquid': 'liquid',
'.svelte': 'svelte',
};

/**
Expand Down Expand Up @@ -186,6 +187,7 @@ export function detectLanguage(filePath: string): Language {
* Check if a language is supported by currently available parsers.
*/
export function isLanguageSupported(language: Language): boolean {
if (language === 'svelte') return true; // custom extractor (script block delegation)
if (language === 'liquid') return true; // custom regex extractor
if (language === 'unknown') return false;
return loadGrammar(language) !== null;
Expand All @@ -197,7 +199,7 @@ export function isLanguageSupported(language: Language): boolean {
export function getSupportedLanguages(): Language[] {
const available = (Object.keys(grammarLoaders) as GrammarLanguage[])
.filter((language) => loadGrammar(language) !== null);
return [...available, 'liquid'];
return [...available, 'svelte', 'liquid'];
}

/**
Expand Down Expand Up @@ -241,6 +243,7 @@ export function getLanguageDisplayName(language: Language): string {
swift: 'Swift',
kotlin: 'Kotlin',
dart: 'Dart',
svelte: 'Svelte',
liquid: 'Liquid',
unknown: 'Unknown',
};
Expand Down
201 changes: 201 additions & 0 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,201 @@ export class LiquidExtractor {
}
}

/**
* 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) {
captureException(error, { operation: 'svelte-extraction', filePath: this.filePath });
this.errors.push({
message: `Svelte extraction error: ${error instanceof Error ? error.message : String(error)}`,
severity: '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);
}
}
}

/**
* Extract nodes and edges from source code
*/
Expand All @@ -2297,6 +2492,12 @@ export function extractFromSource(
): ExtractionResult {
const detectedLanguage = language || detectLanguage(filePath);

// Use custom extractor for Svelte
if (detectedLanguage === 'svelte') {
const extractor = new SvelteExtractor(filePath, source);
return extractor.extract();
}

// Use custom extractor for Liquid
if (detectedLanguage === 'liquid') {
const extractor = new LiquidExtractor(filePath, source);
Expand Down
3 changes: 3 additions & 0 deletions src/resolution/frameworks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FrameworkResolver, ResolutionContext } from '../types';
import { laravelResolver } from './laravel';
import { expressResolver } from './express';
import { reactResolver } from './react';
import { svelteResolver } from './svelte';
import { djangoResolver, flaskResolver, fastapiResolver } from './python';
import { railsResolver } from './ruby';
import { springResolver } from './java';
Expand All @@ -25,6 +26,7 @@ const FRAMEWORK_RESOLVERS: FrameworkResolver[] = [
// JavaScript/TypeScript
expressResolver,
reactResolver,
svelteResolver,
// Python
djangoResolver,
flaskResolver,
Expand Down Expand Up @@ -88,6 +90,7 @@ export function registerFrameworkResolver(resolver: FrameworkResolver): void {
export { laravelResolver, FACADE_MAPPINGS } from './laravel';
export { expressResolver } from './express';
export { reactResolver } from './react';
export { svelteResolver } from './svelte';
export { djangoResolver, flaskResolver, fastapiResolver } from './python';
export { railsResolver } from './ruby';
export { springResolver } from './java';
Expand Down
Loading