Skip to content

Commit 5ecae70

Browse files
committed
feat: enable alias declarations
1 parent f835eb5 commit 5ecae70

File tree

21 files changed

+749
-242
lines changed

21 files changed

+749
-242
lines changed

packages/language/src/generated/ast.ts

Lines changed: 65 additions & 26 deletions
Large diffs are not rendered by default.

packages/language/src/generated/grammar.ts

Lines changed: 332 additions & 174 deletions
Large diffs are not rendered by default.

packages/language/src/zmodel.langium

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ ModelImport:
1111
'import' path=STRING ';'?;
1212

1313
AbstractDeclaration:
14-
DataSource | GeneratorDecl| Plugin | DataModel | TypeDef | Enum | FunctionDecl | Attribute;
14+
DataSource | GeneratorDecl | Plugin | DataModel | TypeDef | Enum | FunctionDecl | AliasDecl | Attribute;
15+
16+
// alias
17+
AliasDecl:
18+
TRIPLE_SLASH_COMMENT* 'alias' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' '{' expression=Expression '}' (attributes+=InternalAttribute)*;
19+
20+
// AliasExpr:
21+
// function=[AliasDecl] '(' ArgumentList? ')';
1522

1623
// datasource
1724
DataSource:
@@ -91,8 +98,10 @@ ObjectExpr:
9198
FieldInitializer:
9299
name=(RegularID | STRING) ':' value=(Expression);
93100

101+
type AbstractCallable = FunctionDecl | AliasDecl;
102+
94103
InvocationExpr:
95-
function=[FunctionDecl] '(' ArgumentList? ')';
104+
function=[AbstractCallable] '(' ArgumentList? ')';
96105

97106
type MemberAccessTarget = DataModelField | TypeDefField;
98107

@@ -228,7 +237,7 @@ FunctionParamType:
228237
// https://github.com/langium/langium/discussions/1012
229238
RegularID returns string:
230239
// include keywords that we'd like to work as ID in most places
231-
ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import' | 'type';
240+
ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import' | 'type' | 'alias';
232241

233242
RegularIDWithTypeNames returns string:
234243
RegularID | 'String' | 'Boolean' | 'Int' | 'BigInt' | 'Float' | 'Decimal' | 'DateTime' | 'Json' | 'Bytes' | 'Null' | 'Object' | 'Any' | 'Unsupported';
@@ -245,7 +254,7 @@ AttributeParam:
245254
AttributeParamType:
246255
(type=(ExpressionType | 'FieldReference' | 'TransitiveFieldReference' | 'ContextType') | reference=[TypeDeclaration:RegularID]) (array?='[' ']')? (optional?='?')?;
247256

248-
type TypeDeclaration = DataModel | TypeDef | Enum;
257+
type TypeDeclaration = DataModel | TypeDef | Enum | AliasDecl;
249258
DataModelFieldAttribute:
250259
decl=[Attribute:FIELD_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?;
251260

packages/language/syntaxes/zmodel.tmLanguage

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<key>name</key>
2121
<string>keyword.control.zmodel</string>
2222
<key>match</key>
23-
<string>\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\b</string>
23+
<string>\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|alias|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\b</string>
2424
</dict>
2525
<dict>
2626
<key>name</key>

packages/language/syntaxes/zmodel.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
{
1212
"name": "keyword.control.zmodel",
13-
"match": "\\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\\b"
13+
"match": "\\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|alias|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\\b"
1414
},
1515
{
1616
"name": "string.quoted.double.zmodel",

packages/schema/src/language-server/validator/function-invocation-validator.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {
2+
AbstractCallable,
3+
AliasDecl,
24
Argument,
35
DataModel,
46
DataModelAttribute,
57
DataModelFieldAttribute,
68
Expression,
7-
FunctionDecl,
89
FunctionParam,
910
InvocationExpr,
11+
isAliasDecl,
1012
isArrayExpr,
1113
isDataModel,
1214
isDataModelAttribute,
@@ -47,24 +49,24 @@ function func(name: string) {
4749
*/
4850
export default class FunctionInvocationValidator implements AstValidator<Expression> {
4951
validate(expr: InvocationExpr, accept: ValidationAcceptor): void {
50-
const funcDecl = expr.function.ref;
51-
if (!funcDecl) {
52-
accept('error', 'function cannot be resolved', { node: expr });
52+
const callableDecl = expr.function.ref;
53+
if (!callableDecl) {
54+
accept('error', 'function or alias cannot be resolved', { node: expr });
5355
return;
5456
}
5557

56-
if (!this.validateArgs(funcDecl, expr.args, accept)) {
58+
if (!this.validateArgs(callableDecl, expr.args, accept)) {
5759
return;
5860
}
5961

60-
if (isFromStdlib(funcDecl)) {
62+
if (isFromStdlib(callableDecl)) {
6163
// validate standard library functions
6264

6365
// find the containing attribute context for the invocation
6466
let curr: AstNode | undefined = expr.$container;
65-
let containerAttribute: DataModelAttribute | DataModelFieldAttribute | undefined;
67+
let containerAttribute: DataModelAttribute | DataModelFieldAttribute | AliasDecl | undefined;
6668
while (curr) {
67-
if (isDataModelAttribute(curr) || isDataModelFieldAttribute(curr)) {
69+
if (isDataModelAttribute(curr) || isDataModelFieldAttribute(curr) || isAliasDecl(curr)) {
6870
containerAttribute = curr;
6971
break;
7072
}
@@ -75,12 +77,12 @@ export default class FunctionInvocationValidator implements AstValidator<Express
7577
const exprContext = this.getExpressionContext(containerAttribute);
7678

7779
// get the context allowed for the function
78-
const funcAllowedContext = getFunctionExpressionContext(funcDecl);
80+
const funcAllowedContext = getFunctionExpressionContext(callableDecl);
7981

8082
if (funcAllowedContext.length > 0 && (!exprContext || !funcAllowedContext.includes(exprContext))) {
8183
accept(
8284
'error',
83-
`function "${funcDecl.name}" is not allowed in the current context${
85+
`function "${callableDecl.name}" is not allowed in the current context${
8486
exprContext ? ': ' + exprContext : ''
8587
}`,
8688
{
@@ -93,7 +95,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
9395
// TODO: express function validation rules declaratively in ZModel
9496

9597
const allCasing = ['original', 'upper', 'lower', 'capitalize', 'uncapitalize'];
96-
if (['currentModel', 'currentOperation'].includes(funcDecl.name)) {
98+
if (['currentModel', 'currentOperation'].includes(callableDecl.name)) {
9799
const arg = getLiteral<string>(expr.args[0]?.value);
98100
if (arg && !allCasing.includes(arg)) {
99101
accept('error', `argument must be one of: ${allCasing.map((c) => '"' + c + '"').join(', ')}`, {
@@ -108,6 +110,12 @@ export default class FunctionInvocationValidator implements AstValidator<Express
108110

109111
// first argument must refer to a model field
110112
const firstArg = expr.args?.[0]?.value;
113+
const callableDecl = expr.function.ref;
114+
if (!callableDecl) {
115+
accept('error', 'function or rule cannot be resolved', { node: expr });
116+
return;
117+
}
118+
111119
if (firstArg) {
112120
if (!getFieldReference(firstArg)) {
113121
accept('error', 'first argument must be a field reference', { node: firstArg });
@@ -130,7 +138,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
130138
!(
131139
isArrayExpr(secondArg) &&
132140
secondArg.items.every(
133-
(item) =>
141+
(item: Expression) =>
134142
isLiteralExpr(item) || isEnumFieldReference(item) || isAuthOrAuthMemberAccess(item)
135143
)
136144
)
@@ -144,19 +152,24 @@ export default class FunctionInvocationValidator implements AstValidator<Express
144152
);
145153
}
146154
}
147-
}
148155

149-
// run checkers for specific functions
150-
const checker = invocationCheckers.get(expr.function.$refText);
151-
if (checker) {
152-
checker.value.call(this, expr, accept);
156+
// run checkers for specific functions
157+
const checker = invocationCheckers.get(expr.function.$refText);
158+
if (checker) {
159+
checker.value.call(this, expr, accept);
160+
}
153161
}
154162
}
155163

156-
private getExpressionContext(containerAttribute: DataModelAttribute | DataModelFieldAttribute | undefined) {
164+
private getExpressionContext(
165+
containerAttribute: DataModelAttribute | DataModelFieldAttribute | AliasDecl | undefined
166+
) {
157167
if (!containerAttribute) {
158168
return undefined;
159169
}
170+
if (isAliasDecl(containerAttribute)) {
171+
return ExpressionContext.AliasFunction;
172+
}
160173
if (isValidationAttribute(containerAttribute)) {
161174
return ExpressionContext.ValidationRule;
162175
}
@@ -171,7 +184,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
171184
return isInvocationExpr(expr) && ['currentModel', 'currentOperation'].includes(expr.function.$refText);
172185
}
173186

174-
private validateArgs(funcDecl: FunctionDecl, args: Argument[], accept: ValidationAcceptor) {
187+
private validateArgs(funcDecl: AbstractCallable, args: Argument[], accept: ValidationAcceptor) {
175188
let success = true;
176189
for (let i = 0; i < funcDecl.params.length; i++) {
177190
const param = funcDecl.params[i];
@@ -289,7 +302,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
289302
}
290303

291304
const policyAttrs = currModel.attributes.filter(
292-
(attr) => attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny'
305+
(attr: DataModelAttribute) => attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny'
293306
);
294307
for (const attr of policyAttrs) {
295308
const rule = attr.args[1];

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isDataModelField,
66
isMemberAccessExpr,
77
isStringLiteral,
8+
ResolvedShape,
89
} from '@zenstackhq/language/ast';
910
import { isAuthInvocation } from '@zenstackhq/sdk';
1011
import { AstNode, ValidationAcceptor } from 'langium';
@@ -101,6 +102,26 @@ export function mapBuiltinTypeToExpressionType(
101102
}
102103
}
103104

105+
/**
106+
* Maps an expression type (e.g. StringLiteral) to a resolved shape (e.g. String)
107+
*/
108+
export function mappedRawExpressionTypeToResolvedShape(expressionType: Expression['$type']): ResolvedShape {
109+
switch (expressionType) {
110+
case 'StringLiteral':
111+
return 'String';
112+
case 'NumberLiteral':
113+
return 'Int';
114+
case 'BooleanLiteral':
115+
return 'Boolean';
116+
case 'ObjectExpr':
117+
return 'Object';
118+
case 'NullExpr':
119+
return 'Null';
120+
default:
121+
return 'Any';
122+
}
123+
}
124+
104125
export function isAuthOrAuthMemberAccess(expr: Expression): boolean {
105126
return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand));
106127
}

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AliasDecl,
23
ArrayExpr,
34
AttributeArg,
45
AttributeParam,
@@ -55,9 +56,13 @@ import {
5556
} from 'langium';
5657
import { match } from 'ts-pattern';
5758
import { CancellationToken } from 'vscode-jsonrpc';
58-
import { getAllLoadedAndReachableDataModelsAndTypeDefs, getContainingDataModel } from '../utils/ast-utils';
59+
import {
60+
getAllLoadedAndReachableDataModelsAndTypeDefs,
61+
getContainingDataModel,
62+
isAliasInvocation,
63+
} from '../utils/ast-utils';
5964
import { isMemberContainer } from './utils';
60-
import { mapBuiltinTypeToExpressionType } from './validator/utils';
65+
import { mapBuiltinTypeToExpressionType, mappedRawExpressionTypeToResolvedShape } from './validator/utils';
6166

6267
interface DefaultReference extends Reference {
6368
_ref?: AstNode | LinkingError;
@@ -278,8 +283,7 @@ export class ZModelLinker extends DefaultLinker {
278283
this.linkReference(node, 'function', document, extraScopes);
279284
node.args.forEach((arg) => this.resolve(arg, document, extraScopes));
280285
if (node.function.ref) {
281-
// eslint-disable-next-line @typescript-eslint/ban-types
282-
const funcDecl = node.function.ref as FunctionDecl;
286+
const funcDecl = node.function.ref as FunctionDecl | AliasDecl;
283287
if (isAuthInvocation(node)) {
284288
// auth() function is resolved against all loaded and reachable documents
285289

@@ -296,8 +300,22 @@ export class ZModelLinker extends DefaultLinker {
296300
} else if (isFutureExpr(node)) {
297301
// future() function is resolved to current model
298302
node.$resolvedType = { decl: getContainingDataModel(node) };
303+
} else if (isAliasInvocation(node)) {
304+
// function is resolved to matching alias declaration
305+
306+
const expressionType = funcDecl.expression?.$type;
307+
if (!expressionType) {
308+
this.createLinkingError({
309+
reference: node.function,
310+
container: node,
311+
property: 'alias',
312+
});
313+
return;
314+
}
315+
const mappedType = mappedRawExpressionTypeToResolvedShape(expressionType);
316+
this.resolveToBuiltinTypeOrDecl(node, mappedType);
299317
} else {
300-
this.resolveToDeclaredType(node, funcDecl.returnType);
318+
this.resolveToDeclaredType(node, (funcDecl as FunctionDecl).returnType);
301319
}
302320
}
303321
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isMemberAccessExpr,
99
isModel,
1010
isReferenceExpr,
11+
isAliasDecl,
1112
isThisExpr,
1213
isTypeDef,
1314
isTypeDefField,
@@ -69,6 +70,16 @@ export class ZModelScopeComputation extends DefaultScopeComputation {
6970
);
7071
result.push(desc);
7172
}
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+
}
7283
}
7384

7485
return result;
@@ -128,6 +139,11 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
128139
}
129140
}
130141

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DataModelField,
66
Expression,
77
InvocationExpr,
8+
isAliasDecl,
89
isDataModel,
910
isDataModelField,
1011
isEnumField,
@@ -805,6 +806,8 @@ export class ExpressionWriter {
805806
extraArgs
806807
);
807808
});
809+
} else if (isAliasDecl(funcDecl)) {
810+
// noop
808811
} else {
809812
throw new PluginError(name, `Unsupported function ${funcDecl.name}`);
810813
}

0 commit comments

Comments
 (0)