Recover parenthesized object declarations only when their apparent parameters resolve to earlier visible value bindings. Respect recovered scope, alias hiding and prototype shadowing instead of manufacturing receiver types.
Preserve heuristic confidence and source provenance. Add positive and negative regressions plus a three-repeat labeled initialization fixture to the quality gate.
Validation: cargo fmt --check and ./scripts/quality-gate.sh passed.
Problem: a bare call in a Dart file resolved against every imported
library. An `import 'a.dart' as p;` still bound `foo()` to a.dart's foo,
`hide` and `show` combinators were ignored, and a name re-exported through
a barrel library (`export 'impl.dart';`) never resolved.
Fix: record each Dart import/export directive with its `as` prefix and
show/hide lists. A bare name resolves only in libraries imported without a
prefix whose combinators let it through, plus that library's own exports
that also let it through (one level).
Test: tests/dart_imports.rs covers a prefixed, a hiding, a showing and a
barrelled import; it failed on the prefixed case before this change.
When several files declare the same type leaf name, rust_method keeps
only the impls of the type a call site means, asking each impl's file
which `Foo` it means. An impl in a third file written as
`impl crate:🅰️:Foo` resolved nothing (that file has no `Foo`
declaration or `use` binding), so its methods were dropped and
`x.run()` on an `a::Foo` read "member-not-found". It resolved before #5.
The extractor now records the impl's type path when it is written as
one (`impl_type_path`, generic arguments removed), the method index
keeps it, and the impl filter resolves that path to its declaring file
before falling back to the file's own view of the leaf name. Glob
imports (`use super::*`) are still not expanded; such an impl stays
unresolved rather than guessed.
Test: with `Foo` declared in a.rs and c.rs and `impl crate:🅰️:Foo` in
ext.rs, `x.run()` on `crate:🅰️:Foo` resolves to ext.rs's `run`; it
failed before this change.
Since #5 Python calls resolve only through import edges, but the
lexical importer could not produce those edges for the common forms:
- `from .util import helper` never matched (the regex needed a leading
letter), so the call went unresolved;
- `from pkg import f` looked up the stem "pkg" instead of
pkg/__init__.py;
- `from x.common import f` failed when y/common.py also existed, and
`from other.common import f` (a module the repository does not have)
resolved to y/common.py by basename.
Imports now resolve Python-specifically: leading dots are relative to
the importing package, and a dotted name maps to `a/b.py` or
`a/b/__init__.py` from the repository root or `src/`. Only a
single-component name (`import foo`) may still fall back to a unique
module basename; a dotted name never does. `from . import x` stays
unresolved (the module is in the import list, not the name).
Tests (both failed before): relative, package and duplicate-stem imports
resolve their calls to pkg/util.py, pkg/__init__.py and x/common.py;
an unknown dotted module does not bind to y/common.py.
Swift ran through the C-style lexical patterns, which never recognize
`func`. On a small file (`func realTarget()`, `func ghost()`, a class
method `caller` that calls `realTarget()` below a `/* ghost() */`
comment) the preview reported:
a.swift -> ghost (a.swift:2 declaration, a.swift:5 in a comment)
a.swift -> caller (its declaration line)
a.swift -> realTarget (its declaration line)
and missed the real call. Now:
caller -> realTarget (a.swift:7)
- Swift gets its own patterns: `func` declarations (with attributes and
modifiers), class/struct/enum/protocol/actor/extension declarations
(`class func` / `class var` are members, not classes), and imports.
- Block comments are masked across lines, nested as Swift allows, and a
`/*` inside a string literal does not open one. The masker is salvaged
from the unmerged language-parity branch (543bafc).
- Calls are attributed to their enclosing function through the existing
brace-scoped scopes, now that functions are recognized.
Test: tests/swift_preview.rs (functions and a class recognized, the call
attributed to `caller`, nothing from comments, nothing from the file
node); it failed before this change.
`check` treated any difference between stored and recomputed metrics as
a gate failure, so a model written by an older brick (without the
`metrics.resolution` field, or with an older formula) always failed,
even with every threshold met. CI checking an archived or cached model
broke on upgrade.
The gate already uses recomputed metrics, so a mismatch is now a
stderr warning. Only fields present in both copies are compared, so a
missing newer field does not count as a mismatch. Tampered metrics
still cannot pass: the existing test's recomputed cycle count fails the
gate, and it still reports "stored metrics".
Test: a freshly built model with `metrics.resolution` removed passes
`check --min-score 0`; it failed before this change.
`.c` files were parsed with tree-sitter-cpp, so valid C that uses C++
keywords as identifiers (`int delete = 0; int new = 1;`, a parameter
named `class`) produced grammar-error diagnostics and lost symbols.
`.c` files now parse with tree-sitter-c (pinned =0.24.2, backend
`c:0.24.2`) while staying on the shared C/C++ extraction path, so C
sources and the headers they include still share one include/declaration
index. Headers (`.h`) keep the C++ grammar, which parses the C subset.
Test: cpp_xtu::c_sources_parse_with_the_c_grammar fails before this
change (grammar errors in util.c).
A single invalid byte, such as a Latin-1 `é` in a comment, made
`read_to_string` fail and the whole file and its symbols were skipped.
Files are now read as bytes: a NUL byte still marks a binary file,
which is skipped with a diagnostic; otherwise invalid UTF-8 is decoded
lossily (U+FFFD) and a warning diagnostic records it, so the file keeps
its symbols and calls.
Test: languages::a_non_utf8_byte_does_not_drop_the_whole_file fails
before this change (latin1.rs has no symbols).
A Dart import exposes a library's public top-level names without a
per-name binding, so the binding-based lookup never matched and no
cross-file Dart call resolved, even when the import itself did (pipe's
VerifyCommand::run lost loadProject, analyzeProject, verifyAnalysis and
decodeBaselineEntries).
After the lexical-owner walk and binding lookup fail, a Dart call now
resolves to the one public (not `_`-prefixed) top-level declaration of
that name in the libraries the file imports; several such declarations
make it `ambiguous`, none leaves it external. `package:<name>/x.dart`
imports resolve to `lib/x.dart` when `<name>` is the root pubspec's
package; other packages stay external. `show`/`hide` combinators are not
modelled (noted as a follow-up).
Test: languages::dart_calls_resolve_through_imported_libraries fails
before this change (helper stays not-visible); an unimported file and a
library-private name stay unresolved.
Three lexical-preview bugs, reproduced on one probe:
- A blank or comment-only line in a Python function fell through to the
brace-depth scope rule, which popped every open scope, so every call
after the first blank line in a body was attributed to the file.
Blank/comment lines now leave Python scopes unchanged.
- `class Child(Base):` matched the call pattern and produced a call edge
from the file to Child. The declared class name is no longer a call
on its own header line.
- Calls resolved to the only function with that name anywhere in the
repository, imported or not (a NameError at runtime became an edge).
Resolution now prefers the caller's file; Python then considers only
files the caller imports (resolved import edges, cached per file);
otherwise the call stays external. Swift keeps the module-wide unique
name after the same-file check, because files of one Swift module see
each other without imports. Imported bases resolve the same way, so
`from pkg.b import Base` disambiguates `class Child(Base)`.
Test: languages::python_calls_keep_their_function_and_resolve_only_what_is_visible
fails before this change (go() had no call edges at all).
A directory with no supported source (Go/Java only) built
"1 nodes / 0 edges; score 100.0" with no diagnostic, and
`check --min-score 90 --max-cycles 0` passed it. The one-line summary
also hid diagnostics entirely: the yams run's 7,428 warnings (505 files
with grammar errors) never appeared.
- scan records a warning diagnostic when no source file was scanned;
- `build` prints the diagnostic and warning counts in its summary and
warns on stderr when the model is empty;
- `check` fails a model that contains no scanned source files.
Test: diff_cli::a_scan_with_no_supported_source_is_reported_and_fails_check
fails before this change (no diagnostic; check passes).
`brick check` read `metrics` from the model JSON and gated on them, so
setting `cycle_count` to 0 and `score` to 100 in the file turned a
failing `--max-cycles 0` gate into "passed" without changing the graph.
check now recomputes the metrics from the loaded nodes and edges and
gates on those; if the stored copy differs, that is itself a failure
("stored metrics do not match the graph"). Honest models recompute to
identical values, so existing gates are unchanged.
Test: diff_cli::check_recomputes_metrics_instead_of_trusting_the_file
fails before this change (the tampered model passes).
The base walker descended into type-argument lists, so
`class MyBloc extends Bloc<Ev, St>` emitted inherits edges to Bloc, Ev
and St; on pipe, 39 of 145 Dart inherits edges (27%) pointed at type
arguments (e.g. `Command<int>` -> `int`). C++ `: public Base<Arg>`
shared the walker through `template_argument_list`.
The walker stops at `type_arguments` / `template_argument_list` /
`type_parameters`, and handles tree-sitter-dart's shape, where the
arguments are sibling `type` fields of `superclass` (only the first
names the base) and nested `type` children of a `type`.
Test: languages::generic_type_arguments_are_not_base_classes fails
before this change (["Bloc", "Ev", "St"]).
Every preprocessor branch was indexed as live. In yams,
InternalEventBus.h guards spdlog with
`#if __has_include(<spdlog/spdlog.h>) ... #else namespace spdlog { stubs }`,
so 1,243 spdlog::warn/info/error call sites resolved to the fallback
stubs, which became the 4th and 5th highest fan-in nodes.
Brick does not evaluate conditions, so only the first branch is treated
as live: declarations and prototypes inside `#elif`/`#else` stay in the
graph (marked `source.preprocessor-branch: alternative`) but are not
definitions, declarations or classes for call resolution. Branches are
tracked textually from the directive lines, because conditions like
`__has_include(<x>)` leave tree-sitter-cpp with an error-recovered tree
that has no `preproc_else` node to find. Include-guard (`#ifndef`) code
is a first branch and still resolves.
Test: cpp_xtu::preprocessor_alternative_branches_are_not_call_targets
fails before this change (spdlog::info resolves into the #elif stub).
With `using namespace a; using namespace b;` and both `a::run` and
`b::run` declared, a call to `run` resolved to whichever directive came
first in file order and was labelled `using-directive`. In C++ that
call is ambiguous (or an overload choice Brick does not model).
Directives are now grouped: each `using N::x;` and `using namespace N;`
nominates at most one declaration (its innermost resolving scope), and
the call resolves only when exactly one distinct declaration is
nominated; otherwise it stays external as `ambiguous`.
Test: cpp_xtu::two_using_directives_naming_the_same_function_are_ambiguous
fails before this change; its single-directive case still resolves.
The Rust impl index keyed types by leaf name, so `a::Config` and
`b::Config` shared one `(Config, load)` entry and the inherent method
won: `use crate:🅱️:Config; c.load()` produced a `declared-receiver-type`
edge to `a::Config::load`.
The index now records which files declare each type name and the file
of every impl. When several files declare a name, a call resolves only
among impls of the type the calling file means (its own declaration,
then a `use` binding, then the only declaration); with no such binding
the call is left external as `ambiguous` instead of guessed. Unique
names behave as before.
Test: rust_methods::same_named_types_in_different_modules_resolve_through_use_bindings
fails before this change (edge to src/a.rs).
Agents asking "is this function used?" get resolved, unknown, or
unreferenced per function instead of a bare zero in-degree. Unknown lists
why callers may exist: same-name unresolved call sites (excluding foreign
receivers and macros), name references (callbacks, address-taken, macro
bodies), virtual/trait-impl/external-base/outside-class members, framework
attributes, implicit protocol invocations, entry points, test code,
incomplete parses, and, unless assume_closed_world is set, exported or
external-linkage functions. Unreferenced is a review candidate with an
explicit heuristic/compiler evidence basis, never a dead-code proof.
The queries are read-only over validated schema-4 models and return
complete canonical nodes and edges plus metrics.resolution coverage.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
C++ calls now resolve through the include closure: header prototypes pair
with definitions in other translation units, lookup walks class (including
in-repository bases), namespace, and `using` scopes outward, and static or
anonymous-namespace definitions stay file-local. Member calls resolve only
through explicitly declared receiver types (parameters, locals, fields,
this, smart pointers, aliases); template parameters stay unknown. Calls
hidden by the most vexing parse are recovered, casts are no longer calls,
reference-returning and conversion-operator declarations are named
correctly, and benign `= {}` default-argument recoveries no longer drop
prototypes or flag files.
Rust method calls resolve through a crate-wide impl index for self, typed
parameters and lets, struct fields, Self-returning constructors, dyn
traits, and Type::/Self:: associated calls, with foreign imports and
generics never guessed. Calls inside expression macros are re-parsed.
Every unresolved call records source.unresolved-reason, weaker resolutions
record source.resolution/source.extraction, and metrics.resolution reports
per-language call-site coverage. Declarations gain facts consumed by
caller-status queries (qualified names, linkage, virtual, trait impls,
attributes, name references, parse-incomplete). C++/Rust External targets
include the reason in their identity; out-of-line C++ methods keep their
IDs.
Linear node/edge scans and per-node ancestor walks are replaced with
indexes, cutting a YAMS scan from 575 s to 23 s.
New labeled cpp_xtu and rust_methods corpora run three times in the
quality gate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Expand module-scoped use trees into exact import paths and bindings. Resolve renamed public symbols and module aliases without global-name fallback; keep unknown and block-local bindings conservative.
Add positive and negative integration tests and an exact three-repeat import benchmark to the quality gate.
Use pinned grammar frontends and scoped heuristic resolution to recover declarations and prevent false dependencies. Constrain filesystem traversal and normalize Fold root aliases.
Add exact multilingual fixtures, seventeen regression tests, and a three-repeat benchmark to the quality gate.