Skip to content

Commit eec3840

Browse files
abc
1 parent 114e1f6 commit eec3840

2 files changed

Lines changed: 85 additions & 3 deletions

File tree

packages/compiler/src/core/checker.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
515515
program.reportDiagnostic(x);
516516
};
517517

518+
// State for deferred member resolution: when a member access targets a container
519+
// that is still being checked (creating=true), we store the info here so that
520+
// the calling checkModelProperty can register a waitingForResolution callback
521+
// instead of emitting an error.
522+
let pendingMemberResolution:
523+
| { containerType: Type; node: MemberExpressionNode; baseSym: Sym }
524+
| undefined;
525+
518526
const typePrototype: TypePrototype = {};
519527
const globalNamespaceType = createGlobalNamespaceType();
520528

@@ -4397,6 +4405,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
43974405
}
43984406
}
43994407

4408+
// If the late-bound member resolution detected a creating container, the member may
4409+
// become available when the container finishes. Don't emit an error — let the caller
4410+
// handle deferred resolution via waitingForResolution callback.
4411+
if (pendingMemberResolution) {
4412+
return undefined;
4413+
}
4414+
44004415
if (base.flags & SymbolFlags.Namespace) {
44014416
reportCheckerDiagnostic(
44024417
createDiagnostic({
@@ -4469,7 +4484,19 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
44694484
}
44704485

44714486
// Don't force-check if the container is currently being resolved (cycle).
4487+
// Record pending info so the caller can defer resolution via waitingForResolution.
44724488
if (pendingResolutions.has(base, ResolutionKind.Type)) {
4489+
const containerType = links.declaredType;
4490+
if (containerType?.creating) {
4491+
pendingMemberResolution = { containerType, node, baseSym: base };
4492+
}
4493+
return undefined;
4494+
}
4495+
4496+
// If the container type is already being checked (creating), we can't force-resolve
4497+
// its members yet. Record pending info for deferred resolution.
4498+
if (links.declaredType?.creating) {
4499+
pendingMemberResolution = { containerType: links.declaredType, node, baseSym: base };
44734500
return undefined;
44744501
}
44754502

@@ -6999,6 +7026,25 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
69997026

70007027
type.type = getTypeForNode(prop.value, ctx);
70017028

7029+
// If the type resolved to errorType because a member access targeted a container
7030+
// that was still being checked, register a callback to update the property type
7031+
// when the container finishes.
7032+
const pending = pendingMemberResolution;
7033+
pendingMemberResolution = undefined;
7034+
if (pending && type.type === errorType) {
7035+
ensureResolved([pending.containerType], type, () => {
7036+
const resolvedType = tryForceResolveLateBoundMember(ctx, pending.baseSym, pending.node);
7037+
if (resolvedType) {
7038+
if (resolvedType.flags & SymbolFlags.LateBound) {
7039+
compilerAssert(resolvedType.type, "Expected late bound symbol to have type");
7040+
type.type = resolvedType.type as Type;
7041+
} else {
7042+
type.type = getTypeForNode(getSymNode(resolvedType)!, ctx);
7043+
}
7044+
}
7045+
});
7046+
}
7047+
70027048
// Detect property-to-property cycles (e.g., A.a -> B.a -> A.a)
70037049
if (hasPropertyTypeCycle(type)) {
70047050
if (ctx.mapper === undefined) {
@@ -7439,7 +7485,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
74397485
*/
74407486
function checkAugmentDecorator(ctx: CheckContext, node: AugmentDecoratorStatementNode) {
74417487
// This will validate the target type is pointing to a valid ref.
7442-
resolveTypeReferenceSym(ctx.withMapper(undefined), node.targetType, {
7488+
const targetSym = resolveTypeReferenceSym(ctx.withMapper(undefined), node.targetType, {
74437489
resolveDeclarationOfTemplate: true,
74447490
});
74457491

@@ -7482,6 +7528,28 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
74827528
);
74837529
}
74847530

7531+
// If the target sym was resolved at checker time (e.g. late-bound member from template)
7532+
// but wasn't registered during name resolution, apply the decorator directly.
7533+
if (targetSym && targetSym.flags & SymbolFlags.LateBound) {
7534+
const augmentDecsForSym = resolver.getAugmentDecoratorsForSym(targetSym);
7535+
if (!augmentDecsForSym.includes(node)) {
7536+
const targetType = targetSym.type && isType(targetSym.type) ? targetSym.type : undefined;
7537+
if (targetType && "decorators" in targetType) {
7538+
const decorator = checkDecoratorApplication(ctx, targetType, node);
7539+
if (decorator) {
7540+
targetType.decorators.unshift(decorator);
7541+
const validators = applyDecoratorToType(program, decorator, targetType);
7542+
if (validators?.onTargetFinish) {
7543+
program.reportDiagnostics(validators.onTargetFinish());
7544+
}
7545+
if (validators?.onGraphFinish) {
7546+
postCheckValidators.push(validators.onGraphFinish);
7547+
}
7548+
}
7549+
}
7550+
}
7551+
}
7552+
74857553
// If this was used to get a type this is invalid, only used for validation.
74867554
return errorType;
74877555
}

packages/compiler/test/checker/circular-resolution.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { describe, expect, it } from "vitest";
2+
import { getDoc } from "../../src/index.js";
3+
import { t } from "../../src/testing/index.js";
24
import { Tester } from "../tester.js";
35

46
describe("circular model resolution", () => {
@@ -16,8 +18,8 @@ describe("circular model resolution", () => {
1618
model A { t: B }
1719
model B { a: A.t; }
1820
`);
19-
// circular-prop is expected since A.t references B and B.a references A.t
20-
expect(diagnostics.map((d) => d.code)).toContain("circular-prop");
21+
// With late-bound member resolution, A.t resolves without errors
22+
expect(diagnostics.map((d) => `${d.code}: ${d.message}`)).toEqual([]);
2123
});
2224

2325
it("circular: model A is Template<{t: B}> with B accessing A.t", async () => {
@@ -29,4 +31,16 @@ describe("circular model resolution", () => {
2931
// A.t should resolve to the 't' property (type B) after A finishes via deferred resolution
3032
expect(diagnostics.map((d) => `${d.code}: ${d.message}`)).toEqual([]);
3133
});
34+
35+
it("augment decorator on template-derived member applies to A and spread copies", async () => {
36+
const { A, C, program } = await Tester.compile(t.code`
37+
model Template<T> { ...T; }
38+
model ${t.model("A")} is Template<{ t: string; }>;
39+
model ${t.model("C")} { ...A; }
40+
@@doc(A.t, "Some doc");
41+
`);
42+
const aProp = A.properties.get("t");
43+
expect(aProp).toBeDefined();
44+
expect(getDoc(program, aProp!)).toBe("Some doc");
45+
});
3246
});

0 commit comments

Comments
 (0)