Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ import {
Value,
ValueWithTemplate,
VoidType,
WhenCondition,
WhenExpressionNode,
} from "./types.js";

export type CreateTypeProps = Omit<Type, "isFinished" | "entityKind" | keyof TypePrototype>;
Expand Down Expand Up @@ -5759,8 +5761,62 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
decorator: sym.value ?? ((...args: any[]) => {}),
node: decNode,
args,
when: resolveWhenConditions(decNode),
};
}

/**
* Resolve the `when` clause on a decorator into structured conditions.
* Returns undefined if no `when` clause is present.
*/
function resolveWhenConditions(
decNode: DecoratorExpressionNode | AugmentDecoratorStatementNode,
): WhenCondition[] | undefined {
if (decNode.kind !== SyntaxKind.DecoratorExpression || !decNode.when) {
return undefined;
}
const conditions: WhenCondition[] = [];
for (const condNode of decNode.when.conditions) {
const condition = resolveWhenExpression(condNode);
if (condition) {
conditions.push(condition);
}
}
return conditions.length > 0 ? conditions : undefined;
}

/**
* Resolve a single when condition expression into a WhenCondition.
*/
function resolveWhenExpression(
node: WhenExpressionNode,
): WhenCondition | undefined {
// Determine the filter kind from the target identifier
if (node.target.kind === SyntaxKind.Identifier) {
const name = node.target.sv;
const knownFilters = ["emitter", "language", "target", "since", "between"];
if (knownFilters.includes(name)) {
// Extract raw string args for simple matching
const rawArgs: string[] = [];
for (const arg of node.arguments) {
if (arg.kind === SyntaxKind.StringLiteral) {
rawArgs.push(arg.value);
}
}
return {
kind: name as WhenCondition["kind"],
args: [],
rawArgs: rawArgs.length > 0 ? rawArgs : undefined,
};
}
}
// Treat as enum member reference (e.g., Lifecycle.read)
return {
kind: "member",
args: [],
};
}

/** Check the decorator target is valid */

function checkDecoratorTarget(targetType: Type, declaration: Decorator, decoratorNode: Node) {
Expand Down Expand Up @@ -6830,6 +6886,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
): ValidatorFn[] {
const postSelfValidators: ValidatorFn[] = [];
for (const decApp of typeDef.decorators) {
// Skip scoped decorators — they are deferred until an emitter applies a scope view
if (decApp.when) {
continue;
}
const validators = applyDecoratorToType(program, decApp, typeDef);
if (validators?.onTargetFinish) {
postSelfValidators.push(validators.onTargetFinish);
Expand Down Expand Up @@ -7530,7 +7590,7 @@ function getDocContent(content: readonly DocContent[]) {
return docs.join("");
}

function applyDecoratorToType(
export function applyDecoratorToType(
program: Program,
decApp: DecoratorApplication,
target: Type,
Expand Down
117 changes: 113 additions & 4 deletions packages/compiler/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ import {
UsingStatementNode,
ValueOfExpressionNode,
VoidKeywordNode,
WhenBlockStatementNode,
WhenClauseNode,
WhenExpressionNode,
} from "./types.js";

/**
Expand Down Expand Up @@ -466,6 +469,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
item = parseDeclaration(pos, decorators, docs, directives);
break;
default:
if (isWhenKeyword()) {
reportInvalidDecorators(decorators, "when block statement");
item = parseWhenBlockStatement(pos);
break;
}
item = parseInvalidStatement(pos, decorators);
break;
}
Expand Down Expand Up @@ -499,7 +507,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
return stmts;
}

function parseStatementList(): Statement[] {
function parseStatementList(): Statement[] {
const stmts: Statement[] = [];

while (token() !== Token.CloseBrace) {
Expand Down Expand Up @@ -544,6 +552,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
item = parseEmptyStatement(pos);
break;
default:
if (isWhenKeyword()) {
reportInvalidDecorators(decorators, "when block statement");
item = parseWhenBlockStatement(pos);
break;
}
item = parseInvalidStatement(pos, decorators);
break;
}
Expand Down Expand Up @@ -934,6 +947,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
bodyRange: propDetail.range,
modifiers,
modifierFlags: modifiersToFlags(modifiers),
when: parseOptionalWhenClause(),
...finishNode(pos),
};
}
Expand Down Expand Up @@ -1046,13 +1060,15 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa

const hasDefault = parseOptional(Token.Equals);
const defaultValue = hasDefault ? parseExpression() : undefined;
const whenClause = parseOptionalWhenClause();
return {
kind: SyntaxKind.ModelProperty,
id,
decorators,
value,
optional,
default: defaultValue,
when: whenClause,
...finishNode(pos),
};
}
Expand Down Expand Up @@ -1595,10 +1611,95 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
allowReservedIdentifierInMember: true,
});
const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression);
const whenClause = parseOptionalWhenClause();
return {
kind: SyntaxKind.DecoratorExpression,
arguments: args,
target,
when: whenClause,
...finishNode(pos),
};
}

/**
* Parse an optional `when` clause after a decorator, statement, or property.
* Returns undefined if no `when` keyword is present.
* Uses contextual keyword detection to avoid breaking `when` used as an identifier.
*
* Grammar:
* WhenClause ::= 'when' WhenExpression (',' WhenExpression)*
*/
function parseOptionalWhenClause(): WhenClauseNode | undefined {
if (!isWhenKeyword()) {
return undefined;
}
const pos = tokenPos();
nextToken(); // consume 'when'

const conditions: WhenExpressionNode[] = [];
conditions.push(parseWhenExpression());

while (parseOptional(Token.Comma)) {
conditions.push(parseWhenExpression());
}

return {
kind: SyntaxKind.WhenClause,
conditions,
...finishNode(pos),
};
}

/**
* Check if the current token is the contextual keyword `when`.
* `when` is not a reserved keyword — it can still be used as an identifier.
*/
function isWhenKeyword(): boolean {
return token() === Token.Identifier && tokenValue() === "when";
}

/**
* Parse a single when condition expression.
* Either a filter call like `emitter("name")` or an enum member reference like `Lifecycle.read`.
*
* Grammar:
* WhenExpression ::= Identifier('.' Identifier)* ('(' Expression (',' Expression)* ')')?
*/
function parseWhenExpression(): WhenExpressionNode {
const pos = tokenPos();
const target = parseIdentifierOrMemberExpression({
allowReservedIdentifier: true,
allowReservedIdentifierInMember: true,
});

// Parse optional arguments (for filter calls like `emitter("name")`)
const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression);

return {
kind: SyntaxKind.WhenExpression,
target,
arguments: args,
...finishNode(pos),
};
}

/**
* Parse a `when` block statement: `when condition { ...statements }`
* The 'when' identifier has already been verified but not consumed.
*/
function parseWhenBlockStatement(pos: number): WhenBlockStatementNode {
// parseOptionalWhenClause will consume 'when' and parse conditions
const whenClause = parseOptionalWhenClause()!;

// Parse the block body: { ...statements }
parseExpected(Token.OpenBrace);
const statements = parseStatementList();
parseExpected(Token.CloseBrace);

return {
kind: SyntaxKind.WhenBlockStatement,
when: whenClause,
statements,
...finishNode(pos),
};
}
Expand Down Expand Up @@ -2980,7 +3081,7 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
visitEach(cb, node.arguments)
);
case SyntaxKind.DecoratorExpression:
return visitNode(cb, node.target) || visitEach(cb, node.arguments);
return visitNode(cb, node.target) || visitEach(cb, node.arguments) || visitNode(cb, node.when);
case SyntaxKind.CallExpression:
return visitNode(cb, node.target) || visitEach(cb, node.arguments);
case SyntaxKind.DirectiveExpression:
Expand Down Expand Up @@ -3028,7 +3129,8 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
visitEach(cb, node.decorators) ||
visitNode(cb, node.id) ||
visitNode(cb, node.value) ||
visitNode(cb, node.default)
visitNode(cb, node.default) ||
visitNode(cb, node.when)
);
case SyntaxKind.ModelSpreadProperty:
return visitNode(cb, node.target);
Expand All @@ -3041,7 +3143,8 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
visitEach(cb, node.templateParameters) ||
visitNode(cb, node.extends) ||
visitNode(cb, node.is) ||
visitEach(cb, node.properties)
visitEach(cb, node.properties) ||
visitNode(cb, node.when)
);
case SyntaxKind.ScalarStatement:
return (
Expand Down Expand Up @@ -3154,6 +3257,12 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
return visitNode(cb, node.target);
case SyntaxKind.ArrayLiteral:
return visitEach(cb, node.values);
case SyntaxKind.WhenClause:
return visitEach(cb, node.conditions);
case SyntaxKind.WhenExpression:
return visitNode(cb, node.target) || visitEach(cb, node.arguments);
case SyntaxKind.WhenBlockStatement:
return visitNode(cb, node.when) || visitEach(cb, node.statements);
// no children for the rest of these.
case SyntaxKind.StringTemplateHead:
case SyntaxKind.StringTemplateMiddle:
Expand Down
Loading
Loading