Commit Graph
65 Commits
Author SHA1 Message Date
Martin Vogel a3ae7513a4 Merge pull request #1400 from AmirF194/fix/1249-binary-concat-route-url
fix(extract): resolve Go route paths built via string concatenation
2026-08-15 08:57:12 +02:00
Martin VogelandHarshita Joshi 5eae0a4371 test(extract): bind the Java interface/enum method dedup fix (#1234)
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>
2026-08-04 16:37:06 +02:00
Amir Fathi 478ce25ce3 fix(extract): resolve Go route paths built via string concatenation
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>
2026-08-01 06:17:02 +00:00
Martin Vogel 029416fda6 Merge remote-tracking branch 'origin/main' into qa/call-reference-rebase
# Conflicts:
#	Makefile.cbm
#	internal/cbm/extract_defs.c
#	src/foundation/compat_fs.c
#	src/foundation/compat_fs.h
#	src/pipeline/pipeline.c
#	src/pipeline/pipeline_incremental.c
#	tests/test_main.c
#	tests/test_pipeline.c
#	tests/test_store_checkpoint.c
2026-08-01 01:29:20 +02:00
Martin Vogel 8f06867d78 Merge pull request #1305 from sini/feat/nix-attrpath-and-variables
fix(extract): model Nix attrpaths (leaf name, scoped QN) and mint Variables for module-level bindings
2026-07-31 08:17:56 +02:00
Martin Vogel facd32b230 Merge pull request #1008 from CharlesQueiroz/fix/template-string-urls
fix: flatten JS/TS template-literal URLs to {} placeholders
2026-07-31 05:33:38 +02:00
Jason Bowman 7f9feb6b63 test(nix): cover Variable extraction, scope bound, and the Function/Variable split
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>
2026-07-30 17:45:25 -07:00
Jason Bowman b5209af7a3 test(nix): cover attrpath qualification, quoting, and interpolation
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>
2026-07-30 17:45:25 -07:00
Martin Vogel 4573a51d8f Merge pull request #1329 from harshitaajoshi/fix/cpp-gtest-name-collision-1266
fix(extract): derive unique names for GoogleTest macros to prevent same-file node collisions (#1266)
2026-07-31 02:39:32 +02:00
Martin Vogel 475a698d69 Merge pull request #1097 from JhohanBustamante/fix/decorator-comment-drop
fix(extract): keep decorators separated by a comment
2026-07-31 02:39:25 +02:00
Martin Vogel 8313b9abd8 Merge pull request #1304 from sini/spike/nix-function-header-defs
fix(extract): Nix definitions are dropped in any file whose root expression is a function
2026-07-31 02:08:50 +02:00
Martin Vogel ae52db3147 test: make three Windows nondeterminisms deterministic
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>
2026-07-29 17:05:10 +02:00
Harshita Joshi a6f32d0fba fix(extract): derive unique names for GoogleTest macros to prevent same-file node collisions (#1266)
Signed-off-by: Harshita Joshi <j.harshitaa06@gmail.com>
2026-07-28 23:25:36 -05:00
Jason Bowman c7cb8c4e33 test(nix): cover the root shapes, and assert defs rather than absence of error
`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>
2026-07-27 17:14:39 -07:00
Martin Vogel 87a0e3f74f fix: preserve semantic graph relationships
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 15:52:50 +02:00
Charles Queiroz a029b3a63b fix: flatten JS/TS template-literal URLs to {} placeholders (#1006)
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>
2026-07-16 14:30:55 -03:00
Martin Vogel a75f4b6a3b fix(extract): don't flag in-body function-like macro calls as parse gaps
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>
2026-07-16 18:19:54 +02:00
KolisCode 012be57815 fix(extract): keep decorators separated by a comment
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>
2026-07-15 21:01:29 -05:00
Martin Vogel e2d41d9cd4 Merge pull request #1094 from harshitaajoshi/fix/utf8-docstring-truncation-1017
fix(extract): snap docstring truncation to UTF-8 codepoint boundary (#1017)
2026-07-16 02:16:11 +02:00
Martin Vogel 2080d82100 Merge pull request #1060 from isc-tdyar/fix/semantic-body-tokens
fix(extract): populate body_tokens before MinHash gate; add OS/TS identifier node types
2026-07-16 01:50:34 +02:00
Martin Vogel ef1482245f Merge pull request #949 from blankanswer/agent/fix-cpp-preproc-signature-gap
fix(cpp): remap recovered definitions to original lines
2026-07-16 00:05:49 +02:00
Harshita Joshi 4ce04f2d03 Merge branch 'main' into fix/utf8-docstring-truncation-1017 2026-07-15 16:28:54 -05:00
Harshita Joshi 3ebcf48a46 fix(extract): snap docstring truncation to UTF-8 codepoint boundary (#1017)
Signed-off-by: Harshita Joshi <j.harshitaa06@gmail.com>
2026-07-15 16:27:34 -05:00
Martin Vogel 34867d9b34 Merge pull request #1007 from CharlesQueiroz/fix/jaxrs-path-composition
fix: compose JAX-RS method-level @Path with class-level prefix
2026-07-15 22:21:38 +02:00
Thomas Dyar b9797ecd44 fix(extract): populate body_tokens before MinHash gate; add OS/TS identifier node types
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
2026-07-14 20:53:58 -04:00
Martin Vogel feadbe1955 Merge pull request #467 from isc-tdyar/pr/objectscript-language-support
feat(objectscript): InterSystems IRIS ObjectScript language support
2026-07-15 01:33:19 +02:00
Charles Queiroz 041eb99c11 fix: compose JAX-RS method-level @Path with class-level prefix (#1005)
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>
2026-07-13 23:17:08 -03:00
Blank_Answer 99d39192cd fix(cpp): remap preprocessed recovery lines
Signed-off-by: Blank_Answer <97771966+blankanswer@users.noreply.github.com>
2026-07-13 05:21:54 +00:00
Martin Vogel db330bf3a0 Merge pull request #1050 from DeusData/fix/961-ifdef-split-defs
fix(extract): recover C/C++ defs lost to #ifdef-split brace ERROR regions
2026-07-12 16:41:22 +02:00
Martin Vogel 65a176e4d8 fix(extract): recover C/C++ defs lost to #ifdef-split brace ERROR regions
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>
2026-07-12 15:09:07 +02:00
Martin Vogel 2f3be252a3 test(extract): guard delegation-receiver recursion false positives
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>
2026-07-12 13:55:45 +02:00
Thomas Dyar 4d6f2dd34a feat(objectscript): add InterSystems IRIS ObjectScript language support
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>
2026-07-11 19:09:43 -04:00
Martin Vogel 975dc76709 fix(test): constant def count in the wide-flat scaling fixture
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>
2026-07-07 15:55:24 +02:00
Martin Vogel f380a4514f fix(test): scaling-ratio form for the wide-flat linearity guard
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>
2026-07-07 14:51:15 +02:00
Martin Vogel 7b8b7038f7 fix: TypeScript-at-scale indexing cascade and parallel-only crash recovery
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>
2026-07-07 10:34:06 +02:00
Greg Tiller 7b7e05652b fix(extract-rust): mark #[test]/#[tokio::test] fns is_test so they are filtered
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>
2026-07-04 14:22:13 -03:00
Hitoshi OkazakiandClaude Fable 5 60d0855541 fix(extract): flag TS/JS member calls with non-this receivers as method calls
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>
2026-07-04 12:37:17 +09:00
Martin VogelandGen Li 87091edd24 fix(extract): receiver-aware self-recursion detection
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>
2026-07-03 20:20:49 +02:00
Martin Vogel 174e56b447 fix(extract): heap-allocate walk_defs frame stack — fix overflow + >4096 truncation (#668)
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>
2026-07-02 18:39:17 +02:00
Martin Vogel b3b3a48998 fix(fqn): directory-based module QN for Java/Go (de-double) + nested-class enclosing
Java/Go QNs doubled the top-level class/package (proj.Outer.Outer.run; Go module
included the filename). Add cbm_fqn_module_source_lang / cbm_fqn_compute_source_lang
(dir-based module for Java/Go; legacy cbm_fqn_compute UNCHANGED for import paths),
route the single authority cbm.c:569 result->module_qn + the def/calls/pipeline
module derivations through them. Also fix unified compute_class_qn to prefix the
enclosing class for nested classes (was ignored) so def-QN == LSP caller_qn ==
calls-enclosing → lsp_outer_dispatch joins. Reproduce-first tests added; legacy
test_fqn.c/test_registry.c (generic helper) preserved. Per external second-opinion plan.

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

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 16:23:26 +02:00
Martin Vogel 4953fcaf21 test(extract): guard C++ out-of-line method/ctor/dtor enclosing attribution
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>
2026-06-25 21:42:31 +02:00
Kris Kersey 13c3429651 fix(extract): attribute C/C++ CALLS edges to the enclosing function
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>
2026-06-24 12:42:38 -04:00
Thomas Dyar 0b94f44f96 fix(extract): string-dispatch calls in dead code path, never linked to target
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>
2026-06-22 19:56:46 -04:00
92b4506cd5 fix(perl): eliminate false-positive Perl CALLS edges (builtins, framework method calls, config strings) (#477)
* 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>
2026-06-22 22:03:53 +02:00
Martin Vogel a50b086809 Attribute out-of-line C++ method definitions to their class (distill #428)
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).
2026-06-09 14:32:26 +02:00
Martin Vogel 0f04003508 Reproduce Python class-inheritance base-name extraction bug
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).
2026-06-05 21:53:25 +02:00
Martin Vogel 12683b84a9 feat(graph): queryable computation-bottleneck metrics + indexing perf & fixes
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.
2026-06-02 18:50:55 +02:00
Martin Vogel 1b308ec864 feat(helm): Chart.yaml dependencies -> DEPENDS_ON edges + values.yaml top-level keys
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
2026-06-01 19:43:15 +02:00
Martin Vogel a8fc901129 feat(helm): extract Helm/Go-template named templates and include calls
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
2026-06-01 18:41:49 +02:00