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.
Files changed (54) hide show
  1. package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
  2. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  3. package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
  4. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  5. package/dist/cli/analyze-config.js +1 -0
  6. package/dist/cli/analyze.d.ts +6 -0
  7. package/dist/cli/analyze.js +2 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/core/ingestion/cfg/cfg-builder.d.ts +51 -0
  10. package/dist/core/ingestion/cfg/cfg-builder.js +100 -0
  11. package/dist/core/ingestion/cfg/collect.d.ts +30 -0
  12. package/dist/core/ingestion/cfg/collect.js +34 -0
  13. package/dist/core/ingestion/cfg/control-flow-context.d.ts +27 -0
  14. package/dist/core/ingestion/cfg/control-flow-context.js +49 -0
  15. package/dist/core/ingestion/cfg/emit.d.ts +64 -0
  16. package/dist/core/ingestion/cfg/emit.js +95 -0
  17. package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
  18. package/dist/core/ingestion/cfg/traversal-result.js +2 -0
  19. package/dist/core/ingestion/cfg/types.d.ts +66 -0
  20. package/dist/core/ingestion/cfg/types.js +13 -0
  21. package/dist/core/ingestion/cfg/visitors/typescript.d.ts +49 -0
  22. package/dist/core/ingestion/cfg/visitors/typescript.js +492 -0
  23. package/dist/core/ingestion/language-provider.d.ts +11 -0
  24. package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
  25. package/dist/core/ingestion/languages/cpp/captures.js +24 -1
  26. package/dist/core/ingestion/languages/cpp/query.js +23 -0
  27. package/dist/core/ingestion/languages/typescript.js +5 -0
  28. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
  29. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  30. package/dist/core/ingestion/method-types.d.ts +2 -0
  31. package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
  32. package/dist/core/ingestion/model/symbol-table.js +1 -0
  33. package/dist/core/ingestion/parsing-processor.js +1 -0
  34. package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
  35. package/dist/core/ingestion/pipeline.d.ts +23 -0
  36. package/dist/core/ingestion/scope-extractor.js +3 -0
  37. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
  38. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
  39. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +3 -0
  40. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
  41. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
  42. package/dist/core/ingestion/scope-resolution/pipeline/run.js +62 -0
  43. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  44. package/dist/core/ingestion/utils/method-props.js +1 -0
  45. package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
  46. package/dist/core/ingestion/workers/parse-worker.js +43 -1
  47. package/dist/core/ingestion/workers/worker-pool.d.ts +9 -0
  48. package/dist/core/ingestion/workers/worker-pool.js +5 -2
  49. package/dist/core/run-analyze.d.ts +32 -0
  50. package/dist/core/run-analyze.js +75 -1
  51. package/dist/storage/parse-cache.d.ts +22 -1
  52. package/dist/storage/parse-cache.js +20 -10
  53. package/dist/storage/repo-manager.d.ts +31 -7
  54. 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
- * Detect `= delete` and `= default` special member function declarations.
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 === 'delete_method_clause' || child?.type === 'default_method_clause') {
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. */
@@ -102,6 +102,7 @@ export interface AddMetadata {
102
102
  templateArguments?: string[];
103
103
  ownerId?: string;
104
104
  qualifiedName?: string;
105
+ isDeleted?: boolean;
105
106
  }
106
107
  /**
107
108
  * Pure read-only view over the file and callable indexes. Does NOT
@@ -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)) {
@@ -49,6 +49,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
49
49
  templateArguments: sym.templateArguments,
50
50
  ownerId: sym.ownerId,
51
51
  qualifiedName: sym.qualifiedName,
52
+ isDeleted: sym.isDeleted,
52
53
  });
53
54
  }
54
55
  if (exportedTypeMap) {
@@ -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 = findOwnedMember(implDef.nodeId, memberName, model);
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
- memberDef = findOwnedMember(ownerId, memberName, model);
184
- if (memberDef !== undefined)
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) but without overload
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 candidate = findOwnedMember(ownerId, memberName, model);
237
- if (candidate === undefined)
238
- continue;
239
- if (provider.isStaticOnly?.(candidate) === true) {
240
- // Skip static-only candidate; walk to next ancestor.
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 = candidate;
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
- memberDef = findOwnedMember(ownerId, memberName, model);
430
- if (memberDef !== undefined) {
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 memberDef = findOwnedMember(classDef3.nodeId, memberName, model);
470
- if (memberDef !== undefined) {
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
- // Static-only filter (#1756 / U3): mirrors Case 0's chain
508
- // walk `findOwnedMember` without overload narrowing. When
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 candidate = findOwnedMember(ownerId, memberName, model);
517
- if (candidate === undefined)
518
- continue;
519
- if (provider.isStaticOnly?.(candidate) === true) {
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 = candidate;
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
- if (existing.some((e) => e.nodeId === def.nodeId)) {
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