Skip to content

Commit 06a5c88

Browse files
committed
fix enum in alias expression
1 parent c199967 commit 06a5c88

File tree

4 files changed

+87
-26
lines changed

4 files changed

+87
-26
lines changed

packages/schema/src/language-server/validator/expression-validator.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,24 @@ export default class ExpressionValidator implements AstValidator<Expression> {
4343
} else {
4444
const hasReferenceResolutionError = streamAst(expr).some((node) => {
4545
if (isMemberAccessExpr(node)) {
46+
console.log('Member access error:', node.member.error?.message);
4647
return !!node.member.error;
4748
}
4849
if (isReferenceExpr(node)) {
50+
console.log('Reference error:', node.target.error?.message);
4951
return !!node.target.error;
5052
}
5153
return false;
5254
});
5355
if (hasReferenceResolutionError) {
5456
// report silent errors not involving linker errors
55-
accept('error', `Expression cannot be resolved: ${expr.$cstNode?.text}`, {
56-
node: expr,
57-
});
57+
accept(
58+
'error',
59+
`Expression cannot be resolved: ${expr.$cstNode?.text} (${expr.$container.$cstNode?.text}) ${expr.$type}`,
60+
{
61+
node: expr,
62+
}
63+
);
5864
}
5965
}
6066
}
@@ -68,6 +74,9 @@ export default class ExpressionValidator implements AstValidator<Expression> {
6874
}
6975

7076
private validateBinaryExpr(expr: BinaryExpr, accept: ValidationAcceptor) {
77+
console.log('Validating binary expression:', expr.$cstNode?.text);
78+
console.log('Left operand:', expr.left.$cstNode?.text, expr.left.$type);
79+
console.log('Right operand:', expr.right.$cstNode?.text, expr.right.$type);
7180
switch (expr.operator) {
7281
case 'in': {
7382
if (typeof expr.left.$resolvedType?.decl !== 'string' && !isEnum(expr.left.$resolvedType?.decl)) {
@@ -111,18 +120,26 @@ export default class ExpressionValidator implements AstValidator<Expression> {
111120
typeof expr.left.$resolvedType?.decl !== 'string' ||
112121
!supportedShapes.includes(expr.left.$resolvedType.decl)
113122
) {
114-
accept('error', `invalid operand type for "${expr.operator}" operator`, {
115-
node: expr.left,
116-
});
123+
accept(
124+
'error',
125+
`invalid operand type for "${expr.operator}" operator left: ${expr.left.$cstNode?.text} ${expr.left.$type}`,
126+
{
127+
node: expr.left,
128+
}
129+
);
117130
return;
118131
}
119132
if (
120133
typeof expr.right.$resolvedType?.decl !== 'string' ||
121134
!supportedShapes.includes(expr.right.$resolvedType.decl)
122135
) {
123-
accept('error', `invalid operand type for "${expr.operator}" operator`, {
124-
node: expr.right,
125-
});
136+
accept(
137+
'error',
138+
`invalid operand type for "${expr.operator}" operator right: ${expr.right.$cstNode?.text} ${expr.right.$type}`,
139+
{
140+
node: expr.right,
141+
}
142+
);
126143
return;
127144
}
128145

packages/schema/src/language-server/validator/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,26 @@ export function mappedRawExpressionTypeToResolvedShape(expressionType: Expressio
120120
}
121121
}
122122

123+
// /**
124+
// * Maps a resolved shape (e.g. String) to an expression type (e.g. StringLiteral)
125+
// */
126+
// export function mappedResolvedShapeToExpressionType(shape: ResolvedShape): Expression['$type'] {
127+
// switch (shape) {
128+
// case 'String':
129+
// return 'StringLiteral';
130+
// case 'Int':
131+
// return 'NumberLiteral';
132+
// case 'Boolean':
133+
// return 'BooleanLiteral';
134+
// case 'Object':
135+
// return 'ObjectExpr';
136+
// case 'Null':
137+
// return 'NullExpr';
138+
// default:
139+
// return 'ObjectExpr';
140+
// }
141+
// }
142+
123143
export function isAuthOrAuthMemberAccess(expr: Expression): boolean {
124144
return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand));
125145
}

packages/schema/src/language-server/zmodel-linker.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,17 @@ export class ZModelLinker extends DefaultLinker {
144144
}
145145

146146
private resolve(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[] = []) {
147+
// if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes
148+
// eslint-disable-next-line
149+
// @ts-ignore
150+
if (node?.type?.reference?.ref && isEnum(node?.type?.reference?.ref)) {
151+
// eslint-disable-next-line
152+
// @ts-ignore
153+
const contextEnum = node.type?.reference?.ref as Enum;
154+
const enumScope: ScopeProvider = (name) => contextEnum?.fields?.find((f) => f.name === name);
155+
extraScopes = [enumScope, ...extraScopes];
156+
}
157+
147158
switch (node.$type) {
148159
case StringLiteral:
149160
case NumberLiteral:
@@ -160,12 +171,7 @@ export class ZModelLinker extends DefaultLinker {
160171
break;
161172

162173
case ReferenceExpr:
163-
// If the reference comes from an alias, we resolve it against the first matching data model
164-
if (getContainerOfType(node, isAliasDecl)) {
165-
this.resolveAliasExpr(node, document);
166-
} else {
167-
this.resolveReference(node as ReferenceExpr, document, extraScopes);
168-
}
174+
this.resolveReference(node as ReferenceExpr, document, extraScopes);
169175
break;
170176

171177
case MemberAccessExpr:
@@ -265,6 +271,10 @@ export class ZModelLinker extends DefaultLinker {
265271
}
266272

267273
private resolveReference(node: ReferenceExpr, document: LangiumDocument<AstNode>, extraScopes: ScopeProvider[]) {
274+
// If the reference comes from an alias, we resolve it against the first matching data model
275+
if (getContainerOfType(node, isAliasDecl)) {
276+
this.resolveAliasExpr(node as ReferenceExpr, document);
277+
}
268278
this.resolveDefault(node, document, extraScopes);
269279

270280
if (node.target.ref) {
@@ -468,11 +478,6 @@ export class ZModelLinker extends DefaultLinker {
468478
// Find the first model that has the alias reference as a field
469479
const matchingModel = models.find((model) => model.fields.some((f) => f.name === node.$cstNode?.text));
470480
if (!matchingModel) {
471-
this.createLinkingError({
472-
reference: (node as ReferenceExpr).target,
473-
container: node,
474-
property: 'target',
475-
});
476481
return;
477482
}
478483

@@ -481,6 +486,11 @@ export class ZModelLinker extends DefaultLinker {
481486

482487
const visitExpr = (node: Expression) => {
483488
if (isReferenceExpr(node)) {
489+
// enums in alias expressions are already resolved
490+
if (isEnum(node.target.ref?.$container)) {
491+
return;
492+
}
493+
484494
const resolved = this.resolveFromScopeProviders(node, 'target', document, [scopeProvider]);
485495
if (resolved) {
486496
this.resolveToDeclaredType(node, (resolved as DataModelField).type);
@@ -585,6 +595,10 @@ export class ZModelLinker extends DefaultLinker {
585595
//#region Utils
586596

587597
private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType | TypeDefFieldType) {
598+
// enums from alias expressions are already resolved and do not exist in the scope
599+
if (!type) {
600+
return;
601+
}
588602
let nullable = false;
589603
if (isDataModelFieldType(type) || isTypeDefField(type)) {
590604
nullable = type.optional;

tests/integration/tests/plugins/policy.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,22 @@ model M {
174174

175175
it('complex alias expressions', async () => {
176176
const model = `
177+
enum TaskStatus {
178+
TODO
179+
IN_PROGRESS
180+
DONE
181+
}
182+
183+
alias isInProgress() {
184+
status == IN_PROGRESS
185+
}
186+
177187
alias currentUserId() {
178188
auth().id
179189
}
180190
181191
alias complexAlias() {
182-
auth().cart.tasks?[id == 123] && value >10 && currentUserId() != null
192+
status == IN_PROGRESS && auth().cart.tasks?[id == 123] && value >10 && currentUserId() != null
183193
}
184194
185195
model User {
@@ -196,6 +206,7 @@ model M {
196206
197207
model Task {
198208
id Int @id @default(autoincrement())
209+
status TaskStatus @default(TODO)
199210
cart Cart @relation(fields: [cartId], references: [id])
200211
cartId Int
201212
value Int
@@ -213,12 +224,11 @@ model M {
213224
(policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 1 }] } } })
214225
).toEqual(
215226
expect.objectContaining({
216-
AND: [{ AND: [{ OR: [] }, { value: { gt: 10 } }] }, { OR: [] }],
227+
AND: [
228+
{ AND: [{ AND: [{ status: { equals: 'IN_PROGRESS' } }, { OR: [] }] }, { value: { gt: 10 } }] },
229+
{ OR: [] },
230+
],
217231
})
218232
);
219-
220-
expect(
221-
(policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 123 }] } } })
222-
).toEqual(expect.objectContaining({ AND: [{ AND: [{ AND: [] }, { value: { gt: 10 } }] }, { OR: [] }] }));
223233
});
224234
});

0 commit comments

Comments
 (0)