Skip to content

Commit 03315cc

Browse files
authored
fix: stricter binary operation operand type compatibility check (#846)
1 parent 73f2cec commit 03315cc

File tree

3 files changed

+65
-1
lines changed

3 files changed

+65
-1
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { isDataModelFieldReference } from '@zenstackhq/sdk';
1111
import { ValidationAcceptor } from 'langium';
1212
import { isAuthInvocation, isCollectionPredicate } from '../../utils/ast-utils';
1313
import { AstValidator } from '../types';
14+
import { typeAssignable } from './utils';
1415

1516
/**
1617
* Validates expressions.
@@ -124,6 +125,28 @@ export default class ExpressionValidator implements AstValidator<Expression> {
124125
break;
125126
}
126127

128+
if (
129+
(expr.left.$resolvedType?.nullable && isNullExpr(expr.right)) ||
130+
(expr.right.$resolvedType?.nullable && isNullExpr(expr.left))
131+
) {
132+
// comparing nullable field with null
133+
return;
134+
}
135+
136+
if (
137+
typeof expr.left.$resolvedType?.decl === 'string' &&
138+
typeof expr.right.$resolvedType?.decl === 'string'
139+
) {
140+
// scalar types assignability
141+
if (
142+
!typeAssignable(expr.left.$resolvedType.decl, expr.right.$resolvedType.decl) &&
143+
!typeAssignable(expr.right.$resolvedType.decl, expr.left.$resolvedType.decl)
144+
) {
145+
accept('error', 'incompatible operand types', { node: expr });
146+
}
147+
return;
148+
}
149+
127150
// disallow comparing model type with scalar type or comparison between
128151
// incompatible model types
129152
const leftType = expr.left.$resolvedType?.decl;

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {
5353
} from 'langium';
5454
import { match } from 'ts-pattern';
5555
import { CancellationToken } from 'vscode-jsonrpc';
56-
import { getAllDeclarationsFromImports } from '../utils/ast-utils';
56+
import { getAllDeclarationsFromImports, isAuthInvocation } from '../utils/ast-utils';
5757
import { mapBuiltinTypeToExpressionType } from './validator/utils';
5858

5959
interface DefaultReference extends Reference {
@@ -337,6 +337,12 @@ export class ZModelLinker extends DefaultLinker {
337337
this.linkReference(node, 'member', document, [provider], true);
338338
if (node.member.ref) {
339339
this.resolveToDeclaredType(node, node.member.ref.type);
340+
341+
if (node.$resolvedType && isAuthInvocation(node.operand)) {
342+
// member access on auth() function is nullable
343+
// because user may not have provided all fields
344+
node.$resolvedType.nullable = true;
345+
}
340346
}
341347
}
342348
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { loadModelWithError } from '@zenstackhq/testtools';
2+
3+
describe('Regression: issue 804', () => {
4+
it('regression', async () => {
5+
expect(
6+
await loadModelWithError(
7+
`
8+
generator client {
9+
provider = "prisma-client-js"
10+
}
11+
12+
datasource db {
13+
provider = "postgresql"
14+
url = env("DATABASE_URL")
15+
}
16+
17+
model User {
18+
id Int @id @default(autoincrement())
19+
email Int
20+
posts Post[]
21+
}
22+
23+
model Post {
24+
id Int @id @default(autoincrement())
25+
author User? @relation(fields: [authorId], references: [id])
26+
authorId Int
27+
published Boolean
28+
29+
@@allow('all', auth().posts?[published] == 'TRUE')
30+
}
31+
`
32+
)
33+
).toContain('incompatible operand types');
34+
});
35+
});

0 commit comments

Comments
 (0)