Skip to content

Commit 8cea39a

Browse files
progress
1 parent 6e33ae2 commit 8cea39a

2 files changed

Lines changed: 204 additions & 1 deletion

File tree

packages/compiler/src/core/checker.ts

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4525,6 +4525,16 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
45254525
return undefined;
45264526
}
45274527

4528+
// If the type is still being created (e.g., model A is Template<{t: B}> where B
4529+
// references A.t), check if we can find the member from its `is` base or spreads
4530+
// that are already resolved.
4531+
if (type.creating && type.kind === "Model") {
4532+
const memberFromCreating = tryResolveMemberFromCreatingModel(ctx, type, node.id.sv);
4533+
if (memberFromCreating) {
4534+
return memberFromCreating;
4535+
}
4536+
}
4537+
45284538
// Late-bind the container and its members.
45294539
switch (type.kind) {
45304540
case "Model":
@@ -4542,6 +4552,178 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
45424552
return getCanonicalResolvedMemberSymbol(type, node.id.sv);
45434553
}
45444554

4555+
/**
4556+
* Try to resolve a member from a model that is still being created.
4557+
* This handles the case where `model A is Template<{t: B}>` and B references `A.t`.
4558+
* The member 't' comes from the `is` base (the template instantiation) which may
4559+
* already have its properties resolved even though A hasn't copied them yet.
4560+
*/
4561+
function tryResolveMemberFromCreatingModel(
4562+
ctx: CheckContext,
4563+
model: Model,
4564+
memberName: string,
4565+
): Sym | undefined {
4566+
// First check if the model already has the property (from its own declared properties)
4567+
const existingProp = model.properties.get(memberName);
4568+
if (existingProp) {
4569+
return getTypeSymbol(existingProp) ?? undefined;
4570+
}
4571+
4572+
// Check the model's AST node for `is` base and spreads
4573+
const modelNode = model.node;
4574+
if (!modelNode || modelNode.kind !== SyntaxKind.ModelStatement) {
4575+
return undefined;
4576+
}
4577+
4578+
// If there's an `is` clause, resolve the `is` expression type.
4579+
// getTypeForNode will return a cached type if the `is` target was already checked.
4580+
if (modelNode.is) {
4581+
const isBaseType = getTypeForNode(modelNode.is, ctx);
4582+
if (isBaseType && isBaseType.kind === "Model") {
4583+
// First check if the is-base already has the member (even if creating)
4584+
const prop = isBaseType.properties.get(memberName);
4585+
if (prop) {
4586+
return createLateBoundMemberSym(model, prop, memberName, modelNode);
4587+
}
4588+
// If the is-base is still creating, look at ITS spread sources recursively
4589+
if (isBaseType.creating) {
4590+
const found = findMemberInCreatingModelSources(ctx, isBaseType, memberName);
4591+
if (found) {
4592+
return createLateBoundMemberSym(model, found, memberName, modelNode);
4593+
}
4594+
}
4595+
}
4596+
}
4597+
4598+
// Check spread targets
4599+
for (const propNode of modelNode.properties) {
4600+
if (propNode.kind === SyntaxKind.ModelSpreadProperty) {
4601+
const spreadType = getTypeForNode(propNode.target, ctx);
4602+
if (spreadType && spreadType.kind === "Model") {
4603+
const prop = spreadType.properties.get(memberName);
4604+
if (prop) {
4605+
return createLateBoundMemberSym(model, prop, memberName, modelNode);
4606+
}
4607+
if (spreadType.creating) {
4608+
const found = findMemberInCreatingModelSources(ctx, spreadType, memberName);
4609+
if (found) {
4610+
return createLateBoundMemberSym(model, found, memberName, modelNode);
4611+
}
4612+
}
4613+
}
4614+
}
4615+
}
4616+
4617+
return undefined;
4618+
}
4619+
4620+
function createLateBoundMemberSym(
4621+
model: Model,
4622+
prop: ModelProperty,
4623+
memberName: string,
4624+
containerNode: Node,
4625+
): Sym | undefined {
4626+
const sym = createSymbol(
4627+
prop.node ?? containerNode,
4628+
memberName,
4629+
SymbolFlags.Member | SymbolFlags.Declaration | SymbolFlags.LateBound,
4630+
model.symbol,
4631+
);
4632+
mutate(sym).type = prop;
4633+
if (model.symbol?.members) {
4634+
const containerMembers: Mutable<SymbolTable> = resolver.getAugmentedSymbolTable(
4635+
model.symbol.members,
4636+
);
4637+
containerMembers.set(memberName, sym);
4638+
}
4639+
return sym;
4640+
}
4641+
4642+
/**
4643+
* Search for a member in a model that's still being created by looking at its
4644+
* source models (spreads and `is` targets). This handles the case where:
4645+
* model Template<T> {...T}
4646+
* model A is Template<{t: B}>
4647+
* When A is creating and its properties haven't been copied yet, we can look
4648+
* at Template<{t:B}>'s spread sources to find property `t`.
4649+
*/
4650+
function findMemberInCreatingModelSources(
4651+
ctx: CheckContext,
4652+
model: Model,
4653+
memberName: string,
4654+
visited: Set<Model> = new Set(),
4655+
): ModelProperty | undefined {
4656+
if (visited.has(model)) return undefined;
4657+
visited.add(model);
4658+
4659+
// Check own properties first (already resolved)
4660+
const ownProp = model.properties.get(memberName);
4661+
if (ownProp) return ownProp;
4662+
4663+
// Look at the model's AST node for spread sources.
4664+
// Use the model's templateMapper when resolving references in its body,
4665+
// since template instances share the same AST node as the template declaration.
4666+
const modelNode = model.node;
4667+
if (!modelNode) return undefined;
4668+
4669+
const resolveCtx = model.templateMapper ? ctx.withMapper(model.templateMapper) : ctx;
4670+
4671+
if (
4672+
modelNode.kind === SyntaxKind.ModelStatement ||
4673+
modelNode.kind === SyntaxKind.ModelExpression
4674+
) {
4675+
// Check direct property declarations that might not be resolved yet.
4676+
// If the model expression has `t: B` as a declared property but hasn't
4677+
// been fully checked, we can still resolve the property from its member symbol.
4678+
// Due to early linkType on properties, the symbol may already have a type.
4679+
if (model.creating) {
4680+
const memberSym = model.node?.symbol?.members
4681+
? getMemberSymbol(model.node.symbol, memberName)
4682+
: undefined;
4683+
if (memberSym) {
4684+
const memberLinks = resolver.getSymbolLinks(memberSym);
4685+
if (memberLinks.declaredType && memberLinks.declaredType.kind === "ModelProperty") {
4686+
return memberLinks.declaredType as ModelProperty;
4687+
}
4688+
}
4689+
}
4690+
4691+
for (const propNode of modelNode.properties) {
4692+
if (propNode.kind === SyntaxKind.ModelSpreadProperty) {
4693+
const spreadType = getTypeForNode(propNode.target, resolveCtx);
4694+
if (spreadType && spreadType.kind === "Model") {
4695+
const prop = spreadType.properties.get(memberName);
4696+
if (prop) return prop;
4697+
if (spreadType.creating) {
4698+
const found = findMemberInCreatingModelSources(ctx, spreadType, memberName, visited);
4699+
if (found) return found;
4700+
}
4701+
}
4702+
}
4703+
}
4704+
4705+
// For model statements, also check the `is` base
4706+
if (modelNode.kind === SyntaxKind.ModelStatement && modelNode.is) {
4707+
const isBaseType = getTypeForNode(modelNode.is, resolveCtx);
4708+
if (isBaseType && isBaseType.kind === "Model") {
4709+
const prop = isBaseType.properties.get(memberName);
4710+
if (prop) return prop;
4711+
if (isBaseType.creating) {
4712+
const found = findMemberInCreatingModelSources(
4713+
ctx,
4714+
isBaseType,
4715+
memberName,
4716+
visited,
4717+
);
4718+
if (found) return found;
4719+
}
4720+
}
4721+
}
4722+
}
4723+
4724+
return undefined;
4725+
}
4726+
45454727
function getMemberKindName(node: Node) {
45464728
switch (node.kind) {
45474729
case SyntaxKind.ModelStatement:
@@ -5092,6 +5274,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
50925274
derivedModels: [],
50935275
});
50945276
linkType(ctx, links, type);
5277+
// Set templateMapper early so that member lookups on this creating model
5278+
// can resolve spread targets through the correct mapper context.
5279+
linkMapper(type, ctx.mapper);
50955280

50965281
if (node.symbol.members) {
50975282
const members = resolver.getAugmentedSymbolTable(node.symbol.members);
@@ -5157,7 +5342,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
51575342

51585343
decorators.push(...checkDecorators(ctx, type, node));
51595344

5160-
linkMapper(type, ctx.mapper);
51615345
finishType(type, { skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration) });
51625346

51635347
lateBindMemberContainer(type);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, it, expect } from "vitest";
2+
import { Model } from "../../src/core/types.js";
3+
import { Tester } from "../tester.js";
4+
5+
describe("circular reference with template spread and member access", () => {
6+
it("model A is Template<{t: B}> with B accessing A.t", async () => {
7+
const diagnostics = await Tester.diagnose(`
8+
model Template<T> {...T}
9+
10+
model A is Template<{t: B}>;
11+
12+
model B {
13+
a: A.t;
14+
}
15+
`);
16+
// Should have no errors — A.t should resolve to the 't' property
17+
expect(diagnostics.map(d => `${d.code}: ${d.message}`)).toEqual([]);
18+
});
19+
});

0 commit comments

Comments
 (0)