Regression coverage for #1234: Java interface and enum methods were
emitted as both a Method node and a duplicate top-level Function node.
The production fix landed via 87a0e3f (language-agnostic class-body
routing); this adds the missing test coverage so it cannot silently
regress:
- java_interface_no_duplicate_function_issue1234: interface methods
produce Method nodes only, zero Function nodes
- java_enum_dedup_preserves_calls_issue1234: enum methods dedup'd while
cross-class CALLS edges survive
- cp_interface_method_no_dup_function: convergence probe asserting the
surviving Method still carries its CALLS edge end-to-end
Distilled from PR #1327.
Co-Authored-By: Harshita Joshi <j.harshitaa06@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
r.HandleFunc(base+"/login", handler) fell through extract_positional_url
with no branch for binary_expression, so first_string_arg stayed NULL. A
real BFF with 47 such registrations (all base+"literal") produced only 9
Route nodes. Recover the literal suffix from the right-hand operand of a
"+" concatenation; a right side that is not itself a literal is left
unresolved rather than guessed.
Fixes#1249
Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
nix_module_level_bindings_mint_variables
a `let` binding and an attrset binding both mint Variables, and the QN
carries the attrpath (services.nginx.enable).
nix_nested_bindings_are_not_module_level
a binding two attrsets deep mints nothing, and neither `deep` nor `nested`
becomes a Variable since both are scopes.
nix_lambda_binding_is_function_not_variable
a lambda-valued binding is a Function and NOT a Variable; a scalar binding
is the reverse.
Every absence assertion is paired with a positive one on the same predicate in
the same result — `topLevel` in the nesting test, `val` in the split test — so
none of them can pass by extracting nothing at all. Several false "clean" results
in this area came from predicates that could not have matched.
Signed-off-by: Jason Bowman <jason@json64.dev>
Four extraction cases and one pipeline case, each asserting a distinct claim so a
regression names what it broke:
nix_attrset_scope_disambiguates_leaf_names
two attrsets each holding `dup` produce two definitions with distinct QNs.
Unqualified they shared one QN and the second was discarded.
nix_dotted_attrpath_qualifies_like_nested
`wrap.deep.fn = …` and `wrap = { deep = { fn = …; }; }` produce the SAME
QN. This is the equality that makes attrpath qualification a correctness
fix rather than a preference — the two are the same expression.
nix_quoted_attr_name_strips_quotes
`"kebab-case"` is named kebab-case, and asserts the quoted form is absent
rather than merely that the bare form is present.
nix_interpolated_attr_mints_no_def
`"${dynamic}" = …` mints nothing, and the sibling binding still does — so
the test cannot pass by extracting nothing at all.
pipeline_nix_scoped_binding_calls_resolve (tests/test_pipeline.c)
a call inside a scoped binding reaches its target through the store, for
both routes into a qualified name: an enclosing attrset and a dotted
attrpath.
The pipeline case has to live at that level. Definition QNs and call-scope QNs
come from two separate functions, and if they disagree the edge is dropped at
write with no error — every extraction-level assertion still passes while the
graph quietly loses edges. Only the store shows it.
Adds has_def_qn alongside the existing has_def. find_def_by_name returns the
first match by NAME and so cannot tell two same-named definitions in different
scopes apart, which is exactly what these tests exist to separate.
Extraction and pipeline suites: 491 passed, 0 failed.
Signed-off-by: Jason Bowman <jason@json64.dev>
All three cost real release cycles, and none is fixed by widening a budget.
daemon_application_cancels_physical_job_only_after_final_session waited for the
SUBSCRIBER COUNT to reach 2, then cancelled both sessions and asserted the
physical job had started exactly once. The job starts asynchronously after
subscription, so both cancels could land first and leave starts == 0 --
arguably the correct outcome. It now waits for the state the assertions
actually require. Verified 47/47 on Windows, the only platform it ever failed.
The parallel harness refused outright when the suite leader had already exited,
because taskkill /T cannot walk a tree from a dead PID. But the leader can exit
between the timeout decision and that call, so the harness itself lost a race:
a natural exit at the wrong moment failed the whole wave. It now proves cleanup
the only way still available -- nothing parented to that PID -- and its contract
asserts the PROPERTY rather than the phrase "tree cleanup" it used to grep for.
That string pin is what broke when the guard was reworded while behaving
correctly; the contract now checks rc==2 AND that the descendant really did
survive, which would also catch a guard that claims to fail closed while
leaking.
extract_wide_flat_file_is_linear took ONE sample per size, so the ratio carried
the noise of both. On a loaded Windows VM linear code measured 51x against a 40x
bound (184ms -> 9387ms). Best-of-N instead: timing noise only ever adds time, so
the minimum is the cheapest good estimate of the noise-free cost. The bound is
deliberately unchanged -- it sits where linear (~20x) and quadratic (~128x) are
each >=2x away, so raising it would move the test toward the very signal it
exists to catch. Now measures 19.1x on Windows, 21.4x on macOS.
Also: the smoke's `cli` helper redirected stderr to a file and discarded it, so
any of the 10 bare `VAR=$(cli ...)` assignments could kill the run under
`set -euo pipefail` printing NOTHING. One such abort cost a full Windows cycle
just to locate and still could not be attributed. It now surfaces the command
and its stderr. Neutral wording on purpose: one call site expects a non-zero
exit and must not read as a failure.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`nix_function` was the only Nix extraction test, and it could not fail for the
bug the previous commit fixes. Three separate reasons:
- its fixture is function-headed — the exact broken shape;
- its binding, `hello = pkgs.writeShellScriptBin "hello" ''...''`, is an
application, not a function, so no Function def is expected from it either
way;
- it asserts only ASSERT_NOT_NULL and ASSERT_FALSE(has_error), omitting the
has_def check every adjacent language test makes.
So the suite reported green while definitions were being dropped for the
dominant file shape in the ecosystem.
This keeps `nix_function` as-is — it usefully pins the "parses, yields no def"
case — and adds one test per root shape, each asserting its own definitions so a
regression names the shape it broke:
nix_defs_in_let_rooted_file let ... in
nix_defs_in_attrset_rooted_file { ... }
nix_defs_in_nested_let let inside let
nix_defs_survive_function_header_let { prelude }: let ... in
nix_defs_survive_function_header_attrset { prelude }: { ... }
nix_defs_survive_curried_header final: prev: { ... }
nix_curried_lambda_mints_one_def guards the other direction
The curried-header case is the nixpkgs overlay signature, the most common
multi-arm header in the ecosystem: two nested function_expressions sit between
the file root and the body. The last test exists because the fix descends past
the header — it pins that `iota = a: b: a + b` mints exactly one Function, so a
change that starts minting the inner arm as a second def is caught.
Verified as a negative control — with the previous commit's one-line change
reverted, the three function-header tests fail on their own assertions (gamma,
eta, iota) while the pre-existing shapes stay green. With it applied the
extraction suite is 260 passed, 0 failed.
Signed-off-by: Jason Bowman <jason@json64.dev>
Client-side URL extraction only recognized static string literals; any
template literal was silently skipped, so parameterized endpoints never
produced HTTP_CALLS edges or Route nodes and cross-repo route matching
missed them (the server side already normalizes path params to {}).
New cbm_template_string_text() flattens a template_string node: string
fragments verbatim, each ${...} substitution becomes {}. Wired into:
- extract_positional_url / extract_string_value (call-arg URLs)
- handle_string_refs (URL-shaped refs from const/return positions)
- handle_string_constants (module-level const lookups)
`/api/v1/things/${id}` now yields __route__ANY__/api/v1/things/{} and
the enclosing function gets the HTTP_CALLS edge, joining the canonical
placeholder shape of server-side routes.
Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
A function-like macro invocation whose argument is a type token — e.g.
ALLOC(int, n) — makes tree-sitter's C/C++ grammar emit an ERROR node (it parses
the type in expression position), which cbm_collect_error_regions recorded as a
parse_partial coverage gap. But the macro is #defined in the same file and the
call sits inside an already-extracted function body, so nothing is actually
missing from the graph — it's a benign call the grammar can't parse without the
preprocessor (#1071, systematic across allocation-macro-style codebases).
Subtract an error region only when it both (a) contains an invocation of a
file-defined function-like macro and (b) is fully enclosed by an extracted
Function/Method body. Condition (b) keeps a TOP-LEVEL macro invocation that
expands to a definition still flagged (#949: the generated def isn't in the
original span), and fails safe — a benign top-level call stays flagged rather
than a real gap being hidden.
Tests: a type-arg macro call inside a function no longer reports parse_partial;
a real in-body syntax error still does; #949/#946 preserved.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Comments are NAMED tree-sitter nodes, so the prev-sibling walk in
extract_decorators() stopped at one — silently dropping every decorator ABOVE
an interleaved comment:
@Post('login') <-- dropped
@HttpCode(HttpStatus.OK) <-- dropped
// throttled per IP and account
@Throttle({ ... }) <-- kept
async login(...)
The route then vanished from decorator/route queries, so documenting a
decorator made the endpoint disappear from the graph. Real-world impact: on a
NestJS backend, 1 of 95 HTTP endpoints was missing — the one whose throttle
policy carried an explanatory comment.
Comments are now transparent to the walk, the same way anonymous tokens
(e.g. TS `export`) already were. Reuses the existing is_comment_node() helper.
Signed-off-by: KolisCode <jhohantma@gmail.com>
JAX-RS splits a route across two annotations: the verb comes from a bare
@GET/@POST/... and the path from a sibling @Path. The annotation scan
returned on the first mapping annotation, so the @GET matched, defaulted
the path to "/" and the sibling @Path was never read. Class-level @Path
was never recognized as a prefix either, because a lone @Path carries no
verb and the scan reported no route.
scan_route_annotations() now collects the whole annotation set (mapping
verb + optional inline path, and a separate JAX-RS @Path), then:
- method-level: verb required; path = inline mapping path, else @Path,
else "/"
- class-level prefix: inline mapping path, else @Path
Also recognizes bare @HEAD/@OPTIONS verbs.
Spring behavior is unchanged (mapping annotations keep their inline
path); covered by the existing test suite plus a new regression test.
Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
A function whose body braces are split across #ifdef/#else branches
(one open brace per branch, a single shared close) parses with an
ERROR region on the raw source - both branches are present at once, so
the braces are unbalanced - and the defs walk silently dropped it
while its callers stayed as unresolved names (cbm_path_within_root,
handle_process_kill).
The preprocessed second pass (simplecpp) already exists for macro-
hidden CALLS; it evaluates the conditionals, so one branch is chosen
and the expanded tree parses clean, with same-file token lines aligned
to the original. Recover definitions from it, adopting only defs that
(a) intersect a raw-tree ERROR region, (b) have their name visible on
the corresponding original source line (rejects header-inlined defs
whose physical expanded lines alias unrelated raw lines when
compile_commands include paths resolve), and (c) are absent by
qualified name from the raw pass.
Verified on the real instances: handle_process_kill (580-647) and
cbm_path_within_root (5070-5085) both index with correct line ranges.
Closes#961
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The #599 receiver-aware narrowing (87091ed) shipped without coverage
for the delegation/store bucket reported in #876: same-named calls
whose receiver is a call result (_get_store().get inside get), a
module singleton (_default.check inside check), or a loop-local cursor
(cur.execute inside execute). All three are clean on current main;
this guard pins the bucket so it cannot regress. Refs #876
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Add ObjectScript (InterSystems IRIS / Caché) as a supported language,
covering the UDL class format (.cls), MAC/INT routines (.mac/.int/.rtn),
include/macro files (.inc), and IRIS Studio Export XML.
Definition extraction (extract_defs.c): Class, Method, ClassMethod,
Property, Parameter, Index, Trigger (with body text), XData, Storage,
and Query members as graph nodes; base classes from the Extends clause.
Call dispatch resolution (extract_calls.c) — four ObjectScript patterns
that are structurally invisible to text search:
1. ##class(Pkg.Class).Method() explicit cross-class call
2. ..Method() relative-dot self-call (the dominant
intra-class form; large impact on
CALLS completeness)
3. $$$Macro macro expansion via a per-project
table built from .inc files
4. type inference from %New/%OpenId + declared return types
Ensemble production topology (pass_ensemble_routing.c): EnsembleItem
nodes per production component and ROUTES_TO edges resolved from
ProductionDefinition XData, plus WorkMgr .Queue("##class(X).method")
dispatch — all parsed statically at index time, no live IRIS required.
Language detection (language.c): .mac/.int/.rtn map to ObjectScript
routine directly; .cls (shared with Apex) and .inc (shared with BitBake)
are disambiguated by content, defaulting to the existing language on any
doubt so neither Apex nor BitBake detection regresses.
The two new per-project tables (macros, return types) are threaded
through a new internal cbm_extract_file_ex() so the public
cbm_extract_file() signature is unchanged.
The tree-sitter grammars for ObjectScript UDL and routine are vendored
in internal/cbm/vendored/grammars/objectscript_{udl,routine}/ from
https://github.com/intersystems/tree-sitter-objectscript (MIT, ABI 15).
Refs #462
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
The guard's sparse defs scaled WITH the input (one per 2000 lines), so
the fixture inherited a slice of the separate per-def sibling-scan cost
(O(defs x siblings) — its own tracked finding): an n^2/2000 term that
is negligible on clang-macOS but dominant enough on windows-CLANG64
ASan to push LINEAR walk code to a 43x measured ratio, over the 40x
bound. Ten defs at fixed positions regardless of size keep the
anti-vacuous breadth check while the sibling-scan term stays 10 x n =
linear. Measured after the change: fixed code 22x, pre-merge quadratic
walk 339x — the discriminator now has ~2x headroom on the green side
and ~8x on the red side across all three toolchains measured.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
An absolute wall-clock bound conflates machine speed with complexity
class: the gcc-13-ARM ASan CI leg runs this extraction ~200x slower
than clang at measured-perfectly-linear complexity (27.1/54.2/108.3/
214.8s for 50k/100k/200k/400k siblings) and flunked a 30s bound on
LINEAR code — the leg has always been the slow one (71min in the last
fully green run, unchanged since).
Assert the complexity class instead: extract the same comment-sibling
fixture at 20k and 400k lines and bound the growth ratio at 40x.
Calibrated from measurement, not models — the ASan builds carry a large
linear per-line baseline that dilutes small-growth ratios (at 8x growth
the known-quadratic pre-merge walk measured 22.4x and slipped under a
24x bound), while at 20x growth linear code measures ~20x on both
toolchains and the quadratic walk measures 64x. Verified RED (64.4x,
fails) against the pre-merge walk_defs and GREEN (19x) on the fix; the
test now prints both timings unconditionally for future triage.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
An 81k-file TypeScript corpus previously never finished indexing. Three
stacked defects, found by stack-sampling the live grind and reading the
supervisor's quarantine records:
1) walk_defs pushed children via index-based ts_node_child(i), which is
O(i) per call in tree-sitter — O(n^2) per wide node. A 3.5 MB file
with ~580k flat comment siblings needed ~1.7e11 iterator steps and
hung extraction past the supervisor's quiet-timeout. All six descent
sites (including the default loop) now collect children linearly with
a TSTreeCursor (wd_collect_children); small nodes keep the direct
index path. Guard: extract_wide_flat_file_is_linear (400k comment
siblings — the monster's exact shape — bounded at 30s).
2) The sequential cross-LSP driver handed the FULL def list to a full
per-file registry build+finalize — O(files x defs); 74% of samples
sat in build_qn_index. The parallel resolve worker's dispatch
(module-def filter -> shared prebuilt registries -> filtered
fallback) is now extracted into cbm_pxc_dispatch_file and drives
BOTH pipelines — one path, one semantics. The shared registries
live in a caller-owned arena that outlives the calls pass
(resolved_calls borrow registry strings; freeing at pass end was a
use-after-free ASan caught in pass_calls). Rust is exempt from the
def filter: Cargo-manifest cross-crate resolution needs defs from
OTHER workspace crates that the own+imports filter drops (this
starvation predated the unification on the parallel path). Guard:
pipeline_seq_ts_cross_uses_shared_registry (full builds 40 -> 0,
cross-file edge preserved).
3) The crash supervisor's recovery re-ran the worker SINGLE-THREADED to
keep one exact marker. At scale that fell into the sequential crawl,
was killed as a hang mid-pass, and the stale extraction marker got
four innocent files quarantined, one 15-minute retry at a time.
Recovery (and the terminal partial run) now re-run PARALLEL; the
marker is an append journal ("S <rel>" / "D <rel>", written by
extraction and both resolve drivers), the suspect set is the open-S
in-flight set, and a file is quarantined only when it recurs across
two consecutive failed runs — one per round, oldest open S first;
disjoint consecutive sets stop the loop rather than blame an
innocent. Guard: index_recovery_parallel_quarantines_crasher
(single-threaded spawn count must stay zero; verified RED against
the old loop).
Also: parse_ts_type_text gains a dynamic work budget proportional to
the input (1M + 64x source bytes, CBM_TS_TYPE_BUDGET override, warn
once then degrade to unknown) replacing unbounded recursion cost.
Measured on microsoft/TypeScript (81,398 files): infinite -> 33s cold
full index, 295k nodes / 779k edges, zero dangling edges, zero files
skipped — the monster file itself now indexes cleanly. Full 9-language
sweep green with no quality flags; kernel unchanged at 390s / 8.5M
nodes / 0 dangling.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Inline Rust test fns (#[test], #[tokio::test], #[async_std::test],
#[actix_rt::test], #[test_case::case]) inside a regular .rs file were indexed
as ordinary Functions (is_test=false) and leaked past the store.c is_test!=1
filter into graph/agent context — test detection was file-path-based only
(cbm_is_test_file: *_test.rs / test_*). Add rust_def_is_test(decorators)
(mirroring rust_cfg_qualified_name's scan) and set def.is_test in
extract_func_def's Rust branch.
Closes#855.
Signed-off-by: Greg Tiller <tiller@dal.ca>
A JS/TS/TSX member call `x.foo()` whose receiver is not `this`/`super` now
sets CBMCall.is_method, mirroring the existing Perl arrow/method flag. This
lets the call-resolution passes recognise a member call whose receiver type
the TS-LSP could not resolve, so they can decline to bind it to an unrelated
project symbol by a weak short-name guess (#592/#606; precedent PR #477 for
Perl).
`this`/`super` receivers are left unflagged — their target is the enclosing
class, where a namespace-proximity weak match is usually right. Bare calls and
new-expressions have no member receiver, so they stay is_method=false; every
non-TS/JS/Perl language is byte-identical (the struct is zero-init).
The pre-#592 test that asserted a JS member call never sets is_method inverts;
it is replaced by TS and JS flagging tests plus a Go test that keeps the
"flag-exempt languages are unaffected" contract, and the cbm.h field comment
is updated to describe both the Perl and TS/JS semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hitoshi Okazaki <okazaki.hitoshi@tecnos.co.jp>
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>
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>
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>
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>
After the C declarator-name walker was de-duplicated into one shared helper
(cbm_resolve_c_declarator_name_node), the C/C++ enclosing-function resolver now
resolves qualified names (Foo::bar) via resolve_qualified_name() and no longer
treats type_identifier as a terminal name. Add reproduce-first coverage so the
#438 fix cannot silently regress on the qualified-declarator path:
- cpp_out_of_line_method_caller_attribution: a call inside `void Foo::bar()` must
attribute to the method, not the module.
- cpp_out_of_line_ctor_dtor_caller_attribution: calls inside `Foo::Foo()` and
`Foo::~Foo()` must attribute to the special member, not the module.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A CALLS edge whose caller is a C/C++/CUDA/GLSL function was sourced to the
file's Module node instead of the calling Function. "Find callers of X"
returned a file path, outbound trace_path returned empty, and
(:Function)-[:CALLS]->(:Function) queries missed for these languages.
Root cause: the enclosing-function resolvers read only tree-sitter's `name`
field, but a `function_definition` node has none — the name lives in the
declarator chain (pointer/function/parenthesized/array declarators). So
func_node_name() (internal/cbm/helpers.c) and resolve_func_name_node()
(internal/cbm/extract_unified.c) returned NULL, the enclosing scope fell
back to the module QN, and the edge was attributed to the Module node. This
is the C counterpart to #220, which fixed the definition-naming path but not
the enclosing-call path.
Fix: descend the declarator chain to the innermost name node (mirroring
resolve_c_declarator_name in extract_defs.c, including qualified and operator
names) when a function_definition lacks a `name` field. Adds the regression
test c_caller_attribution asserting a C call's enclosing_func_qn is the
function, not the module.
Fixes#438
Signed-off-by: Kris Kersey <kris@kerseyfabrications.com>
Python calls of the form iris_obj.classMethodValue('Pkg.Class', 'Method')
emitted no CALLS edge. The detection existed in walk_calls, which is dead
code — the pipeline uses cbm_extract_unified → handle_calls, not walk_calls.
Move the IRIS Native API string-dispatch detection to handle_calls. When a
Python call's callee ends in .classMethodValue, .classMethodVoid,
.classMethodBoolean, or .classMethodObject, and both string-literal arguments
can be extracted, emit a synthetic CALLS edge to Arg1.Arg2.
The pattern generalizes beyond this specific API: any library that takes
class/method names as string literals follows the same shape.
Regression test: python_iris_classMethodValue in test_extraction.c.
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
* fix(perl): stop extracting config strings as call targets
The Perl branch of extract_scripting_callee blindly returned the text of
child(0) of every call node. In config-heavy Perl (.cgi/.pl with embedded
log4perl-style config), tree-sitter-perl misparses dotted config tokens
(e.g. "log4perl.appender.File.utf8") into call-shaped nodes, and that
dotted string was emitted as a callee_name, later matched by the generic
short-name resolver to unrelated project subs.
Now the Perl branch pulls the real name token (method/function field, else
child(0)) and validates it as a bare Perl sub/method identifier via
perl_is_identifier_callee: must start with a letter or '_' and contain only
[A-Za-z0-9_:] (allowing the '::' package separator). Any '.', whitespace,
quote, or '/' disqualifies it and NULL is returned so no CALLS edge forms.
Gated to CBM_LANG_PERL; other languages are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(resolver): don't match Perl builtins to project subs
Perl builtins (push/shift/keys/sprintf/...) carry no language or call-kind
awareness through the generic name-matcher in cbm_registry_resolve. When a
project defines a sub whose name collides with a builtin, an invocation of
the builtin was wired to that sub by same-module / suffix matching - a
false-positive CALLS edge.
Adds cbm_perl_is_builtin (curated, sorted bsearch set of 94 perlfunc core
builtins) and applies it in both call-resolution passes (sequential
resolve_single_call and parallel resolve_file_calls), gated on the file
language == CBM_LANG_PERL and only AFTER LSP resolution has declined, so a
genuine LSP-resolved call is never suppressed. The file language is threaded
into both resolvers via a new trailing CBMLanguage parameter; every other
language reaches cbm_registry_resolve unchanged (byte-identical behavior).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(resolver): suppress generic short-name matching for Perl method calls
A Perl method call ($obj->m / Class->m) carries no receiver type at the
structural tier, so the generic short-name matcher in cbm_registry_resolve
would wire $dbh->commit / $cgi->param / $logger->log to any project sub
sharing the bare method name - the dominant source of false-positive CALLS
edges in CPAN/framework-heavy Perl. Resolving such a call correctly is the
LSP's job, not the bare-name matcher's.
Adds a CBMCall.is_method flag (zero-init false, so all other languages and
existing call sites are unaffected). method_call_expression is added to the
Perl call node set and handle_calls sets is_method=true only for that node
type when the file language is Perl. Both call-resolution passes then skip
generic resolution for Perl method calls (combined with the builtin guard
from the prior commit). Genuine intra-project function calls (non-method,
non-builtin) still resolve as before. LSP-resolved method calls are
unaffected because the guard runs only after LSP resolution declines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(perl): cover call-graph noise fixes (builtins, methods, config strings)
Hermetic tests for the three Perl call-graph noise fixes:
test_extraction.c (extraction tier):
- config string is never emitted as a callee; genuine call still extracted
- builtin calls (push/keys) extracted but never flagged is_method
- arrow/method calls ($self->commit / $dbh->commit) set is_method=true,
while the genuine function call (helper) does not
- a JS method call never sets is_method (flag is Perl-only — other
languages unaffected)
test_registry.c (resolver tier):
- cbm_perl_is_builtin recognizes core builtins (incl. first/last of the
sorted set) and rejects project subs, case variants, empty, and NULL
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(perl): address QA round 1
Round 1 (Claude panel + DO DeepSeek) findings:
- Rework the Perl noise guard so it suppresses only WEAK generic matches
(suffix_match/unique_name) and KEEPS high-confidence same_module/import_map.
The prior guard ran before cbm_registry_resolve and dropped genuine same-file
calls to builtin-named subs (e.g. a project sub log/index/open called as a
bare function) that pre-PR resolved via same_module. Extracted the decision
into pure, unit-tested cbm_perl_suppress_generic_match() shared by the
sequential (pass_calls.c) and parallel (pass_parallel.c) resolvers; corrected
the inaccurate comments (Perl has no LSP/textual stage before the guard).
- Tighten perl_is_identifier_callee to require '::' pairs (reject a lone ':',
':::', or trailing '::').
- Add resolver-contract tests covering weak-match suppression, same_module/
import_map retention, genuine-call survival, non-Perl no-op, and NULL strategy.
Verified on a real Perl monorepo: .cgi builtin/CPAN/config-string noise stays
eliminated while same_module edges to builtin-named subs are recovered.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(perl): address QA round 2
Round 2 (Claude panel) caught a regression introduced by the round-1 refactor:
cbm_perl_suppress_generic_match whitelisted only the exact strategies
"same_module" and "import_map", but resolve_import_map can also return
"import_map_suffix" (confidence 0.85 — a genuine import-based resolution, not a
weak short-name guess). A '::'-qualified Perl builtin/method call resolved via
the import-suffix fallback was therefore dropped, contradicting the helper's
documented contract and partially missing acceptance criterion (d).
Add import_map_suffix to the kept (high-confidence) set so only the weak
short-name strategies (suffix_match / unique_name) are suppressed; update the
doc comment and add a unit-test case asserting import_map_suffix is retained.
Deferred as advisory (non-blocking, noted on the PR): a hypothetical leading-'::'
(main:: shorthand) under-extraction in perl_is_identifier_callee, and a
colon-edge-case coverage gap (logic correct by inspection).
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
C++/CUDA out-of-line method definitions (Foo::bar in a .cpp; the class body lives declaration-only in a header) were recorded as free Functions with no link to their class. A new helper cpp_out_of_line_parent_class() detects a qualified_identifier declarator on a function_definition and resolves the immediate enclosing class (descending nested scopes ns::Foo::bar to the direct parent); extract_func_def then promotes the def to label Method with parent_class set to the class QN so DEFINES_METHOD edges resolve. Distilled (reimplemented) from PR #428. Reproduce-first: cpp_out_of_line_method_issue428 (single-level out-of-line defs; the nested-namespace path isn't isolation-testable because tree-sitter-cpp parses a synthetic doubly-qualified def as an ERROR node, so it is exercised by real codebases).
class Animal(Base) extracts the base as the argument_list text "(Base)" instead of the bare "Base", so it never resolves to the Base class node and Python subclasses get zero INHERITS edges. Capture this as a failing test ahead of the fix (root cause: collect_bases_from_field ignores the identifier node tree-sitter-python uses for the base).
Per-function complexity metadata is now stored on graph nodes and queryable,
alongside several indexing performance and correctness fixes developed and
validated together (3704 tests, ASan/UBSan clean).
Bottleneck metrics (query via query_graph):
- Tier A (in the extraction AST walk): cyclomatic (complexity), cognitive
(nesting-weighted), loop_count, loop_depth (max nested-loop depth),
param_count, max_access_depth.
- Tier B (new pre-dump pass, pass_complexity.c): transitive_loop_depth
propagated along CALLS edges + a recursive flag (direct self-recursion and
mutual-recursion cycles), plus the call-context signals linear_scan_in_loop,
alloc_in_loop, recursion_in_loop and unguarded_recursion.
- query_graph and get_architecture tool descriptions document the metrics and
the Leiden community clusters.
Cypher engine:
- node_prop exposes arbitrary persisted node properties to WHERE/RETURN.
- Fix projection aliasing: multi-property rows shared a single static buffer so
every column returned the last value read; now per-column/rotating buffers.
- Fix a stack-use-after-scope in aggregate RETURN (caller-owned value buffers).
Indexing performance:
- Gate C/C++ #define Macro-node extraction to full mode (it is ~49% of nodes on
the Linux kernel); moderate/fast skip it.
- Emit the complexity property block only for Function/Method nodes so the
millions of Macro/Field/Variable/Class/Enum nodes no longer carry zeroed
fields — large RAM reduction at scale.
- Classify node types via tree-sitter TSSymbol bitsets in cbm_kind_in_set
instead of per-node strcmp scans (thread-local cache, strcmp fallback;
behaviour-identical).
- Subsample frequent (Zipfian) tokens in the semantic co-occurrence finalize;
~14x faster finalize on the kernel, output unchanged.
- pass_lsp_cross: replace O(n^2) linear dedup with hash-set dedup.
Windows:
- Canonicalize drive-letter case during path normalization so "c:/repo" and
"C:/repo" derive the same project key and cache file (#394/#227/#367).
Tests: extraction, pipeline and cypher regressions covering all of the above.
Two more Helm semantic improvements for #338:
- Chart.yaml: parse the top-level name and the dependencies: list
(cbm_parse_helm_chart) and, in the k8s pass, emit a Chart node per chart plus
a DEPENDS_ON edge to a shared, per-project deduplicated Chart node for each
declared dependency. So "which charts depend on postgresql" is now traversable.
- values.yaml: extract only the top-level keys as structured Variables instead
of one node per nested leaf key — killing the node flood the issue described
(86 nodes for a typical values.yaml down to a handful).
Generic-YAML one-node-per-file dedup (repo-wide behavior change) is intentionally
left as a separate follow-up. Tests cover the dependency parser (with/without
deps) and the values.yaml top-level-only extraction.
Closes#338
Helm chart helpers (.tpl) and Go templates produced no semantic graph — just a
flat key flood. Wire the existing gotemplate grammar for real structure:
- .tpl files map to the gotemplate language (Helm _helpers.tpl).
- {{ define "chart.fullname" }} becomes a Function node named after the
template (the name is a string literal, not an identifier, so it's resolved
in a dedicated walker branch).
- {{ include "x" . }} and {{ template "x" . }} resolve to the referenced
template name instead of the bare `include` builtin, so they form CALLS
edges to the define'd Function. template_action is added to the call set.
Lets you trace which charts include which named templates. Tests cover define
-> Function and include/template -> CALLS, plus the .tpl mapping.
Refs #338