Skip to content

Commit

Permalink
feat(no-invalid-conditional-action): add a new rule for linting condi…
Browse files Browse the repository at this point in the history
…tional actions

Add a rule for linting conditional actions declared with the `choose` action creator.
  • Loading branch information
rlaffers committed Aug 21, 2023
1 parent 69ff424 commit 4c7c201
Show file tree
Hide file tree
Showing 5 changed files with 519 additions and 1 deletion.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Then configure the rules you want to use under the rules section.
"xstate/no-misplaced-on-transition": "error",
"xstate/no-invalid-transition-props": "error",
"xstate/no-invalid-state-props": "error",
"xstate/no-invalid-conditional-action": "error",
"xstate/no-async-guard": "error",
"xstate/event-names": ["warn", "macroCase"],
"xstate/state-names": ["warn", "camelCase"],
Expand Down Expand Up @@ -105,7 +106,7 @@ If you do not use shareable configs, you need to manually specify the XState ver

| Rule | Description | Recommended |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------ |
| [spawn-usage](docs/rules/spawn-usage.md) | Enforce correct usage of `spawn`. **Only for XState v4!** | :heavy_check_mark: |
| [spawn-usage](docs/rules/spawn-usage.md) | Enforce correct usage of `spawn`. **Only for XState v4!** | :heavy_check_mark: |
| [no-infinite-loop](docs/rules/no-infinite-loop.md) | Detect infinite loops with eventless transitions | :heavy_check_mark: |
| [no-imperative-action](docs/rules/no-imperative-action.md) | Forbid using action creators imperatively | :heavy_check_mark: |
| [no-ondone-outside-compound-state](docs/rules/no-ondone-outside-compound-state.md) | Forbid onDone transitions on `atomic`, `history` and `final` nodes | :heavy_check_mark: |
Expand All @@ -115,6 +116,7 @@ If you do not use shareable configs, you need to manually specify the XState ver
| [no-invalid-transition-props](docs/rules/no-invalid-transition-props.md) | Forbid invalid properties in transition declarations | :heavy_check_mark: |
| [no-invalid-state-props](docs/rules/no-invalid-state-props.md) | Forbid invalid properties in state node declarations | :heavy_check_mark: |
| [no-async-guard](docs/rules/no-async-guard.md) | Forbid asynchronous guard functions | :heavy_check_mark: |
| [no-invalid-conditional-action](docs/rules/no-invalid-conditional-action.md) | Forbid invalid declarations inside the `choose` action creator | :heavy_check_mark: |

### Best Practices

Expand Down
142 changes: 142 additions & 0 deletions docs/rules/no-invalid-conditional-action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Forbid invalid properties in conditional actions

Forbid unrecognized properties in conditional actions passed to the `choose` action creator.

## Rule Details

Conditional action declarations may contains only `cond` and `actions` props.

### XState v5

In XState v5, the `cond` propery has been renamed to `guard`.

Examples of **incorrect** code for this rule:

```javascript
// ❌ (XState v4)
createMachine({
states: {
active: {
on: {
EVENT1: {
actions: choose([
{
cond: 'myGuard',
actions: [],
foo: 'bar', // ???
guard: 'myGuard', // ???
invoke: 'myService', // ???
},
]),
},
},
entry: choose([
{
cond: 'myGuard',
actions: [],
foo: 'bar', // ???
guard: 'myGuard', // ???
invoke: 'myService', // ???
},
]),
},
},
})

// ❌ (XState v5)
createMachine({
states: {
active: {
on: {
EVENT1: {
actions: choose([
{
guard: 'myGuard',
actions: [],
cond: 'myGuard', // ???
},
]),
},
},
entry: choose([
{
guard: 'myGuard',
actions: [],
cond: 'myGuard', // ???
},
]),
},
},
})

// ❌ The first argument passed to "choose" must be an array
createMachine({
states: {
active: {
on: {
EVENT1: {
actions: [
choose(), // ???
choose({}), // ???
choose(() => []), // ???
],
},
},
entry: choose(''), // ???
exit: choose(null), // ???
},
},
})
```

Examples of **correct** code for this rule:

```javascript
// ✅ only recognized properties inside conditional actions (XState v4)
createMachine({
states: {
active: {
on: {
EVENT1: {
actions: choose([
{
cond: 'myGuard',
actions: [],
},
]),
},
},
entry: choose([
{
cond: 'myGuard',
actions: [],
},
]),
},
},
})

// ✅ only recognized properties inside conditional actions (XState v5)
createMachine({
states: {
active: {
on: {
EVENT1: {
actions: choose([
{
guard: 'myGuard',
actions: [],
},
]),
},
},
entry: choose([
{
guard: 'myGuard',
actions: [],
},
]),
},
},
})
```
5 changes: 5 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
'no-invalid-transition-props': require('./rules/no-invalid-transition-props'),
'no-invalid-state-props': require('./rules/no-invalid-state-props'),
'no-async-guard': require('./rules/no-async-guard'),
'no-invalid-conditional-action': require('./rules/no-invalid-conditional-action'),
},
configs: {
// Requires: xstate@5
Expand All @@ -48,6 +49,7 @@ module.exports = {
'xstate/no-misplaced-on-transition': 'error',
'xstate/no-invalid-transition-props': 'error',
'xstate/no-invalid-state-props': 'error',
'xstate/no-invalid-conditional-action': 'error',
'xstate/no-async-guard': 'error',
'xstate/no-auto-forward': 'error',
},
Expand Down Expand Up @@ -75,6 +77,7 @@ module.exports = {
'xstate/no-misplaced-on-transition': 'error',
'xstate/no-invalid-transition-props': 'error',
'xstate/no-invalid-state-props': 'error',
'xstate/no-invalid-conditional-action': 'error',
'xstate/no-async-guard': 'error',
},
},
Expand All @@ -98,6 +101,7 @@ module.exports = {
'xstate/no-misplaced-on-transition': 'error',
'xstate/no-invalid-transition-props': 'error',
'xstate/no-invalid-state-props': 'error',
'xstate/no-invalid-conditional-action': 'error',
'xstate/no-async-guard': 'error',
},
},
Expand Down Expand Up @@ -125,6 +129,7 @@ module.exports = {
'xstate/no-misplaced-on-transition': 'error',
'xstate/no-invalid-transition-props': 'error',
'xstate/no-invalid-state-props': 'error',
'xstate/no-invalid-conditional-action': 'error',
'xstate/no-async-guard': 'error',
},
},
Expand Down
122 changes: 122 additions & 0 deletions lib/rules/no-invalid-conditional-action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use strict'

const getDocsUrl = require('../utils/getDocsUrl')
const {
isFunctionExpression,
isArrayExpression,
isObjectExpression,
} = require('../utils/predicates')
const getSettings = require('../utils/getSettings')

const validChooseActionProperty = {
4: ['cond', 'actions'],
5: ['guard', 'actions'],
}
function isValidChooseActionProperty(property, version) {
return (
validChooseActionProperty[version] &&
validChooseActionProperty[version].includes(property.key.name)
)
}

const propertyOfChoosableActionObject =
'CallExpression[callee.name=/^createMachine$|^Machine$/] CallExpression[callee.name="choose"] > ArrayExpression > ObjectExpression > Property'
const chooseFunctionCall =
'CallExpression[callee.name=/^createMachine$|^Machine$/] CallExpression[callee.name="choose"]'

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'forbid invalid usage of the "choose" action creator',
category: 'Possible Errors',
url: getDocsUrl('no-invalid-conditional-action'),
recommended: true,
},
schema: [],
messages: {
invalidConditionalActionProperty:
'"{{propName}}" is not a valid property for a conditional action.',
invalidArgumentForChoose:
'"{{argType}}" cannot be passed to the "choose" action creator. Pass an array instead.',
missingFirstArgumentForChoose:
'The "choose" action creator requires an argument.',
},
},

create: function (context) {
const { version } = getSettings(context)

return {
[propertyOfChoosableActionObject]:
function checkChooseActionObjectProperty(node) {
if (!isValidChooseActionProperty(node, version)) {
context.report({
node,
messageId: 'invalidConditionalActionProperty',
data: { propName: node.key.name },
})
}
},
[chooseFunctionCall]: function checkChooseFirstArgument(node) {
if (node.arguments.length < 1) {
context.report({
node,
messageId: 'missingFirstArgumentForChoose',
})
return
}
const firstArgument = node.arguments[0]
if (isArrayExpression(firstArgument)) {
return
}
if (isObjectExpression(firstArgument)) {
context.report({
node,
messageId: 'invalidArgumentForChoose',
data: { argType: 'object' },
})
return
}
if (firstArgument.type === 'Literal') {
context.report({
node,
messageId: 'invalidArgumentForChoose',
data: {
argType:
firstArgument.value === null
? 'null'
: typeof firstArgument.value,
},
})
return
}
if (isFunctionExpression(firstArgument)) {
context.report({
node,
messageId: 'invalidArgumentForChoose',
data: { argType: 'function' },
})
return
}
if (firstArgument.type === 'Identifier') {
context.report({
node,
messageId: 'invalidArgumentForChoose',
data: {
argType:
firstArgument.name === 'undefined' ? 'undefined' : 'identifier',
},
})
return
}

context.report({
node,
messageId: 'invalidArgumentForChoose',
data: { argType: firstArgument.type },
})
},
}
},
}
Loading

0 comments on commit 4c7c201

Please sign in to comment.