fix(csharp): copy-on-read frozen binding arrays in namespace-siblings#1083
Closed
mann1x wants to merge 1 commit into
Closed
fix(csharp): copy-on-read frozen binding arrays in namespace-siblings#1083mann1x wants to merge 1 commit into
mann1x wants to merge 1 commit into
Conversation
`scope-extractor.ts` freezes per-name binding arrays via
`frozenBindings.set(name, Object.freeze(refs.slice()))`. When
`populateCsharpNamespaceSiblings` later reads back an existing entry
via `scopeBindings.get(name) ?? []` and tries to `existing.push(...)`
on it, the frozen reference throws
TypeError: Cannot add property N, object is not extensible
at three sites (using-static `origin: 'import'`, namespace-import
`origin: 'namespace'`, and the cross-namespace bucket loop). The
populator's leading comment even says "indexes.bindings is typed
ReadonlyMap<...> but is a plain Map at runtime; mutating here is the
established pattern" — which is true for the Map itself, but the
inner per-name arrays were nonetheless frozen by the extractor.
Reproduced live on the PersistentWindows real-world repo (any C#
codebase where a file locally declares a class with the same simple
name as a class in a `using`-imported sibling namespace). Verified
the crash via the runner.js cause-stack and pinpointed line 321 in
the compiled file (now line ~314 source).
Fix: spread to a fresh mutable copy on read at all three sites:
const existing: BindingRef[] = [...(scopeBindings.get(name) ?? [])];
Behavior: when the populator hits a name pre-populated by the
extractor, it now appends to a fresh array and writes that back,
replacing the frozen entry in the per-scope Map. Origin precedence
(`local` shadows `namespace`) is preserved by the existing
`csharpMergeBindings` ordering — see csharp-hooks.test.ts.
Test: integration regression in csharp.test.ts under "C# regression:
namespace-siblings injection vs frozen bindings" + minimal fixture
at csharp-frozen-binding-collision/. Pre-fix the new `beforeAll`
rejects with "Cannot add property N..."; post-fix the suite passes.
Verified:
- npx tsc --noEmit clean
- test/integration/resolvers/csharp.test.ts: 3 new + 200 existing
- test/unit/scope-resolution/csharp/, test/unit/named-bindings/csharp.test.ts:
full suite green
|
Someone is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
mann1x
pushed a commit
to mann1x/claude-hooks
that referenced
this pull request
Apr 26, 2026
PR opened: abhigyanpatwari/GitNexus#1083 — "fix(csharp): copy-on-read frozen binding arrays in namespace-siblings". Root cause: scope-extractor.ts froze per-name binding arrays via Object.freeze(refs.slice()), then populateCsharpNamespaceSiblings tried to .push() onto them at three sites — TypeError: Cannot add property N, object is not extensible. Crashed scopeResolution for the entire repo on any C# codebase that locally declared a class with the same simple name as a class in a `using`-imported sibling namespace. Reproduced live on PersistentWindows; minimal fixture committed to the upstream test suite. Fix: spread to a fresh mutable copy on read at all three sites. Verified by 5 test files / 282 tests / 0 fail under the C# vitest suites + a new regression test that reproduces the bug pre-fix. This commit: - COMPANION_TOOLS.md: replace the "known limitation" wording with a pointer to PR #1083 + a one-shot install recipe from a forked build until the fix ships in an npm release. - docker/gitnexus/Dockerfile: optional GITNEXUS_TARBALL build-arg so hosts using the Docker wrapper can bake in the fork build directly. Default behavior (npm i -g gitnexus@latest) unchanged. Real-world verification on pandorum: $ gitnexus analyze ./PersistentWindows Repository indexed successfully (5.6s) 1025 nodes | 1715 edges | 28 clusters | 25 flows $ gitnexus list Indexed Repositories (3) foo_dsd_trellis / osync / PersistentWindows (PersistentWindows previously hard-crashed scopeResolution; now indexes cleanly.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
|
I just merged, i think, a fix, for it. Could you please check #1082 ? |
Collaborator
|
We could use your test fixture to test it against. |
Contributor
Author
|
Wow almost in parallel! |
Contributor
Author
|
Confirmed, it works and it's great, real major improvement! Closing this and opening a small PR for the fixture-only. |
4 tasks
magyargergo
pushed a commit
that referenced
this pull request
Apr 27, 2026
…1085) The csharp-large-cache-miss-resolution fixture added in #1082 reproduces the freeze contract failure via tree-sitter cache-miss reparse on >32 KB files. This adds a complementary trigger for the same root cause that does not depend on file size: a small-file pair where the importer locally declares a class with the same simple name as a sibling reached through `using`. Pre-#1082 path: scope-extractor pre-populates (and freezes) `User` in the importer's Module bindings, then populateCsharpNamespaceSiblings' namespace-import loop calls push() on the frozen array and throws "Cannot add property N, object is not extensible", aborting the whole scopeResolution phase. Post-#1082 the augmentation channel keeps both bindings visible; the local `Collision.App.User` shadows the namespace-imported one per origin precedence, so `Program.Run -> new User()` resolves to the local class. Three assertions: - scopeResolution completes (no throw on the colliding bucket). - both `User` declarations are detected across the two namespaces. - `Program.Run -> User` constructor edge points at App/Program.cs (not Models/User.cs), verifying origin:local shadows origin:namespace. Verified: full csharp.test.ts suite green (207/207). tsc --noEmit clean. Refs: #1066, #1082, #1083 (closed as superseded).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
scope-extractor.tsfreezes per-name binding arrays viafrozenBindings.set(name, Object.freeze(refs.slice()))(line 180). WhenpopulateCsharpNamespaceSiblingslater reads back an existing entry viascopeBindings.get(name) ?? []and tries toexisting.push(...)on it, the frozen reference throws…which abandons scope resolution for the entire repo (the index is never produced). The populator's leading comment says "indexes.bindings is typed ReadonlyMap<...> but is a plain Map at runtime; mutating here is the established pattern" — that's true for the outer
Map, but the inner per-name arrays are nonetheless frozen by the extractor, so the established pattern is silently broken at three sites.Reproduction
Triggered when a file locally declares a class with the same simple name as a class in a sibling namespace imported via
using. Hit live on the PersistentWindows real-world WinForms repo (large mix of.Designer.cs+ namespaced classes); also hit on smaller mixed-namespace C# codebases.Minimal fixture added under
test/fixtures/cross-file-binding/csharp-frozen-binding-collision/:Models/User.csdeclaresCollision.Models.UserApp/Program.csimportsusing Collision.Models;AND locally declaresclass UserinCollision.App— the local declaration causes scope-extractor to populate (and freeze)Userin the Module scope's bindings; the populator's namespace-import path then attempts to pushCollision.Models.Userinto the same frozen array.Fix
Spread to a fresh mutable copy on read at all three sites in
populateCsharpNamespaceSiblings:When the populator hits a name pre-populated by the extractor, it now appends to a fresh array and writes that back, replacing the frozen entry in the per-scope Map. Origin precedence (
localshadowsnamespace) is preserved by the existingcsharpMergeBindingsordering (seecsharp-hooks.test.ts).Three sites fixed:
populateCsharpNamespaceSiblingsusing-static loop (origin: 'import')populateCsharpNamespaceSiblingsnamespace-import loop (origin: 'namespace')populateCsharpNamespaceSiblingscross-namespace bucket loop (third namespace-injection site)Verification
npx tsc --noEmitcleannpx vitest run test/integration/resolvers/csharp.test.ts test/unit/scope-resolution/csharp/ test/unit/named-bindings/csharp.test.ts→ 5 test files / 282 tests / 0 fail (3 of those are the new regression test + 279 existing).beforeAllof the new test rejects withPhase 'scopeResolution' failed: Cannot add property N, object is not extensible. Post-fix all three test cases pass: scope resolution completes, bothUserdeclarations are detected, andnew User()insideProgram.Run()resolves to the localCollision.App.User(origin:local shadows origin:namespace).Risk / rollback
Minimal — purely a copy-on-read tweak in a populator that already documented its intent to mutate the per-name arrays. Behavior is unchanged for callers that don't pre-populate the same name. The
Object.freezeupstream inscope-extractor.tsis left in place; if a future change wants to enforce true immutability throughout, this is the right starting point because it makes the freeze contract honest at the populator boundary instead of silently broken.Rollback: revert the commit; the upstream behavior degrades back to crashing on collision.
Test plan
npx tsc --noEmitingitnexus/