Skip to content

Commit 791bc52

Browse files
committed
fix: link alias references to first matching model field
1 parent 25b5e97 commit 791bc52

File tree

9 files changed

+102
-93
lines changed

9 files changed

+102
-93
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export function mapBuiltinTypeToExpressionType(
8787
case 'Int':
8888
case 'Float':
8989
case 'Null':
90+
case 'Object':
91+
case 'Unsupported':
9092
return type;
9193
case 'BigInt':
9294
return 'Int';
@@ -95,10 +97,6 @@ export function mapBuiltinTypeToExpressionType(
9597
case 'Json':
9698
case 'Bytes':
9799
return 'Any';
98-
case 'Object':
99-
return 'Object';
100-
case 'Unsupported':
101-
return 'Unsupported';
102100
}
103101
}
104102

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
DataModelFieldType,
1111
Enum,
1212
EnumField,
13+
Expression,
1314
ExpressionType,
1415
FunctionDecl,
1516
FunctionParam,
@@ -27,16 +28,20 @@ import {
2728
ThisExpr,
2829
TypeDefFieldType,
2930
UnaryExpr,
31+
isAliasDecl,
3032
isArrayExpr,
33+
isBinaryExpr,
3134
isBooleanLiteral,
3235
isDataModel,
3336
isDataModelField,
3437
isDataModelFieldType,
3538
isEnum,
39+
isModel,
3640
isNumberLiteral,
3741
isReferenceExpr,
3842
isStringLiteral,
3943
isTypeDefField,
44+
isUnaryExpr,
4045
} from '@zenstackhq/language/ast';
4146
import { getAuthDecl, getModelFieldsWithBases, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk';
4247
import {
@@ -155,10 +160,16 @@ export class ZModelLinker extends DefaultLinker {
155160
break;
156161

157162
case ReferenceExpr:
158-
this.resolveReference(node as ReferenceExpr, document, extraScopes);
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+
}
159169
break;
160170

161171
case MemberAccessExpr:
172+
// TODO: check from alias ?
162173
this.resolveMemberAccess(node as MemberAccessExpr, document, extraScopes);
163174
break;
164175

@@ -261,6 +272,7 @@ export class ZModelLinker extends DefaultLinker {
261272
if (node.target.ref.$type === EnumField) {
262273
this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container);
263274
} else {
275+
// TODO: if the reference is from an alias, we should resolve it against the first matching data model
264276
this.resolveToDeclaredType(node, (node.target.ref as DataModelField | FunctionParam).type);
265277
}
266278
}
@@ -300,9 +312,8 @@ export class ZModelLinker extends DefaultLinker {
300312
} else if (isFutureExpr(node)) {
301313
// future() function is resolved to current model
302314
node.$resolvedType = { decl: getContainingDataModel(node) };
303-
} else if (isAliasInvocation(node)) {
315+
} else if (isAliasInvocation(node) || !!getContainerOfType(node, isAliasDecl)) {
304316
// function is resolved to matching alias declaration
305-
306317
const expressionType = funcDecl.expression?.$type;
307318
if (!expressionType) {
308319
this.createLinkingError({
@@ -448,6 +459,46 @@ export class ZModelLinker extends DefaultLinker {
448459
node.$resolvedType = node.value.$resolvedType;
449460
}
450461

462+
private resolveAliasExpr(node: AstNode, document: LangiumDocument<AstNode>) {
463+
const container = getContainerOfType(node, isAliasDecl);
464+
if (!container) {
465+
return;
466+
}
467+
const model = getContainerOfType(node, isModel);
468+
const models = model?.declarations.filter(isDataModel) ?? [];
469+
// Find the first model that has the alias reference as a field
470+
const matchingModel = models.find((model) => model.fields.some((f) => f.name === node.$cstNode?.text));
471+
if (!matchingModel) {
472+
this.createLinkingError({
473+
reference: (node as ReferenceExpr).target,
474+
container: node,
475+
property: 'target',
476+
});
477+
return;
478+
}
479+
480+
const scopeProvider = (name: string) =>
481+
getModelFieldsWithBases(matchingModel).find((field) => field.name === name);
482+
483+
const visitExpr = (node: Expression) => {
484+
if (isReferenceExpr(node)) {
485+
const resolved = this.resolveFromScopeProviders(node, 'target', document, [scopeProvider]);
486+
if (resolved) {
487+
this.resolveToDeclaredType(node, (resolved as DataModelField).type);
488+
} else {
489+
this.unresolvableRefExpr(node);
490+
}
491+
} else if (isBinaryExpr(node)) {
492+
visitExpr(node.left);
493+
visitExpr(node.right);
494+
} else if (isUnaryExpr(node)) {
495+
visitExpr(node.operand);
496+
}
497+
};
498+
499+
visitExpr(container.expression);
500+
}
501+
451502
private unresolvableRefExpr(item: ReferenceExpr) {
452503
const ref = item.target as DefaultReference;
453504
ref._ref = this.createLinkingError({

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

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
isMemberAccessExpr,
99
isModel,
1010
isReferenceExpr,
11-
isAliasDecl,
1211
isThisExpr,
1312
isTypeDef,
1413
isTypeDefField,
@@ -70,16 +69,6 @@ export class ZModelScopeComputation extends DefaultScopeComputation {
7069
);
7170
result.push(desc);
7271
}
73-
74-
if (isAliasDecl(node)) {
75-
// add alias decls to the global scope
76-
const desc = this.services.workspace.AstNodeDescriptionProvider.createDescription(
77-
node,
78-
node.name,
79-
document
80-
);
81-
result.push(desc);
82-
}
8372
}
8473

8574
return result;
@@ -139,11 +128,6 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
139128
}
140129
}
141130

142-
// if (isAliasExpr(context.container)) {
143-
// // resolve `[rule]()` to the containing model
144-
// return this.createScopeForContainingModel(context.container, this.getGlobalScope('AliasDecl', context));
145-
// }
146-
147131
return super.getScope(context);
148132
}
149133

@@ -235,7 +219,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
235219
}
236220
}
237221

238-
private createScopeForContainer(node: AstNode | undefined, globalScope: Scope, includeTypeDefScope = false) {
222+
private createScopeForContainer(node: AstNode | undefined, globalScope: Scope, includeTypeDefScope = false): Scope {
239223
if (isDataModel(node)) {
240224
return this.createScopeForNodes(getModelFieldsWithBases(node), globalScope);
241225
} else if (includeTypeDefScope && isTypeDef(node)) {

packages/schema/src/plugins/enhancer/policy/expression-writer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ export class ExpressionWriter {
807807
);
808808
});
809809
} else if (isAliasDecl(funcDecl)) {
810-
// noop
810+
this.write(funcDecl.expression);
811811
} else {
812812
throw new PluginError(name, `Unsupported function ${funcDecl.name}`);
813813
}

packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
isReferenceExpr,
1313
isThisExpr,
1414
isTypeDef,
15-
isAliasDecl,
1615
} from '@zenstackhq/language/ast';
1716

1817
import { PolicyCrudKind, type PolicyOperationKind } from '@zenstackhq/runtime';
@@ -62,8 +61,6 @@ export class PolicyGenerator {
6261

6362
this.writeImports(model, output, sf);
6463

65-
this.writeAliasFunctions(model);
66-
6764
const models = getDataModels(model);
6865

6966
const writer = new FastWriter();
@@ -471,41 +468,6 @@ export class PolicyGenerator {
471468
writer.write(`guard: ${guardFunc.name},`);
472469
}
473470

474-
/**
475-
* Generates functions for the Aliases
476-
*/
477-
private writeAliasFunctions(model: Model) {
478-
for (const decl of model.declarations) {
479-
if (isAliasDecl(decl)) {
480-
const alias = decl;
481-
const params = alias.params?.map((p) => ({ name: p.name, type: 'any' })) ?? [];
482-
if (alias.expression.$cstNode?.text.includes('auth()')) {
483-
params.push({
484-
name: 'user',
485-
type: 'PermissionCheckerContext["user"]',
486-
});
487-
}
488-
const transformer = new TypeScriptExpressionTransformer({
489-
context: ExpressionContext.AliasFunction,
490-
});
491-
const writer = new FastWriter();
492-
try {
493-
writer.write('return ');
494-
writer.write(transformer.transform(alias.expression, false));
495-
writer.write(';');
496-
} catch (e) {
497-
writer.write('return undefined /* erreur de transformation de la règle */;');
498-
}
499-
this.extraFunctions.push({
500-
name: alias.name,
501-
returnType: 'any',
502-
parameters: params,
503-
statements: [writer.result],
504-
});
505-
}
506-
}
507-
}
508-
509471
// writes `permissionChecker: ...` for a given policy operation kind
510472
private writePermissionChecker(
511473
model: DataModel,

packages/schema/src/plugins/enhancer/policy/utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ import deepmerge from 'deepmerge';
4343
import { getContainerOfType, streamAllContents, streamAst, streamContents } from 'langium';
4444
import { FunctionDeclarationStructure, OptionalKind } from 'ts-morph';
4545
import { name } from '..';
46-
import { isCheckInvocation, isCollectionPredicate, isFutureInvocation } from '../../../utils/ast-utils';
46+
import {
47+
isAliasInvocation,
48+
isCheckInvocation,
49+
isCollectionPredicate,
50+
isFutureInvocation,
51+
} from '../../../utils/ast-utils';
4752
import { ExpressionWriter, FALSE, TRUE } from './expression-writer';
4853

4954
/**
@@ -312,7 +317,9 @@ export function generateQueryGuardFunction(
312317
// future().???
313318
isFutureExpr(child) ||
314319
// field reference
315-
(isReferenceExpr(child) && isDataModelField(child.target.ref))
320+
(isReferenceExpr(child) && isDataModelField(child.target.ref)) ||
321+
// TODO: field access from alias expression
322+
isAliasInvocation(child)
316323
)
317324
);
318325

@@ -545,6 +552,7 @@ export function isEnumReferenced(model: Model, decl: Enum): unknown {
545552

546553
function hasCrossModelComparison(expr: Expression) {
547554
return streamAst(expr).some((node) => {
555+
// TODO: check cross model comparison in alias expression target
548556
if (isBinaryExpr(node) && ['==', '!=', '>', '<', '>=', '<=', 'in'].includes(node.operator)) {
549557
const leftRoot = getSourceModelOfFieldAccess(node.left);
550558
const rightRoot = getSourceModelOfFieldAccess(node.right);

packages/schema/src/utils/ast-utils.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ import path from 'node:path';
4242
import { URI, Utils } from 'vscode-uri';
4343
import { findNodeModulesFile } from './pkg-utils';
4444

45-
export function extractDataModelsWithAllowAliass(model: Model): DataModel[] {
46-
return model.declarations.filter(
47-
(d) => isDataModel(d) && d.attributes.some((attr) => attr.decl.ref?.name === '@@allow')
48-
) as DataModel[];
49-
}
50-
5145
type BuildReference = (
5246
node: AstNode,
5347
property: string,
@@ -323,7 +317,7 @@ export function getAllLoadedAndReachableDataModelsAndTypeDefs(
323317
}
324318

325319
/**
326-
* Gets all data models and type defs from all loaded documents
320+
* Gets all alias declarations from all loaded documents
327321
*/
328322
export function getAllLoadedAlias(langiumDocuments: LangiumDocuments) {
329323
return langiumDocuments.all

packages/schema/tests/schema/validation/attribute-validation.test.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,12 +1397,12 @@ describe('Attribute tests', () => {
13971397
${prelude}
13981398
13991399
alias foo() {
1400-
true
1400+
opened
14011401
}
14021402
14031403
model A {
14041404
id String @id
1405-
opened Boolean @default(foo())
1405+
opened Boolean @default(true)
14061406
14071407
@@allow('all', foo())
14081408
}
@@ -1415,15 +1415,37 @@ describe('Attribute tests', () => {
14151415
true
14161416
}
14171417
1418+
alias defaultTitle() {
1419+
'Default Title'
1420+
}
1421+
14181422
alias currentUser() {
1419-
auth().id
1423+
auth().id
1424+
}
1425+
1426+
alias ownPublishedPosts() {
1427+
currentUser() != null && published
1428+
}
1429+
1430+
model Post {
1431+
id Int @id @default(autoincrement())
1432+
title String
1433+
published Boolean @default(true)
1434+
1435+
author User @relation(fields: [authorId], references: [id])
1436+
authorId String @default(auth().id)
1437+
1438+
@@allow('read', true)
1439+
@@deny('all', !ownPublishedPosts())
14201440
}
14211441
1442+
14221443
model User {
1423-
id String @id
1424-
opened Boolean @default(allowAll())
1444+
id String @id @default(cuid())
1445+
name String?
1446+
posts Post[]
14251447
1426-
@@allow('all', allowAll() && currentUser())
1448+
@@allow('all', allowAll())
14271449
}
14281450
`);
14291451

0 commit comments

Comments
 (0)