From 448868f891dcd42aa2144b8d4c2d8432c77b5fd5 Mon Sep 17 00:00:00 2001 From: ReidenXerx Date: Thu, 6 Aug 2026 02:27:15 +0300 Subject: [PATCH] feat(typescript): make object-type aliases own their members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A TS object-type alias declares the same `property_signature` members as the interface beside it and answers the same question, but was not a member owner: its fields were minted with bare ids and no owner edge, so two aliases in one file sharing a field name collapsed onto one node, while the identical interface resolved normally. `type_alias_declaration` joins CLASS_CONTAINER_TYPES (and CONTAINER_TYPE_TO_LABEL, as that set's invariant requires — a container missing there gets orphaned member edges or a wrong owner label). Aliases with no object type (`type Id = string`) declare no members, so they own nothing and are unaffected. This also lands the INTERFACE field -> consumer edges, verified on the mini-repo fixture rather than only on a purpose-built one: `saveToDb` now links to `ValidationResult.value`, and `formatLogEntry` to `LogEntry.level` and `LogEntry.message` — three real contract-field reads that previously had no graph path at all. Golden updated: +3 ACCESSES, no node changes. The ALIAS field -> consumer edge is still not linked and is recorded as a todo with the exact blocker: resolving a receiver typed as the alias needs the NAME to resolve to a class-like def, and `isClassLike` is Class|Interface|Struct|Record|Enum|Trait. That predicate is read from ~12 sites including MRO and heritage, and every language mints TypeAlias, so widening it would enrol aliases in linearizations where they do not belong. Widening only the scope index was tried and reverted — the type-name walkers gate on it independently, so it fixed nothing and left dead code. That needs a deliberate "shape-like" concept, not more call-site widening. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/core/ingestion/utils/ast-helpers.ts | 12 ++++++++++++ .../mini-repo/expected-graph.json | 5 +++-- .../resolvers/typescript-alias-fields.test.ts | 17 ++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index 9b243d044..f4ec2dd2f 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -335,6 +335,15 @@ export const CLASS_CONTAINER_TYPES = new Set([ 'class_declaration', 'abstract_class_declaration', 'interface_declaration', + // A TypeScript object-type alias owns its members exactly as the interface + // beside it does — same `property_signature` members, same "who reads this + // contract field?" question. Without it an alias member is minted with a + // bare id and no owner, so two aliases in one file sharing a field name + // collapse onto one node and nothing links the field to its consumers, + // while the identical interface resolves. Aliases with no object type + // (`type Id = string`) declare no members, so they own nothing and are + // unaffected. + 'type_alias_declaration', 'struct_declaration', 'record_declaration', 'class_specifier', @@ -398,6 +407,9 @@ export const CONTAINER_TYPE_TO_LABEL: Record = { class_declaration: 'Class', abstract_class_declaration: 'Class', interface_declaration: 'Interface', + // Required by the CLASS_CONTAINER_TYPES invariant above: a container missing + // here gets orphaned member edges or a wrong owner label. + type_alias_declaration: 'TypeAlias', struct_declaration: 'Struct', struct_specifier: 'Struct', class_specifier: 'Class', diff --git a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json index 14d56f642..91ee4fa05 100644 --- a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json +++ b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json @@ -3,7 +3,7 @@ "fixture": "mini-repo", "totalFileCount": 7, "symbols": 41, - "relationships": 77, + "relationships": 80, "processes": 4, "byType": { "Class": 1, @@ -17,6 +17,7 @@ "Property": 8 }, "byRelType": { + "ACCESSES": 3, "CALLS": 9, "CONTAINS": 7, "DEFINES": 16, @@ -26,5 +27,5 @@ "MEMBER_OF": 12, "STEP_IN_PROCESS": 12 }, - "edgeDigest": "1ccfc9e5de04d7e71430bcfeb3ce61fe85135b826f33aad2b1bdd2ee193e4728" + "edgeDigest": "d617ad94554d760eb0497bf5365b7b0a2e21842ab5b16568def721b954aa1594" } diff --git a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts index 420aa7e8c..d896c68dc 100644 --- a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts +++ b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts @@ -73,8 +73,19 @@ describe('TypeScript type-alias and interface members (A4)', () => { // `fieldFallbackOnMethodLookup: false` (scope-resolver.ts) because name // matching over-connects in a typed language, and the unique-name pass // honors that opt-out. The precise path is the only route for TS, by design. - it.todo('links an alias field to its consumer'); - it.todo('links an interface field to its consumer'); + it('links an interface field to its consumer', () => { + expect(readersOf('ifaceSlots')).toContain('renderIface'); + }); - void readersOf; + // The ALIAS half still does not link, and the remaining blocker is now + // exact: resolving `cfg: LiveModeConfig` to its members requires the name + // `LiveModeConfig` to resolve to a CLASS-LIKE def, and `isClassLike` is + // Class|Interface|Struct|Record|Enum|Trait — no TypeAlias. That predicate is + // consulted from ~12 sites including MRO and heritage, and every language + // mints TypeAlias (Rust type_item, Kotlin/Swift/Dart typealias, C typedef), + // so widening it would enrol aliases in linearization where they do not + // belong. Widening only the scope index was tried and is NOT sufficient — + // the type-name walkers gate on it independently. Needs a deliberate + // "shape-like" concept rather than more call-site widening. + it.todo('links an alias field to its consumer'); });