From 34d65aa0be9c791503ffb0816523ba5f54b54d5b Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Mon, 14 Sep 2026 12:08:00 +0000 Subject: [PATCH 1/3] feat(compiler): add synchronous compilation --- src/compiler/main.ts | 132 +++++++++-- src/compiler/nodes/array.ts | 6 +- src/compiler/nodes/literal.ts | 6 +- src/compiler/nodes/object.ts | 6 +- src/compiler/nodes/record.ts | 6 +- src/compiler/nodes/tuple.ts | 6 +- src/scripts/define_inline_functions.ts | 14 +- src/scripts/field/validations.ts | 22 +- src/types.ts | 29 ++- .../compiler/sync_compilation.spec.ts | 209 ++++++++++++++++++ 10 files changed, 396 insertions(+), 40 deletions(-) create mode 100644 tests/integration/compiler/sync_compilation.spec.ts diff --git a/src/compiler/main.ts b/src/compiler/main.ts index da3162d..504e225 100644 --- a/src/compiler/main.ts +++ b/src/compiler/main.ts @@ -23,14 +23,13 @@ import { createRecordField } from './fields/record_field.js' import { defineInlineFunctions } from '../scripts/define_inline_functions.js' import { defineInlineErrorMessages } from '../scripts/define_error_messages.js' import type { - Refs, RootNode, CompilerField, CompilerNodes, CompilerParent, CompilerOptions, - ErrorReporterContract, - MessagesProviderContact, + CompiledValidator, + CompiledSyncValidator, } from '../types.js' /** @@ -64,6 +63,11 @@ export class Compiler { */ #buffer: CompilerBuffer = new CompilerBuffer() + /** + * Whether the current compilation targets a synchronous function + */ + #synchronous: boolean = false + constructor(rootNode: RootNode, options?: CompilerOptions) { this.#rootNode = rootNode this.#options = options || { convertEmptyStringsToNull: false } @@ -81,7 +85,7 @@ export class Compiler { ...this.#options.messages, }) ) - this.#buffer.writeStatement(defineInlineFunctions(this.#options)) + this.#buffer.writeStatement(defineInlineFunctions(this.#options, this.#synchronous)) this.#buffer.writeStatement('let out;') } @@ -109,13 +113,7 @@ export class Compiler { /** * Returns compiled output as a function */ - #toAsyncFunction>(): ( - data: any, - meta: Record, - refs: Refs, - messagesProvider: MessagesProviderContact, - errorReporter: ErrorReporterContract - ) => Promise { + #toAsyncFunction(): CompiledValidator { return new AsyncFunction( 'root', 'meta', @@ -126,6 +124,36 @@ export class Compiler { ) } + /** + * Returns compiled output as a synchronous function + */ + #toFunction(): CompiledSyncValidator { + return new Function( + 'root', + 'meta', + 'refs', + 'messagesProvider', + 'errorReporter', + this.#buffer.toString() + ) as CompiledSyncValidator + } + + /** + * Ensures async validation rules are not part of synchronous compilation + */ + #ensureCanCompileSync(node: CompilerNodes) { + if (!this.#synchronous || !('validations' in node)) { + return + } + + const asyncRule = node.validations.find((validation) => validation.isAsync) + if (asyncRule) { + throw new Error( + `Cannot compile validation rule "${asyncRule.name || asyncRule.ruleFnId}" for field "${node.fieldName}" synchronously because it is async` + ) + } + } + /** * Converts a node to a field. Optionally accepts a parent node to create * a field for a specific parent type. @@ -154,17 +182,54 @@ export class Compiler { parent: CompilerParent, parentField?: CompilerField ) { + this.#ensureCanCompileSync(node) + switch (node.type) { case 'literal': - return new LiteralNodeCompiler(node, buffer, this, parent, parentField).compile() + return new LiteralNodeCompiler( + node, + buffer, + this, + parent, + parentField, + this.#synchronous + ).compile() case 'array': - return new ArrayNodeCompiler(node, buffer, this, parent, parentField).compile() + return new ArrayNodeCompiler( + node, + buffer, + this, + parent, + parentField, + this.#synchronous + ).compile() case 'record': - return new RecordNodeCompiler(node, buffer, this, parent, parentField).compile() + return new RecordNodeCompiler( + node, + buffer, + this, + parent, + parentField, + this.#synchronous + ).compile() case 'object': - return new ObjectNodeCompiler(node, buffer, this, parent, parentField).compile() + return new ObjectNodeCompiler( + node, + buffer, + this, + parent, + parentField, + this.#synchronous + ).compile() case 'tuple': - return new TupleNodeCompiler(node, buffer, this, parent, parentField).compile() + return new TupleNodeCompiler( + node, + buffer, + this, + parent, + parentField, + this.#synchronous + ).compile() case 'union': return new UnionNodeCompiler(node, buffer, this, parent, parentField).compile() } @@ -173,16 +238,35 @@ export class Compiler { /** * Compile schema nodes to an async function */ - compile() { - this.#initiateJSOutput() - this.#compileNodes() - this.#finishJSOutput() + compile(): CompiledValidator { + return this.#compile(false) + } - const outputFunction = this.#toAsyncFunction() + /** + * Compile schema nodes to a synchronous function + */ + compileSync(): CompiledSyncValidator { + return this.#compile(true) + } - this.variablesCounter = 0 - this.#buffer.flush() + /** + * Compile schema nodes to a function using the requested execution mode + */ + #compile(synchronous: false): CompiledValidator + #compile(synchronous: true): CompiledSyncValidator + #compile(synchronous: boolean) { + this.#synchronous = synchronous + this.#initiateJSOutput() + + try { + this.#compileNodes() + this.#finishJSOutput() - return outputFunction + return synchronous ? this.#toFunction() : this.#toAsyncFunction() + } finally { + this.#synchronous = false + this.variablesCounter = 0 + this.#buffer.flush() + } } } diff --git a/src/compiler/nodes/array.ts b/src/compiler/nodes/array.ts index 394fba2..73cb145 100644 --- a/src/compiler/nodes/array.ts +++ b/src/compiler/nodes/array.ts @@ -27,18 +27,21 @@ export class ArrayNodeCompiler extends BaseNode { #node: ArrayNode #buffer: CompilerBuffer #compiler: Compiler + #synchronous: boolean constructor( node: ArrayNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField?: CompilerField + parentField: CompilerField | undefined, + synchronous: boolean ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler + this.#synchronous = synchronous } /** @@ -98,6 +101,7 @@ export class ArrayNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ + synchronous: this.#synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/literal.ts b/src/compiler/nodes/literal.ts index 6b3a027..9fb1db9 100644 --- a/src/compiler/nodes/literal.ts +++ b/src/compiler/nodes/literal.ts @@ -23,17 +23,20 @@ import { validateLiteralField } from '../../scripts/field/variables.js' export class LiteralNodeCompiler extends BaseNode { #node: LiteralNode #buffer: CompilerBuffer + #synchronous: boolean constructor( node: LiteralNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField?: CompilerField + parentField: CompilerField | undefined, + synchronous: boolean ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer + this.#synchronous = synchronous } compile() { @@ -70,6 +73,7 @@ export class LiteralNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ + synchronous: this.#synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/object.ts b/src/compiler/nodes/object.ts index 8b464e2..a8a125e 100644 --- a/src/compiler/nodes/object.ts +++ b/src/compiler/nodes/object.ts @@ -29,18 +29,21 @@ export class ObjectNodeCompiler extends BaseNode { #node: ObjectNode #buffer: CompilerBuffer #compiler: Compiler + #synchronous: boolean constructor( node: ObjectNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField?: CompilerField + parentField: CompilerField | undefined, + synchronous: boolean ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler + this.#synchronous = synchronous } /** @@ -163,6 +166,7 @@ export class ObjectNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ + synchronous: this.#synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/record.ts b/src/compiler/nodes/record.ts index 57febb1..7fe93ee 100644 --- a/src/compiler/nodes/record.ts +++ b/src/compiler/nodes/record.ts @@ -27,18 +27,21 @@ export class RecordNodeCompiler extends BaseNode { #node: RecordNode #buffer: CompilerBuffer #compiler: Compiler + #synchronous: boolean constructor( node: RecordNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField?: CompilerField + parentField: CompilerField | undefined, + synchronous: boolean ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler + this.#synchronous = synchronous } /** @@ -98,6 +101,7 @@ export class RecordNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ + synchronous: this.#synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/tuple.ts b/src/compiler/nodes/tuple.ts index 73204e6..7f215f9 100644 --- a/src/compiler/nodes/tuple.ts +++ b/src/compiler/nodes/tuple.ts @@ -26,18 +26,21 @@ export class TupleNodeCompiler extends BaseNode { #node: TupleNode #buffer: CompilerBuffer #compiler: Compiler + #synchronous: boolean constructor( node: TupleNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField?: CompilerField + parentField: CompilerField | undefined, + synchronous: boolean ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler + this.#synchronous = synchronous } /** @@ -91,6 +94,7 @@ export class TupleNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ + synchronous: this.#synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/scripts/define_inline_functions.ts b/src/scripts/define_inline_functions.ts index 6aa6f60..d0de25b 100644 --- a/src/scripts/define_inline_functions.ts +++ b/src/scripts/define_inline_functions.ts @@ -11,7 +11,10 @@ * Returns JS fragment for inline function needed by the * validation runtime code. */ -export function defineInlineFunctions(options: { convertEmptyStringsToNull: boolean }) { +export function defineInlineFunctions( + options: { convertEmptyStringsToNull: boolean }, + synchronous: boolean = false +) { return `function report(message, rule, field, args) { field.isValid = false; errorReporter.report(messagesProvider.getMessage(message, rule, field, args), rule, field, args); @@ -56,6 +59,15 @@ function ensureIsArray(field) { field.report(NOT_AN_ARRAY, 'array', field); return false; }; +${ + synchronous + ? `function ensureIsNotPromise(result, rule, field) { + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') { + throw new Error(\`Validation rule "\${rule}" for field "\${field.getFieldPath()}" returned a promise or thenable while marked as synchronous\`); + } +};` + : '' +} function copyProperties(val) { let k, out, tmp; diff --git a/src/scripts/field/validations.ts b/src/scripts/field/validations.ts index 4741ad7..6300d16 100644 --- a/src/scripts/field/validations.ts +++ b/src/scripts/field/validations.ts @@ -14,6 +14,7 @@ import { type ValidationNode } from '../../types.js' */ type ValidationOptions = { bail: boolean + synchronous?: boolean variableName: string validations: ValidationNode[] @@ -60,14 +61,15 @@ function wrapInConditional(conditions: [string, string], wrappingCode: string) { * Emits code for executing a validation function */ function emitValidationSnippet( - { isAsync, implicit, ruleFnId }: ValidationNode, + { name, isAsync, implicit, ruleFnId }: ValidationNode, variableName: string, bail: boolean, + synchronous: boolean, dropMissingCheck: boolean, existenceCheckExpression?: string ) { const rule = `refs['${ruleFnId}']` - const callable = `${rule}.validator(${variableName}.value, ${rule}.options, ${variableName});` + const callable = `${rule}.validator(${variableName}.value, ${rule}.options, ${variableName})` existenceCheckExpression = existenceCheckExpression || `${variableName}.isDefined` /** @@ -86,7 +88,11 @@ function emitValidationSnippet( */ return wrapInConditional( [bailCondition, implicitCondition], - isAsync ? `await ${callable}` : `${callable}` + isAsync + ? `await ${callable};` + : synchronous + ? `ensureIsNotPromise(${callable}, ${JSON.stringify(name || ruleFnId)}, ${variableName});` + : `${callable};` ) } @@ -95,6 +101,7 @@ function emitValidationSnippet( */ export function defineFieldValidations({ bail, + synchronous = false, validations, variableName, dropMissingCheck, @@ -102,7 +109,14 @@ export function defineFieldValidations({ }: ValidationOptions) { return `${validations .map((one) => - emitValidationSnippet(one, variableName, bail, dropMissingCheck, existenceCheckExpression) + emitValidationSnippet( + one, + variableName, + bail, + synchronous, + dropMissingCheck, + existenceCheckExpression + ) ) .join('\n')}` } diff --git a/src/types.ts b/src/types.ts index 7976d22..32481b1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -434,12 +434,7 @@ export type RootNode = { * Known tree nodes accepted by the compiler */ export type CompilerNodes = - | LiteralNode - | ObjectNode - | ArrayNode - | UnionNode - | RecordNode - | TupleNode + LiteralNode | ObjectNode | ArrayNode | UnionNode | RecordNode | TupleNode /** * Properties of a parent node as the compiler loops through the @@ -526,6 +521,28 @@ export interface MessagesProviderContact { ): string } +/** + * A validator produced by Compiler.compile + */ +export type CompiledValidator> = ( + data: any, + meta: Record, + refs: Refs, + messagesProvider: MessagesProviderContact, + errorReporter: ErrorReporterContract +) => Promise + +/** + * A validator produced by Compiler.compileSync + */ +export type CompiledSyncValidator> = ( + data: any, + meta: Record, + refs: Refs, + messagesProvider: MessagesProviderContact, + errorReporter: ErrorReporterContract +) => Output + /** * Options accepted by the compiler */ diff --git a/tests/integration/compiler/sync_compilation.spec.ts b/tests/integration/compiler/sync_compilation.spec.ts new file mode 100644 index 0000000..0b998e0 --- /dev/null +++ b/tests/integration/compiler/sync_compilation.spec.ts @@ -0,0 +1,209 @@ +/* + * @vinejs/compiler + * + * (c) VineJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Compiler } from '../../../index.js' +import type { LiteralNode, ObjectNode, Refs, ValidationNode } from '../../../src/types.js' +import { ErrorReporterFactory } from '../../../factories/error_reporter.js' +import { MessagesProviderFactory } from '../../../factories/messages_provider.js' + +function literal(fieldName: string, validations: ValidationNode[] = []): LiteralNode { + return { + type: 'literal', + bail: true, + fieldName, + validations, + propertyName: fieldName, + allowNull: false, + isOptional: false, + } +} + +function object(fieldName: string, properties: ObjectNode['properties']): ObjectNode { + return { + type: 'object', + groups: [], + bail: true, + fieldName, + validations: [], + propertyName: fieldName, + allowNull: false, + isOptional: false, + properties, + allowUnknownProperties: false, + } +} + +function executeSync(compiler: Compiler, data: any, refs: Refs = {}) { + return compiler.compileSync()( + data, + {}, + refs, + new MessagesProviderFactory().create(), + new ErrorReporterFactory().create() + ) +} + +test.group('Synchronous compilation', () => { + test('returns the validated value directly', ({ assert }) => { + let ruleCalled = false + const compiler = new Compiler({ + type: 'root', + schema: literal('*', [ + { + name: 'string', + ruleFnId: 'ref://1', + implicit: false, + isAsync: false, + }, + ]), + }) + const refs: Refs = { + 'ref://1': { + validator(value) { + assert.equal(value, 'virk') + ruleCalled = true + }, + }, + } + + const output = executeSync(compiler, 'virk', refs) + + assert.equal(output, 'virk') + assert.isFalse(output instanceof Promise) + assert.isTrue(ruleCalled) + }) + + test('throws validation errors directly', ({ assert }) => { + assert.plan(2) + const compiler = new Compiler({ type: 'root', schema: literal('username') }) + + try { + executeSync(compiler, undefined) + } catch (error) { + assert.equal(error.message, 'Validation failure') + assert.deepEqual(error.messages, ['value is required']) + } + }) + + test('validates nested objects and arrays synchronously', ({ assert }) => { + const compiler = new Compiler({ + type: 'root', + schema: object('*', [ + object('profile', [ + literal('name'), + { + type: 'array', + bail: true, + fieldName: 'tags', + validations: [], + propertyName: 'tags', + allowNull: false, + isOptional: false, + each: literal('*'), + }, + ]), + ]), + }) + + const data = { profile: { name: 'virk', tags: ['maintainer', 'author'] } } + const output = executeSync(compiler, data) + + assert.deepEqual(output, data) + assert.notStrictEqual(output, data) + assert.notStrictEqual(output.profile, data.profile) + assert.notStrictEqual(output.profile.tags, data.profile.tags) + }) + + test('rejects async rules during compilation', ({ assert }) => { + const compiler = new Compiler({ + type: 'root', + schema: object('*', [ + literal('email', [ + { + name: 'unique', + ruleFnId: 'ref://1', + implicit: false, + isAsync: true, + }, + ]), + ]), + }) + + assert.throws( + () => compiler.compileSync(), + 'Cannot compile validation rule "unique" for field "email" synchronously because it is async' + ) + }) + + test('rejects thenables returned by rules marked as synchronous', ({ assert }) => { + const compiler = new Compiler({ + type: 'root', + schema: object('*', [ + literal('email', [ + { + name: 'unique', + ruleFnId: 'ref://1', + implicit: false, + isAsync: false, + }, + ]), + ]), + }) + const refs: Refs = { + 'ref://1': { + validator() { + return { then() {} } + }, + }, + } + + assert.throws( + () => executeSync(compiler, { email: 'romain@adonisjs.com' }, refs), + 'Validation rule "unique" for field "email" returned a promise or thenable while marked as synchronous' + ) + }) + + test('keeps compile asynchronous and awaits async rules', async ({ assert }) => { + let ruleCompleted = false + const compiler = new Compiler({ + type: 'root', + schema: literal('*', [ + { + name: 'delayed', + ruleFnId: 'ref://1', + implicit: false, + isAsync: true, + }, + ]), + }) + const refs: Refs = { + 'ref://1': { + async validator() { + await Promise.resolve() + ruleCompleted = true + }, + }, + } + const validate = compiler.compile() + + const pendingOutput = validate( + 'virk', + {}, + refs, + new MessagesProviderFactory().create(), + new ErrorReporterFactory().create() + ) + + assert.instanceOf(pendingOutput, Promise) + assert.isFalse(ruleCompleted) + assert.equal(await pendingOutput, 'virk') + assert.isTrue(ruleCompleted) + }) +}) From fcb32c9c0cbad24c9388bd69a75493e28d9fd092 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 29 Sep 2026 08:49:38 +0200 Subject: [PATCH 2/3] refactor(compiler): read synchronous mode from the compiler in node compilers --- src/compiler/main.ts | 52 ++++++++--------------------------- src/compiler/nodes/array.ts | 7 ++--- src/compiler/nodes/literal.ts | 9 +++--- src/compiler/nodes/object.ts | 7 ++--- src/compiler/nodes/record.ts | 7 ++--- src/compiler/nodes/tuple.ts | 7 ++--- 6 files changed, 24 insertions(+), 65 deletions(-) diff --git a/src/compiler/main.ts b/src/compiler/main.ts index 504e225..6e35986 100644 --- a/src/compiler/main.ts +++ b/src/compiler/main.ts @@ -68,6 +68,13 @@ export class Compiler { */ #synchronous: boolean = false + /** + * Read by node compilers to emit synchronous validation calls + */ + get synchronous() { + return this.#synchronous + } + constructor(rootNode: RootNode, options?: CompilerOptions) { this.#rootNode = rootNode this.#options = options || { convertEmptyStringsToNull: false } @@ -186,50 +193,15 @@ export class Compiler { switch (node.type) { case 'literal': - return new LiteralNodeCompiler( - node, - buffer, - this, - parent, - parentField, - this.#synchronous - ).compile() + return new LiteralNodeCompiler(node, buffer, this, parent, parentField).compile() case 'array': - return new ArrayNodeCompiler( - node, - buffer, - this, - parent, - parentField, - this.#synchronous - ).compile() + return new ArrayNodeCompiler(node, buffer, this, parent, parentField).compile() case 'record': - return new RecordNodeCompiler( - node, - buffer, - this, - parent, - parentField, - this.#synchronous - ).compile() + return new RecordNodeCompiler(node, buffer, this, parent, parentField).compile() case 'object': - return new ObjectNodeCompiler( - node, - buffer, - this, - parent, - parentField, - this.#synchronous - ).compile() + return new ObjectNodeCompiler(node, buffer, this, parent, parentField).compile() case 'tuple': - return new TupleNodeCompiler( - node, - buffer, - this, - parent, - parentField, - this.#synchronous - ).compile() + return new TupleNodeCompiler(node, buffer, this, parent, parentField).compile() case 'union': return new UnionNodeCompiler(node, buffer, this, parent, parentField).compile() } diff --git a/src/compiler/nodes/array.ts b/src/compiler/nodes/array.ts index 73cb145..c5fc608 100644 --- a/src/compiler/nodes/array.ts +++ b/src/compiler/nodes/array.ts @@ -27,21 +27,18 @@ export class ArrayNodeCompiler extends BaseNode { #node: ArrayNode #buffer: CompilerBuffer #compiler: Compiler - #synchronous: boolean constructor( node: ArrayNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField: CompilerField | undefined, - synchronous: boolean + parentField?: CompilerField ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler - this.#synchronous = synchronous } /** @@ -101,7 +98,7 @@ export class ArrayNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ - synchronous: this.#synchronous, + synchronous: this.#compiler.synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/literal.ts b/src/compiler/nodes/literal.ts index 9fb1db9..b7f4f11 100644 --- a/src/compiler/nodes/literal.ts +++ b/src/compiler/nodes/literal.ts @@ -23,20 +23,19 @@ import { validateLiteralField } from '../../scripts/field/variables.js' export class LiteralNodeCompiler extends BaseNode { #node: LiteralNode #buffer: CompilerBuffer - #synchronous: boolean + #compiler: Compiler constructor( node: LiteralNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField: CompilerField | undefined, - synchronous: boolean + parentField?: CompilerField ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer - this.#synchronous = synchronous + this.#compiler = compiler } compile() { @@ -73,7 +72,7 @@ export class LiteralNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ - synchronous: this.#synchronous, + synchronous: this.#compiler.synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/object.ts b/src/compiler/nodes/object.ts index a8a125e..45424a8 100644 --- a/src/compiler/nodes/object.ts +++ b/src/compiler/nodes/object.ts @@ -29,21 +29,18 @@ export class ObjectNodeCompiler extends BaseNode { #node: ObjectNode #buffer: CompilerBuffer #compiler: Compiler - #synchronous: boolean constructor( node: ObjectNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField: CompilerField | undefined, - synchronous: boolean + parentField?: CompilerField ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler - this.#synchronous = synchronous } /** @@ -166,7 +163,7 @@ export class ObjectNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ - synchronous: this.#synchronous, + synchronous: this.#compiler.synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/record.ts b/src/compiler/nodes/record.ts index 7fe93ee..a791638 100644 --- a/src/compiler/nodes/record.ts +++ b/src/compiler/nodes/record.ts @@ -27,21 +27,18 @@ export class RecordNodeCompiler extends BaseNode { #node: RecordNode #buffer: CompilerBuffer #compiler: Compiler - #synchronous: boolean constructor( node: RecordNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField: CompilerField | undefined, - synchronous: boolean + parentField?: CompilerField ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler - this.#synchronous = synchronous } /** @@ -101,7 +98,7 @@ export class RecordNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ - synchronous: this.#synchronous, + synchronous: this.#compiler.synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, diff --git a/src/compiler/nodes/tuple.ts b/src/compiler/nodes/tuple.ts index 7f215f9..cb2d2ea 100644 --- a/src/compiler/nodes/tuple.ts +++ b/src/compiler/nodes/tuple.ts @@ -26,21 +26,18 @@ export class TupleNodeCompiler extends BaseNode { #node: TupleNode #buffer: CompilerBuffer #compiler: Compiler - #synchronous: boolean constructor( node: TupleNode, buffer: CompilerBuffer, compiler: Compiler, parent: CompilerParent, - parentField: CompilerField | undefined, - synchronous: boolean + parentField?: CompilerField ) { super(node, compiler, parent, parentField) this.#node = node this.#buffer = buffer this.#compiler = compiler - this.#synchronous = synchronous } /** @@ -94,7 +91,7 @@ export class TupleNodeCompiler extends BaseNode { */ this.#buffer.writeStatement( defineFieldValidations({ - synchronous: this.#synchronous, + synchronous: this.#compiler.synchronous, variableName: this.field.variableName, validations: this.#node.validations, bail: this.#node.bail, From b6ca65b159b6561d5e8b3c0f54653db639baf16a Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 29 Sep 2026 08:49:39 +0200 Subject: [PATCH 3/3] fix(compiler): handle rejections of thenables returned by synchronous rules --- src/scripts/define_inline_functions.ts | 3 ++ .../compiler/sync_compilation.spec.ts | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/scripts/define_inline_functions.ts b/src/scripts/define_inline_functions.ts index d0de25b..4acbd18 100644 --- a/src/scripts/define_inline_functions.ts +++ b/src/scripts/define_inline_functions.ts @@ -63,6 +63,9 @@ ${ synchronous ? `function ensureIsNotPromise(result, rule, field) { if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') { + try { + result.then(undefined, function () {}); + } catch {} throw new Error(\`Validation rule "\${rule}" for field "\${field.getFieldPath()}" returned a promise or thenable while marked as synchronous\`); } };` diff --git a/tests/integration/compiler/sync_compilation.spec.ts b/tests/integration/compiler/sync_compilation.spec.ts index 0b998e0..fa2d350 100644 --- a/tests/integration/compiler/sync_compilation.spec.ts +++ b/tests/integration/compiler/sync_compilation.spec.ts @@ -170,6 +170,49 @@ test.group('Synchronous compilation', () => { ) }) + test('handles rejections of thenables returned by rules marked as synchronous', ({ assert }) => { + let rejectionHandler: unknown + const compiler = new Compiler({ + type: 'root', + schema: object('*', [ + { + type: 'array', + bail: true, + fieldName: 'emails', + validations: [], + propertyName: 'emails', + allowNull: false, + isOptional: false, + each: literal('*', [ + { + name: 'unique', + ruleFnId: 'ref://1', + implicit: false, + isAsync: false, + }, + ]), + }, + ]), + }) + const refs: Refs = { + 'ref://1': { + validator() { + return { + then(_: unknown, onRejected: unknown) { + rejectionHandler = onRejected + }, + } + }, + }, + } + + assert.throws( + () => executeSync(compiler, { emails: ['romain@adonisjs.com'] }, refs), + 'Validation rule "unique" for field "emails.0" returned a promise or thenable while marked as synchronous' + ) + assert.isFunction(rejectionHandler) + }) + test('keeps compile asynchronous and awaits async rules', async ({ assert }) => { let ruleCompleted = false const compiler = new Compiler({