From 6e596deaf4325ed51e09aa7e4c4098a32f4526cd Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Sun, 16 Aug 2026 16:44:53 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Refine=20plugin=20API=20(#7221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Let's simplify this API. Async vs Sync issue being just for this minot release, we prefer putting it on the side and let the rare implementers of plugins taking the responsability to handle that burden instead of complexifying all typings for that temporary issue. In addition of that we added a plugin-index that could help implementers determining whether or not they could envision merging two plugins in one. ## Checklist — _Don't delete this checklist and make sure you do the following before opening the PR_ - [ ] I have a full understanding of every line in this PR — whether the code was hand-written, AI-generated, copied from external sources or produced by any other tool - [ ] I flagged the impact of my change (minor / patch / major) either by running `pnpm run bump` or by following the instructions from the changeset bot - [ ] I kept this PR focused on a single concern and did not bundle unrelated changes - [ ] I followed the [gitmoji](https://gitmoji.dev/) specification for the name of the PR, including the package scope (e.g. `🐛(vitest) Something...`) when the change targets a package other than `fast-check` - [ ] I added relevant tests and they would have failed without my PR (when applicable) --- .changeset/moody-hairs-boil.md | 5 ++++ .../fast-check/src/check/plugin/Plugin.ts | 27 +++++++------------ .../fast-check/src/check/runner/Runner.ts | 9 +++---- .../check/runner/configuration/Parameters.ts | 2 +- .../configuration/QualifiedParameters.ts | 2 +- packages/fast-check/test/e2e/Plugins.spec.ts | 8 +++--- .../configuration/QualifiedParameters.spec.ts | 2 +- 7 files changed, 25 insertions(+), 30 deletions(-) create mode 100644 .changeset/moody-hairs-boil.md diff --git a/.changeset/moody-hairs-boil.md b/.changeset/moody-hairs-boil.md new file mode 100644 index 00000000..fdab3f6c --- /dev/null +++ b/.changeset/moody-hairs-boil.md @@ -0,0 +1,5 @@ +--- +"fast-check": minor +--- + +✨ Refine plugin API diff --git a/packages/fast-check/src/check/plugin/Plugin.ts b/packages/fast-check/src/check/plugin/Plugin.ts index 08d1ea06..e0ed38d3 100644 --- a/packages/fast-check/src/check/plugin/Plugin.ts +++ b/packages/fast-check/src/check/plugin/Plugin.ts @@ -12,14 +12,7 @@ import type { RunDetails } from '../runner/reporter/RunDetails.js'; * @remarks Since 4.10.0 * @public */ -export type PluginInstance = { - /** - * Whether or not the plugin can only be attached to asynchronous properties. - * - * @defaultValue false - * @remarks Since 4.10.0 - */ - asyncOnly?: IsAsync extends false ? false : boolean; +export type PluginInstance = { /** * Enrich the execution of the predicate linked to the property with extra behaviors. * Called once per execution of the predicate. @@ -29,7 +22,7 @@ export type PluginInstance = { * * @remarks Since 4.10.0 */ - decorateRun?: (nestedRun: IRawProperty['run']) => IRawProperty['run']; + decorateRun?: (nestedRun: IRawProperty['run']) => IRawProperty['run']; /** * Called once at the end of the full property assessment. * Gets called with the result of the execution. @@ -39,21 +32,21 @@ export type PluginInstance = { * * @remarks Since 4.10.0 */ - afterAll?: (runDetails: RunDetails) => IsAsync extends true ? Promise | void : void; + afterAll?: (runDetails: RunDetails) => Promise | void; }; /** * Builder instantiating a plugin. * Each property will instantiate its own plugin when starting to be assessed via {@link check} or {@link assert}. * - * NOTE: The function gets called with a parameter shared across all builders. - * The variable can be leveraged to exchange insights with other builders. As such it is writable and can be mutated via the builder. - * We recommend using symbol keys when adding entries to the variable to reduce the risk of collision with other unrelated plugins. + * Parameters received by the Plugin function: + * - 1st argument or pluginIndex: Corresponds to the index of the plugin (starts at zero). + * - 2nd argument or crossPluginContext: Context parameter shared across all builders. + * The variable can be leveraged to exchange insights with other builders. + * As such it is writable and can be mutated via the builder. + * We recommend using symbol keys when adding entries to the variable to reduce the risk of collision with other unrelated plugins. * * @remarks Since 4.10.0 * @public */ -export type Plugin = (crossPluginContext: { [K in any]?: unknown }) => PluginInstance< - Ts, - IsAsync ->; +export type Plugin = (pluginIndex: number, crossPluginContext: { [K in any]?: unknown }) => PluginInstance; diff --git a/packages/fast-check/src/check/runner/Runner.ts b/packages/fast-check/src/check/runner/Runner.ts index 15bf5764..e8115e02 100644 --- a/packages/fast-check/src/check/runner/Runner.ts +++ b/packages/fast-check/src/check/runner/Runner.ts @@ -126,16 +126,13 @@ function check(rawProperty: IRawProperty, params?: Parameters): unkn const property = decorateProperty(rawProperty, qParams); const pluginSharedSessionContext: { [K in any]?: unknown } = {}; - const plugins: Plugin[] = qParams.plugins; - const pluginAfterAllCallbacks: Required>['afterAll'][] = []; + const plugins: Plugin[] = qParams.plugins; + const pluginAfterAllCallbacks: Required>['afterAll'][] = []; let run: typeof property.run = property.isAsync() ? async (v) => asyncPropertyExecution(property, v) : (v) => propertyExecution(property, v); for (let index = 0; index !== plugins.length; ++index) { - const pluginInstance = plugins[index](pluginSharedSessionContext); - if (pluginInstance.asyncOnly && !property.isAsync()) { - throw new Error('Cannot execute an asynchronous plugin on a synchronous property'); - } + const pluginInstance = plugins[index](index, pluginSharedSessionContext); if (pluginInstance.decorateRun !== undefined) { run = pluginInstance.decorateRun(run); } diff --git a/packages/fast-check/src/check/runner/configuration/Parameters.ts b/packages/fast-check/src/check/runner/configuration/Parameters.ts index 88e78e81..cc16c25b 100644 --- a/packages/fast-check/src/check/runner/configuration/Parameters.ts +++ b/packages/fast-check/src/check/runner/configuration/Parameters.ts @@ -212,5 +212,5 @@ export interface Parameters { * * @remarks Since 4.10.0 */ - plugins?: Plugin[]; + plugins?: Plugin[]; } diff --git a/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts b/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts index 54069cf9..b328c7e8 100644 --- a/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts +++ b/packages/fast-check/src/check/runner/configuration/QualifiedParameters.ts @@ -44,7 +44,7 @@ export class QualifiedParameters { reporter: ((runDetails: RunDetails) => void) | undefined; asyncReporter: ((runDetails: RunDetails) => Promise) | undefined; includeErrorInReport: boolean; - plugins: Plugin[]; + plugins: Plugin[]; constructor(op?: Parameters) { const p = op || {}; diff --git a/packages/fast-check/test/e2e/Plugins.spec.ts b/packages/fast-check/test/e2e/Plugins.spec.ts index dc5ffa23..da360984 100644 --- a/packages/fast-check/test/e2e/Plugins.spec.ts +++ b/packages/fast-check/test/e2e/Plugins.spec.ts @@ -6,7 +6,7 @@ describe(`Plugins (seed: ${seed})`, () => { it('should wait and queue afterAll', async () => { // Arrange const probes: string[] = []; - const buildPlugin = (pluginName: string): fc.Plugin<[number], true> => { + const buildPlugin = (pluginName: string): fc.Plugin<[number]> => { return () => { probes.push(`${pluginName} instantiated`); return { @@ -45,7 +45,7 @@ describe(`Plugins (seed: ${seed})`, () => { it('should stack decorateRun with the first plugin being the closest to the predicate', () => { // Arrange const probes: string[] = []; - const buildPlugin = (pluginName: string): fc.Plugin<[number], false> => { + const buildPlugin = (pluginName: string): fc.Plugin<[number]> => { return () => { probes.push(`${pluginName} instantiated`); return { @@ -94,7 +94,7 @@ describe(`Plugins (seed: ${seed})`, () => { it('should await decorateRun of asynchronous plugins', async () => { // Arrange const probes: string[] = []; - const buildPlugin = (pluginName: string): fc.Plugin<[number], true> => { + const buildPlugin = (pluginName: string): fc.Plugin<[number]> => { return () => { return { asyncOnly: true, @@ -137,7 +137,7 @@ describe(`Plugins (seed: ${seed})`, () => { it('should support mixes of sync and async afterAll', async () => { // Arrange const probes: string[] = []; - const buildPlugin = (pluginName: string, isAsync: boolean): fc.Plugin<[number], true> => { + const buildPlugin = (pluginName: string, isAsync: boolean): fc.Plugin<[number]> => { return () => { probes.push(`${pluginName} instantiated`); return { diff --git a/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts b/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts index 10caeed5..1717f8aa 100644 --- a/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts +++ b/packages/fast-check/test/unit/check/runner/configuration/QualifiedParameters.spec.ts @@ -32,7 +32,7 @@ const parametersArbitrary = fc.record( reporter: fc.func(fc.constant(undefined)), asyncReporter: fc.func(fc.constant(Promise.resolve(undefined))), includeErrorInReport: fc.boolean(), - plugins: fc.constant([] satisfies Plugin[]), + plugins: fc.constant([] satisfies Plugin[]), }, { requiredKeys: [] }, ); -- 2.51.2