gitnexus 1.6.8-rc.11 → 1.6.8-rc.13
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/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/analyze-config.js +1 -0
- package/dist/cli/analyze.d.ts +6 -0
- package/dist/cli/analyze.js +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +51 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +100 -0
- package/dist/core/ingestion/cfg/collect.d.ts +30 -0
- package/dist/core/ingestion/cfg/collect.js +34 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +27 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +49 -0
- package/dist/core/ingestion/cfg/emit.d.ts +64 -0
- package/dist/core/ingestion/cfg/emit.js +95 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +66 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +49 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +492 -0
- package/dist/core/ingestion/language-provider.d.ts +11 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
- package/dist/core/ingestion/languages/cpp/captures.js +24 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/parsing-processor.js +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline.d.ts +23 -0
- package/dist/core/ingestion/scope-extractor.js +3 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +3 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +62 -0
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
- package/dist/core/ingestion/workers/parse-worker.js +43 -1
- package/dist/core/ingestion/workers/worker-pool.d.ts +9 -0
- package/dist/core/ingestion/workers/worker-pool.js +5 -2
- package/dist/core/run-analyze.d.ts +32 -0
- package/dist/core/run-analyze.js +75 -1
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +20 -10
- package/dist/storage/repo-manager.d.ts +31 -7
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
9
9
|
import { defineLanguage } from '../language-provider.js';
|
|
10
10
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
11
11
|
import { typescriptClassConfig, javascriptClassConfig, } from '../class-extractors/configs/typescript-javascript.js';
|
|
12
|
+
import { createTypeScriptCfgVisitor } from '../cfg/visitors/typescript.js';
|
|
12
13
|
import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js';
|
|
13
14
|
import { tsExportChecker } from '../export-detection.js';
|
|
14
15
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
@@ -293,6 +294,8 @@ export const typescriptProvider = defineLanguage({
|
|
|
293
294
|
// canonical capture vocabulary in ./typescript/query.ts
|
|
294
295
|
// (TYPESCRIPT_SCOPE_QUERY constant).
|
|
295
296
|
emitScopeCaptures: emitTsScopeCaptures,
|
|
297
|
+
// CFG/PDG substrate (#2081 M1) — runs in the worker on a --pdg run.
|
|
298
|
+
cfgVisitor: createTypeScriptCfgVisitor(),
|
|
296
299
|
interpretImport: interpretTsImport,
|
|
297
300
|
interpretTypeBinding: interpretTsTypeBinding,
|
|
298
301
|
bindingScopeFor: tsBindingScopeFor,
|
|
@@ -352,6 +355,8 @@ export const javascriptProvider = defineLanguage({
|
|
|
352
355
|
// JSDoc type bindings) live in ./javascript/captures.ts.
|
|
353
356
|
// See ./javascript/index.ts for the full per-module rationale.
|
|
354
357
|
emitScopeCaptures: emitJsScopeCaptures,
|
|
358
|
+
// CFG/PDG substrate (#2081 M1) — TS and JS share the same grammar family.
|
|
359
|
+
cfgVisitor: createTypeScriptCfgVisitor(),
|
|
355
360
|
interpretImport: interpretJsImport,
|
|
356
361
|
interpretTypeBinding: interpretJsTypeBinding,
|
|
357
362
|
bindingScopeFor: jsBindingScopeFor,
|
|
@@ -33,18 +33,12 @@ function findFunctionDeclarator(node) {
|
|
|
33
33
|
}
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
* These are not callable methods and should be suppressed from extraction.
|
|
39
|
-
* tree-sitter-cpp ^0.23.4 emits `delete_method_clause` / `default_method_clause`
|
|
40
|
-
* as named children of the function_definition node.
|
|
41
|
-
*/
|
|
42
|
-
function isDeletedOrDefaulted(node) {
|
|
36
|
+
/** Detect a C++ special member clause by its tree-sitter node type. */
|
|
37
|
+
function hasSpecialMethodClause(node, clauseType) {
|
|
43
38
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
44
39
|
const child = node.namedChild(i);
|
|
45
|
-
if (child?.type ===
|
|
40
|
+
if (child?.type === clauseType)
|
|
46
41
|
return true;
|
|
47
|
-
}
|
|
48
42
|
}
|
|
49
43
|
return false;
|
|
50
44
|
}
|
|
@@ -57,10 +51,6 @@ function extractCppMethodName(node) {
|
|
|
57
51
|
const funcDecl = findFunctionDeclarator(node);
|
|
58
52
|
if (!funcDecl)
|
|
59
53
|
return undefined;
|
|
60
|
-
// Suppress `= delete` and `= default` special members — these are not callable
|
|
61
|
-
// methods and should not appear in HAS_METHOD edges.
|
|
62
|
-
if (isDeletedOrDefaulted(node))
|
|
63
|
-
return undefined;
|
|
64
54
|
const nameNode = funcDecl.childForFieldName('declarator');
|
|
65
55
|
if (!nameNode)
|
|
66
56
|
return undefined;
|
|
@@ -360,6 +350,9 @@ export const cppMethodConfig = {
|
|
|
360
350
|
}
|
|
361
351
|
return false;
|
|
362
352
|
},
|
|
353
|
+
isDeleted(node) {
|
|
354
|
+
return hasSpecialMethodClause(node, 'delete_method_clause');
|
|
355
|
+
},
|
|
363
356
|
};
|
|
364
357
|
// ---------------------------------------------------------------------------
|
|
365
358
|
// C config (minimal — C has no classes/methods, only struct function pointers)
|
|
@@ -198,6 +198,7 @@ function buildMethod(node, ownerNode, context, config) {
|
|
|
198
198
|
...(config.isAsync?.(node) ? { isAsync: true } : {}),
|
|
199
199
|
...(config.isPartial?.(node) ? { isPartial: true } : {}),
|
|
200
200
|
...(config.isConst?.(node) ? { isConst: true } : {}),
|
|
201
|
+
...(config.isDeleted?.(node) ? { isDeleted: true } : {}),
|
|
201
202
|
annotations: config.extractAnnotations?.(node) ?? [],
|
|
202
203
|
sourceFile: context.filePath,
|
|
203
204
|
line: node.startPosition.row + 1,
|
|
@@ -27,6 +27,7 @@ export interface MethodInfo {
|
|
|
27
27
|
isAsync?: boolean;
|
|
28
28
|
isPartial?: boolean;
|
|
29
29
|
isConst?: boolean;
|
|
30
|
+
isDeleted?: boolean;
|
|
30
31
|
annotations: string[];
|
|
31
32
|
sourceFile: string;
|
|
32
33
|
line: number;
|
|
@@ -74,6 +75,7 @@ export interface MethodExtractionConfig {
|
|
|
74
75
|
isAsync?: (node: SyntaxNode) => boolean;
|
|
75
76
|
isPartial?: (node: SyntaxNode) => boolean;
|
|
76
77
|
isConst?: (node: SyntaxNode) => boolean;
|
|
78
|
+
isDeleted?: (node: SyntaxNode) => boolean;
|
|
77
79
|
/** Owner node types where member functions are effectively static (e.g.
|
|
78
80
|
* Ruby singleton_class, Kotlin companion_object / object_declaration).
|
|
79
81
|
* When the ownerNode matches one of these types, isStatic is forced true. */
|
|
@@ -136,6 +136,7 @@ export const createSymbolTable = () => {
|
|
|
136
136
|
? { templateArguments: metadata.templateArguments }
|
|
137
137
|
: {}),
|
|
138
138
|
...(metadata?.ownerId !== undefined ? { ownerId: metadata.ownerId } : {}),
|
|
139
|
+
...(metadata?.isDeleted === true ? { isDeleted: true } : {}),
|
|
139
140
|
};
|
|
140
141
|
// A. File Index — unconditional.
|
|
141
142
|
if (!fileIndex.has(filePath)) {
|
|
@@ -18,6 +18,7 @@ import { BindingAccumulator, enrichExportedTypeMap, } from '../binding-accumulat
|
|
|
18
18
|
import { mergeChunkResults, dispatchChunkParse } from '../parsing-processor.js';
|
|
19
19
|
import { fileContentHash, computeChunkHash, loadParseCacheChunk, persistParseCacheChunk, PARSE_CACHE_VERSION, } from '../../../storage/parse-cache.js';
|
|
20
20
|
import { clearParsedFileStore, persistParsedFileChunk, getDurableParsedFileDir, loadDurableParsedFileIndex, restoreDurableParsedFileShard, } from '../../../storage/parsedfile-store.js';
|
|
21
|
+
import { DEFAULT_PDG_MAX_FUNCTION_LINES } from '../cfg/collect.js';
|
|
21
22
|
import { processRoutesFromExtracted, buildExportedTypeMapFromGraph, } from '../call-processor.js';
|
|
22
23
|
import { createSemanticModel } from '../model/index.js';
|
|
23
24
|
import { getLanguageFromFilename, } from '../../../_shared/index.js';
|
|
@@ -319,6 +320,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
319
320
|
// Initialized below before the chunk loop (same deferred-init pattern
|
|
320
321
|
// as `parsedFileStorePath`); this closure only runs from the loop.
|
|
321
322
|
durableParsedFileStoragePath: durableParsedFileDir,
|
|
323
|
+
// CFG/PDG opt-in (#2081 M1) — baked into each worker's workerData so the
|
|
324
|
+
// worker builds + attaches cfgSideChannel. Off by default.
|
|
325
|
+
pdg: options?.pdg === true,
|
|
326
|
+
pdgMaxFunctionLines: options?.pdgMaxFunctionLines,
|
|
322
327
|
// Fan each chunk across the whole pool (#worker-idle): without this a
|
|
323
328
|
// chunk smaller than the 8 MB sub-batch cap became a single job on a
|
|
324
329
|
// single worker. Honors an explicit `subBatchMaxBytes` / env override.
|
|
@@ -566,7 +571,19 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
566
571
|
filePath: f.path,
|
|
567
572
|
contentHash: fileContentHash(f.content),
|
|
568
573
|
}));
|
|
569
|
-
chunkHash = computeChunkHash(entries
|
|
574
|
+
chunkHash = computeChunkHash(entries,
|
|
575
|
+
// Only worker-visible pdg config participates in the key —
|
|
576
|
+
// pdgMaxEdgesPerFunction is emit-time-only and deliberately
|
|
577
|
+
// excluded (see PdgCacheKey in parse-cache.ts; #2099 F3). The line
|
|
578
|
+
// cap is RESOLVED to the worker's default before folding so an
|
|
579
|
+
// explicit-default run shares the default run's keys (the worker
|
|
580
|
+
// output is byte-identical either way).
|
|
581
|
+
options?.pdg === true
|
|
582
|
+
? {
|
|
583
|
+
pdg: true,
|
|
584
|
+
maxFunctionLines: options?.pdgMaxFunctionLines ?? DEFAULT_PDG_MAX_FUNCTION_LINES,
|
|
585
|
+
}
|
|
586
|
+
: false);
|
|
570
587
|
}
|
|
571
588
|
const cachedRaw = chunkHash && parseCache ? await loadParseCacheChunk(parseCache, chunkHash) : undefined;
|
|
572
589
|
// Track every chunk hash we touched so the orchestrator can
|
|
@@ -25,6 +25,29 @@ export interface PipelineOptions {
|
|
|
25
25
|
* to retain those nodes under `skipGraphPhases`.
|
|
26
26
|
*/
|
|
27
27
|
skipGraphPhases?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Build the control-flow-graph / PDG substrate (#2081 M1, opt-in via `--pdg`).
|
|
30
|
+
* Off by default: workers skip all CFG work and emit no `cfgSideChannel`, and
|
|
31
|
+
* scope-resolution emits no BasicBlock nodes or CFG edges — so the default
|
|
32
|
+
* graph is byte-identical to a pre-#2081 run. Folded into the parse-cache key
|
|
33
|
+
* so a pdg-off warm cache is not reused on a `--pdg` run.
|
|
34
|
+
*/
|
|
35
|
+
pdg?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Per-function source-line cap for worker-side CFG construction.
|
|
38
|
+
* `undefined` ⇒ the worker applies `DEFAULT_PDG_MAX_FUNCTION_LINES`; `0` ⇒ no
|
|
39
|
+
* cap (unlimited). Bounds the cost of a pathological mega-function; over-cap
|
|
40
|
+
* functions are skipped (no CFG emitted for them). No CLI flag in M1 —
|
|
41
|
+
* programmatic / server analyze-worker path only.
|
|
42
|
+
*/
|
|
43
|
+
pdgMaxFunctionLines?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Per-function CFG edge cap for the scope-resolution emit step.
|
|
46
|
+
* `undefined` ⇒ `DEFAULT_MAX_CFG_EDGES_PER_FUNCTION`; `0` ⇒ no cap (unlimited).
|
|
47
|
+
* Over-cap functions stop at the cap and log a structured drop warning (no
|
|
48
|
+
* silent truncation). No CLI flag in M1 — programmatic / server path only.
|
|
49
|
+
*/
|
|
50
|
+
pdgMaxEdgesPerFunction?: number;
|
|
28
51
|
/**
|
|
29
52
|
* Request parsing with the worker pool disabled. The sequential parser was
|
|
30
53
|
* removed — the worker pool is the sole parse path — so setting this now
|
|
@@ -399,6 +399,7 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
|
|
|
399
399
|
const returnType = match['@declaration.return-type']?.text;
|
|
400
400
|
const templateConstraints = parseJsonCapture(match['@declaration.template-constraints']);
|
|
401
401
|
const isExplicit = parseBooleanCapture(match['@declaration.is-explicit']);
|
|
402
|
+
const isDeleted = parseBooleanCapture(match['@declaration.is-deleted']);
|
|
402
403
|
return {
|
|
403
404
|
nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
|
|
404
405
|
filePath,
|
|
@@ -413,6 +414,7 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
|
|
|
413
414
|
...(templateArguments !== undefined ? { templateArguments } : {}),
|
|
414
415
|
...(templateConstraints !== undefined ? { templateConstraints } : {}),
|
|
415
416
|
...(isExplicit === true ? { isExplicit: true } : {}),
|
|
417
|
+
...(isDeleted === true ? { isDeleted: true } : {}),
|
|
416
418
|
};
|
|
417
419
|
}
|
|
418
420
|
/** Parse an opaque JSON payload synthesized by per-language captures
|
|
@@ -922,6 +924,7 @@ const KNOWN_SUB_TAGS = new Set([
|
|
|
922
924
|
'@declaration.return-type',
|
|
923
925
|
'@declaration.template-constraints',
|
|
924
926
|
'@declaration.is-explicit',
|
|
927
|
+
'@declaration.is-deleted',
|
|
925
928
|
]);
|
|
926
929
|
/**
|
|
927
930
|
* Return the anchor capture for a match — the one whose name begins with
|
|
@@ -289,6 +289,18 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
289
289
|
}
|
|
290
290
|
if (fnDef === undefined)
|
|
291
291
|
continue;
|
|
292
|
+
if (fnDef.isDeleted === true) {
|
|
293
|
+
recordSuppressedOutcome(options.recordResolutionOutcome, {
|
|
294
|
+
phase: 'free-call-fallback',
|
|
295
|
+
filePath: parsed.filePath,
|
|
296
|
+
name: site.name,
|
|
297
|
+
range: site.atRange,
|
|
298
|
+
reason: 'selected-callable-deleted',
|
|
299
|
+
candidates: [fnDef],
|
|
300
|
+
});
|
|
301
|
+
handledSites.add(siteKey(parsed.filePath, site));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
292
304
|
if ((fnDefFromImplicitThis || fnDef.type === 'Method' || fnDef.type === 'Constructor') &&
|
|
293
305
|
options.isCallableVisibleFromCaller !== undefined &&
|
|
294
306
|
!options.isCallableVisibleFromCaller({
|
|
@@ -138,9 +138,12 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
138
138
|
return 0;
|
|
139
139
|
let n = 0;
|
|
140
140
|
for (const implDef of impls) {
|
|
141
|
-
const implMember =
|
|
142
|
-
if (implMember === undefined
|
|
141
|
+
const implMember = pickOverload(implDef.nodeId, memberName, site, model, provider);
|
|
142
|
+
if (implMember === undefined ||
|
|
143
|
+
implMember === OVERLOAD_AMBIGUOUS ||
|
|
144
|
+
implMember.isDeleted === true) {
|
|
143
145
|
continue;
|
|
146
|
+
}
|
|
144
147
|
if (implMember.nodeId === primaryMemberDef.nodeId)
|
|
145
148
|
continue;
|
|
146
149
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, implMember, 'interface-dispatch', seen, confidence, collapse);
|
|
@@ -179,12 +182,30 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
179
182
|
? extendsOnly(enclosingClass.nodeId)
|
|
180
183
|
: scopes.methodDispatch.mroFor(enclosingClass.nodeId);
|
|
181
184
|
let memberDef;
|
|
185
|
+
let ambiguousOwnerId;
|
|
182
186
|
for (const ownerId of ancestors) {
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
const picked = site.kind === 'call'
|
|
188
|
+
? pickOverload(ownerId, memberName, site, model, provider)
|
|
189
|
+
: findOwnedMember(ownerId, memberName, model);
|
|
190
|
+
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
191
|
+
ambiguousOwnerId = ownerId;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
if (picked !== undefined) {
|
|
195
|
+
memberDef = picked;
|
|
185
196
|
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (ambiguousOwnerId !== undefined) {
|
|
200
|
+
recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ambiguousOwnerId, memberName, model, provider);
|
|
201
|
+
handledSites.add(siteKey);
|
|
202
|
+
continue;
|
|
186
203
|
}
|
|
187
204
|
if (memberDef !== undefined) {
|
|
205
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
206
|
+
handledSites.add(siteKey);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
188
209
|
// Super/base calls resolve through the MRO chain, not
|
|
189
210
|
// through imports — the ancestor method is found by
|
|
190
211
|
// walking `methodDispatch.mroFor(enclosingClass)`, which
|
|
@@ -219,9 +240,9 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
219
240
|
if (currentClass !== undefined) {
|
|
220
241
|
const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)];
|
|
221
242
|
let memberDef;
|
|
243
|
+
let ambiguousOwnerId;
|
|
222
244
|
// Static-only filter (#1756 / U3): same shape as Case 4's
|
|
223
|
-
// chain walk (skip-and-walk-on)
|
|
224
|
-
// narrowing — Case 0 uses `findOwnedMember` directly. When
|
|
245
|
+
// overload-aware chain walk (skip-and-walk-on). When
|
|
225
246
|
// an owner's resolved candidate is static-only (Kotlin
|
|
226
247
|
// companion-promoted), continue to the next ancestor in
|
|
227
248
|
// the MRO chain so a legitimate instance member can bind.
|
|
@@ -233,17 +254,29 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
233
254
|
// shapes like `Logger.create("a")`), so there's no wrong
|
|
234
255
|
// target to suppress.
|
|
235
256
|
for (const ownerId of chain) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
257
|
+
const picked = site.kind === 'call'
|
|
258
|
+
? pickFirstNonStaticOnly(ownerId, memberName, site, model, provider)
|
|
259
|
+
: findOwnedMember(ownerId, memberName, model);
|
|
260
|
+
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
261
|
+
ambiguousOwnerId = ownerId;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
if (picked === STATIC_ONLY_FILTERED || picked === undefined) {
|
|
241
265
|
continue;
|
|
242
266
|
}
|
|
243
|
-
memberDef =
|
|
267
|
+
memberDef = picked;
|
|
244
268
|
break;
|
|
245
269
|
}
|
|
270
|
+
if (ambiguousOwnerId !== undefined) {
|
|
271
|
+
recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ambiguousOwnerId, memberName, model, provider);
|
|
272
|
+
handledSites.add(siteKey);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
246
275
|
if (memberDef !== undefined) {
|
|
276
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
277
|
+
handledSites.add(siteKey);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
247
280
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
|
|
248
281
|
if (ok)
|
|
249
282
|
emitted++;
|
|
@@ -290,6 +323,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
290
323
|
}
|
|
291
324
|
if (languageResolution?.kind === 'resolved') {
|
|
292
325
|
const memberDef = languageResolution.definition;
|
|
326
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
327
|
+
handledSites.add(siteKey);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
293
330
|
const reason = site.kind === 'write' || site.kind === 'read'
|
|
294
331
|
? site.kind
|
|
295
332
|
: memberDef.filePath !== parsed.filePath
|
|
@@ -357,6 +394,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
357
394
|
continue;
|
|
358
395
|
}
|
|
359
396
|
if (memberDef !== undefined) {
|
|
397
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
398
|
+
handledSites.add(siteKey);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
360
401
|
const reason = site.kind === 'write' || site.kind === 'read'
|
|
361
402
|
? site.kind
|
|
362
403
|
: memberDef.filePath !== parsed.filePath
|
|
@@ -373,11 +414,16 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
373
414
|
}
|
|
374
415
|
// ── Case 1: namespace receiver ───────────────────────────────
|
|
375
416
|
const targetFiles = namespaceTargets.get(receiverName);
|
|
376
|
-
if (targetFiles !== undefined) {
|
|
417
|
+
if (targetFiles !== undefined && provider.resolveQualifiedReceiverMember === undefined) {
|
|
377
418
|
let found = false;
|
|
378
419
|
for (const targetFile of targetFiles) {
|
|
379
420
|
const memberDef = findExportedDef(targetFile, memberName, index);
|
|
380
421
|
if (memberDef !== undefined) {
|
|
422
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
423
|
+
handledSites.add(siteKey);
|
|
424
|
+
found = true;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
381
427
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
|
|
382
428
|
if (ok)
|
|
383
429
|
emitted++;
|
|
@@ -413,6 +459,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
413
459
|
continue;
|
|
414
460
|
}
|
|
415
461
|
if (memberDef !== undefined) {
|
|
462
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
463
|
+
handledSites.add(siteKey);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
416
466
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
|
|
417
467
|
if (ok)
|
|
418
468
|
emitted++;
|
|
@@ -425,9 +475,17 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
425
475
|
if (classDef !== undefined) {
|
|
426
476
|
const chain = [classDef.nodeId, ...scopes.methodDispatch.mroFor(classDef.nodeId)];
|
|
427
477
|
let memberDef;
|
|
478
|
+
let ambiguousOwnerId;
|
|
428
479
|
for (const ownerId of chain) {
|
|
429
|
-
|
|
430
|
-
|
|
480
|
+
const picked = site.kind === 'call'
|
|
481
|
+
? pickOverload(ownerId, memberName, site, model, provider)
|
|
482
|
+
: findOwnedMember(ownerId, memberName, model);
|
|
483
|
+
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
484
|
+
ambiguousOwnerId = ownerId;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
if (picked !== undefined) {
|
|
488
|
+
memberDef = picked;
|
|
431
489
|
// The MRO chain is most-derived-first ([classDef, ...ancestors]).
|
|
432
490
|
// If the most-derived definition is arity-incompatible with the
|
|
433
491
|
// call site, PHP throws ArgumentCountError at runtime — it does
|
|
@@ -441,7 +499,16 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
441
499
|
break;
|
|
442
500
|
}
|
|
443
501
|
}
|
|
502
|
+
if (ambiguousOwnerId !== undefined) {
|
|
503
|
+
recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ambiguousOwnerId, memberName, model, provider);
|
|
504
|
+
handledSites.add(siteKey);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
444
507
|
if (memberDef !== undefined) {
|
|
508
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
509
|
+
handledSites.add(siteKey);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
445
512
|
const reason = site.kind === 'write' || site.kind === 'read'
|
|
446
513
|
? site.kind
|
|
447
514
|
: memberDef.filePath !== parsed.filePath
|
|
@@ -466,8 +533,22 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
466
533
|
for (const targetFile3 of targetFiles3) {
|
|
467
534
|
const classDef3 = findExportedDef(targetFile3, className, index);
|
|
468
535
|
if (classDef3 !== undefined) {
|
|
469
|
-
const
|
|
470
|
-
|
|
536
|
+
const picked = site.kind === 'call'
|
|
537
|
+
? pickOverload(classDef3.nodeId, memberName, site, model, provider)
|
|
538
|
+
: findOwnedMember(classDef3.nodeId, memberName, model);
|
|
539
|
+
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
540
|
+
recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, classDef3.nodeId, memberName, model, provider);
|
|
541
|
+
handledSites.add(siteKey);
|
|
542
|
+
found3 = true;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
if (picked !== undefined) {
|
|
546
|
+
const memberDef = picked;
|
|
547
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
548
|
+
handledSites.add(siteKey);
|
|
549
|
+
found3 = true;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
471
552
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen);
|
|
472
553
|
if (ok) {
|
|
473
554
|
emitted++;
|
|
@@ -504,8 +585,9 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
504
585
|
if (ownerDef !== undefined) {
|
|
505
586
|
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
|
506
587
|
let memberDef;
|
|
507
|
-
|
|
508
|
-
//
|
|
588
|
+
let ambiguousOwnerId;
|
|
589
|
+
// Static-only filter (#1756 / U3): mirrors Case 0's
|
|
590
|
+
// overload-aware chain walk. When
|
|
509
591
|
// a static-only candidate is found at an ancestor, walk on
|
|
510
592
|
// so a legitimate instance member can bind. If the entire
|
|
511
593
|
// chain is static-only, no edge is emitted (Case 3b is fed
|
|
@@ -513,16 +595,29 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
513
595
|
// `emitReferencesViaLookup` for compound shapes, so no
|
|
514
596
|
// handled-site marker is needed for chain-only-static).
|
|
515
597
|
for (const ownerId of chain) {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (
|
|
598
|
+
const picked = site.kind === 'call'
|
|
599
|
+
? pickFirstNonStaticOnly(ownerId, memberName, site, model, provider)
|
|
600
|
+
: findOwnedMember(ownerId, memberName, model);
|
|
601
|
+
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
602
|
+
ambiguousOwnerId = ownerId;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
if (picked === STATIC_ONLY_FILTERED || picked === undefined) {
|
|
520
606
|
continue;
|
|
521
607
|
}
|
|
522
|
-
memberDef =
|
|
608
|
+
memberDef = picked;
|
|
523
609
|
break;
|
|
524
610
|
}
|
|
611
|
+
if (ambiguousOwnerId !== undefined) {
|
|
612
|
+
recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ambiguousOwnerId, memberName, model, provider);
|
|
613
|
+
handledSites.add(siteKey);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
525
616
|
if (memberDef !== undefined) {
|
|
617
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
618
|
+
handledSites.add(siteKey);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
526
621
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
|
|
527
622
|
if (ok)
|
|
528
623
|
emitted++;
|
|
@@ -574,6 +669,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
574
669
|
}
|
|
575
670
|
if (languageResolution?.kind === 'resolved') {
|
|
576
671
|
const memberDef = languageResolution.definition;
|
|
672
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
673
|
+
handledSites.add(siteKey);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
577
676
|
const reason = site.kind === 'write' || site.kind === 'read'
|
|
578
677
|
? site.kind
|
|
579
678
|
: memberDef.filePath !== parsed.filePath
|
|
@@ -657,6 +756,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
657
756
|
continue;
|
|
658
757
|
}
|
|
659
758
|
if (memberDef !== undefined) {
|
|
759
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, memberDef)) {
|
|
760
|
+
handledSites.add(siteKey);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
660
763
|
// For read/write ACCESSES, mirror the legacy DAG's reason
|
|
661
764
|
// convention so consumers asserting `reason === 'write'`
|
|
662
765
|
// keep working.
|
|
@@ -713,6 +816,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
713
816
|
continue;
|
|
714
817
|
}
|
|
715
818
|
if (picked !== undefined) {
|
|
819
|
+
if (suppressDeletedCallTarget(options.recordResolutionOutcome, parsed.filePath, site, picked)) {
|
|
820
|
+
handledSites.add(siteKey);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
716
823
|
// Static-only filter (#1756 / U3): unlike Case 4 there's no
|
|
717
824
|
// MRO chain to walk here — Case 5 dispatches on a single
|
|
718
825
|
// owner via `pickOverload`. When the picked candidate is
|
|
@@ -873,6 +980,20 @@ function pickFirstNonStaticOnly(ownerId, memberName, site, model, provider) {
|
|
|
873
980
|
return OVERLOAD_AMBIGUOUS;
|
|
874
981
|
return candidates[0] ?? overloads[0];
|
|
875
982
|
}
|
|
983
|
+
function suppressDeletedCallTarget(record, filePath, site, target) {
|
|
984
|
+
if (site.kind !== 'call' || target.isDeleted !== true)
|
|
985
|
+
return false;
|
|
986
|
+
record?.({
|
|
987
|
+
kind: 'suppressed',
|
|
988
|
+
phase: 'receiver-bound-calls',
|
|
989
|
+
filePath,
|
|
990
|
+
name: site.name,
|
|
991
|
+
range: site.atRange,
|
|
992
|
+
reason: 'selected-callable-deleted',
|
|
993
|
+
candidateIds: [target.nodeId],
|
|
994
|
+
});
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
876
997
|
function recordReceiverOverloadSuppression(record, filePath, site, ownerId, memberName, model, provider) {
|
|
877
998
|
if (record === undefined)
|
|
878
999
|
return;
|
|
@@ -285,6 +285,9 @@ export const scopeResolutionPhase = {
|
|
|
285
285
|
prebuiltNodeLookup: sharedNodeLookup,
|
|
286
286
|
preExtractedParsedFiles: preExtractedByPath,
|
|
287
287
|
scopeIndexStorePath: parsedFileStorePath,
|
|
288
|
+
// CFG/PDG emission (#2081 M1) — opt-in; off ⇒ byte-identical graph.
|
|
289
|
+
pdg: ctx.options?.pdg === true,
|
|
290
|
+
pdgMaxEdgesPerFunction: ctx.options?.pdgMaxEdgesPerFunction,
|
|
288
291
|
recordResolutionOutcome: (outcome) => {
|
|
289
292
|
resolutionOutcomes.push(outcome);
|
|
290
293
|
},
|
|
@@ -64,14 +64,39 @@ export function reconcileOwnership(parsedFiles, model) {
|
|
|
64
64
|
for (const parsed of parsedFiles) {
|
|
65
65
|
for (const def of parsed.localDefs) {
|
|
66
66
|
const ownerId = def.ownerId;
|
|
67
|
-
if (ownerId === undefined)
|
|
68
|
-
continue;
|
|
69
67
|
const simple = simpleQualifiedName(def);
|
|
70
68
|
if (simple === undefined)
|
|
71
69
|
continue;
|
|
72
70
|
if (def.type === 'Method' || def.type === 'Function' || def.type === 'Constructor') {
|
|
71
|
+
if (ownerId === undefined) {
|
|
72
|
+
if (def.isDeleted !== true)
|
|
73
|
+
continue;
|
|
74
|
+
const existingDef = model.symbols
|
|
75
|
+
.lookupExactAll(def.filePath, simple)
|
|
76
|
+
.find((candidate) => callableSignatureMatches(candidate, def));
|
|
77
|
+
if (existingDef !== undefined) {
|
|
78
|
+
existingDef.isDeleted = true;
|
|
79
|
+
skippedAlreadyPresent++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
model.symbols.add(def.filePath, simple, def.nodeId, def.type, {
|
|
83
|
+
parameterCount: def.parameterCount,
|
|
84
|
+
requiredParameterCount: def.requiredParameterCount,
|
|
85
|
+
parameterTypes: def.parameterTypes,
|
|
86
|
+
parameterTypeClasses: def.parameterTypeClasses,
|
|
87
|
+
returnType: def.returnType,
|
|
88
|
+
qualifiedName: def.qualifiedName,
|
|
89
|
+
isDeleted: true,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
73
93
|
const existing = model.methods.lookupAllByOwner(ownerId, simple);
|
|
74
|
-
|
|
94
|
+
const existingDef = existing.find((candidate) => candidate.nodeId === def.nodeId ||
|
|
95
|
+
(def.isDeleted === true && callableSignatureMatches(candidate, def)));
|
|
96
|
+
if (existingDef !== undefined) {
|
|
97
|
+
if (def.isDeleted === true) {
|
|
98
|
+
existingDef.isDeleted = true;
|
|
99
|
+
}
|
|
75
100
|
skippedAlreadyPresent++;
|
|
76
101
|
continue;
|
|
77
102
|
}
|
|
@@ -103,6 +128,21 @@ export function reconcileOwnership(parsedFiles, model) {
|
|
|
103
128
|
}
|
|
104
129
|
return { methodsRegistered, fieldsRegistered, nestedTypesRegistered, skippedAlreadyPresent };
|
|
105
130
|
}
|
|
131
|
+
function callableSignatureMatches(left, right) {
|
|
132
|
+
if (left.filePath !== right.filePath)
|
|
133
|
+
return false;
|
|
134
|
+
if (left.parameterCount !== right.parameterCount)
|
|
135
|
+
return false;
|
|
136
|
+
if (left.requiredParameterCount !== right.requiredParameterCount)
|
|
137
|
+
return false;
|
|
138
|
+
const leftTypes = left.parameterTypes;
|
|
139
|
+
const rightTypes = right.parameterTypes;
|
|
140
|
+
if (leftTypes === undefined || rightTypes === undefined) {
|
|
141
|
+
return leftTypes === rightTypes;
|
|
142
|
+
}
|
|
143
|
+
return (leftTypes.length === rightTypes.length &&
|
|
144
|
+
leftTypes.every((parameterType, index) => parameterType === rightTypes[index]));
|
|
145
|
+
}
|
|
106
146
|
/**
|
|
107
147
|
* Debug-mode parity validator. Runs only when
|
|
108
148
|
* `VALIDATE_SEMANTIC_MODEL !== '0'` AND `NODE_ENV !== 'production'`.
|
|
@@ -58,6 +58,16 @@ interface RunScopeResolutionInput {
|
|
|
58
58
|
readonly treeCache?: {
|
|
59
59
|
get(filePath: string): unknown;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* CFG/PDG opt-in (#2081 M1). When true, emit BasicBlock nodes + CFG edges
|
|
63
|
+
* from each ParsedFile's worker-built `cfgSideChannel` during Phase-4 graph
|
|
64
|
+
* emission (while the disk store is still live). Default/false ⇒ no CFG
|
|
65
|
+
* nodes or edges and a byte-identical graph.
|
|
66
|
+
*/
|
|
67
|
+
readonly pdg?: boolean;
|
|
68
|
+
/** Per-function CFG edge cap. `undefined` ⇒ {@link DEFAULT_MAX_CFG_EDGES_PER_FUNCTION};
|
|
69
|
+
* `0` ⇒ no cap (unlimited). */
|
|
70
|
+
readonly pdgMaxEdgesPerFunction?: number;
|
|
61
71
|
/**
|
|
62
72
|
* Optional graph-node lookup built ONCE by the caller and shared across
|
|
63
73
|
* every language pass. `buildGraphNodeLookup` scans the whole graph and is
|