feat: Add complete Svelte language support with template call extraction
Addresses Svelte function calls invisible in template expressions and ugly destructured variable names. Adds SvelteExtractor that delegates `
This commit is contained in:
@@ -455,6 +455,10 @@ test().catch(console.error);
|
|||||||
| Kotlin `navigation_expression` calls not resolved cleanly | `navigation_expression` fell through to `getNodeText` producing messy names with parentheses | `src/extraction/tree-sitter.ts: extractCall` — handle `navigation_expression` by extracting method name from `navigation_suffix > simple_identifier` |
|
| Kotlin `navigation_expression` calls not resolved cleanly | `navigation_expression` fell through to `getNodeText` producing messy names with parentheses | `src/extraction/tree-sitter.ts: extractCall` — handle `navigation_expression` by extracting method name from `navigation_suffix > simple_identifier` |
|
||||||
| Kotlin `fun interface` declarations invisible | Tree-sitter-kotlin doesn't support `fun interface` syntax (Kotlin 1.4+), producing ERROR or misparse as `function_declaration` | `src/extraction/languages/kotlin.ts: visitNode` detects three misparse patterns: (1) ERROR node + lambda body, (2) function_declaration with `user_type("interface")` direct child + name in ERROR child, (3) function_declaration with ERROR child containing `user_type("interface")` + name. `isFunInterfaceNode` checks both direct and ERROR-nested `user_type` children |
|
| Kotlin `fun interface` declarations invisible | Tree-sitter-kotlin doesn't support `fun interface` syntax (Kotlin 1.4+), producing ERROR or misparse as `function_declaration` | `src/extraction/languages/kotlin.ts: visitNode` detects three misparse patterns: (1) ERROR node + lambda body, (2) function_declaration with `user_type("interface")` direct child + name in ERROR child, (3) function_declaration with ERROR child containing `user_type("interface")` + name. `isFunInterfaceNode` checks both direct and ERROR-nested `user_type` children |
|
||||||
| Kotlin class/interface methods missing when nested `fun interface` present | Tree-sitter misparsed parent body as ERROR (starting with `{`) + class_body (nested interface body); `resolveBody` found wrong body | `src/extraction/languages/kotlin.ts: resolveBody` prefers ERROR bodies starting with `{`; `visitNode` excludes body-like ERROR from `fun interface` detection |
|
| Kotlin class/interface methods missing when nested `fun interface` present | Tree-sitter misparsed parent body as ERROR (starting with `{`) + class_body (nested interface body); `resolveBody` found wrong body | `src/extraction/languages/kotlin.ts: resolveBody` prefers ERROR bodies starting with `{`; `visitNode` excludes body-like ERROR from `fun interface` detection |
|
||||||
|
| Svelte `$props()` destructuring produces ugly variable names | `let { x, y } = $props()` has `object_pattern` as variable name node; `getNodeText` returns full pattern | `src/extraction/tree-sitter.ts: extractVariable` skips `object_pattern`/`array_pattern` named declarators |
|
||||||
|
| Svelte template function calls invisible (e.g. `class={cn(...)}`) | SvelteExtractor only parsed `<script>` blocks, missing calls in template markup | `src/extraction/svelte-extractor.ts: extractTemplateCalls` scans `{expression}` blocks in template for call patterns |
|
||||||
|
| Svelte `$state`/`$derived` rune calls creating noise | Runes are compiler builtins, not real function calls | `src/extraction/svelte-extractor.ts` filters `SVELTE_RUNES` set from unresolved references |
|
||||||
|
| Object literal getters/setters extracted as standalone functions | `method_definition` inside `object` literals treated same as class methods | `src/extraction/tree-sitter.ts: extractMethod` skips `method_definition` nodes whose parent is `object`/`object_expression` |
|
||||||
|
|
||||||
## After Fixing Issues
|
## After Fixing Issues
|
||||||
|
|
||||||
@@ -541,6 +545,7 @@ if (receiverType) {
|
|||||||
- [x] **TypeScript** — NOT needed for `getReceiverType`. Methods nested in class body. Added `abstract_class_declaration` to `classTypes` so abstract classes are properly extracted. Fixed single-expression arrow function extraction (`const fn = () => expr` was silently dropped because `extractName` picked up the body identifier instead of returning `<anonymous>` for parent name resolution). Verified against Grafana
|
- [x] **TypeScript** — NOT needed for `getReceiverType`. Methods nested in class body. Added `abstract_class_declaration` to `classTypes` so abstract classes are properly extracted. Fixed single-expression arrow function extraction (`const fn = () => expr` was silently dropped because `extractName` picked up the body identifier instead of returning `<anonymous>` for parent name resolution). Verified against Grafana
|
||||||
- [x] **Dart** — NOT needed for `getReceiverType`. Methods nested in class body. Added bare call extraction for selector-based method calls (e.g. `object.method()`). Verified against Flutter
|
- [x] **Dart** — NOT needed for `getReceiverType`. Methods nested in class body. Added bare call extraction for selector-based method calls (e.g. `object.method()`). Verified against Flutter
|
||||||
- [x] **Kotlin** — `getReceiverType` extracts receiver from extension functions `fun Type.method()`. Added `classifyClassNode` to distinguish interfaces/enums from classes (all use `class_declaration` AST node). Added `resolveBody` hook since Kotlin's tree-sitter grammar doesn't use field names. Added `navigation_expression` handling for method call extraction. Added `object_declaration` via `extraClassNodeTypes`. Added `delegation_specifier` handling in `extractInheritance` for Kotlin's `: Parent, Interface` syntax. Also fixed `extractInterface` to visit body children (interface methods were not being extracted). Added `visitNode` hook to handle `fun interface` (SAM) declarations — tree-sitter-kotlin doesn't support this Kotlin 1.4+ syntax, producing ERROR or function_declaration misparse; the hook detects both patterns and extracts the interface. Verified against Koin, LeakCanary
|
- [x] **Kotlin** — `getReceiverType` extracts receiver from extension functions `fun Type.method()`. Added `classifyClassNode` to distinguish interfaces/enums from classes (all use `class_declaration` AST node). Added `resolveBody` hook since Kotlin's tree-sitter grammar doesn't use field names. Added `navigation_expression` handling for method call extraction. Added `object_declaration` via `extraClassNodeTypes`. Added `delegation_specifier` handling in `extractInheritance` for Kotlin's `: Parent, Interface` syntax. Also fixed `extractInterface` to visit body children (interface methods were not being extracted). Added `visitNode` hook to handle `fun interface` (SAM) declarations — tree-sitter-kotlin doesn't support this Kotlin 1.4+ syntax, producing ERROR or function_declaration misparse; the hook detects both patterns and extracts the interface. Verified against Koin, LeakCanary
|
||||||
|
- [x] **Svelte** — Custom `SvelteExtractor` delegates `<script>` blocks to TS/JS parser; creates `component` nodes for each `.svelte` file. Added template expression call extraction: scans `{expression}` blocks in markup for function calls (e.g. `class={cn(...)}`), creating call edges from component to callees — increased Svelte call edges from 29 to 387. Filtered Svelte 5 rune calls (`$state`, `$props`, `$derived`, `$effect`, `$bindable`). Also fixed: destructured `$props()` patterns (e.g. `let { x, y } = $props()`) no longer extracted as ugly multi-line variable names (skip `object_pattern`/`array_pattern` in `extractVariable`). Object literal getter/setter methods no longer extracted as standalone functions. Verified against shadcn-svelte
|
||||||
|
|
||||||
### Needs Verification
|
### Needs Verification
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { generateNodeId } from './tree-sitter-helpers';
|
|||||||
import { TreeSitterExtractor } from './tree-sitter';
|
import { TreeSitterExtractor } from './tree-sitter';
|
||||||
import { isLanguageSupported } from './grammars';
|
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
|
* SvelteExtractor - Extracts code relationships from Svelte component files
|
||||||
*
|
*
|
||||||
@@ -10,6 +16,9 @@ import { isLanguageSupported } from './grammars';
|
|||||||
* parsing the full Svelte grammar, we extract the <script> block content
|
* parsing the full Svelte grammar, we extract the <script> block content
|
||||||
* and delegate it to the TypeScript/JavaScript TreeSitterExtractor.
|
* and delegate it to the TypeScript/JavaScript TreeSitterExtractor.
|
||||||
*
|
*
|
||||||
|
* Also extracts function calls from template expressions (`{fn(...)}`) so
|
||||||
|
* cross-file call edges are captured even when calls live in markup.
|
||||||
|
*
|
||||||
* Every .svelte file produces a component node (Svelte components are always importable).
|
* Every .svelte file produces a component node (Svelte components are always importable).
|
||||||
*/
|
*/
|
||||||
export class SvelteExtractor {
|
export class SvelteExtractor {
|
||||||
@@ -41,6 +50,14 @@ export class SvelteExtractor {
|
|||||||
for (const block of scriptBlocks) {
|
for (const block of scriptBlocks) {
|
||||||
this.processScriptBlock(block, componentNode.id);
|
this.processScriptBlock(block, componentNode.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract function calls from template expressions ({fn(...)})
|
||||||
|
this.extractTemplateCalls(componentNode.id, scriptBlocks);
|
||||||
|
|
||||||
|
// Filter out Svelte rune calls ($state, $props, $derived, etc.)
|
||||||
|
this.unresolvedReferences = this.unresolvedReferences.filter(
|
||||||
|
ref => !SVELTE_RUNES.has(ref.referenceName)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.errors.push({
|
this.errors.push({
|
||||||
message: `Svelte extraction error: ${error instanceof Error ? error.message : String(error)}`,
|
message: `Svelte extraction error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
@@ -196,4 +213,64 @@ export class SvelteExtractor {
|
|||||||
this.errors.push(error);
|
this.errors.push(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract function calls from Svelte template expressions.
|
||||||
|
*
|
||||||
|
* In Svelte, many function calls happen in markup (e.g., `class={cn(...)}`),
|
||||||
|
* not inside `<script>` blocks. We scan the template portion for `{expression}`
|
||||||
|
* blocks and extract call patterns from them.
|
||||||
|
*/
|
||||||
|
private extractTemplateCalls(
|
||||||
|
componentNodeId: string,
|
||||||
|
_scriptBlocks: Array<{ content: string; startLine: number }>
|
||||||
|
): void {
|
||||||
|
// Build a set of line ranges covered by <script> and <style> blocks so we skip them
|
||||||
|
const coveredRanges: Array<[number, number]> = [];
|
||||||
|
|
||||||
|
// Find all <script>...</script> and <style>...</style> 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -590,6 +590,12 @@ export class TreeSitterExtractor {
|
|||||||
// Languages with methodsAreTopLevel (e.g. Go) always treat them as methods
|
// Languages with methodsAreTopLevel (e.g. Go) always treat them as methods
|
||||||
// Languages with getReceiverType (e.g. Rust) extract as method when receiver is found
|
// Languages with getReceiverType (e.g. Rust) extract as method when receiver is found
|
||||||
if (!this.isInsideClassLikeNode() && !this.extractor.methodsAreTopLevel && !receiverType) {
|
if (!this.isInsideClassLikeNode() && !this.extractor.methodsAreTopLevel && !receiverType) {
|
||||||
|
// Skip method_definition nodes inside object literals (getters/setters/methods
|
||||||
|
// in inline objects). These are ephemeral and create noise (e.g., Svelte context
|
||||||
|
// objects: `ctx.set({ get view() { ... } })`).
|
||||||
|
if (node.parent?.type === 'object' || node.parent?.type === 'object_expression') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Not inside a class-like node and no receiver type, treat as function
|
// Not inside a class-like node and no receiver type, treat as function
|
||||||
this.extractFunction(node);
|
this.extractFunction(node);
|
||||||
return;
|
return;
|
||||||
@@ -929,6 +935,11 @@ export class TreeSitterExtractor {
|
|||||||
const valueNode = getChildByField(child, 'value');
|
const valueNode = getChildByField(child, 'value');
|
||||||
|
|
||||||
if (nameNode) {
|
if (nameNode) {
|
||||||
|
// Skip destructured patterns (e.g., `let { x, y } = $props()` in Svelte)
|
||||||
|
// These produce ugly multi-line names like "{ class: className }"
|
||||||
|
if (nameNode.type === 'object_pattern' || nameNode.type === 'array_pattern') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const name = getNodeText(nameNode, this.source);
|
const name = getNodeText(nameNode, this.source);
|
||||||
// Arrow functions / function expressions: extract as function instead of variable
|
// Arrow functions / function expressions: extract as function instead of variable
|
||||||
if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) {
|
if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user