diff --git a/src/services/code-graph.ts b/src/services/code-graph.ts index c51f8c8..79fb310 100644 --- a/src/services/code-graph.ts +++ b/src/services/code-graph.ts @@ -305,6 +305,9 @@ export function getAstGrepLang(ext: string): Lang | string | null { ".dart": "dart", ".lua": "lua", ".sh": "bash", ".bash": "bash", ".zsh": "bash", + // Composite languages (parsed via HTML + script re-parse) + ".svelte": "svelte", + ".vue": "vue", // Built-in languages (Lang enum) ".js": Lang.JavaScript, ".jsx": Lang.JavaScript, ".mjs": Lang.JavaScript, ".cjs": Lang.JavaScript, ".ts": Lang.TypeScript, diff --git a/src/services/graph-imports.ts b/src/services/graph-imports.ts index 3d8e863..1b60799 100644 --- a/src/services/graph-imports.ts +++ b/src/services/graph-imports.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Giancarlo Erra - Altaire Limited -import { type Lang, parse } from "@ast-grep/napi"; +import { Lang, parse } from "@ast-grep/napi"; import { logger } from "./logger.js"; // ── Import extraction per language ─────────────────────────────────────── @@ -10,6 +10,46 @@ export interface ImportInfo { isDynamic: boolean; } +/** Extract JS/TS imports from an ast-grep root node. Shared by JS/TS and Svelte/Vue handlers. */ +function extractJsTsImportsFromNode(sgNode: ReturnType["root"]>): ImportInfo[] { + const imports: ImportInfo[] = []; + + // import ... from "..." + for (const node of sgNode.findAll({ rule: { kind: "import_statement" } })) { + const sourceNode = node.find({ rule: { kind: "string" } }); + if (sourceNode) { + const spec = sourceNode.text().replace(/['"]/g, ""); + imports.push({ moduleSpecifier: spec, isDynamic: false }); + } + } + // require("...") + for (const node of sgNode.findAll({ rule: { kind: "call_expression" } })) { + const text = node.text(); + const match = text.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/); + if (match) { + imports.push({ moduleSpecifier: match[1], isDynamic: false }); + } + } + // dynamic import("...") + for (const node of sgNode.findAll({ rule: { kind: "call_expression" } })) { + const text = node.text(); + const match = text.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/); + if (match) { + imports.push({ moduleSpecifier: match[1], isDynamic: true }); + } + } + // export ... from "..." + for (const node of sgNode.findAll({ rule: { kind: "export_statement" } })) { + const sourceNode = node.find({ rule: { kind: "string" } }); + if (sourceNode) { + const spec = sourceNode.text().replace(/['"]/g, ""); + imports.push({ moduleSpecifier: spec, isDynamic: false }); + } + } + + return imports; +} + /** * Extract import statements from source code using ast-grep. * Returns raw module specifiers for each language's import syntax. @@ -43,6 +83,29 @@ export function extractImports(source: string, lang: Lang | string, _ext: string return imports; } + // ── Svelte/Vue: parse as HTML, extract + + +`; + const imports = extractImports(source, "svelte", ".svelte"); + const specs = imports.map((i) => i.moduleSpecifier); + + expect(specs).toContain("svelte"); + expect(specs).toContain("./Button.svelte"); + expect(specs).toContain("../types.js"); + }); + + it("extracts imports from + + + +
content
+`; + const imports = extractImports(source, "svelte", ".svelte"); + const specs = imports.map((i) => i.moduleSpecifier); + + expect(specs).toContain("./Button.svelte"); + expect(specs).toContain("svelte"); + }); + + it("extracts dynamic imports from Svelte files", () => { + const source = ` + +`; + const imports = extractImports(source, "svelte", ".svelte"); + const dynamicImports = imports.filter((i) => i.isDynamic); + + expect(dynamicImports.length).toBeGreaterThanOrEqual(1); + expect( + dynamicImports.some( + (i) => i.moduleSpecifier === "./DynamicComponent.svelte", + ), + ).toBe(true); + }); + + it("handles Svelte files with no script block", () => { + const source = ` +
Just markup, no script
+ +`; + const imports = extractImports(source, "svelte", ".svelte"); + expect(imports).toHaveLength(0); + }); + + it("handles Svelte files with JavaScript (no lang=ts)", () => { + const source = ` + +`; + const imports = extractImports(source, "svelte", ".svelte"); + const specs = imports.map((i) => i.moduleSpecifier); + + expect(specs).toContain("svelte/store"); + expect(specs).toContain("./Item.svelte"); + }); + }); + + // ── Vue ──────────────────────────────────────────────────────────────── + + describe("Vue imports", () => { + it("extracts imports from + + +`; + const imports = extractImports(source, "vue", ".vue"); + const specs = imports.map((i) => i.moduleSpecifier); + + expect(specs).toContain("vue"); + expect(specs).toContain("./MyComponent.vue"); + }); + }); + // ── Python ───────────────────────────────────────────────────────────── describe("Python imports", () => { @@ -342,8 +450,12 @@ import kotlinx.coroutines.launch const specs = imports.map((i) => i.moduleSpecifier); expect(specs.length).toBeGreaterThanOrEqual(3); - expect(specs.some((s) => s.includes("com.example.models.User"))).toBe(true); - expect(specs.some((s) => s.includes("com.example.utils.StringHelper"))).toBe(true); + expect(specs.some((s) => s.includes("com.example.models.User"))).toBe( + true, + ); + expect( + specs.some((s) => s.includes("com.example.utils.StringHelper")), + ).toBe(true); }); it("handles wildcard imports", () => { @@ -353,7 +465,9 @@ import com.example.models.* const imports = extractImports(source, "kotlin", ".kt"); expect(imports.length).toBeGreaterThanOrEqual(1); - expect(imports.some((i) => i.moduleSpecifier.includes("com.example.models"))).toBe(true); + expect( + imports.some((i) => i.moduleSpecifier.includes("com.example.models")), + ).toBe(true); }); }); @@ -372,7 +486,11 @@ import com.example.services._ const specs = imports.map((i) => i.moduleSpecifier); expect(specs.length).toBeGreaterThanOrEqual(2); - expect(specs.some((s) => s.includes("scala.collection") || s.includes("ListBuffer"))).toBe(true); + expect( + specs.some( + (s) => s.includes("scala.collection") || s.includes("ListBuffer"), + ), + ).toBe(true); }); }); @@ -429,7 +547,9 @@ using static System.Math; const imports = extractImports(source, "csharp", ".cs"); expect(imports.length).toBeGreaterThanOrEqual(1); - expect(imports.some((i) => i.moduleSpecifier.includes("System.Math"))).toBe(true); + expect( + imports.some((i) => i.moduleSpecifier.includes("System.Math")), + ).toBe(true); }); it("skips using alias directives", () => {