mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(typescript): reuse suffix index in scope resolver (#1840)
* fix(typescript): reuse suffix index in scope resolver Build a suffix index once per TypeScript scope-resolution pass and pass it into standard import resolution so package-style imports avoid repeated linear file-list scans.\n\nFixes #1839 * test(typescript): add wiring-level test for scope-resolver suffix index - Test typescriptScopeResolver.resolveImportTarget directly (the real production entry point) with package-style, unresolvable, and relative imports - Use vi.spyOn on buildSuffixIndex to verify the index is built inside the makeTsResolveImportTarget closure — fails if index wiring is removed - Fix existing test to pass real file lists instead of empty arrays alongside the prebuilt index, matching production wiring --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Test <test@example.com>
This commit is contained in:
parent
6c572749b0
commit
d903152eba
3 changed files with 78 additions and 2 deletions
|
|
@ -17,6 +17,7 @@
|
|||
import type { ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { resolveImportPath } from '../../import-resolvers/standard.js';
|
||||
import type { SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import type { TsconfigPaths } from '../../language-config.js';
|
||||
|
||||
export interface TsResolveContext {
|
||||
|
|
@ -31,6 +32,8 @@ export interface TsResolveContext {
|
|||
readonly normalizedFileList?: readonly string[];
|
||||
/** Per-call resolution cache to dedupe repeated lookups. */
|
||||
readonly resolveCache?: Map<string, string | null>;
|
||||
/** Prebuilt suffix index for O(1)-style package/absolute import matching. */
|
||||
readonly index?: SuffixIndex;
|
||||
/** Parsed tsconfig path-aliases. `null` = no aliases configured. */
|
||||
readonly tsconfigPaths?: TsconfigPaths | null;
|
||||
/** JavaScript vs TypeScript switch — affects the extensions the
|
||||
|
|
@ -84,6 +87,7 @@ export function resolveTsTarget(targetRaw: string, ctx: TsResolveContext): strin
|
|||
resolveCache,
|
||||
language,
|
||||
ctx.tsconfigPaths ?? null,
|
||||
ctx.index,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.
|
|||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { typescriptProvider } from '../typescript.js';
|
||||
import { loadTsconfigPaths, type TsconfigPaths } from '../../language-config.js';
|
||||
import { buildSuffixIndex, type SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import {
|
||||
typescriptArityCompatibility,
|
||||
typescriptMergeBindings,
|
||||
|
|
@ -50,6 +51,7 @@ function makeTsResolveImportTarget(): ScopeResolver['resolveImportTarget'] {
|
|||
readonly allFilePaths: Set<string>;
|
||||
readonly allFileList: readonly string[];
|
||||
readonly normalizedFileList: readonly string[];
|
||||
readonly index: SuffixIndex;
|
||||
readonly resolveCache: Map<string, string | null>;
|
||||
}
|
||||
let cached: PassCache | null = null;
|
||||
|
|
@ -57,11 +59,13 @@ function makeTsResolveImportTarget(): ScopeResolver['resolveImportTarget'] {
|
|||
return (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
|
||||
if (cached === null || cached.key !== allFilePaths) {
|
||||
const allFileList = Array.from(allFilePaths);
|
||||
const normalizedFileList = allFileList.map((f) => f.toLowerCase());
|
||||
cached = {
|
||||
key: allFilePaths,
|
||||
allFilePaths: new Set(allFilePaths),
|
||||
allFileList,
|
||||
normalizedFileList: allFileList.map((f) => f.toLowerCase()),
|
||||
normalizedFileList,
|
||||
index: buildSuffixIndex(normalizedFileList, allFileList),
|
||||
resolveCache: new Map(),
|
||||
};
|
||||
}
|
||||
|
|
@ -72,6 +76,7 @@ function makeTsResolveImportTarget(): ScopeResolver['resolveImportTarget'] {
|
|||
allFilePaths: cached.allFilePaths,
|
||||
allFileList: cached.allFileList,
|
||||
normalizedFileList: cached.normalizedFileList,
|
||||
index: cached.index,
|
||||
resolveCache: cached.resolveCache,
|
||||
tsconfigPaths: cfg?.tsconfigPaths ?? null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* set of fake file paths (with and without tsconfig path aliases).
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { emitTsScopeCaptures } from '../../../../src/core/ingestion/languages/typescript/captures.js';
|
||||
import { splitImportStatement } from '../../../../src/core/ingestion/languages/typescript/import-decomposer.js';
|
||||
import { interpretTsImport } from '../../../../src/core/ingestion/languages/typescript/interpret.js';
|
||||
|
|
@ -14,6 +14,9 @@ import {
|
|||
resolveTsImportTarget,
|
||||
type TsResolveContext,
|
||||
} from '../../../../src/core/ingestion/languages/typescript/import-target.js';
|
||||
import { buildSuffixIndex } from '../../../../src/core/ingestion/import-resolvers/utils.js';
|
||||
import * as importResolverUtils from '../../../../src/core/ingestion/import-resolvers/utils.js';
|
||||
import { typescriptScopeResolver } from '../../../../src/core/ingestion/languages/typescript/scope-resolver.js';
|
||||
import type { SyntaxNode } from '../../../../src/core/ingestion/utils/ast-helpers.js';
|
||||
import type { ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
|
@ -399,4 +402,68 @@ describe('resolveTsImportTarget — standard suffix + alias resolution', () => {
|
|||
);
|
||||
expect(result).toBe('src/a.js');
|
||||
});
|
||||
|
||||
it('uses a prebuilt suffix index for package-style imports', () => {
|
||||
const parsed: ParsedImport = {
|
||||
kind: 'named',
|
||||
localName: 'Button',
|
||||
importedName: 'Button',
|
||||
targetRaw: 'components/Button',
|
||||
};
|
||||
const files = ['src/main.ts', 'src/components/Button.ts'];
|
||||
const index = buildSuffixIndex(
|
||||
files.map((f) => f.toLowerCase()),
|
||||
files,
|
||||
);
|
||||
const result = resolveTsImportTarget(
|
||||
parsed,
|
||||
ctx('src/main.ts', files, {
|
||||
allFileList: files,
|
||||
normalizedFileList: files.map((f) => f.toLowerCase()),
|
||||
index,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBe('src/components/Button.ts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('typescriptScopeResolver.resolveImportTarget — real wiring', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('builds suffix index and resolves package-style imports through the production path', () => {
|
||||
const spy = vi.spyOn(importResolverUtils, 'buildSuffixIndex');
|
||||
const files = new Set(['src/main.ts', 'src/components/Button.ts']);
|
||||
|
||||
const result = typescriptScopeResolver.resolveImportTarget!(
|
||||
'components/Button',
|
||||
'src/main.ts',
|
||||
files,
|
||||
);
|
||||
|
||||
expect(result).toBe('src/components/Button.ts');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns null for unresolvable package-style imports', () => {
|
||||
const files = new Set(['src/main.ts', 'src/utils/helper.ts']);
|
||||
|
||||
const result = typescriptScopeResolver.resolveImportTarget!(
|
||||
'nonexistent/Module',
|
||||
'src/main.ts',
|
||||
files,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('resolves relative imports through the scope-resolver entry point', () => {
|
||||
const files = new Set(['src/main.ts', 'src/utils.ts']);
|
||||
|
||||
const result = typescriptScopeResolver.resolveImportTarget!('./utils', 'src/main.ts', files);
|
||||
|
||||
expect(result).toBe('src/utils.ts');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue