Skip to content

Commit

Permalink
feat: add entry-exit-action rule
Browse files Browse the repository at this point in the history
  • Loading branch information
rlaffers committed Apr 20, 2021
1 parent 7efb3a3 commit 3a673a4
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Then configure the rules you want to use under the rules section.
"xstate/no-root-ondone": "error",
"xstate/no-ondone-outside-compound-state": "error",
"xstate/invoke-usage": "error",
"xstate/entry-exit-action": "error",
"xstate/event-names": "warn",
"xstate/no-inline-implementation": "warn"
}
Expand Down Expand Up @@ -77,6 +78,7 @@ There is also an `all` configuration which includes every available rule. It enf
| [no-root-ondone](docs/rules/no-root-ondone.md) | Forbid onDone transitions on root nodes | :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: |
| [invoke-usage](docs/rules/invoke-usage.md) | Enforce correct invocation of services | :heavy_check_mark: |
| [entry-exit-action](docs/rules/entry-exit-action.md) | Forbid invalid declarations of entry/exit actions | :heavy_check_mark: |

### Best Practices

Expand Down
87 changes: 87 additions & 0 deletions docs/rules/entry-exit-action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Forbid invalid entry/exit actions declarations

Enforce valid declarations of `entry` and `exit` actions.

# Rule Details

It is easy to mistake `entry`/`exit` action declarations with transition declarations, especially when user wishes to declare a conditional action. Valid `entry`/`exit` actions are:

- a function
- an [action creator](https://xstate.js.org/docs/guides/actions.html#declarative-actions) call
- an action object
- a string, which refers to any of the above in this machine's `options.actions`
- a variable, which refers to any of the above
- an array with any of the above elements

Most notably, an action **is not** an object with a `cond` property:

```javascript
// ❌ this is a transition, not action declaration
{
entry: {
cond: 'someGuard',
actions: 'someAction'
}
}
```

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

```javascript
// ❌ confused transitions with actions
createMachine({
entry: [
{
cond: 'someGuard',
actions: 'someAction',
},
{
actions: 'defaultAction',
},
],
})

// ❌ invalid entry/exit action values
createMachine({
entry: 123, // numbers are invalid
exit: {}, // objects without a "type" property are invalid
})

// ❌ array od invalid action values
createMachine({
entry: [123, {}, { actions: 'someAction' }],
})
```

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

```javascript
// ✅ using valid action values
createMachine({
entry: 'someAction',
exit: ['someAction', () => {}, assign({ foo: true }), someAction],
})

// ✅ declare conditional actions with "choose"
createMachine({
entry: choose([
{
cond: 'someGuard',
actions: 'someAction',
},
{
actions: 'defaultAction',
},
]),
})

// ✅ alternatively, declare conditional/dynamic actions with "pure"
createMachine({
entry: pure((ctx) => {
const actions = []
// your conditional logic here
// if (someCondition) actions.push('someAction')
return actions
}),
})
```
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
'no-root-ondone': require('./rules/no-root-ondone'),
'no-ondone-outside-compound-state': require('./rules/no-ondone-outside-compound-state'),
'invoke-usage': require('./rules/invoke-usage'),
'entry-exit-action': require('./rules/entry-exit-action'),
},
configs: {
recommended: {
Expand All @@ -30,6 +31,7 @@ module.exports = {
'xstate/no-root-ondone': 'error',
'xstate/no-ondone-outside-compound-state': 'error',
'xstate/invoke-usage': 'error',
'xstate/entry-exit-action': 'error',
},
},
all: {
Expand All @@ -41,6 +43,7 @@ module.exports = {
'xstate/no-root-ondone': 'error',
'xstate/no-ondone-outside-compound-state': 'error',
'xstate/invoke-usage': 'error',
'xstate/entry-exit-action': 'error',
'xstate/event-names': 'warn',
'xstate/no-inline-implementation': 'warn',
},
Expand Down
100 changes: 100 additions & 0 deletions lib/rules/entry-exit-action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict'

const getDocsUrl = require('../utils/getDocsUrl')
const {
hasProperty,
isStringLiteralOrIdentifier,
isFunctionExpression,
isCallExpression,
} = require('../utils/predicates')

function isObjectWithGuard(node) {
return node.type === 'ObjectExpression' && hasProperty('cond', node)
}

function isValidAction(node) {
return (
isStringLiteralOrIdentifier(node) ||
isFunctionExpression(node) ||
isCallExpression(node) ||
(node.type === 'ObjectExpression' && hasProperty('type', node))
)
}

module.exports = {
meta: {
type: 'problem',
docs: {
description:
'enforce usage of choose or pure action creator for guarded entry/exit actions',
category: 'Possible Errors',
url: getDocsUrl('conditional-entry-exit'),
recommended: true,
},
schema: [],
messages: {
invalidGuardedEntryAction:
'Invalid declaration of an "entry" action. Use the "choose" or "pure" action creators to specify a conditional entry action.',
invalidGuardedExitAction:
'Invalid declaration of an "entry" action. Use the "choose" or "pure" action creators to specify a conditional entry action.',
invalidEntryAction:
'The "entry" action has an invalid value. Specify a function, string, variable, action creator call, action object, or an array of those.',
invalidExitAction:
'The "exit" action has an invalid value. Specify a function, string, variable, action creator call, action object, or an array of those.',
},
},

create: function (context) {
const validateAction = (actionType) => (node) => {
if (isObjectWithGuard(node.value)) {
context.report({
node,
messageId:
actionType === 'entry'
? 'invalidGuardedEntryAction'
: 'invalidGuardedExitAction',
})
return
}

if (node.value.type !== 'ArrayExpression' && !isValidAction(node.value)) {
context.report({
node,
messageId:
actionType === 'entry' ? 'invalidEntryAction' : 'invalidExitAction',
})
return
}

if (node.value.type === 'ArrayExpression') {
node.value.elements.forEach((element) => {
if (isObjectWithGuard(element)) {
context.report({
node: element,
messageId:
actionType === 'entry'
? 'invalidGuardedEntryAction'
: 'invalidGuardedExitAction',
})
} else if (!isValidAction(element)) {
context.report({
node: element,
messageId:
actionType === 'entry'
? 'invalidEntryAction'
: 'invalidExitAction',
})
}
})
}
}
return {
'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="entry"]': validateAction(
'entry'
),
'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="exit"]': validateAction(
'exit'
),
}
},
}
77 changes: 77 additions & 0 deletions tests/lib/rules/entry-exit-action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/entry-exit-action')

const tests = {
valid: [
`
createMachine({
entry: 'someAction',
exit: ['someAction', () => {}, assign({ foo: true }), someAction],
})
`,
`
createMachine({
entry: choose([
{
cond: 'someGuard',
actions: 'someAction',
},
{
actions: 'defaultAction',
},
]),
})
`,
],
invalid: [
{
code: `
createMachine({
entry: [
{
cond: 'someGuard',
actions: 'someAction',
},
{
actions: 'defaultAction',
},
],
exit: [
{
cond: 'someGuard',
actions: 'someAction',
},
{
actions: 'defaultAction',
},
],
})
`,
errors: [
{ messageId: 'invalidGuardedEntryAction' },
{ messageId: 'invalidEntryAction' },
{ messageId: 'invalidGuardedExitAction' },
{ messageId: 'invalidExitAction' },
],
},
{
code: `
createMachine({
entry: 123, // numbers are invalid
exit: {}, // objects without a "type" property are invalid
})
`,
errors: [
{ messageId: 'invalidEntryAction' },
{ messageId: 'invalidExitAction' },
],
},
],
}

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 6,
},
})
ruleTester.run('entry-exit-action', rule, tests)

0 comments on commit 3a673a4

Please sign in to comment.