mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(cpp): rank homogeneous braced-init overloads (#2214)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
This commit is contained in:
parent
b895a20415
commit
72876ab69a
18 changed files with 456 additions and 9 deletions
|
|
@ -20,6 +20,8 @@ export interface ParameterTypeClass {
|
|||
indirection: 'value' | 'lvalue-ref' | 'rvalue-ref' | 'pointer' | 'unknown';
|
||||
/** Number of pointer markers when indirection is `pointer`; otherwise 0. */
|
||||
pointerDepth: number;
|
||||
/** Normalized top-level template arguments, when a language preserves them. */
|
||||
templateArguments?: string[];
|
||||
}
|
||||
|
||||
export interface SymbolDefinition {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@
|
|||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
|
||||
},
|
||||
"cpp": {
|
||||
"fingerprint": "9b5b4393d158d76dcf1ef9807e0326462c45a5266310f0ae7894d017f3858219",
|
||||
"fingerprint": "5ef259c2cf9c4804bc83c41d25a3141d1a2cc57ce46485cce09013cf6f5e725e",
|
||||
"scaling_budget": 1.5,
|
||||
"_note_1899_followup": "#1899 follow-up: braced-init metadata now carries element count, intentionally changing C++ capture output; CI benchmark scaling remains linear (1.129 < 1.5).",
|
||||
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.",
|
||||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression). #2094: deleted C++ declarations retain @declaration.is-deleted metadata; deleted operator and pointer-return shapes plus the expanded deleted-overload fixture are included. Intended capture drift; scaling remains linear (1.139 < 1.5).",
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5)."
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5). #1899: braced-init call arguments emit a conservative parameter-type capture; fixture_count 277, scaling remains linear (1.141 < 1.5)."
|
||||
},
|
||||
"csharp": {
|
||||
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ export function classifyCppParameterType(
|
|||
cv,
|
||||
indirection,
|
||||
pointerDepth,
|
||||
...templateArgumentsFor(`${source} ${rawType} ${declaratorText ?? ''}`),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -213,6 +214,39 @@ function unknownTypeClass(base: string): ParameterTypeClass {
|
|||
};
|
||||
}
|
||||
|
||||
function templateArgumentsFor(rawType: string): Pick<ParameterTypeClass, 'templateArguments'> {
|
||||
const args = parseTopLevelTemplateArguments(rawType);
|
||||
return args === undefined ? {} : { templateArguments: args };
|
||||
}
|
||||
|
||||
function parseTopLevelTemplateArguments(rawType: string): string[] | undefined {
|
||||
const start = rawType.indexOf('<');
|
||||
if (start < 0) return undefined;
|
||||
|
||||
const args: string[] = [];
|
||||
let depth = 0;
|
||||
let argStart = start + 1;
|
||||
for (let i = start + 1; i < rawType.length; i++) {
|
||||
const ch = rawType[i];
|
||||
if (ch === '<') {
|
||||
depth++;
|
||||
} else if (ch === '>') {
|
||||
if (depth === 0) {
|
||||
const finalArg = rawType.slice(argStart, i).trim();
|
||||
if (finalArg.length > 0) args.push(normalizeCppParamType(finalArg));
|
||||
return args.length > 0 ? args : undefined;
|
||||
}
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
const arg = rawType.slice(argStart, i).trim();
|
||||
if (arg.length > 0) args.push(normalizeCppParamType(arg));
|
||||
argStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findFuncDeclarator(node: SyntaxNode): SyntaxNode | null {
|
||||
let decl = node.childForFieldName('declarator');
|
||||
if (decl === null) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './a
|
|||
import { markCppInlineNamespaceRange } from './inline-namespaces.js';
|
||||
import { extractCppTemplateConstraints } from './constraint-extractor.js';
|
||||
import { captureCppMemberLookupFacts } from './member-lookup.js';
|
||||
import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js';
|
||||
|
||||
export function emitCppScopeCaptures(
|
||||
sourceText: string,
|
||||
|
|
@ -1022,6 +1023,8 @@ function unknownTypeClass(base: string): ParameterTypeClass {
|
|||
*/
|
||||
function inferCppLiteralType(node: SyntaxNode): string {
|
||||
switch (node.type) {
|
||||
case 'initializer_list':
|
||||
return inferCppBracedInitType(node);
|
||||
case 'number_literal': {
|
||||
const text = node.text;
|
||||
// Floating-point literals contain '.', 'e', 'E', or end with 'f'/'F'
|
||||
|
|
@ -1053,6 +1056,25 @@ function inferCppLiteralType(node: SyntaxNode): string {
|
|||
}
|
||||
}
|
||||
|
||||
function inferCppBracedInitType(node: SyntaxNode): string {
|
||||
const elementTypes: string[] = [];
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === ',' || child.type === '{' || child.type === '}') continue;
|
||||
const elementType = inferCppLiteralType(child);
|
||||
if (elementType === '' || elementType.startsWith(CPP_BRACED_INIT_TYPE_PREFIX)) {
|
||||
return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length + 1}`;
|
||||
}
|
||||
elementTypes.push(elementType);
|
||||
}
|
||||
if (elementTypes.length === 0) return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:0`;
|
||||
const first = elementTypes[0];
|
||||
return elementTypes.every((type) => type === first)
|
||||
? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}:${elementTypes.length}`
|
||||
: `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the declared type of a variable by scanning sibling declarations
|
||||
* in the enclosing compound_statement (function body). Handles:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
import type { ParameterTypeClass } from 'gitnexus-shared';
|
||||
import { normalizeCppParamType } from './arity-metadata.js';
|
||||
import { hasCppUserDefinedConversion } from './user-defined-conversions.js';
|
||||
|
||||
/** Set of normalized arithmetic types that support implicit conversion. */
|
||||
|
|
@ -32,6 +33,29 @@ const INTEGRAL_PROMOTION = new Map([
|
|||
['bool', 'int'],
|
||||
]);
|
||||
|
||||
export const CPP_BRACED_INIT_TYPE_PREFIX = 'braced-init:';
|
||||
export const CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES = [CPP_BRACED_INIT_TYPE_PREFIX] as const;
|
||||
|
||||
const BRACED_INIT_CONTAINER_TYPES = new Set([
|
||||
'array',
|
||||
'deque',
|
||||
'list',
|
||||
'set',
|
||||
'std::array',
|
||||
'std::deque',
|
||||
'std::list',
|
||||
'std::set',
|
||||
'std::unordered_set',
|
||||
'std::vector',
|
||||
'unordered_set',
|
||||
'vector',
|
||||
]);
|
||||
|
||||
interface BracedInitArgType {
|
||||
elementType: string;
|
||||
elementCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the conversion rank from `argType` to `paramType`.
|
||||
*
|
||||
|
|
@ -46,6 +70,20 @@ export function cppConversionRank(
|
|||
argTypeClass?: ParameterTypeClass,
|
||||
paramTypeClass?: ParameterTypeClass,
|
||||
): number {
|
||||
const bracedInitType = parseBracedInitArgType(argType);
|
||||
if (bracedInitType !== undefined) {
|
||||
if (bracedInitType.elementType === 'unknown') return Infinity;
|
||||
if (bracedInitType.elementCount === 1) {
|
||||
const scalarRank = cppConversionRank(
|
||||
bracedInitType.elementType,
|
||||
paramType,
|
||||
undefined,
|
||||
paramTypeClass,
|
||||
);
|
||||
if (isFinite(scalarRank)) return scalarRank;
|
||||
}
|
||||
return bracedInitConversionRank(paramType, bracedInitType, paramTypeClass);
|
||||
}
|
||||
if (argType === paramType) {
|
||||
return exactShapeCompatible(argTypeClass, paramTypeClass) ? 0 : Infinity;
|
||||
}
|
||||
|
|
@ -60,6 +98,79 @@ export function cppConversionRank(
|
|||
return Infinity;
|
||||
}
|
||||
|
||||
function parseBracedInitArgType(argType: string): BracedInitArgType | undefined {
|
||||
if (!argType.startsWith(CPP_BRACED_INIT_TYPE_PREFIX)) return undefined;
|
||||
const payload = argType.slice(CPP_BRACED_INIT_TYPE_PREFIX.length);
|
||||
if (payload === '') return undefined;
|
||||
const separator = payload.lastIndexOf(':');
|
||||
if (separator > 0) {
|
||||
const countText = payload.slice(separator + 1);
|
||||
if (/^\d+$/.test(countText)) {
|
||||
return {
|
||||
elementType: payload.slice(0, separator),
|
||||
elementCount: Number(countText),
|
||||
};
|
||||
}
|
||||
}
|
||||
return { elementType: payload };
|
||||
}
|
||||
|
||||
function bracedInitConversionRank(
|
||||
paramType: string,
|
||||
argType: BracedInitArgType,
|
||||
paramTypeClass?: ParameterTypeClass,
|
||||
): number {
|
||||
const targetBase = bracedInitTargetBase(paramType);
|
||||
if (targetBase === 'initializer_list' || targetBase === 'std::initializer_list') {
|
||||
return bracedInitValueTypeMatches(paramType, argType, paramTypeClass) ? 0 : Infinity;
|
||||
}
|
||||
if (BRACED_INIT_CONTAINER_TYPES.has(targetBase)) {
|
||||
return bracedInitValueTypeMatches(paramType, argType, paramTypeClass) ? 4 : Infinity;
|
||||
}
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
function bracedInitValueTypeMatches(
|
||||
paramType: string,
|
||||
argType: BracedInitArgType,
|
||||
paramTypeClass?: ParameterTypeClass,
|
||||
): boolean {
|
||||
const valueType = bracedInitTargetValueType(paramType, paramTypeClass);
|
||||
if (valueType === undefined) return false;
|
||||
return isFinite(cppConversionRank(argType.elementType, valueType));
|
||||
}
|
||||
|
||||
function bracedInitTargetValueType(
|
||||
paramType: string,
|
||||
paramTypeClass?: ParameterTypeClass,
|
||||
): string | undefined {
|
||||
return firstTemplateArgument(paramType) ?? paramTypeClass?.templateArguments?.[0];
|
||||
}
|
||||
|
||||
function firstTemplateArgument(rawType: string): string | undefined {
|
||||
const start = rawType.indexOf('<');
|
||||
if (start < 0) return undefined;
|
||||
|
||||
let depth = 0;
|
||||
for (let i = start + 1; i < rawType.length; i++) {
|
||||
const ch = rawType[i];
|
||||
if (ch === '<') {
|
||||
depth++;
|
||||
} else if (ch === '>') {
|
||||
if (depth === 0) return bracedInitTargetBase(rawType.slice(start + 1, i));
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
return bracedInitTargetBase(rawType.slice(start + 1, i));
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function bracedInitTargetBase(paramType: string): string {
|
||||
return normalizeCppParamType(paramType);
|
||||
}
|
||||
|
||||
function isPointer(typeClass: ParameterTypeClass | undefined): boolean {
|
||||
return typeClass?.indirection === 'pointer' && typeClass.pointerDepth > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import {
|
|||
isOverloadAmbiguousAfterNormalization,
|
||||
narrowOverloadCandidates,
|
||||
} from '../../scope-resolution/passes/overload-narrowing.js';
|
||||
import { cppConversionRank } from './conversion-rank.js';
|
||||
import { CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, cppConversionRank } from './conversion-rank.js';
|
||||
|
||||
interface RangeKey {
|
||||
readonly startLine: number;
|
||||
|
|
@ -167,7 +167,12 @@ export function resolveCppQualifiedNamespaceMember(
|
|||
allHits,
|
||||
callsite?.arity,
|
||||
callsite?.argumentTypes,
|
||||
callsite !== undefined ? { conversionRankFn: cppConversionRank } : undefined,
|
||||
callsite !== undefined
|
||||
? {
|
||||
conversionRankFn: cppConversionRank,
|
||||
conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
if (narrowed.length === 1) return narrowed[0];
|
||||
if (narrowed.length === 0) return undefined;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { cppConstraintCompatibility } from './constraint-filter.js';
|
||||
import { cppConversionRank } from './conversion-rank.js';
|
||||
import { CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, cppConversionRank } from './conversion-rank.js';
|
||||
|
||||
interface CapturedBaseEdge {
|
||||
readonly childName: string;
|
||||
|
|
@ -309,6 +309,7 @@ function chooseOverload(
|
|||
const narrowed = narrowOverloadCandidates(candidates, callsite.arity, callsite.argumentTypes, {
|
||||
argumentTypeClasses: callsite.argumentTypeClasses,
|
||||
conversionRankFn: cppConversionRank,
|
||||
conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
|
||||
constraintCompatibility: cppConstraintCompatibility,
|
||||
});
|
||||
if (narrowed.length === 1) return { kind: 'resolved', definition: narrowed[0]! };
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { cppProvider } from '../c-cpp.js';
|
||||
import { cppArityCompatibility } from './arity.js';
|
||||
import { cppConversionRank } from './conversion-rank.js';
|
||||
import { CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, cppConversionRank } from './conversion-rank.js';
|
||||
import { cppMergeBindings } from './merge-bindings.js';
|
||||
import { resolveCppImportTarget } from './import-target.js';
|
||||
import { scanCppHeaderFiles } from './header-scan.js';
|
||||
|
|
@ -257,6 +257,7 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
// Disambiguates `f(int)` vs `f(double)` called with `f(2.5)` by scoring
|
||||
// each candidate's conversion cost; exact match wins over standard conversion.
|
||||
conversionRankFn: cppConversionRank,
|
||||
conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
|
||||
// Range-for element type inference: for (auto& user : users) → bind user to User
|
||||
populateRangeBindings: populateCppRangeBindings,
|
||||
// C++ method return-type bindings need to be visible from module scope
|
||||
|
|
|
|||
|
|
@ -654,12 +654,19 @@ function parseJsonParameterTypeClassesCapture(
|
|||
if (typeof o.pointerDepth !== 'number' || !Number.isFinite(o.pointerDepth)) {
|
||||
return undefined;
|
||||
}
|
||||
out.push({
|
||||
const shape: ParameterTypeClass = {
|
||||
base: o.base,
|
||||
cv: o.cv,
|
||||
indirection: o.indirection,
|
||||
pointerDepth: o.pointerDepth,
|
||||
});
|
||||
};
|
||||
if (Array.isArray(o.templateArguments)) {
|
||||
if (!o.templateArguments.every((x): x is string => typeof x === 'string')) {
|
||||
return undefined;
|
||||
}
|
||||
shape.templateArguments = [...o.templateArguments];
|
||||
}
|
||||
out.push(shape);
|
||||
}
|
||||
return out;
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -706,6 +706,16 @@ export interface ScopeResolver {
|
|||
*/
|
||||
readonly conversionRankFn?: ConversionRankFn;
|
||||
|
||||
/**
|
||||
* Optional per-language argument-type prefixes for conversion-only
|
||||
* argument sentinels. When ranking cannot find any viable candidate
|
||||
* for a multi-overload set containing one of these sentinels, shared
|
||||
* narrowing suppresses the ambiguous set instead of falling back to
|
||||
* arity-only candidates. Languages without such sentinels leave this
|
||||
* undefined.
|
||||
*/
|
||||
readonly conversionOnlyArgTypePrefixes?: readonly string[];
|
||||
|
||||
/**
|
||||
* Optional predicate to identify definitions with file-local linkage
|
||||
* (e.g. C `static` functions). When provided, `pickUniqueGlobalCallable`
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export function emitFreeCallFallback(
|
|||
parsedFiles: readonly ParsedFile[],
|
||||
) => readonly SymbolDefinition[] | undefined;
|
||||
readonly conversionRankFn?: ConversionRankFn;
|
||||
readonly conversionOnlyArgTypePrefixes?: readonly string[];
|
||||
/** Optional per-language constraint hook threaded into
|
||||
* `narrowOverloadCandidates`. Drops candidates whose template
|
||||
* constraints (e.g. C++ `enable_if_t`, C++20 `requires`) provably
|
||||
|
|
@ -164,6 +165,7 @@ export function emitFreeCallFallback(
|
|||
if (fnDef === undefined) {
|
||||
fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, {
|
||||
conversionRankFn: options.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: options.constraintCompatibility,
|
||||
});
|
||||
fnDefFromImplicitThis = fnDef !== undefined;
|
||||
|
|
@ -191,6 +193,7 @@ export function emitFreeCallFallback(
|
|||
{
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: options.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: options.constraintCompatibility,
|
||||
},
|
||||
);
|
||||
|
|
@ -277,6 +280,7 @@ export function emitFreeCallFallback(
|
|||
const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: options.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: options.constraintCompatibility,
|
||||
});
|
||||
if (narrowed.length === 1) {
|
||||
|
|
@ -324,6 +328,7 @@ export function emitFreeCallFallback(
|
|||
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: options.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: options.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: options.constraintCompatibility,
|
||||
});
|
||||
if (narrowed.length === 1) {
|
||||
|
|
@ -380,6 +385,7 @@ export function emitFreeCallFallback(
|
|||
site.argumentTypeClasses,
|
||||
options.conversionRankFn,
|
||||
scopeDefsCache,
|
||||
options.conversionOnlyArgTypePrefixes,
|
||||
);
|
||||
}
|
||||
if (fnDef === undefined) continue;
|
||||
|
|
@ -576,6 +582,7 @@ export function pickUniqueGlobalCallable(
|
|||
callArgTypeClasses?: readonly ParameterTypeClass[],
|
||||
conversionRankFn?: ConversionRankFn,
|
||||
scopeDefsCache?: Map<string, readonly SymbolDefinition[]>,
|
||||
conversionOnlyArgTypePrefixes?: readonly string[],
|
||||
): SymbolDefinition | undefined {
|
||||
// The scope-index candidate list is a pure function of (name, callerFilePath):
|
||||
// the same-name bucket is fixed for the pass, the file-local filter depends
|
||||
|
|
@ -637,6 +644,7 @@ export function pickUniqueGlobalCallable(
|
|||
const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, {
|
||||
argumentTypeClasses: callArgTypeClasses,
|
||||
conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes,
|
||||
});
|
||||
if (narrowed.length === 1) return narrowed[0];
|
||||
}
|
||||
|
|
@ -678,6 +686,7 @@ export function pickUniqueGlobalCallable(
|
|||
const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, {
|
||||
argumentTypeClasses: callArgTypeClasses,
|
||||
conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes,
|
||||
});
|
||||
if (narrowed.length === 1) return narrowed[0];
|
||||
}
|
||||
|
|
@ -808,6 +817,7 @@ export function pickImplicitThisOverload(
|
|||
model: SemanticModel,
|
||||
hookCtx?: {
|
||||
readonly conversionRankFn?: ConversionRankFn;
|
||||
readonly conversionOnlyArgTypePrefixes?: readonly string[];
|
||||
readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
|
||||
},
|
||||
): SymbolDefinition | undefined {
|
||||
|
|
@ -840,6 +850,7 @@ export function pickImplicitThisOverload(
|
|||
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: hookCtx?.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: hookCtx?.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: hookCtx?.constraintCompatibility,
|
||||
});
|
||||
if (candidates.length !== 1) return undefined;
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ export interface OverloadNarrowingHookCtx {
|
|||
/** Conversion-rank scoring fallback (step 4b). Engages when the
|
||||
* exact-type filter rejects every candidate. */
|
||||
readonly conversionRankFn?: ConversionRankFn;
|
||||
/** Per-language argument-type prefixes whose conversion-rank failures
|
||||
* should suppress genuinely ambiguous multi-overload sets instead of
|
||||
* falling back to arity-only candidates. */
|
||||
readonly conversionOnlyArgTypePrefixes?: readonly string[];
|
||||
/** Constraint filter (step 4c). Drops candidates whose template
|
||||
* guards (SFINAE `enable_if_t`, C++20 `requires`, future Rust
|
||||
* trait bounds, etc.) provably fail at the call site. Three-valued
|
||||
|
|
@ -176,6 +180,12 @@ export function narrowOverloadCandidates(
|
|||
hookCtx.argumentTypeClasses,
|
||||
);
|
||||
if (ranked.length > 0) result = ranked;
|
||||
else if (
|
||||
candidates.length > 1 &&
|
||||
hasConversionOnlyArgType(argTypes, hookCtx.conversionOnlyArgTypePrefixes)
|
||||
) {
|
||||
result = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +232,14 @@ export function narrowOverloadCandidates(
|
|||
return result;
|
||||
}
|
||||
|
||||
function hasConversionOnlyArgType(
|
||||
argTypes: readonly string[],
|
||||
prefixes: readonly string[] | undefined,
|
||||
): boolean {
|
||||
if (prefixes === undefined || prefixes.length === 0) return false;
|
||||
return argTypes.some((type) => prefixes.some((prefix) => type.startsWith(prefix)));
|
||||
}
|
||||
|
||||
function exactTypeSlotMatches(
|
||||
argType: string,
|
||||
paramType: string,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ type ReceiverBoundProviderSubset = Pick<
|
|||
| 'resolveReceiverMember'
|
||||
| 'resolveThisViaEnclosingClass'
|
||||
| 'conversionRankFn'
|
||||
| 'conversionOnlyArgTypePrefixes'
|
||||
| 'constraintCompatibility'
|
||||
| 'isStaticOnly'
|
||||
>;
|
||||
|
|
@ -519,6 +520,7 @@ export function emitReceiverBoundCalls(
|
|||
{
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
},
|
||||
);
|
||||
|
|
@ -1275,6 +1277,7 @@ function pickOverload(
|
|||
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
});
|
||||
// When narrowing leaves >1 candidate that share identical normalized
|
||||
|
|
@ -1382,6 +1385,7 @@ function pickFirstNonStaticOnly(
|
|||
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
});
|
||||
// Same ambiguity handling as `pickOverload`: when normalization
|
||||
|
|
@ -1427,6 +1431,7 @@ function recordReceiverOverloadSuppression(
|
|||
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
});
|
||||
const reason: ResolutionSuppressionReason = isOverloadAmbiguousAfterNormalization(
|
||||
|
|
|
|||
|
|
@ -741,6 +741,7 @@ export function runScopeResolution(
|
|||
isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller,
|
||||
resolveAdlCandidates: provider.resolveAdlCandidates,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
recordResolutionOutcome,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
methodToTypeArgPosition,
|
||||
type TypeArgPosition,
|
||||
} from './shared.js';
|
||||
import { CPP_BRACED_INIT_TYPE_PREFIX } from '../languages/cpp/conversion-rank.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set(['declaration']);
|
||||
|
||||
|
|
@ -479,6 +480,8 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
/** Infer the type of a literal AST node for C++ overload disambiguation. */
|
||||
const inferLiteralType: LiteralTypeInferrer = (node) => {
|
||||
switch (node.type) {
|
||||
case 'initializer_list':
|
||||
return inferBracedInitLiteralType(node);
|
||||
case 'number_literal': {
|
||||
const t = node.text;
|
||||
// Float suffixes
|
||||
|
|
@ -505,6 +508,23 @@ const inferLiteralType: LiteralTypeInferrer = (node) => {
|
|||
}
|
||||
};
|
||||
|
||||
function inferBracedInitLiteralType(node: SyntaxNode): string | undefined {
|
||||
const elementTypes: string[] = [];
|
||||
for (const child of node.children) {
|
||||
if (child.type === ',' || child.type === '{' || child.type === '}') continue;
|
||||
const elementType = inferLiteralType(child);
|
||||
if (elementType === undefined || elementType.startsWith(CPP_BRACED_INIT_TYPE_PREFIX)) {
|
||||
return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length + 1}`;
|
||||
}
|
||||
elementTypes.push(elementType);
|
||||
}
|
||||
if (elementTypes.length === 0) return `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:0`;
|
||||
const first = elementTypes[0];
|
||||
return elementTypes.every((type) => type === first)
|
||||
? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}:${elementTypes.length}`
|
||||
: `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length}`;
|
||||
}
|
||||
|
||||
/** C++: detect constructor type from smart pointer factory calls (make_shared<Dog>()).
|
||||
* Extracts the template type argument as the constructor type for virtual dispatch. */
|
||||
const detectCppConstructorType: ConstructorTypeDetector = (node, classNames) => {
|
||||
|
|
|
|||
69
gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp
vendored
Normal file
69
gitnexus/test/fixtures/lang-resolution/cpp-braced-init-list-overload/service.cpp
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace std {
|
||||
template <typename T>
|
||||
class initializer_list {};
|
||||
|
||||
template <typename T>
|
||||
class vector {};
|
||||
|
||||
class string {};
|
||||
}
|
||||
|
||||
class InitListService {
|
||||
public:
|
||||
void consume(std::initializer_list<int> values) {}
|
||||
void consume(int value) {}
|
||||
|
||||
void consumeVector(std::vector<int> values) {}
|
||||
void consumeVector(int value) {}
|
||||
|
||||
void consumeScalarOrVector(std::vector<int> values) {}
|
||||
void consumeScalarOrVector(int value) {}
|
||||
|
||||
void consumeStringVectorMismatch(std::vector<int> values) {}
|
||||
void consumeStringVectorMismatch(std::string value) {}
|
||||
|
||||
void consumeMixed(std::initializer_list<int> values) {}
|
||||
void consumeMixed(std::initializer_list<double> values) {}
|
||||
|
||||
void consumeEmpty(std::initializer_list<int> values) {}
|
||||
void consumeEmpty(std::initializer_list<double> values) {}
|
||||
|
||||
void consumeSingleMixed(std::initializer_list<int> values) {}
|
||||
void consumeSingleEmpty(std::initializer_list<int> values) {}
|
||||
|
||||
void callHomogeneousInitList() {
|
||||
consume({1, 2, 3});
|
||||
}
|
||||
|
||||
void callHomogeneousVector() {
|
||||
consumeVector({1, 2, 3});
|
||||
}
|
||||
|
||||
void callSingleElementScalar() {
|
||||
consumeScalarOrVector({5});
|
||||
}
|
||||
|
||||
void callStringVectorMismatch() {
|
||||
consumeStringVectorMismatch({"a", "b"});
|
||||
}
|
||||
|
||||
void callHeterogeneousInitList() {
|
||||
consumeMixed({1, 2.0});
|
||||
}
|
||||
|
||||
void callEmptyInitList() {
|
||||
consumeEmpty({});
|
||||
}
|
||||
|
||||
void callSingleHeterogeneousInitList() {
|
||||
consumeSingleMixed({1, 2.0});
|
||||
}
|
||||
|
||||
void callSingleEmptyInitList() {
|
||||
consumeSingleEmpty({});
|
||||
}
|
||||
};
|
||||
|
|
@ -1230,6 +1230,70 @@ describe('C++ overload disambiguation by parameter types', () => {
|
|||
|
||||
// ── Phase P: Same-arity overloads — cross-file + chain resolution ─────────
|
||||
|
||||
describe('C++ braced-init-list overload disambiguation (#1899 A8 conservative)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-braced-init-list-overload'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
const callsFrom = (source: string, target: string) =>
|
||||
getRelationships(result, 'CALLS').filter(
|
||||
(edge) => edge.source === source && edge.target === target,
|
||||
);
|
||||
|
||||
const singleTargetParameterTypes = (source: string, target: string) => {
|
||||
const calls = callsFrom(source, target);
|
||||
expect(calls).toHaveLength(1);
|
||||
const [call] = calls;
|
||||
expect(call).toBeDefined();
|
||||
return call === undefined
|
||||
? undefined
|
||||
: result.graph.getNode(call.rel.targetId)?.properties.parameterTypes;
|
||||
};
|
||||
|
||||
it('resolves homogeneous literal braces to initializer_list overloads', () => {
|
||||
expect(singleTargetParameterTypes('callHomogeneousInitList', 'consume')).toEqual([
|
||||
'std::initializer_list<int>',
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves homogeneous literal braces to container overloads', () => {
|
||||
expect(singleTargetParameterTypes('callHomogeneousVector', 'consumeVector')).toEqual([
|
||||
'std::vector<int>',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prefers a scalar overload for single-element braced-init lists', () => {
|
||||
expect(singleTargetParameterTypes('callSingleElementScalar', 'consumeScalarOrVector')).toEqual([
|
||||
'int',
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects container overloads whose value type cannot accept the braced elements', () => {
|
||||
expect(callsFrom('callStringVectorMismatch', 'consumeStringVectorMismatch')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('suppresses heterogeneous braced-init lists instead of guessing an element type', () => {
|
||||
expect(callsFrom('callHeterogeneousInitList', 'consumeMixed')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('suppresses empty braced-init lists instead of guessing an element type', () => {
|
||||
expect(callsFrom('callEmptyInitList', 'consumeEmpty')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('preserves single-overload heterogeneous braced-init recall', () => {
|
||||
expect(callsFrom('callSingleHeterogeneousInitList', 'consumeSingleMixed')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('preserves single-overload empty braced-init recall', () => {
|
||||
expect(callsFrom('callSingleEmptyInitList', 'consumeSingleEmpty')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ same-arity overload cross-file and chain resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import type { ParameterTypeClass, SymbolDefinition } from 'gitnexus-shared';
|
||||
import { cppConversionRank } from '../../../../src/core/ingestion/languages/cpp/conversion-rank.js';
|
||||
import {
|
||||
CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
|
||||
cppConversionRank,
|
||||
} from '../../../../src/core/ingestion/languages/cpp/conversion-rank.js';
|
||||
import {
|
||||
clearCppUserDefinedConversions,
|
||||
registerCppUserDefinedConversion,
|
||||
|
|
@ -102,6 +105,68 @@ describe('cppConversionRank user-defined conversion ranks (#1631)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cppConversionRank braced-init-list ranks (#1899)', () => {
|
||||
it('ranks homogeneous braced-init lists toward initializer_list and containers', () => {
|
||||
expect(cppConversionRank('braced-init:int:3', 'std::initializer_list<int>')).toBe(0);
|
||||
expect(cppConversionRank('braced-init:int:3', 'std::vector<int>')).toBe(4);
|
||||
expect(cppConversionRank('braced-init:int', 'int')).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('uses element count and type before ranking container targets', () => {
|
||||
expect(cppConversionRank('braced-init:int:1', 'int')).toBe(0);
|
||||
expect(cppConversionRank('braced-init:int:1', 'std::vector<int>')).toBe(4);
|
||||
expect(cppConversionRank('braced-init:string:2', 'std::vector<int>')).toBe(Infinity);
|
||||
expect(
|
||||
cppConversionRank('braced-init:int:3', 'std::vector', value('braced-init:int:3'), {
|
||||
...value('std::vector'),
|
||||
templateArguments: ['int'],
|
||||
}),
|
||||
).toBe(4);
|
||||
});
|
||||
|
||||
it('suppresses unknown braced-init lists when conversion ranking finds no viable target', () => {
|
||||
const byIntList = mkDef(
|
||||
'f:int-list',
|
||||
['std::initializer_list<int>'],
|
||||
[value('std::initializer_list')],
|
||||
);
|
||||
const byDoubleList = mkDef(
|
||||
'f:double-list',
|
||||
['std::initializer_list<double>'],
|
||||
[value('std::initializer_list')],
|
||||
);
|
||||
|
||||
const result = narrowOverloadCandidates(
|
||||
[byIntList, byDoubleList],
|
||||
1,
|
||||
['braced-init:unknown:2'],
|
||||
{
|
||||
argumentTypeClasses: [value('braced-init:unknown:2')],
|
||||
conversionRankFn: cppConversionRank,
|
||||
conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('preserves single-candidate recall for unrankable braced-init lists', () => {
|
||||
const byIntList = mkDef(
|
||||
'f:int-list',
|
||||
['std::initializer_list<int>'],
|
||||
[value('std::initializer_list')],
|
||||
);
|
||||
|
||||
const result = narrowOverloadCandidates([byIntList], 1, ['braced-init:unknown:2'], {
|
||||
argumentTypeClasses: [value('braced-init:unknown:2')],
|
||||
conversionRankFn: cppConversionRank,
|
||||
conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
|
||||
});
|
||||
|
||||
expect(result.map((d) => d.nodeId)).toEqual(['f:int-list']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('narrowOverloadCandidates with C++ pointer-rank sidecars (#1637)', () => {
|
||||
it('selects pointer overload for nullptr over bool overload', () => {
|
||||
const byPointer = mkDef('f:intptr', ['int'], [pointer('int')]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue