From eef95a177be05f1899f91a5e48397da913beaedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Grimal=20Torres?= Date: Wed, 1 Jul 2026 23:29:19 +0200 Subject: [PATCH 1/5] feat: Typescript ignore type imports setting --- package-lock.json | 18 -------- package.json | 5 +++ src/analyzer/IndexerWorker.ts | 3 ++ src/analyzer/IndexerWorkerHost.ts | 1 + src/analyzer/LanguageService.ts | 12 +++--- src/analyzer/Parser.ts | 12 +++++- src/analyzer/SpiderBuilder.ts | 18 +++++++- src/analyzer/spider/SpiderIndexingService.ts | 1 + src/analyzer/spider/SpiderWorkerManager.ts | 2 + src/analyzer/types.ts | 2 + .../services/ProviderStateManager.ts | 2 + .../services/graphProviderServiceContainer.ts | 1 + tests/analyzer/LanguageService.test.ts | 19 +++++++++ tests/analyzer/SpiderBuilder.test.ts | 13 +++--- tests/analyzer/parser-type-imports.test.ts | 41 +++++++++++++++++++ tests/extension/ProviderStateManager.test.ts | 2 + 16 files changed, 122 insertions(+), 30 deletions(-) create mode 100644 tests/analyzer/parser-type-imports.test.ts diff --git a/package-lock.json b/package-lock.json index 3ff3d6f3..3d15e619 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1859,9 +1859,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1879,9 +1876,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1899,9 +1893,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1919,9 +1910,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1939,9 +1927,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1959,9 +1944,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 74384f49..1f11ffdb 100644 --- a/package.json +++ b/package.json @@ -943,6 +943,11 @@ "default": true, "description": "Exclude node_modules from graph" }, + "graph-it-live.ignoreTypeImports": { + "type": "boolean", + "default": false, + "description": "Ignore TypeScript type-only imports and exports in the dependency graph. You might need to restart VS Code for this setting to take effect." + }, "graph-it-live.enableMcpServer": { "type": "boolean", "default": false, diff --git a/src/analyzer/IndexerWorker.ts b/src/analyzer/IndexerWorker.ts index 76452a6c..4c5247d3 100644 --- a/src/analyzer/IndexerWorker.ts +++ b/src/analyzer/IndexerWorker.ts @@ -62,6 +62,8 @@ interface WorkerConfig { * Example: context.extensionPath from VS Code extension activation */ extensionPath?: string; + /** When true, type-only imports/exports are not treated as file dependencies. */ + ignoreTypeImports?: boolean; } interface WorkerMessage { @@ -218,6 +220,7 @@ async function runIndexing(config: WorkerConfig): Promise { config.rootDir, config.tsConfigPath, config.extensionPath, + config.ignoreTypeImports, ); // Phase 1: Count files without loading paths into memory diff --git a/src/analyzer/IndexerWorkerHost.ts b/src/analyzer/IndexerWorkerHost.ts index 4b60846c..3fd652bb 100644 --- a/src/analyzer/IndexerWorkerHost.ts +++ b/src/analyzer/IndexerWorkerHost.ts @@ -15,6 +15,7 @@ interface WorkerConfig { excludeNodeModules?: boolean; tsConfigPath?: string; extensionPath?: string; + ignoreTypeImports?: boolean; /** * Maximum ms to wait for indexing before forcefully terminating the worker. * Protects against hangs due to WASM initialization failures or silent crashes. diff --git a/src/analyzer/LanguageService.ts b/src/analyzer/LanguageService.ts index bd47d75d..c123cbf9 100644 --- a/src/analyzer/LanguageService.ts +++ b/src/analyzer/LanguageService.ts @@ -41,10 +41,12 @@ export class LanguageService { private readonly rootDir?: string; private readonly extensionPath?: string; + private readonly ignoreTypeImports?: boolean; - constructor(rootDir?: string, _tsConfigPath?: string, extensionPath?: string) { + constructor(rootDir?: string, _tsConfigPath?: string, extensionPath?: string, ignoreTypeImports?: boolean) { this.rootDir = rootDir; this.extensionPath = extensionPath; + this.ignoreTypeImports = ignoreTypeImports; } /** @@ -52,7 +54,7 @@ export class LanguageService { */ getAnalyzer(filePath: string): ILanguageAnalyzer { const actualPath = extractFilePath(filePath); - return LanguageService.getAnalyzer(actualPath, this.rootDir, this.extensionPath); + return LanguageService.getAnalyzer(actualPath, this.rootDir, this.extensionPath, this.ignoreTypeImports); } /** @@ -111,18 +113,18 @@ export class LanguageService { * Get the appropriate parser for the given file path. * Lazy-loads parsers only when needed. */ - static getAnalyzer(filePath: string, rootDir?: string, extensionPath?: string): ILanguageAnalyzer { + static getAnalyzer(filePath: string, rootDir?: string, extensionPath?: string, ignoreTypeImports?: boolean): ILanguageAnalyzer { const actualPath = extractFilePath(filePath); const language = this.detectLanguage(actualPath); switch (language) { case Language.TypeScript: { - const cacheKey = this.buildCacheKey(rootDir); + const cacheKey = this.buildCacheKey(rootDir, ignoreTypeImports ? "ignore-types" : ""); const cached = this.typeScriptParsers.get(cacheKey); if (cached) { return cached; } - const created = new Parser(rootDir); + const created = new Parser(rootDir, ignoreTypeImports); this.typeScriptParsers.set(cacheKey, created); return created; } diff --git a/src/analyzer/Parser.ts b/src/analyzer/Parser.ts index 612b824f..a54751c7 100644 --- a/src/analyzer/Parser.ts +++ b/src/analyzer/Parser.ts @@ -24,9 +24,11 @@ export class Parser implements ILanguageAnalyzer { "g", ); })(); - constructor(rootDir?: string) { + private readonly ignoreTypeImports: boolean; + constructor(rootDir?: string, ignoreTypeImports?: boolean) { this.fileReader = new FileReader(); this.pathResolver = new PathResolver(rootDir); + this.ignoreTypeImports = ignoreTypeImports ?? false; } // Regex patterns for different import types private readonly patterns = { @@ -175,6 +177,14 @@ export class Parser implements ILanguageAnalyzer { continue; } + // Skip type-only imports if configured to ignore them + if (this.ignoreTypeImports) { + const fullMatch = match[0].trim(); + if (fullMatch.startsWith("import type") || fullMatch.startsWith("export type")) { + continue; + } + } + seen.add(module); // Find line number diff --git a/src/analyzer/SpiderBuilder.ts b/src/analyzer/SpiderBuilder.ts index 057797d9..303faa06 100644 --- a/src/analyzer/SpiderBuilder.ts +++ b/src/analyzer/SpiderBuilder.ts @@ -276,6 +276,7 @@ export class SpiderBuilder { private maxSymbolCacheSize: number = 200; private maxSymbolAnalyzerFiles: number = 100; private indexingProgressInterval?: number; + private ignoreTypeImports: boolean = false; // Service overrides (for testing) private customCache?: Cache; @@ -393,6 +394,17 @@ export class SpiderBuilder { return this; } + /** + * Set whether to ignore type-only imports and exports when parsing dependencies (default: false). + * + * @param ignore - Whether to ignore type-only imports/exports + * @returns This builder instance for method chaining + */ + withIgnoreTypeImports(ignore: boolean): this { + this.ignoreTypeImports = ignore; + return this; + } + /** * Enable or disable reverse index (default: false). * @@ -559,6 +571,9 @@ export class SpiderBuilder { if (config.excludeNodeModules !== undefined) { this.excludeNodeModules = config.excludeNodeModules; } + if (config.ignoreTypeImports !== undefined) { + this.ignoreTypeImports = config.ignoreTypeImports; + } if (config.enableReverseIndex !== undefined) { this.enableReverseIndex = config.enableReverseIndex; } @@ -781,7 +796,7 @@ export class SpiderBuilder { // Phase 1: Core services (no dependencies) const languageService = this.customLanguageService ?? - new LanguageService(this.rootDir!, this.tsConfigPath, this.extensionPath); + new LanguageService(this.rootDir!, this.tsConfigPath, this.extensionPath, this.ignoreTypeImports); const resolver = this.customPathResolver ?? new PathResolver(this.tsConfigPath, this.excludeNodeModules, this.rootDir!); @@ -946,6 +961,7 @@ export class SpiderBuilder { extensionPath: this.extensionPath, maxDepth: this.maxDepth, excludeNodeModules: this.excludeNodeModules, + ignoreTypeImports: this.ignoreTypeImports, enableReverseIndex: this.enableReverseIndex, indexingConcurrency: this.indexingConcurrency, maxCacheSize: this.maxCacheSize, diff --git a/src/analyzer/spider/SpiderIndexingService.ts b/src/analyzer/spider/SpiderIndexingService.ts index 3fe8ebe6..e05d1313 100644 --- a/src/analyzer/spider/SpiderIndexingService.ts +++ b/src/analyzer/spider/SpiderIndexingService.ts @@ -155,6 +155,7 @@ export class SpiderIndexingService { tsConfigPath: config.tsConfigPath, progressInterval: config.indexingProgressInterval, extensionPath: config.extensionPath, + ignoreTypeImports: config.ignoreTypeImports, }, }); } diff --git a/src/analyzer/spider/SpiderWorkerManager.ts b/src/analyzer/spider/SpiderWorkerManager.ts index 23c64c4a..fdb7e1f8 100644 --- a/src/analyzer/spider/SpiderWorkerManager.ts +++ b/src/analyzer/spider/SpiderWorkerManager.ts @@ -12,6 +12,7 @@ interface WorkerBuildOptions { tsConfigPath?: string; progressInterval?: number; extensionPath?: string; + ignoreTypeImports?: boolean; }; progressCallback?: IndexingProgressCallback; } @@ -74,6 +75,7 @@ export class SpiderWorkerManager { excludeNodeModules: config.excludeNodeModules, tsConfigPath: config.tsConfigPath, extensionPath: config.extensionPath, + ignoreTypeImports: config.ignoreTypeImports, }); this.importWorkerResult(result); diff --git a/src/analyzer/types.ts b/src/analyzer/types.ts index bde56512..80e1e1c7 100644 --- a/src/analyzer/types.ts +++ b/src/analyzer/types.ts @@ -158,6 +158,8 @@ export interface SpiderConfig { extensionPath?: string; maxDepth?: number; excludeNodeModules?: boolean; + /** Ignore type-only imports/exports on TypeScript files */ + ignoreTypeImports?: boolean; /** Interval for worker progress reporting (defaults to 100 files) */ indexingProgressInterval?: number; /** Enable reverse index for O(1) reverse dependency lookups */ diff --git a/src/extension/services/ProviderStateManager.ts b/src/extension/services/ProviderStateManager.ts index 0b23a939..19a33941 100644 --- a/src/extension/services/ProviderStateManager.ts +++ b/src/extension/services/ProviderStateManager.ts @@ -9,6 +9,7 @@ export interface ProviderConfigSnapshot extends BackgroundIndexingConfig { excludeNodeModules: boolean; maxDepth: number; indexingConcurrency: number; + ignoreTypeImports: boolean; unusedDependencyMode: UnusedDependencyMode; unusedAnalysisConcurrency: number; unusedAnalysisMaxEdges: number; @@ -56,6 +57,7 @@ export class ProviderStateManager { : profileDefaults.indexingConcurrency, indexingStartDelay: config.get('indexingStartDelay', this.defaultIndexingDelay), persistIndex: config.get('persistIndex', false), + ignoreTypeImports: config.get('ignoreTypeImports', false), unusedDependencyMode: config.get<'none' | 'hide' | 'dim'>('unusedDependencyMode', 'none'), unusedAnalysisConcurrency: isCustomProfile ? config.get('unusedAnalysisConcurrency', profileDefaults.unusedAnalysisConcurrency) diff --git a/src/extension/services/graphProviderServiceContainer.ts b/src/extension/services/graphProviderServiceContainer.ts index 9178c7be..d514a67d 100644 --- a/src/extension/services/graphProviderServiceContainer.ts +++ b/src/extension/services/graphProviderServiceContainer.ts @@ -140,6 +140,7 @@ export function createGraphProviderServiceContainer( .withTsConfigPath(path.join(workspaceRoot, "tsconfig.json")) .withExtensionPath(options.context.extensionPath) .withExcludeNodeModules(configSnapshot.excludeNodeModules) + .withIgnoreTypeImports(configSnapshot.ignoreTypeImports) .withMaxDepth(configSnapshot.maxDepth) .withReverseIndex(configSnapshot.enableBackgroundIndexing) .withIndexingConcurrency(configSnapshot.indexingConcurrency) diff --git a/tests/analyzer/LanguageService.test.ts b/tests/analyzer/LanguageService.test.ts index cbc8a97d..aca88aea 100644 --- a/tests/analyzer/LanguageService.test.ts +++ b/tests/analyzer/LanguageService.test.ts @@ -194,6 +194,25 @@ describe("LanguageService", () => { expect(analyzer1).toBe(analyzer2); }); + it("should use a separate TypeScript parser when ignoreTypeImports changes", () => { + const analyzer1 = LanguageService.getAnalyzer("/project/file1.ts"); + const analyzer2 = LanguageService.getAnalyzer( + "/project/file2.ts", + undefined, + undefined, + true, + ); + const analyzer3 = LanguageService.getAnalyzer( + "/project/file3.ts", + undefined, + undefined, + true, + ); + + expect(analyzer1).not.toBe(analyzer2); + expect(analyzer2).toBe(analyzer3); + }); + it("should throw error for Python files (not yet implemented)", () => { const analyzer = LanguageService.getAnalyzer("/project/main.py"); expect(analyzer).toBeDefined(); diff --git a/tests/analyzer/SpiderBuilder.test.ts b/tests/analyzer/SpiderBuilder.test.ts index f2474273..5bbe28e7 100644 --- a/tests/analyzer/SpiderBuilder.test.ts +++ b/tests/analyzer/SpiderBuilder.test.ts @@ -18,11 +18,12 @@ describe('SpiderBuilder', () => { const result1 = builder.withRootDir(fixturesPath); const result2 = result1.withMaxDepth(50); const result3 = result2.withExcludeNodeModules(true); - const result4 = result3.withReverseIndex(false); - const result5 = result4.withIndexingConcurrency(4); - const result6 = result5.withCacheConfig({ maxCacheSize: 500 }); - const result7 = result6.withTsConfigPath(path.join(fixturesPath, 'tsconfig.json')); - const result8 = result7.withIndexingProgressInterval(100); + const result4 = result3.withIgnoreTypeImports(true); + const result5 = result4.withReverseIndex(false); + const result6 = result5.withIndexingConcurrency(4); + const result7 = result6.withCacheConfig({ maxCacheSize: 500 }); + const result8 = result7.withTsConfigPath(path.join(fixturesPath, 'tsconfig.json')); + const result9 = result8.withIndexingProgressInterval(100); // All methods should return the same builder instance expect(result1).toBe(builder); @@ -33,6 +34,7 @@ describe('SpiderBuilder', () => { expect(result6).toBe(builder); expect(result7).toBe(builder); expect(result8).toBe(builder); + expect(result9).toBe(builder); }); it('should support method chaining in a single expression', () => { @@ -277,6 +279,7 @@ describe('SpiderBuilder', () => { tsConfigPath: path.join(fixturesPath, 'tsconfig.json'), maxDepth: 30, excludeNodeModules: false, + ignoreTypeImports: true, enableReverseIndex: true, indexingConcurrency: 8, maxCacheSize: 1000, diff --git a/tests/analyzer/parser-type-imports.test.ts b/tests/analyzer/parser-type-imports.test.ts new file mode 100644 index 00000000..d0e0a6b7 --- /dev/null +++ b/tests/analyzer/parser-type-imports.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { Parser } from '../../src/analyzer/Parser'; + +describe('Parser - Type-Only Imports', () => { + it('should skip type-only imports and exports when configured', () => { + const parser = new Parser(undefined, true); + + const content = ` +import type { Foo } from './foo'; +import { Bar } from './bar'; +export type { Baz } from './baz'; +export { Quux } from './quux'; +`; + + const imports = parser.parse(content, '/project/file.ts'); + + expect(imports).toHaveLength(2); + expect(imports.map((entry) => entry.module)).toEqual(['./bar', './quux']); + }); + + it('should keep type-only imports and exports when not configured to ignore them', () => { + const parser = new Parser(); + + const content = ` +import type { Foo } from './foo'; +import { Bar } from './bar'; +export type { Baz } from './baz'; +export { Quux } from './quux'; +`; + + const imports = parser.parse(content, '/project/file.ts'); + + expect(imports).toHaveLength(4); + expect(imports.map((entry) => entry.module)).toEqual([ + './foo', + './bar', + './baz', + './quux', + ]); + }); +}); \ No newline at end of file diff --git a/tests/extension/ProviderStateManager.test.ts b/tests/extension/ProviderStateManager.test.ts index c13ff59f..d73b969e 100644 --- a/tests/extension/ProviderStateManager.test.ts +++ b/tests/extension/ProviderStateManager.test.ts @@ -8,6 +8,7 @@ const configValues: Record = { indexingConcurrency: 8, indexingStartDelay: 2000, persistIndex: true, + ignoreTypeImports: true, }; vi.mock('vscode', () => ({ @@ -51,6 +52,7 @@ describe('ProviderStateManager', () => { expect(snapshot.maxDepth).toBe(25); expect(snapshot.enableBackgroundIndexing).toBe(true); expect(snapshot.indexingStartDelay).toBe(2000); + expect(snapshot.ignoreTypeImports).toBe(true); }); it('tracks current symbol path', () => { From e6539929f8cd6d8fb3130fe2b4d90df293b324fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Grimal?= <105001778+oscar30gt@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:36:14 +0200 Subject: [PATCH 2/5] fix: inside-braces type imports Old version was only ignoring "import type" and "export type" clauses. Now import/export { type foo, type bar} from ... is also recognized as a type import/export Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/analyzer/Parser.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/analyzer/Parser.ts b/src/analyzer/Parser.ts index a54751c7..b1c81edc 100644 --- a/src/analyzer/Parser.ts +++ b/src/analyzer/Parser.ts @@ -180,9 +180,24 @@ export class Parser implements ILanguageAnalyzer { // Skip type-only imports if configured to ignore them if (this.ignoreTypeImports) { const fullMatch = match[0].trim(); - if (fullMatch.startsWith("import type") || fullMatch.startsWith("export type")) { + + // `import type ...` / `export type ...` + if (/^(import|export)\s+type\b/.test(fullMatch)) { continue; } + + // TS 5+ type-only named imports/exports: `import { type Foo } from '...'` + const braceMatch = fullMatch.match(/^(import|export)\s+\{([\s\S]*?)\}\s+from\b/); + if (braceMatch) { + const specifiers = braceMatch[2] + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + + if (specifiers.length > 0 && specifiers.every((s) => /^type\b/.test(s))) { + continue; + } + } } seen.add(module); From 74a1b149277bf276db032546fd6f3794c0aa09fe Mon Sep 17 00:00:00 2001 From: magic5644 Date: Fri, 3 Jul 2026 09:11:24 +0200 Subject: [PATCH 3/5] chore: remove eslint disable comment for explicit any in WikiGenerator constructor --- src/analyzer/wiki/WikiGenerator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/analyzer/wiki/WikiGenerator.ts b/src/analyzer/wiki/WikiGenerator.ts index 8823bf2b..78b72975 100644 --- a/src/analyzer/wiki/WikiGenerator.ts +++ b/src/analyzer/wiki/WikiGenerator.ts @@ -140,7 +140,6 @@ export class WikiGenerator { private readonly externalNodeMetadata: Record | undefined; constructor(opts: WikiGeneratorOptions) { - this.db = opts.db; this.outputDir = opts.outputDir; this.workspaceRoot = opts.workspaceRoot; From 2c75373e151d9fc94a2bf0bbaebdc6b85bd8302d Mon Sep 17 00:00:00 2001 From: magic5644 Date: Fri, 3 Jul 2026 09:11:36 +0200 Subject: [PATCH 4/5] test: cover per-specifier and mixed type-only imports --- tests/analyzer/parser-type-imports.test.ts | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/analyzer/parser-type-imports.test.ts b/tests/analyzer/parser-type-imports.test.ts index d0e0a6b7..2338ed3d 100644 --- a/tests/analyzer/parser-type-imports.test.ts +++ b/tests/analyzer/parser-type-imports.test.ts @@ -38,4 +38,38 @@ export { Quux } from './quux'; './quux', ]); }); + + it('should skip TS 5+ per-specifier type-only imports when configured', () => { + const parser = new Parser(undefined, true); + + const content = ` +import { type Foo } from './foo'; +import { type Bar, type Baz } from './types'; +import { Quux } from './quux'; +export { type Corge } from './corge'; +`; + + const imports = parser.parse(content, '/project/file.ts'); + + expect(imports).toHaveLength(1); + expect(imports.map((entry) => entry.module)).toEqual(['./quux']); + }); + + it('should NOT skip mixed imports (type + value specifiers) when configured', () => { + const parser = new Parser(undefined, true); + + const content = ` +import { type Foo, Bar } from './mixed'; +import { type Baz, type Qux } from './types-only'; +import { Value } from './value'; +`; + + const imports = parser.parse(content, '/project/file.ts'); + + // './mixed' has a value specifier (Bar) → must be kept + // './types-only' has only type specifiers → skipped + // './value' is a value import → kept + expect(imports).toHaveLength(2); + expect(imports.map((entry) => entry.module)).toEqual(['./mixed', './value']); + }); }); \ No newline at end of file From 1c289aa8430af94cc4ccde97e75ec26565e93e52 Mon Sep 17 00:00:00 2001 From: magic5644 Date: Fri, 3 Jul 2026 09:11:49 +0200 Subject: [PATCH 5/5] chore: revert package-lock.json churn from npm version difference --- package-lock.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3d15e619..3ff3d6f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1859,6 +1859,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1876,6 +1879,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1893,6 +1899,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1910,6 +1919,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1927,6 +1939,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1944,6 +1959,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [