Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions gitnexus/src/core/group/extractors/http-patterns/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions gitnexus/src/core/ingestion/method-extractors/configs/c-cpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
});
Expand All @@ -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,
});
Expand All @@ -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,
});
Expand Down
3 changes: 2 additions & 1 deletion gitnexus/src/core/ingestion/method-types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
}
Expand Down
10 changes: 9 additions & 1 deletion gitnexus/src/core/ingestion/parsing-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
typeTagForId,
constTagForId,
buildCollisionGroups,
parameterShapeIdTag,
} from './utils/method-props.js';
import {
extractTemplateArguments,
Expand Down Expand Up @@ -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?.({
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,6 +77,7 @@ export function resolveDefGraphId(
qualifiedName?: string;
type?: NodeLabel;
parameterTypes?: readonly string[];
parameterTypeClasses?: readonly ParameterTypeClass[];
templateArguments?: readonly string[];
templateConstraints?: unknown;
},
Expand All @@ -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
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand All @@ -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
Expand Down Expand Up @@ -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)`)
Expand All @@ -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
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading