* fix(cpp): complete scope-resolution parity
* fix(ci): resolve formatting, lint errors for PR #1520
- prettier: format arity-metadata.ts, captures.ts, index.ts
- eslint: rename unused HEADER_GLOB to _HEADER_GLOB
- eslint: replace unsafe parser.parse() with parseSourceSafe()
- eslint: suppress intentional console.warn/log in sync.ts
- eslint: remove unused _it import alias in cpp.test.ts
* fix(ci): complete formatting, lint, and typecheck fixes
- prettier: format call-processor.ts, imported-return-types.ts,
include-extractor.test.ts, cpp-captures.test.ts, cpp-imports.test.ts
- eslint: suppress intentional console.warn in manifest-extractor.ts
- typecheck: restore 'thrift' in ContractType union (was accidentally
removed) and add thrift case to exhaustive switch in manifest-extractor
* fix(ci): revert unintended group module changes that broke tests
Restore types.ts, config-parser.ts, matching.ts, sync.ts, and
manifest-extractor.ts to upstream/main versions. The original commit
accidentally removed fields (thrift, workspace_deps, exclude_links_paths,
exclude_links_param_only_paths) from DetectConfig/MatchingConfig/ContractType
which are still referenced by matching.test.ts, config-parser.test.ts,
sync.test.ts and other integration tests.
This PR's scope is C++ scope-resolution parity only — group module
type definitions and logic should remain unchanged.
* fix(codeql): address security and quality alerts
- arity-metadata.ts, interpret.ts: replace single-pass template strip
regex (/<[^>]*>/g) with a while-loop to fully handle nested templates
like Map<List<int>> — resolves 'Incomplete multi-character sanitization'
- cpp.test.ts: remove unused vitest 'it' import since the file defines
its own 'it' via createResolverParityIt — resolves 'Assignment to constant'
- include-extractor.test.ts: use fs.mkdtempSync() instead of predictable
os.tmpdir()+Date.now() paths — resolves 'Insecure temporary file'
- interpret.ts: remove redundant 'name !== undefined' check (already
guaranteed by early return) — resolves 'Comparison between inconvertible types'
* review: address Claude review findings on PR #1520
- Findings 1-3 (BLOCKERS): restore include-extractor.ts and its test to
the main baseline. Block-comment fallback regression, suffix-resolve
false-positive suppression, and the four deleted regression tests
(#3-#6) are now back. These changes were unrelated to C++ scope
parity and should not have been in this PR.
- Finding 4 (MAJOR, partial): revert COMPOUND_RECEIVER_MAX_DEPTH 6 to
4. No C++ test exercises depth > 4 (cpp-chain-call uses a 2-hop
chain), so the bump risked silent regressions on other migrated
languages without justification. The wildcard-origin propagation in
imported-return-types.ts is retained — C++ #include and using
namespace both emit wildcard-origin bindings (cpp/import-decomposer
.ts:40,90), so wildcard propagation is causal to C++ parity.
- Finding 6: tighten write-access dedup test with exact per-field
counts (nameWrites = 2, addrWrites = 1) instead of total-count + sub
string containment, so a regression in one of the two name writes
can no longer be masked.
- Finding 8: skipped. Box-drawing characters in cpp/query.ts comments
match the established convention used in csharp/java/php query
files.
Finding 5 (int/long normalization tie-breaker) left as documented
follow-up — proper fix requires resolver-level tie-breaker logic and
risks regressing other arity-matching tests.
* fix(cpp): stop #include from leaking class methods and namespace members (U1)
The C++ registry-primary resolver was emitting impossible CALLS edges
for ordinary headers: an including file's unqualified save() resolved
to User::save and unqualified foo() resolved to ns::foo. Two leak
paths converged on localDefs:
1. expandCppWildcardNames (file-local-linkage.ts) iterated the
flattened localDefs and exported every simple tail, including
class-owned methods and namespace-contained symbols. Replaced with
a scope-aware filter: build nodeId -> owning Scope from
Scope.ownedDefs and skip defs whose owning scope is Namespace or
Class.
2. The shared global free-call fallback's pickUniqueGlobalCallable
walks the workspace registry by simple name and would still hit
class methods / namespace members even with wildcard expansion
fixed. Plugged the gap via the existing isFileLocalDef hook —
semantically 'logically invisible cross-file' — by tracking per-
file non-globally-visible nodeIds (populateCppNonGloballyVisible,
called from populateOwners) and adding an ownerId !== undefined
fast-path for class-owned defs.
Side fix in shared finalize-algorithm.ts: when wildcard expansion
resolves to a real target but produces zero propagating names, the
edge was dropped, taking the file-level IMPORTS edge with it.
Preserve the original wildcard edge so #include dependencies survive
even when the header exposes no unqualified bindings.
Tests: cpp-include-no-class-leak, cpp-include-no-namespace-leak, and
cpp-anon-ns-same-file-visible fixtures. Negative tests mode-gated to
REGISTRY_PRIMARY_CPP=1 via the expected-failures registry — legacy
DAG has no scope-aware filtering on the global fallback; backporting
is out of scope. All 2104 resolver integration tests pass under
registry-primary mode.
* fix(cpp): suppress receiver-bound CALLS when integer-width overloads collide (U2)
C++ arity-metadata normalizes int, long, short, unsigned, size_t to
'int' so single-candidate flows like 'process(42L)' match a 'long'-
typed parameter via loose matching. But when both 'process(int)' and
'process(long)' coexist as method overloads, they both end up with
parameterTypes=['int'] in the registry, and pickOverload's narrowing
returns 2 candidates with no way to disambiguate. The previous code
picked candidates[0] arbitrarily, emitting a CALLS edge to the wrong
overload roughly half the time.
Fix:
- Add isOverloadAmbiguousAfterNormalization in overload-narrowing.ts
that detects >1 candidate sharing identical parameterTypes sequences.
- Have pickOverload return a new OVERLOAD_AMBIGUOUS sentinel when this
fires.
- In the receiver-bound-calls loop, when pickOverload signals ambiguity,
suppress the edge AND add the site to handledSites so the late-stage
emitReferencesViaLookup pass does not re-emit the pre-resolved
reference. Without the handled-mark, the reference index still
carries a toDef and emits the same wrong edge.
Graph schema has no ambiguous-target edge model, so emitting two
edges (one per candidate) would require a separate schema change.
Zero-edge is the only safe outcome.
Other languages: the ambiguity check is a precondition gate, not a
behavior change for normal narrowing. Languages whose normalizers do
not collapse distinct types into a single token (verified by grep
over *-arity-metadata.ts) will never produce >1 candidate with
identical parameterTypes from genuinely distinct declarations, so
the branch is effectively C++-only in practice.
Test: cpp-overload-int-long fixture asserts exactly .toBe(0) CALLS
edges. Count=1 = arbitrary pick (the bug); count>1 = unsupported
ambiguous-edge model. Mode-gated to REGISTRY_PRIMARY_CPP=1 — legacy
DAG has no OVERLOAD_AMBIGUOUS wiring; backporting is out of scope.
All 2105 resolver integration tests pass under registry-primary; all
139 cpp tests pass under both modes (3 negative tests skipped in
legacy as documented).
* test(cpp): add integration coverage for anonymous-namespace, using-namespace conflict, and std-shim leakage (U3+U4+U5)
Three new end-to-end fixtures exercise the resolver pipeline against
scenarios that previously had only unit-level coverage or no coverage
at all (Claude review Finding 7):
U3 — cpp-anon-ns-cross-file:
helper.cpp declares 'namespace { void worker(); }' and calls it
internally. caller.cpp declares a separate 'void worker()' and calls
it. Asserts (a) the cross-file CALLS edge from caller's run() does
not target helper.cpp's anonymous-namespace worker, and (b) the
same-file edge from helper_entry() to its own worker still resolves
(positive guard against a 'no edges at all' regression making the
negative check vacuously pass). Includes a state-isolation guard
that re-runs the same fixture and asserts identical results,
proving clearFileLocalNames() is called by the pipeline entry.
U4 — cpp-using-namespace-conflict:
Two headers each declaring 'namespace a { foo() }' and
'namespace b { foo() }' respectively, plus a caller doing
'using namespace a; using namespace b; foo()'. Asserts exactly
zero CALLS edges. One edge = arbitrary pick (the bug); two edges
would require an ambiguous-target edge model GitNexus does not
have. Depends on U1 — without scope-aware filtering, both foo()s
would already be in the importer's wildcard binding set as simple
'foo', so the test would pass for the wrong reason.
U5 — cpp-using-namespace-std-smoke:
Fixture-local 'namespace std { void cout_write(); void println(); }'
shim rather than real <iostream> — captures the wildcard-leak
shape deterministically without depending on system-header modeling
stability (out of scope per plan). Asserts (a) the project-local
call resolves correctly, (b) no leak to shim STL symbols, and (c)
no CALLS/ACCESSES edges from the caller into std-shim.h at all.
Negative tests for U2/U4 mode-gated to REGISTRY_PRIMARY_CPP=1 via
the expected-failures registry; legacy DAG lacks the OVERLOAD_AMBIGUOUS
suppression and the namespace-aware filtering, so the leaks persist
there. All 2112 resolver integration tests pass under registry-primary;
all 146 cpp tests pass under both modes (4 negative tests skipped in
legacy as documented).
* chore(autofix): apply prettier + eslint fixes via /autofix command
* 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.
* fix(cpp): suppress receiver-bound CALLS when default-arg overloads collide (U4)
ISO C++ rejects 's.f(1)' as ambiguous when both 'void f(int)' and
'void f(int, int = 0)' are declared on S. The previous resolver
returned the first viable candidate via pickOverload's fallback.
Extended isOverloadAmbiguousAfterNormalization to take an optional
argCount: when provided, the predicate compares only the first
argCount slots of each candidate's parameterTypes. Candidates whose
declared-prefix matches up to argCount are treated as ambiguous
because default arguments make all of them equally viable for the
call.
Without argCount, behavior is unchanged (the original int/long
normalization-collapse contract, full-length equality required).
pickOverload now passes site.arity so default-arg ambiguity fires.
Test: cpp-overload-default-arg-ambiguous fixture. s.f(1) where S has
f(int) and f(int, int = 0) asserts exactly .toBe(0) CALLS edges.
Passes under both REGISTRY_PRIMARY_CPP=1 and =0.
All 2114 resolver integration tests pass; all 148 cpp tests pass
under both modes.
* fix(cpp): two-phase template lookup suppresses dependent-base members (U3)
ISO C++ two-phase name lookup: inside a class template body, unqualified
calls MUST NOT bind to members of a dependent base class. Only this->name
or Base<T>::name forms make the lookup dependent. GCC and Clang both
reject the unqualified form with 'declaration of f must be available'.
Before this fix, GitNexus's global free-call fallback walked the
workspace registry by simple name and bound unqualified calls inside
template bodies to dependent-base members, producing CALLS edges the
compiler would reject.
Implementation:
- New languages/cpp/two-phase-lookup.ts module: per-pipeline state
recording (className, dependentBaseName) pairs at capture time and
resolving them to nodeId sets during populateOwners.
- captures.ts detectCppDependentBases walks the AST once finding every
template_declaration containing a class/struct definition. For each,
it collects template-parameter names (typename T, class T, non-type
int N, template-template parameters) and walks each base in the
base_class_clause checking whether any inner type_identifier matches
a template parameter. Conservative bias: typename T::U, decltype,
and template-template-parameter shapes also classified as dependent.
- Extended scope-resolution contract's isCallableVisibleFromCaller
hook with optional callerScope and scopes fields. C++ implements
the hook to consult isCppDependentBaseMember: when the candidate
is a member of a dependent base of the caller's enclosing class,
the hook returns false and pickUniqueGlobalCallable skips the
candidate.
- clearFileLocalNames also clears the dependent-base state per
pipeline run.
Fixtures:
- cpp-two-phase-dependent-base: Derived<T> deriving from Base<T>,
unqualified f() and i inside Derived's body. Asserts zero CALLS
edges and zero ACCESSES edges respectively.
- cpp-two-phase-this-qualified, cpp-two-phase-non-dependent-base,
cpp-two-phase-namespace-free-call-inside-template: positive
fixtures left as documented gaps (this-> and qualified-name
resolution inside template bodies are pre-existing resolver
weaknesses independent of U3). Tracked separately.
Negative test mode-gated to REGISTRY_PRIMARY_CPP=1 via the expected-
failures registry; legacy DAG has no two-phase lookup.
All 2116 resolver integration tests pass under registry-primary; all
150 cpp tests pass under both modes (5 negative tests skipped in legacy
as documented).
* fix(cpp): implement V1 ADL (Koenig lookup) for free-function calls (U2)
Plan 2026-05-13-001 U2. Adds argument-dependent lookup as a new
candidate-generating tier in `emitFreeCallFallback`: when ordinary
unqualified lookup is empty, ADL surfaces candidates from each
value-class-typed argument's enclosing namespace.
V1 boundary (locked by cpp-adl-pointer-arg-boundary fixture):
- only direct enclosing-namespace closure
- only directly-named class-type values (pointer / reference / template-
spec args excluded; closure rules deferred to V2)
- ADL fires ONLY when ordinary lookup is empty (no union-and-resolve)
Parenthesized name `(f)(s)` suppresses ADL per ISO C++
[basic.lookup.argdep]/3.1. Multi-candidate ambiguity (e.g. `process(int)`
vs `process(long)` after C++ int-width normalization) returns the
ADL_AMBIGUOUS sentinel — caller suppresses entirely, mirroring the
OVERLOAD_AMBIGUOUS contract from plan 2026-05-12-002 U2.
Implementation:
- `cpp/adl.ts` — new module: per-pipeline argInfoBySite + noAdlSites Maps
populated at capture time, classToNamespaceQualifiedName Map populated
during populateOwners; `pickCppAdlCandidates` returns
SymbolDefinition | ADL_AMBIGUOUS | undefined
- `scope-resolution/contract/scope-resolver.ts` — adds optional
`resolveAdlCandidates` hook
- `scope-resolution/passes/free-call-fallback.ts` — invokes ADL hook
between `findCallableBindingInScope` and `pickUniqueGlobalCallable`;
marks site handled on `'ambiguous'` so emit-references doesn't retry
- `cpp/captures.ts` — detects `parenthesized_expression` function wrap;
per-arg classification (pointer/reference/value class) preserving the
shape info the existing arity-narrowing normalizer strips
- `cpp/scope-resolver.ts` — registers hook, populates associated
namespaces, clears state in loadResolutionConfig
Negative tests (parens, pointer-boundary, ambiguous) gated under
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp — legacy DAG has no V1/V2
ADL boundary or ADL_AMBIGUOUS suppression.
154/154 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
147 pass + 7 skipped under =0 (legacy parity baseline).
* fix(cpp): inline namespace transitive walking + qualified namespace resolution (U5)
Plan 2026-05-13-001 U5. Two ISO C++ inline-namespace semantics:
1. Unqualified-lookup transitive visibility: inline-namespace members
reach the enclosing namespace's scope as if declared there. The
`populateCppNonGloballyVisible` exemption keeps them globally visible
so cross-file unqualified lookup finds them.
2. Qualified-receiver transitive visibility: `outer::foo()` resolves to
`outer::v1::foo()` when `v1` is inline (and through arbitrarily-deep
nesting like `outer::v1::experimental::foo`, matching libc++ `__1` /
libstdc++ `__cxx11`).
The second behavior required a new resolver case in
`receiver-bound-calls.ts` (Case 1.5: language-specific qualified-receiver
member lookup) because C++ qualified-namespace member calls had no prior
resolution path — receiver-bound Case 1 only handled
`ParsedImport.kind === 'namespace'` (Python/JS-style) and Case 2 handles
class receivers, neither of which fired for `outer::foo()`. The new
hook `resolveQualifiedReceiverMember` is opt-in; languages without
C++-style qualified-name semantics omit it.
Implementation:
- `cpp/inline-namespaces.ts` — new module: per-pipeline
`inlineNamespaceRangesByFile` + `inlineNamespaceScopeIds` Sets;
`markCppInlineNamespaceRange` at capture time;
`populateCppInlineNamespaceScopes` resolves ranges → scope IDs;
`resolveCppQualifiedNamespaceMember` walks namespace scopes by simple
name and descends transitively through inline children only.
- `scope-resolution/contract/scope-resolver.ts` — adds optional
`resolveQualifiedReceiverMember` hook to the contract.
- `scope-resolution/passes/receiver-bound-calls.ts` — Case 1.5 invokes
the hook between Case 1 (namespace imports) and Case 2 (class-name
receiver). Returns undefined for non-namespace receivers so Case 2
still resolves class-qualified calls.
- `cpp/captures.ts` — detects `inline` keyword child on
`namespace_definition`; records 1-based range to match Scope.range.
- `cpp/file-local-linkage.ts` — `populateCppNonGloballyVisible` exempts
inline-namespace scopes so cross-file unqualified lookup keeps their
members visible.
- `cpp/scope-resolver.ts` — wires `populateCppInlineNamespaceScopes`
into populateOwners (BEFORE `populateCppNonGloballyVisible` so the
exemption sees populated state); registers
`resolveQualifiedReceiverMember` hook.
4 fixtures: `cpp-inline-namespace-unqualified`, `-versioned`,
`-nested` (two transitive inline hops, STL `__1` shape), and
`-adl-participation` (composes with U2 — ADL surfaces records declared
inside inline child namespaces). All 4 assert exactly 1 CALLS edge with
correct target file.
Versioned fixture gated under LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp
— legacy DAG can't disambiguate two same-name foos without inline
awareness. Other 3 coincidentally resolve in legacy.
158/158 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
150 pass + 8 skipped under =0 (legacy parity baseline).
* test(cpp): Phase 5 cross-unit composition tests for U1/U2/U3/U5
Plan 2026-05-13-001 Phase 5. Locks in correct behavior at the
intersections between the previously-shipped scope-resolver units.
Enhancement to U1: `isSuperReceiverInContext` strips template-argument
lists (`Base<T>` → `Base`) and namespace prefixes (`outer::v1::Base` →
`Base`) before resolving the receiver in the caller's scope chain. This
makes the super-receiver classification work for template-class
heritage shapes like `Base<T>::method()` and `outer::v1::Base<T>::f()`.
Three fixtures + four tests:
- `cpp-phase5-u1-u3-qualified-base-call`:
`template<class T> struct Derived : Base<T>` with
`Base<T>::method()` inside a template body. Asserts NO mis-routing
(count = 0) — documents the V1 gap that template-class inheritance
isn't captured as EXTENDS by the legacy DAG, so MRO walks are empty
and the super branch can't dispatch. The composition still works
correctly: U1's template-arg-stripping classifies `Base<T>` as a
super candidate, but the empty-MRO terminates without false edges.
- `cpp-phase5-u2-u3-adl-from-derived`:
`Derived : Base<T>` where `Base::record` shadows `audit::record`.
Unqualified `record(e)` inside the template body should resolve via
ADL to `audit::record` (because U3 + the `isFileLocalDef` class-
owned filter suppress `Base::record`). Asserts 1 edge to audit.h
and 0 edges to base.h.
- `cpp-phase5-u3-u5-inline-base`:
`template<class T> struct Derived : outer::v1::Base<T>` where `v1`
is inline. Unqualified `f()` inside `Derived<T>::g()` should NOT
bind to Base::f (dependent-base suppression even across inline
namespace prefix). Asserts count = 0.
Phase 5 tests asserting no-false-positives are gated under
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp — legacy DAG over-
resolves without the template-arg-stripping qualified-receiver path
and without two-phase dependent-base suppression.
162/162 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
152 pass + 10 skipped under =0 (legacy parity baseline).
---------
Co-authored-by: HuangWenjie <zhoudeng.hwj@alibaba-inc.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>