The recursion detector flagged any call whose short name matched the
enclosing def as self-recursion, so super().save() inside save,
axios.get() inside get, and console.error() inside error were all
false positives feeding is_recursive, recursion_in_loop and
unguarded_recursion.
Add is_self_receiver() and AND it into the short-name comparison: a
qualified callee only counts when its whole receiver chain (everything
before the last '.') names the same object — self/this/cls/@self, or
the enclosing def's own receiver identifier parsed from
CBMDefinition.receiver (Go: s in 'func (s *Store) save()'). Bare names
keep the prior behavior. Matching the full chain keeps self.obj.recur()
(a field's same-named method) out, and the dynamic receiver whitelist
keeps Go s.save() detected.
Distilled from PR #699 with two corrections: the receiver chain is
matched via the last dot instead of the first segment (self.obj.recur
was still a false positive), and the enclosing-receiver whitelist so
Go method self-recursion is not lost.
Closes#599
Co-authored-by: Gen Li <lg320531124@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Spring class-level RequestMapping prefixes were not folded into method mapping route_path values, so the graph could contain partial Route nodes like /orders instead of the actual endpoint /api/orders even while HANDLES edges existed.
Combine Java/Kotlin class mapping prefixes with method-level mappings during method definition extraction, and scan bounded annotation-argument subtrees so named or array-valued Spring annotations can provide the path literal.
Strengthen edge_types_probe to assert exact Spring Route node names for Java and Kotlin, not just HANDLES edge counts.
Signed-off-by: jonghyun.lee <jonghyun.lee@musinsa.com>
The per-language "resolve calls in AST node" walkers recurse once per nesting
level. Four of them — Python (py_resolve_calls_in), Go (resolve_calls_in_node),
PHP (php_resolve_calls_in_node), Kotlin (kt_resolve_calls_in_node) — had no depth
guard, so a deeply-nested or cyclic file could drive them into a native stack
overflow (SIGSEGV) that takes down the whole index run. C and Java already guard
this (C_LSP_MAX_WALK_DEPTH / JAVA_LSP_MAX_WALK_DEPTH).
Add the same wrapper/inner depth guard to all four, backed by a shared,
env-configurable cap: CBM_LSP_MAX_WALK_DEPTH (default 512) in scope.h. Past the
cap the wrapper skips the subtree — those calls stay unresolved (graceful
degradation, not a crash). The walk_depth-- runs after the inner body returns, so
early returns can't leak the counter.
Reproduce-first: lsp_{python,go,php,kotlin}_deep_nesting_no_crash (30000-deep
nested calls, run under a forked child + WIFSIGNALED). RED without the cap (the
child SIGSEGVs — verified by raising CBM_LSP_MAX_WALK_DEPTH to 100M), GREEN with
it. Full suite 5752/0, no ASan/leak.
Part of the resilient-indexing effort (Track B prevention layer). Refs #668.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
walk_defs used a fixed `walk_defs_frame_t stack[4096]` — a single ~160 KB frame on
the C stack. Two problems:
- On small thread stacks (notably the pre-2026-03 Windows 1 MB main thread) that
huge frame overflowed the stack during the definitions pass — the 0xC00000FD
crash a reporter hit indexing a large SQL file (#668). Reproduced locally by
running extraction under a constrained stack (~208 KB) + ASan.
- The `top < 4096` push guards SILENTLY DROPPED every top-level definition past
4096, so a file with more than 4096 top-level defs was only partially indexed.
Replace the fixed array with a growable heap stack (wd_stack_t + wd_push): a
256-frame initial buffer that doubles on demand, bounded by a generous,
env-configurable ceiling (CBM_WALK_DEFS_MAX, default 8M frames) that WARNs once
instead of dropping, and returns cleanly on realloc OOM (no NULL deref). A normal
file now uses ~10 KB of heap instead of 160 KB of stack, and files with thousands
of top-level defs are fully extracted. The frame stack is a pure index-addressed
LIFO (frames copied by value on pop), so heap-backing is semantically identical.
Reproduce-first: walk_defs_no_truncation_over_4096_issue668 — 5000 top-level defs
must all be extracted (RED on the old 4096 cap, GREEN after). Full suite 5748/0,
no ASan/UBSan reports on the new alloc.
Root cause was pinned by reproduction (constrained-stack + ASan), refuting earlier
grammar/scanner hypotheses — extraction and the vendored tree-sitter tree ops are
otherwise iterative. The separate error-recovery assertion mode
(symbol<token_count) was not locally reproducible and is addressed by the
forthcoming general crash-containment layer.
Refs #668
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Django REST Framework ViewSets expose extra endpoints through @action-decorated
methods, e.g. @action(detail=True, methods=["post"], url_path="approve"). On
main these methods carried no route_path, so Phase 2a created no Route node and
no HANDLES edge for them -- the endpoint was invisible in the graph (#603).
Add try_drf_action_decorator (internal/cbm/extract_defs.c): when a method's
decorator identifier is "action", read the methods / url_path / detail kwargs
and set *out_path/*out_method. The existing decorator-route pipeline
(ensure_one_decorator_route, Phase 2a) then creates the Route + handler->Route
HANDLES edge in the established direction. Fallbacks: method name for url_path,
"GET" for methods; detail=True -> /{pk}/<seg>, else /<seg>.
Reproduce-first guard: handles_drf_action_python in the edge_types_probe suite
-- a DRF @action method must produce >=1 HANDLES edge (RED with 0 edges before
the extractor, GREEN after).
Known limitation: multi-method actions (methods=["get","post"]) capture only the
first method -> a single Route.
Distilled from #647 (thanks @muskiteer). Drops that PR's unrelated project-wide
HANDLES edge-direction reversal, which was unnecessary for the fix and broke the
handler->Route convention relied on by pass_cross_repo.c's find_route_handler.
Closes#603
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: muskiteer <agarwal.vansh3110@gmail.com>
cbm.c uses git_allocator (with GIT_OPT_SET_ALLOCATOR) but includes only
<git2.h>, which does not pull in <git2/sys/alloc.h> where the type is
declared. This compiles out on a bare macOS toolchain (libgit2 absent
from the pkg-config path, HAVE_LIBGIT2 undefined), but fails to compile
when libgit2 is present — e.g. in the Nix dev shell:
internal/cbm/cbm.c:347: error: unknown type name 'git_allocator'
Refs #705.
Signed-off-by: Kris Williams <115474+kriswill@users.noreply.github.com>
The per-file C cross-LSP resolver mutated the finalized, project-wide cross
registry that is shared read-only across the parallel resolve workers. Each
post-finalize add landed in a tail the hash index does not cover, so every
lookup linear-scanned an ever-growing tail -> O(files*defs) on large C
codebases. The Linux-kernel full index hung at "[4/9] Resolving" on 11 cores
for >6 min and never finished, plus a heap data race across workers.
Seal every shared cross-registry: CBMTypeRegistry gains a read_only flag, set
by all five {c,py,cs,ts,go}_build_cross_registry builders right after
finalize, and cbm_registry_add_func/_type no-op on a sealed registry. One
chokepoint guards every language, robust to any resolver mutation site. The
plain-C function-registration site is also skipped directly when the registry
is shared.
Add six cross-language invariant tests (c, c++, py, c#, ts, go): resolving
against a finalized shared registry must leave func_count/type_count
unchanged -- RED before the fix (C +1, C++ +2 post-finalize adds), GREEN
after.
Verified: full suite 5720 passed / 0 failed; Linux-kernel full index now
completes all 9 phases in 643s (4.88M nodes, 11.9M edges) instead of hanging
at phase 4.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Kotlin LSP resolves a method call on an unknown-typed receiver whose member
is a universal kotlin.Any method (toString/equals/hashCode) to kotlin.Any.<m>
and emits lsp_kt_any, but no kotlin.Any node existed in the graph, so pass_calls
had no target node and dropped the call (callable=0).
Add kotlin_builtins.c (mirror of py_builtins.c): inject kotlin.Any plus its
three universal methods into result->defs during cbm_run_kotlin_lsp, before the
pipeline mints def nodes. The lsp_kt_any call now targets the injected node and
forms a CALLS edge.
Update the kotlin grammar-label golden to include the 4 injected nodes, exactly
as python's golden already includes its injected builtins.
Un-skips repro_lsp_kt_any. Repro suite 312/0, unit suite 5693/0.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The call extractor dropped any callee matched by cbm_is_keyword(), and the
Python keyword set lists builtins (len, str, range, int, list, dict, print) to
suppress them as bare usages. That also dropped the textual CALL, so the LSP
resolution to the injected builtins.<name> node (py_builtins.c) had no call to
attach to and no CALLS edge formed (callable=0) for free-function and
constructor builtins. Method builtins (s.upper) were unaffected because method
names are not in the keyword set.
Add cbm_is_resolvable_builtin() — the builtins we mint a real graph node for —
and keep those callees in call extraction only; the keyword filter still
applies to usage and semantic extraction (its four other call sites are
unchanged). Every un-filtered builtin has a target node, so the new edge is
always callable-sourced, never Module-sourced.
Un-skips repro_lsp_py_builtin and repro_lsp_py_builtin_constructor.
Repro suite 311/0, unit suite 5693/0.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Kotlin destructuring call (extract_calls.c, commit 15e541b) and the Dart
signature-sibling while-loop (extract_unified.c, commit e01e77f) carried
clang-format violations that failed the dry-run lint gate. Reformat just those
two statements (per-region, LLVM-22 = clang-format-20 canonical) so lint-format
is clean. No behavior change.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
For a JSX component imported by a relative specifier (`import { Widget } from
"./widget"`), the per-file pass emitted lsp_ts_jsx_import with the raw specifier
as the module QN ("./widget.Widget"). That QN matches no node, but — winning the
exact-equality/confidence join over the cross-file pass's correct resolution
(equal confidence, emitted first) — it dropped the edge, so the strategy was
absent. The cross-file pass re-runs with the specifier resolved to the real
module QN and emits the correct "<proj>.widget.Widget" resolution. Skip the
per-file emission for relative specifiers (mark unresolved) so the cross-file
resolution stands.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`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>
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>
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>
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>
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>
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>
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>