mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
refactor(resolution): one elementTypeOf hook for both container routes (#2766)
The contract carried TWO near-identical hooks answering one question —
"element type of this container" — differing only in the syntactic route
taken to reach it:
unwrapCollectionAccessor property view (`dict.Values`), C# only
unwrapCollectionElement subscript (`repos[0]`), TypeScript only
So a language implementing one silently got nothing for the other: C#
parsed `Dictionary<K,V>` for `.Values` but returned nothing for
`list[0]`, and TypeScript did the reverse — for no principled reason,
just because each hook was added by whoever needed that route. Two
entries that would each accrete one implementation per language.
Collapsed to `elementTypeOf(containerType, via)` where `via` is
`{kind:'index'}` or `{kind:'accessor', name}`. C#'s existing dictionary
parser now serves the subscript route too, correctly yielding the VALUE
type — `dict["k"]` is a V, never a K. TypeScript answers the index route
and declines the accessor route, because it spells collection views as
method calls that the call-expression branch already handles.
Honest scope note: this did NOT flip C#'s `indexElement` bench cell. That
gap has a different, undiagnosed cause. What this fixes is the structural
duplication and C#'s inability to answer the index route at all — not
that cell.
Still consulted ONLY where the source performed the access. It is not a
general type normalizer: unwrapping a container at a bare class lookup
would let `repos.find(x)` fold to `Repo.find`, since a container's member
set is not its element's.
Shape matrix unchanged at 55/23/18. 4366 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a8cec54789
commit
5a89747c96
6 changed files with 69 additions and 62 deletions
|
|
@ -1,16 +1,18 @@
|
|||
import type { ElementAccessRoute } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { extractElementTypeFromString } from '../../type-extractors/shared.js';
|
||||
|
||||
/**
|
||||
* C# collection-accessor unwrapping.
|
||||
* C# container element-type unwrapping.
|
||||
*
|
||||
* When the compound-receiver resolver encounters a trailing
|
||||
* `.Values` / `.Keys` on a dotted member-access chain, it calls the
|
||||
* provider's `unwrapCollectionAccessor` hook to find the element
|
||||
* provider's `elementTypeOf` hook to find the element
|
||||
* type. This module supplies the C# implementation — recognizing
|
||||
* Dictionary-family generics and returning the value or key type.
|
||||
*
|
||||
* Other languages (Python, Java, TypeScript) use method-call syntax
|
||||
* for the same access (`.values()` / `.keys()`), which the compound-
|
||||
* receiver's call-expression branch already handles; they leave this
|
||||
* hook undefined.
|
||||
* Other languages (Python, Java, TypeScript) use method-call syntax for the
|
||||
* same access (`.values()` / `.keys()`), which the compound-receiver's
|
||||
* call-expression branch already handles; they answer only the `index` route.
|
||||
*/
|
||||
|
||||
/** Extract (K, V) from `Dictionary<K, V>` / `IDictionary<K, V>` /
|
||||
|
|
@ -46,12 +48,19 @@ function extractDictionaryArgs(rawName: string): { key: string; value: string }
|
|||
* receiver / accessor combination we don't recognize, letting the
|
||||
* compound-receiver pass fall through to the regular field walk.
|
||||
*/
|
||||
export function unwrapCsharpCollectionAccessor(
|
||||
receiverType: string,
|
||||
accessor: string,
|
||||
export function unwrapCsharpElementType(
|
||||
containerType: string,
|
||||
via: ElementAccessRoute,
|
||||
): string | undefined {
|
||||
if (accessor !== 'Values' && accessor !== 'Keys') return undefined;
|
||||
const args = extractDictionaryArgs(receiverType);
|
||||
const args = extractDictionaryArgs(containerType);
|
||||
// Subscript on a dictionary yields the VALUE type — `dict["k"]` is a V, never
|
||||
// a K. Previously this route returned nothing at all for C#, because the
|
||||
// dictionary parse below was reachable only from the accessor hook.
|
||||
if (via.kind === 'index') {
|
||||
if (args !== undefined) return args.value;
|
||||
return extractElementTypeFromString(containerType);
|
||||
}
|
||||
if (via.name !== 'Values' && via.name !== 'Keys') return undefined;
|
||||
if (args === undefined) return undefined;
|
||||
return accessor === 'Values' ? args.value : args.key;
|
||||
return via.name === 'Values' ? args.value : args.key;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from './index.js';
|
||||
import { populateCsharpNamespaceSiblings } from './namespace-siblings.js';
|
||||
import { loadCsharpResolutionConfig, type CsharpResolutionConfig } from './resolution-config.js';
|
||||
import { unwrapCsharpCollectionAccessor } from './accessor-unwrap.js';
|
||||
import { unwrapCsharpElementType } from './accessor-unwrap.js';
|
||||
|
||||
const csharpScopeResolver: ScopeResolver = {
|
||||
// Construction is keyword-prefixed: `new Service(db).doWork()` (#2708).
|
||||
|
|
@ -89,7 +89,7 @@ const csharpScopeResolver: ScopeResolver = {
|
|||
// `data.Values` / `data.Keys` on Dictionary-like receivers unwrap
|
||||
// to the value / key element type. Other languages use method-call
|
||||
// syntax for the same access and leave this hook undefined.
|
||||
unwrapCollectionAccessor: unwrapCsharpCollectionAccessor,
|
||||
elementTypeOf: unwrapCsharpElementType,
|
||||
|
||||
// C# matches legacy DAG by collapsing member-call CALLS edges to
|
||||
// `(caller, target)` — multiple `g.Greet(...)` sites from Main
|
||||
|
|
|
|||
|
|
@ -107,17 +107,12 @@ function makeTsResolveImportTarget(): ScopeResolver['resolveImportTarget'] {
|
|||
}
|
||||
|
||||
const typescriptScopeResolver: ScopeResolver = {
|
||||
// Collections only, consulted ONLY by an index step (see the contract field).
|
||||
// Delegates to the shared, bracket-balanced extractor rather than a local
|
||||
// regex: that helper is already used by seven language type-extractors, and it
|
||||
// covers `Map<K,V>` / `Record<K,V>` (returning the VALUE type, which is what a
|
||||
// subscript yields) where a hand-rolled single-arg regex returned undefined
|
||||
// and made `cache["k"].save()` decline.
|
||||
//
|
||||
// Deliberately NOT part of `stripTypePreservingDecoration`: a container
|
||||
// changes the member set, so unwrapping it at the bare class lookup would let
|
||||
// `repos.find(x)` fold to `User.find`.
|
||||
unwrapCollectionElement: (typeName) => extractElementTypeFromString(typeName),
|
||||
// One hook, both routes. TypeScript exposes collection views as METHOD calls
|
||||
// (`.values()`), which the call-expression branch already handles, so the
|
||||
// accessor route yields nothing here — but it is now the same hook rather
|
||||
// than a second one left undefined.
|
||||
elementTypeOf: (containerType, via) =>
|
||||
via.kind === 'index' ? extractElementTypeFromString(containerType) : undefined,
|
||||
|
||||
// Construction is keyword-prefixed: `new Service(db).doWork()` (#2708).
|
||||
constructionSyntax: { keyword: 'new' },
|
||||
|
|
@ -168,10 +163,10 @@ const typescriptScopeResolver: ScopeResolver = {
|
|||
fieldFallbackOnMethodLookup: false,
|
||||
propagatesReturnTypesAcrossImports: true,
|
||||
|
||||
// TypeScript uses `.values()` / `.keys()` method-call syntax for
|
||||
// collection views -- no property-style accessors like C#'s
|
||||
// `Dictionary<K,V>.Values`. Leave `unwrapCollectionAccessor`
|
||||
// undefined and let the regular member-call branch handle them.
|
||||
// TypeScript uses `.values()` / `.keys()` method-call syntax for collection
|
||||
// views -- no property-style accessors like C#'s `Dictionary<K,V>.Values` --
|
||||
// so `elementTypeOf` answers only the `index` route and lets the regular
|
||||
// member-call branch handle the rest.
|
||||
//
|
||||
// `collapseMemberCallsByCallerTarget` left undefined (= false) --
|
||||
// TypeScript legacy DAG emits one edge per call site, so
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* - propagatesReturnTypesAcrossImports (default true)
|
||||
* - fieldFallbackOnMethodLookup (default true — turn OFF for
|
||||
* statically-typed languages; the heuristic over-connects)
|
||||
* - unwrapCollectionAccessor — property-style collection views
|
||||
* - elementTypeOf — container element type, by subscript or accessor
|
||||
* - collapseMemberCallsByCallerTarget — one edge per caller/target
|
||||
* - populateNamespaceSiblings — cross-file implicit visibility
|
||||
* - hoistTypeBindingsToModule — enable ONLY when method return
|
||||
|
|
@ -314,6 +314,11 @@ export interface ImportResolutionContext {
|
|||
* `RegistryProviders.constraintCompatibility`'s third parameter. */
|
||||
export type { ConstraintContext } from 'gitnexus-shared';
|
||||
|
||||
/** How a container's element was reached in the source. */
|
||||
export type ElementAccessRoute =
|
||||
| { readonly kind: 'index' }
|
||||
| { readonly kind: 'accessor'; readonly name: string };
|
||||
|
||||
export interface ScopeResolver {
|
||||
/** Identity for telemetry + per-language flag check. */
|
||||
readonly language: SupportedLanguages;
|
||||
|
|
@ -709,22 +714,28 @@ export interface ScopeResolver {
|
|||
readonly fieldFallbackOnMethodLookup?: boolean;
|
||||
|
||||
/**
|
||||
* Unwrap a property-style collection accessor on a typed receiver
|
||||
* to its element type. Called by `resolveCompoundReceiverClass`
|
||||
* when walking dotted member-access chains of the form
|
||||
* `receiver.Accessor`. The provider returns the element type's
|
||||
* simple name, or `undefined` when the accessor doesn't unwrap —
|
||||
* in which case the regular field-walk resumes.
|
||||
* Element type of a container, reached either by a subscript (`repos[0]`) or
|
||||
* by a property-style collection view (`dict.Values`). Returns the element
|
||||
* type's simple name, or `undefined` when the container does not unwrap by
|
||||
* that route — in which case the caller resumes its normal walk.
|
||||
*
|
||||
* Use this only for languages that expose collection views as
|
||||
* properties rather than method calls; languages whose collection
|
||||
* views are `.values()` / `.keys()` method calls leave this
|
||||
* undefined and let the normal call-expression branch handle them.
|
||||
* ONE hook for both routes, deliberately. They were previously two
|
||||
* (`unwrapCollectionAccessor` for the property route, `unwrapCollectionElement`
|
||||
* for the subscript route), which meant a language implementing one silently
|
||||
* got nothing for the other: C# parsed `Dictionary<K,V>` for `.Values` but
|
||||
* returned nothing for `list[0]`, and TypeScript did the reverse. Two entries
|
||||
* answering one question, each accreting an implementation per language.
|
||||
*
|
||||
* `via` carries the route so a provider can distinguish them where it matters
|
||||
* (a `Dictionary` yields its VALUE type by subscript but either type by
|
||||
* accessor name); a provider that does not care can ignore it.
|
||||
*
|
||||
* Consulted ONLY where the source actually performed the access. It is NOT a
|
||||
* general type-name normalizer: unwrapping a container at a bare class lookup
|
||||
* would let `repos.find(x)` fold to `Repo.find`, because a container's member
|
||||
* set is not its element's.
|
||||
*/
|
||||
readonly unwrapCollectionAccessor?: (
|
||||
receiverType: string,
|
||||
accessor: string,
|
||||
) => string | undefined;
|
||||
readonly elementTypeOf?: (containerType: string, via: ElementAccessRoute) => string | undefined;
|
||||
|
||||
/**
|
||||
* Collapse member-call CALLS edges by `(caller, target)` rather
|
||||
|
|
@ -1167,7 +1178,6 @@ export interface ScopeResolver {
|
|||
* both kinds of path land on the element type without the core needing to know
|
||||
* which is which.
|
||||
*/
|
||||
readonly unwrapCollectionElement?: DecorationStripper;
|
||||
|
||||
/**
|
||||
* Whether the compound-receiver resolver should strip C-style cast
|
||||
|
|
@ -1195,7 +1205,7 @@ export interface ScopeResolver {
|
|||
*
|
||||
* A second opting language must extend the classifier grammar or
|
||||
* convert this toggle into a per-language classifier hook (the
|
||||
* `unwrapCollectionAccessor` pattern) — do not flip this flag for
|
||||
* `elementTypeOf` pattern) — do not flip this flag for
|
||||
* another language as-is.
|
||||
*
|
||||
* Known non-goal: the compound-receiver options built from this
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
*/
|
||||
|
||||
import type { ScopeId, SymbolDefinition, TypeRef } from 'gitnexus-shared';
|
||||
import type { ScopeResolver } from '../contract/scope-resolver.js';
|
||||
import type { ElementAccessRoute, ScopeResolver } from '../contract/scope-resolver.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
||||
import { stripTemplateArguments } from '../../utils/template-arguments.js';
|
||||
|
|
@ -82,10 +82,9 @@ interface ResolveCompoundReceiverOptions {
|
|||
* Dictionary<K,V>-typed receiver yields V (C#), etc. Returns the
|
||||
* element type's simple name, or `undefined` to let the regular
|
||||
* field-walk handle the access. */
|
||||
readonly unwrapCollectionAccessor?: (
|
||||
receiverType: string,
|
||||
accessor: string,
|
||||
) => string | undefined;
|
||||
/** Container -> element, by subscript or accessor. See the `ScopeResolver`
|
||||
* field of the same name for why the two routes share one hook. */
|
||||
readonly elementTypeOf?: (containerType: string, via: ElementAccessRoute) => string | undefined;
|
||||
/** Walk up from the class scope to ancestor (Module) scopes when
|
||||
* looking up a method's return-type typeBinding. Only enable for
|
||||
* languages that hoist return-type bindings to Module scope (C#);
|
||||
|
|
@ -129,10 +128,6 @@ interface ResolveCompoundReceiverOptions {
|
|||
* by the shared lookup's other callers — see the contract's own note on why
|
||||
* this is opt-in rather than global. */
|
||||
readonly stripTypePreservingDecoration?: DecorationStripper;
|
||||
/** Collection -> element unwrap, consulted ONLY by an index step. See the
|
||||
* `ScopeResolver` field of the same name for why this is separate from the
|
||||
* type-preserving stripper. */
|
||||
readonly unwrapCollectionElement?: DecorationStripper;
|
||||
}
|
||||
|
||||
/** Is this hop the language's construction selector applied to the class
|
||||
|
|
@ -508,7 +503,7 @@ export function foldReceiverChain(
|
|||
// annotation) needs exactly one unwrap here.
|
||||
const declared = current.declaredTypeName;
|
||||
const element =
|
||||
declared === undefined ? undefined : options.unwrapCollectionElement?.(declared);
|
||||
declared === undefined ? undefined : options.elementTypeOf?.(declared, { kind: 'index' });
|
||||
if (element !== undefined) {
|
||||
const scopeForLookup = current.declaredAtScope ?? inScope;
|
||||
const elementClass = findClassBindingInScope(
|
||||
|
|
@ -884,7 +879,7 @@ export function resolveCompoundReceiverClass(
|
|||
// the final segment and unwraps the receiver's generic, return
|
||||
// the element class directly. Resolved before the field-walk
|
||||
// because Dictionary-family types aren't local class defs.
|
||||
if (options.unwrapCollectionAccessor !== undefined && parts.length >= 2) {
|
||||
if (options.elementTypeOf !== undefined && parts.length >= 2) {
|
||||
const last = parts[parts.length - 1];
|
||||
const headInner = parts[0];
|
||||
if (last === undefined || headInner === undefined) return undefined;
|
||||
|
|
@ -912,7 +907,7 @@ export function resolveCompoundReceiverClass(
|
|||
prefixType = cur;
|
||||
}
|
||||
if (prefixType !== undefined) {
|
||||
const elemName = options.unwrapCollectionAccessor(prefixType.rawName, last);
|
||||
const elemName = options.elementTypeOf(prefixType.rawName, { kind: 'accessor', name: last });
|
||||
if (elemName !== undefined) {
|
||||
return findClassBindingInScope(prefixType.declaredAtScope, elemName, scopes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,12 +94,11 @@ type ReceiverBoundProviderSubset = Pick<
|
|||
| 'isSuperReceiverInContext'
|
||||
| 'fieldFallbackOnMethodLookup'
|
||||
| 'collapseMemberCallsByCallerTarget'
|
||||
| 'unwrapCollectionAccessor'
|
||||
| 'elementTypeOf'
|
||||
| 'hoistTypeBindingsToModule'
|
||||
| 'stripReceiverCastExpressions'
|
||||
| 'constructionSyntax'
|
||||
| 'stripTypePreservingDecoration'
|
||||
| 'unwrapCollectionElement'
|
||||
| 'resolveQualifiedReceiverMember'
|
||||
| 'resolveReceiverMember'
|
||||
| 'resolveThisViaEnclosingClass'
|
||||
|
|
@ -184,12 +183,11 @@ export function emitReceiverBoundCalls(
|
|||
const hoistTypeBindingsToModule = provider.hoistTypeBindingsToModule === true;
|
||||
const compoundOpts = {
|
||||
fieldFallback,
|
||||
unwrapCollectionAccessor: provider.unwrapCollectionAccessor,
|
||||
elementTypeOf: provider.elementTypeOf,
|
||||
hoistTypeBindingsToModule,
|
||||
stripReceiverCastExpressions: provider.stripReceiverCastExpressions === true,
|
||||
constructionSyntax: provider.constructionSyntax,
|
||||
stripTypePreservingDecoration: provider.stripTypePreservingDecoration,
|
||||
unwrapCollectionElement: provider.unwrapCollectionElement,
|
||||
};
|
||||
|
||||
// Build an interface → implementors map from IMPLEMENTS edges.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue