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
4 changes: 4 additions & 0 deletions gitnexus-shared/src/scope-resolution/symbol-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export interface SymbolDefinition {
* `ScopeResolver.constraintCompatibility` hook during overload narrowing.
* Absent for symbols that have no constraints (the common case). */
templateConstraints?: unknown;
/** True when the producing language marked this callable as explicit.
* Currently used by C++ overload ranking to exclude explicit constructors
* from implicit user-defined conversion candidates. */
isExplicit?: boolean;
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
ownerId?: string;
}
21 changes: 21 additions & 0 deletions gitnexus/src/core/ingestion/languages/cpp/captures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export function emitCppScopeCaptures(
JSON.stringify(arity.parameterTypeClasses),
);
}
if (hasExplicitSpecifier(fnNode)) {
grouped['@declaration.is-explicit'] = syntheticCapture(
'@declaration.is-explicit',
fnNode,
'true',
);
}

// Detect static storage class (file-local linkage)
if (hasStaticStorageClass(fnNode)) {
Expand Down Expand Up @@ -1542,6 +1549,20 @@ function extractDeclaratorLeafName(node: SyntaxNode): string | null {
return null;
}

/**
* Check if a C++ declaration has an `explicit` specifier. Tree-sitter-cpp
* exposes `explicit` as a direct keyword child on constructor declarations in
* current grammar builds; the bounded text prefix keeps this resilient across
* small grammar shape differences without scanning whole function bodies.
*/
function hasExplicitSpecifier(node: SyntaxNode): boolean {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child !== null && child.text === 'explicit') return true;
}
return /\bexplicit\b/.test(node.text.slice(0, 128));
}

/**
* Check if a C++ function_definition or declaration has `static` storage class.
*/
Expand Down
10 changes: 7 additions & 3 deletions gitnexus/src/core/ingestion/languages/cpp/conversion-rank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
* - rank 2: standard conversion (arithmetic, nullptr -> T*, T* -> bool,
* T* -> void*)
* - rank 3: nullptr -> bool (kept worse than nullptr -> T*)
* - rank 4: ellipsis conversion (worst viable)
* - rank 4: user-defined conversion (one-step, conservative)
* - rank 5: ellipsis conversion (worst viable)
* - Infinity: mismatch (string -> int, user types, unsupported shapes)
*
* This function is intentionally C++-specific. Other languages may define
* their own `ConversionRankFn` in the future.
*/

import type { ParameterTypeClass } from 'gitnexus-shared';
import { hasCppUserDefinedConversion } from './user-defined-conversions.js';

/** Set of normalized arithmetic types that support implicit conversion. */
const ARITHMETIC = new Set(['int', 'double', 'char', 'bool']);
Expand All @@ -34,7 +36,8 @@ const INTEGRAL_PROMOTION = new Map([
* Return the conversion rank from `argType` to `paramType`.
*
* @returns 0 for exact match, 1 for integral promotion, 2 for standard
* conversion, 3 for nullptr -> bool, 4 for ellipsis, Infinity
* conversion, 3 for nullptr -> bool, 4 for user-defined conversion,
* 5 for ellipsis, Infinity
* for mismatch.
*/
export function cppConversionRank(
Expand All @@ -46,13 +49,14 @@ export function cppConversionRank(
if (argType === paramType) {
return exactShapeCompatible(argTypeClass, paramTypeClass) ? 0 : Infinity;
}
if (paramType === '...') return 4;
if (paramType === '...') return 5;
if (INTEGRAL_PROMOTION.get(argType) === paramType) return 1;
if (ARITHMETIC.has(argType) && ARITHMETIC.has(paramType)) return 2;
if (argType === 'null' && isPointer(paramTypeClass)) return 2;
if (argType === 'null' && paramType === 'bool') return 3;
if (isPointer(argTypeClass) && paramType === 'bool') return 2;
if (isPointer(argTypeClass) && isPointer(paramTypeClass) && paramType === 'void') return 2;
if (hasCppUserDefinedConversion(argType, paramType)) return 4;
return Infinity;
}

Expand Down
6 changes: 6 additions & 0 deletions gitnexus/src/core/ingestion/languages/cpp/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,12 @@ const CPP_SCOPE_QUERY = `
declarator: (function_declarator
declarator: (field_identifier) @declaration.name))) @declaration.method

;; Constructor prototype in class body: User(int id);
(field_declaration_list
(declaration
declarator: (function_declarator
declarator: (identifier) @declaration.name)) @declaration.method)

;; Method prototype with reference return: User& getRef();
(field_declaration
declarator: (reference_declarator
Expand Down
9 changes: 9 additions & 0 deletions gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import {
} from './inline-namespaces.js';
import { populateCppRangeBindings } from './range-bindings.js';
import { cppConstraintCompatibility } from './constraint-filter.js';
import {
clearCppUserDefinedConversions,
populateCppUserDefinedConversions,
} from './user-defined-conversions.js';

/**
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
Expand Down Expand Up @@ -61,6 +65,7 @@ export const cppScopeResolver: ScopeResolver = {
clearCppDependentBases();
clearCppAdlState();
clearCppInlineNamespaces();
clearCppUserDefinedConversions();
return scanCppHeaderFiles(repoPath);
},

Expand Down Expand Up @@ -110,6 +115,10 @@ export const cppScopeResolver: ScopeResolver = {
// by ADL (U2 of plan 2026-05-13-001) to identify each argument type's
// associated namespace for Koenig lookup.
populateCppAssociatedNamespaces(parsed);
// Build conservative one-step user-defined conversion facts for
// overload ranking (#1631): implicit converting constructors only,
// with no chaining or conversion-operator handling.
populateCppUserDefinedConversions(parsed);
},

// Resolve recorded template-class → dependent-base simple names to
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
import type { ScopeId } from 'gitnexus-shared';
import { normalizeCppParamType } from './arity-metadata.js';

const userDefinedConversions = new Set<string>();
const pendingUserDefinedConversions: PendingUserDefinedConversion[] = [];
const classIdentitiesBySimpleName = new Map<string, Set<string>>();

interface PendingUserDefinedConversion {
readonly argType: string;
readonly paramType: string;
readonly ownerClassName: string;
}

export function clearCppUserDefinedConversions(): void {
userDefinedConversions.clear();
pendingUserDefinedConversions.length = 0;
classIdentitiesBySimpleName.clear();
}

export function hasCppUserDefinedConversion(argType: string, paramType: string): boolean {
return userDefinedConversions.has(conversionKey(argType, paramType));
}

export function populateCppUserDefinedConversions(parsed: ParsedFile): void {
const scopesById = new Map<ScopeId, (typeof parsed.scopes)[number]>();
for (const scope of parsed.scopes) scopesById.set(scope.id, scope);

for (const classScope of parsed.scopes) {
if (classScope.kind !== 'Class') continue;
const classDef = classScope.ownedDefs.find(isClassLike);
if (classDef !== undefined) recordClassIdentity(classDef);
}

for (const classScope of parsed.scopes) {
if (classScope.kind !== 'Class') continue;
const classDef = classScope.ownedDefs.find(isClassLike);
if (classDef === undefined) continue;
const className = normalizedSimpleName(classDef);
if (className === '') continue;

const methodDefs = collectClassMethodDefs(classScope.id, parsed, scopesById);
for (const def of methodDefs) {
const simpleName = simpleNameOf(def);
if (simpleName === className && def.parameterTypes?.length === 1) {
if (def.isExplicit === true) continue;
registerPendingCppUserDefinedConversion(def.parameterTypes[0], className, className);
}
}
}

rebuildCppUserDefinedConversions();
}

export function registerCppUserDefinedConversion(argType: string, paramType: string): void {
if (argType === '' || paramType === '') return;
if (argType === paramType) return;
userDefinedConversions.add(conversionKey(argType, paramType));
}

function collectClassMethodDefs(
classScopeId: ScopeId,
parsed: ParsedFile,
scopesById: ReadonlyMap<ScopeId, (typeof parsed.scopes)[number]>,
): SymbolDefinition[] {
const methods: SymbolDefinition[] = [];
const classScope = scopesById.get(classScopeId);
if (classScope === undefined) return methods;

for (const def of classScope.ownedDefs) {
if (isCallableMember(def)) methods.push(def);
}
for (const scope of parsed.scopes) {
if (scope.parent !== classScopeId) continue;
if (scope.kind === 'Class') continue;
for (const def of scope.ownedDefs) {
if (isCallableMember(def)) methods.push(def);
}
}
return methods;
}

function conversionKey(argType: string, paramType: string): string {
return `${argType}\0${paramType}`;
}

function registerPendingCppUserDefinedConversion(
argType: string,
paramType: string,
ownerClassName: string,
): void {
if (argType === '' || paramType === '') return;
if (argType === paramType) return;
pendingUserDefinedConversions.push({ argType, paramType, ownerClassName });
}

function rebuildCppUserDefinedConversions(): void {
userDefinedConversions.clear();
for (const conversion of pendingUserDefinedConversions) {
if (isAmbiguousClassName(conversion.ownerClassName)) continue;
userDefinedConversions.add(conversionKey(conversion.argType, conversion.paramType));
}
}

function recordClassIdentity(def: SymbolDefinition): void {
const simpleName = normalizedSimpleName(def);
if (simpleName === '') return;
const identities = classIdentitiesBySimpleName.get(simpleName) ?? new Set<string>();
identities.add(normalizedQualifiedClassName(def));
classIdentitiesBySimpleName.set(simpleName, identities);
}

function isAmbiguousClassName(simpleName: string): boolean {
return (classIdentitiesBySimpleName.get(simpleName)?.size ?? 0) > 1;
}

function normalizedQualifiedClassName(def: SymbolDefinition): string {
const qualifiedName = def.qualifiedName ?? simpleNameOf(def);
if (qualifiedName === '' || !qualifiedName.includes('.')) return `${def.filePath}:${def.nodeId}`;
return qualifiedName
.split('.')
.map((part) => normalizeCppParamType(part))
.join('.');
}

function normalizedSimpleName(def: SymbolDefinition): string {
return normalizeCppParamType(simpleNameOf(def));
}

function simpleNameOf(def: SymbolDefinition): string {
return def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
}

function isClassLike(def: SymbolDefinition): boolean {
return def.type === 'Class' || def.type === 'Struct' || def.type === 'Interface';
}

function isCallableMember(def: SymbolDefinition): boolean {
return def.type === 'Method' || def.type === 'Constructor';
}
10 changes: 10 additions & 0 deletions gitnexus/src/core/ingestion/scope-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ function buildDefFromDeclarationMatch(
const declaredType = match['@declaration.field-type']?.text;
const returnType = match['@declaration.return-type']?.text;
const templateConstraints = parseJsonCapture(match['@declaration.template-constraints']);
const isExplicit = parseBooleanCapture(match['@declaration.is-explicit']);

return {
nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
Expand All @@ -588,6 +589,7 @@ function buildDefFromDeclarationMatch(
...(returnType !== undefined ? { returnType } : {}),
...(templateArguments !== undefined ? { templateArguments } : {}),
...(templateConstraints !== undefined ? { templateConstraints } : {}),
...(isExplicit === true ? { isExplicit: true } : {}),
};
}

Expand All @@ -610,6 +612,13 @@ function parseIntCapture(cap: { readonly text: string } | undefined): number | u
return Number.isFinite(n) ? n : undefined;
}

function parseBooleanCapture(cap: { readonly text: string } | undefined): boolean | undefined {
if (cap === undefined) return undefined;
if (cap.text === 'true') return true;
if (cap.text === 'false') return false;
return undefined;
}

function parseJsonParameterTypeClassesCapture(
cap: { readonly text: string } | undefined,
): ParameterTypeClass[] | undefined {
Expand Down Expand Up @@ -1079,6 +1088,7 @@ const KNOWN_SUB_TAGS: ReadonlySet<string> = new Set<string>([
'@declaration.parameter-types',
'@declaration.parameter-type-classes',
'@declaration.template-constraints',
'@declaration.is-explicit',
]);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#include "lib.h"

namespace alpha {

Other::Other(int value) {}
void Service::f(Token value) {}
void Service::f(Other value) {}

} // namespace alpha

namespace beta {

Token::Token(int value) {}

} // namespace beta
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once

namespace alpha {

class Token {};

class Other {
public:
Other(int value);
};

class Service {
public:
void f(Token value);
void f(Other value);

void run() {
f(42);
}
};

} // namespace alpha

namespace beta {

class Token {
public:
Token(int value);
};

} // namespace beta
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include "lib.h"

Wrap::Wrap(int value) {}
WrapA::WrapA(int value) {}
WrapB::WrapB(int value) {}
ExplicitWrap::ExplicitWrap(int value) {}

void Service::f(Wrap value) {}
void Service::f(double value) {}
void Service::g(Wrap value) {}
void Service::h(WrapA value) {}
void Service::h(WrapB value) {}
void Service::e(Wrap value) {}
void Service::e(ExplicitWrap value) {}
Loading
Loading