Merge branch 'main' into fix/issue-1518-docker-local-path

This commit is contained in:
Gergő Magyar 2026-05-29 21:38:36 +01:00 • committed by GitHub
commit aa7563bc13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 392 additions and 16 deletions

View file

@ -622,8 +622,9 @@ interface PythonRepoContext {
/** Strip `.py` and return the bare basename (e.g. `api/users.py` → `users`). */
function fileShortKey(rel: string): string {
const slash = rel.lastIndexOf('/');
const file = slash >= 0 ? rel.slice(slash + 1) : rel;
const normalized = rel.replace(/\\/g, '/');
const slash = normalized.lastIndexOf('/');
const file = slash >= 0 ? normalized.slice(slash + 1) : normalized;
return file.endsWith('.py') ? file.slice(0, -3) : file;
}
@ -633,7 +634,8 @@ function fileShortKey(rel: string): string {
* case callers should fall back to the short key.
*/
function fileLongKey(rel: string): string {
const noExt = rel.endsWith('.py') ? rel.slice(0, -3) : rel;
const normalized = rel.replace(/\\/g, '/');
const noExt = normalized.endsWith('.py') ? normalized.slice(0, -3) : normalized;
const lastSlash = noExt.lastIndexOf('/');
if (lastSlash < 0) return '';
const beforeLast = noExt.slice(0, lastSlash);

View file

@ -8,6 +8,7 @@ import type {
MethodVisibility,
} from '../../method-types.js';
import { hasKeyword } from '../../field-extractors/configs/helpers.js';
import { classifyCppParameterType } from '../../languages/cpp/arity-metadata.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
@ -149,6 +150,11 @@ function extractCppParameters(node: SyntaxNode): ParameterInfo[] {
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
: null,
rawType: typeNode?.text?.trim() ?? null,
typeClass: classifyCppParameterType(
typeNode?.text?.trim() ?? 'unknown',
declNode?.text,
param.text,
),
isOptional: false,
isVariadic: false,
});
@ -164,6 +170,11 @@ function extractCppParameters(node: SyntaxNode): ParameterInfo[] {
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
: null,
rawType: typeNode?.text?.trim() ?? null,
typeClass: classifyCppParameterType(
typeNode?.text?.trim() ?? 'unknown',
declNode?.text,
param.text,
),
isOptional: true,
isVariadic: false,
});
@ -180,6 +191,11 @@ function extractCppParameters(node: SyntaxNode): ParameterInfo[] {
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
: null,
rawType: typeNode?.text?.trim() ?? null,
typeClass: classifyCppParameterType(
typeNode?.text?.trim() ?? 'unknown',
declNode?.text,
param.text,
),
isOptional: false,
isVariadic: true,
});

View file

@ -1,6 +1,6 @@
// gitnexus/src/core/ingestion/method-types.ts
import type { SupportedLanguages } from 'gitnexus-shared';
import type { ParameterTypeClass, SupportedLanguages } from 'gitnexus-shared';
import type { FieldVisibility } from './field-types.js';
import type { SyntaxNode } from './utils/ast-helpers.js';
@ -14,6 +14,7 @@ export interface ParameterInfo {
* Used by typeTagForId for overload disambiguation where generic args matter.
* Falls back to `type` when not set. */
rawType?: string | null;
typeClass?: ParameterTypeClass;
isOptional: boolean;
isVariadic: boolean;
}

View file

@ -30,6 +30,7 @@ import {
typeTagForId,
constTagForId,
buildCollisionGroups,
parameterShapeIdTag,
} from './utils/method-props.js';
import {
extractTemplateArguments,
@ -665,6 +666,13 @@ const processParsingSequential = async (
cached.groups,
);
}
const parameterShapeTag =
nodeLabel === 'Function' || nodeLabel === 'Method'
? parameterShapeIdTag(
methodProps.parameterTypes as string[] | undefined,
methodProps.parameterTypeClasses as ParameterTypeClass[] | undefined,
)
: '';
const classTemplateArguments =
extractedClassSymbol?.templateArguments ??
provider.classExtractor?.extractTemplateArgumentsFromCapture?.({
@ -717,7 +725,7 @@ const processParsingSequential = async (
}
const nodeId = generateId(
nodeLabel,
`${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}`,
`${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}${parameterShapeTag}`,
);
const classNodeForSymbol = definitionNodeForRange || definitionNode || nameNode;
const qualifiedTypeName =

View file

@ -17,11 +17,12 @@
* migrate.
*/
import type { NodeLabel, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import type { NodeLabel, ParameterTypeClass, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import { generateId } from '../../../../lib/utils.js';
import { qualifiedKey, simpleKey, type GraphNodeLookup } from '../graph-bridge/node-lookup.js';
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
import { parameterShapeIdTag } from '../../utils/method-props.js';
/**
* Labels that may legitimately ANCHOR a CALLS/ACCESSES edge as the
* source ("caller"). A Variable / Property can be the TARGET of an
@ -76,6 +77,7 @@ export function resolveDefGraphId(
qualifiedName?: string;
type?: NodeLabel;
parameterTypes?: readonly string[];
parameterTypeClasses?: readonly ParameterTypeClass[];
templateArguments?: readonly string[];
templateConstraints?: unknown;
},
@ -102,11 +104,23 @@ export function resolveDefGraphId(
const cHit = nodeLookup.get(cKey);
if (cHit !== undefined) return cHit;
}
if (
(def.type === 'Function' || def.type === 'Method') &&
def.parameterTypes !== undefined &&
def.parameterTypeClasses !== undefined
) {
const shapeTag = parameterShapeIdTag(def.parameterTypes, def.parameterTypeClasses);
if (shapeTag !== '') {
const shapeKey = qualifiedKey(filePath, def.type, `${qn}${shapeTag}`);
const shapeHit = nodeLookup.get(shapeKey);
if (shapeHit !== undefined) return shapeHit;
}
}
// Overload disambiguation: when the def carries parameter types,
// try the parameter-typed key first so same-name same-arity
// overloads route to their distinct graph nodes.
if (
def.type === 'Method' &&
(def.type === 'Function' || def.type === 'Method') &&
def.parameterTypes !== undefined &&
def.parameterTypes.length > 0
) {

View file

@ -18,9 +18,10 @@
* format that downstream consumers (queries, edges, MCP) expect.
*/
import type { NodeLabel } from 'gitnexus-shared';
import type { NodeLabel, ParameterTypeClass } from 'gitnexus-shared';
import type { KnowledgeGraph } from '../../../graph/types.js';
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
import { parameterShapeIdTag } from '../../utils/method-props.js';
export type GraphNodeLookup = ReadonlyMap<string, string>;
@ -42,6 +43,10 @@ function parseQualifiedFromId(id: string, label: NodeLabel, filePath: string): s
return hash === -1 ? suffix : suffix.slice(0, hash);
}
function stripCallableDisambiguatorTags(qualifiedName: string): string {
return qualifiedName.replace(/~shape:.*$/, '').replace(/~c:[a-z0-9]+$/, '');
}
/**
* Build a qualified-key string in a separate keyspace from simple-key
* strings. Prefix `<q>` can't appear in a valid filePath on any OS, so
@ -84,7 +89,8 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
const qualified =
props.qualifiedName ?? parseQualifiedFromId(node.id, node.label, props.filePath);
if (qualified !== undefined && qualified.length > 0) {
const qKey = qualifiedKey(props.filePath, node.label, qualified);
const keyQualified = stripCallableDisambiguatorTags(qualified);
const qKey = qualifiedKey(props.filePath, node.label, keyQualified);
if (!lookup.has(qKey)) lookup.set(qKey, node.id);
// Overload-disambiguating key: include parameter types so two
// same-arity overloads (e.g. `Lookup(int)` vs `Lookup(string)`)
@ -93,10 +99,25 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
// a parameter-types-suffixed key so resolveDefGraphId can find
// the right overload by matching its def's parameterTypes.
const pTypes = (props as { parameterTypes?: readonly string[] }).parameterTypes;
if (pTypes !== undefined && pTypes.length > 0 && node.label === 'Method') {
const pKey = qualifiedKey(props.filePath, node.label, `${qualified}~${pTypes.join(',')}`);
if (
pTypes !== undefined &&
pTypes.length > 0 &&
(node.label === 'Function' || node.label === 'Method')
) {
const pKey = qualifiedKey(
props.filePath,
node.label,
`${keyQualified}~${pTypes.join(',')}`,
);
// Each overload is unique — set unconditionally.
lookup.set(pKey, node.id);
if (!lookup.has(pKey)) lookup.set(pKey, node.id);
}
const pClasses = (props as { parameterTypeClasses?: readonly ParameterTypeClass[] })
.parameterTypeClasses;
const shapeTag = parameterShapeIdTag(pTypes, pClasses);
if (shapeTag !== '' && (node.label === 'Function' || node.label === 'Method')) {
const shapeKey = qualifiedKey(props.filePath, node.label, `${keyQualified}${shapeTag}`);
if (!lookup.has(shapeKey)) lookup.set(shapeKey, node.id);
}
// SFINAE / `requires`-clause disambiguation (issue #1579) — register
// a constraint-fingerprinted key so resolveDefGraphId can locate the
@ -109,7 +130,7 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
const cKey = qualifiedKey(
props.filePath,
node.label,
`${qualified}${templateConstraintsIdTag(tConstraints)}`,
`${keyQualified}${templateConstraintsIdTag(tConstraints)}`,
);
lookup.set(cKey, node.id);
}
@ -125,7 +146,7 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
const tKey = qualifiedKey(
props.filePath,
node.label,
`${qualified}~${props.templateArguments.join(',')}`,
`${keyQualified}~${props.templateArguments.join(',')}`,
);
if (!lookup.has(tKey)) lookup.set(tKey, node.id);
}

View file

@ -35,6 +35,11 @@
* candidates whose template constraints provably fail at the
* call site. Three-valued; `'unknown'` keeps the candidate
* (monotonicity).
* 4d. Conservative C++ template partial-order approximation. When
* template-placeholder overloads remain tied, prefer a candidate
* whose parameter shape is more specialized for the observed
* argument shape (`T*` over `T`, `const T&` over `T`). Unknown or
* incomparable shapes are left ambiguous.
* 5. Empty input returns empty output.
*/
@ -205,6 +210,15 @@ export function narrowOverloadCandidates(
});
}
if (result.length > 1 && argTypes !== undefined && argTypes.length > 0) {
const partiallyOrdered = rankByTemplatePartialOrdering(
result,
argTypes,
hookCtx?.argumentTypeClasses,
);
if (partiallyOrdered !== undefined) result = partiallyOrdered;
}
return result;
}
@ -330,6 +344,109 @@ function pairwiseCompare(a: readonly number[], b: readonly number[]): -1 | 0 | 1
return 0;
}
/**
* Closed-table approximation of C++ function-template partial ordering.
*
* Full `[temp.func.order]` requires template argument deduction. GitNexus
* keeps this graph-safe by recognizing only syntactic placeholder shapes
* that the C++ parameter sidecar already preserves:
* - `T*` is more specialized than `T` for pointer arguments.
*
* Anything with unknown argument shape, non-template parameter spelling, or
* incomparable specialized shapes stays ambiguous so callers suppress. The
* placeholder detector is intentionally narrow: lowercase template parameters
* are left ambiguous rather than guessed.
*/
function rankByTemplatePartialOrdering(
candidates: readonly SymbolDefinition[],
argTypes: readonly string[],
argTypeClasses?: readonly ParameterTypeClass[],
): readonly SymbolDefinition[] | undefined {
if (argTypeClasses === undefined) return undefined;
const viable: Array<{ def: SymbolDefinition; ranks: number[] }> = [];
for (const def of candidates) {
const params = def.parameterTypes;
const paramClasses = def.parameterTypeClasses;
if (params === undefined || paramClasses === undefined) continue;
const ranks: number[] = [];
let sawTemplateSlot = false;
let ok = true;
for (let i = 0; i < argTypes.length; i++) {
const paramType = parameterTypeAt(params, i);
const paramClass = parameterTypeClassAt(paramClasses, i);
const argClass = argTypeClasses[i];
if (paramType === undefined || paramClass === undefined || argClass === undefined) {
ok = false;
break;
}
const rank = templatePartialOrderSlotRank(paramType, paramClass, argClass);
if (rank === undefined) {
ok = false;
break;
}
sawTemplateSlot ||= isTemplatePlaceholder(paramType);
ranks.push(rank);
}
if (ok && sawTemplateSlot) viable.push({ def, ranks });
}
if (viable.length === 0) return undefined;
if (viable.length !== candidates.length) return [];
if (viable.length <= 1) return viable.map((v) => v.def);
const dominated = new Set<number>();
for (let i = 0; i < viable.length; i++) {
if (dominated.has(i)) continue;
for (let j = i + 1; j < viable.length; j++) {
if (dominated.has(j)) continue;
const cmp = compareSpecializationRanks(viable[i].ranks, viable[j].ranks);
if (cmp < 0) dominated.add(j);
else if (cmp > 0) dominated.add(i);
}
}
return viable.filter((_, idx) => !dominated.has(idx)).map((v) => v.def);
}
function templatePartialOrderSlotRank(
paramType: string,
paramClass: ParameterTypeClass,
argClass: ParameterTypeClass,
): number | undefined {
if (!isTemplatePlaceholder(paramType)) return undefined;
if (argClass.indirection === 'unknown' || paramClass.indirection === 'unknown') {
return undefined;
}
if (isPointerShape(paramClass)) {
return isPointerShape(argClass) ? 3 : undefined;
}
if (paramClass.indirection === 'value') return 1;
return undefined;
}
function isTemplatePlaceholder(typeName: string): boolean {
return /^[A-Z]\w*$/.test(typeName);
}
/**
* Higher specialization rank is better. Returns -1 when `a` dominates `b`,
* +1 when `b` dominates `a`, and 0 for ties / incomparable vectors.
*/
function compareSpecializationRanks(a: readonly number[], b: readonly number[]): -1 | 0 | 1 {
let aBetter = false;
let bBetter = false;
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i++) {
if (a[i] > b[i]) aBetter = true;
else if (b[i] > a[i]) bBetter = true;
if (aBetter && bBetter) return 0;
}
if (aBetter && !bBetter) return -1;
if (bBetter && !aBetter) return 1;
return 0;
}
/**
* Detect when >1 candidate share identical `parameterTypes` after the
* per-language normalizer has collapsed distinct underlying types. This

View file

@ -1,5 +1,5 @@
import type { MethodInfo } from '../method-types.js';
import { SupportedLanguages } from 'gitnexus-shared';
import { SupportedLanguages, type ParameterTypeClass } from 'gitnexus-shared';
/** Languages where class overload signatures are declaration-only contracts
* that should collapse to the implementation body's node ID. */
@ -139,13 +139,58 @@ export function constTagForId(
return '';
}
/**
* Disambiguate function-template overloads whose normalized parameter types
* intentionally collapse to the same placeholder token (`T`, `U`, ...), but
* whose C++ sidecar shape is semantically different (`T` vs `T*` / `T&`).
*
* Kept intentionally narrow: concrete types already use the existing raw-type
* overload tag, and non-template languages should not acquire sidecar-shaped
* IDs.
*/
export function parameterShapeIdTag(
parameterTypes?: readonly string[],
parameterTypeClasses?: readonly ParameterTypeClass[],
): string {
if (
parameterTypes === undefined ||
parameterTypeClasses === undefined ||
parameterTypes.length === 0
) {
return '';
}
let hasTemplatePlaceholder = false;
let hasDisambiguatingShape = false;
const parts: string[] = [];
for (let i = 0; i < parameterTypes.length; i++) {
const type = parameterTypes[i];
const typeClass = parameterTypeClasses[i];
if (typeClass === undefined) return '';
if (/^[A-Z]\w*$/.test(type)) hasTemplatePlaceholder = true;
if (
typeClass.indirection !== 'value' ||
typeClass.pointerDepth > 0 ||
(typeClass.cv !== 'none' && typeClass.cv !== 'unknown')
) {
hasDisambiguatingShape = true;
}
parts.push(
`${type}:${typeClass.cv}:${typeClass.indirection}:${typeClass.pointerDepth.toString()}`,
);
}
if (!hasTemplatePlaceholder || !hasDisambiguatingShape) return '';
return `~shape:${parts.join('|')}`;
}
/** Convert MethodInfo from methodExtractor into flat properties for a graph node. */
export function buildMethodProps(info: MethodInfo): Record<string, unknown> {
const types: string[] = [];
const typeClasses: ParameterTypeClass[] = [];
let optionalCount = 0;
let hasVariadic = false;
for (const p of info.parameters) {
if (p.type !== null) types.push(p.type);
if (p.typeClass !== undefined) typeClasses.push(p.typeClass);
if (p.isOptional) optionalCount++;
if (p.isVariadic) hasVariadic = true;
}
@ -155,6 +200,9 @@ export function buildMethodProps(info: MethodInfo): Record<string, unknown> {
? { requiredParameterCount: info.parameters.length - optionalCount }
: {}),
...(types.length > 0 ? { parameterTypes: types } : {}),
...(typeClasses.length === info.parameters.length && typeClasses.length > 0
? { parameterTypeClasses: typeClasses }
: {}),
returnType: info.returnType ?? undefined,
visibility: info.visibility,
isStatic: info.isStatic,

View file

@ -85,6 +85,7 @@ import {
typeTagForId,
constTagForId,
buildCollisionGroups,
parameterShapeIdTag,
} from '../utils/method-props.js';
import { extractTemplateArguments, templateArgumentsIdTag } from '../utils/template-arguments.js';
import type { LanguageProvider } from '../language-provider.js';
@ -1814,6 +1815,13 @@ const processFileGroup = (
);
arityTag += constTagForId(defMethodMap, nodeName, arityForId, defMethodInfo, groups);
}
const parameterShapeTag =
nodeLabel === 'Function' || nodeLabel === 'Method'
? parameterShapeIdTag(
methodProps.parameterTypes as string[] | undefined,
methodProps.parameterTypeClasses as ParameterTypeClass[] | undefined,
)
: '';
const classTemplateArguments =
extractedClassSymbol?.templateArguments ??
provider.classExtractor?.extractTemplateArgumentsFromCapture?.({
@ -1837,7 +1845,7 @@ const processFileGroup = (
: '';
const nodeId = generateId(
nodeLabel,
`${file.path}:${qualifiedName}${classTemplateTag}${arityTag}`,
`${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${parameterShapeTag}`,
);
const classNodeForSymbol = definitionNode || nameNode;
const qualifiedTypeName =

View file

@ -0,0 +1,13 @@
template<class T>
void pick(T value) {
}
template<class T>
void pick(T* value) {
}
void run() {
int n = 0;
int* p = &n;
pick(p);
}

View file

@ -0,0 +1,13 @@
template<class T>
void pick(T* lhs, T rhs) {
}
template<class T>
void pick(T lhs, T* rhs) {
}
void run() {
int n = 0;
int* p = &n;
pick(p, p);
}

View file

@ -1473,6 +1473,34 @@ describe('C++ template overload disambiguation (vector<int> vs vector<string>)',
});
});
describe('C++ template partial ordering (#1635)', () => {
it('pick(T*) wins over pick(T) for pointer arguments', async () => {
const result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-template-partial-order-pointer'),
() => {},
);
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'pick',
);
expect(calls.length).toBe(1);
const target = result.graph.getNode(calls[0].rel.targetId);
expect(target?.properties.startLine).toBe(5);
});
it('pick(T*, T) vs pick(T, T*) emits zero CALLS edges when partial ordering is incomparable', async () => {
const result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-template-partial-order-tied'),
() => {},
);
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'pick',
);
expect(calls.length).toBe(0);
});
});
// ── Phase P: C++ template overload cross-file + chain resolution ──────────
describe('C++ template overload cross-file and chain resolution', () => {

View file

@ -384,6 +384,10 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
// PR #1634: deep-nesting suppression. The scope-resolver enforces a
// one-level cap on namespace walking. The legacy DAG picks arbitrarily.
'Derived<T>::g() -> this->f() emits zero CALLS when Inner is two levels deep (ns.a.b) — one-level cap enforced',
// Template partial ordering (#1635) relies on C++ parameter type-class
// sidecars and scope-resolver overload narrowing. The legacy DAG does not
// rank function-template shapes, so it leaves the call unresolved.
'pick(T*) wins over pick(T) for pointer arguments',
]),
};

View file

@ -21,6 +21,13 @@ const pointer = (base: string): ParameterTypeClass => ({
pointerDepth: 1,
});
const constRef = (base: string): ParameterTypeClass => ({
base,
cv: 'const',
indirection: 'lvalue-ref',
pointerDepth: 0,
});
const ellipsis = (): ParameterTypeClass => ({
base: '...',
cv: 'unknown',
@ -143,3 +150,79 @@ describe('narrowOverloadCandidates with C++ pointer-rank sidecars (#1637)', () =
expect(result.map((d) => d.nodeId)).toEqual(['log:ellipsis']);
});
});
describe('narrowOverloadCandidates with C++ template partial ordering (#1635)', () => {
it('selects T* over T for pointer arguments', () => {
const byValue = mkDef('pick:T', ['T'], [value('T')]);
const byPointer = mkDef('pick:T*', ['T'], [pointer('T')]);
const result = narrowOverloadCandidates([byValue, byPointer], 1, ['int'], {
argumentTypeClasses: [pointer('int')],
});
expect(result.map((d) => d.nodeId)).toEqual(['pick:T*']);
});
it('keeps const T& versus T ambiguous for value arguments', () => {
const byValue = mkDef('pick:T', ['T'], [value('T')]);
const byReference = mkDef('pick:const-T-ref', ['T'], [constRef('T')]);
const result = narrowOverloadCandidates([byValue, byReference], 1, ['int'], {
argumentTypeClasses: [value('int')],
});
expect(result.map((d) => d.nodeId)).toEqual([]);
});
it('suppresses when any surviving candidate cannot participate in ordering', () => {
const concreteSlot = mkDef('pick:T-int', ['T', 'int'], [value('T'), value('int')]);
const pointerSlot = mkDef('pick:T-T*', ['T', 'T'], [value('T'), pointer('T')]);
const result = narrowOverloadCandidates([concreteSlot, pointerSlot], 2, ['int', 'int'], {
argumentTypeClasses: [pointer('int'), pointer('int')],
});
expect(result.map((d) => d.nodeId)).toEqual([]);
});
it('suppresses when a surviving candidate lacks parameter sidecars', () => {
const withoutSidecar: SymbolDefinition = {
...mkDef('pick:no-sidecar', ['T'], [value('T')]),
parameterTypeClasses: undefined,
};
const byPointer = mkDef('pick:T*', ['T'], [pointer('T')]);
const result = narrowOverloadCandidates([withoutSidecar, byPointer], 1, ['int'], {
argumentTypeClasses: [pointer('int')],
});
expect(result.map((d) => d.nodeId)).toEqual([]);
});
it('leaves lowercase template placeholders ambiguous rather than guessing', () => {
const byValue = mkDef('pick:t', ['t'], [value('t')]);
const byPointer = mkDef('pick:t*', ['t'], [pointer('t')]);
const result = narrowOverloadCandidates([byValue, byPointer], 1, ['int'], {
argumentTypeClasses: [pointer('int')],
});
expect(result.map((d) => d.nodeId)).toEqual(['pick:t', 'pick:t*']);
});
it('keeps crossed template shapes ambiguous', () => {
const pointerThenValue = mkDef('pick:T*-T', ['T', 'T'], [pointer('T'), value('T')]);
const valueThenPointer = mkDef('pick:T-T*', ['T', 'T'], [value('T'), pointer('T')]);
const result = narrowOverloadCandidates(
[pointerThenValue, valueThenPointer],
2,
['int', 'int'],
{
argumentTypeClasses: [pointer('int'), pointer('int')],
},
);
expect(result.map((d) => d.nodeId)).toEqual(['pick:T*-T', 'pick:T-T*']);
});
});