From 3136344364d11635266ea1b64f2053a859bf0fb6 Mon Sep 17 00:00:00 2001 From: Richard Laffers Date: Thu, 24 Aug 2023 21:52:16 +0200 Subject: [PATCH] fix(entry-exit-action): recognize "guard" in entry/exit actions with xstate v5 --- lib/rules/entry-exit-action.js | 49 ++++++++++------ lib/utils/getSelectorPrefix.js | 7 +++ lib/utils/isXStateLintingEnforced.js | 3 + tests/lib/rules/entry-exit-action.js | 88 +++++++++++++++++++++++++--- 4 files changed, 123 insertions(+), 24 deletions(-) create mode 100644 lib/utils/getSelectorPrefix.js create mode 100644 lib/utils/isXStateLintingEnforced.js diff --git a/lib/rules/entry-exit-action.js b/lib/rules/entry-exit-action.js index 4b3fcec..2b84252 100644 --- a/lib/rules/entry-exit-action.js +++ b/lib/rules/entry-exit-action.js @@ -7,9 +7,14 @@ const { isFunctionExpression, isCallExpression, } = require('../utils/predicates') +const getSelectorPrefix = require('../utils/getSelectorPrefix') +const getSettings = require('../utils/getSettings') -function isObjectWithGuard(node) { - return node.type === 'ObjectExpression' && hasProperty('cond', node) +function isObjectWithGuard(node, version) { + return ( + node.type === 'ObjectExpression' && + hasProperty(version > 4 ? 'guard' : 'cond', node) + ) } function isValidAction(node) { @@ -21,17 +26,25 @@ function isValidAction(node) { ) } -const entryActionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name!="states"] > ObjectExpression > Property[key.name="entry"]' +const entryActionDeclaration = (prefix) => + prefix === '' + ? 'Property[key.name!="states"] > ObjectExpression > Property[key.name="entry"]' + : `${prefix}Property[key.name!="states"] > ObjectExpression > Property[key.name="entry"]` -const rootEntryActionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:nth-child(1) > Property[key.name="entry"]' +const rootEntryActionDeclaration = (prefix) => + prefix === '' + ? 'Property[key.name="entry"]' + : `${prefix}> ObjectExpression:nth-child(1) > Property[key.name="entry"]` -const exitActionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name!="states"] > ObjectExpression > Property[key.name="exit"]' +const exitActionDeclaration = (prefix) => + prefix === '' + ? 'Property[key.name!="states"] > ObjectExpression > Property[key.name="exit"]' + : `${prefix}Property[key.name!="states"] > ObjectExpression > Property[key.name="exit"]` -const rootExitActionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:nth-child(1) > Property[key.name="exit"]' +const rootExitActionDeclaration = (prefix) => + prefix === '' + ? 'Property[key.name="exit"]' + : `${prefix}> ObjectExpression:nth-child(1) > Property[key.name="exit"]` module.exports = { meta: { @@ -48,7 +61,7 @@ module.exports = { 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.', + 'Invalid declaration of an "exit" action. Use the "choose" or "pure" action creators to specify a conditional exit 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: @@ -57,8 +70,10 @@ module.exports = { }, create: function (context) { + const { version } = getSettings(context) + const prefix = getSelectorPrefix(context.sourceCode) const validateAction = (actionType) => (node) => { - if (isObjectWithGuard(node.value)) { + if (isObjectWithGuard(node.value, version)) { context.report({ node, messageId: @@ -80,7 +95,7 @@ module.exports = { if (node.value.type === 'ArrayExpression') { node.value.elements.forEach((element) => { - if (isObjectWithGuard(element)) { + if (isObjectWithGuard(element, version)) { context.report({ node: element, messageId: @@ -101,10 +116,10 @@ module.exports = { } } return { - [entryActionDeclaration]: validateAction('entry'), - [rootEntryActionDeclaration]: validateAction('entry'), - [exitActionDeclaration]: validateAction('exit'), - [rootExitActionDeclaration]: validateAction('exit'), + [entryActionDeclaration(prefix)]: validateAction('entry'), + [rootEntryActionDeclaration(prefix)]: validateAction('entry'), + [exitActionDeclaration(prefix)]: validateAction('exit'), + [rootExitActionDeclaration(prefix)]: validateAction('exit'), } }, } diff --git a/lib/utils/getSelectorPrefix.js b/lib/utils/getSelectorPrefix.js new file mode 100644 index 0000000..859e667 --- /dev/null +++ b/lib/utils/getSelectorPrefix.js @@ -0,0 +1,7 @@ +const isXStateLintingEnforced = require('./isXStateLintingEnforced') + +module.exports = function getSelectorPrefix(sourceCode) { + return isXStateLintingEnforced(sourceCode) + ? '' + : 'CallExpression[callee.name=/^createMachine$|^Machine$/] ' +} diff --git a/lib/utils/isXStateLintingEnforced.js b/lib/utils/isXStateLintingEnforced.js new file mode 100644 index 0000000..0944842 --- /dev/null +++ b/lib/utils/isXStateLintingEnforced.js @@ -0,0 +1,3 @@ +module.exports = function isXStateLintingEnforced(sourceCode) { + return sourceCode.getAllComments().some(x => x.type === 'Block' && x.value === ' eslint-plugin-xstate-include ') +} diff --git a/tests/lib/rules/entry-exit-action.js b/tests/lib/rules/entry-exit-action.js index 353f7a1..1389d45 100644 --- a/tests/lib/rules/entry-exit-action.js +++ b/tests/lib/rules/entry-exit-action.js @@ -1,15 +1,21 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/entry-exit-action') +const { withVersion } = require('../utils/settings') const tests = { valid: [ - ` + withVersion( + 4, + ` createMachine({ entry: 'someAction', exit: ['someAction', () => {}, assign({ foo: true }), someAction], }) - `, ` + ), + withVersion( + 4, + ` createMachine({ entry: choose([ { @@ -21,10 +27,36 @@ const tests = { }, ]), }) - `, + ` + ), + withVersion( + 5, + ` + createMachine({ + entry: 'someAction', + exit: ['someAction', () => {}, assign({ foo: true }), someAction], + }) + ` + ), + withVersion( + 5, + ` + createMachine({ + entry: choose([ + { + guard: 'someGuard', + actions: 'someAction', + }, + { + actions: 'defaultAction', + }, + ]), + }) + ` + ), ], invalid: [ - { + withVersion(4, { code: ` createMachine({ entry: [ @@ -53,8 +85,50 @@ const tests = { { messageId: 'invalidGuardedExitAction' }, { messageId: 'invalidExitAction' }, ], - }, - { + }), + withVersion(4, { + code: ` + createMachine({ + entry: 123, // numbers are invalid + exit: {}, // objects without a "type" property are invalid + }) + `, + errors: [ + { messageId: 'invalidEntryAction' }, + { messageId: 'invalidExitAction' }, + ], + }), + withVersion(5, { + code: ` + createMachine({ + entry: [ + { + guard: 'someGuard', + actions: 'someAction', + }, + { + actions: 'defaultAction', + }, + ], + exit: [ + { + guard: 'someGuard', + actions: 'someAction', + }, + { + actions: 'defaultAction', + }, + ], + }) + `, + errors: [ + { messageId: 'invalidGuardedEntryAction' }, + { messageId: 'invalidEntryAction' }, + { messageId: 'invalidGuardedExitAction' }, + { messageId: 'invalidExitAction' }, + ], + }), + withVersion(5, { code: ` createMachine({ entry: 123, // numbers are invalid @@ -65,7 +139,7 @@ const tests = { { messageId: 'invalidEntryAction' }, { messageId: 'invalidExitAction' }, ], - }, + }), ], }