Commit Graph
1050 Commits
Author SHA1 Message Date
Martin Vogel 0366dfe70c fix(java): resolve a package-qualified static-import class by short name
`import static demo.Util.twice` records the import target package-qualified
("demo.Util"), but the class is registered under the project/directory QN
("<proj>.Util") when the `package` declaration and the file's directory differ
(e.g. a flat layout). java_lookup_method("demo.Util", ...) then missed, the call
fell through to the lsp_static_import_text fallback, and the precise
lsp_static_import strategy was absent.

On a miss, resolve the import's class by its short name against the registry and
retry the method lookup, preferring an in-module match. Mirrors the C++
short-name type fallback.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:34:15 +02:00
Martin Vogel e6a46bd19c test(discovery): flip .claude-worktrees skip-dir reproduction to a green guard
QUALITY_ANALYSIS gap #1 (.claude-worktrees absent from ALWAYS_SKIP_DIRS) is
fixed — src/discover/discover.c now lists ".claude-worktrees" next to ".claude",
so cbm_discover() correctly skips it. The reproduction's RED expectation
(expected the canary to be indexed) was stale and the lifecycle assertion fired.
Reclassify it as a GREEN guard (expect skipped) so it now protects against
regressing the skip, per the test's own RED-becomes-GREEN workflow.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:24:15 +02:00
Martin Vogel 77bd8ec5d7 fix(ocaml): attribute in-body calls to the outermost function, not nested lets
OCaml's def walk does not descend into function bodies, so it emits a node only
for the OUTERMOST value_definition (the top-level function). The unified walk,
however, descends to find calls and pushed a SCOPE_FUNC for every value_definition
it met — including a nested local `let x = e in ...`, which is also a
value_definition but has no node. An in-body call then attributed to that nodeless
local binding, so its CALLS edge sourced to neither a Function nor the Module
(callable=0, module=0 in the breadth invariant).

Push a function scope only for the outermost value_definition (none already on
the scope stack) for OCaml, so in-body calls attribute to the top-level function
the def walk actually emitted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:21:26 +02:00
Martin Vogel f4731195c9 fix(cpp): module-qualify out-of-line method caller QN so the LSP rescue joins
For an out-of-line method definition `int Processor::run() {...}`, c_lsp built
the resolved-call caller QN from c_build_qn("Processor::run") = "Processor.run"
— the bare class-qualified name with NO module prefix. The def walk and the
call-scope QN both qualify the same method as "<module>.Processor.run", so the
exact-equality join in cbm_pipeline_find_lsp_resolution missed and the LSP
resolution (the correct caller for a call inside the out-of-line body, which
tree-sitter alone attributes to the module) was silently discarded — the edge
fell through to the registry resolver with no lsp_ strategy (gap #5a).

When the class scope of a qualified declarator is resolved HERE to the full
module-qualified class QN (saved_class_qn != enclosing_class_qn) and the built
func_qn carries a bare class prefix, rebuild it as <class QN>.<method short name>
so the caller QN matches both other producers and the rescue joins.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:06:52 +02:00
Martin Vogel e1bf7cc68e fix(cpp): namespace-qualified QNs so ADL and namespace lookups resolve
A symbol declared in `namespace ns` was collapsed to the file scope
(`proj.file.serialize`, not `proj.file.ns.serialize`) because the def walk
treated namespace_definition as a module-level container that contributes no QN
segment. With the namespace absent from the QN, an unqualified `serialize(d)`
resolved DIRECTLY to the top-level name and the argument-dependent-lookup path
(c_adl_resolve, which reads the namespace out of the argument type's QN) was
never reached.

Make a namespace contribute a QN segment in the def walk (it extends the
enclosing scope like a class but emits no def of its own; its members — both
free functions and nested classes — are walked under the extended scope). Mirror
it in c_lsp:

  - c_process_function qualifies a namespace-scoped free function with
    current_namespace instead of the file module, matching the def QN.
  - c_lookup_member gains a short-name fallback so a type referenced by its
    file-scoped or bare name (e.g. the return type of a namespace-scoped factory
    used outside that namespace) still resolves to its namespace-qualified
    registered QN.
  - c_adl_resolve also tries the module-prefixed form of an argument type's
    namespace, since `ns::Data` evaluates to the bare namespace `ns` while the
    overload is registered under `<module>.ns.serialize`.

C#/PHP need the same treatment paired with their own resolvers (a def-only change
breaks their existing namespace handling) and are done separately.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 11:43:21 +02:00
Martin Vogel 5b60045c55 fix(cpp): resolve implicit destructor, copy-ctor, and conversion-operator calls
Three C++ constructs invoke a member implicitly with no textual call node, so
the call walk produced nothing for the c-LSP to attach its resolution to
(callable=0, no CALLS edge):

  - delete p;        -> destructor   T::~T
  - T a = b;         -> copy/move ctor T::T
  - if (obj) { ... } -> conversion   T::operator bool

Synthesize a textual call sourced to the enclosing function at each trigger so
the lsp_{destructor,copy_constructor,conversion} resolution binds. The
destructor callee QN embeds the type (T.~T) which is not textually available
from `delete p`, so it joins via the reason gate (c_lsp stashes the operand
text); the other two join by callee short-name (the declared type / the
type-independent operator bool).

Two underlying defects surfaced and are fixed:

  - The conversion check evaluated the type of the `condition_clause` node
    (the `( expr )` wrapper, which has no type) instead of the inner
    expression, so `if (obj)` never resolved obj's type. Unwrap the clause.

  - A conversion operator's `operator_cast` node spans the full
    "operator bool() const" in this grammar (params + cv-qualifiers folded in),
    so the method was indexed under that malformed name and member lookups for
    "operator bool" missed. Add cbm_func_name_node_text to normalize it to
    "operator bool", shared by the defs and unified extractors so the def name
    and call-scope QN agree.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:55:27 +02:00
Martin Vogel f33e2772d0 fix(cpp): synthesize a textual call for overloaded binary operators (lsp_operator)
An overloaded C++ binary operator (return a + b; where Vec defines operator+)
invokes the operator method implicitly, so the call walk never produced a call
node — the c-LSP resolved Vec::operator+ (lsp_operator) but had no call site to
attach to (callable=0, no CALLS edge).

Synthesize a textual call to the bare operator name (operator<token>) at each
binary_expression for C++/CUDA, mirroring c_lsp.c's binary handling (the operator
token is the first unnamed child). The join binds it to the c-LSP resolution.
Builtin-operand expressions synthesize operator+ too but resolve no member and
are dropped, so no spurious edges. Mirrors the existing Kotlin operator-call
synthesis.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:13:59 +02:00
Martin Vogel 147ad871d9 test: disable rare-language grammar repros with documented grammar/limitation notes
Maintainer-approved: skip (return -1, not counted) the repros that fail due to
tree-sitter grammar defects or niche-language coverage gaps rather than cbm
extraction bugs, each with an in-code explanation and the original assertions
preserved (unreachable) for re-enable:

- verilog: grammar mis-parses the SystemVerilog task call as a data_declaration
  (fails identically as systemverilog).
- nim: extract-clean fails on standard Nim (indentation-sensitive grammar parse).
- meson: newer func...endfunc user-function syntax not modelled by the grammar.
- cobol / emacslisp: calls target external/undefined programs/symbols (no in-tree
  target node).
- bitbake / puppet / typst / llvm_ir / nasm / nix: niche-DSL/assembly/markup
  call or enclosing-func modelling gaps.

Mainstream-language reds (cpp, cs, java, kt, php, py, go) and the systemic
invariants remain enabled and red.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:05:35 +02:00
Martin Vogel 8e332bc2ac fix(purescript): extract exp_apply callees so function applications form CALLS edges
PureScript function application (greet "world", log (...)) is an exp_apply node
whose function head is an exp_name child — extract_fp_callee only handled
apply/application_expression, and PureScript wasn't routed to it, so the callee
was never extracted and no call to greet was found.

Route PureScript through extract_fp_callee (like Haskell/OCaml) and teach it
exp_apply: the head is an exp_name (its text is the qualified function name), and
curried applications nest exp_apply, so descend the function head to recover the
leftmost callee. Haskell/OCaml apply handling is unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:45:57 +02:00
Martin Vogel 42213168b4 fix(agda): scope in-body calls to the function (body clause vs type signature)
An Agda definition is two 'function' nodes: the type signature
(compute : Nat -> Nat, whose lhs has a function_name child that names the def)
and the body clause (compute x = add x 1, whose lhs has no function_name). The
shared resolver deliberately returns NULL for the body clause to avoid a
duplicate def — but that left the body clause unscoped, so the in-body call
(add x 1) sourced to the Module.

Resolve the body clause's name in compute_func_qn (call-scope only, not the
shared resolver) from the lhs head identifier, so the call attributes to the
function while definition extraction stays single-def. grammar_scientific is
fully green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:34:22 +02:00
Martin Vogel b01c0d2478 fix(scss): extract the callee of a @function call so it forms a CALLS edge
A SCSS @function call (double($x) inside another @function) is a call_expression
whose callee is a function_name child — there is no function field, so the
generic field-based resolver returned NULL and the call was dropped, leaving no
CALLS edge to the in-file @function (enclosing_parity SCSS: 0 edges). The
function scope itself was already correct (the call's enclosing was the calling
@function).

extract_scss_callee now also handles call_expression by reading the function_name
child, alongside the existing include_statement (mixin) case.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:18:40 +02:00
Martin Vogel 5bb745b9f7 fix(julia): recognize short-form 'f(x) = body' (assignment with call LHS) as a function
Julia parses short-form definitions (helper(x) = x + 1) as an 'assignment' node
whose LHS is a call_expression, not as 'short_function_definition' (the
configured julia func type was effectively dead for short form). So helper/run
were never extracted as Function nodes, in-body calls resolved no target, and no
CALLS edge formed (enclosing_parity/grammar_scientific Julia: 0 edges).

Add 'assignment' to julia_func_types and name it in cbm_resolve_func_name only
when the LHS is a call_expression (the head identifier is the function name);
plain 'x = 5' resolves NULL so it is neither extracted nor scoped. This fixes
both the def (helper/run become nodes) and the call scope (the body sources to
the function). The LHS signature call is a self-reference and is dropped by the
source==target guard.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:12:23 +02:00
Martin Vogel f89e0c961f test(nix): correct enclosing-parity fixture — wrap the call so no top-level application
The Nix case asserted zero Module-sourced CALLS edges, but its 'in run 21' body
is a genuinely top-level application: production correctly attributes the
in-lambda call (double n -> run) to the function AND the top-level call
(run 21 -> Module) to the Module. The parity invariant counts every
non-Function-sourced edge as drift, so the legitimately-Module-sourced top-level
call produced a false RED.

Nix's function_expression call-scope resolution already works (named from the
parent binding's attr). Move 'run 21' inside a 'main' lambda and make the 'in'
body a bare reference, so every call is in-function — strengthening the
invariant (now double->run AND run->main are checked) without weakening it. The
contract_edge_async_calls unit test is pre-existing-flaky (passes on re-run;
unaffected by this repro-only change).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:49:00 +02:00
Martin Vogel e01e77fc0b fix(dart): attribute in-body calls to the enclosing function (signature/body siblings)
Dart models a function as a function_signature plus a SIBLING function_body
(both children of program/class_body), not a single node containing the body.
So a scope pushed at the signature never covers the body, and the signature's
name was also unresolved by the shared resolver — in-body calls sourced to the
Module (enclosing-func drift).

Two parts in the unified walk:
- compute_func_qn resolves a dart function_signature/method_signature name from
  its identifier child (method_signature wraps a function_signature).
- push_boundary_scopes pushes the SCOPE_FUNC at the function_body, computing the
  QN from the preceding signature sibling, so the body's children attribute to
  the function. grammar_scripting and enclosing_parity dart cases now green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:36:06 +02:00
Martin Vogel 3f76096b74 fix(rust): push impl-block class scope so impl-method calls source to the method
A call inside a Rust impl method (impl Calc { fn compute(&self,x) { add(...) } })
was sourced to the Module: impl_item is in rust_class_types, so push_boundary_scopes
routed it through compute_class_qn, which returned NULL because impl_item has no
name field (the implementing type is in the type field). The dedicated impl
handler later in push_boundary_scopes is therefore dead code (the class branch
already consumed impl_item). With no class scope, compute_func_qn produced
proj.file.compute instead of proj.file.Calc.compute, which does not match the
class-qualified def-side Method node, so calls_find_source missed it and fell
back to the file/Module node.

Resolve impl_item's type field in compute_class_qn (mirroring the objc class-name
case), so the impl pushes the Calc scope and impl-method calls attribute to the
method. grammar_core is now fully green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:23:37 +02:00
Martin Vogel e986e777f1 fix(objc): extract top-level C functions so calls to them resolve
A top-level Objective-C C function (static int helper(int x) {...}) was never
extracted as a Function def: resolve_func_name_c_family handled function_definition
for C/C++/CUDA/GLSL/HLSL/ISPC/Slang but not Obj-C, so cbm_resolve_func_name
returned no name node and extract_func_def dropped the def. With no helper node,
a call helper(x) from a method body resolved to nothing and produced no CALLS
edge (grammar_core_objc: 0 callable-sourced edges).

Obj-C top-level functions share the exact C declarator structure, so route them
through cbm_resolve_c_declarator_name_node like the other C-family languages.
Completes the Obj-C method-call chain together with the class/method scope fix:
helper is now a node, and the (already correctly attributed) call from
Calc.Calculator.compute resolves an edge to it.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:17:16 +02:00
Martin Vogel f8b3bb0e35 fix(objc): attribute method-body calls to the method, not the @implementation class
Calls inside an Obj-C method (- (int)compute:(int)x { return helper(x); }) were
sourced to the enclosing @implementation class (or the Module) instead of the
method, because the unified walk pushed no class or function scope for
Objective-C:

- compute_class_qn returned NULL for class_interface / class_implementation (no
  name field; the class name is a plain identifier child), so no class scope was
  tracked.
- compute_func_qn could not name a method_definition (the selector keyword is a
  plain identifier child, distinct from the method_type return-type child).

Resolve the class name from the identifier child (mirroring the def extractor),
and resolve the method call-scope QN directly in compute_func_qn (not via the
shared cbm_resolve_func_name, which would make the def pass double-emit the
method as a top-level Function alongside the class-member Method node). In-body
calls now source to <project>.<file>.<Class>.<method>, matching the Method node
QN. Same enclosing-func-drift class as the calls-source-to-callable fix; the
grammar_core_objc red additionally needs the free-function call to resolve an
edge target.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 06:54:31 +02:00
Martin Vogel 9b17927bf6 fix(php-lsp): resolve magic __call dispatch to the __call handler
$p->anything(1) on a class with a __call magic method emitted php_method_dynamic
to a synthetic <class>.anything target at confidence 0.20 — below the pipeline
join's 0.6 floor, and to a node that does not exist — so no CALLS edge ever
carried the strategy.

The call actually dispatches to the class's __call handler. Resolve to
<class>.__call (a real method node) at 0.85 (the dispatch is certain), and stash
the dynamic method name ("anything") in reason so the join binds it to the
textual call site whose callee differs from __call. Admit php_method_dynamic to
the reason-matched join (lsp_resolve.h), the same pattern used for func-pointer,
ctor-ref, and dict-dispatch resolutions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 05:23:39 +02:00
Martin Vogel 10c17a63d0 fix(rust-lsp): resolve trait method calls to lsp_trait_dispatch/lsp_trait_ufcs
Two Rust trait-resolution fixes, both cases where the trait's own abstract
method shadowed the concrete impl:

lsp_trait_dispatch: d.speak(2) where Dog implements trait Speak resolved the
method inherently on Dog and was tagged lsp_method_dispatch. The trait-dispatch
path only fired when a method was resolved THROUGH a separate receiver type. Add
rust_method_is_trait_impl: when an inherently-resolved method also comes from a
trait the receiver implements (a declaring trait among its embedded_types),
tag it lsp_trait_dispatch. Mirrors the existing override-is-dispatch notion.

lsp_trait_ufcs: Speak::speak(2) resolved to the trait's own abstract method
Speak.speak (lsp_ufcs) because the inherent UFCS lookup found it first. A
trait-typed UFCS head must resolve to the sole concrete impl. Detect the trait
via is_interface (set at registration, reliable on the early resolve pass) and
resolve through rust_find_sole_trait_impl, which scans implementers and excludes
the trait's own method. Critically, emit NOTHING when no impl is known yet: the
resolver runs twice and a partial-pass lsp_ufcs to the abstract method would
otherwise outrank the real trait_ufcs from the complete pass and win the join.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 05:14:35 +02:00
Martin Vogel 009b7a8aee fix(c-lsp): tag a derived override of an inherited method as lsp_virtual_dispatch
A virtual method called on a concrete derived object (Derived d; d.speak(2),
where Derived overrides Base::speak) resolved directly to Derived.speak and was
emitted as lsp_type_dispatch. The existing virtual-dispatch path only fired when
a method was resolved THROUGH a base with a derived override; an override
resolved directly on the derived type was missed.

Extend the same notion (a derived override of an inherited method = polymorphic
dispatch) to the direct-resolution case: when a method is found directly on the
receiver type AND a base class also declares it, emit lsp_virtual_dispatch. This
mirrors the codebase's existing heuristic definition of virtual dispatch (an
override exists) rather than the textual 'virtual' keyword, so it stays
consistent. lsp_type_dispatch and lsp_base_dispatch fixtures are unaffected.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 04:49:43 +02:00
Martin Vogel 9ef37e89be fix(py-lsp): resolve dict-dispatch calls funcs["a"](v) to the dispatch-table target
A Python call through a dict dispatch table, funcs["a"](v), produced no CALLS
edge at all: the call's function field is a subscript, which the textual
callee extractor did not handle, so no textual call existed for the py-LSP's
lsp_dict_dispatch resolution to attach to (callable=0, strategy absent).

Three coordinated changes, the same reason-field pattern used for func_ptr and
ctor refs:
- extract_calls: for a Python call whose function field is a subscript with an
  identifier base and a literal string key, emit the base identifier ("funcs")
  as the textual callee so a callable-sourced CALLS edge exists. Gated to that
  exact shape so other subscript calls (arr[i]()) are unaffected.
- py_lsp: the dict-dispatch resolution stashes the subscript base identifier in
  reason (the textual callee differs from the resolved target "foo"), via a new
  py_emit_resolved_call_reason; py_emit_resolved_call delegates with NULL.
- lsp_resolve: admit lsp_dict_dispatch into the reason-matched join so the
  resolution binds to the textual call site.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 04:34:54 +02:00
Martin Vogel 3b2a291e99 fix(cs-lsp): emit cs_inherited_method for inherited bare calls and resolve using-static targets
Two C# bare-invocation resolution fixes, each mirroring an existing pattern:

cs_inherited_method: a bare call Method() resolved via cs_lookup_method on
the enclosing class walks the base chain, so an inherited method was emitted
as cs_self_method. Distinguish by receiver_type exactly as the instance-call
path does — a method declared on the enclosing class is cs_self_method, one
found on a base is cs_inherited_method.

cs_using_static: a 'using static Demo.MathUtil' directive stores its target as
the namespace-qualified source text, but types register under the file-stem QN
(proj.Client.MathUtil), so the method lookup missed. Route the target through
cs_resolve_type_name (whose short-name fallback bridges namespace-qualified
names to the file-stem registration) before looking up the method.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 04:20:17 +02:00
Martin Vogel 14def05cfe fix(java-lsp): resolve interface calls to sole concrete impl before type_dispatch
A call through an interface-typed receiver (s.area() where s:Shape) hit
java_lookup_method on the interface's OWN abstract method and emitted
lsp_type_dispatch on Shape.area, never reaching the sole-implementer
resolution — so lsp_interface_resolve / lsp_interface_dispatch were never
produced.

Resolve interface dispatch first when the receiver is an interface: a sole
concrete in-project implementer -> lsp_interface_resolve (to that impl's
method, synthesizing the QN when the method registry lacks it); two-or-more
-> lsp_interface_dispatch; ZERO in-project impls -> fall through to
type_dispatch on the interface method, keeping JDK interface calls
(List/Stream/Predicate) strict.

The sole-impl detection is factored into java_find_sole_impl and hardened:
consult the method REGISTRY (the dispatch path's source of truth) since the
per-type method-name array is often empty, match supertypes by full QN OR
bare name (duplicate registry entries list a supertype short in one entry and
fully-qualified in another), and dedup implementers by QN so one class with
two registry entries is not miscounted as two implementers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 03:55:50 +02:00
Martin Vogel c18e3dc304 fix(lsp): resolve Java constructor references via class node + reason-match
A constructor reference `ClassName::new` is extracted as a textual call to
`new`, but the LSP resolved it to a synthetic constructor QN with no graph
node, and the join keys on the callee short-name (`new` != the class/ctor
name), so both lsp_method_ref_ctor and lsp_method_ref_ctor_synth dropped
(callable=0).

Resolve the reference to the ClassName CLASS node (which exists, mirroring
the constructor-call handling) and stash the original textual callee `new`
in the resolved call's `reason` field; the join matches the textual `new`
call site on it, gated to the ctor-ref strategies. No struct/ABI change.
java_cs LSP reproductions 19->21.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 03:07:49 +02:00
Martin Vogel c4608070a9 fix(extract): extract Java method references as calls (#lsp_method_ref)
A Java method reference `Lhs::name` (e.g. `String::length`) is never
visited by the call walk (it is not a call_expression), so the LSP's
lsp_method_ref resolution had no call site to attach to (callable=0) and no
edge formed. Record a textual call to the referenced method's bare name (the
constructor ref `Lhs::new` uses the unnamed `new` token); the LSP join
matches on the bare name. The referenced method is invoked indirectly, so
this is an accurate call edge. java_cs LSP reproductions 18->19.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 02:46:53 +02:00
Martin Vogel bef9ab08ff fix(lsp): resolve C/C++ function-pointer calls via the join (#func_ptr)
A function-pointer call `fp(args)` has textual callee `fp` (the pointer
variable), but c_lsp resolves it by data-flow to the pointed-to function
`target`. The pipeline join keys on the callee short-name, so `fp` != `target`
dropped the lsp_func_ptr/lsp_dll_resolve resolution and no edge formed.

Stash the original textual callee (`fp`) in the resolved call's `reason`
field (unused and NULL for resolved calls; already copied across all
cross-file paths) and have the join match the call site on it, gated to the
func_ptr/dll strategies so `reason` is never misread as an unresolved-call
diagnostic. No struct/ABI change. c_cpp LSP reproductions 11->12.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 02:38:49 +02:00
Martin Vogel beaa776ef1 fix(pipeline): emit HTTP_CALLS for external client calls masked by registry mis-resolution (#523)
A client call to an external HTTP library (`requests.get("/api/orders/{id}")`)
carries its service-pattern signal in the callee_name. But the registry
mis-resolves `requests.get` to a spurious builtin short-name match
(`builtins.dict.get` via "get", strategy unique_name) -- a non-empty,
non-HTTP QN. Both the empty-resolution fallback (checks the callee) and the
resolved-QN service check (checks the resolved QN) therefore miss it, the call
is dropped, no HTTP_CALLS edge is written, and cross-repo matching finds
nothing to link.

Detect the HTTP/ASYNC service pattern on the callee_name FIRST (before the
registry can mask it) and emit the edge to a synthesized route node. Applied
to both pipeline paths (pass_calls.c sequential + pass_parallel.c
resolve_file_calls). Cross-repo HTTP linking now works for external client
libraries whose source is not indexed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 02:04:36 +02:00
Martin Vogel 04697c632c fix(pipeline): deny-wins-by-value for infra Route extraction (#521)
A config/upstream URL is often extracted as several string_refs at
different key_path granularities (full path, leaf key, flat/null). Because
the Route node is keyed by the URL VALUE, a value was minted if ANY
granularity passed the per-ref guard -- a denied full path
`registries.terraform-registry.url` was defeated by a sibling leaf `url`,
and a healthcheck `curl ... || exit 1` minted under the leaf key `test`.

Restructure infra-route extraction into two passes: pass 1 collects every
URL value denied under ANY of its refs (whitespace value = command, NULL
key = context-less, or upstream/config/healthcheck key); pass 2 mints only
values never denied. A config-only file now yields zero spurious routes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 01:46:51 +02:00
Martin Vogel b351f76843 test(repro): match MCP-envelope escaping in trace_path repros (#480/#514/#546)
trace_path returns the MCP tool-result envelope
{"content":[{"type":"text","text":"<inner json>"}]}, so the inner json is
embedded as an escaped string value: the "callers"/"callees" keys appear as
backslash-escaped \"callers\" / \"callees\". These reproduce-first tests
hand-rolled strstr(resp, "\"callers\"") (unescaped), which can never match a
correctly escaped envelope, so they stayed RED even though the production
traversal is correct (verified by inspecting the inner json: callers/callees
populated, data_flow arg expressions surfaced, both import styles unioned).

Correct the assertions to the escaped form (the project's passing trace_path
tests in test_incremental.c use the same via resp_has_key) and assert
populated entries by qualified-name tail, keeping the empty-array guards so
the tests still RED on a genuinely-empty traversal. Production code unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 01:23:16 +02:00
Martin Vogel c718d68390 fix(lsp): rank Go sole-implementer interface resolution at 0.95
For a call through an interface with exactly one implementer (s.Speak()
where Speaker is implemented only by Dog), go_lsp emitted both
lsp_interface_resolve -> Dog.Speak at 0.90 and lsp_type_dispatch ->
Speaker.Speak at 0.95. The join keeps the highest-confidence resolution,
so type_dispatch (the interface-method node) always shadowed the concrete
sole-implementer resolution. A sole-implementer dispatch is unambiguous;
rank it 0.95 so the concrete Type.method wins for that call site.

go_py LSP strategy reproductions 15->16.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 00:59:38 +02:00
Martin Vogel 6c1dfc92e9 fix(lsp): emit lsp_kt_lambda_it for calls through the lambda it
A call on the implicit lambda parameter (e.g. `x.let { it.m() }`) resolved
through the regular method path and was tagged lsp_kt_method, because the
LSP already binds `it` to the receiver type. Detect an `it` receiver at the
method-resolution site and emit lsp_kt_lambda_it (lambda-scoped dispatch).

kt LSP strategy reproductions 21->22.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 00:34:48 +02:00
Martin Vogel 15e541bc50 fix(extract): extract Kotlin destructuring + for-in desugared calls
`val (a,b) = e` desugars to e.component1()/e.component2() and `for (x in e)`
to e.iterator()/hasNext()/next(), but the call walk only visits
call_expression nodes so these never became calls, leaving the LSP's
lsp_kt_destructure / lsp_kt_iterator resolutions with no call site to join.

Add Kotlin-gated extraction recording textual calls to those
operator-convention method names (componentN per destructured variable;
iterator/hasNext/next per for-in), matching the LSP's emitted callees.

kt LSP strategy reproductions 19->21.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 00:29:17 +02:00
Martin Vogel 11222653ee fix(extract): extract Kotlin operator expressions as calls
A Kotlin `a OP b` desugars to an operator-method call (a.plus(b) etc.),
but the call walk only visits call_expression nodes, so these
precedence-specific binary-expression nodes were never recorded as calls.
The type-aware LSP operator resolution (lsp_kt_operator -> the user
`operator fun`) therefore had no call site to attach to and was dropped.

Add a Kotlin-gated extractor recording a textual call to the operator
method's bare name (operator-token -> method mapping mirrors the LSP's
binary handler). Builtin operands (Int+Int) resolve to a stdlib type with
no graph node and drop as before; only user `operator fun`s gain edges.

kt LSP strategy reproductions 18->19.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 00:23:29 +02:00
Martin Vogel 5f1e702239 fix(lsp): emit lsp_kt_static for Kotlin object/companion member calls
A call on an `object`/`companion object` singleton member (MathKt.square)
was emitted as lsp_kt_method because kotlin_lookup_method matches the
member without recording that its container is a singleton. Track an
is_object flag on the registered type (set at object_declaration
registration) and emit lsp_kt_static when the receiver type is an object;
regular class instances stay lsp_kt_method.

kt LSP strategy reproductions 17->18.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 00:05:05 +02:00
Martin Vogel 6c2f45da91 fix(lsp): emit precise Kotlin constructor + extension call strategies
Two Kotlin LSP resolutions emitted a callee/strategy the pipeline join
could not use:

- Constructor `Foo()` emitted callee_qn `Foo.<init>`, but the textual
  call site's callee is the bare class name `Foo` and there is no
  `Foo.<init>` graph node, only the `Foo` class node. Resolve the call to
  the class node (matches the join's callee bare-segment and the target),
  mirroring the C#/Java `new Foo()` handling.

- Extension calls `recv.ext()` were emitted as lsp_kt_method because
  kotlin_lookup_method matches both members and extension functions.
  Distinguish by QN shape: a member nests under the receiver
  (`<recv>.<member>`) while an extension is a top-level fun that does not;
  emit lsp_kt_extension for the latter.

kt LSP strategy reproductions 15->17.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 23:56:19 +02:00
Martin Vogel a0420de83c fix(lsp): qualify Kotlin LSP caller/callee QNs with module_qn
The Kotlin LSP initialized package_qn to "" for files without a source
`package` declaration, so it emitted bare resolved-call QNs (caller_qn
"caller", callee_qn "helper"). The pipeline join keys on an exact
caller_qn == enclosing_func_qn match, but the textual extractor qualifies
the enclosing function with the FS-path module_qn ("<project>.main.caller").
Bare != qualified, so every Kotlin LSP resolution was dropped and no
strategy-tagged CALLS edge survived.

Initialize package_qn to module_qn (the same FS-path prefix the textual
extractor uses); a source `package_header`, when present, still overrides
it for cross-file import resolution. Recovers the Kotlin top_level/method/
static/this/super/... LSP strategy reproductions (kt suite 11->15).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 23:36:45 +02:00
Martin Vogel 7b7684bbb6 fix(test): page-size-aware RSS bound for incr_full_index
The incremental full-index RSS-delta assertion used a fixed 2048 MB
ceiling that 16 KB-page ARM hosts exceed -- larger page granularity
inflates resident set. Raise the ceiling to 2816 MB when the page size
is >= 16 KB, keeping the tighter bound on 4 KB-page hosts.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 23:13:07 +02:00
Martin Vogel e51c93554f fix(fqn): keep structural nodes from dir-module QN collision; correct dotfile module QN
A directory-based-module language (Go/Java) derives a file's module QN
from its directory: a root file's module == the project name and a file
in pkg/ has module proj.pkg -- exactly the QNs of the "Project" root and
the "pkg/" Folder structural nodes. The per-file Module def then clobbered
those nodes' labels through the gbuf upsert (last-write-wins on label),
turning Project/Folder into Module. Guard the upsert so a "Module" never
downgrades a "Project" or "Folder" -- the directory node is the package
container. One guard covers both the sequential upsert and the parallel
local-gbuf merge.

Separately, a dotfile such as .env had its leading dot stripped as an
"extension", leaving an empty stem whose module QN also collided with the
project root. Treat a leading dot as a dotfile marker (not an extension)
and drop it from the QN segment, so .env -> proj.env (a distinct module).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 23:13:01 +02:00
Martin Vogel b3b3a48998 fix(fqn): directory-based module QN for Java/Go (de-double) + nested-class enclosing
Java/Go QNs doubled the top-level class/package (proj.Outer.Outer.run; Go module
included the filename). Add cbm_fqn_module_source_lang / cbm_fqn_compute_source_lang
(dir-based module for Java/Go; legacy cbm_fqn_compute UNCHANGED for import paths),
route the single authority cbm.c:569 result->module_qn + the def/calls/pipeline
module derivations through them. Also fix unified compute_class_qn to prefix the
enclosing class for nested classes (was ignored) so def-QN == LSP caller_qn ==
calls-enclosing → lsp_outer_dispatch joins. Reproduce-first tests added; legacy
test_fqn.c/test_registry.c (generic helper) preserved. Per external second-opinion plan.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 21:12:07 +02:00
Martin Vogel 3ab6ab0781 fix(test): rescope rust lsp_macro (external vec! → no-edge) + python-builtins golden
rust lsp_macro targets the external alloc crate (no in-file node) → assert no
dangling edge via inv_no_calls_edge_to_qn. python label golden updated for the
builtin nodes injected by py_builtins.c (Class:6,Function:3,Method:5,Module:1).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 21:12:06 +02:00
Martin Vogel 0849f28c3f fix: bulk parallel — Rust cross-crate, Python builtins, #607 reinstall, grammar reds
Four file-disjoint problem groups fixed in parallel, deployed together:
- G1 (lsp): wire the dead Rust cross-file LSP into pass_lsp_cross.c (CBM_LANG_RUST
  in has_cross_lsp + run_one, CBMLSPDef->CBMRustLSPDef conversion, Cargo manifest
  via cbm_cargo_parse threaded through) -> 8 in-crate Rust strategy reds + #56
  cross-crate (lsp_cross_crate disambiguation).
- G2 (lsp): the py typeshed registry already resolved builtins but no builtins.*
  graph node existed to target -> add py_builtins.c injecting builtin def nodes
  (len/str/list/dict + str.upper/list.append/...). Plus rescope the genuinely
  no-edge LSP markers (lsp_unresolved/_unindexed/external-import) to assert the
  accurate no-edge invariant (new inv_no_calls_edge_to_qn helper).
- G3 (cli): #607 — install now PRESERVES existing indexes by default + a
  --reset-indexes opt-in (cbm_install_handle_existing_indexes helper); uninstall/
  update unchanged; repro_issue607 retargeted to the helper.
- G4 (grammar): Swift struct (Swift uses class_declaration+declaration_kind),
  Nix function defs + a discovered Nix calls-extraction bug, Teal/Magma fixtures
  corrected. LLVM/NASM confirmed already green. (Meson/Nim flagged needs-decision.)

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 19:15:47 +02:00
Martin Vogel b3fdc924d2 fix(k8s): restore Resource label so the K8s pipeline produces nodes + INFRA_MAPS
An earlier relabel (extract_k8s.c Resource->Class, to satisfy labels-valid)
left pass_k8s.c filtering on label=='Resource' (line 391) — so it skipped every
K8s def and produced ZERO Resource nodes and ZERO INFRA_MAPS edges (7 make-test
failures + the consequent LeakSanitizer leak from those tests early-returning).
Fix the whole chain accurately: add 'Resource' to inv_label_valid (the def-label
allowlist), revert extract_k8s.c to emit 'Resource' (manifest + kustomize), so
pass_k8s.c matches -> Resource nodes + INFRA_MAPS restored. K8s repro goldens
updated Class->Resource to match. Also guard 3 vendored scanners' zero-length
memcpy (crystal/rescript/purescript) against UBSan NULL-nonnull (documented in
vendored MANIFEST.md).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 17:40:48 +02:00
Martin Vogel aff9bfbbaf fix(test): resolve 18 make-test regressions from the marathon batch
make-test ran for the first time after the lint gate cleared and caught 18 failures:
- REVERT #521 YAML string_ref skip (extract_unified.c): it broke K8s manifest
  resource/infra extraction (7 tests) and didn't fix its own repro. repro_issue521
  returns to RED (1 repro vs 7 regressions); #523/#520 kept.
- REVERT c_lsp.c new_expression ctor->class-node change: existing tests expect the
  synthesized Type.Type ctor QN (3 clsp tests). repro_lsp_c_cpp kConstructor uses an
  explicit ctor + asserts strategy presence only, so it stays green.
- UPDATE tests for INTENTIONAL label changes (production verified correct): struct
  defs now 'Struct' not 'Class' for Rust/Go (test_extraction.c, test_pipeline.c go
  type-classification/grouped/docstring, test_grammar_labels rust golden); Pony member
  funcs 'Method' (golden + probe_pony); PROPERTIES/GraphQL/Prisma now extract
  Variable/Field (goldens + probe_properties).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 16:23:26 +02:00
Martin Vogel 49aab71489 fix(lint): clear cppcheck errors gating make-test (NULL-target guard + go.mod var shadow)
The CI lint job (cppcheck) failed, which SKIPS the gated make-test job:
- pass_parallel.c: emit_config_edge/emit_normal_calls_edge dereference target->id,
  but emit_service_edge can be reached with target==NULL on the #523 HTTP/ASYNC
  external-client bypass. cppcheck (ctunullpointer) flagged the potential NULL
  deref. Guard both emitters — a CONFIGURES/CALLS edge needs a real target.
- extract_defs.c: the go.mod require_spec local 'spec' shadowed the CBMLangSpec
  *spec parameter (cppcheck shadowArgument, treated as error here). Rename to
  req_spec. Full scripts/lint.sh --ci (clang-format-20 + cppcheck) now passes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 15:04:27 +02:00
Martin Vogel 9cbb1aa90f style: clang-format-20 sweep (marathon-touched files)
The CI lint uses clang-format-20 (scripts/lint.sh --ci CLANG_FORMAT=clang-format-20).
Grammar/LSP/struct/issue edits across these files weren't formatted with cf-20,
failing lint and gating the make-test job. Format with cf-20.1.8 to match CI.
No logic change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 14:34:57 +02:00
Martin Vogel ad33c9b359 fix(issues): #521 YAML config-URL Route dedup, #523 sequential external HTTP edge, #520 fixture project arg
#521: YAML leaf URLs were pushed to string_refs twice (once with NULL key_path
in handle_string_refs, once via the key_path-aware YAML walk); the NULL-key_path
copy bypassed the upstream/config-URL deny check and minted spurious Route nodes.
Skip YAML in handle_string_refs so the key_path walk is the sole authority.
#523: the sequential pass dropped calls to external/unindexed HTTP client libs
(requests.get) before the service-pattern check; classify on the raw callee_name
in the empty-resolution branch and emit the HTTP/ASYNC edge (target=NULL), mirroring
the parallel path, so cross-repo matching can link it.
#520: fixture omitted the required project arg to detect_changes (production
correct); pass project derived from the tmpdir.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 14:31:17 +02:00
Martin Vogel 554b171cf7 fix(graph): precise Struct label for Rust/Swift/D/Go structs (Option B)
Struct definitions were labelled 'Class', conflating a distinct kind. Emit
the precise 'Struct' label for Rust (struct_item), Swift/D (struct_declaration)
and Go (type_spec struct_type); C/C++/Obj-C struct_specifier stays 'Class'.

To avoid regressing type resolution, a struct must still behave as a type
everywhere a class does. Added a centralized predicate cbm_label_is_type_like()
(Class/Struct/Interface/Enum/Type/Trait) and routed every type-like consumer
through it: registry seeding (pass_definitions/pass_parallel/pipeline_incremental
kept in sync), class-resolution (pass_semantic/pass_parallel resolve_as_class),
Go interface satisfaction (collects Struct nodes), the cross-file LSP label map,
and the Go/Rust LSP type registrars. Class-only sites (protobuf services, K8s
kinds, the C-family LSPs) are left unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 13:54:51 +02:00
Martin Vogel d97dee0239 fix(lsp): per-language emission bugs (C++ in-method caller QN, py super-init rank, py module-attr cross-file)
Diagnosed via runtime join tracing:
- C++ (c_lsp.c): a method defined inline in its class body recorded
  caller_qn as module.method (no class), disagreeing with the textual
  enclosing module.Class.method, so cbm_pipeline_find_lsp_resolution never
  joined in-method calls (lsp_implicit_this). Prepend the inherited
  enclosing class; out-of-line defs (Widget::compute) already carry it.
- py (py_lsp.c) super().__init__(): the generic lsp_super (0.88) outranked
  the more-specific lsp_super_init (0.85) in the highest-confidence join.
  Emit lsp_super_init first at 0.90 for __init__; plain super().m() keeps
  lsp_super.
- py (py_lsp.c) module-attr: an in-project sibling 'helpers.do_work' was
  emitted lsp_module_attr_unresolved @0.55 (below the 0.6 floor) because the
  import records the short module name while defs are project-qualified.
  Retry the lookup against <project-root>.<module>; external modules stay
  unresolved.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 13:29:24 +02:00
Martin Vogel 781456818f style: clang-format issue-batch additions (cypher/mcp)
Fixes the dry-run lint failure: the #627 cypher and #514/#520/#546 mcp
additions had lines exceeding the column limit. clang-format 22.1.1
(project version) wraps them; no logic change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 13:13:26 +02:00
Martin Vogel da506f7b17 fix(grammar): extract calls for 11 niche grammars (HDL/asm/build/proof langs)
calls-extracted reds: the call node type was absent from the language spec
and/or the callee identifier was nested too deep for the generic resolver.
Per-language: Verilog/SystemVerilog (tf_call), VHDL (parenthesis_group),
NASM (actual_instruction call/jmp operand), LLVM-IR (instruction_call callee
field), Agda (application expr head qid), FunC (function_application),
Makefile (shell_function), Puppet (include_statement + puppet_keywords so
include/require survive the keyword filter). Just: recipe dependency is the
only call-shaped node, so the fixture asserts the dependency callee 'build'
(grammar exposes no recursive-just call node).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 13:13:00 +02:00