mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
fix(cpp): scope-aware isSuperReceiver classification (U1)
The C++ isSuperReceiver hook used a regex `/^[A-Z]\w*::/` that misclassified any uppercase-qualified call as a super-receiver call. Singleton::getInstance(), std::Foo::bar(), and PascalCase namespace calls all entered the super branch, where the absence of an enclosing class (or wrong MRO context) dropped the resolution entirely. Fix: - New optional ScopeResolver hook isSuperReceiverInContext(text, callerScope, scopes). Languages where super classification depends on caller context define it; receiver-bound-calls.ts prefers it when defined and falls back to the simple isSuperReceiver(text) otherwise. Other migrated languages (Python, Java, C#, PHP, Go, TypeScript) are unchanged. - C++ implementation: parse the LHS of '::' from the receiver text, resolve via findClassBindingInScope, and return true only when the LHS is a class-like def in the caller's enclosing class's MRO. Returns false for namespace LHS, unresolved LHS, self-class LHS (qualified self-calls aren't super), and any non-'::' form. - Extended the C++ tree-sitter query to capture the LHS of qualified_identifier as @reference.receiver so qualified static member calls (Singleton::getInstance()) reach the receiver-bound Case 2 (class-name receiver) path. Without the receiver capture, qualified calls had no explicit receiver and could not resolve through any receiver-bound branch. Test: cpp-namespace-qualified-not-super fixture. Singleton::getInstance() from a free function asserts exactly 1 CALLS edge through the qualified-call path. Passes under both REGISTRY_PRIMARY_CPP=1 and =0. All 2113 resolver integration tests pass; all 147 cpp tests pass under both modes.
This commit is contained in:
parent
0837329aae
commit
6a1e2a9ed0
7 changed files with 133 additions and 5 deletions
|
|
@ -406,9 +406,15 @@ const CPP_SCOPE_QUERY = `
|
|||
(call_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; ─── References — qualified calls (Namespace::func()) ───────────────
|
||||
;; ─── References — qualified calls (Namespace func or Class method) ───
|
||||
;; Capture the LHS of scope-resolution as the explicit receiver so
|
||||
;; qualified static member calls route through receiver-bound-calls
|
||||
;; Case 2 (class-name receiver) path. Without the receiver capture,
|
||||
;; qualified calls have no explicit receiver and class methods cannot
|
||||
;; resolve through receiver-bound paths.
|
||||
(call_expression
|
||||
function: (qualified_identifier
|
||||
scope: (_) @reference.receiver
|
||||
name: (identifier) @reference.name)) @reference.call.qualified
|
||||
|
||||
;; ─── References — member calls (obj.method() / ptr->method()) ───────
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
|
||||
import { findClassBindingInScope, findEnclosingClassDef } from '../../scope-resolution/scope/walkers.js';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
|
|
@ -76,9 +77,44 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
populateCppNonGloballyVisible(parsed);
|
||||
},
|
||||
|
||||
isSuperReceiver: (text) =>
|
||||
// C++ super patterns: explicit base class call `Base::method()`
|
||||
/^[A-Z]\w*::/.test(text),
|
||||
// Simple `isSuperReceiver` returns false for C++. Real super
|
||||
// classification is caller-context-dependent and lives in
|
||||
// `isSuperReceiverInContext` below — without scope context the
|
||||
// previous regex `/^[A-Z]\w*::/` misclassified namespace-qualified
|
||||
// calls (e.g., `Singleton::getInstance()`) as super calls and routed
|
||||
// them through the wrong resolution branch.
|
||||
isSuperReceiver: () => false,
|
||||
|
||||
isSuperReceiverInContext: (text, callerScope, scopes) => {
|
||||
// Extract LHS of `::`. C++ super calls take the form `Base::method()`
|
||||
// where `Base` is a direct or indirect base of the caller's
|
||||
// enclosing class. Anything not in `Class::` form is not a super
|
||||
// call.
|
||||
const sepIdx = text.indexOf('::');
|
||||
if (sepIdx <= 0) return false;
|
||||
const lhs = text.slice(0, sepIdx).trim();
|
||||
if (lhs.length === 0) return false;
|
||||
|
||||
// Resolve the LHS in the caller's scope chain. Only class-like
|
||||
// resolutions can be super receivers; Namespace and unresolved
|
||||
// names are not super calls.
|
||||
const lhsDef = findClassBindingInScope(callerScope, lhs, scopes);
|
||||
if (lhsDef === undefined) return false;
|
||||
|
||||
// The caller must have an enclosing class — super calls only make
|
||||
// sense inside a class body. Free functions can use `ClassName::`
|
||||
// for namespace-qualified calls but those are not super.
|
||||
const enclosing = findEnclosingClassDef(callerScope, scopes);
|
||||
if (enclosing === undefined) return false;
|
||||
|
||||
// `lhsDef` must be in the caller's MRO (i.e., the caller's enclosing
|
||||
// class derives from it). The class itself counts as its own MRO
|
||||
// root — `Self::method()` is a qualified self-call, not a super
|
||||
// call, so exclude the caller's own class.
|
||||
if (lhsDef.nodeId === enclosing.nodeId) return false;
|
||||
const mro = scopes.methodDispatch.mroFor(enclosing.nodeId);
|
||||
return mro.includes(lhsDef.nodeId);
|
||||
},
|
||||
|
||||
// C++ is statically typed — disable field fallback heuristic
|
||||
fieldFallbackOnMethodLookup: false,
|
||||
|
|
|
|||
|
|
@ -432,9 +432,47 @@ export interface ScopeResolver {
|
|||
* `/^super\s*\(/.test(t)`. Java returns `t === 'super'`. C++ may
|
||||
* also need `this` capture. Languages without inheritance return
|
||||
* constant `false`.
|
||||
*
|
||||
* For languages where the answer depends on caller context (e.g.
|
||||
* C++, where `Base::method()` is a super call ONLY when `Base` is
|
||||
* actually a base of the caller's enclosing class, and namespace-
|
||||
* qualified calls like `Singleton::getInstance()` must NOT be
|
||||
* misclassified), implement the optional `isSuperReceiverInContext`
|
||||
* variant below. The receiver-bound-calls pass prefers the context-
|
||||
* aware variant when both are defined.
|
||||
*/
|
||||
isSuperReceiver(receiverText: string): boolean;
|
||||
|
||||
/**
|
||||
* Optional context-aware variant of `isSuperReceiver`. When defined,
|
||||
* the receiver-bound-calls pass prefers this hook over the simple
|
||||
* `isSuperReceiver(text)` form. Languages where super classification
|
||||
* is purely text-driven (Python, Java, PHP) omit this hook and the
|
||||
* simple form is used unchanged.
|
||||
*
|
||||
* C++ uses this to distinguish `Base::method()` (super call when
|
||||
* `Base` is in the caller's MRO) from `Singleton::getInstance()`
|
||||
* (ordinary namespace-qualified call). Without this, the regex
|
||||
* heuristic `/^[A-Z]\w*::/` misclassifies any uppercase-qualified
|
||||
* call as a super-receiver call and routes it through the wrong
|
||||
* resolution branch.
|
||||
*
|
||||
* Returns `true` ONLY when:
|
||||
* - the receiver text parses as `<Name>::<...>` (or another super-
|
||||
* form the language recognizes), AND
|
||||
* - `<Name>` resolves (via scope chain) to a class-like def, AND
|
||||
* - that class is in the MRO of the caller's enclosing class.
|
||||
*
|
||||
* Returns `false` for namespace-qualified calls, unresolved names,
|
||||
* class-qualified calls where the class is NOT in the caller's MRO,
|
||||
* and any text the simple `isSuperReceiver` hook also rejects.
|
||||
*/
|
||||
readonly isSuperReceiverInContext?: (
|
||||
receiverText: string,
|
||||
callerScope: ScopeId,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
) => boolean;
|
||||
|
||||
// ─── Optional toggles ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import { narrowOverloadCandidates, isOverloadAmbiguousAfterNormalization } from
|
|||
type ReceiverBoundProviderSubset = Pick<
|
||||
ScopeResolver,
|
||||
| 'isSuperReceiver'
|
||||
| 'isSuperReceiverInContext'
|
||||
| 'fieldFallbackOnMethodLookup'
|
||||
| 'collapseMemberCallsByCallerTarget'
|
||||
| 'unwrapCollectionAccessor'
|
||||
|
|
@ -162,7 +163,14 @@ export function emitReceiverBoundCalls(
|
|||
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
||||
|
||||
// ── super branch ─────────────────────────────────────────────
|
||||
if (provider.isSuperReceiver(receiverName)) {
|
||||
// Languages with caller-context-dependent super classification
|
||||
// (C++) define `isSuperReceiverInContext`; we prefer it. Simple
|
||||
// text-only languages (Python, Java, PHP) use the plain hook.
|
||||
const isSuper =
|
||||
provider.isSuperReceiverInContext !== undefined
|
||||
? provider.isSuperReceiverInContext(receiverName, site.inScope, scopes)
|
||||
: provider.isSuperReceiver(receiverName);
|
||||
if (isSuper) {
|
||||
const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
|
||||
if (enclosingClass !== undefined) {
|
||||
// For super-receiver dispatch (`parent::`, `base.`, `super()`),
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/cpp-namespace-qualified-not-super/caller.cpp
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-namespace-qualified-not-super/caller.cpp
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#include "singleton.h"
|
||||
|
||||
void run() {
|
||||
Singleton::getInstance();
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-namespace-qualified-not-super/singleton.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-namespace-qualified-not-super/singleton.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
class Singleton {
|
||||
public:
|
||||
static Singleton* getInstance();
|
||||
};
|
||||
|
|
@ -1825,3 +1825,32 @@ describe('C++ using-namespace std smoke test', () => {
|
|||
expect(intoShim.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// U1 (follow-up plan 2026-05-13-001): namespace-qualified or class-qualified
|
||||
// calls from outside that class MUST NOT be classified as super-receiver calls.
|
||||
// The `isSuperReceiverInContext` hook consults the caller's MRO.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ namespace-qualified call is not a super receiver', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-namespace-qualified-not-super'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('resolves Singleton::getInstance() from a free function (not as super call)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const getInstanceCalls = calls.filter(
|
||||
(c) => c.source === 'run' && c.target === 'getInstance',
|
||||
);
|
||||
// Exactly 1: routed through the normal qualified-call path, NOT the super
|
||||
// branch. Before the U1 fix the regex `/^[A-Z]\w*::/` matched Singleton::,
|
||||
// entered the super branch with no enclosing class, and dropped the edge.
|
||||
expect(getInstanceCalls.length).toBe(1);
|
||||
expect(getInstanceCalls[0].targetFilePath).toContain('singleton.h');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue