Skip to content

Commit

Permalink
feat(no-async-guard): support the "guard" prop with xstate v5
Browse files Browse the repository at this point in the history
  • Loading branch information
rlaffers committed Aug 16, 2023
1 parent 409fd67 commit 2b0346d
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 28 deletions.
46 changes: 33 additions & 13 deletions docs/rules/no-async-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -19,37 +19,47 @@ 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',
},
},
},
},
})

// ❌ 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',
},
},
Expand All @@ -67,7 +77,7 @@ createMachine(
Examples of **correct** code for this rule:

```javascript
// ✅ guard is synchronous
// ✅ guard is synchronous (XState v4)
createMachine({
on: {
EVENT: {
Expand All @@ -77,27 +87,37 @@ 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',
},
},
},
},
})

// ✅ 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',
},
},
Expand Down
10 changes: 9 additions & 1 deletion lib/rules/no-async-guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
115 changes: 101 additions & 14 deletions tests/lib/rules/no-async-guards.js
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -12,8 +15,24 @@ const tests = {
},
},
})
`,
`
),
withVersion(
5,
`
createMachine({
on: {
EVENT: {
guard: () => {},
target: 'active',
},
},
})
`
),
withVersion(
4,
`
createMachine({
states: {
active: {
Expand All @@ -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: () => {},
Expand All @@ -46,10 +79,11 @@ const tests = {
},
}
)
`,
`
),
],
invalid: [
{
withVersion(4, {
code: `
createMachine({
entry: choose([
Expand Down Expand Up @@ -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(
{
Expand All @@ -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' },
],
}),
],
}

Expand Down

0 comments on commit 2b0346d

Please sign in to comment.