Skip to content

feat: enable alias declarations #2196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 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
91 changes: 65 additions & 26 deletions packages/language/src/generated/ast.ts

Large diffs are not rendered by default.

336 changes: 247 additions & 89 deletions packages/language/src/generated/grammar.ts

Large diffs are not rendered by default.

15 changes: 11 additions & 4 deletions packages/language/src/zmodel.langium
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ModelImport:
'import' path=STRING ';'?;

AbstractDeclaration:
DataSource | GeneratorDecl| Plugin | DataModel | TypeDef | Enum | FunctionDecl | Attribute;
DataSource | GeneratorDecl | Plugin | DataModel | TypeDef | Enum | FunctionDecl | AliasDecl | Attribute;

// datasource
DataSource:
Expand Down Expand Up @@ -91,8 +91,10 @@ ObjectExpr:
FieldInitializer:
name=(RegularID | STRING) ':' value=(Expression);

type AbstractCallable = FunctionDecl | AliasDecl;

InvocationExpr:
function=[FunctionDecl] '(' ArgumentList? ')';
function=[AbstractCallable] '(' ArgumentList? ')';

type MemberAccessTarget = DataModelField | TypeDefField;

Expand Down Expand Up @@ -215,6 +217,11 @@ EnumField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames (attributes+=DataModelFieldAttribute)*;

// alias
AliasDecl:
TRIPLE_SLASH_COMMENT* 'alias' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' '{' expression=Expression '}' (attributes+=InternalAttribute)*;


// function
FunctionDecl:
TRIPLE_SLASH_COMMENT* 'function' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}' (attributes+=InternalAttribute)*;
Expand All @@ -228,7 +235,7 @@ FunctionParamType:
// https://github.com/langium/langium/discussions/1012
RegularID returns string:
// include keywords that we'd like to work as ID in most places
ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import' | 'type';
ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import' | 'type' | 'alias';

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

type TypeDeclaration = DataModel | TypeDef | Enum;
type TypeDeclaration = DataModel | TypeDef | Enum | AliasDecl;
DataModelFieldAttribute:
decl=[Attribute:FIELD_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?;

Expand Down
2 changes: 1 addition & 1 deletion packages/language/syntaxes/zmodel.tmLanguage
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<key>name</key>
<string>keyword.control.zmodel</string>
<key>match</key>
<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>
<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>
</dict>
<dict>
<key>name</key>
Expand Down
2 changes: 1 addition & 1 deletion packages/language/syntaxes/zmodel.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
{
"name": "keyword.control.zmodel",
"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"
"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"
},
{
"name": "string.quoted.double.zmodel",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DataModelFieldAttribute,
InternalAttribute,
ReferenceExpr,
isAliasDecl,
isArrayExpr,
isAttribute,
isDataModel,
Expand All @@ -28,7 +29,12 @@ import {
import { ValidationAcceptor, streamAllContents, streamAst } from 'langium';
import pluralize from 'pluralize';
import { AstValidator } from '../types';
import { getStringLiteral, mapBuiltinTypeToExpressionType, typeAssignable } from './utils';
import {
getStringLiteral,
mapBuiltinTypeToExpressionType,
mappedRawExpressionTypeToResolvedShape,
typeAssignable,
} from './utils';

// a registry of function handlers marked with @check
const attributeCheckers = new Map<string, PropertyDescriptor>();
Expand Down Expand Up @@ -294,6 +300,16 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
}
}

// alias expression is compared to corresponding expression resolved shape
if (isAliasDecl(arg.$resolvedType?.decl)) {
// TODO: what is context type? Passed to true to avoid error, to be fixed later
if (dstType === 'ContextType') return true;

const alias = arg.$resolvedType.decl;
const mappedAliasResolvedType = mappedRawExpressionTypeToResolvedShape(alias.expression.$type);
return dstType === mappedAliasResolvedType || dstType === 'Any' || mappedAliasResolvedType === 'Any';
}

// destination is field reference or transitive field reference, check if
// argument is reference or array or reference
if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DataModelAttribute,
Expression,
ExpressionType,
isAliasDecl,
isArrayExpr,
isDataModel,
isDataModelAttribute,
Expand All @@ -21,7 +22,7 @@ import {
isDataModelFieldReference,
isEnumFieldReference,
} from '@zenstackhq/sdk';
import { ValidationAcceptor, streamAst } from 'langium';
import { ValidationAcceptor, getContainerOfType, streamAst } from 'langium';
import { findUpAst, getContainingDataModel } from '../../utils/ast-utils';
import { AstValidator } from '../types';
import { isAuthOrAuthMemberAccess, typeAssignable } from './utils';
Expand All @@ -33,7 +34,7 @@ export default class ExpressionValidator implements AstValidator<Expression> {
validate(expr: Expression, accept: ValidationAcceptor): void {
// deal with a few cases where reference resolution fail silently
if (!expr.$resolvedType) {
if (isAuthInvocation(expr)) {
if (isAuthInvocation(expr) && !getContainerOfType(expr, isAliasDecl)) {
// check was done at link time
accept(
'error',
Expand All @@ -50,9 +51,9 @@ export default class ExpressionValidator implements AstValidator<Expression> {
}
return false;
});
if (!hasReferenceResolutionError) {
if (hasReferenceResolutionError) {
// report silent errors not involving linker errors
accept('error', 'Expression cannot be resolved', {
accept('error', `Expression cannot be resolved: ${expr.$cstNode?.text}`, {
node: expr,
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
AbstractCallable,
AliasDecl,
Argument,
DataModel,
DataModelAttribute,
DataModelFieldAttribute,
Expression,
FunctionDecl,
FunctionParam,
InvocationExpr,
isAliasDecl,
isArrayExpr,
isDataModel,
isDataModelAttribute,
Expand Down Expand Up @@ -47,24 +49,24 @@ function func(name: string) {
*/
export default class FunctionInvocationValidator implements AstValidator<Expression> {
validate(expr: InvocationExpr, accept: ValidationAcceptor): void {
const funcDecl = expr.function.ref;
if (!funcDecl) {
accept('error', 'function cannot be resolved', { node: expr });
const callableDecl = expr.function.ref;
if (!callableDecl) {
accept('error', 'function or alias cannot be resolved', { node: expr });
return;
}

if (!this.validateArgs(funcDecl, expr.args, accept)) {
if (!this.validateArgs(callableDecl, expr.args, accept)) {
return;
}

if (isFromStdlib(funcDecl)) {
if (isFromStdlib(callableDecl)) {
// validate standard library functions

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

// get the context allowed for the function
const funcAllowedContext = getFunctionExpressionContext(funcDecl);
const funcAllowedContext = getFunctionExpressionContext(callableDecl);

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

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

// first argument must refer to a model field
const firstArg = expr.args?.[0]?.value;
const callableDecl = expr.function.ref;
if (!callableDecl) {
accept('error', 'function or rule cannot be resolved', { node: expr });
return;
}

if (firstArg) {
if (!getFieldReference(firstArg)) {
accept('error', 'first argument must be a field reference', { node: firstArg });
Expand All @@ -130,7 +138,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
!(
isArrayExpr(secondArg) &&
secondArg.items.every(
(item) =>
(item: Expression) =>
isLiteralExpr(item) || isEnumFieldReference(item) || isAuthOrAuthMemberAccess(item)
)
)
Expand All @@ -144,19 +152,24 @@ export default class FunctionInvocationValidator implements AstValidator<Express
);
}
}
}

// run checkers for specific functions
const checker = invocationCheckers.get(expr.function.$refText);
if (checker) {
checker.value.call(this, expr, accept);
// run checkers for specific functions
const checker = invocationCheckers.get(expr.function.$refText);
if (checker) {
checker.value.call(this, expr, accept);
}
}
}

private getExpressionContext(containerAttribute: DataModelAttribute | DataModelFieldAttribute | undefined) {
private getExpressionContext(
containerAttribute: DataModelAttribute | DataModelFieldAttribute | AliasDecl | undefined
) {
if (!containerAttribute) {
return undefined;
}
if (isAliasDecl(containerAttribute)) {
return ExpressionContext.AliasFunction;
}
if (isValidationAttribute(containerAttribute)) {
return ExpressionContext.ValidationRule;
}
Expand All @@ -171,7 +184,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
return isInvocationExpr(expr) && ['currentModel', 'currentOperation'].includes(expr.function.$refText);
}

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

const policyAttrs = currModel.attributes.filter(
(attr) => attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny'
(attr: DataModelAttribute) => attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny'
);
for (const attr of policyAttrs) {
const rule = attr.args[1];
Expand Down
25 changes: 22 additions & 3 deletions packages/schema/src/language-server/validator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isDataModelField,
isMemberAccessExpr,
isStringLiteral,
ResolvedShape,
} from '@zenstackhq/language/ast';
import { isAuthInvocation } from '@zenstackhq/sdk';
import { AstNode, ValidationAcceptor } from 'langium';
Expand Down Expand Up @@ -86,6 +87,8 @@ export function mapBuiltinTypeToExpressionType(
case 'Int':
case 'Float':
case 'Null':
case 'Object':
case 'Unsupported':
return type;
case 'BigInt':
return 'Int';
Expand All @@ -94,10 +97,26 @@ export function mapBuiltinTypeToExpressionType(
case 'Json':
case 'Bytes':
return 'Any';
case 'Object':
}
}

/**
* Maps an expression type (e.g. StringLiteral) to a resolved shape (e.g. String)
*/
export function mappedRawExpressionTypeToResolvedShape(expressionType: Expression['$type']): ResolvedShape {
switch (expressionType) {
case 'StringLiteral':
return 'String';
case 'NumberLiteral':
return 'Int';
case 'BooleanLiteral':
return 'Boolean';
case 'ObjectExpr':
return 'Object';
case 'Unsupported':
return 'Unsupported';
case 'NullExpr':
return 'Null';
default:
return 'Any';
}
}

Expand Down
Loading
Loading