gitnexus 1.6.5-rc.44 → 1.6.5-rc.46
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/languages/cpp/inline-namespaces.d.ts +3 -3
- package/dist/core/ingestion/languages/cpp/inline-namespaces.js +49 -15
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +1 -1
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +4 -3
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +6 -0
- package/dist/storage/parse-cache.d.ts +8 -2
- package/dist/storage/parse-cache.js +88 -17
- package/package.json +1 -1
|
@@ -60,8 +60,8 @@ export declare function isCppInlineNamespaceScope(scopeId: ScopeId): boolean;
|
|
|
60
60
|
* Returns the most specific (innermost) match — for `outer::foo()`
|
|
61
61
|
* where `inline namespace v1` declares `foo`, returns `v1::foo`. When
|
|
62
62
|
* multiple inline-namespace children declare the same name, ISO C++
|
|
63
|
-
* leaves the call ambiguous;
|
|
64
|
-
*
|
|
63
|
+
* leaves the call ambiguous; returns `'ambiguous'` so the caller
|
|
64
|
+
* suppresses edge emission rather than picking arbitrarily (#1564).
|
|
65
65
|
*/
|
|
66
|
-
export declare function resolveCppQualifiedNamespaceMember(receiverName: string, memberName: string, parsedFiles: readonly ParsedFile[], _scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
|
|
66
|
+
export declare function resolveCppQualifiedNamespaceMember(receiverName: string, memberName: string, parsedFiles: readonly ParsedFile[], _scopes: ScopeResolutionIndexes): SymbolDefinition | 'ambiguous' | undefined;
|
|
67
67
|
export {};
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* `std::vector` qualified calls resolve to the inline-namespace
|
|
27
27
|
* declaration transparently.
|
|
28
28
|
*/
|
|
29
|
+
import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js';
|
|
29
30
|
const inlineNamespaceRangesByFile = new Map();
|
|
30
31
|
const inlineNamespaceScopeIds = new Set();
|
|
31
32
|
function rangeKey(r) {
|
|
@@ -80,10 +81,12 @@ export function isCppInlineNamespaceScope(scopeId) {
|
|
|
80
81
|
* Returns the most specific (innermost) match — for `outer::foo()`
|
|
81
82
|
* where `inline namespace v1` declares `foo`, returns `v1::foo`. When
|
|
82
83
|
* multiple inline-namespace children declare the same name, ISO C++
|
|
83
|
-
* leaves the call ambiguous;
|
|
84
|
-
*
|
|
84
|
+
* leaves the call ambiguous; returns `'ambiguous'` so the caller
|
|
85
|
+
* suppresses edge emission rather than picking arbitrarily (#1564).
|
|
85
86
|
*/
|
|
86
87
|
export function resolveCppQualifiedNamespaceMember(receiverName, memberName, parsedFiles, _scopes) {
|
|
88
|
+
const allHits = [];
|
|
89
|
+
const seenNodeId = new Set();
|
|
87
90
|
for (const parsed of parsedFiles) {
|
|
88
91
|
const scopesById = new Map();
|
|
89
92
|
for (const sc of parsed.scopes)
|
|
@@ -97,27 +100,58 @@ export function resolveCppQualifiedNamespaceMember(receiverName, memberName, par
|
|
|
97
100
|
const nsName = nsDef.qualifiedName?.split('.').pop() ?? nsDef.qualifiedName ?? '';
|
|
98
101
|
if (nsName !== receiverName)
|
|
99
102
|
continue;
|
|
100
|
-
// Found a matching namespace scope in this file. Collect
|
|
101
|
-
//
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Found a matching namespace scope in this file. Collect ALL
|
|
104
|
+
// members transitively through any inline-namespace children.
|
|
105
|
+
const hits = findMemberInNamespaceTransitive(scope, scopesById, memberName);
|
|
106
|
+
for (const hit of hits) {
|
|
107
|
+
if (seenNodeId.has(hit.nodeId))
|
|
108
|
+
continue;
|
|
109
|
+
seenNodeId.add(hit.nodeId);
|
|
110
|
+
allHits.push(hit);
|
|
111
|
+
}
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
|
-
|
|
114
|
+
if (allHits.length === 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (allHits.length === 1)
|
|
117
|
+
return allHits[0];
|
|
118
|
+
// Multi-candidate: the `resolveQualifiedReceiverMember` hook has no
|
|
119
|
+
// access to call-site arity or argument types, so
|
|
120
|
+
// `narrowOverloadCandidates` cannot actually narrow here — the call
|
|
121
|
+
// with `(allHits, undefined, undefined)` is effectively a pass-through.
|
|
122
|
+
// We retain it so that `isOverloadAmbiguousAfterNormalization` can
|
|
123
|
+
// still detect int/long-style normalization collisions on this path,
|
|
124
|
+
// but for any multi-hit case where candidates have genuinely distinct
|
|
125
|
+
// signatures (e.g. `foo(int)` vs `foo(double)` in different inline
|
|
126
|
+
// children), we conservatively suppress rather than pick arbitrarily.
|
|
127
|
+
// A future enhancement could thread call-site argument info through
|
|
128
|
+
// the `resolveQualifiedReceiverMember` contract to enable real
|
|
129
|
+
// narrowing here.
|
|
130
|
+
const narrowed = narrowOverloadCandidates(allHits, undefined, undefined);
|
|
131
|
+
if (narrowed.length === 1)
|
|
132
|
+
return narrowed[0];
|
|
133
|
+
if (narrowed.length === 0)
|
|
134
|
+
return undefined;
|
|
135
|
+
if (isOverloadAmbiguousAfterNormalization(narrowed, undefined))
|
|
136
|
+
return 'ambiguous';
|
|
137
|
+
// Multiple surviving candidates (distinct signatures) — conservative
|
|
138
|
+
// suppress because we lack call-site info to disambiguate.
|
|
139
|
+
return 'ambiguous';
|
|
108
140
|
}
|
|
109
141
|
/** Recursively search a namespace scope and any inline-namespace
|
|
110
|
-
* descendants for
|
|
142
|
+
* descendants for callable defs with the given simple name. Non-inline
|
|
111
143
|
* nested namespaces are NOT traversed — they require explicit
|
|
112
|
-
* qualification (`outer::nested::foo`).
|
|
144
|
+
* qualification (`outer::nested::foo`). Returns ALL matches so the
|
|
145
|
+
* caller can detect same-name ambiguity across inline children (#1564). */
|
|
113
146
|
function findMemberInNamespaceTransitive(scope, scopesById, memberName) {
|
|
147
|
+
const results = [];
|
|
114
148
|
// Check this scope's own ownedDefs first.
|
|
115
149
|
for (const def of scope.ownedDefs) {
|
|
116
150
|
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
|
|
117
151
|
continue;
|
|
118
152
|
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
|
119
153
|
if (simple === memberName)
|
|
120
|
-
|
|
154
|
+
results.push(def);
|
|
121
155
|
}
|
|
122
156
|
// Descend into inline-namespace children.
|
|
123
157
|
for (const childScope of scopesById.values()) {
|
|
@@ -127,11 +161,11 @@ function findMemberInNamespaceTransitive(scope, scopesById, memberName) {
|
|
|
127
161
|
continue;
|
|
128
162
|
if (!inlineNamespaceScopeIds.has(childScope.id))
|
|
129
163
|
continue;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
164
|
+
const childHits = findMemberInNamespaceTransitive(childScope, scopesById, memberName);
|
|
165
|
+
for (const hit of childHits)
|
|
166
|
+
results.push(hit);
|
|
133
167
|
}
|
|
134
|
-
return
|
|
168
|
+
return results;
|
|
135
169
|
}
|
|
136
170
|
function findNamespaceDefInScope(scope) {
|
|
137
171
|
for (const def of scope.ownedDefs) {
|
|
@@ -205,7 +205,7 @@ export const cppScopeResolver = {
|
|
|
205
205
|
if (imp.localName !== site.name)
|
|
206
206
|
continue;
|
|
207
207
|
const member = resolveCppQualifiedNamespaceMember(imp.targetRaw, imp.importedName, parsedFiles, scopes);
|
|
208
|
-
if (member === undefined)
|
|
208
|
+
if (member === undefined || member === 'ambiguous')
|
|
209
209
|
continue;
|
|
210
210
|
if (seenUsing.has(member.nodeId))
|
|
211
211
|
continue;
|
|
@@ -542,10 +542,11 @@ export interface ScopeResolver {
|
|
|
542
542
|
*
|
|
543
543
|
* Receiver-bound-calls invokes this hook AFTER Case 1 (namespace
|
|
544
544
|
* imports) and AFTER Case 2 (class-name receiver) fail to resolve.
|
|
545
|
-
* Returns the target def,
|
|
546
|
-
*
|
|
545
|
+
* Returns the target def, `'ambiguous'` when multiple inline-namespace
|
|
546
|
+
* children declare the same name (suppresses edge emission), or
|
|
547
|
+
* `undefined` to fall through to the remaining cases.
|
|
547
548
|
*/
|
|
548
|
-
readonly resolveQualifiedReceiverMember?: (receiverName: string, memberName: string, callerScope: ScopeId, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition | undefined;
|
|
549
|
+
readonly resolveQualifiedReceiverMember?: (receiverName: string, memberName: string, callerScope: ScopeId, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition | 'ambiguous' | undefined;
|
|
549
550
|
/**
|
|
550
551
|
* Enable the receiver-bound Case 0.5 fallback for explicit `this`
|
|
551
552
|
* receivers (`this->m()` / `this.m()`) that resolves against the
|
|
@@ -324,6 +324,12 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
324
324
|
// class with the same simple name.
|
|
325
325
|
if (provider.resolveQualifiedReceiverMember !== undefined) {
|
|
326
326
|
const memberDef = provider.resolveQualifiedReceiverMember(receiverName, memberName, site.inScope, scopes, parsedFiles);
|
|
327
|
+
if (memberDef === 'ambiguous') {
|
|
328
|
+
// Same-name ambiguity across inline-namespace children (#1564):
|
|
329
|
+
// suppress edge emission, mark site handled.
|
|
330
|
+
handledSites.add(siteKey);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
327
333
|
if (memberDef !== undefined) {
|
|
328
334
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
|
|
329
335
|
if (ok)
|
|
@@ -55,8 +55,14 @@ export declare const computeChunkHash: (entries: Array<{
|
|
|
55
55
|
*/
|
|
56
56
|
export declare const loadParseCache: (storagePath: string) => Promise<ParseCache>;
|
|
57
57
|
/**
|
|
58
|
-
* Persist the cache to disk
|
|
59
|
-
*
|
|
58
|
+
* Persist the cache to disk using a temp directory + rename.
|
|
59
|
+
*
|
|
60
|
+
* Writes shards under `${cacheDir}.tmp`, then removes the old `cacheDir` and
|
|
61
|
+
* renames the temp directory into place. There is a crash window after
|
|
62
|
+
* `rm(cacheDir)` and before `rename(tmpDir, cacheDir)` where no cache exists;
|
|
63
|
+
* that is acceptable — `loadParseCache` yields empty and the next run
|
|
64
|
+
* reparses. This is not a single atomic swap of the whole tree, but avoids
|
|
65
|
+
* leaving a half-written shard set visible to readers.
|
|
60
66
|
*/
|
|
61
67
|
export declare const saveParseCache: (storagePath: string, cache: ParseCache) => Promise<void>;
|
|
62
68
|
/**
|
|
@@ -69,7 +69,12 @@ const GITNEXUS_PKG_VERSION = (() => {
|
|
|
69
69
|
return '0.0.0-unknown';
|
|
70
70
|
})();
|
|
71
71
|
export const PARSE_CACHE_VERSION = `${SCHEMA_BUMP}+${GITNEXUS_PKG_VERSION}`;
|
|
72
|
-
const
|
|
72
|
+
const LEGACY_CACHE_FILENAME = 'parse-cache.json';
|
|
73
|
+
const CACHE_DIRNAME = 'parse-cache';
|
|
74
|
+
const CACHE_INDEX_FILENAME = 'index.json';
|
|
75
|
+
/** Keys on disk always come from `computeChunkHash` — 64-char lowercase hex. */
|
|
76
|
+
const CHUNK_CACHE_KEY_HEX_RE = /^[a-f0-9]{64}$/;
|
|
77
|
+
const isValidChunkCacheKey = (chunkHash) => CHUNK_CACHE_KEY_HEX_RE.test(chunkHash);
|
|
73
78
|
/** SHA-256 hex of a single string or buffer. */
|
|
74
79
|
const sha256Hex = (input) => createHash('sha256')
|
|
75
80
|
.update(typeof input === 'string' ? Buffer.from(input) : input)
|
|
@@ -116,12 +121,12 @@ const mapReviver = (_key, value) => {
|
|
|
116
121
|
}
|
|
117
122
|
return value;
|
|
118
123
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const cachePath =
|
|
124
|
+
const getLegacyCachePath = (storagePath) => path.join(storagePath, LEGACY_CACHE_FILENAME);
|
|
125
|
+
const getCacheDirPath = (storagePath) => path.join(storagePath, CACHE_DIRNAME);
|
|
126
|
+
const getCacheIndexPath = (storagePath) => path.join(getCacheDirPath(storagePath), CACHE_INDEX_FILENAME);
|
|
127
|
+
const getCacheChunkPath = (storagePath, chunkHash) => path.join(getCacheDirPath(storagePath), `${chunkHash}.json`);
|
|
128
|
+
const loadLegacyParseCache = async (storagePath) => {
|
|
129
|
+
const cachePath = getLegacyCachePath(storagePath);
|
|
125
130
|
try {
|
|
126
131
|
const raw = await fs.readFile(cachePath, 'utf-8');
|
|
127
132
|
const data = JSON.parse(raw, mapReviver);
|
|
@@ -143,22 +148,88 @@ export const loadParseCache = async (storagePath) => {
|
|
|
143
148
|
return emptyCache();
|
|
144
149
|
}
|
|
145
150
|
};
|
|
151
|
+
const loadShardedParseCache = async (storagePath) => {
|
|
152
|
+
const indexPath = getCacheIndexPath(storagePath);
|
|
153
|
+
try {
|
|
154
|
+
const raw = await fs.readFile(indexPath, 'utf-8');
|
|
155
|
+
const data = JSON.parse(raw);
|
|
156
|
+
if (typeof data !== 'object' ||
|
|
157
|
+
data === null ||
|
|
158
|
+
data.version !== PARSE_CACHE_VERSION ||
|
|
159
|
+
!Array.isArray(data.keys)) {
|
|
160
|
+
return emptyCache();
|
|
161
|
+
}
|
|
162
|
+
const entries = new Map();
|
|
163
|
+
for (const chunkHash of data.keys) {
|
|
164
|
+
if (typeof chunkHash !== 'string' || !isValidChunkCacheKey(chunkHash))
|
|
165
|
+
continue;
|
|
166
|
+
try {
|
|
167
|
+
const chunkRaw = await fs.readFile(getCacheChunkPath(storagePath, chunkHash), 'utf-8');
|
|
168
|
+
const chunkData = JSON.parse(chunkRaw, mapReviver);
|
|
169
|
+
if (Array.isArray(chunkData))
|
|
170
|
+
entries.set(chunkHash, chunkData);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* skip corrupt or missing shard */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { version: PARSE_CACHE_VERSION, entries, usedKeys: new Set() };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Load the parse cache. Returns an empty cache on any failure (missing
|
|
184
|
+
* file, corrupt JSON, version mismatch). Never throws on a normal load.
|
|
185
|
+
*/
|
|
186
|
+
export const loadParseCache = async (storagePath) => {
|
|
187
|
+
const sharded = await loadShardedParseCache(storagePath);
|
|
188
|
+
if (sharded)
|
|
189
|
+
return sharded;
|
|
190
|
+
return loadLegacyParseCache(storagePath);
|
|
191
|
+
};
|
|
146
192
|
/**
|
|
147
|
-
* Persist the cache to disk
|
|
148
|
-
*
|
|
193
|
+
* Persist the cache to disk using a temp directory + rename.
|
|
194
|
+
*
|
|
195
|
+
* Writes shards under `${cacheDir}.tmp`, then removes the old `cacheDir` and
|
|
196
|
+
* renames the temp directory into place. There is a crash window after
|
|
197
|
+
* `rm(cacheDir)` and before `rename(tmpDir, cacheDir)` where no cache exists;
|
|
198
|
+
* that is acceptable — `loadParseCache` yields empty and the next run
|
|
199
|
+
* reparses. This is not a single atomic swap of the whole tree, but avoids
|
|
200
|
+
* leaving a half-written shard set visible to readers.
|
|
149
201
|
*/
|
|
150
202
|
export const saveParseCache = async (storagePath, cache) => {
|
|
151
203
|
await fs.mkdir(storagePath, { recursive: true });
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
204
|
+
const cacheDir = getCacheDirPath(storagePath);
|
|
205
|
+
const tmpDir = `${cacheDir}.tmp`;
|
|
206
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
207
|
+
await fs.mkdir(tmpDir, { recursive: true });
|
|
208
|
+
const keys = [];
|
|
209
|
+
for (const [chunkHash, chunkResults] of cache.entries) {
|
|
210
|
+
if (!isValidChunkCacheKey(chunkHash))
|
|
211
|
+
continue;
|
|
212
|
+
let payload;
|
|
213
|
+
try {
|
|
214
|
+
payload = JSON.stringify(chunkResults, mapReplacer);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Extremely dense chunks could theoretically exceed string limits; skip
|
|
218
|
+
// rather than failing the entire save (orchestrator catches save errors).
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
keys.push(chunkHash);
|
|
222
|
+
const chunkPath = path.join(tmpDir, `${chunkHash}.json`);
|
|
223
|
+
await fs.writeFile(chunkPath, payload, 'utf-8');
|
|
224
|
+
}
|
|
225
|
+
const index = {
|
|
155
226
|
version: cache.version,
|
|
156
|
-
|
|
227
|
+
keys,
|
|
157
228
|
};
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
await fs.
|
|
161
|
-
await fs.
|
|
229
|
+
await fs.writeFile(path.join(tmpDir, CACHE_INDEX_FILENAME), JSON.stringify(index), 'utf-8');
|
|
230
|
+
await fs.rm(cacheDir, { recursive: true, force: true });
|
|
231
|
+
await fs.rename(tmpDir, cacheDir);
|
|
232
|
+
await fs.rm(getLegacyCachePath(storagePath), { force: true });
|
|
162
233
|
};
|
|
163
234
|
/**
|
|
164
235
|
* Drop entries whose hashes are not in `usedHashes`. Called at the end
|
package/package.json
CHANGED