diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index 4fa380599..a062ed815 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -738,7 +738,7 @@ export function getPluginDocuments(model: Model, schemaPath: string): string[] { } const provider = getLiteral(providerField.value); - if (!provider) { + if (!provider || typeof provider !== 'string') { continue; } diff --git a/packages/language/src/validator.ts b/packages/language/src/validator.ts index 03a37fe70..dff82fbbb 100644 --- a/packages/language/src/validator.ts +++ b/packages/language/src/validator.ts @@ -9,6 +9,7 @@ import type { GeneratorDecl, InvocationExpr, Model, + Plugin, Procedure, TypeDef, ZModelAstType, @@ -17,6 +18,7 @@ import type { ZModelServices } from './module'; import AttributeValidator from './validators/attribute-validator'; import DataModelValidator from './validators/datamodel-validator'; import DataSourceValidator from './validators/datasource-validator'; +import PluginValidator from './validators/plugin-validator'; import EnumValidator from './validators/enum-validator'; import ExpressionValidator from './validators/expression-validator'; import FunctionDeclValidator from './validators/function-decl-validator'; @@ -34,6 +36,7 @@ export function registerValidationChecks(services: ZModelServices) { const checks: ValidationChecks = { Model: validator.checkModel, DataSource: validator.checkDataSource, + Plugin: validator.checkPlugin, GeneratorDecl: validator.checkGenerator, DataModel: validator.checkDataModel, TypeDef: validator.checkTypeDef, @@ -96,4 +99,8 @@ export class ZModelValidator { checkProcedure(node: Procedure, accept: ValidationAcceptor): void { new ProcedureValidator().validate(node, accept); } + + checkPlugin(node: Plugin, accept: ValidationAcceptor): void { + new PluginValidator().validate(node, accept); + } } diff --git a/packages/language/src/validators/plugin-validator.ts b/packages/language/src/validators/plugin-validator.ts new file mode 100644 index 000000000..28b11ddde --- /dev/null +++ b/packages/language/src/validators/plugin-validator.ts @@ -0,0 +1,31 @@ +import type { ValidationAcceptor } from 'langium'; +import { Plugin } from '../generated/ast'; +import { getStringLiteral } from '../utils'; +import { validateDuplicatedDeclarations, type AstValidator } from './common'; + +/** + * Validates plugin declarations. + */ +export default class PluginValidator implements AstValidator { + validate(plugin: Plugin, accept: ValidationAcceptor): void { + validateDuplicatedDeclarations(plugin, plugin.fields, accept); + this.validateProvider(plugin, accept); + } + + private validateProvider(plugin: Plugin, accept: ValidationAcceptor) { + const provider = plugin.fields.find((f) => f.name === 'provider'); + if (!provider) { + accept('error', 'plugin must include a "provider" field', { + node: plugin, + }); + return; + } + + const providerValue = getStringLiteral(provider.value); + if (!providerValue) { + accept('error', '"provider" must be set to a non-empty string literal', { + node: provider.value, + }); + } + } +} diff --git a/packages/language/test/plugin.test.ts b/packages/language/test/plugin.test.ts new file mode 100644 index 000000000..8f7ca1e8c --- /dev/null +++ b/packages/language/test/plugin.test.ts @@ -0,0 +1,119 @@ +import { describe, it } from 'vitest'; +import { loadSchema, loadSchemaWithError } from './utils'; + +describe('Plugin tests', () => { + it('accepts plugins with a string provider', async () => { + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = 'test' + } + `, + ); + }); + + it('rejects plugins without a provider', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + + } + `, + 'plugin must include a "provider" field', + ); + }); + + it('rejects plugins with an empty provider', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = '' + } + `, + '"provider" must be set to a non-empty string literal', + ); + }); + + it('rejects plugins with a non-string provider', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = [] + } + `, + '"provider" must be set to a non-empty string literal', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = true + } + `, + '"provider" must be set to a non-empty string literal', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + + plugin test { + provider = {} + } + `, + '"provider" must be set to a non-empty string literal', + ); + }); +});