gitnexus 1.6.5 → 1.6.6-rc.2
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/index.d.ts +2 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/context.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +20 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/core/ingestion/language-provider.d.ts +29 -0
- package/dist/core/ingestion/languages/c-cpp.js +46 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.d.ts +17 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +51 -2
- package/dist/core/ingestion/languages/cpp/arity.d.ts +4 -1
- package/dist/core/ingestion/languages/cpp/arity.js +4 -1
- package/dist/core/ingestion/languages/cpp/captures.js +73 -0
- package/dist/core/ingestion/languages/cpp/constraint-extractor.d.ts +73 -0
- package/dist/core/ingestion/languages/cpp/constraint-extractor.js +308 -0
- package/dist/core/ingestion/languages/cpp/constraint-filter.d.ts +31 -0
- package/dist/core/ingestion/languages/cpp/constraint-filter.js +135 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +6 -0
- package/dist/core/ingestion/languages/cpp/type-classifier.d.ts +26 -0
- package/dist/core/ingestion/languages/cpp/type-classifier.js +49 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +2 -1
- package/dist/core/ingestion/model/symbol-table.js +3 -0
- package/dist/core/ingestion/parsing-processor.js +35 -2
- package/dist/core/ingestion/scope-extractor.js +63 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +21 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +1 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +14 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +12 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +11 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +50 -21
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +30 -7
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +49 -18
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +10 -4
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +1 -0
- package/dist/core/ingestion/utils/template-arguments.d.ts +19 -0
- package/dist/core/ingestion/utils/template-arguments.js +30 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +2 -1
- package/dist/core/ingestion/workers/parse-worker.js +1 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, getLabelFromCapt
|
|
|
11
11
|
import { detectFrameworkFromAST } from './framework-detection.js';
|
|
12
12
|
import { buildTypeEnv } from './type-env.js';
|
|
13
13
|
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from './utils/method-props.js';
|
|
14
|
-
import { extractTemplateArguments, templateArgumentsIdTag } from './utils/template-arguments.js';
|
|
14
|
+
import { extractTemplateArguments, templateArgumentsIdTag, templateConstraintsIdTag, } from './utils/template-arguments.js';
|
|
15
15
|
import { logger } from '../logger.js';
|
|
16
16
|
import { getTreeSitterBufferSize, getTreeSitterContentByteLength, TREE_SITTER_MAX_BUFFER, } from './constants.js';
|
|
17
17
|
// ============================================================================
|
|
@@ -57,6 +57,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults) => {
|
|
|
57
57
|
parameterCount: sym.parameterCount,
|
|
58
58
|
requiredParameterCount: sym.requiredParameterCount,
|
|
59
59
|
parameterTypes: sym.parameterTypes,
|
|
60
|
+
parameterTypeClasses: sym.parameterTypeClasses,
|
|
60
61
|
returnType: sym.returnType,
|
|
61
62
|
declaredType: sym.declaredType,
|
|
62
63
|
templateArguments: sym.templateArguments,
|
|
@@ -499,7 +500,35 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
499
500
|
classTemplateArguments.length > 0
|
|
500
501
|
? templateArgumentsIdTag(classTemplateArguments)
|
|
501
502
|
: '';
|
|
502
|
-
|
|
503
|
+
// SFINAE / `requires`-clause aware ID disambiguation (issue #1579).
|
|
504
|
+
// Function-template overloads with identical parameterTypes but
|
|
505
|
+
// mutually-exclusive constraints (e.g. `enable_if_t<is_integral_v<T>>`
|
|
506
|
+
// vs `enable_if_t<is_floating_point_v<T>>`) need distinct graph
|
|
507
|
+
// nodes so the constraint-filter step in `narrowOverloadCandidates`
|
|
508
|
+
// has two candidates to narrow between. Without this tag they
|
|
509
|
+
// collapse to a single Function node and the SFINAE call resolves
|
|
510
|
+
// to only one edge regardless of which overload's constraint holds.
|
|
511
|
+
// The provider hook is the right invocation point — parsing-processor
|
|
512
|
+
// sees raw tree-sitter matches without the `@`-prefixed synthetic
|
|
513
|
+
// captures `scope-extractor` consumes, so we delegate extraction to
|
|
514
|
+
// the language adapter (C++ implements this; other languages opt out).
|
|
515
|
+
let parsedTemplateConstraints = undefined;
|
|
516
|
+
let constraintsTag = '';
|
|
517
|
+
if ((nodeLabel === 'Function' || nodeLabel === 'Method') &&
|
|
518
|
+
provider.extractTemplateConstraints !== undefined &&
|
|
519
|
+
definitionNode !== null) {
|
|
520
|
+
try {
|
|
521
|
+
parsedTemplateConstraints = provider.extractTemplateConstraints(definitionNode);
|
|
522
|
+
if (parsedTemplateConstraints !== undefined) {
|
|
523
|
+
constraintsTag = templateConstraintsIdTag(parsedTemplateConstraints);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
parsedTemplateConstraints = undefined;
|
|
528
|
+
constraintsTag = '';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}`);
|
|
503
532
|
const classNodeForSymbol = definitionNodeForRange || definitionNode || nameNode;
|
|
504
533
|
const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
|
|
505
534
|
(classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
|
|
@@ -528,6 +557,9 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
528
557
|
...(classTemplateArguments !== undefined && classTemplateArguments.length > 0
|
|
529
558
|
? { templateArguments: classTemplateArguments }
|
|
530
559
|
: {}),
|
|
560
|
+
...(parsedTemplateConstraints !== undefined
|
|
561
|
+
? { templateConstraints: parsedTemplateConstraints }
|
|
562
|
+
: {}),
|
|
531
563
|
...(frameworkHint
|
|
532
564
|
? {
|
|
533
565
|
astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
|
|
@@ -579,6 +611,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
579
611
|
parameterCount: methodProps.parameterCount,
|
|
580
612
|
requiredParameterCount: methodProps.requiredParameterCount,
|
|
581
613
|
parameterTypes: methodProps.parameterTypes,
|
|
614
|
+
parameterTypeClasses: methodProps.parameterTypeClasses,
|
|
582
615
|
returnType: methodProps.returnType,
|
|
583
616
|
declaredType,
|
|
584
617
|
templateArguments: classTemplateArguments,
|
|
@@ -372,8 +372,10 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
|
|
|
372
372
|
const parameterCount = parseIntCapture(match['@declaration.parameter-count']);
|
|
373
373
|
const requiredParameterCount = parseIntCapture(match['@declaration.required-parameter-count']);
|
|
374
374
|
const parameterTypes = parseJsonStringArrayCapture(match['@declaration.parameter-types']);
|
|
375
|
+
const parameterTypeClasses = parseJsonParameterTypeClassesCapture(match['@declaration.parameter-type-classes']);
|
|
375
376
|
const declaredType = match['@declaration.field-type']?.text;
|
|
376
377
|
const returnType = match['@declaration.return-type']?.text;
|
|
378
|
+
const templateConstraints = parseJsonCapture(match['@declaration.template-constraints']);
|
|
377
379
|
return {
|
|
378
380
|
nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
|
|
379
381
|
filePath,
|
|
@@ -382,17 +384,77 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
|
|
|
382
384
|
...(parameterCount !== undefined ? { parameterCount } : {}),
|
|
383
385
|
...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
|
|
384
386
|
...(parameterTypes !== undefined ? { parameterTypes } : {}),
|
|
387
|
+
...(parameterTypeClasses !== undefined ? { parameterTypeClasses } : {}),
|
|
385
388
|
...(declaredType !== undefined ? { declaredType } : {}),
|
|
386
389
|
...(returnType !== undefined ? { returnType } : {}),
|
|
387
390
|
...(templateArguments !== undefined ? { templateArguments } : {}),
|
|
391
|
+
...(templateConstraints !== undefined ? { templateConstraints } : {}),
|
|
388
392
|
};
|
|
389
393
|
}
|
|
394
|
+
/** Parse an opaque JSON payload synthesized by per-language captures
|
|
395
|
+
* (e.g. C++ `@declaration.template-constraints`). Producer owns the
|
|
396
|
+
* shape; shared code threads it through as `unknown` per the
|
|
397
|
+
* `SymbolDefinition.templateConstraints` contract. */
|
|
398
|
+
function parseJsonCapture(cap) {
|
|
399
|
+
if (cap === undefined)
|
|
400
|
+
return undefined;
|
|
401
|
+
try {
|
|
402
|
+
return JSON.parse(cap.text);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
390
408
|
function parseIntCapture(cap) {
|
|
391
409
|
if (cap === undefined)
|
|
392
410
|
return undefined;
|
|
393
411
|
const n = Number.parseInt(cap.text, 10);
|
|
394
412
|
return Number.isFinite(n) ? n : undefined;
|
|
395
413
|
}
|
|
414
|
+
function parseJsonParameterTypeClassesCapture(cap) {
|
|
415
|
+
if (cap === undefined)
|
|
416
|
+
return undefined;
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(cap.text);
|
|
419
|
+
if (!Array.isArray(parsed))
|
|
420
|
+
return undefined;
|
|
421
|
+
const out = [];
|
|
422
|
+
for (const item of parsed) {
|
|
423
|
+
if (item === null || typeof item !== 'object')
|
|
424
|
+
return undefined;
|
|
425
|
+
const o = item;
|
|
426
|
+
if (typeof o.base !== 'string')
|
|
427
|
+
return undefined;
|
|
428
|
+
if (o.cv !== 'none' &&
|
|
429
|
+
o.cv !== 'const' &&
|
|
430
|
+
o.cv !== 'volatile' &&
|
|
431
|
+
o.cv !== 'const volatile' &&
|
|
432
|
+
o.cv !== 'unknown') {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
if (o.indirection !== 'value' &&
|
|
436
|
+
o.indirection !== 'lvalue-ref' &&
|
|
437
|
+
o.indirection !== 'rvalue-ref' &&
|
|
438
|
+
o.indirection !== 'pointer' &&
|
|
439
|
+
o.indirection !== 'unknown') {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
if (typeof o.pointerDepth !== 'number' || !Number.isFinite(o.pointerDepth)) {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
out.push({
|
|
446
|
+
base: o.base,
|
|
447
|
+
cv: o.cv,
|
|
448
|
+
indirection: o.indirection,
|
|
449
|
+
pointerDepth: o.pointerDepth,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
396
458
|
function parseJsonStringArrayCapture(cap) {
|
|
397
459
|
if (cap === undefined)
|
|
398
460
|
return undefined;
|
|
@@ -747,6 +809,7 @@ const KNOWN_SUB_TAGS = new Set([
|
|
|
747
809
|
'@declaration.parameter-count',
|
|
748
810
|
'@declaration.required-parameter-count',
|
|
749
811
|
'@declaration.parameter-types',
|
|
812
|
+
'@declaration.template-constraints',
|
|
750
813
|
]);
|
|
751
814
|
/**
|
|
752
815
|
* Return the anchor capture for a match — the one whose name begins with
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
* Plan that introduced most of these invariants:
|
|
251
251
|
* `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
|
|
252
252
|
*/
|
|
253
|
-
import type { BindingRef, Callsite, ParsedFile, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
|
|
253
|
+
import type { BindingRef, Callsite, ConstraintContext, ParsedFile, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
|
|
254
254
|
import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
255
255
|
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
256
256
|
import { LanguageProvider } from '../../language-provider.js';
|
|
@@ -264,6 +264,9 @@ import type { ConversionRankFn } from '../passes/overload-narrowing.js';
|
|
|
264
264
|
export type LinearizeStrategy = (classDefId: string, directParents: readonly string[], parentsByDefId: ReadonlyMap<string, readonly string[]>) => string[];
|
|
265
265
|
/** Result of `ScopeResolver.arityCompatibility` — mirrors `RegistryProviders.arityCompatibility`. */
|
|
266
266
|
export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible';
|
|
267
|
+
/** Re-exported for ScopeResolver consumers — same shape as
|
|
268
|
+
* `RegistryProviders.constraintCompatibility`'s third parameter. */
|
|
269
|
+
export type { ConstraintContext } from '../../../../_shared/index.js';
|
|
267
270
|
export interface ScopeResolver {
|
|
268
271
|
/** Identity for telemetry + per-language flag check. */
|
|
269
272
|
readonly language: SupportedLanguages;
|
|
@@ -337,6 +340,23 @@ export interface ScopeResolver {
|
|
|
337
340
|
* `(def, callsite)` and need an adapter at the wiring site.
|
|
338
341
|
*/
|
|
339
342
|
arityCompatibility(callsite: Callsite, def: SymbolDefinition): ArityVerdict;
|
|
343
|
+
/**
|
|
344
|
+
* Per-language constraint compatibility between a callsite and a
|
|
345
|
+
* candidate `def` that carries `templateConstraints` metadata.
|
|
346
|
+
* Mirrors `arityCompatibility` semantics: the three-valued verdict
|
|
347
|
+
* MUST treat `'unknown'` as keep-candidate (monotonicity — adding
|
|
348
|
+
* a predicate can only narrow correctly, never produce a wrong
|
|
349
|
+
* edge). Consulted by `narrowOverloadCandidates` after the arity
|
|
350
|
+
* and parameter-type filters.
|
|
351
|
+
*
|
|
352
|
+
* Optional. Languages without constrained-overload semantics
|
|
353
|
+
* (SFINAE, `requires` clauses, trait bounds, conditional types)
|
|
354
|
+
* leave this undefined and the constraint filter is a pass-through.
|
|
355
|
+
*
|
|
356
|
+
* C++ is the first consumer; see `languages/cpp/constraint-filter.ts`
|
|
357
|
+
* for the Tier-A predicate registry and Kleene 3-valued evaluator.
|
|
358
|
+
*/
|
|
359
|
+
readonly constraintCompatibility?: (callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
|
|
340
360
|
/**
|
|
341
361
|
* Compute the method-dispatch order for every Class def in the
|
|
342
362
|
* workspace. Python uses depth-first first-seen via
|
|
@@ -41,6 +41,7 @@ export declare function resolveDefGraphId(filePath: string, def: {
|
|
|
41
41
|
type?: NodeLabel;
|
|
42
42
|
parameterTypes?: readonly string[];
|
|
43
43
|
templateArguments?: readonly string[];
|
|
44
|
+
templateConstraints?: unknown;
|
|
44
45
|
}, nodeLookup: GraphNodeLookup): string | undefined;
|
|
45
46
|
/** Derive the simple (unqualified) name of a def from its `qualifiedName`. */
|
|
46
47
|
export declare function simpleQualifiedName(def: SymbolDefinition): string | undefined;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { generateId } from '../../../../lib/utils.js';
|
|
20
20
|
import { qualifiedKey, simpleKey } from '../graph-bridge/node-lookup.js';
|
|
21
|
+
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
21
22
|
/**
|
|
22
23
|
* Labels that may legitimately ANCHOR a CALLS/ACCESSES edge as the
|
|
23
24
|
* source ("caller"). A Variable / Property can be the TARGET of an
|
|
@@ -68,6 +69,19 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
|
|
|
68
69
|
if (qn === undefined || qn.length === 0)
|
|
69
70
|
return undefined;
|
|
70
71
|
if (def.type !== undefined) {
|
|
72
|
+
// SFINAE / `requires`-clause disambiguation (issue #1579) — try the
|
|
73
|
+
// constraint-fingerprinted key FIRST. Two function-template overloads
|
|
74
|
+
// with identical `parameterTypes` but mutually-exclusive SFINAE
|
|
75
|
+
// constraints route to their distinct graph nodes via this key.
|
|
76
|
+
// Must run before the parameter-types key because both overloads
|
|
77
|
+
// share the latter.
|
|
78
|
+
if ((def.type === 'Function' || def.type === 'Method') &&
|
|
79
|
+
def.templateConstraints !== undefined) {
|
|
80
|
+
const cKey = qualifiedKey(filePath, def.type, `${qn}${templateConstraintsIdTag(def.templateConstraints)}`);
|
|
81
|
+
const cHit = nodeLookup.get(cKey);
|
|
82
|
+
if (cHit !== undefined)
|
|
83
|
+
return cHit;
|
|
84
|
+
}
|
|
71
85
|
// Overload disambiguation: when the def carries parameter types,
|
|
72
86
|
// try the parameter-typed key first so same-name same-arity
|
|
73
87
|
// overloads route to their distinct graph nodes.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `SymbolDefinition.nodeId` values into the legacy graph-node ID
|
|
18
18
|
* format that downstream consumers (queries, edges, MCP) expect.
|
|
19
19
|
*/
|
|
20
|
+
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
20
21
|
/**
|
|
21
22
|
* Parse a qualified name out of a Function/Method node id.
|
|
22
23
|
*
|
|
@@ -86,6 +87,17 @@ export function buildGraphNodeLookup(graph) {
|
|
|
86
87
|
// Each overload is unique — set unconditionally.
|
|
87
88
|
lookup.set(pKey, node.id);
|
|
88
89
|
}
|
|
90
|
+
// SFINAE / `requires`-clause disambiguation (issue #1579) — register
|
|
91
|
+
// a constraint-fingerprinted key so resolveDefGraphId can locate the
|
|
92
|
+
// correct overload by hashing the def's `templateConstraints`. Mirrors
|
|
93
|
+
// the parameter-types key but keys on the opaque constraint payload
|
|
94
|
+
// instead, separating two `process<T>` overloads whose
|
|
95
|
+
// `parameterTypes=['T']` would otherwise collide.
|
|
96
|
+
const tConstraints = props.templateConstraints;
|
|
97
|
+
if (tConstraints !== undefined && (node.label === 'Function' || node.label === 'Method')) {
|
|
98
|
+
const cKey = qualifiedKey(props.filePath, node.label, `${qualified}${templateConstraintsIdTag(tConstraints)}`);
|
|
99
|
+
lookup.set(cKey, node.id);
|
|
100
|
+
}
|
|
89
101
|
if ((node.label === 'Class' ||
|
|
90
102
|
node.label === 'Struct' ||
|
|
91
103
|
node.label === 'Interface' ||
|
|
@@ -22,6 +22,7 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe
|
|
|
22
22
|
import type { SemanticModel } from '../../model/semantic-model.js';
|
|
23
23
|
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
|
24
24
|
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
25
|
+
import type { ScopeResolver } from '../contract/scope-resolver.js';
|
|
25
26
|
import { type ConversionRankFn } from './overload-narrowing.js';
|
|
26
27
|
export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, _referenceIndex: {
|
|
27
28
|
readonly bySourceScope: ReadonlyMap<ScopeId, readonly Reference[]>;
|
|
@@ -44,6 +45,12 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
|
|
|
44
45
|
};
|
|
45
46
|
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
|
|
46
47
|
readonly conversionRankFn?: ConversionRankFn;
|
|
48
|
+
/** Optional per-language constraint hook threaded into
|
|
49
|
+
* `narrowOverloadCandidates`. Drops candidates whose template
|
|
50
|
+
* constraints (e.g. C++ `enable_if_t`, C++20 `requires`) provably
|
|
51
|
+
* fail at the call site. Three-valued; `'unknown'` keeps the
|
|
52
|
+
* candidate (monotonicity). */
|
|
53
|
+
readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
|
|
47
54
|
}): number;
|
|
48
55
|
/** Walk up from the call-site scope to the enclosing class scope,
|
|
49
56
|
* pick a method member by name with overload narrowing on arity +
|
|
@@ -63,4 +70,7 @@ export declare function pickImplicitThisOverload(site: {
|
|
|
63
70
|
readonly name: string;
|
|
64
71
|
readonly arity?: number;
|
|
65
72
|
readonly argumentTypes?: readonly string[];
|
|
66
|
-
}, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel,
|
|
73
|
+
}, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel, hookCtx?: {
|
|
74
|
+
readonly conversionRankFn?: ConversionRankFn;
|
|
75
|
+
readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
|
|
76
|
+
}): SymbolDefinition | undefined;
|
|
@@ -45,7 +45,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
45
45
|
// the same name in a single class, choose the best match by
|
|
46
46
|
// arity + argument types.
|
|
47
47
|
if (fnDef === undefined) {
|
|
48
|
-
fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model,
|
|
48
|
+
fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, {
|
|
49
|
+
conversionRankFn: options.conversionRankFn,
|
|
50
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
53
|
// Scope-chain callable lookup. First-match preserves scope-chain
|
|
51
54
|
// precedence (local shadows import). When a conversion-rank function
|
|
@@ -63,7 +66,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
63
66
|
if (fnDef !== undefined && options.conversionRankFn !== undefined) {
|
|
64
67
|
const allCallables = findAllCallableBindingsInScope(site.inScope, site.name, scopes);
|
|
65
68
|
if (allCallables.length > 1) {
|
|
66
|
-
const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes,
|
|
69
|
+
const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes, {
|
|
70
|
+
conversionRankFn: options.conversionRankFn,
|
|
71
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
72
|
+
});
|
|
67
73
|
if (narrowed.length === 1) {
|
|
68
74
|
fnDef = narrowed[0];
|
|
69
75
|
}
|
|
@@ -97,36 +103,49 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
97
103
|
argumentTypes: site.argumentTypes,
|
|
98
104
|
atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
|
|
99
105
|
}, parsed, scopes, parsedFiles);
|
|
100
|
-
|
|
101
|
-
// with conversion-rank scoring when multiple overloads exist.
|
|
102
|
-
// Single candidate or empty falls through to first-match.
|
|
106
|
+
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
|
103
107
|
if (adl === undefined || adl.length === 0) {
|
|
104
|
-
|
|
108
|
+
// No ADL contribution. Default behavior: `ordinary[0]` —
|
|
109
|
+
// scope-chain walk preserves local-shadows-import precedence.
|
|
110
|
+
//
|
|
111
|
+
// Narrowing kicks in when either disambiguation signal is
|
|
112
|
+
// present: any candidate carries `templateConstraints`
|
|
113
|
+
// (SFINAE / `requires`-clause guarded templates, #1579), OR
|
|
114
|
+
// a conversion-rank function is provided (#1606 / #1578).
|
|
115
|
+
// Both hooks are threaded into `narrowOverloadCandidates`
|
|
116
|
+
// via the unified `OverloadNarrowingHookCtx`.
|
|
117
|
+
const hasConstraints = ordinary.some((d) => d.templateConstraints !== undefined);
|
|
118
|
+
const canNarrow = hasConstraints || options.conversionRankFn !== undefined;
|
|
119
|
+
if (ordinary.length <= 1 || !canNarrow) {
|
|
105
120
|
fnDef = ordinary[0];
|
|
106
121
|
}
|
|
107
122
|
else {
|
|
108
|
-
const
|
|
109
|
-
|
|
123
|
+
const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, {
|
|
124
|
+
conversionRankFn: options.conversionRankFn,
|
|
125
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
126
|
+
});
|
|
110
127
|
if (narrowed.length === 1) {
|
|
111
128
|
fnDef = narrowed[0];
|
|
112
129
|
}
|
|
113
|
-
else if (narrowed.length
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
else if (narrowed.length === 0) {
|
|
131
|
+
handledSites.add(siteKey);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// >1 survivors: same-file → suppress (true overloads,
|
|
136
|
+
// "degrade not lie" — no edge beats a wrong one, and
|
|
137
|
+
// SFINAE-ambiguous calls land here). Cross-file →
|
|
138
|
+
// first-match (shadowing semantics).
|
|
116
139
|
const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
|
|
117
140
|
if (sameFile) {
|
|
118
141
|
handledSites.add(siteKey);
|
|
119
142
|
continue;
|
|
120
143
|
}
|
|
121
|
-
fnDef = ordinary[0];
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
fnDef = ordinary[0]; // narrowed empty → first-match
|
|
144
|
+
fnDef = ordinary[0];
|
|
125
145
|
}
|
|
126
146
|
}
|
|
127
147
|
}
|
|
128
148
|
else {
|
|
129
|
-
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
|
130
149
|
const merged = [];
|
|
131
150
|
const seenMerge = new Set();
|
|
132
151
|
const push = (defs) => {
|
|
@@ -139,7 +158,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
139
158
|
};
|
|
140
159
|
push(ordinary);
|
|
141
160
|
push(adl);
|
|
142
|
-
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes,
|
|
161
|
+
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, {
|
|
162
|
+
conversionRankFn: options.conversionRankFn,
|
|
163
|
+
constraintCompatibility: options.constraintCompatibility,
|
|
164
|
+
});
|
|
143
165
|
if (narrowed.length === 1) {
|
|
144
166
|
fnDef = narrowed[0];
|
|
145
167
|
}
|
|
@@ -248,7 +270,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
|
|
|
248
270
|
// best-rank candidate when exact-type or conversion-rank scoring can
|
|
249
271
|
// disambiguate (e.g., `f(int)` vs `f(double)` called with `f(2.5)`).
|
|
250
272
|
if (scopeDefs.length > 1) {
|
|
251
|
-
const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes,
|
|
273
|
+
const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, {
|
|
274
|
+
conversionRankFn,
|
|
275
|
+
});
|
|
252
276
|
if (narrowed.length === 1)
|
|
253
277
|
return narrowed[0];
|
|
254
278
|
}
|
|
@@ -286,7 +310,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
|
|
|
286
310
|
}
|
|
287
311
|
// Same argument-type + conversion-rank narrowing for the model pool.
|
|
288
312
|
if (defs.length > 1) {
|
|
289
|
-
const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes,
|
|
313
|
+
const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, {
|
|
314
|
+
conversionRankFn,
|
|
315
|
+
});
|
|
290
316
|
if (narrowed.length === 1)
|
|
291
317
|
return narrowed[0];
|
|
292
318
|
}
|
|
@@ -345,7 +371,7 @@ function pickConstructorOrClass(classDef, workspaceIndex) {
|
|
|
345
371
|
* Exported for unit testing — language-agnostic logic, exercised
|
|
346
372
|
* via synthetic stubs in `pick-implicit-this-overload.test.ts`. The
|
|
347
373
|
* production call site is `applyFreeCallFallback` immediately above. */
|
|
348
|
-
export function pickImplicitThisOverload(site, scopes, workspaceIndex, model,
|
|
374
|
+
export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, hookCtx) {
|
|
349
375
|
// Find the enclosing Class scope by walking parents.
|
|
350
376
|
let curId = site.inScope;
|
|
351
377
|
let classScopeId;
|
|
@@ -374,7 +400,10 @@ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, co
|
|
|
374
400
|
// ambiguous narrowing (multiple compatible candidates with no
|
|
375
401
|
// disambiguating signal) leaves the call unresolved rather than
|
|
376
402
|
// routing to an arbitrary first overload by registration order.
|
|
377
|
-
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes,
|
|
403
|
+
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
|
404
|
+
conversionRankFn: hookCtx?.conversionRankFn,
|
|
405
|
+
constraintCompatibility: hookCtx?.constraintCompatibility,
|
|
406
|
+
});
|
|
378
407
|
if (candidates.length !== 1)
|
|
379
408
|
return undefined;
|
|
380
409
|
return candidates[0];
|
|
@@ -25,14 +25,19 @@
|
|
|
25
25
|
* counts as a match. Mismatches disqualify. A non-empty typed
|
|
26
26
|
* result wins; otherwise return the arity-filtered candidates.
|
|
27
27
|
* 4b. When the exact-type filter from step 4 returns empty AND a
|
|
28
|
-
* `conversionRankFn` is provided, rank candidates
|
|
29
|
-
* dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
-
* only when F1 is not worse for every arg and better
|
|
31
|
-
* least one. Non-dominated candidates are returned;
|
|
32
|
-
* survivors are genuinely ambiguous.
|
|
28
|
+
* `conversionRankFn` is provided (via `hookCtx`), rank candidates
|
|
29
|
+
* via pairwise dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
+
* F1 beats F2 only when F1 is not worse for every arg and better
|
|
31
|
+
* for at least one. Non-dominated candidates are returned;
|
|
32
|
+
* multiple survivors are genuinely ambiguous.
|
|
33
|
+
* 4c. Final per-candidate constraint filter (SFINAE / `requires`).
|
|
34
|
+
* When `constraintCompatibility` is provided via `hookCtx`, drop
|
|
35
|
+
* candidates whose template constraints provably fail at the
|
|
36
|
+
* call site. Three-valued; `'unknown'` keeps the candidate
|
|
37
|
+
* (monotonicity).
|
|
33
38
|
* 5. Empty input returns empty output.
|
|
34
39
|
*/
|
|
35
|
-
import type { SymbolDefinition } from '../../../../_shared/index.js';
|
|
40
|
+
import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from '../../../../_shared/index.js';
|
|
36
41
|
/**
|
|
37
42
|
* Per-slot conversion-rank function. Returns a numeric cost for
|
|
38
43
|
* converting `argType` to `paramType`:
|
|
@@ -45,7 +50,25 @@ import type { SymbolDefinition } from '../../../../_shared/index.js';
|
|
|
45
50
|
* on normalized type strings (output of the language's type normalizer).
|
|
46
51
|
*/
|
|
47
52
|
export type ConversionRankFn = (argType: string, paramType: string) => number;
|
|
48
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Optional hook bundle for narrowing extension points. Threaded in
|
|
55
|
+
* from `pickOverload` / `pickImplicitThisOverload` so per-language
|
|
56
|
+
* narrowing can layer in conversion-rank scoring (#1606) and
|
|
57
|
+
* constraint filtering (#1579) without changing the call signature
|
|
58
|
+
* at every site. Each hook is independently optional — leaving both
|
|
59
|
+
* undefined preserves the legacy arity + exact-type behavior.
|
|
60
|
+
*/
|
|
61
|
+
export interface OverloadNarrowingHookCtx {
|
|
62
|
+
/** Conversion-rank scoring fallback (step 4b). Engages when the
|
|
63
|
+
* exact-type filter rejects every candidate. */
|
|
64
|
+
readonly conversionRankFn?: ConversionRankFn;
|
|
65
|
+
/** Constraint filter (step 4c). Drops candidates whose template
|
|
66
|
+
* guards (SFINAE `enable_if_t`, C++20 `requires`, future Rust
|
|
67
|
+
* trait bounds, etc.) provably fail at the call site. Three-valued
|
|
68
|
+
* — `'unknown'` keeps the candidate (monotonicity). */
|
|
69
|
+
readonly constraintCompatibility?: (callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
|
|
70
|
+
}
|
|
71
|
+
export declare function narrowOverloadCandidates(overloads: readonly SymbolDefinition[], argCount: number | undefined, argTypes: readonly string[] | undefined, hookCtx?: OverloadNarrowingHookCtx): readonly SymbolDefinition[];
|
|
49
72
|
/**
|
|
50
73
|
* Detect when >1 candidate share identical `parameterTypes` after the
|
|
51
74
|
* per-language normalizer has collapsed distinct underlying types. This
|
|
@@ -25,14 +25,19 @@
|
|
|
25
25
|
* counts as a match. Mismatches disqualify. A non-empty typed
|
|
26
26
|
* result wins; otherwise return the arity-filtered candidates.
|
|
27
27
|
* 4b. When the exact-type filter from step 4 returns empty AND a
|
|
28
|
-
* `conversionRankFn` is provided, rank candidates
|
|
29
|
-
* dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
-
* only when F1 is not worse for every arg and better
|
|
31
|
-
* least one. Non-dominated candidates are returned;
|
|
32
|
-
* survivors are genuinely ambiguous.
|
|
28
|
+
* `conversionRankFn` is provided (via `hookCtx`), rank candidates
|
|
29
|
+
* via pairwise dominance comparison (ISO C++ [over.ics.rank]):
|
|
30
|
+
* F1 beats F2 only when F1 is not worse for every arg and better
|
|
31
|
+
* for at least one. Non-dominated candidates are returned;
|
|
32
|
+
* multiple survivors are genuinely ambiguous.
|
|
33
|
+
* 4c. Final per-candidate constraint filter (SFINAE / `requires`).
|
|
34
|
+
* When `constraintCompatibility` is provided via `hookCtx`, drop
|
|
35
|
+
* candidates whose template constraints provably fail at the
|
|
36
|
+
* call site. Three-valued; `'unknown'` keeps the candidate
|
|
37
|
+
* (monotonicity).
|
|
33
38
|
* 5. Empty input returns empty output.
|
|
34
39
|
*/
|
|
35
|
-
export function narrowOverloadCandidates(overloads, argCount, argTypes,
|
|
40
|
+
export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx) {
|
|
36
41
|
if (overloads.length === 0)
|
|
37
42
|
return [];
|
|
38
43
|
const arityMatches = argCount === undefined
|
|
@@ -67,6 +72,7 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, conversi
|
|
|
67
72
|
// args).
|
|
68
73
|
const anyUnknownBounds = overloads.some((d) => d.parameterCount === undefined && d.requiredParameterCount === undefined);
|
|
69
74
|
const candidates = arityMatches.length > 0 ? arityMatches : anyUnknownBounds ? overloads : [];
|
|
75
|
+
let result = candidates;
|
|
70
76
|
if (argTypes !== undefined && argTypes.length > 0) {
|
|
71
77
|
const typed = candidates.filter((d) => {
|
|
72
78
|
const params = d.parameterTypes;
|
|
@@ -80,21 +86,46 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, conversi
|
|
|
80
86
|
}
|
|
81
87
|
return true;
|
|
82
88
|
});
|
|
83
|
-
if (typed.length > 0)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
if (typed.length > 0) {
|
|
90
|
+
result = typed;
|
|
91
|
+
}
|
|
92
|
+
else if (hookCtx?.conversionRankFn !== undefined) {
|
|
93
|
+
// ── Conversion-rank scoring (step 4b) ──────────────────────────
|
|
94
|
+
// The exact-type filter rejected every candidate. Rank via
|
|
95
|
+
// pairwise dominance: F1 beats F2 only when F1 is not worse for
|
|
96
|
+
// every arg and better for at least one. Non-dominated candidates
|
|
97
|
+
// are returned; multiple survivors are genuinely ambiguous. When
|
|
98
|
+
// ranking also yields empty, fall through to the arity-filtered
|
|
99
|
+
// `candidates` set — matches pre-#1606 behavior.
|
|
100
|
+
const ranked = rankByConversion(candidates, argTypes, hookCtx.conversionRankFn);
|
|
93
101
|
if (ranked.length > 0)
|
|
94
|
-
|
|
102
|
+
result = ranked;
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
|
-
|
|
105
|
+
// Constraint filter (step 4c; Tier-A — SFINAE / `requires` clauses).
|
|
106
|
+
// Runs after arity, exact-type, and conversion-rank filters so the
|
|
107
|
+
// hook only sees candidates already viable on the other axes.
|
|
108
|
+
// Three-valued: `'compatible'` and `'unknown'` keep the candidate
|
|
109
|
+
// (monotonicity — adding a predicate must never cause a wrong edge);
|
|
110
|
+
// only `'incompatible'` drops it. Candidates without
|
|
111
|
+
// `templateConstraints` are always kept.
|
|
112
|
+
//
|
|
113
|
+
// No fallback to the unconstrained set when this filter empties the
|
|
114
|
+
// candidate list: a fully-`'incompatible'` verdict is authoritative.
|
|
115
|
+
// The downstream `OVERLOAD_AMBIGUOUS` sentinel still guards the empty
|
|
116
|
+
// case, so a buggy hook that wrongly returns `'incompatible'` for
|
|
117
|
+
// every candidate degrades to today's "suppress edge" behavior rather
|
|
118
|
+
// than emitting a wrong edge.
|
|
119
|
+
if (hookCtx?.constraintCompatibility !== undefined && argCount !== undefined) {
|
|
120
|
+
const callsite = { arity: argCount };
|
|
121
|
+
const ctx = argTypes !== undefined ? { argumentTypes: argTypes } : {};
|
|
122
|
+
result = result.filter((def) => {
|
|
123
|
+
if (def.templateConstraints === undefined)
|
|
124
|
+
return true;
|
|
125
|
+
return hookCtx.constraintCompatibility(callsite, def, ctx) !== 'incompatible';
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
98
129
|
}
|
|
99
130
|
/**
|
|
100
131
|
* Pairwise dominance comparison (ISO C++ [over.ics.rank]).
|
|
@@ -41,7 +41,7 @@ import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
|
|
41
41
|
/** Subset of `ScopeResolver` consumed by this pass. Accepting the
|
|
42
42
|
* subset rather than the full provider keeps tests and partial
|
|
43
43
|
* refactors lighter — callers only need to populate what we read. */
|
|
44
|
-
type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn'>;
|
|
44
|
+
type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn' | 'constraintCompatibility'>;
|
|
45
45
|
export declare function emitReceiverBoundCalls(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, handledSites: Set<string>, provider: ReceiverBoundProviderSubset, index: WorkspaceResolutionIndex, model: SemanticModel): number;
|
|
46
46
|
/**
|
|
47
47
|
* Sentinel returned by `pickOverload` when narrowing leaves >1 candidate
|