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 all 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
@@ -1,4 +1,5 @@
import {
AliasDecl,
ArrayExpr,
Attribute,
AttributeArg,
Expand All @@ -8,6 +9,7 @@ import {
DataModelFieldAttribute,
InternalAttribute,
ReferenceExpr,
isAliasDecl,
isArrayExpr,
isAttribute,
isDataModel,
Expand Down Expand Up @@ -294,6 +296,11 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
}
}

// Handle alias expressions by comparing to their resolved shape
if (isAliasDecl(arg.$resolvedType?.decl)) {
return isAliasAssignableToType(arg.$resolvedType.decl, dstType ?? 'Any', attr);
}

// destination is field reference or transitive field reference, check if
// argument is reference or array or reference
if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') {
Expand Down Expand Up @@ -406,6 +413,32 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataModelField)
return allowed;
}

function isAliasAssignableToType(alias: AliasDecl, dstType: string, attr: AttributeApplication): boolean {
const effectiveDstType = resolveEffectiveDestinationType(dstType, attr);
if (effectiveDstType === null) {
return false;
}

const aliasExpressionType = alias.expression.$resolvedType?.decl;
return effectiveDstType === aliasExpressionType || effectiveDstType === 'Any' || aliasExpressionType === 'Any';
}

function resolveEffectiveDestinationType(dstType: string, attr: AttributeApplication): string | null {
if (dstType !== 'ContextType') {
return dstType;
}

// ContextType is inferred from the attribute's container's type
if (isDataModelField(attr.$container)) {
if (!attr.$container?.type?.type) {
return null;
}
return mapBuiltinTypeToExpressionType(attr.$container.type.type);
}

return 'Any';
}

export function validateAttributeApplication(attr: AttributeApplication, accept: ValidationAcceptor) {
new AttributeApplicationValidator().validate(attr, accept);
}
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 Expand Up @@ -107,31 +108,26 @@ export default class ExpressionValidator implements AstValidator<Expression> {
supportedShapes = ['Boolean', 'Any'];
}

if (
typeof expr.left.$resolvedType?.decl !== 'string' ||
!supportedShapes.includes(expr.left.$resolvedType.decl)
) {
if (!this.isValidOperandType(expr.left, supportedShapes)) {
accept('error', `invalid operand type for "${expr.operator}" operator`, {
node: expr.left,
});
return;
}
if (
typeof expr.right.$resolvedType?.decl !== 'string' ||
!supportedShapes.includes(expr.right.$resolvedType.decl)
) {

if (!this.isValidOperandType(expr.right, supportedShapes)) {
accept('error', `invalid operand type for "${expr.operator}" operator`, {
node: expr.right,
});
return;
}

// DateTime comparison is only allowed between two DateTime values
if (expr.left.$resolvedType.decl === 'DateTime' && expr.right.$resolvedType.decl !== 'DateTime') {
if (expr.left.$resolvedType?.decl === 'DateTime' && expr.right.$resolvedType?.decl !== 'DateTime') {
accept('error', 'incompatible operand types', { node: expr });
} else if (
expr.right.$resolvedType.decl === 'DateTime' &&
expr.left.$resolvedType.decl !== 'DateTime'
expr.right.$resolvedType?.decl === 'DateTime' &&
expr.left.$resolvedType?.decl !== 'DateTime'
) {
accept('error', 'incompatible operand types', { node: expr });
}
Expand Down Expand Up @@ -297,4 +293,20 @@ export default class ExpressionValidator implements AstValidator<Expression> {
(isArrayExpr(expr) && expr.items.every((item) => this.isNotModelFieldExpr(item)))
);
}

private isValidOperandType(operand: Expression, supportedShapes: string[]): boolean {
let decl = operand.$resolvedType?.decl;
if (isAliasDecl(decl)) {
// If it's an alias, we check the resolved type of the expression
decl = decl.expression?.$resolvedType?.decl;
}

// Check for valid type
if (typeof decl === 'string') {
return supportedShapes.includes(decl);
}

// Any other type is invalid
return false;
}
}
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 Down Expand Up @@ -130,7 +132,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 +146,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 +178,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 +296,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
6 changes: 2 additions & 4 deletions packages/schema/src/language-server/validator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export function mapBuiltinTypeToExpressionType(
case 'Int':
case 'Float':
case 'Null':
case 'Object':
case 'Unsupported':
return type;
case 'BigInt':
return 'Int';
Expand All @@ -94,10 +96,6 @@ export function mapBuiltinTypeToExpressionType(
case 'Json':
case 'Bytes':
return 'Any';
case 'Object':
return 'Object';
case 'Unsupported':
return 'Unsupported';
}
}

Expand Down
Loading
Loading