mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
Merge branch 'main' into fix/issue-1518-docker-local-path
This commit is contained in:
commit
aa7563bc13
14 changed files with 392 additions and 16 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
13
gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-pointer/main.cpp
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-pointer/main.cpp
vendored
Normal 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);
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-tied/main.cpp
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/cpp-template-partial-order-tied/main.cpp
vendored
Normal 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);
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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*']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue