mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
feat(scope-resolution): split shape-like from class-like, linking alias fields
Completes A4: a field on a TypeScript object-type alias now links to the code
that reads it, the last unanswerable half of "who breaks if I remove this?"
for a TS frontend that models contracts as `type X = { … }`.
`isClassLike` answered two questions that only coincide for classes:
1. does this declare MEMBERS I can look up? — a SHAPE (structural)
2. does this participate in inheritance / MRO? — a NOMINAL TYPE
An object-type alias is (1) and emphatically not (2) — it has no supertypes
and no place in a linearization. Widening `isClassLike` to buy (1) would have
enrolled every language's aliases (Rust type_item, Kotlin/Swift/Dart
typealias, C typedef) into MRO and heritage, so the two questions now get two
predicates. Call sites split by which they ask, and their names already said
which: `resolveInheritanceBaseInScope` and `resolveQualifiedInheritanceBase`
keep `isClassLike`; receiver typing and member OWNERSHIP take `isShapeLike`.
Three parts, each necessary and none sufficient alone:
- `findShapeBindingInScope`, mirroring `findValueBindingInScope`'s established
relationship to `findClassBindingInScope` (same walker, different accepted
def-type), consulted only AFTER the class lookup misses so a class of the
same name always wins.
- `populateClassOwnedMembers` uses it, so alias members get an `ownerId` and
are registered under the alias. Without this the receiver resolved to the
alias and then found no members under it.
- The TS scope query tags aliases `@declaration.type_alias`, not
`@declaration.type`: `normalizeNodeLabel` accepts typealias / type_alias and
has no "type" case, so the old tag mapped to NO label and TypeScript aliases
produced no scope-resolution def at all. Kotlin and Dart already spelled it
this way; TypeScript alone was on the dead tag.
An earlier attempt concluded a further "scope-attachment gate" existed. That
was wrong and is worth recording: scope extraction runs in the parse WORKER,
which loads built `dist`, so the retag was never executed. Rebuilt, the alias
hoists to Module scope exactly as the interface does. Same trap as the parse
query — `src` edits to anything the worker runs are invisible until
`npm run build`.
Typedef and Union stay out of `isShapeLike` deliberately: they belong
conceptually (the union_item note on MEMBER_OWNER_NODE_TYPES records the same
gap) but neither is wired as a member container, so including them would widen
a predicate nothing exercises.
Verified on the full resolver matrix: 3173 tests, golden unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a49d33a792
commit
1320cc6db3
4 changed files with 77 additions and 31 deletions
|
|
@ -162,8 +162,12 @@ export const TYPESCRIPT_SCOPE_QUERY = `
|
|||
(enum_declaration
|
||||
name: (identifier) @declaration.name) @declaration.enum
|
||||
|
||||
;; Tagged @declaration.type_alias, NOT @declaration.type: normalizeNodeLabel
|
||||
;; accepts typealias / type_alias and has no "type" case, so the old tag mapped
|
||||
;; to no label and TypeScript aliases produced NO scope-resolution def at all.
|
||||
;; Kotlin and Dart already spell it this way.
|
||||
(type_alias_declaration
|
||||
name: (type_identifier) @declaration.name) @declaration.type
|
||||
name: (type_identifier) @declaration.name) @declaration.type_alias
|
||||
|
||||
(internal_module
|
||||
name: (identifier) @declaration.name) @declaration.namespace
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
|||
import { collectNamespaceTargets } from '../scope/namespace-targets.js';
|
||||
import {
|
||||
findClassBindingInScope,
|
||||
findShapeBindingInScope,
|
||||
findEnclosingClassDef,
|
||||
isReceiverOwnedButUnbound,
|
||||
findExportedDef,
|
||||
|
|
@ -140,6 +141,16 @@ function resolveClassBindingForName(
|
|||
const direct = findClassBindingInScope(scopeId, rawClassName, scopes, stripDecoration);
|
||||
if (direct !== undefined) return direct;
|
||||
|
||||
// A receiver may be typed as an object-type ALIAS, which declares members
|
||||
// exactly as an interface does but is not class-like, so it binds nothing
|
||||
// above. Tried only after the class lookup misses, so a class of the same
|
||||
// name always wins and nothing that resolved before changes. Confined to
|
||||
// this helper on purpose: its two callers are member dispatch and an
|
||||
// origin-classifying diagnostic, never inheritance — see `isShapeLike` for
|
||||
// why the two questions must not share a predicate.
|
||||
const shape = findShapeBindingInScope(scopeId, rawClassName, scopes);
|
||||
if (shape !== undefined) return shape;
|
||||
|
||||
if (!rawClassName.includes('<')) return undefined;
|
||||
const baseName = stripTemplateArguments(rawClassName).replace(/\s+/g, '');
|
||||
if (baseName.length === 0) return undefined;
|
||||
|
|
|
|||
|
|
@ -175,6 +175,37 @@ export function isClassLike(t: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this label declare MEMBERS addressable by name?
|
||||
*
|
||||
* `isClassLike` answers two questions that only coincide for classes:
|
||||
* 1. does this declare members I can look up? — a SHAPE (structural)
|
||||
* 2. does this participate in inheritance / MRO? — a NOMINAL TYPE
|
||||
*
|
||||
* A TypeScript object-type alias answers YES to (1) and emphatically NO to
|
||||
* (2): it declares the same `property_signature` members as the interface
|
||||
* beside it, but has no supertypes and no place in a linearization. Answering
|
||||
* (2) "yes" merely to buy (1) is what widening `isClassLike` would do, and it
|
||||
* would enrol every language's aliases (Rust `type_item`, Kotlin/Swift/Dart
|
||||
* typealias, C `typedef`) into MRO and heritage.
|
||||
*
|
||||
* So the two questions get two predicates. Use THIS one where the question is
|
||||
* "find the shape so I can look up a member"; keep `isClassLike` where the
|
||||
* question is inheritance. The call sites announce which they are:
|
||||
* `resolveInheritanceBaseInScope` and `resolveQualifiedInheritanceBase` are
|
||||
* (2); receiver typing is (1).
|
||||
*
|
||||
* NOT YET INCLUDED, deliberately: `Typedef` and `Union`. They belong here
|
||||
* conceptually — the `union_item` note on `MEMBER_OWNER_NODE_TYPES` records
|
||||
* the same gap, that a union owns fields captured as `Property` yet is not a
|
||||
* recognized owner — but neither is wired as a member container today, so
|
||||
* adding them would widen a predicate nothing exercises. They join when their
|
||||
* containers do, with fixtures.
|
||||
*/
|
||||
export function isShapeLike(t: string): boolean {
|
||||
return isClassLike(t) || t === 'TypeAlias';
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the scope chain from `startScope` looking for a typeBinding
|
||||
* named `receiverName`. Returns the TypeRef or undefined if no binding
|
||||
|
|
@ -794,6 +825,24 @@ export function findValueBindingInScope(
|
|||
return walkScopeChain(startScope, receiverName, scopes, (def) => isOwnableValueLabel(def.type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a SHAPE binding (class-like, or an object-type alias) by name.
|
||||
*
|
||||
* Mirrors `findClassBindingInScope` exactly; only the accepted def-type
|
||||
* predicate differs — the same relationship `findValueBindingInScope` has to
|
||||
* it. Exists so a receiver typed as an object-type alias can reach that
|
||||
* alias's members WITHOUT the alias becoming eligible as an inheritance base:
|
||||
* `findClassBindingInScope` is what `resolveInheritanceBaseInScope` calls, so
|
||||
* widening that one would answer a question about hierarchies with a shape.
|
||||
*/
|
||||
export function findShapeBindingInScope(
|
||||
startScope: ScopeId,
|
||||
receiverName: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): SymbolDefinition | undefined {
|
||||
return walkScopeChain(startScope, receiverName, scopes, (def) => isShapeLike(def.type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic scope-chain walker. Walks from `startScope` toward the root,
|
||||
* consulting both the local `scope.bindings` channel and the dual-source
|
||||
|
|
@ -1139,13 +1188,19 @@ export function populateClassOwnedMembers(parsed: ParsedFile): void {
|
|||
// on `class U: def save(self): def helper(): ...` — helper.ownerId will
|
||||
// remain undefined. The theoretical concern is real only if the
|
||||
// extractor ever stops creating scopes for inner defs.
|
||||
// `isShapeLike`, not `isClassLike`: OWNERSHIP is question (1) — "which
|
||||
// declaration do these members belong to?" — and an object-type alias owns
|
||||
// members exactly as the interface beside it does. Without this its members
|
||||
// get no `ownerId`, so nothing is registered under the alias and a receiver
|
||||
// typed as one finds the owner but never its members. Inheritance/MRO keep
|
||||
// `isClassLike`; see the predicate's docstring.
|
||||
for (const scope of parsed.scopes) {
|
||||
// Methods: function scope whose parent is a Class scope. Owner is
|
||||
// the parent's class-like def.
|
||||
// the parent's shape def.
|
||||
if (scope.parent !== null) {
|
||||
const parentScope = scopesById.get(scope.parent);
|
||||
if (parentScope !== undefined && parentScope.kind === 'Class') {
|
||||
const classDef = parentScope.ownedDefs.find((d) => isClassLike(d.type));
|
||||
const classDef = parentScope.ownedDefs.find((d) => isShapeLike(d.type));
|
||||
if (classDef !== undefined) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
(def as { ownerId?: string }).ownerId = classDef.nodeId;
|
||||
|
|
@ -1157,7 +1212,7 @@ export function populateClassOwnedMembers(parsed: ParsedFile): void {
|
|||
// Class-body fields: defs directly owned by a Class scope (the
|
||||
// class-like def itself excluded).
|
||||
if (scope.kind === 'Class') {
|
||||
const classDef = scope.ownedDefs.find((d) => isClassLike(d.type));
|
||||
const classDef = scope.ownedDefs.find((d) => isShapeLike(d.type));
|
||||
if (classDef !== undefined) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def === classDef) continue;
|
||||
|
|
|
|||
|
|
@ -77,31 +77,7 @@ describe('TypeScript type-alias and interface members (A4)', () => {
|
|||
expect(readersOf('ifaceSlots')).toContain('renderIface');
|
||||
});
|
||||
|
||||
// ALIAS field -> consumer is still unlinked. Diagnosis, traced to the end so
|
||||
// the next attempt starts from facts rather than from this list again:
|
||||
//
|
||||
// 1. The graph side is COMPLETE and symmetric with the interface:
|
||||
// `Property:contracts.ts:LiveModeConfig.bookSlots` is owner-qualified
|
||||
// and carries `HAS_PROPERTY LiveModeConfig->bookSlots`.
|
||||
// 2. Resolution enters `resolveClassBindingForName('LiveModeConfig')`
|
||||
// (verified by instrumentation) and misses.
|
||||
// 3. It misses because the module scope binds `LiveModeIface:Interface`,
|
||||
// `renderAlias`, `renderIface` — and NOT `LiveModeConfig`. The alias has
|
||||
// no binding on the receiver's scope chain at all.
|
||||
// 4. The TS scope query tags aliases `@declaration.type`, but
|
||||
// `normalizeNodeLabel` accepts only `typealias` / `type_alias` and has
|
||||
// no `type` case, so it returns undefined. Kotlin and Dart use
|
||||
// `@declaration.type_alias`; TypeScript is alone on the dead tag.
|
||||
// 5. Retagging it to `@declaration.type_alias` is NECESSARY BUT NOT
|
||||
// SUFFICIENT — tried, and the binding still does not appear on the
|
||||
// chain, so a second gate exists in how a declaration anchored on a
|
||||
// node that is ALSO a `@scope.class` anchor gets attached (the alias
|
||||
// appears to bind inside its own scope instead of hoisting to Module,
|
||||
// where `interface_declaration` evidently does hoist).
|
||||
//
|
||||
// A widened predicate (`isShapeLike`) plus a mirrored
|
||||
// `findShapeBindingInScope` were also built and REVERTED: with no binding on
|
||||
// the chain they never fire, and shipping inert widening is worse than none.
|
||||
// Fix step 5 first; the rest is then a small, testable change.
|
||||
it.todo('links an alias field to its consumer');
|
||||
it('links an alias field to its consumer', () => {
|
||||
expect(readersOf('bookNotionalUsdt')).toContain('renderAlias');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue