mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
refactor(ingestion): one per-file-set memo primitive, twenty-one call sites
Every language that indexes its import resolution hand-rolled the same memo:
declare a module-level `WeakMap` keyed on the file-set object, `get`,
`if undefined` build and `set`, return. One concept, written twenty-one times,
and this branch had just added five more.
`import-resolvers/per-file-set.ts` exports it once:
perFileSet<K extends object, T extends object>(build: (key: K) => T): (key: K) => T
Two decisions, both recorded in the file. `T extends object` rather than
`has`-then-`get`: `WeakMap.get` returning `undefined` cannot distinguish "not
built" from "built as undefined", and the `has` form needs a cast or a non-null
assertion, both banned here — the constraint makes the ambiguous case
unrepresentable instead, and a future caller wanting `string | null` gets a
compile error pointing at the decision. A throwing build stores nothing and
runs again next call, so failures are not memoized and a half-filled index is
never published — inert for these pure builders, and the safer direction.
`K extends object` rather than `ReadonlySet<string>` is what lets C#'s
`readonly string[]`-keyed cache share the helper.
Twenty-one sites migrated across `import-resolvers/` and fifteen languages.
Every existing doc comment was re-homed onto the new call rather than deleted —
several record real invariants (the Set-identity contract, the #1918
pass-through rule, why Rust's memo lives on a different hook).
TypeScript, JavaScript and Vue additionally had byte-identical `PassCache`
interfaces and builders. `import-resolvers/pass-cache.ts` now holds the one
builder, taking a single argument — every difference the three have lives in
the CONSUMER (`tsconfigPaths`, the extension list), not the builder. The
builder is shared, the memo deliberately is not: each adapter keeps its own
`perFileSet`, hence its own index and its own `resolveCache`, because the three
disagree about what a specifier resolves to and one shared cache would hand a
language another language's answers. It buys no runtime reuse and the module
says so — each provider pass builds its own `allFilePaths` Set, so the three
are always different keys.
C and C++'s `augmentedFilePaths` was a two-LEVEL memo, and needed no new
abstraction: the outer memo's value is a function and a function is an object,
so `perFileSet(perFileSet(...))` composes. The two instances stay one per file,
and the reason is now in BOTH doc comments rather than only C++'s — cpp
delegates to `resolveCImportTarget`, whose `suffixIndex` is keyed on the
augmented set, so a shared memo would cross the two languages' indexes.
Two sites are deliberately NOT migrated, each with the reason written at the
declaration so the next sweep does not re-litigate them:
- `configs/swift.ts` is a two-input memo keyed on one. `targets` is not
derivable from the key; re-keying on `ctx` would force a banned non-null
assertion or an unreachable fallback inside a memo builder.
- `rust/qualified-call.ts` `MODULE_SCOPE_CACHE` is three inputs keyed on one,
and sits ten lines below a `perFileSet` in the same file — the likeliest
thing to be "fixed" by mistake.
The other ten remaining `WeakMap`s are different concerns and stay: AST-node
caches, worker-pool runtime state, graph metadata, mutable lazily-filled
accumulators, and the C++ ADL / inline-namespace indexes, which are reassigned
by explicit clear functions and epoch-stamped on read — validity rules beyond
key identity that a closure over a private cache cannot express.
Net −20 lines of code, +22 of the two "why not" notes. The primitive's own doc
is where the cost sits: the Set-identity contract and the two design decisions
are written once instead of being twenty-one implicit facts.
Pure refactor: 1764 unit tests, 42 guard tests, all sixteen contract-test
traversal counts unchanged (c 1, cobol 1, cpp 1, csharp 2, dart 1, go 1,
java 2, javascript 2, kotlin 1, php 1, python 1, ruby 1, rust 0, swift 1,
typescript 2, vue 2), 647 C/C++ tests, and every bench fingerprint unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B
This commit is contained in:
parent
7a1b854a6f
commit
e6f15274e0
23 changed files with 324 additions and 376 deletions
|
|
@ -39,6 +39,19 @@ interface SwiftTargetIndex {
|
|||
* stable reference and the index is built once — not once per import. A
|
||||
* fresh run produces a fresh array → a fresh index, so cross-run staleness
|
||||
* is impossible.
|
||||
*
|
||||
* DELIBERATELY NOT ON `import-resolvers/per-file-set.ts` (#2909 sweep): this is
|
||||
* a TWO-input memo keyed on ONE of them. The index is a function of both
|
||||
* `ctx` (`allFileList` + the index-aligned `normalizedFileList`) and `targets`,
|
||||
* but the key is only `ctx.allFileList`, and `perFileSet`'s `build: (key) => T`
|
||||
* hands the builder nothing but the key. It is sound here only because of an
|
||||
* invariant OUTSIDE the memo — `targets` is `ctx.configs.swiftPackageConfig
|
||||
* .targets`, so it shares `ctx`'s lifetime and cannot vary while
|
||||
* `ctx.allFileList` is fixed — and `perFileSet` has no way to express "and this
|
||||
* other input is pinned by the same lifetime". Re-keying on `ctx` to make
|
||||
* `targets` derivable from the key would change what the cache is keyed on and
|
||||
* force an unreachable null-config arm into the builder, so it is a behaviour
|
||||
* change rather than a consolidation. Leave it hand-rolled.
|
||||
*/
|
||||
const SWIFT_TARGET_INDEX_CACHE = new WeakMap<object, SwiftTargetIndex>();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
* This file contains shared helpers for namespace-based resolution.
|
||||
*/
|
||||
|
||||
import { perFileSet } from './per-file-set.js';
|
||||
import type { SuffixIndex } from './utils.js';
|
||||
import { suffixResolve } from './utils.js';
|
||||
import type { CSharpProjectConfig, CSharpNamespaceEvidence } from '../language-config.js';
|
||||
|
|
@ -77,45 +78,40 @@ interface CsharpNamespaceDirIndex {
|
|||
* array to every import, so this build runs once. A caller that copies the
|
||||
* array per import silently restores the O(imports × files) cost.
|
||||
*/
|
||||
const NAMESPACE_DIR_INDEX_CACHE = new WeakMap<readonly string[], CsharpNamespaceDirIndex>();
|
||||
const getCsharpNamespaceDirIndex = perFileSet(
|
||||
(normalizedFileList: readonly string[]): CsharpNamespaceDirIndex => {
|
||||
const dirsByLastSegment = new Map<string, string[]>();
|
||||
const positionsByDir = new Map<string, number[]>();
|
||||
const singleSegmentDirs: string[] = [];
|
||||
|
||||
function getCsharpNamespaceDirIndex(normalizedFileList: string[]): CsharpNamespaceDirIndex {
|
||||
const cached = NAMESPACE_DIR_INDEX_CACHE.get(normalizedFileList);
|
||||
if (cached !== undefined) return cached;
|
||||
for (let i = 0; i < normalizedFileList.length; i++) {
|
||||
const normalized = normalizedFileList[i];
|
||||
if (!normalized.endsWith('.cs')) continue;
|
||||
const lastSlash = normalized.lastIndexOf('/');
|
||||
// A file with no directory can never match: the needle always ends with
|
||||
// '/', so `indexOf` on a slash-free path is always -1.
|
||||
if (lastSlash < 0) continue;
|
||||
|
||||
const dirsByLastSegment = new Map<string, string[]>();
|
||||
const positionsByDir = new Map<string, number[]>();
|
||||
const singleSegmentDirs: string[] = [];
|
||||
|
||||
for (let i = 0; i < normalizedFileList.length; i++) {
|
||||
const normalized = normalizedFileList[i];
|
||||
if (!normalized.endsWith('.cs')) continue;
|
||||
const lastSlash = normalized.lastIndexOf('/');
|
||||
// A file with no directory can never match: the needle always ends with
|
||||
// '/', so `indexOf` on a slash-free path is always -1.
|
||||
if (lastSlash < 0) continue;
|
||||
|
||||
const dir = normalized.slice(0, lastSlash);
|
||||
let positions = positionsByDir.get(dir);
|
||||
if (positions === undefined) {
|
||||
positions = [];
|
||||
positionsByDir.set(dir, positions);
|
||||
const lastSegment = dir.slice(dir.lastIndexOf('/') + 1);
|
||||
if (lastSegment === dir) singleSegmentDirs.push(dir);
|
||||
let dirs = dirsByLastSegment.get(lastSegment);
|
||||
if (dirs === undefined) {
|
||||
dirs = [];
|
||||
dirsByLastSegment.set(lastSegment, dirs);
|
||||
const dir = normalized.slice(0, lastSlash);
|
||||
let positions = positionsByDir.get(dir);
|
||||
if (positions === undefined) {
|
||||
positions = [];
|
||||
positionsByDir.set(dir, positions);
|
||||
const lastSegment = dir.slice(dir.lastIndexOf('/') + 1);
|
||||
if (lastSegment === dir) singleSegmentDirs.push(dir);
|
||||
let dirs = dirsByLastSegment.get(lastSegment);
|
||||
if (dirs === undefined) {
|
||||
dirs = [];
|
||||
dirsByLastSegment.set(lastSegment, dirs);
|
||||
}
|
||||
dirs.push(dir);
|
||||
}
|
||||
dirs.push(dir);
|
||||
positions.push(i);
|
||||
}
|
||||
positions.push(i);
|
||||
}
|
||||
|
||||
const built: CsharpNamespaceDirIndex = { dirsByLastSegment, positionsByDir, singleSegmentDirs };
|
||||
NAMESPACE_DIR_INDEX_CACHE.set(normalizedFileList, built);
|
||||
return built;
|
||||
}
|
||||
return { dirsByLastSegment, positionsByDir, singleSegmentDirs };
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Every directory that could satisfy `dirPrefix`, as a superset — the exact
|
||||
|
|
|
|||
53
gitnexus/src/core/ingestion/import-resolvers/pass-cache.ts
Normal file
53
gitnexus/src/core/ingestion/import-resolvers/pass-cache.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { buildSuffixIndex, type SuffixIndex } from './utils.js';
|
||||
|
||||
/**
|
||||
* Everything the standard `resolveTsTarget` path derives from one workspace
|
||||
* file set: the file list, the lower-cased file list, the suffix index and the
|
||||
* per-pass `resolveCache`.
|
||||
*
|
||||
* Without this memoization the resolver re-derived `allFileList` and
|
||||
* `normalizedFileList` (both O(N_files)), rebuilt the index and threw away the
|
||||
* `resolveCache` on every import — O(N_files × N_imports) total work for what
|
||||
* should be O(N_files + N_imports).
|
||||
*/
|
||||
export interface ImportPassCache {
|
||||
readonly allFilePaths: Set<string>;
|
||||
readonly allFileList: readonly string[];
|
||||
readonly normalizedFileList: readonly string[];
|
||||
readonly index: SuffixIndex;
|
||||
readonly resolveCache: Map<string, string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build that state. Shared by the adapters whose resolution runs through
|
||||
* `resolveTsTarget` (TypeScript, JavaScript, Vue) — three byte-identical copies
|
||||
* before this existed.
|
||||
*
|
||||
* The BUILDER is shared; the MEMO deliberately is not. Each adapter wraps this
|
||||
* in its own `perFileSet(...)`, so each gets its own `WeakMap`, its own index
|
||||
* instance and — the one that would be a behaviour change — its own
|
||||
* `resolveCache`. The languages disagree about what a specifier resolves to
|
||||
* (`tsconfigPaths` is read from config for TypeScript and Vue, pinned to `null`
|
||||
* for JavaScript, and the tried extension list differs), so one shared resolve
|
||||
* cache across them would hand a language another language's answers.
|
||||
*
|
||||
* Sharing the builder is a code dedup and nothing more: it buys no runtime
|
||||
* reuse, because there is none to buy. Each provider pass builds its own
|
||||
* `allFilePaths` Set (`scope-resolution/pipeline/run.ts`, per provider), so
|
||||
* TypeScript's set and JavaScript's set are different objects and therefore
|
||||
* different `WeakMap` keys even where the two memos are the same code.
|
||||
*/
|
||||
export function buildImportPassCache(allFilePaths: ReadonlySet<string>): ImportPassCache {
|
||||
const allFileList = Array.from(allFilePaths);
|
||||
const normalizedFileList = allFileList.map((f) => f.toLowerCase());
|
||||
return {
|
||||
// Copied ONCE per file set, not once per import: `TsResolveContext` wants a
|
||||
// mutable `Set` and the orchestrator hands us a `ReadonlySet`. The copy is
|
||||
// not the #1918 hazard because the cache KEY is the caller's original Set.
|
||||
allFilePaths: new Set(allFilePaths),
|
||||
allFileList,
|
||||
normalizedFileList,
|
||||
index: buildSuffixIndex(normalizedFileList, allFileList),
|
||||
resolveCache: new Map(),
|
||||
};
|
||||
}
|
||||
52
gitnexus/src/core/ingestion/import-resolvers/per-file-set.ts
Normal file
52
gitnexus/src/core/ingestion/import-resolvers/per-file-set.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* The one memo every per-file-set index in this pipeline is built on.
|
||||
*
|
||||
* The scope-resolution orchestrator builds ONE file-set object per provider
|
||||
* pass and threads that same object through every `resolveImportTarget` call in
|
||||
* the pass, so anything derived from it — a suffix index, a package-directory
|
||||
* map, a basename bucket — can be built once and read by every import instead
|
||||
* of rebuilt per import. Keying on the object's IDENTITY is what makes that
|
||||
* work, and it is equally the contract callers must keep: the set is passed
|
||||
* THROUGH, never copied. A defensive `new Set(allFilePaths)` at an adapter
|
||||
* boundary hands a fresh key per import and silently restores
|
||||
* O(imports × files) — the bug PR #1918 shipped and had to fix in review (P1).
|
||||
* The guards are `test/integration/<lang>-import-index-reuse.test.ts` and, for
|
||||
* every registered language at once,
|
||||
* `test/unit/scope-resolution/import-target-index-reuse.contract.test.ts`.
|
||||
*
|
||||
* A `WeakMap` rather than a `Map`: the entry is reclaimed with the file set it
|
||||
* was derived from, so a pass can never read a previous pass's index and memory
|
||||
* does not grow across runs. There is no invalidation rule to get wrong because
|
||||
* there is nothing to invalidate — a new file set is a new key.
|
||||
*
|
||||
* `K extends object` because the key is whatever object the pass keeps stable:
|
||||
* usually the `ReadonlySet<string>` of paths, sometimes a `readonly string[]`
|
||||
* materialized from it once per run (`import-resolvers/csharp.ts`). Stability
|
||||
* for the pass is the only property either needs.
|
||||
*
|
||||
* `T extends object` is deliberate, chosen over probing `has` before `get`.
|
||||
* `WeakMap.get` returning `undefined` cannot distinguish "not built yet" from
|
||||
* "built, and the value is `undefined`"; constraining the value to an object
|
||||
* makes the second case unrepresentable rather than paying a second lookup on
|
||||
* every import, and it needs no cast to type-check. Every index memoized here
|
||||
* is a record, `Map` or `Set`, so the constraint costs nothing today — and a
|
||||
* later caller wanting to memoize a `string | null` gets a compile error
|
||||
* pointing at this line instead of a memo that silently rebuilds on every miss.
|
||||
*
|
||||
* A `build` that THROWS stores nothing, so the next call for that key runs it
|
||||
* again: failures are not memoized, and a half-filled index is never published.
|
||||
* Inert for the builders here — each is a pure, total pass over the file set —
|
||||
* and the safer of the two behaviours if that ever stops being true.
|
||||
*/
|
||||
export function perFileSet<K extends object, T extends object>(
|
||||
build: (key: K) => T,
|
||||
): (key: K) => T {
|
||||
const cache = new WeakMap<K, T>();
|
||||
return (key) => {
|
||||
const cached = cache.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
const built = build(key);
|
||||
cache.set(key, built);
|
||||
return built;
|
||||
};
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@
|
|||
* INSIDE a resolver, by counting how many times the Set is iterated.
|
||||
*/
|
||||
|
||||
import { perFileSet } from './per-file-set.js';
|
||||
import { buildSuffixIndex, type SuffixIndex } from './utils.js';
|
||||
|
||||
export interface WorkspaceFileIndex {
|
||||
|
|
@ -52,27 +53,22 @@ export interface WorkspaceFileIndex {
|
|||
readonly normToRaw: Map<string, string>;
|
||||
}
|
||||
|
||||
const WORKSPACE_FILE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, WorkspaceFileIndex>();
|
||||
export const getWorkspaceFileIndex = perFileSet(
|
||||
(allFilePaths: ReadonlySet<string>): WorkspaceFileIndex => {
|
||||
const all = [...allFilePaths];
|
||||
const normalized = all.map((f) => f.replace(/\\/g, '/'));
|
||||
const normToRaw = new Map<string, string>();
|
||||
for (let i = 0; i < normalized.length; i++) {
|
||||
// First wins, mirroring the `for (const raw of allFilePaths)` scans this
|
||||
// replaces: they returned on the first match in iteration order.
|
||||
if (!normToRaw.has(normalized[i])) normToRaw.set(normalized[i], all[i]);
|
||||
}
|
||||
|
||||
export function getWorkspaceFileIndex(allFilePaths: ReadonlySet<string>): WorkspaceFileIndex {
|
||||
const cached = WORKSPACE_FILE_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const all = [...allFilePaths];
|
||||
const normalized = all.map((f) => f.replace(/\\/g, '/'));
|
||||
const normToRaw = new Map<string, string>();
|
||||
for (let i = 0; i < normalized.length; i++) {
|
||||
// First wins, mirroring the `for (const raw of allFilePaths)` scans this
|
||||
// replaces: they returned on the first match in iteration order.
|
||||
if (!normToRaw.has(normalized[i])) normToRaw.set(normalized[i], all[i]);
|
||||
}
|
||||
|
||||
const built: WorkspaceFileIndex = {
|
||||
normalized,
|
||||
all,
|
||||
index: buildSuffixIndex(normalized, all),
|
||||
normToRaw,
|
||||
};
|
||||
WORKSPACE_FILE_INDEX_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
return {
|
||||
normalized,
|
||||
all,
|
||||
index: buildSuffixIndex(normalized, all),
|
||||
normToRaw,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { dirname, join } from 'path';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
/**
|
||||
* A workspace file path pre-decomposed for the suffix-match fallback:
|
||||
|
|
@ -28,26 +29,20 @@ interface CSuffixCandidate {
|
|||
* `WeakMap`-keyed so it is reclaimed with the pass (no cross-pass staleness).
|
||||
* Shared by C and C++ (`resolveCppImportTarget` delegates here).
|
||||
*/
|
||||
const suffixIndexByPaths = new WeakMap<ReadonlySet<string>, Map<string, CSuffixCandidate[]>>();
|
||||
|
||||
function suffixIndex(allFilePaths: ReadonlySet<string>): Map<string, CSuffixCandidate[]> {
|
||||
let index = suffixIndexByPaths.get(allFilePaths);
|
||||
if (index === undefined) {
|
||||
index = new Map<string, CSuffixCandidate[]>();
|
||||
for (const original of allFilePaths) {
|
||||
const normalized = original.replace(/\\/g, '/');
|
||||
const basename = normalized.slice(normalized.lastIndexOf('/') + 1);
|
||||
let bucket = index.get(basename);
|
||||
if (bucket === undefined) {
|
||||
bucket = [];
|
||||
index.set(basename, bucket);
|
||||
}
|
||||
bucket.push({ original, normalized, depth: normalized.split('/').length });
|
||||
const suffixIndex = perFileSet((allFilePaths: ReadonlySet<string>) => {
|
||||
const index = new Map<string, CSuffixCandidate[]>();
|
||||
for (const original of allFilePaths) {
|
||||
const normalized = original.replace(/\\/g, '/');
|
||||
const basename = normalized.slice(normalized.lastIndexOf('/') + 1);
|
||||
let bucket = index.get(basename);
|
||||
if (bucket === undefined) {
|
||||
bucket = [];
|
||||
index.set(basename, bucket);
|
||||
}
|
||||
suffixIndexByPaths.set(allFilePaths, index);
|
||||
bucket.push({ original, normalized, depth: normalized.split('/').length });
|
||||
}
|
||||
return index;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolve a C #include path to a file in the workspace.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { cArityCompatibility, cMergeBindings, resolveCImportTarget } from './ind
|
|||
import { scanHeaderFiles } from './header-scan.js';
|
||||
import { expandCWildcardNames, isStaticName, clearStaticNames } from './static-linkage.js';
|
||||
import { applyCStaticLinkageSideChannel } from './capture-side-channel.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
/**
|
||||
* Per-pass memo of the augmented `#include`-resolution file set
|
||||
|
|
@ -19,31 +20,26 @@ import { applyCStaticLinkageSideChannel } from './capture-side-channel.js';
|
|||
* handing it a new set identity each time. Both `allFilePaths` (built once in
|
||||
* scope-resolution `run.ts`) and the header set (`loadResolutionConfig`
|
||||
* result) are stable per pass, so the union is built once and reused.
|
||||
* `WeakMap`-keyed → reclaimed with the pass (no cross-pass staleness).
|
||||
* Reclaimed with the pass (no cross-pass staleness).
|
||||
*
|
||||
* Two inputs, so two levels of `perFileSet` composed rather than a second
|
||||
* primitive: the outer memo's value is the inner memo, and a function is an
|
||||
* object, which is all `T extends object` asks for.
|
||||
*
|
||||
* The MEMO stays private to this file even though the C++ resolver's twin is
|
||||
* byte-identical. The augmented set's IDENTITY is load-bearing downstream —
|
||||
* C++ delegates to `resolveCImportTarget`, whose `suffixIndex` memo is keyed on
|
||||
* exactly this set — so one memo shared across the two languages would hand
|
||||
* each the other's index. Same builder-shared/memo-separate rule as
|
||||
* `import-resolvers/pass-cache.ts`.
|
||||
*/
|
||||
const augmentedPathsByPass = new WeakMap<
|
||||
ReadonlySet<string>,
|
||||
WeakMap<ReadonlySet<string>, ReadonlySet<string>>
|
||||
>();
|
||||
|
||||
function augmentedFilePaths(
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
headerPaths: ReadonlySet<string>,
|
||||
): ReadonlySet<string> {
|
||||
let byHeaders = augmentedPathsByPass.get(allFilePaths);
|
||||
if (byHeaders === undefined) {
|
||||
byHeaders = new WeakMap();
|
||||
augmentedPathsByPass.set(allFilePaths, byHeaders);
|
||||
}
|
||||
let augmented = byHeaders.get(headerPaths);
|
||||
if (augmented === undefined) {
|
||||
const augmentedFilePathsFor = perFileSet((allFilePaths: ReadonlySet<string>) =>
|
||||
perFileSet((headerPaths: ReadonlySet<string>): ReadonlySet<string> => {
|
||||
const set = new Set(allFilePaths);
|
||||
for (const h of headerPaths) set.add(h);
|
||||
augmented = set;
|
||||
byHeaders.set(headerPaths, augmented);
|
||||
}
|
||||
return augmented;
|
||||
}
|
||||
return set;
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* C `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
||||
|
|
@ -94,7 +90,7 @@ export const cScopeResolver: ScopeResolver = {
|
|||
return resolveCImportTarget(
|
||||
targetRaw,
|
||||
fromFile,
|
||||
augmentedFilePaths(allFilePaths, headerPaths),
|
||||
augmentedFilePathsFor(allFilePaths)(headerPaths),
|
||||
);
|
||||
}
|
||||
return resolveCImportTarget(targetRaw, fromFile, allFilePaths);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
/**
|
||||
* Per-file set of function names declared with `static` storage class.
|
||||
|
|
@ -59,27 +60,23 @@ export function clearStaticNames(): void {
|
|||
* thousands of resolved includes) that is ~10^10+ comparisons on a single
|
||||
* thread — the dominant term in the scope-resolution finalize grind.
|
||||
*
|
||||
* Building the lookup once collapses it to O(R_include + F). `WeakMap`-keyed
|
||||
* on the array so the index is reclaimed with the pass — no cross-pass
|
||||
* Building the lookup once collapses it to O(R_include + F). `perFileSet` keys
|
||||
* on the array identity so the index is reclaimed with the pass — no cross-pass
|
||||
* staleness (mirrors the {@link clearStaticNames} discipline for server-mode
|
||||
* / multi-repo reuse), and a fresh array transparently rebuilds.
|
||||
*/
|
||||
const moduleScopeIndexByPass = new WeakMap<readonly ParsedFile[], Map<ScopeId, ParsedFile>>();
|
||||
|
||||
function moduleScopeIndex(parsedFiles: readonly ParsedFile[]): Map<ScopeId, ParsedFile> {
|
||||
let index = moduleScopeIndexByPass.get(parsedFiles);
|
||||
if (index === undefined) {
|
||||
index = new Map<ScopeId, ParsedFile>();
|
||||
const moduleScopeIndex = perFileSet(
|
||||
(parsedFiles: readonly ParsedFile[]): Map<ScopeId, ParsedFile> => {
|
||||
const index = new Map<ScopeId, ParsedFile>();
|
||||
// First-wins to preserve `Array.find` semantics (returns the first match).
|
||||
// `moduleScope` is unique per file in practice, so collisions are absent;
|
||||
// the guard only formalises identical behaviour to the prior `.find`.
|
||||
for (const p of parsedFiles) {
|
||||
if (!index.has(p.moduleScope)) index.set(p.moduleScope, p);
|
||||
}
|
||||
moduleScopeIndexByPass.set(parsedFiles, index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Return the names visible through a C wildcard import (`#include`).
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import path from 'node:path';
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { cobolProvider } from '../cobol.js';
|
||||
|
|
@ -56,12 +57,7 @@ interface CobolCopyIndex {
|
|||
readonly sources: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
const COBOL_COPY_INDEX_CACHE = new WeakMap<ReadonlySet<string>, CobolCopyIndex>();
|
||||
|
||||
function getCobolCopyIndex(allFilePaths: ReadonlySet<string>): CobolCopyIndex {
|
||||
const cached = COBOL_COPY_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const getCobolCopyIndex = perFileSet((allFilePaths: ReadonlySet<string>): CobolCopyIndex => {
|
||||
const copybooks = new Map<string, string>();
|
||||
const sources = new Map<string, string>();
|
||||
// One pass builds both tiers: the two scans walked the same files and
|
||||
|
|
@ -79,10 +75,8 @@ function getCobolCopyIndex(allFilePaths: ReadonlySet<string>): CobolCopyIndex {
|
|||
if (!tier.has(basename)) tier.set(basename, fp);
|
||||
}
|
||||
|
||||
const built: CobolCopyIndex = { copybooks, sources };
|
||||
COBOL_COPY_INDEX_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
return { copybooks, sources };
|
||||
});
|
||||
|
||||
const cobolScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Cobol,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ParsedFile, Scope, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { isCppInlineNamespaceScope } from './inline-namespaces.js';
|
||||
|
||||
/**
|
||||
|
|
@ -283,24 +284,20 @@ export function isCppDefGloballyVisible(filePath: string, nodeId: string): boole
|
|||
* `parsedFiles` reference; the old `parsedFiles.find(...)` was therefore O(F)
|
||||
* per edge → O(R·F) overall (at kernel scale the ~25–30k `.h` headers are
|
||||
* classified C++, so this fires hard — the C twin in `c/static-linkage.ts`).
|
||||
* Building the lookup once collapses it to O(R+F). `WeakMap`-keyed so it is
|
||||
* reclaimed with the pass (no cross-pass staleness; mirrors
|
||||
* {@link clearFileLocalNames}).
|
||||
* Building the lookup once collapses it to O(R+F). `perFileSet` keys on the
|
||||
* array identity so it is reclaimed with the pass (no cross-pass staleness;
|
||||
* mirrors {@link clearFileLocalNames}).
|
||||
*/
|
||||
const moduleScopeIndexByPass = new WeakMap<readonly ParsedFile[], Map<ScopeId, ParsedFile>>();
|
||||
|
||||
function moduleScopeIndex(parsedFiles: readonly ParsedFile[]): Map<ScopeId, ParsedFile> {
|
||||
let index = moduleScopeIndexByPass.get(parsedFiles);
|
||||
if (index === undefined) {
|
||||
index = new Map<ScopeId, ParsedFile>();
|
||||
const moduleScopeIndex = perFileSet(
|
||||
(parsedFiles: readonly ParsedFile[]): Map<ScopeId, ParsedFile> => {
|
||||
const index = new Map<ScopeId, ParsedFile>();
|
||||
// First-wins to preserve `Array.find` semantics (returns the first match).
|
||||
for (const p of parsedFiles) {
|
||||
if (!index.has(p.moduleScope)) index.set(p.moduleScope, p);
|
||||
}
|
||||
moduleScopeIndexByPass.set(parsedFiles, index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
);
|
||||
|
||||
export function expandCppWildcardNames(
|
||||
targetModuleScope: ScopeId,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
resolveCppReceiverMember,
|
||||
} from './member-lookup.js';
|
||||
import { stripCppSpecifiers } from './interpret.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
/** A pointee worth binding: a bare identifier, not `T**`, `T[]`, `A::B` or a
|
||||
* template spelling. Hoisted — a literal here would mint a fresh RegExp on
|
||||
|
|
@ -61,32 +62,25 @@ const CPP_SIMPLE_POINTEE_RE = /^[A-Za-z_]\w*$/;
|
|||
* a fresh ~F-entry `Set` on every call AND defeated the shared
|
||||
* `resolveCImportTarget` suffix-index memo (in `c/import-target.ts`) by handing
|
||||
* it a new set identity each time. Both inputs are stable per pass, so the
|
||||
* union is built once and reused. `WeakMap`-keyed → reclaimed with the pass.
|
||||
* (Twin of the C resolver's `augmentedFilePaths`.)
|
||||
* union is built once and reused. Reclaimed with the pass.
|
||||
*
|
||||
* Two inputs, so two levels of `perFileSet` composed rather than a second
|
||||
* primitive: the outer memo's value is the inner memo, and a function is an
|
||||
* object, which is all `T extends object` asks for.
|
||||
*
|
||||
* (Twin of the C resolver's `augmentedFilePathsFor`.) The two memos stay
|
||||
* SEPARATE deliberately. C++ delegates to `resolveCImportTarget`, whose
|
||||
* `suffixIndex` memo is keyed on the augmented set, so a single memo shared
|
||||
* with C would hand each language the other's index — same
|
||||
* builder-shared/memo-separate rule as `import-resolvers/pass-cache.ts`.
|
||||
*/
|
||||
const augmentedPathsByPass = new WeakMap<
|
||||
ReadonlySet<string>,
|
||||
WeakMap<ReadonlySet<string>, ReadonlySet<string>>
|
||||
>();
|
||||
|
||||
function augmentedFilePaths(
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
headerPaths: ReadonlySet<string>,
|
||||
): ReadonlySet<string> {
|
||||
let byHeaders = augmentedPathsByPass.get(allFilePaths);
|
||||
if (byHeaders === undefined) {
|
||||
byHeaders = new WeakMap();
|
||||
augmentedPathsByPass.set(allFilePaths, byHeaders);
|
||||
}
|
||||
let augmented = byHeaders.get(headerPaths);
|
||||
if (augmented === undefined) {
|
||||
const augmentedFilePathsFor = perFileSet((allFilePaths: ReadonlySet<string>) =>
|
||||
perFileSet((headerPaths: ReadonlySet<string>): ReadonlySet<string> => {
|
||||
const set = new Set(allFilePaths);
|
||||
for (const h of headerPaths) set.add(h);
|
||||
augmented = set;
|
||||
byHeaders.set(headerPaths, augmented);
|
||||
}
|
||||
return augmented;
|
||||
}
|
||||
return set;
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
||||
|
|
@ -128,7 +122,7 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
return resolveCppImportTarget(
|
||||
targetRaw,
|
||||
fromFile,
|
||||
augmentedFilePaths(allFilePaths, headerPaths),
|
||||
augmentedFilePathsFor(allFilePaths)(headerPaths),
|
||||
);
|
||||
}
|
||||
return resolveCppImportTarget(targetRaw, fromFile, allFilePaths);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
firstFileDirectlyInPkgDir,
|
||||
type PackageDirIndex,
|
||||
} from '../../import-resolvers/package-dir-index.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { csharpSuffixFallbackAllowed } from '../../csharp-namespace-gate.js';
|
||||
|
||||
export interface CsharpResolveContext {
|
||||
|
|
@ -46,15 +47,10 @@ export interface CsharpResolveContext {
|
|||
* `import-resolvers/package-dir-index.ts`), which the no-csproj path calls once
|
||||
* for the direct match and then up to once per stripped namespace prefix.
|
||||
*/
|
||||
const csharpDirIndexCache = new WeakMap<ReadonlySet<string>, PackageDirIndex>();
|
||||
|
||||
function getCsharpDirIndex(allFilePaths: ReadonlySet<string>): PackageDirIndex {
|
||||
const cached = csharpDirIndexCache.get(allFilePaths);
|
||||
if (cached) return cached;
|
||||
const built = buildPackageDirIndex(allFilePaths, (normalized) => normalized.endsWith('.cs'));
|
||||
csharpDirIndexCache.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
const getCsharpDirIndex = perFileSet(
|
||||
(allFilePaths: ReadonlySet<string>): PackageDirIndex =>
|
||||
buildPackageDirIndex(allFilePaths, (normalized) => normalized.endsWith('.cs')),
|
||||
);
|
||||
|
||||
export function resolveCsharpImportTarget(
|
||||
parsedImport: ParsedImport,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
* `targetRaw` arrives already quote-stripped from `interpretDartImport`.
|
||||
*/
|
||||
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { DART_HERITAGE_PREFIX } from './interpret.js';
|
||||
|
||||
/**
|
||||
|
|
@ -35,11 +36,7 @@ interface DartFileIndex {
|
|||
readonly byBasename: Map<string, string[]>;
|
||||
}
|
||||
|
||||
const DART_FILE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, DartFileIndex>();
|
||||
|
||||
function getDartFileIndex(allFilePaths: ReadonlySet<string>): DartFileIndex {
|
||||
const cached = DART_FILE_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
const getDartFileIndex = perFileSet((allFilePaths: ReadonlySet<string>): DartFileIndex => {
|
||||
const byBasename = new Map<string, string[]>();
|
||||
for (const fp of allFilePaths) {
|
||||
const base = fp.slice(fp.lastIndexOf('/') + 1);
|
||||
|
|
@ -50,10 +47,8 @@ function getDartFileIndex(allFilePaths: ReadonlySet<string>): DartFileIndex {
|
|||
}
|
||||
bucket.push(fp);
|
||||
}
|
||||
const built: DartFileIndex = { byBasename };
|
||||
DART_FILE_INDEX_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
return { byBasename };
|
||||
});
|
||||
|
||||
/** First file (in Set-iteration order) that IS `candidate` or ends with
|
||||
* `/<candidate>` — the exact predicate of the scans this replaces. */
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
sortedRootFiles,
|
||||
type PackageDirIndex,
|
||||
} from '../../import-resolvers/package-dir-index.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
/**
|
||||
* Resolve a Go import path to ALL .go files in the matching package directory.
|
||||
|
|
@ -56,6 +57,11 @@ export function resolveGoImportTarget(
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Go packages exclude `_test.go` files: they are a separate package. */
|
||||
function isGoPackageFile(normalized: string): boolean {
|
||||
return normalized.endsWith('.go') && !normalized.endsWith('_test.go');
|
||||
}
|
||||
|
||||
/**
|
||||
* Package index over the file set, memoized on the Set's identity (#2877).
|
||||
*
|
||||
|
|
@ -69,20 +75,10 @@ export function resolveGoImportTarget(
|
|||
* is built once per run. `resolveGoImportTarget` must therefore never copy the
|
||||
* Set before this point — see `import-resolvers/workspace-file-index.ts`.
|
||||
*/
|
||||
const GO_PACKAGE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, PackageDirIndex>();
|
||||
|
||||
/** Go packages exclude `_test.go` files: they are a separate package. */
|
||||
function isGoPackageFile(normalized: string): boolean {
|
||||
return normalized.endsWith('.go') && !normalized.endsWith('_test.go');
|
||||
}
|
||||
|
||||
function getGoPackageIndex(allFilePaths: ReadonlySet<string>): PackageDirIndex {
|
||||
const cached = GO_PACKAGE_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
const built = buildPackageDirIndex(allFilePaths, isGoPackageFile);
|
||||
GO_PACKAGE_INDEX_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
const getGoPackageIndex = perFileSet(
|
||||
(allFilePaths: ReadonlySet<string>): PackageDirIndex =>
|
||||
buildPackageDirIndex(allFilePaths, isGoPackageFile),
|
||||
);
|
||||
|
||||
function findRootPackageFiles(allFilePaths: ReadonlySet<string>): string[] {
|
||||
return sortedRootFiles(getGoPackageIndex(allFilePaths));
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import {
|
|||
firstFileDirectlyInPkgDir,
|
||||
type PackageDirIndex,
|
||||
} from '../../import-resolvers/package-dir-index.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
export interface JavaResolveContext {
|
||||
readonly fromFile: string;
|
||||
|
|
@ -69,15 +70,10 @@ export interface JavaResolveContext {
|
|||
* identity. Feeds `firstFileDirectlyInPkgDir`, which is called once for the
|
||||
* direct match and then up to once per stripped package prefix.
|
||||
*/
|
||||
const javaDirIndexCache = new WeakMap<ReadonlySet<string>, PackageDirIndex>();
|
||||
|
||||
function getJavaDirIndex(allFilePaths: ReadonlySet<string>): PackageDirIndex {
|
||||
const cached = javaDirIndexCache.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
const built = buildPackageDirIndex(allFilePaths, (normalized) => normalized.endsWith('.java'));
|
||||
javaDirIndexCache.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
const getJavaDirIndex = perFileSet(
|
||||
(allFilePaths: ReadonlySet<string>): PackageDirIndex =>
|
||||
buildPackageDirIndex(allFilePaths, (normalized) => normalized.endsWith('.java')),
|
||||
);
|
||||
|
||||
export function resolveJavaImportTarget(
|
||||
parsedImport: ParsedImport,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@
|
|||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { resolveTsTarget, type TsResolveContext } from '../typescript/import-target.js';
|
||||
import { buildSuffixIndex, type SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import { buildImportPassCache } from '../../import-resolvers/pass-cache.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
export type JsResolveContext = TsResolveContext;
|
||||
|
||||
|
|
@ -69,16 +70,7 @@ export type JsResolveContext = TsResolveContext;
|
|||
* traversals of the SET, and this scan walks the materialized array behind it.
|
||||
* See `test/integration/javascript-import-index-reuse.test.ts` for the guard
|
||||
* that can.
|
||||
*/
|
||||
interface PassCache {
|
||||
readonly allFilePaths: Set<string>;
|
||||
readonly allFileList: readonly string[];
|
||||
readonly normalizedFileList: readonly string[];
|
||||
readonly index: SuffixIndex;
|
||||
readonly resolveCache: Map<string, string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Memoized on the `allFilePaths` Set identity, like every other language's
|
||||
* import index (`import-resolvers/workspace-file-index.ts` and friends).
|
||||
*
|
||||
|
|
@ -94,27 +86,7 @@ interface PassCache {
|
|||
* `new Set(allFilePaths)` at the adapter boundary hands a fresh key per import
|
||||
* and restores the per-import rebuild (PR #1918 review P1).
|
||||
*/
|
||||
const PASS_CACHE = new WeakMap<ReadonlySet<string>, PassCache>();
|
||||
|
||||
function passCacheFor(allFilePaths: ReadonlySet<string>): PassCache {
|
||||
const cached = PASS_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const allFileList = Array.from(allFilePaths);
|
||||
const normalizedFileList = allFileList.map((f) => f.toLowerCase());
|
||||
const built: PassCache = {
|
||||
// Copied ONCE per file set, not once per import: `TsResolveContext` wants a
|
||||
// mutable `Set` and the orchestrator hands us a `ReadonlySet`. The copy is
|
||||
// not the #1918 hazard because the cache KEY is the caller's original Set.
|
||||
allFilePaths: new Set(allFilePaths),
|
||||
allFileList,
|
||||
normalizedFileList,
|
||||
index: buildSuffixIndex(normalizedFileList, allFileList),
|
||||
resolveCache: new Map(),
|
||||
};
|
||||
PASS_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
const passCacheFor = perFileSet(buildImportPassCache);
|
||||
|
||||
/**
|
||||
* Build a memoized `resolveImportTarget` adapter for JavaScript.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
|
||||
import { KOTLIN_EXTENSIONS } from '../../import-resolvers/jvm.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
export interface KotlinResolveContext {
|
||||
readonly fromFile: string;
|
||||
|
|
@ -179,13 +180,9 @@ interface KotlinFileIndex {
|
|||
readonly dirChildren: Map<string, readonly string[]>;
|
||||
}
|
||||
|
||||
const KOTLIN_FILE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, KotlinFileIndex>();
|
||||
|
||||
function getKotlinFileIndex(allFilePaths: ReadonlySet<string>): KotlinFileIndex {
|
||||
const cached = KOTLIN_FILE_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
// Cache miss: materialize a fresh index. That it happens once per run and not
|
||||
// once per import is asserted by counting traversals of the Set itself, in
|
||||
const getKotlinFileIndex = perFileSet((allFilePaths: ReadonlySet<string>): KotlinFileIndex => {
|
||||
// Runs on a cache miss only. That it happens once per run and not once per
|
||||
// import is asserted by counting traversals of the Set itself, in
|
||||
// `test/integration/kotlin-import-index-reuse.test.ts` (#2909).
|
||||
|
||||
const exactByStem = new Map<string, string>();
|
||||
|
|
@ -254,10 +251,8 @@ function getKotlinFileIndex(allFilePaths: ReadonlySet<string>): KotlinFileIndex
|
|||
// future mutation is a loud TypeError instead of a silent edge move.
|
||||
for (const bucket of dirChildren.values()) Object.freeze(bucket);
|
||||
|
||||
const index: KotlinFileIndex = { exactByStem, suffixByStem, dirChildren };
|
||||
KOTLIN_FILE_INDEX_CACHE.set(allFilePaths, index);
|
||||
return index;
|
||||
}
|
||||
return { exactByStem, suffixByStem, dirChildren };
|
||||
});
|
||||
|
||||
function addChild(dirChildren: Map<string, string[]>, dir: string, raw: string): void {
|
||||
const bucket = dirChildren.get(dir);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type { ParsedFile, ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
|
|||
import type { ImportResolutionContext } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { resolvePhpImportInternal } from '../../import-resolvers/php.js';
|
||||
import type { SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { getWorkspaceFileIndex } from '../../import-resolvers/workspace-file-index.js';
|
||||
import type { ComposerConfig } from '../../language-config.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
|
@ -74,12 +75,6 @@ function namespaceDirectories(
|
|||
return [...directories];
|
||||
}
|
||||
|
||||
// A scope-resolution pass shares one stable parsedFiles array across imports.
|
||||
const phpDirectoryIndexCache = new WeakMap<
|
||||
readonly ParsedFile[],
|
||||
ReadonlyMap<string, readonly ParsedFile[]>
|
||||
>();
|
||||
|
||||
function parentDirectory(filePath: string): string {
|
||||
const normalizedPath = normalizePhpPath(filePath);
|
||||
const separator = normalizedPath.lastIndexOf('/');
|
||||
|
|
@ -100,23 +95,25 @@ function directoryAliases(filePath: string): string[] {
|
|||
return [...aliases];
|
||||
}
|
||||
|
||||
function filesByDirectory(
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): ReadonlyMap<string, readonly ParsedFile[]> {
|
||||
const cached = phpDirectoryIndexCache.get(parsedFiles);
|
||||
if (cached) return cached;
|
||||
|
||||
const mutable = new Map<string, ParsedFile[]>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const directory of directoryAliases(parsed.filePath)) {
|
||||
const files = mutable.get(directory) ?? [];
|
||||
files.push(parsed);
|
||||
mutable.set(directory, files);
|
||||
/**
|
||||
* Directory alias → the files under it, built once per pass.
|
||||
*
|
||||
* A scope-resolution pass shares one stable `parsedFiles` array across imports,
|
||||
* so the array identity is the memo key — see `perFileSet`.
|
||||
*/
|
||||
const filesByDirectory = perFileSet(
|
||||
(parsedFiles: readonly ParsedFile[]): ReadonlyMap<string, readonly ParsedFile[]> => {
|
||||
const mutable = new Map<string, ParsedFile[]>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const directory of directoryAliases(parsed.filePath)) {
|
||||
const files = mutable.get(directory) ?? [];
|
||||
files.push(parsed);
|
||||
mutable.set(directory, files);
|
||||
}
|
||||
}
|
||||
}
|
||||
phpDirectoryIndexCache.set(parsedFiles, mutable);
|
||||
return mutable;
|
||||
}
|
||||
return mutable;
|
||||
},
|
||||
);
|
||||
|
||||
// ─── workspace index (#2901) ───────────────────────────────────────────────
|
||||
|
||||
|
|
@ -185,9 +182,8 @@ interface PhpWorkspaceIndex {
|
|||
readonly suffixIndex: SuffixIndex;
|
||||
}
|
||||
|
||||
const PHP_WORKSPACE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, PhpWorkspaceIndex>();
|
||||
|
||||
function buildPhpWorkspaceIndex(allFilePaths: ReadonlySet<string>): PhpWorkspaceIndex {
|
||||
/** Memoized on the `allFilePaths` Set identity, like `getWorkspaceFileIndex`. */
|
||||
const getPhpWorkspaceIndex = perFileSet((allFilePaths: ReadonlySet<string>): PhpWorkspaceIndex => {
|
||||
// The Set is passed THROUGH to the shared cache, never copied — a defensive
|
||||
// `new Set(...)` here or in `scope-resolver.ts` would hand both WeakMaps a
|
||||
// fresh key per import and silently restore O(imports × files) (#1918 P1).
|
||||
|
|
@ -246,16 +242,7 @@ function buildPhpWorkspaceIndex(allFilePaths: ReadonlySet<string>): PhpWorkspace
|
|||
};
|
||||
|
||||
return { normalized, all, suffixIndex };
|
||||
}
|
||||
|
||||
/** Memoized on the `allFilePaths` Set identity, like `getWorkspaceFileIndex`. */
|
||||
function getPhpWorkspaceIndex(allFilePaths: ReadonlySet<string>): PhpWorkspaceIndex {
|
||||
const cached = PHP_WORKSPACE_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
const built = buildPhpWorkspaceIndex(allFilePaths);
|
||||
PHP_WORKSPACE_INDEX_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── loadResolutionConfig ──────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
*/
|
||||
|
||||
import type { ParsedFile, ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { resolvePythonImportInternal } from '../../import-resolvers/python.js';
|
||||
|
||||
export interface PythonResolveContext {
|
||||
|
|
@ -347,13 +348,9 @@ interface PythonFileIndex {
|
|||
readonly dirPrefixes: Set<string>;
|
||||
}
|
||||
|
||||
const PYTHON_FILE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, PythonFileIndex>();
|
||||
|
||||
function getPythonFileIndex(allFilePaths: ReadonlySet<string>): PythonFileIndex {
|
||||
const cached = PYTHON_FILE_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
// Cache miss: materialize a fresh index. That it happens once per run and not
|
||||
// once per import is asserted by counting traversals of the Set itself, in
|
||||
const getPythonFileIndex = perFileSet((allFilePaths: ReadonlySet<string>): PythonFileIndex => {
|
||||
// Runs on a cache miss only. That it happens once per run and not once per
|
||||
// import is asserted by counting traversals of the Set itself, in
|
||||
// `test/integration/python-import-index-reuse.test.ts` — the PR #1918 review
|
||||
// P1 guard (#2909).
|
||||
|
||||
|
|
@ -413,10 +410,8 @@ function getPythonFileIndex(allFilePaths: ReadonlySet<string>): PythonFileIndex
|
|||
}
|
||||
}
|
||||
|
||||
const index: PythonFileIndex = { normSet, byBasename, byInitParent, dirPrefixes };
|
||||
PYTHON_FILE_INDEX_CACHE.set(allFilePaths, index);
|
||||
return index;
|
||||
}
|
||||
return { normSet, byBasename, byInitParent, dirPrefixes };
|
||||
});
|
||||
|
||||
function pythonImportedSubmoduleTarget(parsedImport: ParsedImport): string | null {
|
||||
if (parsedImport.kind !== 'named' && parsedImport.kind !== 'alias') return null;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
*/
|
||||
|
||||
import type { ParsedFile, Scope, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { isOverloadableCallable } from '../../utils/callable-labels.js';
|
||||
import { lookupBindingsAt } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
|
|
@ -53,16 +54,9 @@ import {
|
|||
* The hook is invoked per call site; rebuilding the index each time would make
|
||||
* qualified-call resolution O(sites x files).
|
||||
*/
|
||||
const MODULE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, RustModuleIndex>();
|
||||
|
||||
function moduleIndexFor(allFilePaths: ReadonlySet<string>): RustModuleIndex {
|
||||
let index = MODULE_INDEX_CACHE.get(allFilePaths);
|
||||
if (index === undefined) {
|
||||
index = buildRustModuleIndex(allFilePaths);
|
||||
MODULE_INDEX_CACHE.set(allFilePaths, index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
const moduleIndexFor = perFileSet(
|
||||
(allFilePaths: ReadonlySet<string>): RustModuleIndex => buildRustModuleIndex(allFilePaths),
|
||||
);
|
||||
|
||||
export function resolveRustQualifiedFreeCall(
|
||||
site: { readonly name: string; readonly rawQualifiedName?: string; readonly inScope: ScopeId },
|
||||
|
|
@ -488,6 +482,15 @@ interface PassModuleIndex {
|
|||
readonly inlineModuleKeys: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* DELIBERATELY NOT ON `import-resolvers/per-file-set.ts` (#2909 sweep), unlike
|
||||
* {@link moduleIndexFor} above. {@link passIndexFor} takes THREE inputs —
|
||||
* `workspaceIndex`, `index` and `scopes` — and keys on the first alone; the
|
||||
* builder reads `scopes.defs.byId` and `index`, neither of which is derivable
|
||||
* from the key, and `perFileSet`'s `build: (key) => T` hands the builder
|
||||
* nothing but the key. Sound here only because all three share the resolution
|
||||
* pass's lifetime, which is an invariant the primitive cannot express.
|
||||
*/
|
||||
const MODULE_SCOPE_CACHE = new WeakMap<WorkspaceResolutionIndex, PassModuleIndex>();
|
||||
|
||||
function moduleKey(module: RustModule): string {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
*/
|
||||
|
||||
import type { ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
|
||||
export interface SwiftResolveContext {
|
||||
readonly fromFile: string;
|
||||
|
|
@ -39,12 +40,7 @@ interface SwiftModuleIndex {
|
|||
readonly byModule: Map<string, string[]>;
|
||||
}
|
||||
|
||||
const SWIFT_MODULE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, SwiftModuleIndex>();
|
||||
|
||||
function getSwiftModuleIndex(allFilePaths: ReadonlySet<string>): SwiftModuleIndex {
|
||||
const cached = SWIFT_MODULE_INDEX_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const getSwiftModuleIndex = perFileSet((allFilePaths: ReadonlySet<string>): SwiftModuleIndex => {
|
||||
const byModule = new Map<string, string[]>();
|
||||
for (const raw of allFilePaths) {
|
||||
const norm = raw.replace(/\\/g, '/');
|
||||
|
|
@ -66,10 +62,8 @@ function getSwiftModuleIndex(allFilePaths: ReadonlySet<string>): SwiftModuleInde
|
|||
}
|
||||
}
|
||||
|
||||
const index: SwiftModuleIndex = { byModule };
|
||||
SWIFT_MODULE_INDEX_CACHE.set(allFilePaths, index);
|
||||
return index;
|
||||
}
|
||||
return { byModule };
|
||||
});
|
||||
|
||||
export function resolveSwiftImportTarget(
|
||||
parsedImport: ParsedImport,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ import { simpleKey } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
|||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { typescriptProvider } from '../typescript.js';
|
||||
import { loadTsconfigPaths, type TsconfigPaths } from '../../language-config.js';
|
||||
import { buildSuffixIndex, type SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import { buildImportPassCache } from '../../import-resolvers/pass-cache.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { indexOnlyElementType } from '../../type-extractors/shared.js';
|
||||
import {
|
||||
typescriptArityCompatibility,
|
||||
|
|
@ -54,24 +55,6 @@ const TYPESCRIPT_TYPE_ONLY_BINDING_TYPES = new Set<NodeLabel>([
|
|||
'Decorator',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Everything `resolveTsTarget` derives from one workspace file set: the file
|
||||
* list, the lower-cased file list, the suffix index and the per-pass
|
||||
* `resolveCache`.
|
||||
*
|
||||
* Without this memoization `resolveTsTarget` re-derived `allFileList` and
|
||||
* `normalizedFileList` (both O(N_files)), rebuilt the index and threw away the
|
||||
* `resolveCache` on every import — O(N_files × N_imports) total work for what
|
||||
* should be O(N_files + N_imports).
|
||||
*/
|
||||
interface TsPassCache {
|
||||
readonly allFilePaths: Set<string>;
|
||||
readonly allFileList: readonly string[];
|
||||
readonly normalizedFileList: readonly string[];
|
||||
readonly index: SuffixIndex;
|
||||
readonly resolveCache: Map<string, string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoized on the `allFilePaths` Set identity, like every other language's
|
||||
* import index (`import-resolvers/workspace-file-index.ts` and friends).
|
||||
|
|
@ -89,27 +72,7 @@ interface TsPassCache {
|
|||
* `new Set(allFilePaths)` at the adapter boundary hands a fresh key per import
|
||||
* and restores the per-import rebuild (PR #1918 review P1).
|
||||
*/
|
||||
const TS_PASS_CACHE = new WeakMap<ReadonlySet<string>, TsPassCache>();
|
||||
|
||||
function tsPassCacheFor(allFilePaths: ReadonlySet<string>): TsPassCache {
|
||||
const cached = TS_PASS_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const allFileList = Array.from(allFilePaths);
|
||||
const normalizedFileList = allFileList.map((f) => f.toLowerCase());
|
||||
const built: TsPassCache = {
|
||||
// Copied ONCE per file set, not once per import: `TsResolveContext` wants a
|
||||
// mutable `Set` and the orchestrator hands us a `ReadonlySet`. The copy is
|
||||
// not the #1918 hazard because the cache KEY is the caller's original Set.
|
||||
allFilePaths: new Set(allFilePaths),
|
||||
allFileList,
|
||||
normalizedFileList,
|
||||
index: buildSuffixIndex(normalizedFileList, allFileList),
|
||||
resolveCache: new Map(),
|
||||
};
|
||||
TS_PASS_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
const tsPassCacheFor = perFileSet(buildImportPassCache);
|
||||
|
||||
/**
|
||||
* Build a `resolveImportTarget` adapter that reads the memoized per-file-set
|
||||
|
|
|
|||
|
|
@ -20,21 +20,14 @@
|
|||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { resolveTsTarget, type TsResolveContext } from '../typescript/import-target.js';
|
||||
import { buildSuffixIndex, type SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import { buildImportPassCache } from '../../import-resolvers/pass-cache.js';
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import type { TsconfigPaths } from '../../language-config.js';
|
||||
|
||||
interface VueResolutionConfig {
|
||||
readonly tsconfigPaths: TsconfigPaths | null;
|
||||
}
|
||||
|
||||
interface PassCache {
|
||||
readonly allFilePaths: Set<string>;
|
||||
readonly allFileList: readonly string[];
|
||||
readonly normalizedFileList: readonly string[];
|
||||
readonly index: SuffixIndex;
|
||||
readonly resolveCache: Map<string, string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoized on the `allFilePaths` Set identity, like every other language's
|
||||
* import index (`import-resolvers/workspace-file-index.ts` and friends).
|
||||
|
|
@ -53,27 +46,7 @@ interface PassCache {
|
|||
* `new Set(allFilePaths)` at the adapter boundary hands a fresh key per import
|
||||
* and restores the per-import rebuild (PR #1918 review P1).
|
||||
*/
|
||||
const PASS_CACHE = new WeakMap<ReadonlySet<string>, PassCache>();
|
||||
|
||||
function passCacheFor(allFilePaths: ReadonlySet<string>): PassCache {
|
||||
const cached = PASS_CACHE.get(allFilePaths);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const allFileList = Array.from(allFilePaths);
|
||||
const normalizedFileList = allFileList.map((f) => f.toLowerCase());
|
||||
const built: PassCache = {
|
||||
// Copied ONCE per file set, not once per import: `TsResolveContext` wants a
|
||||
// mutable `Set` and the orchestrator hands us a `ReadonlySet`. The copy is
|
||||
// not the #1918 hazard because the cache KEY is the caller's original Set.
|
||||
allFilePaths: new Set(allFilePaths),
|
||||
allFileList,
|
||||
normalizedFileList,
|
||||
index: buildSuffixIndex(normalizedFileList, allFileList),
|
||||
resolveCache: new Map(),
|
||||
};
|
||||
PASS_CACHE.set(allFilePaths, built);
|
||||
return built;
|
||||
}
|
||||
const passCacheFor = perFileSet(buildImportPassCache);
|
||||
|
||||
/**
|
||||
* Build a memoized `resolveImportTarget` adapter for Vue SFCs.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue