cs_lookup_method searches the other declarations of a type (same simple
name, same declared namespace: `partial` pieces, reference-assembly
copies) before it walks the base classes. With global-namespace types now
carrying the namespace "", every same-name type declared without a
namespace anywhere in the repository became a twin of every other one.
Across a repository that is a different type per assembly (each test
project has its own Program or Helper), so a member missing on the
receiver bound to an unrelated type ahead of the receiver's own base.
cs_namespace_twins returns no twins for a type in the global namespace.
Test: cs_lsp::cslsp_global_namespace_types_are_not_partial_twins runs the
production Tier-2 C# registry builder over cross-file defs: two unrelated
global Helper types and a base class. RED without the guard:
stop_base=0 stop_unrelated_global_twin=1. GREEN with it.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
C# looks a simple type name up namespace by namespace, innermost outward.
At each namespace N it checks the members of N, then the usings of the
namespace declaration enclosing the reference. The global namespace is
the last N, and its declaration is the compilation unit. So a type
declared without a namespace hides a type that only a compilation-unit
`using` imports, while a `using` inside a namespace body still wins over
the global namespace. Step 8b used to rank every using before the global
namespace.
- CBMCSUsing records whether a directive sits inside a namespace body
(in_namespace). cs_collect_imports tracks it per node, including the
compilation-unit siblings after a file-scoped namespace; the cross-file
entry pre-loads the pipeline import list without placement and the AST
walk upgrades it.
- cs_visible_namespaces ranks a simple name as: enclosing namespaces
innermost outward, namespace-body usings, the global namespace "", and
last the compilation-unit usings.
Ranking the global namespace that high also requires that only a type
proven global claims it. The vendored grammar does not parse a
`ref partial struct` with members, and error recovery dissolves the
enclosing namespace, so every type after it in that namespace surfaced
with no namespace. Mapped to "" those types outranked the real, imported
declaration: on dotnet/runtime 5,313 CALLS moved from implementation
files into reference assemblies (System.Runtime.cs, which declares
DefaultInterpolatedStringHandler as a `ref partial struct`).
cs_type_decl_namespace now returns "" only when the compilation unit
parsed without errors and NULL (unknown, invisible to step 8b, as on
main) otherwise; cs_top_level_namespace passes that through.
Tests:
- parallel_csharp_nested_type_and_global_namespace_binding: the
compilation-unit `using` probe now expects the global type (spec), and
a new namespace-body `using` probe expects the imported one. RED with
the previous order: cu_using=0/0.
- parallel_csharp_parse_damaged_namespace_is_not_global (sequential and
parallel). RED without the clean-parse rule: real=0 no_damaged=0 on
both paths.
- cs_lsp::cslsp_decl_namespace_global_needs_clean_parse: extractor
contract ("" for a clean global type, the namespace when found, never
"" after a parse error).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Resolving a qualified type name A.B that no namespace binds now resolves
the qualifier A as a type first (nested types). A using alias whose target
is qualified by the alias itself re-enters that walk through the alias
step without end: `using Loop = Loop.Inner;` resolves Loop to the target
Loop.Inner, whose qualifier is Loop again, and a mutual pair (`using
Ping = Pong.Inner; using Pong = Ping.Inner;`) cycles the same way. Both
are legal C# when namespaces of those names exist, and the walk overflowed
the stack on them.
The walk now carries a depth counter in the per-file context and stops
after CS_QUALIFIER_MAX_DEPTH (8) levels. One level is one type-nesting
step: a namespace-qualified top-level type binds without recursing, so
real nesting stays far below the bound.
Test: cs_lsp::cslsp_cyclic_alias_qualifier_terminates (self and mutual
alias cycles; the file must still resolve Svc.Go). RED without the bound:
AddressSanitizer stack-overflow, exit 134. GREEN with it.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Follow-up to #2347 (issue #2120). Step 8b binds same-name TOP-LEVEL types
by declared namespace, but two shapes still fell through to the short-name
fallback, where the prefix score against a path-derived QN is 0 for every
variant and registration order decided:
1. A qualified A.B whose qualifier A is a TYPE, not a namespace:
new Command.Result(...) and new Grain.Handler(dep).Handle(...).
cs_visible_namespaces only tried A as a namespace, and nested types
carry no namespace_qn, so the scan never had a candidate. Resolution
now falls back to resolving the qualifier as a type (recursively, one
segment shorter) and binding the nested member through the registry.
2. A top-level type with no namespace declaration (global namespace):
cs_top_level_namespace returned NULL for it, making it invisible to
step 8b. Top-level C# types now register namespace_qn "" and
cs_visible_namespaces appends the global namespace last.
Test: parallel_csharp_nested_type_and_global_namespace_binding covers a
nested-type constructor and the member call on its result, a chained
constructor receiver, and a same-name type split across the global
namespace and an imported namespace, in the sequential pipeline and in
the parallel pipeline with the production Tier-2 C# registry. Decoys are
registered first. Adapted to current main: the two constructor-edge
checks gate the parallel path only, because the sequential constructor
edge is still drawn by the textual constructor carrier, a layer this
change does not touch.
Distilled from #2401: the net change of commits d11be54f and 613060ea.
The temporary debug prints added in d11be54f and the stale copy of #2347
the branch carried (already on main as 50d19f85) are not taken.
Co-authored-by: Ersin Tarhan <ersintarhan@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The complexity helpers were inserted between the comment that documents
extract_elixir_func_def and the function itself, so that comment read as
the description of elixir_node_text_is. Move it back above the function it
documents; the comment and signature are again byte-identical to main. No
code change.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Since #2371 the clauses of a multi-clause function fold into one Function
node whose span covers all of them, but only the first clause was measured:
the folded node kept the complexity, cognitive, loop and access metrics and
the line count of that clause while its span grew.
fold_elixir_clause now receives the metrics of each clause it folds and
adds them to the node: cyclomatic, cognitive and loop_count add up,
loop_depth and max_access_depth take the deepest clause, every clause past
the first adds one dispatch decision to cyclomatic, and lines is recomputed
from the folded span. A clause whose walk reports
CBM_WALK_METRIC_UNAVAILABLE makes the folded value unavailable rather than a
partial sum. This is the treatment proposed in the PR description and
agreed in the review thread.
Indexed elixir-lang/elixir (669f8481, 6703 Elixir Function nodes) against
the merge parent: the 2212 folded functions change, the 4491 single-clause
functions keep identical metrics, Functions with complexity above 0 go from
3412 to 4785, and every node now has lines equal to its span (4491 did
before).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
elixir_compute_complexity kept its pending frames in one fixed 4096-entry
array (48 bytes each, 196,608 bytes in a single stack frame) and stopped
pushing children once it was full. Children are pushed last to first, so a
wider body lost its FIRST children and the function reported a lower
complexity with no signal, on exactly the largest functions. Worker threads
do not get the 8 MB main-thread stack, so the array was a risk of its own.
It now uses the walker pattern cbm_compute_complexity has used since #2375:
a 512-frame on-stack first chunk that spills to the heap through
cbm_walk_stack_reserve (memory core, class EXTRACT) and is released with
cbm_walk_stack_release. If the stack cannot grow, every metric reports
CBM_WALK_METRIC_UNAVAILABLE instead of an undercount. This answers items 2
and 3 of the 2026-08-20 review.
The new test builds a 5000-statement body of alternating if and for. The
fixed stack reports complexity 4095 for it instead of 5000.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The new HttpClient receiver rows matched by prefix, and an uppercase
letter after an id counts as CamelCase glue, so "this.http" also matched
this.httpAdapter.get (a NestJS route registration) and this.httpServer.post,
and "$http" matched $httpBackend.expectGET (an AngularJS test double).
Each receiver row now ends in a dot, like "gin.", plus a line-break twin
(LF, and CR for CRLF files): a chain wrapped before the verb keeps the break
in its callee text ("this.http\n .post"). On
gothinkster/angular-realworld-example-app 5 of the 18 client calls are
written that way; with dot-only rows they fell back to phantom route
registrations. "HttpClient" duplicated the Java/C# row and
"this.httpClient." is covered by "httpClient.".
Unit cases pin both directions, and the Angular pipeline test gains a
wrapped call on both resolver routes.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings the branch up to f6fb3e0d, which carries #2371 (guarded def heads,
the multi-clause fold and typespec attributes) and #1730.
Two files conflicted, both at spots where main rewrote the Elixir
definition code. Each is rebuilt as the main version of the file plus the
lines this branch adds, at the same place; no conflict side is
concatenated:
- internal/cbm/extract_defs.c: the complexity helpers sit between the
definition comment and extract_elixir_func_def as before, and the
metrics plus fingerprint block follows def.is_exported in the unfolded
path.
- tests/test_extraction.c: TEST(elixir_complexity_and_fingerprint) and
its RUN_TEST go just before the Haskell block, after the Elixir tests
main added.
Per-file patch-id (git diff -U0 | git patch-id --stable) of this merge
against origin/main equals the branch change against its old base:
extract_defs.c b591a7bc, test_extraction.c 7b507156. The merge removes no
line of either file.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
dg_puml_operand set *label up front but *id only on its success paths, so an
opener without its closer (`Alice ->: x`, `[-> Alice: x`, `class [Unclosed`)
returned with *id unwritten. dg_puml_relation and dg_puml_decl then read an
uninitialized dg_str_t and passed it to dg_element, which crashed the index
worker on some stacks. The operand now starts with an empty id, so those lines
take the existing `!lid.n` / `!rid.n` exits and declarations make no element.
Fixes#2818
Signed-off-by: Rodion Kazennov <nefayran@gmail.com>
test_pipeline.c: kept the main version and re-applied this branch diff; its three Go init RUN_TESTs precede the #2769 alias tests. test_go_lsp.c: the two #2769 alias tests pass NULL for the new rel_path parameter.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Conflict in src/pipeline/pass_calls.c: #2305 moved the inline LSP-target
emission of resolve_single_call into calls_emit_lsp_target() and added
the unresolved-call evidence around emit_classified_edge. The Java
data-member veto of this branch is ported into that structure:
- calls_emit_lsp_target: the veto runs once the target node is known,
before the resolution, the evidence and any spawn or edge. It returns
true (handled), so as before no edge is emitted and no registry
fallback runs after it.
- resolve_single_call: the veto stays in front of main's
candidate_qn/calls_emitted bookkeeping, matching the parallel
resolver, whose veto also precedes that bookkeeping.
pass_parallel.c and the remaining files merged cleanly.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
origin/main (#2305) moved the route-registration checks of the sequential
resolver into calls_emit_route_call and made emit_classified_edge return
whether it emitted. Keep the structure and bookkeeping of main and carry
the ServeMux pattern reading over: emit_classified_edge and
calls_emit_route_call now take the registration path from route_reg_path,
so "POST /v1/x" patterns register in both passes as before.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
#2305 (now on main) took semantic index version 11, so this branch's
Markdown Section change becomes version 12; its history entry is kept
after main's 10 and 11 entries.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>