mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
fix(resolution): type Go pointer-receiver bases at the class lookup (#2766)
A Go method with a pointer receiver binds its receiver to the literal string `*Holder` — `synthesizeGoReceiverBinding` stores `typeNode.text` raw. `findClassBindingInScope` normalizes exactly one decoration, a dotted qualifier, so `*Holder` matched nothing: the scope walk missed, the qualified-name index missed, and the dotted-tail fallback never fired because there is no dot. Receiver typing declined at the BASE, so every `h.field.Method()` in a pointer-receiver method — the dominant Go idiom — emitted no CALLS edge. The reporter measured 178 handler and 585 service call sites lost on a 470k LOC codebase. The defect is the base ALONE. Go already normalizes field type bindings at capture through `normalizeGoTypeName`, so the step lookup was always sound. Isolating the two independently proves it: value receiver + value field resolves, value receiver + POINTER field resolves, pointer receiver + value field does not. Adds an OPT-IN decoration fallback to the class lookup, consulted only after every undecorated branch has failed: - `findAllClassBindingsInScope` enumerates every class-like candidate from both the scope chain and the qualified-name index. Needed because `walkScopeChain` returns the FIRST match and structurally cannot report a collision, so widening what a name matches without it would pick the nearest of several and mint a confident wrong edge. It stops at the first scope that binds the name, so an inner binding shadowing an outer one is not misreported as ambiguity. - The fallback strips one layer at a time and requires exactly ONE surviving nodeId, or it declines. - `stripTypePreservingDecoration` on the ScopeResolver contract carries the per-language vocabulary, so the core names no language (AGENTS.md R6). Go strips `*` only — `[]` and `map[…]` are CONTAINERS whose member set differs from the element's, and stripping one would let `repos: Repo[]` fold `repos.find(x)` to `Repo.find`. Those are unwrapped only by an index step that consumed a subscript. Opt-in rather than global because ~two dozen call sites are shaped `findClassBindingInScope(...) ?? otherResolver(...)`: turning a former `undefined` into a hit SUPPRESSES the fallback that used to answer, which would retarget inheritance edges and bypass generic-specialization selection. Only receiver-chain base and step resolution opts in. The stored `*T` binding is left decorated — `method-owners.ts` consumes the `*T` vs `T` distinction to model Go's value and pointer method sets, so this normalizes at LOOKUP, never by rewriting the binding. Verification: - bench cell `go.decoratedReceiverBase` VISIBLE-GAP -> RESOLVES, and it is the ONLY cell that moved of 164. - #2766's reproduction goes from 4 CALLS edges to 10; cross-package interface field, cross-package concrete field and same-package field all resolve. - Regression test proven to fail without the fix: with the stripper disabled the 3 pointer-receiver assertions fail while both controls (local-variable receiver, value receiver) still pass — so the test targets the changed line and the fix adds edges rather than moving one. - Full resolver suite green (2988 tests). Baseline movement, both fixture-corpus growth rather than code: receiver-resolution callDrops unchanged at 101; scope-capture go fingerprint rebaselined with a documented reason, and go was the only language of 15 that drifted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b442e04bf4
commit
809d2eb63b
10 changed files with 283 additions and 5 deletions
|
|
@ -73,7 +73,7 @@
|
|||
"explicitTypeArgs": "N/A",
|
||||
"indexElement": "INVISIBLE-GAP",
|
||||
"fieldReceiverCall": "RESOLVES",
|
||||
"decoratedReceiverBase": "VISIBLE-GAP",
|
||||
"decoratedReceiverBase": "RESOLVES",
|
||||
"decoratedFieldType": "RESOLVES"
|
||||
},
|
||||
"javascript": {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/<lang>-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. The synthetic source is now HERITAGE-BEARING for every language (each Entity extends/implements/embeds/uses-trait/conforms-to a shared base) so the #1951 @reference.inherits synth is gated at scale, not just the base capture loop. All languages thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp #1951, java #1956).",
|
||||
"go": {
|
||||
"fingerprint": "5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb",
|
||||
"fingerprint": "8cba537ff211fab3bac5fb4456cd1ffba14d6a2db75c40acae28ab8bf29f3d2e",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a -> 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a; scaling 1.058 < 1.5.",
|
||||
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: provider-owned callable assignment/copy/formal/argument/invoke facts with invocation/constructor-result suppression. Prior 09ecd94911b830f52fa8807560abcbd79f163d02a2072870c1a59297e9a326e1 -> 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a; scaling 1.039 < 1.5.",
|
||||
"_rebaselined": "#1976: F33 generic composite literal constructor inference adds generic_type captures in composite_literal patterns; fingerprint drift expected.",
|
||||
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a -> 5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb."
|
||||
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a -> 5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb.",
|
||||
"_rebaselined_2766_go_pointer_receiver_fixture": "#2766: added test/fixtures/lang-resolution/go-pointer-receiver-field-chain/ (2 Go files) as the committed regression fixture for pointer-receiver base resolution. Go fixture_count 100 -> 102. Prior 5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb -> 8cba537ff211fab3bac5fb4456cd1ffba14d6a2db75c40acae28ab8bf29f3d2e. FIXTURE-CORPUS GROWTH, NOT A CAPTURE CHANGE: the accompanying fix is a resolution-time lookup fallback (stripTypePreservingDecoration) and cannot move capture output; go was the ONLY language whose fingerprint drifted, and every other language matched its baseline on the same run."
|
||||
},
|
||||
"cobol": {
|
||||
"fingerprint": "d45bb091b0893d0de4fae2486b31ba21719c9377bf35a0908fd3a36fa1c3bf4e",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,17 @@ export const goScopeResolver: ScopeResolver = {
|
|||
|
||||
arityCompatibility: (callsite, def) => goArityCompatibility(def, callsite),
|
||||
|
||||
// Only `*` — a pointer leaves the method set reachable by selector unchanged,
|
||||
// so `*Host` and `Host` name the same class for receiver typing. `[]` and
|
||||
// `map[…]` are deliberately NOT stripped here: they are containers whose
|
||||
// member set differs from the element's, and unwrapping them belongs to the
|
||||
// index step that consumed a subscript. (Field bindings never reach this
|
||||
// anyway — `normalizeGoTypeName` already strips them at capture. The one
|
||||
// binding that arrives decorated is the receiver self-binding, kept raw on
|
||||
// purpose for `method-owners.ts`.)
|
||||
stripTypePreservingDecoration: (typeName) =>
|
||||
typeName.startsWith('*') ? typeName.slice(1).trim() : undefined,
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
|
||||
|
|
|
|||
|
|
@ -1114,6 +1114,39 @@ export interface ScopeResolver {
|
|||
*/
|
||||
readonly hoistTypeBindingsToModule?: boolean;
|
||||
|
||||
/**
|
||||
* Strip ONE layer of type-preserving decoration off a declared type name,
|
||||
* or return `undefined` when there is nothing left to strip.
|
||||
*
|
||||
* Exists because a declared type is stored as written. Go's
|
||||
* `synthesizeGoReceiverBinding` keeps `typeNode.text`, so a pointer-receiver
|
||||
* method binds its receiver to the literal `*Host` — which matches no class
|
||||
* binding, so receiver-chain resolution declines at the base and every
|
||||
* `h.field.method()` in the dominant Go idiom loses its `CALLS` edge (#2766).
|
||||
* The stored binding is deliberately left decorated (`method-owners.ts`
|
||||
* consumes `*T` vs `T` to model Go's value and pointer method sets), so the
|
||||
* normalization belongs at LOOKUP, never as a rewrite of the binding.
|
||||
*
|
||||
* TYPE-PRESERVING ONLY. Pointer, reference, `const`, nullable, borrow,
|
||||
* deref-transparent smart pointer and sigil all leave the member set
|
||||
* unchanged. A CONTAINER — array, slice, map, `Option` — does not: stripping
|
||||
* one here would type `repos: Repo[]` as `Repo` and let `repos.find(x)` fold
|
||||
* to `Repo.find`, a confident wrong edge the ambiguity gate cannot catch
|
||||
* because `Repo` binds uniquely. Containers are unwrapped only by an index
|
||||
* step that consumed a subscript.
|
||||
*
|
||||
* Consulted ONLY after every undecorated lookup has failed, and only by
|
||||
* receiver-chain base and step resolution — the shared class lookup keeps
|
||||
* exact-name behaviour for its other ~two dozen callers, several of which are
|
||||
* shaped `findClassBindingInScope(...) ?? otherResolver(...)` and would have
|
||||
* their fallback suppressed by a global widening.
|
||||
*
|
||||
* Leave undefined for languages whose declared types carry no type-preserving
|
||||
* decoration. Measured: only Go needs it for a receiver base; Rust, C#, Swift,
|
||||
* TypeScript and C++ need it for field types.
|
||||
*/
|
||||
readonly stripTypePreservingDecoration?: (typeName: string) => string | undefined;
|
||||
|
||||
/**
|
||||
* Whether the compound-receiver resolver should strip C-style cast
|
||||
* expressions from receiver-position text before resolving it —
|
||||
|
|
|
|||
|
|
@ -121,6 +121,13 @@ interface ResolveCompoundReceiverOptions {
|
|||
* (`const Config = make(1); Config.db.query()` emitted `entry → Database.query`),
|
||||
* the exact wrong-edge failure this work exists to avoid. */
|
||||
readonly strictBaseBinding?: boolean;
|
||||
/** Per-language type-preserving decoration stripper, from the `ScopeResolver`
|
||||
* contract. Passed to the class lookup at the base and step sites so a
|
||||
* decorated declared type (`*Host`) resolves to its class. Absent for
|
||||
* languages whose declared types carry no such decoration, and never applied
|
||||
* by the shared lookup's other callers — see the contract's own note on why
|
||||
* this is opt-in rather than global. */
|
||||
readonly stripTypePreservingDecoration?: (typeName: string) => string | undefined;
|
||||
}
|
||||
|
||||
/** Is this hop the language's construction selector applied to the class
|
||||
|
|
@ -304,7 +311,12 @@ function typeOfMemberOnClass(
|
|||
const classScope = classScopeByDefId.get(ownerId);
|
||||
const memberType = classScope?.typeBindings.get(memberName);
|
||||
if (memberType !== undefined) {
|
||||
return findClassBindingInScope(memberType.declaredAtScope, memberType.rawName, scopes);
|
||||
return findClassBindingInScope(
|
||||
memberType.declaredAtScope,
|
||||
memberType.rawName,
|
||||
scopes,
|
||||
options.stripTypePreservingDecoration,
|
||||
);
|
||||
}
|
||||
// Languages whose binding-scope hook hoists a method's return-type binding
|
||||
// out of the class body and onto an ancestor (Module) scope keep NOTHING in
|
||||
|
|
@ -480,7 +492,12 @@ export function resolveCompoundReceiverClass(
|
|||
return findClassBindingInScope(rhsTb.declaredAtScope, arg, scopes);
|
||||
}
|
||||
|
||||
const viaTb = findClassBindingInScope(tb.declaredAtScope, tb.rawName, scopes);
|
||||
const viaTb = findClassBindingInScope(
|
||||
tb.declaredAtScope,
|
||||
tb.rawName,
|
||||
scopes,
|
||||
options.stripTypePreservingDecoration,
|
||||
);
|
||||
if (viaTb !== undefined) return viaTb;
|
||||
|
||||
// Member-alias / call-result shapes store the RHS path on rawName
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ type ReceiverBoundProviderSubset = Pick<
|
|||
| 'hoistTypeBindingsToModule'
|
||||
| 'stripReceiverCastExpressions'
|
||||
| 'constructionSyntax'
|
||||
| 'stripTypePreservingDecoration'
|
||||
| 'resolveQualifiedReceiverMember'
|
||||
| 'resolveReceiverMember'
|
||||
| 'resolveThisViaEnclosingClass'
|
||||
|
|
@ -184,6 +185,7 @@ export function emitReceiverBoundCalls(
|
|||
hoistTypeBindingsToModule,
|
||||
stripReceiverCastExpressions: provider.stripReceiverCastExpressions === true,
|
||||
constructionSyntax: provider.constructionSyntax,
|
||||
stripTypePreservingDecoration: provider.stripTypePreservingDecoration,
|
||||
};
|
||||
|
||||
// Build an interface → implementors map from IMPLEMENTS edges.
|
||||
|
|
|
|||
|
|
@ -321,10 +321,91 @@ export function moduleScopeIdOf(
|
|||
*
|
||||
* Without (2) we'd miss every cross-file class-receiver call.
|
||||
*/
|
||||
/**
|
||||
* Every class-like definition visible for `name`, from the scope chain AND the
|
||||
* qualified-name index, deduped by `nodeId`.
|
||||
*
|
||||
* Exists because `walkScopeChain` returns the FIRST match and cannot report a
|
||||
* collision, so a caller that widens what a name can match (the decoration
|
||||
* normalizer below) has no way to tell "one answer" from "picked the nearest of
|
||||
* several". Mirrors `findAllCallableBindingsInScope`, which solved the same
|
||||
* problem for callables.
|
||||
*/
|
||||
export function findAllClassBindingsInScope(
|
||||
startScope: ScopeId,
|
||||
name: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): readonly SymbolDefinition[] {
|
||||
const byNodeId = new Map<string, SymbolDefinition>();
|
||||
let currentId: ScopeId | null = startScope;
|
||||
const visited = new Set<ScopeId>();
|
||||
while (currentId !== null) {
|
||||
if (visited.has(currentId)) break;
|
||||
visited.add(currentId);
|
||||
const scope = scopes.scopeTree.getScope(currentId);
|
||||
if (scope === undefined) break;
|
||||
|
||||
// `Object` scopes are a hoist boundary only — see walkScopeChain (#2545).
|
||||
if (scope.kind !== 'Object') {
|
||||
const found: SymbolDefinition[] = [];
|
||||
for (const b of scope.bindings.get(name) ?? []) {
|
||||
if (isClassLike(b.def.type)) found.push(b.def);
|
||||
}
|
||||
for (const b of lookupBindingsAt(currentId, name, scopes)) {
|
||||
if (isClassLike(b.def.type)) found.push(b.def);
|
||||
}
|
||||
// Stop at the first scope that binds the name at all: an inner binding
|
||||
// SHADOWS an outer one, so continuing would report a shadowed outer
|
||||
// definition as a competing candidate and decline a name that is actually
|
||||
// unambiguous at this point.
|
||||
if (found.length > 0) {
|
||||
for (const def of found) byNodeId.set(def.nodeId, def);
|
||||
return [...byNodeId.values()];
|
||||
}
|
||||
}
|
||||
currentId = scope.parent;
|
||||
}
|
||||
|
||||
for (const id of scopes.qualifiedNames.get(name)) {
|
||||
const def = scopes.defs.get(id);
|
||||
if (def !== undefined && isClassLike(def.type)) byNodeId.set(def.nodeId, def);
|
||||
}
|
||||
return [...byNodeId.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip one layer of type-preserving decoration off a declared type name, or
|
||||
* `undefined` when there is nothing left to strip. Supplied per language through
|
||||
* the `ScopeResolver` contract; the core never names a language (AGENTS.md R6).
|
||||
*
|
||||
* TYPE-PRESERVING only — pointer, reference, `const`, nullable, borrow,
|
||||
* deref-transparent smart pointer, sigil. A CONTAINER (array, slice, map,
|
||||
* `Option`) changes the member set, so stripping one here would type
|
||||
* `repos: Repo[]` as `Repo` and let `repos.find(x)` fold to `Repo.find`. Those
|
||||
* are unwrapped only by an index step that consumed a subscript.
|
||||
*/
|
||||
export type DecorationStripper = (typeName: string) => string | undefined;
|
||||
|
||||
/** Bounded so a pathological stripper cannot spin. Real decoration nests
|
||||
* shallowly (`*[]T`, `const T&`); three layers is generous. */
|
||||
const MAX_DECORATION_LAYERS = 3;
|
||||
|
||||
export function findClassBindingInScope(
|
||||
startScope: ScopeId,
|
||||
receiverName: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
/**
|
||||
* OPT-IN. When supplied, a name that binds nothing is retried with decoration
|
||||
* stripped one layer at a time, and each retry must resolve to exactly ONE
|
||||
* class-like definition or it declines.
|
||||
*
|
||||
* Opt-in rather than global because roughly two dozen call sites use the shape
|
||||
* `findClassBindingInScope(...) ?? otherResolver(...)`: turning a former
|
||||
* `undefined` into a hit SUPPRESSES the fallback that used to answer, which
|
||||
* would retarget inheritance edges and bypass generic-specialization
|
||||
* selection. Only receiver-chain base and step resolution passes this.
|
||||
*/
|
||||
stripDecoration?: DecorationStripper,
|
||||
): SymbolDefinition | undefined {
|
||||
const local = walkScopeChain(startScope, receiverName, scopes, (def) => isClassLike(def.type));
|
||||
if (local !== undefined) return local;
|
||||
|
|
@ -350,6 +431,25 @@ export function findClassBindingInScope(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decoration fallback (opt-in). Every branch above works on the name exactly
|
||||
// as written; only when none of them bound anything do we consider that the
|
||||
// name may be a decorated spelling of one that would.
|
||||
if (stripDecoration !== undefined) {
|
||||
let current = receiverName;
|
||||
for (let layer = 0; layer < MAX_DECORATION_LAYERS; layer++) {
|
||||
const stripped = stripDecoration(current);
|
||||
if (stripped === undefined || stripped === current || stripped.length === 0) break;
|
||||
current = stripped;
|
||||
const candidates = findAllClassBindingsInScope(startScope, current, scopes);
|
||||
// Exactly one, or decline. Two same-named classes reachable from here mean
|
||||
// the decoration was carrying the only disambiguating information, and
|
||||
// picking the nearest would mint a confident wrong edge — the failure this
|
||||
// whole line of work exists to avoid. A missing edge is recoverable.
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
if (candidates.length > 1) return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
54
gitnexus/test/fixtures/lang-resolution/go-pointer-receiver-field-chain/handlers/handler.go
vendored
Normal file
54
gitnexus/test/fixtures/lang-resolution/go-pointer-receiver-field-chain/handlers/handler.go
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Regression fixture for #2766.
|
||||
//
|
||||
// A Go method with a POINTER receiver binds its receiver to the literal string
|
||||
// `*Holder` (synthesizeGoReceiverBinding stores typeNode.text raw, deliberately,
|
||||
// because method-owners.ts consumes the `*T` vs `T` distinction to model Go's
|
||||
// value and pointer method sets). Before the decoration fallback in
|
||||
// findClassBindingInScope, that string matched no class binding, so receiver
|
||||
// typing declined at the BASE and every `h.field.Method()` here emitted no CALLS
|
||||
// edge — the dominant Go idiom, silently missing from the graph.
|
||||
//
|
||||
// The value-receiver twin at the bottom is the control: it resolved before the
|
||||
// fix and must keep resolving after it. Field decoration is NOT the variable —
|
||||
// Go already normalizes field type bindings at capture via normalizeGoTypeName.
|
||||
package handlers
|
||||
|
||||
import "fixture/repository"
|
||||
|
||||
type Holder struct {
|
||||
thing repository.Thing
|
||||
impl *repository.Impl
|
||||
cart *repository.CartRepo
|
||||
}
|
||||
|
||||
// Pointer receiver, interface-typed cross-package field.
|
||||
func (h *Holder) RunInterface() error {
|
||||
return h.thing.DoWork()
|
||||
}
|
||||
|
||||
// Pointer receiver, concrete-typed cross-package field.
|
||||
func (h *Holder) RunConcrete() error {
|
||||
return h.impl.DoWork()
|
||||
}
|
||||
|
||||
// Pointer receiver, concrete-typed cross-package field returning a value.
|
||||
func (h *Holder) RunCart(tx int) *repository.CartRepo {
|
||||
return h.cart.WithTx(tx)
|
||||
}
|
||||
|
||||
// Control: a local variable receiver typed in the same function resolved even
|
||||
// before the fix, via the text cascade rather than the decorated base.
|
||||
func (h *Holder) RunLocal() error {
|
||||
local := &repository.Impl{}
|
||||
return local.DoWork()
|
||||
}
|
||||
|
||||
type ValueHolder struct {
|
||||
impl *repository.Impl
|
||||
}
|
||||
|
||||
// Control: VALUE receiver. Binds as `ValueHolder` with no decoration, so this
|
||||
// resolved before the fix and must not change.
|
||||
func (v ValueHolder) RunFromValueReceiver() error {
|
||||
return v.impl.DoWork()
|
||||
}
|
||||
17
gitnexus/test/fixtures/lang-resolution/go-pointer-receiver-field-chain/repository/repo.go
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/go-pointer-receiver-field-chain/repository/repo.go
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package repository
|
||||
|
||||
// Thing is an interface-typed dependency, the shape a DI-wired Go service
|
||||
// stores in a struct field.
|
||||
type Thing interface {
|
||||
DoWork() error
|
||||
}
|
||||
|
||||
// Impl is the concrete implementation behind Thing.
|
||||
type Impl struct{}
|
||||
|
||||
func (i *Impl) DoWork() error { return nil }
|
||||
|
||||
// CartRepo is a concrete-typed dependency reached through a struct field.
|
||||
type CartRepo struct{}
|
||||
|
||||
func (c *CartRepo) WithTx(tx int) *CartRepo { return c }
|
||||
|
|
@ -1655,3 +1655,46 @@ describe('Go Child embeds Parent — inherited method resolution (SM-9)', () =>
|
|||
expect(parentMethodCall!.source).toBe('Run');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2766: pointer-receiver base resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Go pointer-receiver field chains (#2766)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'go-pointer-receiver-field-chain'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
const calls = (): Set<string> => edgeSet(getRelationships(result, 'CALLS'));
|
||||
|
||||
// The three rows that emitted nothing before the decoration fallback. All
|
||||
// three have a POINTER receiver, which bound as the literal `*Holder` and
|
||||
// matched no class, so receiver typing declined at the base.
|
||||
it('resolves an interface-typed cross-package field through a pointer receiver', () => {
|
||||
expect(calls()).toContain('RunInterface → DoWork');
|
||||
});
|
||||
|
||||
it('resolves a concrete-typed cross-package field through a pointer receiver', () => {
|
||||
expect(calls()).toContain('RunConcrete → DoWork');
|
||||
});
|
||||
|
||||
it('resolves a concrete cross-package field returning a value', () => {
|
||||
expect(calls()).toContain('RunCart → WithTx');
|
||||
});
|
||||
|
||||
// Controls: these resolved BEFORE the fix. R11 requires they still resolve to
|
||||
// the same target, so a regression here means the fallback moved an edge
|
||||
// rather than adding one.
|
||||
it('keeps resolving a local-variable receiver', () => {
|
||||
expect(calls()).toContain('RunLocal → DoWork');
|
||||
});
|
||||
|
||||
it('keeps resolving a value receiver', () => {
|
||||
expect(calls()).toContain('RunFromValueReceiver → DoWork');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue