`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
$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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
#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>
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>
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>
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>
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>