-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}), | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
), | ||
} | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |