gitnexus 1.6.10-rc.183 → 1.6.10-rc.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ingestion/import-resolvers/configs/csharp.js +5 -1
- package/dist/core/ingestion/import-resolvers/configs/swift.js +13 -0
- package/dist/core/ingestion/import-resolvers/csharp.d.ts +9 -1
- package/dist/core/ingestion/import-resolvers/csharp.js +176 -19
- package/dist/core/ingestion/import-resolvers/go.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/jvm.d.ts +2 -2
- package/dist/core/ingestion/import-resolvers/pass-cache.d.ts +50 -0
- package/dist/core/ingestion/import-resolvers/pass-cache.js +60 -0
- package/dist/core/ingestion/import-resolvers/per-file-set.d.ts +71 -0
- package/dist/core/ingestion/import-resolvers/per-file-set.js +11 -0
- package/dist/core/ingestion/import-resolvers/php.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/php.js +27 -8
- package/dist/core/ingestion/import-resolvers/python-file-index.d.ts +201 -0
- package/dist/core/ingestion/import-resolvers/python-file-index.js +302 -0
- package/dist/core/ingestion/import-resolvers/python.js +25 -6
- package/dist/core/ingestion/import-resolvers/ruby.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/utils.d.ts +56 -6
- package/dist/core/ingestion/import-resolvers/utils.js +215 -36
- package/dist/core/ingestion/import-resolvers/workspace-file-index.d.ts +38 -12
- package/dist/core/ingestion/import-resolvers/workspace-file-index.js +24 -18
- package/dist/core/ingestion/languages/c/import-target.js +12 -16
- package/dist/core/ingestion/languages/c/scope-resolver.js +20 -19
- package/dist/core/ingestion/languages/c/static-linkage.js +12 -16
- package/dist/core/ingestion/languages/cobol/scope-resolver.js +27 -20
- package/dist/core/ingestion/languages/cpp/file-local-linkage.js +11 -15
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +19 -20
- package/dist/core/ingestion/languages/csharp/import-target.js +4 -11
- package/dist/core/ingestion/languages/dart/import-target.js +4 -9
- package/dist/core/ingestion/languages/go/import-target.js +6 -13
- package/dist/core/ingestion/languages/java/captures.js +19 -28
- package/dist/core/ingestion/languages/java/import-target.d.ts +38 -0
- package/dist/core/ingestion/languages/java/import-target.js +113 -66
- package/dist/core/ingestion/languages/javascript/import-target.d.ts +26 -2
- package/dist/core/ingestion/languages/javascript/import-target.js +67 -13
- package/dist/core/ingestion/languages/kotlin/import-target.js +7 -13
- package/dist/core/ingestion/languages/php/import-target.js +128 -14
- package/dist/core/ingestion/languages/python/import-target.js +111 -96
- package/dist/core/ingestion/languages/rust/qualified-call.js +11 -9
- package/dist/core/ingestion/languages/swift/import-target.js +4 -9
- package/dist/core/ingestion/languages/typescript/scope-resolver.js +22 -24
- package/dist/core/ingestion/languages/vue/import-target.d.ts +2 -2
- package/dist/core/ingestion/languages/vue/import-target.js +24 -16
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +21 -13
- package/package.json +1 -1
- package/dist/core/ingestion/languages/kotlin/index-stats.d.ts +0 -18
- package/dist/core/ingestion/languages/kotlin/index-stats.js +0 -25
- package/dist/core/ingestion/languages/python/index-stats.d.ts +0 -18
- package/dist/core/ingestion/languages/python/index-stats.js +0 -25
|
@@ -24,7 +24,11 @@ export const csharpNamespaceStrategy = (rawImportPath, _filePath, ctx) => {
|
|
|
24
24
|
}
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
-
const resolvedFiles = resolveCSharpImportInternal(rawImportPath, csharpConfigs,
|
|
27
|
+
const resolvedFiles = resolveCSharpImportInternal(rawImportPath, csharpConfigs,
|
|
28
|
+
// The Set, not `ctx.normalizedFileList`/`ctx.allFileList`: the resolver
|
|
29
|
+
// derives both from it through the same per-pass memo the ctx's own arrays
|
|
30
|
+
// come from, so this is the identical pair by a shorter route.
|
|
31
|
+
ctx.allFilePaths, ctx.index, evidence);
|
|
28
32
|
if (resolvedFiles.length > 1) {
|
|
29
33
|
const dirSuffix = resolveCSharpNamespaceDir(rawImportPath, csharpConfigs);
|
|
30
34
|
if (dirSuffix) {
|
|
@@ -31,6 +31,19 @@ import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
|
31
31
|
* stable reference and the index is built once — not once per import. A
|
|
32
32
|
* fresh run produces a fresh array → a fresh index, so cross-run staleness
|
|
33
33
|
* is impossible.
|
|
34
|
+
*
|
|
35
|
+
* DELIBERATELY NOT ON `import-resolvers/per-file-set.ts` (#2909 sweep): this is
|
|
36
|
+
* a TWO-input memo keyed on ONE of them. The index is a function of both
|
|
37
|
+
* `ctx` (`allFileList` + the index-aligned `normalizedFileList`) and `targets`,
|
|
38
|
+
* but the key is only `ctx.allFileList`, and `perFileSet`'s `build: (key) => T`
|
|
39
|
+
* hands the builder nothing but the key. It is sound here only because of an
|
|
40
|
+
* invariant OUTSIDE the memo — `targets` is `ctx.configs.swiftPackageConfig
|
|
41
|
+
* .targets`, so it shares `ctx`'s lifetime and cannot vary while
|
|
42
|
+
* `ctx.allFileList` is fixed — and `perFileSet` has no way to express "and this
|
|
43
|
+
* other input is pinned by the same lifetime". Re-keying on `ctx` to make
|
|
44
|
+
* `targets` derivable from the key would change what the cache is keyed on and
|
|
45
|
+
* force an unreachable null-config arm into the builder, so it is a behaviour
|
|
46
|
+
* change rather than a consolidation. Leave it hand-rolled.
|
|
34
47
|
*/
|
|
35
48
|
const SWIFT_TARGET_INDEX_CACHE = new WeakMap();
|
|
36
49
|
function getSwiftTargetIndex(ctx, targets) {
|
|
@@ -13,8 +13,16 @@ import type { CSharpProjectConfig, CSharpNamespaceEvidence } from '../language-c
|
|
|
13
13
|
* The final unanchored suffix fallback is gated on `evidence` so BCL usings
|
|
14
14
|
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
15
15
|
* file (#1881). When `evidence` is omitted the fallback stays permissive.
|
|
16
|
+
*
|
|
17
|
+
* Takes the file SET, not the two materialized lists it used to take: both are
|
|
18
|
+
* derived here from the per-pass `getWorkspaceFileIndex` memo, which is where
|
|
19
|
+
* every caller already got them. That leaves one key shape for the indexes
|
|
20
|
+
* below and makes the `normalized`/`all` pairing structural rather than a
|
|
21
|
+
* contract the caller has to honour. `index` stays a parameter — the parity
|
|
22
|
+
* harness drives this resolver with and without one, and the no-index legs are
|
|
23
|
+
* a tested dimension, not a degenerate case.
|
|
16
24
|
*/
|
|
17
|
-
export declare function resolveCSharpImportInternal(importPath: string, csharpConfigs: CSharpProjectConfig[],
|
|
25
|
+
export declare function resolveCSharpImportInternal(importPath: string, csharpConfigs: CSharpProjectConfig[], allFilePaths: ReadonlySet<string>, index?: SuffixIndex, evidence?: CSharpNamespaceEvidence): string[];
|
|
18
26
|
/**
|
|
19
27
|
* Compute the directory suffix for a C# namespace import (for PackageMap).
|
|
20
28
|
* Returns a suffix like "/ProjectDir/Models/" or null if no config matches.
|
|
@@ -4,8 +4,154 @@
|
|
|
4
4
|
* Strategy lives in configs/csharp.ts.
|
|
5
5
|
* This file contains shared helpers for namespace-based resolution.
|
|
6
6
|
*/
|
|
7
|
+
import { perFileSet } from './per-file-set.js';
|
|
8
|
+
import { getWorkspaceFileIndex } from './workspace-file-index.js';
|
|
7
9
|
import { suffixResolve } from './utils.js';
|
|
8
10
|
import { csharpSuffixFallbackAllowed } from '../csharp-namespace-gate.js';
|
|
11
|
+
/**
|
|
12
|
+
* Memoized on the file SET's identity, the same key every other per-file-set
|
|
13
|
+
* index in this pipeline uses: the orchestrator builds one Set per pass and
|
|
14
|
+
* threads it through every import, so this build runs once.
|
|
15
|
+
*
|
|
16
|
+
* It used to key on the `normalizedFileList` ARRAY, which was a second key
|
|
17
|
+
* shape and — more to the point — one no guard could instrument. Copying an
|
|
18
|
+
* array mints a fresh `WeakMap` key while traversing the SET zero extra times,
|
|
19
|
+
* so a `[...normalized]` copy at the adapter boundary rebuilt this index once
|
|
20
|
+
* per `using` while every scan-counting guard stayed green and only the timing
|
|
21
|
+
* bench noticed (#2911 review). Taking the array from
|
|
22
|
+
* `getWorkspaceFileIndex(allFilePaths)` inside the builder retires that shape:
|
|
23
|
+
* the only way to defeat the memo now is to copy the Set, which is exactly what
|
|
24
|
+
* `CountingSet` counts.
|
|
25
|
+
*
|
|
26
|
+
* It also retires a precondition. The cached positions index `normalized` while
|
|
27
|
+
* the emitted value is read from `all`; both now come from the same
|
|
28
|
+
* `getWorkspaceFileIndex` object, so the caller can no longer pair a position
|
|
29
|
+
* list against a differently-ordered array.
|
|
30
|
+
*/
|
|
31
|
+
const getCsharpNamespaceDirIndex = perFileSet((allFilePaths) => {
|
|
32
|
+
const { normalized: normalizedFileList } = getWorkspaceFileIndex(allFilePaths);
|
|
33
|
+
const dirsByLastSegment = new Map();
|
|
34
|
+
const positionsByDir = new Map();
|
|
35
|
+
const singleSegmentDirs = [];
|
|
36
|
+
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
37
|
+
const normalized = normalizedFileList[i];
|
|
38
|
+
if (!normalized.endsWith('.cs'))
|
|
39
|
+
continue;
|
|
40
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
41
|
+
// A file with no directory can never match: the needle always ends with
|
|
42
|
+
// '/', so `indexOf` on a slash-free path is always -1.
|
|
43
|
+
if (lastSlash < 0)
|
|
44
|
+
continue;
|
|
45
|
+
const dir = normalized.slice(0, lastSlash);
|
|
46
|
+
let positions = positionsByDir.get(dir);
|
|
47
|
+
if (positions === undefined) {
|
|
48
|
+
positions = [];
|
|
49
|
+
positionsByDir.set(dir, positions);
|
|
50
|
+
const lastSegment = dir.slice(dir.lastIndexOf('/') + 1);
|
|
51
|
+
if (lastSegment === dir)
|
|
52
|
+
singleSegmentDirs.push(dir);
|
|
53
|
+
let dirs = dirsByLastSegment.get(lastSegment);
|
|
54
|
+
if (dirs === undefined) {
|
|
55
|
+
dirs = [];
|
|
56
|
+
dirsByLastSegment.set(lastSegment, dirs);
|
|
57
|
+
}
|
|
58
|
+
dirs.push(dir);
|
|
59
|
+
}
|
|
60
|
+
positions.push(i);
|
|
61
|
+
}
|
|
62
|
+
return { dirsByLastSegment, positionsByDir, singleSegmentDirs };
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Every directory that could satisfy `dirPrefix`, as a superset — the exact
|
|
66
|
+
* test runs in `matchingDirPositions`.
|
|
67
|
+
*
|
|
68
|
+
* When `dirPrefix` contains a '/', its own slash forces a segment boundary in
|
|
69
|
+
* any matching directory: `H` ending with `…/<lastSeg>/` means `D` ends with
|
|
70
|
+
* `/<lastSeg>`, so `D`'s last segment IS `lastSeg` and the exact bucket is
|
|
71
|
+
* complete. Without a '/', `D`'s last segment only has to END with `dirPrefix`
|
|
72
|
+
* (`SubModels` for `Models`), which no single bucket holds, so the last-segment
|
|
73
|
+
* KEYS are swept. That is the one term here that is not O(matches), and it is
|
|
74
|
+
* O(distinct last segments), not O(directories): C# repos reuse `Models`,
|
|
75
|
+
* `Services`, `Controllers` under every project, so the sweep collapses on the
|
|
76
|
+
* layouts that actually occur. Measured at 200 000 `.cs` files, 25 000
|
|
77
|
+
* directories: 456 µs per import when every directory name is unique, 7.9 µs
|
|
78
|
+
* on a `SrcN/Models` layout. Closing the unique-name case needs a character-
|
|
79
|
+
* suffix map over the segments, which is the O(files × depth) memory shape
|
|
80
|
+
* `package-dir-index.ts` cites #2649 to avoid — a design change, not a tune.
|
|
81
|
+
*
|
|
82
|
+
* An empty `dirPrefix` would sweep every key and keep every directory, so it is
|
|
83
|
+
* answered from `singleSegmentDirs` instead: its needle is a bare '/', which
|
|
84
|
+
* only a slash-free directory can carry as its LAST slash.
|
|
85
|
+
*/
|
|
86
|
+
function* candidateDirs(index, dirPrefix) {
|
|
87
|
+
if (dirPrefix === '') {
|
|
88
|
+
yield* index.singleSegmentDirs;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const lastSlash = dirPrefix.lastIndexOf('/');
|
|
92
|
+
if (lastSlash >= 0) {
|
|
93
|
+
const bucket = index.dirsByLastSegment.get(dirPrefix.slice(lastSlash + 1));
|
|
94
|
+
if (bucket !== undefined)
|
|
95
|
+
yield* bucket;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const [lastSegment, dirs] of index.dirsByLastSegment) {
|
|
99
|
+
if (!lastSegment.endsWith(dirPrefix))
|
|
100
|
+
continue;
|
|
101
|
+
yield* dirs;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Positions of the `.cs` files in each directory matching `dirPrefix`. */
|
|
105
|
+
function* matchingDirPositions(index, dirPrefix) {
|
|
106
|
+
const needle = dirPrefix + '/';
|
|
107
|
+
for (const dir of candidateDirs(index, dirPrefix)) {
|
|
108
|
+
const haystack = dir + '/';
|
|
109
|
+
// The length guard is not redundant: for a shorter `haystack`, `indexOf`
|
|
110
|
+
// returns -1 and `haystack.length - needle.length` can also be -1, which
|
|
111
|
+
// would report a bogus match.
|
|
112
|
+
if (haystack.length < needle.length)
|
|
113
|
+
continue;
|
|
114
|
+
if (haystack.indexOf(needle) !== haystack.length - needle.length)
|
|
115
|
+
continue;
|
|
116
|
+
const positions = index.positionsByDir.get(dir);
|
|
117
|
+
if (positions !== undefined)
|
|
118
|
+
yield positions;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Append every `.cs` file directly inside a directory matching `dirPrefix`, in
|
|
123
|
+
* `normalizedFileList` order — the order the single-pass scan emitted, which
|
|
124
|
+
* this function's callers return as the whole edge target list.
|
|
125
|
+
*/
|
|
126
|
+
function pushFilesDirectlyInNamespaceDir(index, dirPrefix, allFileList, results) {
|
|
127
|
+
// One matching directory is the overwhelmingly common case, and its positions
|
|
128
|
+
// are already ascending, so the first bucket is held by reference. A second
|
|
129
|
+
// one promotes it to a real accumulator that is appended to from then on —
|
|
130
|
+
// never re-spread per directory, which would cost O(files × dirs²) copies in
|
|
131
|
+
// a monorepo carrying the same namespace directory under many projects.
|
|
132
|
+
let first = null;
|
|
133
|
+
let merged = null;
|
|
134
|
+
for (const positions of matchingDirPositions(index, dirPrefix)) {
|
|
135
|
+
if (first === null) {
|
|
136
|
+
first = positions;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (merged === null)
|
|
140
|
+
merged = [...first];
|
|
141
|
+
for (const position of positions)
|
|
142
|
+
merged.push(position);
|
|
143
|
+
}
|
|
144
|
+
if (first === null)
|
|
145
|
+
return;
|
|
146
|
+
if (merged === null) {
|
|
147
|
+
for (const position of first)
|
|
148
|
+
results.push(allFileList[position]);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
merged.sort((a, b) => a - b);
|
|
152
|
+
for (const position of merged)
|
|
153
|
+
results.push(allFileList[position]);
|
|
154
|
+
}
|
|
9
155
|
/**
|
|
10
156
|
* Resolve a C# using-directive import path to matching .cs files (low-level helper).
|
|
11
157
|
* Tries single-file match first, then directory match for namespace imports.
|
|
@@ -13,8 +159,17 @@ import { csharpSuffixFallbackAllowed } from '../csharp-namespace-gate.js';
|
|
|
13
159
|
* The final unanchored suffix fallback is gated on `evidence` so BCL usings
|
|
14
160
|
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
15
161
|
* file (#1881). When `evidence` is omitted the fallback stays permissive.
|
|
162
|
+
*
|
|
163
|
+
* Takes the file SET, not the two materialized lists it used to take: both are
|
|
164
|
+
* derived here from the per-pass `getWorkspaceFileIndex` memo, which is where
|
|
165
|
+
* every caller already got them. That leaves one key shape for the indexes
|
|
166
|
+
* below and makes the `normalized`/`all` pairing structural rather than a
|
|
167
|
+
* contract the caller has to honour. `index` stays a parameter — the parity
|
|
168
|
+
* harness drives this resolver with and without one, and the no-index legs are
|
|
169
|
+
* a tested dimension, not a degenerate case.
|
|
16
170
|
*/
|
|
17
|
-
export function resolveCSharpImportInternal(importPath, csharpConfigs,
|
|
171
|
+
export function resolveCSharpImportInternal(importPath, csharpConfigs, allFilePaths, index, evidence) {
|
|
172
|
+
const { normalized: normalizedFileList, all: allFileList } = getWorkspaceFileIndex(allFilePaths);
|
|
18
173
|
const namespacePath = importPath.replace(/\./g, '/');
|
|
19
174
|
const results = [];
|
|
20
175
|
for (const config of csharpConfigs) {
|
|
@@ -65,24 +220,26 @@ export function resolveCSharpImportInternal(importPath, csharpConfigs, normalize
|
|
|
65
220
|
if (results.length > 0)
|
|
66
221
|
return results;
|
|
67
222
|
}
|
|
68
|
-
// 3.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
223
|
+
// 3. Directory matching, UNANCHORED.
|
|
224
|
+
//
|
|
225
|
+
// Not redundant with step 2, and not skippable when `index` is present:
|
|
226
|
+
// `getFilesInDir` is keyed on SEGMENT suffixes of a directory, while this
|
|
227
|
+
// leg's predicate is an unanchored substring one, so it additionally
|
|
228
|
+
// answers `Models` with `src/SubModels/` and `src/Models` with
|
|
229
|
+
// `vendor/mysrc/Models/`. It is also the only leg that answers an empty
|
|
230
|
+
// `dirPrefix` — the `relative = ''` branch above (the import IS the root
|
|
231
|
+
// namespace) with no `projectDir` to stand in for it — because
|
|
232
|
+
// `buildSuffixIndex` emits an empty directory suffix only for a path that
|
|
233
|
+
// BEGINS with '/', so over repo-relative paths `getFilesInDir('', '.cs')`
|
|
234
|
+
// is always empty. See `CsharpNamespaceDirIndex` above for the index that
|
|
235
|
+
// replaced the per-import Θ(files) scan this used to be (#2902).
|
|
236
|
+
//
|
|
237
|
+
// `results` is provably empty here: step 2 returns as soon as it pushes
|
|
238
|
+
// anything, and so does this leg, so every iteration of the config loop
|
|
239
|
+
// starts empty.
|
|
240
|
+
pushFilesDirectlyInNamespaceDir(getCsharpNamespaceDirIndex(allFilePaths), dirPrefix, allFileList, results);
|
|
241
|
+
if (results.length > 0)
|
|
242
|
+
return results;
|
|
86
243
|
}
|
|
87
244
|
// Fallback: suffix matching without namespace stripping (single file).
|
|
88
245
|
// Gated on in-repo declared-namespace evidence (#1881).
|
|
@@ -14,4 +14,4 @@ export declare function resolveGoPackageDir(importPath: string, goModule: GoModu
|
|
|
14
14
|
* Resolve a Go internal package import to all .go files in the package directory.
|
|
15
15
|
* Returns an array of file paths.
|
|
16
16
|
*/
|
|
17
|
-
export declare function resolveGoPackage(importPath: string, goModule: GoModuleConfig, normalizedFileList: string[], allFileList: string[]): string[];
|
|
17
|
+
export declare function resolveGoPackage(importPath: string, goModule: GoModuleConfig, normalizedFileList: readonly string[], allFileList: readonly string[]): string[];
|
|
@@ -18,10 +18,10 @@ export declare const appendKotlinWildcard: (importPath: string, importNode: Synt
|
|
|
18
18
|
* Resolve a JVM wildcard import (com.example.*) to all matching files.
|
|
19
19
|
* Works for both Java (.java) and Kotlin (.kt, .kts).
|
|
20
20
|
*/
|
|
21
|
-
export declare function resolveJvmWildcard(importPath: string, normalizedFileList: string[], allFileList: string[], extensions: readonly string[], index?: SuffixIndex): string[];
|
|
21
|
+
export declare function resolveJvmWildcard(importPath: string, normalizedFileList: readonly string[], allFileList: readonly string[], extensions: readonly string[], index?: SuffixIndex): string[];
|
|
22
22
|
/**
|
|
23
23
|
* Try to resolve a JVM member/static import by stripping the member name.
|
|
24
24
|
* Java: "com.example.Constants.VALUE" -> resolve "com.example.Constants"
|
|
25
25
|
* Kotlin: "com.example.Constants.VALUE" -> resolve "com.example.Constants"
|
|
26
26
|
*/
|
|
27
|
-
export declare function resolveJvmMemberImport(importPath: string, normalizedFileList: string[], allFileList: string[], extensions: readonly string[], index?: SuffixIndex): string | null;
|
|
27
|
+
export declare function resolveJvmMemberImport(importPath: string, normalizedFileList: readonly string[], allFileList: readonly string[], extensions: readonly string[], index?: SuffixIndex): string | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type SuffixIndex } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Everything the standard `resolveTsTarget` path derives from one workspace
|
|
4
|
+
* file set: the file list, the lower-cased file list, the suffix index and the
|
|
5
|
+
* per-pass `resolveCache`.
|
|
6
|
+
*
|
|
7
|
+
* Without this memoization the resolver re-derived `allFileList` and
|
|
8
|
+
* `normalizedFileList` (both O(N_files)), rebuilt the index and threw away the
|
|
9
|
+
* `resolveCache` on every import — O(N_files × N_imports) total work for what
|
|
10
|
+
* should be O(N_files + N_imports).
|
|
11
|
+
*/
|
|
12
|
+
export interface ImportPassCache {
|
|
13
|
+
readonly allFilePaths: Set<string>;
|
|
14
|
+
readonly allFileList: readonly string[];
|
|
15
|
+
readonly normalizedFileList: readonly string[];
|
|
16
|
+
readonly index: SuffixIndex;
|
|
17
|
+
readonly resolveCache: Map<string, string | null>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build that state. Shared by every adapter whose resolution runs through
|
|
21
|
+
* `resolveTsTarget`.
|
|
22
|
+
*
|
|
23
|
+
* Not a dedup of identical copies, and the difference is the point. At
|
|
24
|
+
* 49c5b7d81 each of those adapters carried this record inline and they did NOT
|
|
25
|
+
* agree: `languages/typescript/scope-resolver.ts` and
|
|
26
|
+
* `languages/vue/import-target.ts` held six byte-identical fields built around
|
|
27
|
+
* `index: buildSuffixIndex(normalizedFileList, allFileList)`, while
|
|
28
|
+
* `languages/javascript/import-target.ts` held five and never called
|
|
29
|
+
* `buildSuffixIndex` at all. That one missing field IS the O(imports × files)
|
|
30
|
+
* defect PR #2911 fixed — `resolveTsTarget` fell back to `suffixResolve`'s
|
|
31
|
+
* linear scan for every JavaScript import — and the header of
|
|
32
|
+
* `languages/javascript/import-target.ts` carries the measurements. Hoisting
|
|
33
|
+
* the builder is what makes a fourth adapter unable to omit it again: `index`
|
|
34
|
+
* is not optional on `ImportPassCache`.
|
|
35
|
+
*
|
|
36
|
+
* The BUILDER is shared; the MEMO deliberately is not. Each adapter wraps this
|
|
37
|
+
* in its own `perFileSet(...)`, so each gets its own `WeakMap`, its own index
|
|
38
|
+
* instance and — the one that would be a behaviour change — its own
|
|
39
|
+
* `resolveCache`. The languages disagree about what a specifier resolves to
|
|
40
|
+
* (`tsconfigPaths` is read from config for TypeScript and Vue, pinned to `null`
|
|
41
|
+
* for JavaScript, and the tried extension list differs), so one shared resolve
|
|
42
|
+
* cache across them would hand a language another language's answers.
|
|
43
|
+
*
|
|
44
|
+
* Sharing the builder is a code dedup and nothing more: it buys no runtime
|
|
45
|
+
* reuse, because there is none to buy. Each provider pass builds its own
|
|
46
|
+
* `allFilePaths` Set (`scope-resolution/pipeline/run.ts`, per provider), so
|
|
47
|
+
* TypeScript's set and JavaScript's set are different objects and therefore
|
|
48
|
+
* different `WeakMap` keys even where the two memos are the same code.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildImportPassCache(allFilePaths: ReadonlySet<string>): ImportPassCache;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { buildSuffixIndex } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build that state. Shared by every adapter whose resolution runs through
|
|
4
|
+
* `resolveTsTarget`.
|
|
5
|
+
*
|
|
6
|
+
* Not a dedup of identical copies, and the difference is the point. At
|
|
7
|
+
* 49c5b7d81 each of those adapters carried this record inline and they did NOT
|
|
8
|
+
* agree: `languages/typescript/scope-resolver.ts` and
|
|
9
|
+
* `languages/vue/import-target.ts` held six byte-identical fields built around
|
|
10
|
+
* `index: buildSuffixIndex(normalizedFileList, allFileList)`, while
|
|
11
|
+
* `languages/javascript/import-target.ts` held five and never called
|
|
12
|
+
* `buildSuffixIndex` at all. That one missing field IS the O(imports × files)
|
|
13
|
+
* defect PR #2911 fixed — `resolveTsTarget` fell back to `suffixResolve`'s
|
|
14
|
+
* linear scan for every JavaScript import — and the header of
|
|
15
|
+
* `languages/javascript/import-target.ts` carries the measurements. Hoisting
|
|
16
|
+
* the builder is what makes a fourth adapter unable to omit it again: `index`
|
|
17
|
+
* is not optional on `ImportPassCache`.
|
|
18
|
+
*
|
|
19
|
+
* The BUILDER is shared; the MEMO deliberately is not. Each adapter wraps this
|
|
20
|
+
* in its own `perFileSet(...)`, so each gets its own `WeakMap`, its own index
|
|
21
|
+
* instance and — the one that would be a behaviour change — its own
|
|
22
|
+
* `resolveCache`. The languages disagree about what a specifier resolves to
|
|
23
|
+
* (`tsconfigPaths` is read from config for TypeScript and Vue, pinned to `null`
|
|
24
|
+
* for JavaScript, and the tried extension list differs), so one shared resolve
|
|
25
|
+
* cache across them would hand a language another language's answers.
|
|
26
|
+
*
|
|
27
|
+
* Sharing the builder is a code dedup and nothing more: it buys no runtime
|
|
28
|
+
* reuse, because there is none to buy. Each provider pass builds its own
|
|
29
|
+
* `allFilePaths` Set (`scope-resolution/pipeline/run.ts`, per provider), so
|
|
30
|
+
* TypeScript's set and JavaScript's set are different objects and therefore
|
|
31
|
+
* different `WeakMap` keys even where the two memos are the same code.
|
|
32
|
+
*/
|
|
33
|
+
export function buildImportPassCache(allFilePaths) {
|
|
34
|
+
const allFileList = Array.from(allFilePaths);
|
|
35
|
+
// LOWERCASED, not slash-normalized — unlike every other caller of
|
|
36
|
+
// `buildSuffixIndex`. That is what `alreadyLowercased` below records.
|
|
37
|
+
const normalizedFileList = allFileList.map((f) => f.toLowerCase());
|
|
38
|
+
return {
|
|
39
|
+
// Copied ONCE per file set, not once per import: `TsResolveContext` wants a
|
|
40
|
+
// mutable `Set` and the orchestrator hands us a `ReadonlySet`. The copy is
|
|
41
|
+
// not the #1918 hazard because the cache KEY is the caller's original Set.
|
|
42
|
+
allFilePaths: new Set(allFilePaths),
|
|
43
|
+
allFileList,
|
|
44
|
+
normalizedFileList,
|
|
45
|
+
// Every suffix of an all-lowercase path is itself lowercase, so the index's
|
|
46
|
+
// case-folded map came out a byte-for-byte copy of its exact map — same
|
|
47
|
+
// keys, same values, same insertion order — one per `ImportPassCache`, so
|
|
48
|
+
// once per adapter per pass. Measured 14.00 MiB at 32 000 paths, 29.8% of
|
|
49
|
+
// the retained `ImportPassCache`. The flag drops the copy; it does not change
|
|
50
|
+
// what `getInsensitive` answers, because the copy was the identity (see
|
|
51
|
+
// `SuffixIndexOptions`). Checked, not assumed: over 474 524 probes on four
|
|
52
|
+
// mixed-case corpora — Vue PascalCase plus alias specifiers, case-colliding
|
|
53
|
+
// twins, a 600-file deep monorepo, and Unicode paths carrying final sigma,
|
|
54
|
+
// dotted-I and sharp-S — the two maps came out byte-identical, the exact
|
|
55
|
+
// map was the sole answerer 0 times, and `get(s) || getInsensitive(s)`
|
|
56
|
+
// returned the same file 474 524 times out of 474 524.
|
|
57
|
+
index: buildSuffixIndex(normalizedFileList, allFileList, { alreadyLowercased: true }),
|
|
58
|
+
resolveCache: new Map(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one memo every per-file-set index in this pipeline is built on.
|
|
3
|
+
*
|
|
4
|
+
* The scope-resolution orchestrator builds ONE file-set object per provider
|
|
5
|
+
* pass and threads that same object through every `resolveImportTarget` call in
|
|
6
|
+
* the pass, so anything derived from it — a suffix index, a package-directory
|
|
7
|
+
* map, a basename bucket — can be built once and read by every import instead
|
|
8
|
+
* of rebuilt per import. Keying on the object's IDENTITY is what makes that
|
|
9
|
+
* work, and it is equally the contract callers must keep: the set is passed
|
|
10
|
+
* THROUGH, never copied. A defensive `new Set(allFilePaths)` at an adapter
|
|
11
|
+
* boundary hands a fresh key per import and silently restores
|
|
12
|
+
* O(imports × files) — the bug PR #1918 shipped and had to fix in review (P1).
|
|
13
|
+
* The guards are `test/integration/<lang>-import-index-reuse.test.ts` and, for
|
|
14
|
+
* every registered language at once,
|
|
15
|
+
* `test/unit/scope-resolution/import-target-index-reuse.contract.test.ts`,
|
|
16
|
+
* whose inventory arm fails when an entry of `SCOPE_RESOLVERS` has no fixture.
|
|
17
|
+
* That arm is why no language is named here: the registry is the census, and a
|
|
18
|
+
* hand-copied list of languages goes stale the release after it is written.
|
|
19
|
+
*
|
|
20
|
+
* A `WeakMap` rather than a `Map`: the entry is reclaimed with the file set it
|
|
21
|
+
* was derived from, so a pass can never read a previous pass's index and memory
|
|
22
|
+
* does not grow across runs. There is no invalidation rule to get wrong because
|
|
23
|
+
* there is nothing to invalidate — a new file set is a new key.
|
|
24
|
+
*
|
|
25
|
+
* The KEY TYPE is constrained rather than described, because which object is
|
|
26
|
+
* the key decides whether the guards above can see the memo fail, and a prose
|
|
27
|
+
* list of call sites is the thing this file elsewhere tells you not to write.
|
|
28
|
+
* `K` admits exactly the two shapes the orchestrator keeps stable for a pass:
|
|
29
|
+
*
|
|
30
|
+
* - `ReadonlySet<string>`, the pass's file set — every index derived from it,
|
|
31
|
+
* including the derived header-closure sets that `languages/{c,cpp}/
|
|
32
|
+
* scope-resolver.ts` memoize inside an outer per-file-set memo. Defeating
|
|
33
|
+
* one of these means copying the SET, which re-traverses it, which the
|
|
34
|
+
* `CountingSet` instrument (`test/helpers/counting-file-set.ts`) reads as a
|
|
35
|
+
* scan count rising with the import count.
|
|
36
|
+
* - `readonly ParsedFile[]`, the pass's parsed-file array. Not derived from
|
|
37
|
+
* the file set at all, so the file-set guards do not reach them; these key
|
|
38
|
+
* on the array the orchestrator already threads through the pass, and their
|
|
39
|
+
* contract is that same pass-through discipline. The instrument that CAN see
|
|
40
|
+
* them counts element reads on that array — `countedParsedFiles`, beside
|
|
41
|
+
* `CountingSet`, driven by the contract test's `minimumParsedFileReads`.
|
|
42
|
+
*
|
|
43
|
+
* A THIRD shape — an array materialized from the file set — is what the type
|
|
44
|
+
* exists to reject. `import-resolvers/csharp.ts` used one until #2911, and it
|
|
45
|
+
* is worth a compile error rather than a rule: copying an array mints a fresh
|
|
46
|
+
* `WeakMap` key while traversing the Set zero extra times, so every
|
|
47
|
+
* scan-counting guard stays green at its correct value while the index rebuilds
|
|
48
|
+
* once per import. That failure is invisible to the whole instrument family
|
|
49
|
+
* above and was caught only by a timing ratio in `bench/import-target/`. Derive
|
|
50
|
+
* the array inside the builder from `getWorkspaceFileIndex(allFilePaths)`
|
|
51
|
+
* instead. `string[]` is not assignable to `K`, so the shape cannot come back
|
|
52
|
+
* silently — `configs/swift.ts` keeps the one hand-rolled `WeakMap` on
|
|
53
|
+
* `ctx.allFileList` in the tree, deliberately and with its reasons written
|
|
54
|
+
* down, and it is deliberately NOT on this primitive.
|
|
55
|
+
*
|
|
56
|
+
* `T extends object` is deliberate, chosen over probing `has` before `get`.
|
|
57
|
+
* `WeakMap.get` returning `undefined` cannot distinguish "not built yet" from
|
|
58
|
+
* "built, and the value is `undefined`"; constraining the value to an object
|
|
59
|
+
* makes the second case unrepresentable rather than paying a second lookup on
|
|
60
|
+
* every import, and it needs no cast to type-check. Every index memoized here
|
|
61
|
+
* is a record, `Map` or `Set`, so the constraint costs nothing today — and a
|
|
62
|
+
* later caller wanting to memoize a `string | null` gets a compile error
|
|
63
|
+
* pointing at this line instead of a memo that silently rebuilds on every miss.
|
|
64
|
+
*
|
|
65
|
+
* A `build` that THROWS stores nothing, so the next call for that key runs it
|
|
66
|
+
* again: failures are not memoized, and a half-filled index is never published.
|
|
67
|
+
* Inert for the builders here — each is a pure, total pass over the file set —
|
|
68
|
+
* and the safer of the two behaviours if that ever stops being true.
|
|
69
|
+
*/
|
|
70
|
+
import type { ParsedFile } from '../../../_shared/index.js';
|
|
71
|
+
export declare function perFileSet<K extends ReadonlySet<string> | readonly ParsedFile[], T extends object>(build: (key: K) => T): (key: K) => T;
|
|
@@ -21,4 +21,4 @@ import type { ComposerConfig } from '../language-config.js';
|
|
|
21
21
|
* a known limitation — PHP function imports cannot be resolved to a specific file
|
|
22
22
|
* without parsing all candidate files.
|
|
23
23
|
*/
|
|
24
|
-
export declare function resolvePhpImportInternal(importPath: string, composerConfig: ComposerConfig | null, allFiles: Set<string>, normalizedFileList: string[], allFileList: string[], index?: SuffixIndex): string | null;
|
|
24
|
+
export declare function resolvePhpImportInternal(importPath: string, composerConfig: ComposerConfig | null, allFiles: Set<string>, normalizedFileList: readonly string[], allFileList: readonly string[], index?: SuffixIndex): string | null;
|
|
@@ -53,19 +53,38 @@ export function resolvePhpImportInternal(importPath, composerConfig, allFiles, n
|
|
|
53
53
|
// e.g. App\Models\getUser → directory app/Models/, find first .php file in that dir.
|
|
54
54
|
const lastSlash = remainder.lastIndexOf('/');
|
|
55
55
|
const nsDir = lastSlash >= 0 ? dirPrefix + '/' + remainder.slice(0, lastSlash) : dirPrefix;
|
|
56
|
-
// Prefer SuffixIndex directory lookup (O(log n + matches)) over linear scan
|
|
56
|
+
// Prefer SuffixIndex directory lookup (O(log n + matches)) over linear scan.
|
|
57
|
+
//
|
|
58
|
+
// An EMPTY bucket is a final answer, not a miss to retry with the scan
|
|
59
|
+
// below — which is what the `else` restores, and what this comment
|
|
60
|
+
// always claimed. Re-scanning on empty was the last per-import
|
|
61
|
+
// workspace traversal left in PHP resolution after #2901: any `use`
|
|
62
|
+
// matching a PSR-4 prefix whose directory holds no direct `.php` child
|
|
63
|
+
// (`App\Legacy\Ghost`) paid a full pass, measured at 201 traversals for
|
|
64
|
+
// 200 imports.
|
|
65
|
+
//
|
|
66
|
+
// The bucket is a superset of what the scan can find, for BOTH index
|
|
67
|
+
// shapes that reach here. A root-anchored direct child `nsDir/<x>.php`
|
|
68
|
+
// has its directory exactly equal to `nsDir`, and `nsDir` is always one
|
|
69
|
+
// of that directory's own suffixes — so the shared `dirMap` (keyed on
|
|
70
|
+
// every directory suffix) necessarily contains it, as does the
|
|
71
|
+
// root-anchored parity index `languages/php/import-target.ts` builds.
|
|
72
|
+
// Empty superset therefore implies empty scan, and control falls
|
|
73
|
+
// through to the next PSR-4 prefix exactly as before.
|
|
57
74
|
if (index) {
|
|
58
75
|
const candidates = index.getFilesInDir(nsDir, '.php');
|
|
59
76
|
if (candidates.length > 0)
|
|
60
77
|
return candidates[0];
|
|
61
78
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
f.
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
else {
|
|
80
|
+
// Linear scan, only when a SuffixIndex is genuinely unavailable.
|
|
81
|
+
const nsDirPrefix = nsDir.endsWith('/') ? nsDir : nsDir + '/';
|
|
82
|
+
for (const f of allFiles) {
|
|
83
|
+
if (f.startsWith(nsDirPrefix) &&
|
|
84
|
+
f.endsWith('.php') &&
|
|
85
|
+
!f.slice(nsDirPrefix.length).includes('/')) {
|
|
86
|
+
return f;
|
|
87
|
+
}
|
|
69
88
|
}
|
|
70
89
|
}
|
|
71
90
|
}
|