- Windows absolute `.path` deps (`C:\x`, `C:/x`) return null from
`normalizeZigDepPath` like POSIX ones; a `/`-only check let them
through as repo-relative.
- `parseZigBuildZon` accepts the `.dependencies = .{` header only at
brace depth 1 (a direct field of the file's `.{`), so a same-named
field nested in an earlier struct cannot hijack the block.
- `importsExecuteWhereWritten: false` on the provider: `@import` is
compile-time name lookup (as C `#include`, Rust `use`); a body-level
`@import` is no longer marked `runsOnlyWhenCalled`.
- Namespace imports record the MODULE as `importedName`
(`zigModuleNameOf`: last path segment without `.zig`), per the shared
contract; the local handle stays `localName`.
- Keyword-less `<ident> = @import(…)` (`_ = @import("x.zig")` in a test
block) is a `side-effect` import: file edge, no binding. Only
`const`/`var` declarations bind a name or feed alias promotion.
- `extractZigFunctionName` doc: an empty name is falsy, so the enclosing-
function walk skips the test node and continues to the File; it does
not "end" there.
Not re-fixed: "DIR_LANG has no zig entry" — carried over for the fourth
pass in a row; `test/helpers/literal-collectors.ts` has had `zig` in
`DIR_LANG` (line 91) and `BASENAME_LANGS` since the second-pass commit.
Regression tests: absolute-path spellings, nested `.dependencies`
decoy, namespace/side-effect interpretation, function-scoped `@import`
not deferred (all four fail on the previous source).
Coverage gaps found by indexing idiomatic Zig against the branch:
- Const / Variable nodes: ZIG_QUERIES had no @definition.const /
@definition.variable, so `pub const VERSION`, error sets and type
aliases were absent and zigVariableConfig never ran. Rules are gated on
the literal `const` / `var` keyword — tree-sitter-zig 1.1.2 parses
statement assignments (`x = 5;`, `x += 1;`, `_ = expr;`) as keyword-
less `variable_declaration`s, and the scope query minted a phantom
local per assignment and one `_` per discard. Container and @import
bindings are skipped via `shouldSkipDefinitionCapture`.
- Imports: `const X = @import("x.zig").X` (named / alias), `const X =
ns.X` where `ns` is an @import binding of the file (promoted to a
named import), and `pub usingnamespace @import(...)` (wildcard, with
`expandsWildcardTo`). All three lost the file-level IMPORTS edge.
- Receiver typing: `var x: T = undefined` / decl literals `const x: T =
.init()` (annotation), `var c = T.init()` / `mod.T.init()` (call
return), `List(u8){}` (instantiation literal); `normalizeZigTypeName`
drops the comptime argument list.
- Generic type constructors `fn List(comptime T: type) type { return
struct {…}; }`: the returned container is a Struct/Union/Enum named
after the fn, owns its members (HAS_METHOD / HAS_PROPERTY), binds in
the module scope beside the Function def, and is emitted ahead of it
so a named import binds the type.
- `export` vs `pub`: `visibility` is now `pub`-only (Zig-module fact);
`isExported` keeps `pub|export` (visible outside the unit, as C's
external linkage). `export fn` without `pub` is not reachable from
other Zig files.
- Extractor configs share `zigContainerName`; ast-helpers' owner walk
learns the type-constructor shape.
Tests: zig-idioms fixture (10 resolver cases), extractor/interpret unit
cases for each rule.
The bare-name resolver only knew `src/<name>.zig` and `src/main.zig`.
`zig init` has written `src/root.zig` for libraries since 0.12, so the
default library layout never resolved. Now: the root the dep's own
build.zig declares (`b.addModule("<name>", .{ .root_source_file =
b.path("…") })`, name-matched module first), then src/root.zig,
src/<name>.zig, src/main.zig. `normalizeZigDepPath` is shared by the
loader and the resolver.
`gitnexus analyze` exited 1 on every Zig repository that declares a
`union` (including the zig-basic fixture itself): the member-ownership
commit made `union_declaration` a MEMBER_OWNER, so HAS_PROPERTY /
HAS_METHOD edges are emitted FROM a Union node, but `Union` was not in
LINKABLE_LABELS, so the schema's scope-bridge cross product never
generated a `FROM Union` pair and LadybugDB rejected the edge
(`labelPair: "Union|Property"`). Resolver tests stayed green because
they never write to the DB.
- `Union` joins LINKABLE_LABELS (also bridges `Tag{…}` constructor
references); the three hand-written `→ Union` target pairs move to the
generated half, per the STRUCTURAL_PAIR_DDL rule.
- structural-pair-coverage gains an optional-grammar corpus with
zig-basic (`Union|Property`, `Union|Method` sentinels), skipped when
the grammar is absent.
- Rust `union_item` note updated: the three gates it cited are widened.
Note for reviewers: the DDL fingerprint changes (#2808), so existing
indexes are rebuilt on next analyze.
The loader parses the opt-out variable lazily once per module copy, so
under `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=zig` (or `all`) — a supported way
to run — the fresh loader took the opt-out branch and the "not the
opt-out path" assertion failed before the absent-binding path ran.
Clear the variable for the fresh module and restore it in `finally`,
instead of returning early: the branch stays exercised in every
environment. Verified: fails on the previous code under `=zig`, passes
with and without the variable now.
Ports the parts of upstream PR #305 (closed, unmerged) that our Zig
provider lacked, plus two gaps found while porting.
- `export fn` / `export var` (C-ABI linkage, never `pub`) are exported;
the pub/export predicate is now shared by the export checker and the
method/variable extractors' visibility (`hasZigVisibilityKeyword`).
- `const H = opaque { … }` is a Struct-labelled container (it may own
methods, never fields) in both the structure queries and the scope
query; ZIG_CONTAINER_TYPES is the single source for the extractor
configs.
- `test "name" { … }` blocks are Function nodes named by the string
node WITH quotes, so `test "add"` beside `fn add` cannot merge onto
Function:<file>:add; `test_declaration` joins FUNCTION_NODE_TYPES and
the Zig method config names it in the enclosing-function walk, so
calls inside a test attribute to the test. Anonymous `test {}` and
decl-tests `test add {}` are scopes without a node (an empty-name hook
result stops the walk instead of falling through to the identifier of
the function under test).
- Empty container bodies (`struct {}`, `opaque {}`) no longer mint a
nameless Property: tree-sitter-zig 1.1.2 recovers them as a
container_field with a MISSING identifier; #not-eq? guards in both
queries and the field extractor drop it.
- Owner walk (`findEnclosingClassInfo`): an anonymous container bound
by the enclosing `variable_declaration` takes the binding identifier,
same shape as the Go `type_spec` branch. Before this NO Zig member
had an owner — zero HAS_METHOD / HAS_PROPERTY edges for Zig.
Not ported from #305, deliberately: `builtInNames` (a bare-name call-site
drop filter; `alloc`/`free`/`append`/`print` are the most common user
method names in Zig and `std.*` receivers are already external via the
import binding), `usingnamespace` (removed in Zig 0.15), `@cImport`,
`build.zig` ignore, and the web-app changes.
- tree-sitter-languages test: the Zig "functions, structs, enums, and
imports" case now asserts the `import.source` capture for
`const std = @import("std");` (the fixture's only import), so a query
change that drops Zig import matching fails it instead of passing
unchanged.
Not re-fixed:
- "Return type is read from the wrong tree-sitter field": carried over from
the fourth pass unchanged. tree-sitter-zig 1.1.2 has no `return_type`
field on function_declaration; the return type IS the `type` field, and
the pin test added in the fourth-pass commit
(zig-extractors.test.ts, `childForFieldName('return_type')` is null,
`returnType` = `void` / `!*Counter`) proves it.
- "`DIR_LANG` has no zig entry": carried over unchanged for the third time;
the entry exists since the second-pass commit.
- language-config: `parseZigBuildZon` only accepts a `.path` that is a
DIRECT field of a dependency entry. Nested blocks inside the entry body
are blanked (string-aware, offsets preserved) before the `.path` regex
runs, and a match starting inside a string literal is rejected, so
`.dep = .{ .url = "…", .meta = .{ .path = "x" } }` no longer becomes a
path dep. Regression test in zig-import-resolver.test.ts (fails on the
previous code).
- tree-sitter-languages test: the "grammar is absent" case now drives the
loader's real `source.load()` catch branch — `node:module` is stood in
with a `createRequire` whose require throws MODULE_NOT_FOUND for
`@tree-sitter-grammars/tree-sitter-zig` and delegates everything else —
instead of the `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` opt-out, which has its
own test. It also asserts the opt-out flag is NOT set and that other
grammars still load (non-fatal optional failure).
Not re-fixed:
- "Return type is read from the wrong tree-sitter field": tree-sitter-zig
1.1.2 has NO `return_type` field on function_declaration — the type after
`)` is the `type` field (AST dump: `builtin_type "i32" field=type`; the
proposed `childForFieldName('return_type')` is null for every function).
A pin test in zig-extractors.test.ts asserts both the grammar fact and
that `returnType` is extracted (`void`, `!*Counter`).
- "`DIR_LANG` has no zig entry": it does (added in the first pass and
answered again in the third); the finding is carried over unchanged.
- language-config `parseZigBuildZon`: the `.dependencies = .{` header and
the per-entry `.<name> = .{` headers are matched only OUTSIDE string
literals (per-offset string mask + `matchZonHeader`). A `.name` or
`.description` value spelling `.dependencies = .{ .fake = .{ .path = … } }`
used to be taken as the block and returned the fake dep instead of the
real top-level one.
- import-resolvers/zig: an absolute import (`@import("/foo.zig")`) returns
null. The path walker skipped every empty component, so the leading `/`
vanished and `/foo.zig` resolved as importer-relative `src/foo.zig` — an
in-repo edge for an import Zig rejects as outside the module path.
- tree-sitter-languages.test.ts: the Zig parsing case gates on the PACKAGE
being installed (`createRequire().resolve`, minus a deliberate
`GITNEXUS_SKIP_OPTIONAL_GRAMMARS` opt-out), not on `isLanguageAvailable`,
which is false for absent AND for installed-but-broken bindings — so a
load failure (ABI mismatch, bad export) now fails the test instead of
skipping it, as the comment already claimed.
- walkers.ts `isShapeLike` doc: `Union` sits in `isClassLike` because that
is the label set the ownership walkers consult, not because unions
inherit — Zig has no inheritance and no heritage hooks. The previous
wording ("inheritance-capable owner") said otherwise.
- Not re-fixed (already addressed in the second pass, findings carried
over): "receiver = any parameter named self" — `interpretZigTypeBinding`
only sources a first-position parameter as `self`; `zigReceiverBinding`'s
doc now states that invariant. "`DIR_LANG` has no zig entry" — it does.
Extended one level out: `BASENAME_LANGS` (`zig.ts`) and `PREFIX_LANGS`
(`ZIG_`) map to the Zig grammar too, so extractor configs and the
export-detection set are validated against Zig alone. That immediately
caught a dead `childForFieldName('parameters')` in
method-extractors/configs/zig `zigParameterList` (there is no such field;
the named-child lookup was already the one doing the work) — removed.
Tests: zig-import-resolver (+2: absolute path, header inside a string);
both fail on the previous code.
- import-resolvers/zig: `..` above the repository root now returns null
instead of aliasing a same-named root file (`../bar.zig` from `main.zig`
is not `bar.zig`); the stale "extension is stripped and re-added" comment
is corrected to what the code does.
- variable-extractors/configs/zig `extractType`: read the `type:` field only.
The positional fallback returned the INITIALIZER of `const f = target;`
as its type and gave up on compound annotations (`*Foo`, `?[]const u8`).
The comment claiming 1.1.2 has no `type` field on variable_declaration
was wrong (verified by AST dump) — and it is what led the review to
suspect the callable-flow `extractAssignment` callback, which was
already correct for `extern var f: T;`.
- receiver detection: only a FIRST parameter named `self` is the receiver.
`emitZigScopeCaptures` tags first-position parameters
(`@type-binding.first-parameter`), `interpretZigTypeBinding` requires the
tag as well as the name, so `zigReceiverBinding` no longer turns
`fn f(a: u32, self: T)` into an instance method.
- resolvers/zig.test.ts: both suites `describe.skipIf(!zigAvailable)`
(Swift/Dart pattern) — the grammar is an optionalDependency.
- tree-sitter-languages.test.ts: the Zig parsing case gates on
`isLanguageAvailable` instead of a catch-all `return`, so an installed
grammar that fails to load fails the test; comment no longer calls Dart
and Swift npm optionalDependencies (they are vendored).
- test/helpers/literal-collectors: `DIR_LANG` gains `zig`, so literals under
`languages/zig/**` are validated against the Zig grammar alone rather than
against every grammar.
- walkers.ts `isShapeLike` doc: Union IS included (via isClassLike, wired by
Zig's union member container); Typedef remains the only deferred one.
- language-config `ZigBuildZonConfig.pathDeps` doc: values are the raw
`.path` strings; the resolver normalizes.
Tests: zig-import-resolver (+1), zig-extractors (+3).
One commit per the bot's list so each item is easy to check off:
- parser-loader: Zig row gains `userSkippable: true`, so
`GITNEXUS_SKIP_OPTIONAL_GRAMMARS` (=1 or a list naming `zig`) disables it
at analyze time like swift/dart/kotlin — as `optional-grammars.ts` already
documented. Covered in parser-loader-skip-optional.test.ts.
- export-detection: `zigExportChecker` stops at the first declaration it
reaches. A non-`pub` fn inside `pub const T = struct {…}` was reported
exported because the walk continued up to the wrapper.
- import-resolvers/zig: a `.path = "."` dep normalizes to '' and no longer
grows a leading slash (`/src/main.zig` could never match).
- language-config: build.zig.zon parsing strips `//` comments string-aware
(a `//` inside `.url = "https://…"` survives) and matches braces while
skipping string literals, so a commented-out `.path` cannot declare a dep
and a `}` in a comment/string cannot truncate the block.
- method-extractors/configs/zig: the leading `self` receiver is excluded
from `parameters` (Rust parity). Fixing that exposed a worse bug: the
`parameters` node is a plain child of `function_declaration`, not a
`parameters:` field, so `childForFieldName('parameters')` was always null
and every Zig method had no parameters, no receiver and `isStatic: true`.
One `zigParameterList` helper now feeds all three readers.
- variable-extractors/configs/zig: container (`struct`/`enum`/`union`) and
`@import` bindings are skipped via the same predicate the scope captures
use (`isZigContainerOrImportBinding`), instead of the comment merely
claiming they were.
- tree-sitter-languages.test.ts: the "missing grammar" case now forces the
absent-binding path through the loader's runtime opt-out on a fresh module
instead of passing vacuously when the package is installed.
New: test/unit/zig-extractors.test.ts (exports, receiver/parameters,
variable guard); zig-import-resolver.test.ts gains the `.` dep, comment and
brace cases.
The `createFieldExtractor` heads-up needs no change: the added branch is
unreachable for every existing config (none has empty `bodyNodeTypes`).
Post-rebase catch-up: since this branch forked, main added three "every
registered language must appear here" tests. Each needs a Zig entry:
- callable-value-flow (#2522): Zig now emits `@callable-flow.*` facts via
`synthesizeCallableFlowCaptures` (ZIG_CALLABLE_CAPTURE_OPTIONS in
zig/captures.ts). tree-sitter-zig's `call_expression` carries arguments
as direct children with no wrapper node, which the shared helper could
not decompose, so this adds a language-neutral `extractCallArguments`
hook (mirror of `extractFunctionParameters`; `undefined` = shared path).
Zig joins the provider matrix as 'matrix' with a real assign→copy→
argument→invoke case.
- external-import-conformance (#2953): `@import("std")` beside a decoy
`src/std.zig` resolves to nothing; the decoy stays reachable via the
relative spelling. Zig holds the property (no suffix fallback), so it is
a case, not a KNOWN_GAPS entry.
- import-target-index-reuse contract (#2909): membership-probe-only
fixture (minimumScans: 0, same shape as Rust).
`(variable_declaration (identifier) @declaration.name)` matched EVERY
identifier child of the node, so `const first = target;` also declared a
phantom local named `target` in the enclosing block. That phantom shadowed
the real function for later references (and starved callable-value-flow
seeds of a target). The `.` anchor pins the pattern to the first named
child — the bound name.
Regression test in resolvers/zig.test.ts pins the capture set.
Self-review findings on the Zig branch:
- scope/walkers.ts `isClassLike` and finalize-algorithm's
CALLABLE_OR_TYPE_LIKE did not include 'Union': a `union(enum)`
container's methods got no ownerId from populateClassOwnedMembers, so
method dispatch on union receivers silently dropped. Widened both
sets; the zig-basic fixture gains a Tag method + a CALLS assertion
(main → isEnergy) that fails without the widening (verified by
reverting).
- optional-grammars.ts now lists tree-sitter-zig with an npm `probe`
(it is an optionalDependency, not vendored): users with .zig files
and no prebuild get the standard one-line stderr warning instead of
a silently degraded index.
- Deduplicated the container-method predicate: `isZigContainerMethod`
+ ZIG_CONTAINER_TYPES now live once in languages/zig/captures.ts and
feed both the provider labelOverride and the scope-capture relabel.
- README language matrices: Zig row now claims Type Annotations,
Constructor Inference, and Config (build.zig.zon) — all true since
the scope-resolution hooks landed.
Implements the registry-primary scope-resolution path for Zig, the
prerequisite for cross-file edges since the legacy DAG removal. Adds the
standard per-language stack under languages/zig/:
- query.ts: scope query (containers as Class scopes, blocks, functions),
declarations (container anchors placed on the container node itself so
the def lands in its own Class scope and the name binding auto-hoists
to the parent — populateClassOwnedMembers needs the class-like def
among the class scope's ownedDefs), @import statements (#eq?-gated
builtin), parameter/constructor type bindings, and call/constructor
reference sites. The grammar is required lazily (optionalDependency).
- captures.ts: emitZigScopeCaptures — groups query matches, drops the
plain-variable group for container/import bindings (their dedicated
rules bind the name), and relabels container-nested fns
@declaration.function → @declaration.method (labelOverride parity).
- interpret.ts: namespace-kind imports (const x = @import("…")) and
type bindings — self-parameter convention marks the receiver, Zig
sigils (*, ?, [], error unions, const) stripped from type names while
dotted qualifiers (mod.T) are preserved for Case-3 namespace-prefix
receiver dispatch.
- simple-hooks.ts: parameter bindings stay function-local (Go
rationale), local-over-import merge precedence, bounds-check arity
(always 'unknown' today — no synthesized arity metadata).
- scope-resolver.ts: emit-side wiring; build.zig.zon threads through
loadResolutionConfig into the same resolveZigImportInternal the legacy
resolver config wraps. fieldFallbackOnMethodLookup off (statically
typed). Registered in SCOPE_RESOLVERS.
The resolvers integration test un-skips the import-edge case and gains
CALLS assertions: free call (main → helper) and receiver-bound method
dispatch through a namespace-qualified constructor
(var p = pioneer.Pioneer{…}; p.tick() → main → tick).
Ports the additive pieces of grgisme's standalone Zig provider PR
(https://github.com/abhigyanpatwari/GitNexus/pull/1096) onto the rebased
provider:
- build.zig.zon `.path` dependency resolution for bare-name
@import("pkg") (parseZigBuildZon / loadZigBuildZon in
language-config.ts, resolveZigImportInternal in import-resolvers/zig.ts,
wired through ImportConfigs.zigBuildZon). `.url` deps and
repo-escaping paths return null cleanly. 13 unit tests.
- `union(enum)` containers now produce `Union` nodes (not Struct):
'Union' added to ClassLikeNodeLabel + CLASS_LIKE_LABELS, the Zig query
tags @definition.union, CONTAINER_TYPE_TO_LABEL maps union_declaration
to 'Union'. The label was already plumbed graph-wide on main.
- /^build$/ entry-point pattern (build.zig).
- zig-basic lang-resolution fixture + resolvers integration test.
Adapted to current main while porting:
- labelOverride relabels container-nested fns Function → Method
(mirrors isKotlinClassMethod); the structure phase no longer derives
Method from the legacy method-extraction path for plain
@definition.function captures.
- IMPORTS/CALLS edges require scope-resolution hooks
(emitScopeCaptures / interpretImport) since the legacy DAG removal;
Zig does not implement them yet, so the integration test documents
that with a skipped import-edge case. The resolver itself is wired
into the resolver factory and becomes live when the hooks land.
Not ported: named-bindings extractor (the legacy namedBindingExtractor
API no longer exists) and the bespoke field extractor (the generic
factory's extractOwnerName / empty-bodyNodeTypes hooks cover Zig).
Co-authored-by: Garrett Griffin-Morales <grgisme@gmail.com>
Adds a Zig LanguageProvider grounded in @tree-sitter-grammars/tree-sitter-zig 1.1.2.
The grammar's published peerOptional `tree-sitter@^0.22.1` is suppressed via an
npm `overrides` entry that aliases the peer to the bundled `tree-sitter@0.21.x`;
load-time smoke testing confirmed ABI compatibility.
v1 capabilities:
- .zig file detection + Prism syntax mapping
- Top-level + nested function_declaration as Function/Method
- struct/enum/union (anonymous in the grammar — owner name resolved from the
enclosing variable_declaration in class/field/method extractors)
- container_field as struct/union fields and enum variants
- top-level const/var as Variable nodes
- free + member call_expression as @call edges
- @import("./foo.zig") local-file resolution; std and external packages
return empty (no ghost edges)
- pub keyword detection for export checking
- no heritage hooks (Zig has no inheritance; queries never emit @heritage.*)
Generic extractor changes (backward-compatible):
- field-extractors/generic.ts and method-extractors/generic.ts: empty
`bodyNodeTypes` falls back to the type declaration node itself as its own
body container — needed because Zig's struct_declaration directly contains
its container_field children. (The `extractOwnerName` hook this commit
originally introduced now exists upstream; Zig just configures it.)
Out of scope (deferred):
- usingnamespace, build.zig.zon package graph, comptime/anytype
- scope-resolution hooks (emitScopeCaptures, interpretImport, …): Zig is
classified `experimental` and uses the generic fallback resolution path
- cross-package imports (std, deps)
Tests:
- new fixture test/fixtures/sample-code/simple.zig
- Zig describe block in tree-sitter-languages integration test
- simple.zig added to parsing.test.ts fixture-existence list
- Zig added to ingestion-utils detection unit test
- Zig smoke case in parser-loader-abi.test.ts
- Zig grammar registered in the grammar-literal validation gate
Remediates GHSA-xcpc-8h2w-3j85 (DoS via crafted ZIP file 4GB memory allocation in onnxruntime-node).
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Resolve Java imports against parsed package declarations, expand package wildcards deterministically, and keep external imports unresolved when no in-repo package declares them.
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
* fix(typescript): resolve imports against declared config, not path suffixes (#2953)
TypeScript/JavaScript/Vue import resolution ended in `suffixResolve`, which
answers "does any file in this repo have a path ending in this specifier?" and
answers it by dropping leading segments until something matches. That is not
module resolution, and it failed in both directions at once:
- `@acme/telemetry/nest`, a registry dependency with no in-repo file, landed
on the repo's only path ending in `nest/index.ts` — a false IMPORTS edge at
confidence 1.0, indistinguishable downstream from a real one. The reporter
measured 44 of 74 `apps/ -> packages/` edges landing on two such files.
- `@repo/utils`, a first-party workspace package, resolved to nothing: its
name lives in `packages/utils/package.json` and appears in no file path, so
a path matcher cannot find it. Zero CALLS from 75 import statements.
Both come from the same missing input — nothing read the config that says what
exists — so both are fixed by reading it.
Replaces the suffix matcher on this path with the algorithm tsc and Node
actually run, in their order: relative/absolute, `#imports`, tsconfig `paths`
(longest literal prefix wins, every target tried), tsconfig `baseUrl`, then the
workspace package's own `exports`/`main`. A specifier none of those declare is
external, and resolves to nothing. There is deliberately no fallback.
New:
- `typescript/tsconfig.ts` — every tsconfig/jsconfig in the repo with
`extends` chains resolved, nearest-config-wins per file. The old loader read
three filenames at the repo root, required `paths` to exist, and kept only
`targets[0]` — none of which describes a monorepo, where `apps/web/
tsconfig.json` is what governs `apps/web/src/main.ts`.
- `typescript/module-resolution.ts` — the algorithm.
- `typescript/file-candidates.ts` — 11 TS-family extensions, replacing a
shared 39-entry list spanning every indexed language, so a TypeScript
import can no longer resolve to a `.py` file.
- `import-resolvers/node-workspace-packages.ts` — in-repo manifests, with
`exports` subpath maps, patterns, condition nesting, and the restriction
that a package declaring `exports` exposes only what it lists.
The per-pass `SuffixIndex` is gone from these three adapters: real resolution
derives nothing from the file list — every candidate comes from a declared
source and is checked with one `Set.has` — so there is nothing left to cache.
Their `*-import-index-reuse` guards and the JS index-vs-scan differential are
deleted with the mechanism they measured; the cross-language contract test
moves the three languages to its existing `KNOWN_UNINDEXED` channel, and pins
the exemption as a list so a fourth arrival is deliberate.
Python, Ruby, Java, Go and the rest still route through `suffixResolve` and are
untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* test(scope-resolution): assert every resolver refuses external imports (#2953)
One property, for all 16 registered resolvers: a specifier naming something
outside the repository must not resolve to a file inside it.
That is the property #2953 was filed against, and its violation is not a
missing edge but a fabricated one — an IMPORTS edge at full confidence between
two files with no relationship, which `impact` then reports as blast radius.
The mechanism is shared (`suffixResolve`), so the guard is too.
Every case pairs an external specifier with a DECOY: an unrelated in-repo file
whose path ends the way the specifier does. Without one a resolver that merely
found nothing would pass while holding no property at all, so each case also
asserts the decoy is reachable by the spelling that SHOULD find it — a typo in
a fixture cannot manufacture a pass.
Two fixtures had to be corrected before the results meant anything, and both
would have recorded a false gap:
- C# reads its #1881 gate from scanned namespace evidence and fails OPEN
without any, so passing `undefined` measured nothing. Armed, C# holds.
- C++ was posting a pass on an extension mismatch (`vector` could never match
`src/vector.hpp` whatever the resolver did). Given the header spelling, it
does not hold.
Result: six hold it — TypeScript, JavaScript and Vue because they resolve
against declared config only (#2953); Python (#898) and C# (#1881) because they
gate the fallback on in-repo evidence; Rust because `::` never decomposes into
a path suffix, which the decoy-reachability arm confirms is a real pass rather
than a vacuous one.
Ten do not, and are recorded in KNOWN_GAPS with what each currently answers:
Java, Kotlin, Go, Ruby, PHP, Dart, Swift, C, C++, COBOL. The map is a work
list, not an allowance — the entries are ASSERTED, so a language that starts
holding the property fails here and its line gets deleted deliberately rather
than rotting into a lie.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* fix(typescript): admit only declared workspace packages, and fix four resolver defects (#2953)
Review of #2956 found one boundary bug and four correctness defects. The
boundary one is the same defect class this PR exists to fix, arriving from a
different direction.
## The workspace boundary (review)
`loadNodeWorkspacePackages` registered every `package.json` the repo-wide scan
found, and never read `pnpm-workspace.yaml` or a root `workspaces`
declaration. Finding a manifest is not the same as the workspace admitting one:
an app importing registry package `foo` would bind to an excluded fixture or
example that happens to declare `name: "foo"` — the false-positive half of
#2953, from a new source of evidence. This repository is the example, since
`test/fixtures/**` declares `@repo/utils` among others.
The admitted set now comes from the declaration — `workspaces` (array and yarn
object form), `pnpm-workspace.yaml`, `lerna.json`, with `!` exclusions and
`*`/`**` — plus the root package itself. A repo that declares no workspace has
exactly one package: the root. A negative fixture pins it, with a named package
outside the declared globs that must not resolve.
## Four defects
- tsconfig `paths` targets were resolved against the config's own directory
when it declared `paths` but inherited `baseUrl`. tsc resolves them against
the EFFECTIVE base, so an extending config loaded the right alias pattern
and pointed every target at the wrong directory.
- two configs in one directory were ranked by directory-listing order, so
`tsconfig.base.json` could govern instead of `tsconfig.json` and a config's
own `paths` went invisible. Found by the test written for the fix above.
- an unexported package subpath also tried `<dir>/src/<subpath>`. Nothing
declares that mapping; it is the same kind of guess this PR removes, and
the import it "resolved" is broken in the real project too.
- `imports` pattern keys (`"#internal/*"`) were looked up exactly, so a valid
`#internal/foo` never matched. `exports` and `imports` now share one
matcher, which is where they should never have diverged.
- a relative specifier climbing past the repo root was silently clamped, so
`../../../secret` from `src/main.ts` became `secret` and could resolve a
root file it never named.
## Test rigor
The conformance suite asserted less than it claimed. The decoy-reachability arm
only checked non-empty, so five cases paired `reachesDecoy` with a different
file than `decoy` and passed while establishing nothing; the KNOWN_GAPS arm
likewise accepted any in-repo answer instead of the recorded one. Both now
assert the exact file. The reachability arm runs only for languages that HOLD
the property — for a gap language the recorded-answer assertion IS that proof,
and for Swift and COBOL no other spelling exists, since `Foundation` and
`EXTERNAL` name the in-repo directory and copybook as well as the external
module, which is precisely why those resolvers cannot tell them apart.
## Benchmarks
Both `--check` guards were red, and both were reporting something true.
`import-target`: the ts-family arms resolved 0 of 3200 imports. Their corpus is
bare specifiers with no config, which the deleted `suffixResolve` answered
without one — so the arms measured an empty branch while printing a clean
scaling ratio. Each now carries the config its corpus is spelled for, and the
`deep` arm's uniform prefix reaches it. THE FINGERPRINTS THEN MATCHED THE
RECORDED BASELINES EXACTLY: same corpus, same targets, once the config it
always implied is passed explicitly. Retained per-pass index went from
26 745 296 B (js, ts) and 28 884 016 B (vue) at 32 000 files to 0-16 B, because
these resolvers no longer build one; they move to the `HEAP_BOUNDED` tier rust
already occupies for the same reason. Depth ratio moved 2.0 -> ~2.2 and the
budget goes to 2.6: candidates now carry the 16-segment baseUrl prefix, so each
`Set.has` hashes a longer string — linear in path LENGTH, independent of file
COUNT.
`scope-capture`: TypeScript capture fingerprint drift, caused by this PR's 12
new `.ts` fixtures entering the corpus. Attribution is exact rather than
inferred — moving that one fixture directory aside returns the fingerprint to
`f719163e…` byte-for-byte with `fixture_count` back at 155 and all 15 languages
passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* fix(typescript): honour exports fallback arrays, paths precedence and package extends (#2953)
Second review round. Four findings, judged against what this tool is: a static
analyser building a code graph, not a compiler. The bar is resolving what the
project DECLARES, on a checkout that may never have been built or installed,
and never inventing an edge.
- `exports` and `imports` ARRAYS were skipped. An array is Node's ordered
fallback list, and `{"./feature": ["./dist/feature.js", "./src/feature.ts"]}`
is exactly what a workspace package publishes to mean "built output, or
source". Skipping it dropped the declaration entirely and left the package
looking as though it exported no subpaths. The source arm is the one that
matters here, because `dist/` is build output and is not indexed — and for
a static analyser the build need not have run at all.
- an exact `paths` pattern did not reliably outrank a wildcard. `a` and `a*`
both match `a` with the same literal prefix length, so sorting on length
alone left tsc's exact-wins rule to declaration order.
- package-form `extends` (`"@acme/tsconfig"`) was refused outright. Not
indexing `node_modules` is different from not READING it, and a shared
internal base is where a monorepo puts the `paths` its packages import
through. It is now read from disk, walking `node_modules` up from the
extending config the way Node does, and absent on an un-installed checkout
it degrades to whatever that config declared itself.
The test pins what tsc actually does with such a base rather than what one
might hope: `extends` never rebases `baseUrl`, so a package base's paths
point at the package's own directory. That is why a published base rarely
contributes aliases a repo's files resolve through, and why the
`@tsconfig/*` family — which sets `target` and `lib`, never `paths` — is a
no-op here either way.
- CodeQL flagged `String.replace('*', …)` in two places as replacing only the
first occurrence. Node subpath patterns and tsconfig `paths` both allow AT
MOST one `*`, so that IS the specified behaviour — but the spelling states
it by accident and reads as the replace-all footgun. `substituteStar`
slices at the known index and says the rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* fix(typescript): treat `exports` as the whole interface, and keep empty tsconfig scopes (#2953)
Third review round. Two findings, both valid, both cases of this resolver being
laxer than the thing it models — which is the direction that fabricates edges.
- `exports`, when a manifest declares it, is the package's ENTIRE public
interface: Node ignores `main` outright and refuses any subpath the map
does not list. This resolver already honoured that restriction for
SUBPATHS and not for the package ROOT, which is the same rule. A manifest
exporting only `"./feature"` therefore still answered a bare `@repo/pkg`
with `main` or `src/index` — an edge for an import that does not resolve in
the real project. Legacy and conventional root candidates are now offered
only when there is no `exports` field at all.
- a tsconfig declaring neither `baseUrl` nor `paths` was dropped rather than
kept as an empty scope, so `tsconfigFor` fell through to an enclosing
config. A package whose own tsconfig declares no `baseUrl` — meaning its
non-relative specifiers are package lookups — silently inherited the repo
root's aliases instead. An empty scope is the accurate answer for such a
file, and only a scope can express it.
Both are pinned at the level they broke: the manifest arms assert what
`readManifest` produces, not a hand-built package, since the resolver honouring
empty entries and the loader producing them are different claims.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(scope-resolution): make interface dispatch generic-instantiation aware (#2912)
Interface-dispatch fan-out walked the subtype closure with generic arguments
erased, so `IValidator<string>` and `IValidator<int>` — one declaration, one
subtype list — were indistinguishable and a call through the first reached
`IntValidator.Check(int)`, a target no runtime dispatch can produce.
The arguments were already in the capture, unread: every language anchors
`@reference.inherits` on the whole base node while `@reference.name` keeps the
erased base. `ReferenceSite.typeArguments` is therefore derived generically in
`scope-extractor.ts` from the anchor's own spelling — no per-language query
changed — covering C#, Java, TypeScript, Kotlin, Go (`Base[int]` embedding),
Python (`Base[User]`) and Swift; Rust and Dart anchor on the bare name and get
nothing, which reads as "unknown". `preEmitInheritanceEdges` is the only code
that pairs a heritage site with a resolved (subtype, supertype), so it records
the instantiation there and hands it to the dispatch pass.
The closure is then walked carrying a substitution, as a type checker would:
`Wrapper<T> : IValidator<T>` binds T to the receiver's argument and stays
reachable from every instantiation, while its own subtypes are matched against
that binding. An incompatible hop is skipped without being marked seen, so a
type reachable by a second, compatible path still gets its edge, and without
descending, since its subtypes inherit the mismatch.
Pruning happens only on positive evidence that two instantiations differ.
Unknown arguments on either side, an arity that does not line up, an unresolved
qualified spelling of the same simple name, or an argument that might be a type
variable the language never captured all keep the target. Telling an uncaptured
type VARIABLE from a concrete type is the crux: `typeParameters` is absent both
for a non-generic declaration and for every declaration in a language whose
query omits `@declaration.type-parameters`, so the pass reads the evidence in
front of it — one run resolves one language, so a single generic declaration
anywhere in it proves the captures record parameters. A language recording
neither arguments nor parameters keeps exactly its pre-#2912 fan-out.
Type arguments are compared as resolved declarations rather than spellings, so
`Models.User` and an imported `User` are one type; the new optional
`ScopeResolver.normalizeTypeArgument` hook canonicalizes a language's predefined
aliases, implemented for C# (`string` ≡ `String`) where mixing the spellings
would otherwise delete a real implementor.
Fan-out cap, skipped-target reporting, overload selection and non-generic
closure behaviour are unchanged. SCHEMA_BUMP 60 -> 64: the heritage arguments
are a parse-time capture, so a warm cache would replay pre-fix sites and leave
the filter silently inert on unchanged files (61/62/63 are claimed by open PRs).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef
* fix(scope-resolution): close the two generic-dispatch gaps (#2912)
The first commit left two shapes on the pre-#2912 fan-out. Both are now
covered, and the second one turned out to need a route the pipeline did not
have at all.
**Folded receivers (Cases 0 and 3b).** `this._validator.Check(x)` is typed by
the compound fold, and the fold answers with a CLASS — which is exactly what
loses the instantiation, since `IValidator<string>` and `IValidator<int>` fold
to one declaration. The fold now reports the SPELLING it typed each receiver
position from, through a pure side channel (`recordReceiverType`) added to the
one helper every declared-type route already shares plus the two return-type
routes; resolution is unchanged whether or not a caller passes it. The reader
keeps the last report and uses it only when it names the class the fold
returned, so an intermediate position cannot lend its arguments to another
class. This covers the dependency-injection shape the issue is really about —
a field-held generic interface — and multi-hop chains, where it is the last
hop's spelling that types the receiver.
**Rust and Dart heritage.** Neither recorded arguments, for two different
reasons, so both routes exist now:
- Rust's `@reference.inherits` anchor is the trait identifier INSIDE a
`generic_type`. Widening the anchor would move the site's range, and that
range is part of every inheritance edge's id, so the arguments arrive
through a new `@reference.type-arguments` sub-tag instead.
- Dart's `implements` / `with` never become reference sites at all: they
travel as heritage MARKERS and their edges are emitted by the language
hook. The arguments ride the marker payload as an optional fourth field
(dropped, not encoded, when the spelling contains the marker delimiter),
and `ScopeResolver.emitHeritageEdges` now receives the same sink
`preEmitInheritanceEdges` writes to, so whichever pass emits an edge
records that edge's instantiation. Dart also gained the
`@declaration.type-parameters` capture, without which its own type
VARIABLES are indistinguishable from concrete arguments and
`class Box<T> implements Validator<T>` would be pruned from every
instantiation.
Note this makes Rust and Dart record their instantiations; it does not make
them fan out. Interface dispatch still fires only for a receiver whose folded
type is an `Interface` symbol, so a Rust `Trait` or a Dart abstract `Class`
receiver has no secondary targets to filter. Widening that gate emits new
edges for several languages and belongs to its own issue.
**Two matcher rules the wider coverage exposed.** A WILDCARD names a set of
types rather than one — `Repo<? extends User>` holds a `Repo<User>`, and
Kotlin's `Repo<*>` / `Repo<out User>` say the same — so a position with one on
either side is unknown; nullable spellings trip the same test, which costs a
little precision in the safe direction. And insignificant whitespace inside a
nested spelling (`Map<string, User>` vs `Map<string,User>`) is no longer a
difference.
One expectation changed in the #2833 field-receiver matrix: a
`Repo<Repo<User>>` receiver no longer reaches `UserRepo implements Repo<User>`.
That edge is precisely the false positive this issue is about, and the primary
edge to the interface's own declaration — which is what the matrix row exists
to prove — is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef
* refactor(scope-resolution): apply the quality pass to the #2912 change
Three cleanups, no behaviour change.
**One balanced-list scanner, not two.** `erasedTypeApplication` and
`typeApplicationArguments` each carried a copy of the same fiddly scan — one
bracket list, balanced, closing on the last character, non-empty — differing
only in what they did with the result. Both now call `balancedTailList`; the
rule that rejects `User[][]` and `Repo<User>?` lives in one place instead of
being free to drift between two.
**The receiver's arguments are parsed after the gates, not before them.**
`emitInterfaceDispatchFor` takes the receiver's declared SPELLING and parses it
itself, once the owner is known to be an Interface with subtypes. Every one of
the five cases calls it unconditionally and the overwhelming majority of
receivers are concrete classes that return at the first line, so the parse was
running per resolved receiver site to be discarded immediately. Case 4 and Case
6 now hand over the string they already hold, and the folded-receiver helper
returns the recorded spelling rather than parsing it.
**One question gates the whole instantiation apparatus.** Inside the closure
walk, the graph-id lookups now hang off "is the supertype's instantiation
known?" — false for every non-generic receiver and for every language that
captures no heritage arguments, which is what makes those walks cost exactly
what they cost before #2912.
Also lifted the argument-route choice in `pass5CollectReferences` out of a
nested ternary into a named `heritageTypeArguments`, where the reason the
explicit sub-tag wins over the anchor text can be stated once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef
* test(scope-resolution): cover generic interface dispatch in Kotlin and Go (#2912)
Extends the #2912 dispatch coverage past C#/Java/TypeScript. No production
code changes — the derivation is language-agnostic by construction
(`heritageTypeArguments` reads the heritage anchor's own spelling), so the
question was only which languages actually reach the filter.
Kotlin rides the shared heritage pre-pass; Go reaches the same filter from
the other side, matching implementors structurally while the receiver's
`Validator[string]` spelling carries the instantiation. Both are confirmed
to prune the mismatched implementor.
Each language gets a NON-GENERIC control asserting the fan-out still reaches
every implementor. Without it the `not.toContain` assertion passes just as
well when a language emits no dispatch edge at all — which is what Dart,
Python and Rust were measured doing for this receiver shape, generic or not.
They are deliberately not asserted on here: a "filtered correctly" test over
a path that never fans out measures nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* test(bench): re-baseline the Rust and Dart capture fingerprints for #2912
The Rust trait-impl and Dart heritage capture changes this branch makes are
additive TEXT on existing matches — each carries the instantiation the clause
was written with — so they drift the scope-capture digest without adding or
removing a match. The baselines were never re-measured when those captures
landed, which left `measure.mjs --check` red on this branch independently of
the merge.
Re-measured rather than hand-edited. Rust's capture_groups_fp (3556) and
fixture_count (202) are unchanged across the move, which is the evidence that
this is digest drift and not a capture-set regression. The other 13 languages
are byte-identical; 15/15 pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* refactor(scope-resolution): quality pass over the #2912 change
Cleanup only — no behavior change. Findings from a four-angle review (reuse,
simplification, efficiency, altitude), applied where they were verified.
Reuse / duplication:
* `stripTrailingCallSuffix` was a second copy of `matchingOpenParen`'s backward
balanced-paren scan. Both now live in `template-arguments.ts` beside
`balancedTailList`, for the reason that helper was shared in the first place:
two copies of a scan this fiddly are free to disagree.
* The two call-return arms of the compound fold repeated the same four-part
expression character for character; they share `classOfReturnType` now, the
return-type twin of `classOfDeclaredType`, which keeps the "look up by
rawName, report the erased application" pairing in one place.
* `pipeline/run.ts` implemented first-writer-wins twice — once in the pre-pass
and once in the provider sink. One store, one sink, one rule; the pass keeps
its `Set<string>` return and the callable-flow-only arm stops building an
empty map to satisfy a widened return shape.
Simplification:
* `subtypeParametersComplete` dropped a disjunct that could never decide: every
`subDef` reaching it comes out of the same loop that sets
`languageCapturesTypeParameters`, from exactly those defs.
* The heritage-argument lookup asked "is the supertype's instantiation known?"
three times; `superGraphId` now gates the block once.
* `TypeArgumentResolver` and `HeritageInstantiationResult` un-exported — no
consumer outside their module.
Efficiency (all on the per-site dispatch walk):
* `resolveSupertypeArgument` captures only the site, so it is built once per
site instead of once per subtype visited; the subtype's scope id is looked up
once per subtype instead of once per argument position.
* `erasedTypeApplication` no longer runs on every fold hop through a call — the
spelling is built only once the lookup has found a class, since it is
discarded otherwise.
* `normalize`+`compact` computed once per side rather than twice.
* Regex literals and the identity `normalize` fallback hoisted to module scope.
* C# `System.` prefix stripped with `startsWith`/`slice` instead of a regex.
Verified: tsc clean, build clean, 1994 scope-resolution unit tests, 171
generic-dispatch + generic-field-receiver integration tests, 15/15 capture
bench fingerprints unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* style: apply Prettier to the two files the quality pass reformatted
Whitespace only — `quality / format` (npx prettier --check .) was red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* fix(scope-resolution): close the generic-dispatch review findings (#2912)
Addresses the gitnexus-check review on #2939.
A repeated type variable was rebound rather than unified: `class C<T> :
Pair<T, T>` accepted a `Pair<string, int>` receiver, with `T = int` silently
replacing `T = string` and the bogus substitution carried to the next hop.
It now unifies, and prunes only on the same positive evidence the concrete
path demands — an undecidable repeat keeps the target with no binding.
A type PARAMETER of the declaration enclosing either side is now recognised
and never compared. `subtypeParametersComplete` is evidence about the
SUBTYPE's parameter list and says nothing about a `T` written at the call
site, so `void Run<T>(IValidator<T> v) { v.Check(x); }` pruned every
implementor: unbounded, `T` grounds to nothing; bounded, it grounds to its
BOUND. Both read as a difference of type. That is the missing-edge failure
this filter is built to avoid, and it is the common dependency-injection
shape in C#, Java and Kotlin.
Making that recognition reliable is why generic METHODS now capture
`@declaration.type-parameters` in C#, Java and Kotlin — TypeScript already
did, which is why its generic functions never had the defect. The capture
feeds the existing `bindsTypeParameter` guard, so a method-level `T` also
stops resolving to a same-named class in every other lookup.
C# alias normalization additionally strips the `global::` qualifier, which
`import-decomposer` already unwraps elsewhere: `global::System.String` read
as unequal to `string` and pruned a live implementor.
The C# captures golden fixture is regenerated for the new capture; the
extractor reads `@declaration.type-parameters` generically, so no reader
changed. SCHEMA_BUMP 64 already covers these capture changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* fix(scope-resolution): close the two remaining gitnexus-check findings (#2912)
`balancedTailList` counted ONE bracket family, so a crossed pair slipped
through: scanning `Foo<Bar]>` it never sees the `]`, reaches the final `>` at
depth zero, and reports `Bar]` as a balanced argument list — which
`typeApplicationArguments` then splits and `erasedTypeApplication` rebuilds a
spelling from. It now tracks a stack of expected closers, so every closer must
match the opener it actually closes and a crossed pair declines to `undefined`,
the "unknown" both callers already fail open on. Well-formed mixed nesting
(`List<Dict[a, b]>`) is unaffected.
C# `normalizeTypeArgument` stripped `System.` from every qualified spelling, so
`System.Custom` answered `Custom` and compared equal to an unrelated `Custom`
elsewhere in the workspace. The strip is now earned: a keyword answers from the
alias table first, and the qualifier is dropped only when what remains IS a
predefined type. `System.Custom` is returned as written and goes to the identity
comparison instead — the step that can actually tell two declarations apart.
`global::System.String` still meets `string`.
Both are pinned by unit tests, including the well-formed mixed nesting and the
`global::`-qualified ordinary type that must keep its qualifier.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* docs(csharp): record why a shadowed `String` keeps its implementor (#2912)
Answers a review finding rather than changing behavior.
A workspace may declare its own type named `String`, shadowing the BCL simple
name, and the alias table then reads `IValidator<String>` as the `string`
instantiation and keeps that implementor. That is the SAFE direction, not an
oversight: pruning instead would rest on the belief that two spellings differ,
which is the missing-edge failure `generic-instantiation.ts` exists to avoid.
Resolving rather than normalizing cannot settle it either — the identity
comparison needs a `definitionId` from both sides, and a built-in name carries
none, so "built-in versus workspace-declared implies different" would be a new
prune with no positive evidence behind it. The cost is one surplus edge for that
pair, which is exactly the pre-#2912 fan-out and no worse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* refactor(scope-resolution): pair the receiver spelling with the class structurally (#2912)
The fan-out needs the spelling a receiver position was typed from, because the
class the fold returns has lost the generic arguments. That was carried by a
PASS-LEVEL mutable holder, written by every declared-type lookup anywhere in the
fold and read back through a def-id coincidence check, with the holder cleared
by hand before each call site. Three things were load-bearing and none were
enforced:
* the reset had to be remembered at every call site. It was not: the Case 3b
retry (`rawName` then `rawName + '()'`) reset once, BEFORE the first attempt,
so a spelling reported by the attempt that failed could be attributed to the
one that succeeded.
* the holder outlived every resolution, so a site that resolved through a route
reporting nothing could read the previous site's spelling if the def ids
happened to line up.
* the pairing itself was inferred from "whichever lookup reported last", not
from the fold's own bookkeeping — losing branches (an MRO walk that moved on,
a step later folded past) report too.
`foldReceiverChain` already had the answer and threw it away: its final
`FoldState` holds `def` and `declaredType` produced by the SAME step. It now
reports that pairing last, so the structural route is the one that stands.
`resolveCompoundReceiverTyped` returns `{def, declaredSpelling}` and owns a sink
created and read within the single call, which is what removes the reset
discipline — a local cannot be forgotten, and each of the two retry attempts
carries its own. The def-id guard stays as the check that a report names the
class actually returned.
Behavior is unchanged: 1975 scope-resolution unit tests, 177 generic-dispatch
and generic-field-receiver integration tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(java): materialize record component accessors
* fix(java): ignore receiver params in record accessor arity
* fix(java): address record-accessor review findings (#2917)
Five findings from the tri-review of #2936.
P1 — a synthesized callable evicted a source-written one from the method map.
`getMethodInfo` keyed its per-class map by `name:line`, but a callable that is
SYNTHESIZED at a position that is not its own declaration shares its owner's
line: a record's implicit accessor is minted at the component, and a C# 12
primary constructor at the owner's `parameter_list`. Both are appended last by
their extractor, so on a single line the synthesized entry overwrote the
explicit method's MethodInfo and both definitions collapsed onto one id —
`record P(int x, int y) { int x(int s) {...} }` lost `P.x#1` and rebound the
arity-1 call to the zero-argument accessor. Adds a required `MethodInfo.column`
and keys the map by `name:line:column` through a single `methodInfoKey` helper.
Required, not optional: an absent column would key an entry no lookup could
reach — a silent, whole-language loss of enrichment instead of a compile error.
All three lookup sites move together; the file's own lockstep docblock warns
that a half-applied change loses caller edges silently rather than dangling.
This also fixes the same collision in C#, which never touched record code.
Degenerate component names no longer mint a node. tree-sitter's zero-width
MISSING recovery token satisfies `name: (identifier)`, so `record M(int x, y) {}`
minted an empty-named Method whose returnType was the neighbouring `y`; and the
grammar admits `underscore_pattern` in the same field, which the query rejected
but the scope path accepted, so `record R(int _) {}` left a scope declaration
with no node behind it. One `isRecordComponentName` predicate now gates all
three emitters — query suppression, scope synthesis, and the method extractor —
so they cannot drift apart again.
Component annotations reach the implicit accessor (JLS 8.10.3 / 9.7.4) by
reusing the shared `extractAnnotations` helper. Deliberately over-approximate
and commented as such: `@Target` lives in another file and parsing is per-file.
`explicitZeroArgAccessorNames` is memoised per record node. It was rebuilt on
every component capture — O(components x body members) for one record, measured
at ~4x per 2x input — while the scope path already hoisted the identical call.
Docs: the `java-local-types` baseline now stores the `capture_groups_fp` its own
note cites, the SCHEMA_BUMP ledger no longer claims a v65 that nothing holds,
and `shouldSkipDefinitionCapture` documents that `defaultLabel` may be ignored.
Scope-capture fingerprints are unchanged (`measure.mjs --check` PASS, 15
languages): the bench corpus contains no degenerate components, so the new
predicate is inert on it. SCHEMA_BUMP stays 67 — this branch's existing claim
already covers the changed worker output; re-check it against origin/main before
merging.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
* docs(ingestion): reunite the overload-suffix JSDoc with typeTagForId
The block describing the `~type1,type2` same-arity discriminator was stranded
above `buildCollisionGroups` when that function was inserted between it and the
`typeTagForId` it documents (#658). Adding `methodInfoKey` in this branch parked
it directly above yet another unrelated function, which gitnexus-check flagged.
Moves the comment down to the function it describes. No behaviour change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>