Commit Graph
4461 Commits
Author SHA1 Message Date
Martin Vogel 7ec13add98 fix(test-impact): mark conditionally registered runner suites CONDITIONAL on every selection path
A runner suite registered only under a preprocessor condition (the four
coverage_* suites under `#if defined(CBM_TEST_COVERAGE) && !defined(_WIN32)`
in tests/test_main.c) is absent from builds without that condition. The
selection marked such a suite CONDITIONAL only on the per-registration path,
and only when a registration of its own was conditional and contributed a
reason. Every suite-level path (setup unknown or hit, suite-body, changed and
rule triggers, inventory, rejected artifact) made the suite whole without it,
and the runner registration's own condition was never recorded.

PR CI on the x86 Linux gcc leg then failed with "the test selection names
suites this build does not register: coverage_setup_success
coverage_setup_uncertain" (#2679, #2680): the suite-body trigger and an
incomplete setup row selected both suites whole without CONDITIONAL, so
selection.py left them out of optional-suites.txt.

The model now records whether a runner registration sits under a condition
(conditional only when every site of the suite is), and the selection marks
any selected conditionally registered suite whole with CONDITIONAL after every
whole-suite path. Unconditionally registered suites keep the strict contract.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 16:52:36 +02:00
Martin Vogel fed5cbf9cc fix(extract): test the empty Rust impl name before the cut
cppcheck 2.20 reports the post-cut `if (!name[0])` as always false
(knownConditionTrueFalse): it cannot see that writing through `lt`
may empty `name`. The name is non-empty here, so the cut empties it
only when `<` is the first character. Test exactly that before the
cut. The behaviour is unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 16:49:36 +02:00
Martin Vogel 67ff40d5cb fix(kotlin): resolve calls through typed class properties (#1555)
The cross-file Kotlin walk resolved a call on a typed method parameter by
type, but a call on a class property - a primary-constructor val, a body
property or `this.prop` - fell to the name guess. The cross registry held
each own-file class from its extraction defs, whose properties carry no type
(constructor vals none at all), and the walk's typed AST registration of the
same class was added behind that entry under the same QN, so every lookup
returned the field-less one. Constructor injection is the common Kotlin
dependency shape, so `repository.inserir(...)` on an injected port bound to
whichever same-named method the guess found, including an implementation of
another port (#1555).

The walk now registers the file's own declarations into an overlay chained
to the cross registry and checked first, indexed once the top-level
declarations are collected. The sole-definer fallback walks the whole chain
so its candidate set is unchanged.

Typing more receivers exposed resolution gaps that the untyped receivers had
hidden; each would otherwise turn a typed call into a wrong name guess:
- a member of the receiver class wins over a same-named extension of the
  same file (Kotlin resolution order; the overlay holds both);
- `x.hashCode()` / `toString()` / `equals()` on a receiver typed by another
  file: the per-file pass emits the kotlin.Any placeholder, the cross pass
  the declared override, and two targets at one occurrence are ambiguous to
  the call join. The per-file placeholder is dropped when the cross pass
  resolves the same occurrence;
- the same members on a stdlib or JDK type (`count.toString()` on an Int)
  resolve to the kotlin.Any builtin instead of a seeded stdlib member that
  has no graph node;
- a property READ on a typed receiver whose QN the graph spells differently
  (a nested class's property) never becomes a CALL_REFERENCE to a
  same-named function: the reference join falls back by name to
  non-callable targets only, which binds the property's own node;
- a typed value without an `invoke` operator (`private val handshake:
  Handshake?`) no longer shadows the same-named member function;
- inside a lambda whose typed receiver becomes `this` (`repo.let { .. }`),
  and inside a member extension, the enclosing class's members stay
  callable bare (the implicit-receiver chain); a call naming a class stays
  a constructor call.

Fixes #1555

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 16:42:30 +02:00
Martin Vogel 2ecf202b3c fix(routes): keep every overload's decorator route on both resolvers (#1692)
An ASP.NET MVC GET form action and its POST submit share one method name,
so both overloads collapse into one Method node whose merged properties
keep only one route. The parallel resolver emitted each definition's route
at node creation and kept both; the sequential one rebuilt decorator routes
from the merged properties afterwards and kept only the last. On six
ASP.NET corpora the sequential run lost 28 HANDLES edges this way.

Both resolvers now call cbm_pipeline_emit_def_route() per definition at
node creation (the parallel block moved into it unchanged), so the venues
agree. The later decorator-route pass still skips handlers it already has.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 16:42:21 +02:00
Martin Vogel 753df46b78 Merge remote-tracking branch 'origin/main' into fix/issue-1692-aspnet-routes-v2
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 16:40:04 +02:00
Martin Vogel 090580b3c9 refactor(python): scope the nested-def counter to the Python branch
cppcheck 2.20 variableScope on defs_before; same behaviour.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 16:36:35 +02:00
Martin Vogel f6b505914a Merge origin/main into fix/issue-1548-python-nested-defs-v2
Conflict in pass_definitions.c: process_def gained main's link_parents and this branch's module_prefix_len; both kept.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 16:34:04 +02:00
Martin Vogel 8469fed960 test(rust-lsp): tag the shared-registry fixtures as Rust defs
The shared Tier-2 build now registers Rust definitions only. Two
fixtures that feed cbm_rust_build_cross_registry left CBMLSPDef.lang
zero, which the pipeline never does (pass_lsp_cross.c tags every def
with its file's language). The other shared-registry fixtures in this
file already set CBM_LANG_RUST. Set it on these two as well.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 06:55:57 +02:00
Martin Vogel eec3418fef fix(python): a test's nested helper keeps TESTS for its calls (#1548)
Since #1548 a def declared inside a Python test function is extracted, so its calls start at the helper instead of the test FILE node, whose name passed the test-name check. 4,641 of 6,315 TESTS edges lost on the corpus were this shape. TESTS for a call from such a helper is now attributed to the nearest enclosing test function of the same file; a helper nested in a non-test function stays without TESTS, like a module-level helper.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 06:18:09 +02:00
Martin Vogel a120c9439e Merge origin/main into fix/py-relative-package-import
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 05:30:37 +02:00
Martin Vogel f2534d1e3d Merge origin/main into fix/py-dotted-base-qn
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 05:00:49 +02:00
Martin Vogel d4beec3c58 test(python): local classes are extracted defs now (#1548)
pylsp_nested_and_local_classbody_calls_join_extracted_caller asserted that a class declared in a function body is NOT a def, which is the behaviour #1548 fixes. Their class-body calls still join the enclosing function; that part is unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 04:48:27 +02:00
Martin Vogel 5bfa7b53bf test(python): note that H1 may move the branch-def case to an lsp strategy
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 04:11:16 +02:00
Martin Vogel 0bab94ce1c test(python): a def under a branch is bound by the lexical step, not the LSP
At a compound statement's join the Python LSP invalidates what the branch bound, so the local def under if True: is resolved by the registry's lexical_scope step; pin that producer instead of an lsp_ strategy. Found at the branch's first build.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 04:08:24 +02:00
Martin Vogel 9b5139d73d fix(python): bound the function-local walks at the symbol's own file
A package __init__.py can define a function whose QN equals a submodule's
(poetry: layouts/__init__.py `def layout` is the QN prefix of every symbol in
layouts/layout.py). The prefix walks that recognise a def declared inside a
function body accepted that other file's function as the enclosing def, so
class Layout and its methods read as locals of layout(): they left the
short-name index and the import symbol fallback, and the answer depended on
which file registered first (discovery order on a full index, node-id order on
an incremental one).

Every walk now stops at the symbol's own module:
- registration (pass_definitions.c, pass_parallel.c, and the incremental seed)
  passes the defining file's symbol prefix length,
  cbm_pipeline_fqn_symbol_prefix_len, computed without allocating, to
  cbm_registry_add_lang_in_module; cbm_registry_add_lang keeps its signature and
  does no local detection;
- cbm_registry_resolve_lexical stops at the caller's module QN;
- the import fallback's local test only counts an enclosing def in the same file.

Also moves the #1548 registry tests out of the test they had been inserted into.

Tests: registry_python_local_bound_at_own_file_issue1548 (both registration
orders), fqn_symbol_prefix_len_matches_fqn_compute,
pipeline_python_init_function_shares_submodule_qn{,_parallel}_issue1548 (src/
layout: the import and calls onto Layout survive, then again after an
incremental re-index).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 03:41:31 +02:00
Martin Vogel e106c415cf fix(python): from . import x names the importing package, not its parent
A package-relative from-import (`from . import x`, `from .. import x`,
`from . import x as y`) has a module made only of dots. The extractor joined
it to the imported name with another '.', recording "..x" for `from . import x`:
the parent package. The import resolver then missed the real binding and fell
to the symbol-name guess (the lexicographically first same-named definition in
the project), so the import map, and the Python LSP binding that consumes it,
sent every call to an unrelated function at import_map / lsp_callable_alias
confidence. On home-assistant, 7,175 caller/callee pairs bound to another
integration's function (webhook's `async_register` to accuweather's).

Join without the extra separator when the module already ends in '.'.
`from .m import f` (".m.f") and absolute imports are unchanged.

Tests: python_package_relative_import_paths (extraction: the recorded paths);
pipeline_python_package_relative_imports{,_parallel} (all three forms resolve
to the own / parent package, never to the namesake that sorts first, without a
name guess, on both resolvers); pipeline_python_from_import_bare_call_issue1237
{,_parallel} (#1237 guard: a plain from-import and bare calls bind the imported
def and keep it across a re-index that adds a file).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 03:18:15 +02:00
Martin Vogel f75af00a8b fix(rust-lsp): a generic impl's self type drops its type arguments
The extractor fix above names a generic impl's methods `Gen.in_generic`
on both the def and the call-scope side. The Rust LSP still built its
self type from the impl's raw text, `Gen<T>`. That broke three things:

- its caller QN (`Gen<T>.in_generic`) no longer joined the pipeline's
  call scope, so every LSP row inside a generic or lifetime impl was
  dropped for a registry guess. On 9 Rust repos that turned 1,927
  lsp_* edges into suffix_match/unique_name;
- `self.` and `Self::` dispatch never found the impl's own methods;
  `Self::assoc_g()` had no edge at all, on main as well;
- the return-type harvest never matched those methods, so chained
  calls on them stayed untyped.

Strip the generic arguments from the impl's self type in both places.
Text that starts with `<` (a qualified path) is left alone.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 03:03:38 +02:00
Martin Vogel 6b1cd8aa0d style(python): clang-format the nested-def binding and lexical step
Whitespace only; the token stream is unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 03:02:50 +02:00
Martin Vogel ec4f0eaae6 fix(rust-lsp): a type-qualified call never takes the free-function fallback
`Opts::write(true)` is an associated function. When the Rust LSP cannot
place it, the registry can (import_map or qualified_suffix onto
fsx.Opts.write). The two free-function short-name fallbacks (the
cross-crate member lookup and the project-wide lsp_short_name_unique)
ran first, though. They bound the call to the project's only free
`write`, and that LSP row pre-empted the right answer.

While the Rust registry also held other languages' definitions, a
same-named JS or Perl function often made the free name ambiguous and
hid this. With Rust-only registries the hijack replaced 516
qualified_suffix and 84 import_map edges on 7 Rust repos.

A call whose qualifying segment names a type now skips both fallbacks.
That covers an ASCII-uppercase initial, `Self`, and a qualified
`<T as Tr>::f` path.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 02:59:00 +02:00
Martin Vogel cc79e1d63f style(pipeline): clang-format the dotted-base resolution
Whitespace only; the token stream is unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 02:51:25 +02:00
Martin Vogel 2fbbecc550 Merge remote-tracking branch 'origin/main' into fix/rust-generic-impl-call-scope
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 02:49:30 +02:00
Martin Vogel 42769c2d6d Merge remote-tracking branch 'origin/main' into fix/rust-registry-rust-defs-only
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 02:49:30 +02:00
Martin Vogel 354ccecbe6 Merge pull request #2431 from kaNEGY/test/ts-lsp-allocation-failures
test(ts-lsp): fail when fixture allocation fails
2026-10-18 02:32:00 +02:00
Martin Vogel 0533beb7c6 Merge pull request #2678 from DeusData/fix/seq-link-methods-after-registration
fix(pipeline): link methods to their type after every file is registered
2026-10-18 02:31:56 +02:00
Martin Vogel bb650ca45a Merge branch 'main' into test/ts-lsp-allocation-failures 2026-10-18 00:17:58 +02:00
Martin Vogel d40f8e6be1 test(pipeline): generic Rust impl methods source their own calls (both resolvers)
Sequential and parallel pipeline twin of extract_rust_generic_impl_call_scope_matches_def: the calls in a type-generic and a lifetime-generic impl method are sourced from the method, not from the File node.

Refs #763

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 00:06:42 +02:00
Martin Vogel 09c4885e11 fix(extract): a Rust generic impl's call scope drops its type arguments
The def side names a Rust impl method after the implementing type with
its generic arguments stripped (`impl<T> Gen<T>` -> `Gen`, extract_rust_impl).
The call-scope QN in extract_unified.c used the impl's type text verbatim,
`Gen<T>` / `Life<'a>`. So every method of a generic or lifetime-parameterised
impl had a scope QN that no node carries, and every call in its body was
sourced from the File node instead of the method. Base census, 9f85f104 par
(lane L7's #763 measurement): 87,855 Rust CALLS start at a File/Module node,
84,331 of them (96%) on a line inside a function span. Go is 0%.

Strip the generic arguments on the call side exactly as the def side does.
Plain impls, trait impls, free functions and inline modules were already
consistent (checked on a fixture with the base binary).

Test: extract_rust_generic_impl_call_scope_matches_def (plain, type-generic
and lifetime-generic impls; def QN == call scope).

Refs #763

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-18 00:05:47 +02:00
Martin Vogel 9dba7fe1af Merge pull request #2676 from DeusData/feat/smart-ci-live
ci(test-impact): make PR test selection narrow for real
2026-10-17 23:56:49 +02:00
Martin Vogel db8d547ae1 fix(extract): keep both Go method QN sides flat when the type QN is NULL
cbm_fqn_compute_source_lang returns NULL only when the module QN cannot be
built (arena exhaustion). The receiver-qualified branches then passed that
NULL to "%s". Both sides now fall back to the flat QN together, so the def
and its calls still agree. Allocation-failure path only; no change on any
normal run. (Raised in our security review of the updated head.)

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 23:37:31 +02:00
Martin Vogel bfe53f344b style: clang-format the new lines
Formatting only, with the repo's clang-format (Homebrew LLVM).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 23:34:24 +02:00
Martin Vogel 2ac9c33e18 fix(rust-lsp): a Rust call never binds another language's definition
Both Rust registries were built from the pipeline's mixed-language def
set. The shared Tier-2 build (cbm_rust_build_cross_registry) and the
per-file conversion (pxc_lspdefs_to_rust) copied every def, so a Perl
`sub new` or any C function became a Rust free function. The
lsp_short_name_unique fallback then bound `Store::new()` to the Perl
constructor at confidence 0.90 on both venues: its "crate scope" check
compares only the first module-QN segment, which is the project name.
(Found by the generic-engine lane on a 12-language fixture.)

Register Rust defs only, at both conversion points, exactly as the Go
builder filters to Go. Rust impl-relation records already carry
CBM_LANG_RUST. As a side effect, the shared Rust build no longer
registers the C defs of a mixed C+Rust tree.

Tests: pipeline_rust_call_never_binds_another_language (sequential and
parallel): no CALLS from the Rust caller into the Perl file, and the
Rust associated function still binds.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 23:22:56 +02:00
Pcristin 399b7de80f Merge branch 'main' into fix/windows-local-transition-cleanup 2026-10-18 00:18:38 +03:00
Martin Vogel d2ea417ec2 fix(pipeline): link methods to their type after every file is registered
The sequential pipeline (50 files or fewer, one worker, CBM_WORKERS=1)
linked a method's DEFINES_METHOD edge while it was still registering
files. A method defined in a file the directory walk returns before its
type's file -- a Go method in another file of its package, an out-of-line
C++ method in foo.cpp before foo.h -- found no parent and lost the edge,
and with it the IMPLEMENTS and OVERRIDE edges derived from it. The
parallel registry build links after every node exists, so the same input
gave different graphs depending on repository size and worker count, and
on the order the filesystem enumerates a directory.

With the extraction cache, DEFINES_METHOD and diagram parent edges are now
linked in one pass after Phase 1, as the parallel registry build does.
Without the cache (allocation failure) each file still links its own.

The test pair puts each of two Go types in the file of the other's
method, so one method precedes its type whatever the walk order: RED on
main for the sequential venue (alpha_method == 0), and the venue-parity
case compares the padded input through both resolvers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 23:05:56 +02:00
Martin Vogel 51a3e4e7b5 fix(python): resolve a base class spelled through a root-binding import
`import pkg.mod.nodes` binds the ROOT name `pkg`, so `class X(pkg.mod.nodes.Context)`
names module pkg.mod.nodes's Context. The cross-LSP base resolution asked
the registry, whose import_map step joined the imported MODULE (the
IMPORTS edge target, pkg.mod.nodes) with the whole remaining spelling
(mod.nodes.Context) and missed; the base kept its raw spelling, so the
Python LSP's inheritance view never saw it. Every member inherited through
a dotted base fell to a name guess or to nothing: self.m() became
unique_name, super().m() suffix_match, super().__init__() only resolved
through a synthesized QN.

pxc_resolve_base_qn now resolves such a spelling against the module the
import names: the import map's module when the root local is unique there,
else the dotted module path under the project's QN rule. Python only,
import-backed and exact (no short-name step), and only a type-like project
node is accepted. `import a.b as m` and `from a import b` bind the module
itself and keep resolving through the import map.

Tests: python_dotted_base_inheritance(_parallel): self.m(), super().m()
and super().__init__() through a dotted base resolve by the LSP; the
from-imported control keeps resolving; an untyped receiver stays without
an LSP edge.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 22:52:58 +02:00
Martin Vogel 9cdae25c12 fix(python): bind a function's nested defs in the LSP; lexical_scope after it
Re-port onto current main (agreed with lane L7):
- The Python LSP binds every def/class a function body defines as a local
  before walking the body (LEGB): an undecorated def as an exact callable
  alias to its nested QN, anything else as a hard shadow, so no call falls
  through to a same-named module symbol. Defs inside the body's compound
  statements count too, and their bodies are now walked.
- Classes declared in a function are walked under the function's QN
  (py_process_class_in), so super()/self calls in their methods resolve.
- cbm_registry_resolve_lexical moves AFTER the LSP override: both resolvers
  call one entry point, cbm_registry_resolve_call_scoped, at the same place
  (before the language-aware chain). It never uses the per-file resolve cache.
- Main's cbm_registry_add_lang carries the def-local exclusion, so every
  registration site (full, parallel, incremental seed) applies it; the
  separate cbm_registry_add_for_language and the second incremental language
  derivation are dropped.
- The pass_pkgmap.c def-local import skip is its own continue.

Tests: local-vs-module shadowing in both orders, a decorated local, and a
function-local class's super()/self calls, sequential and parallel.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 22:52:43 +02:00
Martin Vogel 87cafb9ef8 fix(extract): bump the index format for receiver-qualified Go method QNs
Review follow-up for #1913 (asks from the 2026-09-28 review), plus one
finding from the review of the updated head:

- store.h: CBM_INDEX_FORMAT_VERSION 3 -> 4. Every Go method QN changes
  (pkg.method -> pkg.Recv.method), so an index built before this change
  and refreshed incrementally would mix flat and receiver-qualified
  method nodes, with the old fused nodes keeping their stale edges. The
  bump routes such an index through one full rebuild.
- pipeline_go_method_caller_keeps_lsp_join: the by-name assertion would
  also pass for a binding onto the decoy other.Decoy.helper. The test now
  pins the edge by QN suffix on both ends (.svc.Repo.Install ->
  .svc.Repo.helper) with an lsp_* strategy, and asserts no edge to the
  decoy.
- extract_defs.c: the receiver-qualified def QN is Go-only. The templ
  grammar is Go-derived and reaches the same receiver branch, but its
  call-scope QN (compute_func_qn) stays flat, so a templ method would get
  a def QN no call names and its calls would degrade to the File node.
  extract_templ_method_qn_matches_call_scope pins def QN == call scope.
- pass_semantic.c: the check_go_class_implements comment no longer
  describes Go method QNs as flat.

Refs #1909

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 22:40:55 +02:00
Martin Vogel 9aeea8eb6f Merge branch 'fix/go-weak-selector-calls' (with main) into fix/go-method-receiver-qn
Brings the branch up to date with the updated #1907 head, which carries
main 9f85f104 and the #1907 review follow-up. One conflict in
tests/test_pipeline.c, at the shared anchor: keep this branch's
pipeline_go_method_caller_keeps_lsp_join and the shared
write_go_receiver_fixture plus its sequential and parallel probes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 22:36:07 +02:00
Martin Vogel f1272d8b14 fix(routes): extract ASP.NET Core attribute routes (#1692, #1041)
A controller routed with ASP.NET Core attributes produced no Route node
and no HANDLES edge:

  [Route("api/[controller]")] class QuotesController
    [HttpGet("{symbol}")]                        -> nothing
    [HttpPost] + [Route("orders/{id}/confirm")]  -> nothing

Root cause: the route extractor only knew decorator calls (Flask, FastAPI,
Express-style) and Spring/JAX-RS annotation names. HttpGet/HttpPost/.../
Route matched none of them, the class-prefix join ran only for Java and
Kotlin, and ASP.NET templates are relative ("{symbol}", "api/[controller]"),
while the path filter and ensure_one_decorator_route accept only
'/'-prefixed paths. #1759 made every stacked C# attribute visible as
DECORATES but did not turn any of them into a route.

Fix: a C# path in push_method_def, next to the JVM prefix join, that
reads the attributes of the action and of its class:
- HttpGet/Post/Put/Delete/Patch/Head/Options with or without a template,
  [Route] on the class and on the action, [AcceptVerbs("GET", Route = "x")];
  qualified names and the "Attribute" suffix are accepted.
- [controller] is the class name minus "Controller", [action] the method
  name minus "Async" (the ASP.NET Core default) or its [ActionName], and
  [area] comes from [Area].
- A template is relative to the class prefix; "/" or "~/" overrides it.
- Constraints, defaults and optional markers reduce to the bare parameter
  ("{id:int}", "{code:regex(^\d{{3}}$)}", "{page=1}" -> "{id}", "{code}",
  "{page}"), so the path is '/'-led and canonicalizes like every other
  decorator route. This works with both the current cbm_route_canon_path
  and the depth-matched variant.
An action without a verb or [Route] attribute stays route-less, and other
C# attributes never become routes.

Known limits, unchanged in kind from the JVM path: one route per action
(a stacked [HttpGet] + [HttpHead] records the first), and a [Route]
inherited from a base controller in another file is not applied.

Proof on jellyfin/jellyfin (shallow clone, depth 1, 208c278): Route
nodes 0 -> 360, HANDLES 0 -> 388, e.g.
GET /Items/{itemId}/Images/{imageType} <- ImageController.GetItemImage.

Tests (edge_types_probe, exact Route-name sets): the #1692 controller,
the #1041 AuthController, [controller]/[action]/[ActionName]/[AcceptVerbs]
tokens, "/" and "~/" overrides with constraints plus a controller without
a class [Route], and a non-controller C# control.

Refs #1692
Refs #1041

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 22:35:05 +02:00
Martin Vogel 7528ec06fe test(pipeline): bind the Go selector guard on the parallel resolver
Review follow-up for #1907 (asks from the 2026-09-28 review):

- pipeline_go_receiver_parallel_suppresses_weak_method_edge: parallel
  twin of pipeline_go_receiver_suppresses_weak_method_edge. The guard is
  wired into pass_parallel.c as well as pass_calls.c, but the four-file
  fixture only runs the sequential resolver, so the parallel hunk was
  bound by no test. The fixture is shared (write_go_receiver_fixture) and
  padded with 52 Go files past MIN_FILES_FOR_PARALLEL, with
  CBM_WORKERS=4, in the shape of the Python parallel twin.
- registry.c: cbm_go_suppress_weak_method_match builds on the shared
  weak_short_name_strategy list and narrows it in two places only
  (field_type_hint kept, unique_name only when penalized). Behaviour is
  unchanged; weak_call_guards_share_one_drop_list now binds the Go guard
  to the shared list too.
- test_extraction.c: the flag-exempt comment names the current flag set
  (Perl, Python, TS/JS/TSX and Go) and Rust as the exempt language.

Refs #1906

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 22:33:37 +02:00
Martin Vogel 6f3fdba92a fix(python): extract defs nested in a function body (#1548)
Why
The Python def walk stopped at every function_definition: it extracted
the def and never looked at its body. Every def or class declared inside
a function -- closures, decorator wrappers, the route handlers of a Flask
or FastAPI app factory (`@app.route` inside create_app), local classes and
their methods -- was absent from the graph. Blocks at module level (if /
try / for / with) were transparent, so only function bodies were blind.
The calls inside those defs were not re-attributed either: the call-scope
walk named a nested def by its class or module only (`pkg.mod.nested`, a
QN no node had), so the edges fell back to the file node, and inbound
traces of the called function silently lost the caller. A def nested in a
method got the method's own QN (`Tree.visit` for the `def visit` inside
method Tree.visit), so its calls, recursion included, landed on the
method. On an app-factory codebase this removes the whole HTTP layer.

Fix
- Extraction (extract_defs.c): after a Python def is extracted, its body
  is walked with the def's QN as the enclosing scope; methods' bodies are
  walked under the method QN. A nested def or class is qualified by the
  def it sits in -- pkg.mod.create_app.index, pkg.mod.Tree.visit.visit,
  pkg.mod.create_app.Settings.value -- the QN the Python LSP already gives
  the calls inside it (py_process_function). Decorated nested defs keep
  their decorators, so their route_path mints the Route and HANDLES.
  Lambdas and comprehensions hold no statements and stay as they are.
- Call scope (extract_unified.c): for Python the nearest named scope, a
  def or a class, prefixes a nested def or class, so every call, usage and
  env read inside a nested def carries the nested def's QN.
- A def nested in a function is a local of it (Python LEGB), so it must
  not become a global name: a Python symbol whose QN extends a
  Function/Method node is registered for exact lookups only, never in the
  short-name index (registry.c cbm_registry_add_for_language, used by the
  full, parallel and incremental registration sites alike). Without this,
  django's per-test `class MyModelAdmin` and every decorator's `def
  wrapper` became unique_name / suffix_match targets for unrelated calls.
  The incremental seed derives each stored file's language the way
  discovery does (name, then shebang), so it keeps the same symbols out.
  The import symbol-name fallback skips such locals too (pass_pkgmap.c),
  and the Python LSP no longer binds a def-local class module-wide by its
  short name (py_lsp.c).
- A bare call now resolves lexically first (registry.c
  cbm_registry_resolve_lexical, strategy lexical_scope): the def or class
  bound in the nearest enclosing function, skipping class bodies, stopping
  at the module. The sequential and parallel call passes use it
  identically, and so does the handler lookup of a call-registered route:
  `app.add_url_rule("/ping", "ping", ping)` in create_app reaches the
  local ping.

Proof (real code; every node and edge diffed, main 1f9b4db1 vs this)
- pallets/flask examples/tutorial (flaskr, create_app factory): the
  nested `hello` handler is now create_app.hello with HANDLES to
  /hello; 4 nested defs/classes added, 0 nodes and 0 edges removed.
- pallets/flask d73fa1c (tests declare handlers inside test functions):
  Function 532 -> 1068, Method 318 -> 381, Class 130 -> 209. The +678
  equals the 678 unique nested def/class QNs Python's own ast finds.
  Handler HANDLES (decorators and add_url_rule) 32 -> 296. 0 nodes
  removed, 0 duplicate QNs.
  The 94 removed CALLS: 90 moved from the file node onto the nested def
  that makes the call, 4 replaced by the lexically bound local.
- django fb11376 (local, 2,927 .py files): nodes 55,483 -> 60,052,
  Function 1,992 -> 3,027, Method 28,850 -> 29,491, Class 8,187 ->
  11,073. 0 nodes removed and 0 duplicate QNs. Every added
  Function/Method/Class sits under a Function/Method of the same file,
  and they match ast's 4,567 nested QNs except for 5 methods declared in
  an `if` inside a local class body (class-body `if` is not walked for
  any class). CALLS 62,430 -> 63,815. Every added CALLS edge has a nested
  def at one end: 1,015 from nested defs, 1,219 lexical_scope into a
  local, and none from outside the local's function. Of the 849 removed:
  507 moved from the file node onto the nested def, 334 replaced by the
  local the name binds, and 8 were false on main, all checked (e.g. the
  recursive `get_actions` nested in call_command had been suffix-matched
  to ModelAdmin.get_actions). HANDLES unchanged (135).
- Samples checked against the source:
  AbstractBaseUser.check_password.setter -> set_password / save;
  View.as_view.view -> setup / dispatch;
  Apps.lazy_model_operation -> lazy_model_operation.apply_next_model
  (line 426);
  InstrumentedASGIRequest.META -> the local InstrumentedDict (line 279);
  flask test_provide_automatic_options_arg_disable.index -> GET /.

Tests
- extraction: extract_python_nested_def_qualified_by_enclosing_def,
  _calls_attributed_to_it, _decorated_nested_route_handler,
  _nested_class, _nested_def_named_like_its_method,
  _lambda_and_comprehension_stay_in_enclosing_def (control),
  _nested_def_env_access_attributed_to_it (all _issue1548).
- pipeline: pipeline_python_app_factory_routes, _nested_def_is_function_
  local, _local_classes_stay_in_their_def, _import_never_binds_a_local_def,
  _nested_defs_parallel (>= 50 files, CBM_WORKERS=4),
  _nested_def_local_after_incremental (all _issue1548).
- registry: registry_python_function_local_is_exact_only,
  registry_resolve_lexical_walks_enclosing_defs (both _issue1548).
The extraction and pipeline tests were RED on main 3/3 for the asserted
reason and are green with the fix; the registry tests cover the new
registry API. Each production piece was reverted on its own, and each
revert turns at least one of these tests RED.

Fixes #1548
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
(cherry picked from commit 62fda43fd66200879db62a58903a32dab0dd1a20)
2026-10-17 22:33:37 +02:00
Martin Vogel 285d21c1f1 Merge branch 'main' into fix/go-weak-selector-calls
Brings the branch up to date with main (989 commits since the
2026-09-28 merge). Four conflicts, all additive on both sides:

- pipeline.h / registry.c: main added cbm_suppress_weak_import_bound_call
  (#2127) at the same anchor as cbm_go_suppress_weak_method_match; both
  kept.
- pass_calls.c / pass_parallel.c: drop_plain_call now chains the Go
  selector guard and main's Python import-binding guard, in the same
  order in both resolvers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 22:18:04 +02:00
Martin Vogel 48d33fadf6 test(ci): hold README pushes to the test legs in the selector history replay
README.md, the site files and pkg/npm/README.md are read by test_cli, so
a change to one now runs the test legs (T1-tests). The September replay
held 15 README pushes to the docs tier the analysis assigned and failed
every test leg's contract step. Those rows are now a documented
deviation, read-by-tests, which like makefile-by-name may only raise a
tier: 767 pushes as expected, 53 documented fail-safe deviations, all 22
real failures still caught.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 21:53:13 +02:00
Martin Vogel d21949ef2c feat(deploy): render Helm images, mark default-valued images, link crds and werf charts
Four gaps of the deployment graph, closed together.

Images named from a default value keep their Image; the edge says so.
A Dockerfile FROM / COPY --from filled by an ARG default (an ARG chain
names each ARG) carries default_of, as do compose `${X:-d}` images and
`docker build -t ${IMAGE:-x}` tags. An override found in the repository
adds its own edge: compose build.args and --build-arg make the Image the
build names DEPEND_ON the base they choose {override_of}; compose's .env
replacing an inline default marks the effective edge override_of and keeps
the default as {ref: image_default}, an image the service does not run.

Helm template images render (helm_template.c): a bounded Go
text/template + Sprig evaluator over the vendored gotmpl grammar, with
values read the way Helm reads them (go-yaml v2 scalars, float64 numbers,
Helm's coalesce). Each templated container image renders from its chart
family's defaults -- include / define helpers, with / range / if, subchart
values under their alias, global -- and the object USES it {default_of:
the values keys read}; the chart DEPLOYS it. A value that does not render
(a .Release field, lookup, a missing define, a field of a missing map, a
function not modelled) names no Image; a rendered value is checked against
the image reference grammar first. Releases render their charts with their
values: a Flux HelmRelease's spec.values, a kustomize helmCharts entry's
valuesFile + valuesInline (a chart in chartHome now links locally), a
Terraform helm_release's values + set (literals only; set_sensitive is
never read) -- each image that differs is the release's own DEPLOYS
{override_of}. A release of a chart outside the repository names images by
convention only where its values' `repository` names a registry host
{inferred: values}; a host-less one names none.

A chart DEPLOYS the CRDs of its crds/ {phase: crds}. A werf `.helm` dir
beside werf.yaml is a chart named after the werf project.

Incremental runs: a werf.yaml edit and a werf chart's values / templates
decline like a Chart.yaml chart's; a release's values file (a kustomize
valuesFile, a Terraform file()) declines too -- Terraform's now IMPORTS
{local_name: values} it, like kustomize's valuesFile.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 21:14:39 +02:00
Martin Vogel 57b557ced8 fix(test-impact): close the role table's fail-opens, follow literals through macros
The first commit of this branch is the Smart CI change as of the rebase
onto 9f85f104. A review of it found places where the engine could run
fewer tests than a change needs; each now runs everything or selects the
right tests, with a test that fails when its fix is reverted.

Engine:
- A `#define NAME "literal"` line counted as naming a role but selected
  nobody, so the tests that start the blocking-git probe through their
  marker macros would not have run. The scan now follows NAME, as a whole
  word, to the code that uses it, through chains of up to 4 macros; a
  define it cannot follow runs everything.
- main dispatches through the role table, so no edge leads from main to a
  handler. A table entry the engine cannot read, or whose handler is not
  exactly one function, was skipped; it now runs everything.
- In a runner file, only a test case names a role's literal. The runner's
  own helper that reads the marker no longer counts as a spawner, so a
  role only the runner names runs everything again.
- A handler reached first by a parse gap or by the spawn lift was never
  lifted. Role lifts and parse gaps now run to a fixpoint, before the
  runner check and again after the spawn lift.
- A reached sink that main calls (the blocking-git probe) counted as a way
  into main and hid the real one: #2644 now names the start-up path main
  -> cbm_index_supervisor_capture_build_fingerprint.
- No pointer past the end of a role line; the RUNNER_REACHED note clamps
  its prefix.

CI:
- pkg/npm/README.md, which test_cli reads, runs the test legs instead of
  the docs tier.
- The build rule lists the harness scripts the test legs run:
  run-test-wave.py, path-safety.sh, clean.sh.
- The revert guard rejects the ancestor's coverage when a diff fails.
- The vetoed selection is uploaded (test-selection-vetoed).

Docs: SMART_CI.md states the new fail-safe cases, the version that lacks
the new config keys, the selection base, and that the narrowed leg builds
with gcc while the coverage comes from clang.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 20:44:27 +02:00
Martin Vogel 7f6f567840 wip: Smart CI narrows PR CI for real (pre-rebase checkpoint)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 14:48:52 +02:00
Martin Vogel 9f85f1043a Break-glass merge PR #2674 2026-10-17 14:40:44 +02:00
Martin Vogel b1225e57ad chore(pkg): sync packaging metadata to v0.12.1
Every release surface of the version-metadata contract states the
newest tag: the Chocolatey nuspec and installer (checksum re-pinned to
the released windows-amd64 archive), the Scoop manifest (both Windows
archive hashes), the Homebrew formula (all four archive hashes; Linux
installs the static -portable builds), the Go installer, the Nix flake,
server.json, and the npm and PyPI wrappers. Every hash is taken from the
release's checksums.txt.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 14:38:59 +02:00
Martin Vogel a7158ac2f4 Break-glass merge PR #2669 v0.12.1 pkg/go/v0.12.1 2026-10-16 23:38:08 +02:00
Martin Vogel 6382fad7b3 fix(ci): give the wrapper smoke a Go toolchain and a deletable work tree
The v0.12.1 release smoke failed on every leg in Phase 13w, the
package-wrapper install gate added in #2664:

- macOS: the runner images have no Go on PATH, and
  SMOKE_REQUIRE_WRAPPERS=1 rightly fails without one. pr-smoke and the
  three release smoke jobs now set up Go from pkg/go/go.mod, with the
  same pinned step the wrapper test job uses.
- Windows: `mkdir -m 700` changes the mode in a second step, which MSYS2
  cannot do under the smoke profile's owner-only DACL. mktemp -d sets
  the mode at creation.
- Linux: the runner's older Go downloaded the 1.26.1 toolchain into the
  smoke profile's HOME. Go writes its module cache read-only, so the
  harness could not delete its profile afterwards. All Go state now stays
  in the wrapper's own work tree, and its cleanup restores write
  permission before removing it.

npm and PyPI passed on every leg that reached them; the gate itself is
unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 22:12:26 +02:00
Martin Vogel e7ea0aff8e Break-glass merge PR #2663 2026-10-16 18:16:50 +02:00