-
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.
feat(no-invalid-conditional-action): add a new rule for linting condi…
…tional actions Add a rule for linting conditional actions declared with the `choose` action creator.
- Loading branch information
Showing
5 changed files
with
519 additions
and
1 deletion.
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,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: [], | ||
}, | ||
]), | ||
}, | ||
}, | ||
}) | ||
``` |
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,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 }, | ||
}) | ||
}, | ||
} | ||
}, | ||
} |
Oops, something went wrong.