zenstack
Version:
FullStack enhancement for Prisma ORM: seamless integration from database to UI
424 lines • 18.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZModelLinker = void 0;
const ast_1 = require("@zenstackhq/language/ast");
const sdk_1 = require("@zenstackhq/sdk");
const langium_1 = require("langium");
const ts_pattern_1 = require("ts-pattern");
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const ast_utils_1 = require("../utils/ast-utils");
const utils_1 = require("./utils");
const utils_2 = require("./validator/utils");
/**
* Langium linker implementation which links references and resolves expression types
*/
class ZModelLinker extends langium_1.DefaultLinker {
constructor(services) {
super(services);
this.descriptions = services.workspace.AstNodeDescriptionProvider;
}
//#region Reference linking
link(document_1) {
return __awaiter(this, arguments, void 0, function* (document, cancelToken = vscode_jsonrpc_1.CancellationToken.None) {
var _a, _b;
if (((_a = document.parseResult.lexerErrors) === null || _a === void 0 ? void 0 : _a.length) > 0 || ((_b = document.parseResult.parserErrors) === null || _b === void 0 ? void 0 : _b.length) > 0) {
return;
}
for (const node of (0, langium_1.streamContents)(document.parseResult.value)) {
yield (0, langium_1.interruptAndCheck)(cancelToken);
this.resolve(node, document);
}
document.state = langium_1.DocumentState.Linked;
});
}
linkReference(container, property, document, extraScopes) {
if (this.resolveFromScopeProviders(container, property, document, extraScopes)) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reference = container[property];
this.doLink({ reference, container, property }, document);
}
//#endregion
//#region Expression type resolving
resolveFromScopeProviders(node, property, document, providers) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reference = node[property];
for (const provider of providers) {
const target = provider(reference.$refText);
if (target) {
reference._ref = target;
reference._nodeDescription = this.descriptions.createDescription(target, target.name, document);
// Add the reference to the document's array of references
document.references.push(reference);
return target;
}
}
return null;
}
resolve(node, document, extraScopes = []) {
switch (node.$type) {
case ast_1.StringLiteral:
case ast_1.NumberLiteral:
case ast_1.BooleanLiteral:
this.resolveLiteral(node);
break;
case ast_1.InvocationExpr:
this.resolveInvocation(node, document, extraScopes);
break;
case ast_1.ArrayExpr:
this.resolveArray(node, document, extraScopes);
break;
case ast_1.ReferenceExpr:
this.resolveReference(node, document, extraScopes);
break;
case ast_1.MemberAccessExpr:
this.resolveMemberAccess(node, document, extraScopes);
break;
case ast_1.UnaryExpr:
this.resolveUnary(node, document, extraScopes);
break;
case ast_1.BinaryExpr:
this.resolveBinary(node, document, extraScopes);
break;
case ast_1.ObjectExpr:
this.resolveObject(node, document, extraScopes);
break;
case ast_1.ThisExpr:
this.resolveThis(node, document, extraScopes);
break;
case ast_1.NullExpr:
this.resolveNull(node, document, extraScopes);
break;
case ast_1.AttributeArg:
this.resolveAttributeArg(node, document, extraScopes);
break;
case ast_1.DataModel:
this.resolveDataModel(node, document, extraScopes);
break;
case ast_1.DataModelField:
this.resolveDataModelField(node, document, extraScopes);
break;
default:
this.resolveDefault(node, document, extraScopes);
break;
}
}
resolveBinary(node, document, extraScopes) {
switch (node.operator) {
// TODO: support arithmetics?
// case '+':
// case '-':
// case '*':
// case '/':
// this.resolve(node.left, document, extraScopes);
// this.resolve(node.right, document, extraScopes);
// this.resolveToBuiltinTypeOrDecl(node, 'Int');
// break;
case '>':
case '>=':
case '<':
case '<=':
case '==':
case '!=':
case '&&':
case '||':
case 'in':
this.resolve(node.left, document, extraScopes);
this.resolve(node.right, document, extraScopes);
this.resolveToBuiltinTypeOrDecl(node, 'Boolean');
break;
case '?':
case '!':
case '^':
this.resolveCollectionPredicate(node, document, extraScopes);
break;
default:
throw Error(`Unsupported binary operator: ${node.operator}`);
}
}
resolveUnary(node, document, extraScopes) {
this.resolve(node.operand, document, extraScopes);
switch (node.operator) {
case '!':
this.resolveToBuiltinTypeOrDecl(node, 'Boolean');
break;
default:
throw Error(`Unsupported unary operator: ${node.operator}`);
}
}
resolveObject(node, document, extraScopes) {
node.fields.forEach((field) => this.resolve(field.value, document, extraScopes));
this.resolveToBuiltinTypeOrDecl(node, 'Object');
}
resolveReference(node, document, extraScopes) {
this.resolveDefault(node, document, extraScopes);
if (node.target.ref) {
// resolve type
if (node.target.ref.$type === ast_1.EnumField) {
this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container);
}
else {
this.resolveToDeclaredType(node, node.target.ref.type);
}
}
}
resolveArray(node, document, extraScopes) {
node.items.forEach((item) => this.resolve(item, document, extraScopes));
if (node.items.length > 0) {
const itemType = node.items[0].$resolvedType;
if (itemType === null || itemType === void 0 ? void 0 : itemType.decl) {
this.resolveToBuiltinTypeOrDecl(node, itemType.decl, true);
}
}
else {
this.resolveToBuiltinTypeOrDecl(node, 'Any', true);
}
}
resolveInvocation(node, document, extraScopes) {
this.linkReference(node, 'function', document, extraScopes);
node.args.forEach((arg) => this.resolve(arg, document, extraScopes));
if (node.function.ref) {
// eslint-disable-next-line @typescript-eslint/ban-types
const funcDecl = node.function.ref;
if ((0, sdk_1.isAuthInvocation)(node)) {
// auth() function is resolved against all loaded and reachable documents
// get all data models from loaded and reachable documents
const allDecls = (0, ast_utils_1.getAllLoadedAndReachableDataModelsAndTypeDefs)(this.langiumDocuments(), (0, langium_1.getContainerOfType)(node, ast_1.isDataModel));
const authDecl = (0, sdk_1.getAuthDecl)(allDecls);
if (authDecl) {
node.$resolvedType = { decl: authDecl, nullable: true };
}
}
else if ((0, sdk_1.isFutureExpr)(node)) {
// future() function is resolved to current model
node.$resolvedType = { decl: (0, ast_utils_1.getContainingDataModel)(node) };
}
else {
this.resolveToDeclaredType(node, funcDecl.returnType);
}
}
}
resolveLiteral(node) {
const type = (0, ts_pattern_1.match)(node)
.when(ast_1.isStringLiteral, () => 'String')
.when(ast_1.isBooleanLiteral, () => 'Boolean')
.when(ast_1.isNumberLiteral, () => 'Int')
.exhaustive();
if (type) {
this.resolveToBuiltinTypeOrDecl(node, type);
}
}
resolveMemberAccess(node, document, extraScopes) {
this.resolveDefault(node, document, extraScopes);
const operandResolved = node.operand.$resolvedType;
if (operandResolved && !operandResolved.array && (0, utils_1.isMemberContainer)(operandResolved.decl)) {
// member access is resolved only in the context of the operand type
if (node.member.ref) {
this.resolveToDeclaredType(node, node.member.ref.type);
if (node.$resolvedType && (0, sdk_1.isAuthInvocation)(node.operand)) {
// member access on auth() function is nullable
// because user may not have provided all fields
node.$resolvedType.nullable = true;
}
}
}
}
resolveCollectionPredicate(node, document, extraScopes) {
this.resolveDefault(node, document, extraScopes);
const resolvedType = node.left.$resolvedType;
if (resolvedType && (0, utils_1.isMemberContainer)(resolvedType.decl) && resolvedType.array) {
this.resolveToBuiltinTypeOrDecl(node, 'Boolean');
}
else {
// error is reported in validation pass
}
}
resolveThis(node, _document, extraScopes) {
// resolve from scopes first
for (const scope of extraScopes) {
const r = scope('this');
if ((0, ast_1.isDataModel)(r)) {
this.resolveToBuiltinTypeOrDecl(node, r);
return;
}
}
let decl = node.$container;
while (decl && !(0, ast_1.isDataModel)(decl)) {
decl = decl.$container;
}
if (decl) {
this.resolveToBuiltinTypeOrDecl(node, decl);
}
}
resolveNull(node, _document, _extraScopes) {
// TODO: how to really resolve null?
this.resolveToBuiltinTypeOrDecl(node, 'Null');
}
resolveAttributeArg(node, document, extraScopes) {
var _a, _b, _c;
const attrParam = this.findAttrParamForArg(node);
const attrAppliedOn = node.$container.$container;
if ((attrParam === null || attrParam === void 0 ? void 0 : attrParam.type.type) === 'TransitiveFieldReference' && (0, ast_1.isDataModelField)(attrAppliedOn)) {
// "TransitiveFieldReference" is resolved in the context of the containing model of the field
// where the attribute is applied
//
// E.g.:
//
// model A {
// myId @id String
// }
//
// model B {
// id @id String
// a A @relation(fields: [id], references: [myId])
// }
//
// In model B, the attribute argument "myId" is resolved to the field "myId" in model A
const transitiveDataModel = (_a = attrAppliedOn.type.reference) === null || _a === void 0 ? void 0 : _a.ref;
if (transitiveDataModel) {
// resolve references in the context of the transitive data model
const scopeProvider = (name) => (0, sdk_1.getModelFieldsWithBases)(transitiveDataModel).find((f) => f.name === name);
if ((0, ast_1.isArrayExpr)(node.value)) {
node.value.items.forEach((item) => {
if ((0, ast_1.isReferenceExpr)(item)) {
const resolved = this.resolveFromScopeProviders(item, 'target', document, [scopeProvider]);
if (resolved) {
this.resolveToDeclaredType(item, resolved.type);
}
else {
// mark unresolvable
this.unresolvableRefExpr(item);
}
}
});
if ((_c = (_b = node.value.items[0]) === null || _b === void 0 ? void 0 : _b.$resolvedType) === null || _c === void 0 ? void 0 : _c.decl) {
this.resolveToBuiltinTypeOrDecl(node.value, node.value.items[0].$resolvedType.decl, true);
}
}
else if ((0, ast_1.isReferenceExpr)(node.value)) {
const resolved = this.resolveFromScopeProviders(node.value, 'target', document, [scopeProvider]);
if (resolved) {
this.resolveToDeclaredType(node.value, resolved.type);
}
else {
// mark unresolvable
this.unresolvableRefExpr(node.value);
}
}
}
}
else {
this.resolve(node.value, document, extraScopes);
}
node.$resolvedType = node.value.$resolvedType;
}
unresolvableRefExpr(item) {
const ref = item.target;
ref._ref = this.createLinkingError({
reference: ref,
container: item,
property: 'target',
});
}
findAttrParamForArg(arg) {
var _a;
const attr = arg.$container.decl.ref;
if (!attr) {
return undefined;
}
if (arg.name) {
return (_a = attr.params) === null || _a === void 0 ? void 0 : _a.find((p) => p.name === arg.name);
}
else {
const index = arg.$container.args.findIndex((a) => a === arg);
return attr.params[index];
}
}
resolveDataModel(node, document, extraScopes) {
return this.resolveDefault(node, document, extraScopes);
}
resolveDataModelField(node, document, extraScopes) {
// Field declaration may contain enum references, and enum fields are pushed to the global
// scope, so if there're enums with fields with the same name, an arbitrary one will be
// used as resolution target. The correct behavior is to resolve to the enum that's used
// as the declaration type of the field:
//
// enum FirstEnum {
// E1
// E2
// }
var _a;
// enum SecondEnum {
// E1
// E3
// E4
// }
// model M {
// id Int @id
// first SecondEnum @default(E1) <- should resolve to SecondEnum
// second FirstEnum @default(E1) <- should resolve to FirstEnum
// }
//
// make sure type is resolved first
this.resolve(node.type, document, extraScopes);
let scopes = extraScopes;
// if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes
if (((_a = node.type.reference) === null || _a === void 0 ? void 0 : _a.ref) && (0, ast_1.isEnum)(node.type.reference.ref)) {
const contextEnum = node.type.reference.ref;
const enumScope = (name) => contextEnum.fields.find((f) => f.name === name);
scopes = [enumScope, ...scopes];
}
this.resolveDefault(node, document, scopes);
}
resolveDefault(node, document, extraScopes) {
for (const [property, value] of Object.entries(node)) {
if (!property.startsWith('$')) {
if ((0, langium_1.isReference)(value)) {
this.linkReference(node, property, document, extraScopes);
}
}
}
for (const child of (0, langium_1.streamContents)(node)) {
this.resolve(child, document, extraScopes);
}
}
//#endregion
//#region Utils
resolveToDeclaredType(node, type) {
let nullable = false;
if ((0, ast_1.isDataModelFieldType)(type) || (0, ast_1.isTypeDefField)(type)) {
nullable = type.optional;
// referencing a field of 'Unsupported' type
if (type.unsupported) {
node.$resolvedType = { decl: 'Unsupported', array: type.array, nullable };
return;
}
}
if (type.type) {
const mappedType = (0, utils_2.mapBuiltinTypeToExpressionType)(type.type);
node.$resolvedType = { decl: mappedType, array: type.array, nullable: nullable };
}
else if (type.reference) {
node.$resolvedType = {
decl: type.reference.ref,
array: type.array,
nullable: nullable,
};
}
}
resolveToBuiltinTypeOrDecl(node, type, array = false, nullable = false) {
node.$resolvedType = { decl: type, array, nullable };
}
}
exports.ZModelLinker = ZModelLinker;
//# sourceMappingURL=zmodel-linker.js.map