From 2b0346d0182ce56b47eea5820ab5a701041f00ef Mon Sep 17 00:00:00 2001 From: Richard Laffers Date: Wed, 16 Aug 2023 15:52:15 +0200 Subject: [PATCH] feat(no-async-guard): support the "guard" prop with xstate v5 --- docs/rules/no-async-guard.md | 46 ++++++++---- lib/rules/no-async-guard.js | 10 ++- tests/lib/rules/no-async-guards.js | 115 +++++++++++++++++++++++++---- 3 files changed, 143 insertions(+), 28 deletions(-) diff --git a/docs/rules/no-async-guard.md b/docs/rules/no-async-guard.md index b4c3d69..3499ebb 100644 --- a/docs/rules/no-async-guard.md +++ b/docs/rules/no-async-guard.md @@ -4,12 +4,12 @@ ## Rule Details -Async functions return a promise which is a truthy value. Therefore, async guard functions always pass. Transitions guarded by such functions will always be taken as if no `cond` was specified. +Async functions return a promise which is a truthy value. Therefore, async guard functions always pass. Transitions guarded by such functions will always be taken as if no `cond` (XState v4) or `guard` (XState v5) was specified. Examples of **incorrect** code for this rule: ```javascript -// ❌ async guard in an event transition +// ❌ async guard in an event transition (XState v4) createMachine({ on: { EVENT: { @@ -19,14 +19,24 @@ createMachine({ }, }) -// ❌ async guard in an onDone transition +// ❌ async guard in an event transition (XState v5) +createMachine({ + on: { + EVENT: { + guard: async () => {}, + target: 'active', + }, + }, +}) + +// ❌ async guard in an onDone transition (XState v5) createMachine({ states: { active: { invoke: { src: 'myService', onDone: { - cond: async function () {}, + guard: async function () {}, target: 'finished', }, }, @@ -34,22 +44,22 @@ createMachine({ }, }) -// ❌ async guard in the choose action creator +// ❌ async guard in the choose action creator (XState v5) createMachine({ entry: choose([ { - cond: async () => {}, + guard: async () => {}, actions: 'myAction', }, ]), }) -// ❌ async guards in machine options +// ❌ async guards in machine options (XState v5) createMachine( { on: { EVENT: { - cond: 'myGuard', + guard: 'myGuard', target: 'active', }, }, @@ -67,7 +77,7 @@ createMachine( Examples of **correct** code for this rule: ```javascript -// ✅ guard is synchronous +// ✅ guard is synchronous (XState v4) createMachine({ on: { EVENT: { @@ -77,14 +87,24 @@ createMachine({ }, }) -// ✅ guard is synchronous +// ✅ guard is synchronous (XState v5) +createMachine({ + on: { + EVENT: { + guard: () => {}, + target: 'active', + }, + }, +}) + +// ✅ guard is synchronous (XState v5) createMachine({ states: { active: { invoke: { src: 'myService', onDone: { - cond: function () {}, + guard: function () {}, target: 'finished', }, }, @@ -92,12 +112,12 @@ createMachine({ }, }) -// ✅ all guards in machine options are synchronous +// ✅ all guards in machine options are synchronous (XState v5) createMachine( { on: { EVENT: { - cond: 'myGuard', + guard: 'myGuard', target: 'active', }, }, diff --git a/lib/rules/no-async-guard.js b/lib/rules/no-async-guard.js index 6b6858c..a814e5f 100644 --- a/lib/rules/no-async-guard.js +++ b/lib/rules/no-async-guard.js @@ -2,6 +2,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const { isFunctionExpression } = require('../utils/predicates') +const getSettings = require('../utils/getSettings') function isAsyncFunctionExpression(node) { return isFunctionExpression(node) && node.async @@ -23,9 +24,16 @@ module.exports = { }, create: function (context) { + const { version } = getSettings(context) return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="cond"]': + 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child Property[key.name=/^cond|guard$/]': function (node) { + if (version === 4 && node.key.name !== 'cond') { + return + } + if (version > 4 && node.key.name !== 'guard') { + return + } if (isAsyncFunctionExpression(node.value)) { context.report({ node: node.value, diff --git a/tests/lib/rules/no-async-guards.js b/tests/lib/rules/no-async-guards.js index 609f124..730df2a 100644 --- a/tests/lib/rules/no-async-guards.js +++ b/tests/lib/rules/no-async-guards.js @@ -1,9 +1,12 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/no-async-guard') +const { withVersion } = require('../utils/settings') const tests = { valid: [ - ` + withVersion( + 4, + ` createMachine({ on: { EVENT: { @@ -12,8 +15,24 @@ const tests = { }, }, }) - `, ` + ), + withVersion( + 5, + ` + createMachine({ + on: { + EVENT: { + guard: () => {}, + target: 'active', + }, + }, + }) + ` + ), + withVersion( + 4, + ` createMachine({ states: { active: { @@ -27,17 +46,31 @@ const tests = { }, }, }) - `, ` - createMachine( - { - on: { - EVENT: { - cond: 'myGuard', - target: 'active', + ), + withVersion( + 5, + ` + createMachine({ + states: { + active: { + invoke: { + src: 'myService', + onDone: { + guard: function () {}, + target: 'finished', + }, }, }, }, + }) + ` + ), + withVersion( + 4, + ` + createMachine( + {}, { guards: { myGuard: () => {}, @@ -46,10 +79,11 @@ const tests = { }, } ) - `, + ` + ), ], invalid: [ - { + withVersion(4, { code: ` createMachine({ entry: choose([ @@ -82,9 +116,43 @@ const tests = { { messageId: 'guardCannotBeAsync' }, { messageId: 'guardCannotBeAsync' }, ], - }, + }), + withVersion(5, { + code: ` + createMachine({ + entry: choose([ + { + guard: async () => {}, + actions: 'myAction', + }, + ]), + states: { + active: { + invoke: { + src: 'myService', + onDone: { + guard: async function () {}, + target: 'finished', + }, + }, + }, + }, + on: { + EVENT: { + guard: async () => {}, + target: 'active', + }, + }, + }) + `, + errors: [ + { messageId: 'guardCannotBeAsync' }, + { messageId: 'guardCannotBeAsync' }, + { messageId: 'guardCannotBeAsync' }, + ], + }), // async guard in machine options - { + withVersion(4, { code: ` createMachine( { @@ -109,7 +177,26 @@ const tests = { { messageId: 'guardCannotBeAsync' }, { messageId: 'guardCannotBeAsync' }, ], - }, + }), + withVersion(5, { + code: ` + createMachine( + {}, + { + guards: { + myGuard: async () => {}, + myGuard2: async function () {}, + async myGuard3() {}, + }, + } + ) + `, + errors: [ + { messageId: 'guardCannotBeAsync' }, + { messageId: 'guardCannotBeAsync' }, + { messageId: 'guardCannotBeAsync' }, + ], + }), ], }