From 48cd08956bfadea19094970f382a12ad6db10f5f Mon Sep 17 00:00:00 2001 From: Richard Laffers Date: Sat, 26 Aug 2023 16:47:23 +0200 Subject: [PATCH] feat: add a comment directive tu enforce this plugin By default, this plugin does not lint code outside of createMachine call epxressions. This commit add a comment direective eslint-plugin-xstate-include which enforces these rules over any file. fix #20 --- README.md | 14 ++ lib/rules/event-names.js | 12 +- lib/rules/invoke-usage.js | 18 ++- lib/rules/no-async-guard.js | 63 +++++--- lib/rules/no-auto-forward.js | 6 +- lib/rules/no-imperative-action.js | 10 +- lib/rules/no-infinite-loop.js | 15 +- lib/rules/no-inline-implementation.js | 125 +++++++------- lib/rules/no-invalid-conditional-action.js | 14 +- lib/rules/no-invalid-state-props.js | 113 ++++++++----- lib/rules/no-invalid-transition-props.js | 36 ++--- lib/rules/no-misplaced-on-transition.js | 33 ++-- lib/rules/no-ondone-outside-compound-state.js | 69 ++++---- lib/rules/prefer-always.js | 4 +- .../prefer-predictable-action-arguments.js | 152 +++++++++++------- lib/rules/state-names.js | 32 ++-- lib/utils/isInsideMachineDeclaration.js | 12 -- tests/lib/rules/event-names.js | 37 +++++ tests/lib/rules/no-async-guards.js | 76 +++++++++ tests/lib/rules/no-auto-forward.js | 25 +++ tests/lib/rules/no-imperative-action.js | 92 +++++++++++ tests/lib/rules/no-infinite-loop.js | 29 +++- tests/lib/rules/no-inline-implementation.js | 101 ++++++++++++ .../rules/no-invalid-conditional-action.js | 146 +++++++++++++++++ tests/lib/rules/no-invalid-state-props.js | 74 +++++++++ .../lib/rules/no-invalid-transition-props.js | 128 +++++++++++++++ tests/lib/rules/no-misplaced-on-transition.js | 37 +++++ .../rules/no-ondone-outside-compound-state.js | 57 +++++++ tests/lib/rules/prefer-always.js | 72 +++++++++ .../prefer-predictable-action-arguments.js | 70 ++++++++ tests/lib/rules/state-names.js | 98 +++++++++++ 31 files changed, 1454 insertions(+), 316 deletions(-) delete mode 100644 lib/utils/isInsideMachineDeclaration.js diff --git a/README.md b/README.md index b122d80..5856c2f 100644 --- a/README.md +++ b/README.md @@ -133,3 +133,17 @@ If you do not use shareable configs, you need to manually specify the XState ver | ---------------------------------------- | ------------------------------------------------------------------------ | ----------- | | [event-names](docs/rules/event-names.md) | Suggest consistent formatting of event names | | | [state-names](docs/rules/state-names.md) | Suggest consistent formatting of state names and prevent confusing names | | + +## Comment Directives + +By default, the plugin lints only code within the `createMachine` or `Machine` calls. However, if your machine configuration is imported from another file, you will need to enable this plugin's rules by adding a comment directive to the top of the file: + +```js +/* eslint-plugin-xstate-include */ +// 💡 This machine config will no w be linted too. +export machine = { + initial: 'active', + context: {}, + // etc +} +``` diff --git a/lib/rules/event-names.js b/lib/rules/event-names.js index 6497aee..2074be5 100644 --- a/lib/rules/event-names.js +++ b/lib/rules/event-names.js @@ -6,6 +6,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const { isStringLiteral } = require('../utils/predicates') const { getTypeProperty } = require('../utils/selectors') const { init, last } = require('../utils/arrays') +const getSelectorPrefix = require('../utils/getSelectorPrefix') const toMacroCase = (string) => { const words = string.split('.').filter(Boolean) @@ -61,8 +62,10 @@ function containsWildcardOrDot(name) { return wildcardOrDot.test(name) } -const selectorSendEvent = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] CallExpression[callee.name=/^send$|^sendParent$|^respond$|^raise$/]' +const selectorSendEvent = (prefix) => + prefix === '' + ? 'CallExpression[callee.name=/^send$|^sendTo$|^sendParent$|^respond$|^raise$|^forwardTo$/]' + : `${prefix}CallExpression[callee.name=/^send$|^sendTo$|^sendParent$|^respond$|^raise$|^forwardTo$/]` /** * Default regular expression for the regex option. @@ -107,6 +110,7 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const mode = context.options[0] || 'macroCase' const regexOption = mode === 'regex' @@ -115,7 +119,7 @@ module.exports = { const regex = regexOption !== null ? new RegExp(regexOption) : null return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"] > ObjectExpression > Property': + [`${prefix}Property[key.name="on"] > ObjectExpression > Property`]: function (node) { // key names [varName] are dynamic values and cannot be linted if (node.computed) { @@ -156,7 +160,7 @@ module.exports = { } }, - [selectorSendEvent]: function (node) { + [selectorSendEvent(prefix)]: function (node) { const eventArg = node.arguments[0] if (!eventArg) { return diff --git a/lib/rules/invoke-usage.js b/lib/rules/invoke-usage.js index 07fd565..89bc130 100644 --- a/lib/rules/invoke-usage.js +++ b/lib/rules/invoke-usage.js @@ -9,6 +9,7 @@ const { isCreateMachineCall, isCallExpression, } = require('../utils/predicates') +const getSelectorPrefix = require('../utils/getSelectorPrefix') module.exports = { meta: { @@ -30,15 +31,16 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) + return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="invoke"]': - function (node) { - if (node.value.type === 'ArrayExpression') { - node.value.elements.forEach((node) => testInvokeDefinition(node)) - } else { - testInvokeDefinition(node.value) - } - }, + [`${prefix}Property[key.name="invoke"]`]: function (node) { + if (node.value.type === 'ArrayExpression') { + node.value.elements.forEach((node) => testInvokeDefinition(node)) + } else { + testInvokeDefinition(node.value) + } + }, } function testInvokeDefinition(node) { diff --git a/lib/rules/no-async-guard.js b/lib/rules/no-async-guard.js index a814e5f..2dbae00 100644 --- a/lib/rules/no-async-guard.js +++ b/lib/rules/no-async-guard.js @@ -3,6 +3,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const { isFunctionExpression } = require('../utils/predicates') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') function isAsyncFunctionExpression(node) { return isFunctionExpression(node) && node.async @@ -25,31 +26,43 @@ module.exports = { create: function (context) { const { version } = getSettings(context) - return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child Property[key.name=/^cond|guard$/]': - function (node) { - if (version === 4 && node.key.name !== 'cond') { - return - } - if (version > 4 && node.key.name !== 'guard') { - return - } - if (isAsyncFunctionExpression(node.value)) { - context.report({ - node: node.value, - messageId: 'guardCannotBeAsync', - }) - } - }, - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:nth-child(2) > Property[key.name="guards"] > ObjectExpression > Property': - function (node) { - if (isAsyncFunctionExpression(node.value)) { - context.report({ - node: node.value, - messageId: 'guardCannotBeAsync', - }) - } - }, + const prefix = getSelectorPrefix(context.sourceCode) + + function checkInlineGuard(node) { + if (version === 4 && node.key.name !== 'cond') { + return + } + if (version > 4 && node.key.name !== 'guard') { + return + } + if (isAsyncFunctionExpression(node.value)) { + context.report({ + node: node.value, + messageId: 'guardCannotBeAsync', + }) + } + } + + function checkGuardImplementation(node) { + if (isAsyncFunctionExpression(node.value)) { + context.report({ + node: node.value, + messageId: 'guardCannotBeAsync', + }) + } } + + return prefix === '' + ? { + 'Property[key.name=/^cond|guard$/]': checkInlineGuard, + 'Property[key.name="guards"] > ObjectExpression > Property': + checkGuardImplementation, + } + : { + [`${prefix}> ObjectExpression:first-child Property[key.name=/^cond|guard$/]`]: + checkInlineGuard, + [`${prefix}> ObjectExpression:nth-child(2) > Property[key.name="guards"] > ObjectExpression > Property`]: + checkGuardImplementation, + } }, } diff --git a/lib/rules/no-auto-forward.js b/lib/rules/no-auto-forward.js index 3f60ceb..466fcee 100644 --- a/lib/rules/no-auto-forward.js +++ b/lib/rules/no-auto-forward.js @@ -2,6 +2,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') module.exports = { meta: { @@ -22,9 +23,10 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="invoke"] > ObjectExpression > Property[key.name="autoForward"]': + [`${prefix}Property[key.name="invoke"] > ObjectExpression > Property[key.name="autoForward"]`]: function (node) { if (version !== 4) { context.report({ @@ -41,7 +43,7 @@ module.exports = { } }, - 'CallExpression[callee.name=/^createMachine$|^Machine$/] CallExpression[callee.name="spawn"] > ObjectExpression > Property[key.name="autoForward"]': + [`${prefix}CallExpression[callee.name="spawn"] > ObjectExpression > Property[key.name="autoForward"]`]: function (node) { if (version !== 4) { context.report({ diff --git a/lib/rules/no-imperative-action.js b/lib/rules/no-imperative-action.js index 4e3697b..532a19a 100644 --- a/lib/rules/no-imperative-action.js +++ b/lib/rules/no-imperative-action.js @@ -2,6 +2,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') const { isKnownActionCreatorCall, @@ -60,9 +61,10 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^entry$|^exit$/] ArrowFunctionExpression CallExpression': + [`${prefix}Property[key.name=/^entry$|^exit$/] ArrowFunctionExpression CallExpression`]: function (node) { if ( isKnownActionCreatorCall(node, version) && @@ -76,7 +78,7 @@ module.exports = { } }, - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^entry$|^exit$/] FunctionExpression CallExpression': + [`${prefix}Property[key.name=/^entry$|^exit$/] FunctionExpression CallExpression`]: function (node) { if ( isKnownActionCreatorCall(node, version) && @@ -90,7 +92,7 @@ module.exports = { } }, - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="actions"] ArrowFunctionExpression CallExpression': + [`${prefix}Property[key.name="actions"] ArrowFunctionExpression CallExpression`]: function (node) { if ( isKnownActionCreatorCall(node, version) && @@ -104,7 +106,7 @@ module.exports = { } }, - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="actions"] FunctionExpression CallExpression': + [`${prefix}Property[key.name="actions"] FunctionExpression CallExpression`]: function (node) { if ( isKnownActionCreatorCall(node, version) && diff --git a/lib/rules/no-infinite-loop.js b/lib/rules/no-infinite-loop.js index 38f702e..7235a5d 100644 --- a/lib/rules/no-infinite-loop.js +++ b/lib/rules/no-infinite-loop.js @@ -3,7 +3,6 @@ const { pipe, map, fromMaybe, Just, Nothing } = require('sanctuary') const { allPass, complement } = require('../utils/combinators') const getDocsUrl = require('../utils/getDocsUrl') -const isInsideMachineDeclaration = require('../utils/isInsideMachineDeclaration') const { isFirstArrayItem, propertyHasName, @@ -12,6 +11,7 @@ const { isStringLiteralOrIdentifier, } = require('../utils/predicates') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') function isEventlessTransitionDeclaration(node) { return node.type === 'Property' && node.key.name === 'always' @@ -159,14 +159,14 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) return { // always: {} // always: { actions: whatever} - 'Property[key.name="always"] > ObjectExpression': function (node) { - if (!isInsideMachineDeclaration(node)) { - return - } + [`${prefix}Property[key.name="always"] > ObjectExpression`]: function ( + node + ) { if ( !hasProperty('target', node) && !isConditionalTransition(node, version) @@ -180,11 +180,8 @@ module.exports = { // always: [{}] // always: [{ actions: whatever }] - 'Property[key.name="always"] > ArrayExpression > ObjectExpression': + [`${prefix}Property[key.name="always"] > ArrayExpression > ObjectExpression`]: function (node) { - if (!isInsideMachineDeclaration(node)) { - return - } if ( !hasProperty('target', node) && !isConditionalTransition(node, version) diff --git a/lib/rules/no-inline-implementation.js b/lib/rules/no-inline-implementation.js index 794b490..2d51d48 100644 --- a/lib/rules/no-inline-implementation.js +++ b/lib/rules/no-inline-implementation.js @@ -14,6 +14,7 @@ const { anyPass } = require('../utils/combinators') const getSettings = require('../utils/getSettings') const XStateDetector = require('../utils/XStateDetector') const isSpawnFromParametersCallExpresion = require('../utils/isSpawnFromParametersCallExpression') +const getSelectorPrefix = require('../utils/getSelectorPrefix') function isArrayWithFunctionExpressionOrIdentifier(node) { return ( @@ -47,42 +48,46 @@ function isValidCallExpression(node, pattern = '') { } // states: { idle: { on: { EVENT: { target: 'active' }}}} -const propertyOfEventTransition = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"] > ObjectExpression > Property > ObjectExpression > Property' +const propertyOfEventTransition = (prefix) => + `${prefix}Property[key.name="on"] > ObjectExpression > Property > ObjectExpression > Property` // states: { idle: { on: { EVENT: [{ target: 'active' }]}}} -const propertyOfEventTransitionInArray = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"] > ObjectExpression > Property > ArrayExpression > ObjectExpression > Property' +const propertyOfEventTransitionInArray = (prefix) => + `${prefix}Property[key.name="on"] > ObjectExpression > Property > ArrayExpression > ObjectExpression > Property` // states: { idle: { on: [ { event: 'EVENT', target: 'active' } ]}} -const propertyOfAltEventTransitionInArray = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"] > ArrayExpression > ObjectExpression > Property' +const propertyOfAltEventTransitionInArray = (prefix) => + `${prefix}Property[key.name="on"] > ArrayExpression > ObjectExpression > Property` -const srcPropertyInsideInvoke = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="invoke"] > ObjectExpression > Property[key.name="src"]' +const srcPropertyInsideInvoke = (prefix) => + `${prefix}Property[key.name="invoke"] > ObjectExpression > Property[key.name="src"]` -const srcPropertyInsideInvokeArray = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="invoke"] > ArrayExpression > ObjectExpression > Property[key.name="src"]' +const srcPropertyInsideInvokeArray = (prefix) => + `${prefix}Property[key.name="invoke"] > ArrayExpression > ObjectExpression > Property[key.name="src"]` -const entryExitProperty = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^entry$|^exit$/]' +const entryExitProperty = (prefix) => + `${prefix}Property[key.name=/^entry$|^exit$/]` // invoke: { onDone: { target: 'active' }} -const propertyOfOnDoneOnErrorTransition = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^onDone$|^onError$/] > ObjectExpression > Property' +const propertyOfOnDoneOnErrorTransition = (prefix) => + `${prefix}Property[key.name=/^onDone$|^onError$/] > ObjectExpression > Property` // invoke: { onDone: [{ target: 'active' }] } -const propertyOfOnDoneOnErrorTransitionInArray = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^onDone$|^onError$/] > ArrayExpression > ObjectExpression > Property' +const propertyOfOnDoneOnErrorTransitionInArray = (prefix) => + `${prefix}Property[key.name=/^onDone$|^onError$/] > ArrayExpression > ObjectExpression > Property` -const activitiesProperty = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="activities"]' +const activitiesProperty = (prefix) => + `${prefix}Property[key.name="activities"]` -const propertyOfChoosableActionObject = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child CallExpression[callee.name="choose"] > ArrayExpression > ObjectExpression > Property' +const propertyOfChoosableActionObject = (prefix) => + prefix === '' + ? 'CallExpression[callee.name="choose"] > ArrayExpression > ObjectExpression > Property' + : `${prefix}> ObjectExpression:first-child CallExpression[callee.name="choose"] > ArrayExpression > ObjectExpression > Property` -const propertyOfChoosableActionObjectAlt = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child CallExpression[callee.type="MemberExpression"][callee.object.name="actions"][callee.property.name="choose"] > ArrayExpression > ObjectExpression > Property' +const propertyOfChoosableActionObjectAlt = (prefix) => + prefix === '' + ? 'CallExpression[callee.type="MemberExpression"][callee.object.name="actions"][callee.property.name="choose"] > ArrayExpression > ObjectExpression > Property' + : `${prefix}> ObjectExpression:first-child CallExpression[callee.type="MemberExpression"][callee.object.name="actions"][callee.property.name="choose"] > ArrayExpression > ObjectExpression > Property` const defaultOptions = { allowKnownActionCreators: false, @@ -156,6 +161,7 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) const options = context.options[0] || defaultOptions @@ -247,17 +253,23 @@ module.exports = { } } + const spawnCall = (prefix) => + prefix === '' + ? 'CallExpression' + : `${prefix}> ObjectExpression:first-child CallExpression` + return { ...(version === 4 ? xstateDetector.visitors : {}), - [propertyOfEventTransition]: checkTransitionProperty, - [propertyOfEventTransitionInArray]: checkTransitionProperty, - [propertyOfAltEventTransitionInArray]: checkTransitionProperty, - [propertyOfOnDoneOnErrorTransition]: checkTransitionProperty, - [propertyOfOnDoneOnErrorTransitionInArray]: checkTransitionProperty, - [propertyOfChoosableActionObject]: checkTransitionProperty, - [propertyOfChoosableActionObjectAlt]: checkTransitionProperty, - - [activitiesProperty]: function (node) { + [propertyOfEventTransition(prefix)]: checkTransitionProperty, + [propertyOfEventTransitionInArray(prefix)]: checkTransitionProperty, + [propertyOfAltEventTransitionInArray(prefix)]: checkTransitionProperty, + [propertyOfOnDoneOnErrorTransition(prefix)]: checkTransitionProperty, + [propertyOfOnDoneOnErrorTransitionInArray(prefix)]: + checkTransitionProperty, + [propertyOfChoosableActionObject(prefix)]: checkTransitionProperty, + [propertyOfChoosableActionObjectAlt(prefix)]: checkTransitionProperty, + + [activitiesProperty(prefix)]: function (node) { if ( isFunctionExpression(node.value) || isIdentifier(node.value) || @@ -272,11 +284,11 @@ module.exports = { } }, - [srcPropertyInsideInvoke]: checkActorSrc, + [srcPropertyInsideInvoke(prefix)]: checkActorSrc, - [srcPropertyInsideInvokeArray]: checkActorSrc, + [srcPropertyInsideInvokeArray(prefix)]: checkActorSrc, - [entryExitProperty]: function (node) { + [entryExitProperty(prefix)]: function (node) { if ( isInlineAction( node.value, @@ -308,34 +320,33 @@ module.exports = { } }, - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child CallExpression': - function (node) { - if (version === 4) { - if ( - xstateDetector.isSpawnCallExpression(node) && - node.arguments[0] && - !isStringLiteral(node.arguments[0]) - ) { - context.report({ - node: node.arguments[0], - messageId: 'moveActorToOptions', - }) - } - return - } - - // In XState v5, spawn comes from arguments passed to the callback within assign() - if (!isSpawnFromParametersCallExpresion(node)) { - return - } - - if (node.arguments[0] && !isStringLiteral(node.arguments[0])) { + [spawnCall(prefix)]: function (node) { + if (version === 4) { + if ( + xstateDetector.isSpawnCallExpression(node) && + node.arguments[0] && + !isStringLiteral(node.arguments[0]) + ) { context.report({ node: node.arguments[0], messageId: 'moveActorToOptions', }) } - }, + return + } + + // In XState v5, spawn comes from arguments passed to the callback within assign() + if (!isSpawnFromParametersCallExpresion(node)) { + return + } + + if (node.arguments[0] && !isStringLiteral(node.arguments[0])) { + context.report({ + node: node.arguments[0], + messageId: 'moveActorToOptions', + }) + } + }, } }, } diff --git a/lib/rules/no-invalid-conditional-action.js b/lib/rules/no-invalid-conditional-action.js index 71f437c..97b747e 100644 --- a/lib/rules/no-invalid-conditional-action.js +++ b/lib/rules/no-invalid-conditional-action.js @@ -7,6 +7,7 @@ const { isObjectExpression, } = require('../utils/predicates') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') const validChooseActionProperty = { 4: ['cond', 'actions'], @@ -19,10 +20,10 @@ function isValidChooseActionProperty(property, version) { ) } -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"]' +const propertyOfChoosableActionObject = (prefix) => + `${prefix}CallExpression[callee.name="choose"] > ArrayExpression > ObjectExpression > Property` +const chooseFunctionCall = (prefix) => + `${prefix}CallExpression[callee.name="choose"]` module.exports = { meta: { @@ -45,10 +46,11 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) return { - [propertyOfChoosableActionObject]: + [propertyOfChoosableActionObject(prefix)]: function checkChooseActionObjectProperty(node) { if (!isValidChooseActionProperty(node, version)) { context.report({ @@ -58,7 +60,7 @@ module.exports = { }) } }, - [chooseFunctionCall]: function checkChooseFirstArgument(node) { + [chooseFunctionCall(prefix)]: function checkChooseFirstArgument(node) { if (node.arguments.length < 1) { context.report({ node, diff --git a/lib/rules/no-invalid-state-props.js b/lib/rules/no-invalid-state-props.js index 96bf71e..305fb56 100644 --- a/lib/rules/no-invalid-state-props.js +++ b/lib/rules/no-invalid-state-props.js @@ -9,6 +9,7 @@ const { } = require('../utils/predicates') const { allPass } = require('../utils/combinators') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') const validProperties = { 4: [ @@ -170,11 +171,12 @@ function validateHistoryPropertyValue(prop, context) { return true } -const stateDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="states"] > ObjectExpression > Property > ObjectExpression' +const stateDeclaration = (prefix) => + `${prefix}Property[key.name="states"] > ObjectExpression > Property > ObjectExpression` -const rootStateDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child' +// Without createMachine we have no way of checking whether an ObjectExpression is root state node +const rootStateDeclaration = (prefix) => + `${prefix}> ObjectExpression:first-child` module.exports = { meta: { @@ -205,9 +207,10 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) return { - [stateDeclaration]: function (node) { + [stateDeclaration(prefix)]: function (node) { const isHistoryNode = hasHistoryTypeProperty(node) const isCompoundStateNode = isCompoundState(node) node.properties.forEach((prop) => { @@ -256,46 +259,76 @@ module.exports = { }) }, - [rootStateDeclaration]: function (node) { - const isHistoryNode = hasHistoryTypeProperty(node) - const isCompoundStateNode = isCompoundState(node) - node.properties.forEach((prop) => { - if ( - !isHistoryNode && - (prop.key.name === 'history' || prop.key.name === 'target') - ) { - context.report({ - node: prop, - messageId: 'propAllowedOnHistoryStateOnly', - data: { propName: prop.key.name }, - }) - return + ...(prefix !== '' + ? { + [rootStateDeclaration(prefix)]: checkRootNode, } + : { + // If the createMachine prefix cannot be considered, we search for + // root state nodes by some tell-tale props: context, types + // In case of XState v4: context, tsTypes, schema + ObjectExpression(node) { + // check if it is a root state node config + if ( + version === 4 && + !( + hasProperty('context', node) || + hasProperty('tsTypes', node) || + hasProperty('schema', node) + ) + ) { + return false + } + if ( + version === 5 && + !(hasProperty('context', node) || hasProperty('types', node)) + ) { + return false + } + return checkRootNode(node) + }, + }), + } - if (prop.key.name === 'initial' && !isCompoundStateNode) { - context.report({ - node: prop, - messageId: 'initialAllowedOnlyOnCompoundNodes', - }) - return - } + function checkRootNode(node) { + const isHistoryNode = hasHistoryTypeProperty(node) + const isCompoundStateNode = isCompoundState(node) + node.properties.forEach((prop) => { + if ( + !isHistoryNode && + (prop.key.name === 'history' || prop.key.name === 'target') + ) { + context.report({ + node: prop, + messageId: 'propAllowedOnHistoryStateOnly', + data: { propName: prop.key.name }, + }) + return + } - if (!isValidRootStateProperty(prop, version)) { - context.report({ - node: prop, - messageId: 'invalidRootStateProperty', - data: { propName: prop.key.name }, - }) - return - } + if (prop.key.name === 'initial' && !isCompoundStateNode) { + context.report({ + node: prop, + messageId: 'initialAllowedOnlyOnCompoundNodes', + }) + return + } - if (!validateTypePropertyValue(prop, context)) { - return - } + if (!isValidRootStateProperty(prop, version)) { + context.report({ + node: prop, + messageId: 'invalidRootStateProperty', + data: { propName: prop.key.name }, + }) + return + } - validateHistoryPropertyValue(prop, context) - }) - }, + if (!validateTypePropertyValue(prop, context)) { + return + } + + validateHistoryPropertyValue(prop, context) + }) } }, } diff --git a/lib/rules/no-invalid-transition-props.js b/lib/rules/no-invalid-transition-props.js index ab9037d..5ef24c2 100644 --- a/lib/rules/no-invalid-transition-props.js +++ b/lib/rules/no-invalid-transition-props.js @@ -3,6 +3,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const { isObjectExpression, isArrayExpression } = require('../utils/predicates') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') const validTransitionProperties = { 4: ['target', 'cond', 'actions', 'in', 'internal', 'description'], @@ -19,25 +20,19 @@ function isValidTransitionProperty(property, version) { // e.g. // states: { idle: { on: { EVENT: { target: 'active' }}}} // states: { idle: { on: { EVENT: [{ target: 'active' }]}}} -const eventTransitionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="states"] > ObjectExpression > Property > ObjectExpression > Property[key.name="on"] > ObjectExpression > Property' - -const globalEventTransitionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression > Property[key.name="on"] > ObjectExpression > Property' +const eventTransitionDeclaration = (prefix) => + `${prefix}ObjectExpression > Property[key.name="on"] > ObjectExpression > Property` // e.g. // states: { idle: { on: [ { event: 'EVENT', target: 'active' } ]}} -const eventTransitionArrayDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="states"] > ObjectExpression > Property > ObjectExpression > Property[key.name="on"] > ArrayExpression' - -const globalEventTransitionArrayDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression > Property[key.name="on"] > ArrayExpression' +const eventTransitionArrayDeclaration = (prefix) => + `${prefix}ObjectExpression > Property[key.name="on"] > ArrayExpression` -const onDoneOrOnErrorTransitionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^onDone$|^onError$/]' +const onDoneOrOnErrorTransitionDeclaration = (prefix) => + `${prefix}Property[key.name=/^onDone$|^onError$/]` -const alwaysTransitionDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="states"] > ObjectExpression > Property > ObjectExpression > Property[key.name="always"] > ArrayExpression' +const alwaysTransitionDeclaration = (prefix) => + `${prefix}Property[key.name="states"] > ObjectExpression > Property > ObjectExpression > Property[key.name="always"] > ArrayExpression` module.exports = { meta: { @@ -56,6 +51,7 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) function checkTransitionDeclaration(node) { const transitionValue = node.value @@ -111,14 +107,14 @@ module.exports = { } return { - [eventTransitionDeclaration]: checkTransitionDeclaration, - [globalEventTransitionDeclaration]: checkTransitionDeclaration, + [eventTransitionDeclaration(prefix)]: checkTransitionDeclaration, - [eventTransitionArrayDeclaration]: checkTransitionArrayDeclaration, - [globalEventTransitionArrayDeclaration]: checkTransitionArrayDeclaration, + [eventTransitionArrayDeclaration(prefix)]: + checkTransitionArrayDeclaration, - [onDoneOrOnErrorTransitionDeclaration]: checkTransitionDeclaration, - [alwaysTransitionDeclaration]: checkTransitionArrayDeclaration, + [onDoneOrOnErrorTransitionDeclaration(prefix)]: + checkTransitionDeclaration, + [alwaysTransitionDeclaration(prefix)]: checkTransitionArrayDeclaration, } }, } diff --git a/lib/rules/no-misplaced-on-transition.js b/lib/rules/no-misplaced-on-transition.js index ab6936c..cf2581f 100644 --- a/lib/rules/no-misplaced-on-transition.js +++ b/lib/rules/no-misplaced-on-transition.js @@ -2,6 +2,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const { isWithinInvoke } = require('../utils/predicates') +const getSelectorPrefix = require('../utils/getSelectorPrefix') function isWithinStatesDeclaration(node) { const parentProp = node.parent.parent @@ -27,23 +28,23 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"]': - function (node) { - if (isWithinInvoke(node)) { - context.report({ - node, - messageId: 'onTransitionInsideInvokeForbidden', - }) - return - } - if (isWithinStatesDeclaration(node)) { - context.report({ - node, - messageId: 'onTransitionInsideStatesForbidden', - }) - } - }, + [`${prefix}Property[key.name="on"]`]: function (node) { + if (isWithinInvoke(node)) { + context.report({ + node, + messageId: 'onTransitionInsideInvokeForbidden', + }) + return + } + if (isWithinStatesDeclaration(node)) { + context.report({ + node, + messageId: 'onTransitionInsideStatesForbidden', + }) + } + }, } }, } diff --git a/lib/rules/no-ondone-outside-compound-state.js b/lib/rules/no-ondone-outside-compound-state.js index f468f28..f95dcbf 100644 --- a/lib/rules/no-ondone-outside-compound-state.js +++ b/lib/rules/no-ondone-outside-compound-state.js @@ -3,6 +3,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const { getTypeProperty } = require('../utils/selectors') const { hasProperty, isWithinInvoke } = require('../utils/predicates') +const getSelectorPrefix = require('../utils/getSelectorPrefix') function isWithinCompoundStateNode(node) { const stateNode = node.parent @@ -61,44 +62,44 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="onDone"]': - function (node) { - if (isWithinInvoke(node)) { - return - } - if (isWithinCompoundStateNode(node)) { - return - } - if (isWithinParallelStateNode(node)) { - return - } - if (isWithinAtomicStateNode(node)) { - context.report({ - node, - messageId: 'onDoneOnAtomicStateForbidden', - }) - return - } - if (isWithinHistoryStateNode(node)) { - context.report({ - node, - messageId: 'onDoneOnHistoryStateForbidden', - }) - return - } - if (isWithinFinalStateNode(node)) { - context.report({ - node, - messageId: 'onDoneOnFinalStateForbidden', - }) - return - } + [`${prefix}Property[key.name="onDone"]`]: function (node) { + if (isWithinInvoke(node)) { + return + } + if (isWithinCompoundStateNode(node)) { + return + } + if (isWithinParallelStateNode(node)) { + return + } + if (isWithinAtomicStateNode(node)) { context.report({ node, - messageId: 'onDoneUsedIncorrectly', + messageId: 'onDoneOnAtomicStateForbidden', }) - }, + return + } + if (isWithinHistoryStateNode(node)) { + context.report({ + node, + messageId: 'onDoneOnHistoryStateForbidden', + }) + return + } + if (isWithinFinalStateNode(node)) { + context.report({ + node, + messageId: 'onDoneOnFinalStateForbidden', + }) + return + } + context.report({ + node, + messageId: 'onDoneUsedIncorrectly', + }) + }, } }, } diff --git a/lib/rules/prefer-always.js b/lib/rules/prefer-always.js index 3537306..e3c5ea2 100644 --- a/lib/rules/prefer-always.js +++ b/lib/rules/prefer-always.js @@ -2,6 +2,7 @@ const getDocsUrl = require('../utils/getDocsUrl') const getSettings = require('../utils/getSettings') +const getSelectorPrefix = require('../utils/getSelectorPrefix') module.exports = { meta: { @@ -22,9 +23,10 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"] > ObjectExpression > Property[key.value=""]': + [`${prefix}Property[key.name="on"] > ObjectExpression > Property[key.value=""]`]: function (node) { if (version !== 4) { context.report({ diff --git a/lib/rules/prefer-predictable-action-arguments.js b/lib/rules/prefer-predictable-action-arguments.js index 2c3f49d..dd520b0 100644 --- a/lib/rules/prefer-predictable-action-arguments.js +++ b/lib/rules/prefer-predictable-action-arguments.js @@ -2,6 +2,8 @@ const getDocsUrl = require('../utils/getDocsUrl') const getSettings = require('../utils/getSettings') +const { hasProperty } = require('../utils/predicates') +const getSelectorPrefix = require('../utils/getSelectorPrefix') module.exports = { meta: { @@ -24,72 +26,98 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const { version } = getSettings(context) - return { - 'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child': - function (node) { - const predictableActionArgumentsProperty = node.properties.find( - (prop) => { - return prop.key.name === 'predictableActionArguments' - } - ) - if (version > 4) { - if (!predictableActionArgumentsProperty) { - return - } - context.report({ - node: predictableActionArgumentsProperty, - messageId: 'deprecatedPredictableActionArguments', - fix(fixer) { - return fixer.remove(predictableActionArgumentsProperty) - }, - }) - return - } - if (!predictableActionArgumentsProperty) { - if (node.properties.length === 0) { - context.report({ - node, - messageId: 'preferPredictableActionArguments', - fix(fixer) { - return fixer.replaceText( - node, - '{ predictableActionArguments: true }' - ) - }, - }) - } else { - context.report({ + function check(node) { + const predictableActionArgumentsProperty = node.properties.find( + (prop) => { + return prop.key.name === 'predictableActionArguments' + } + ) + if (version > 4) { + if (!predictableActionArgumentsProperty) { + return + } + context.report({ + node: predictableActionArgumentsProperty, + messageId: 'deprecatedPredictableActionArguments', + fix(fixer) { + return fixer.remove(predictableActionArgumentsProperty) + }, + }) + return + } + + if (!predictableActionArgumentsProperty) { + if (node.properties.length === 0) { + context.report({ + node, + messageId: 'preferPredictableActionArguments', + fix(fixer) { + return fixer.replaceText( node, - messageId: 'preferPredictableActionArguments', - fix(fixer) { - return fixer.insertTextBefore( - node.properties[0], - 'predictableActionArguments: true,\n' - ) - }, - }) - } - return - } + '{ predictableActionArguments: true }' + ) + }, + }) + } else { + context.report({ + node, + messageId: 'preferPredictableActionArguments', + fix(fixer) { + return fixer.insertTextBefore( + node.properties[0], + 'predictableActionArguments: true,\n' + ) + }, + }) + } + return + } - if ( - !predictableActionArgumentsProperty.value || - predictableActionArgumentsProperty.value.value !== true - ) { - context.report({ - node: predictableActionArgumentsProperty, - messageId: 'preferPredictableActionArguments', - fix(fixer) { - return fixer.replaceText( - predictableActionArgumentsProperty.value, - 'true' - ) - }, - }) - } - }, + if ( + !predictableActionArgumentsProperty.value || + predictableActionArgumentsProperty.value.value !== true + ) { + context.report({ + node: predictableActionArgumentsProperty, + messageId: 'preferPredictableActionArguments', + fix(fixer) { + return fixer.replaceText( + predictableActionArgumentsProperty.value, + 'true' + ) + }, + }) + } } + + return prefix !== '' + ? { + [`${prefix}> ObjectExpression:first-child`]: check, + } + : { + ObjectExpression(node) { + // check if it is a root state node config + if ( + version === 4 && + !( + hasProperty('context', node) || + hasProperty('tsTypes', node) || + hasProperty('schema', node) + ) + ) { + return false + } + if ( + version === 5 && + !(hasProperty('context', node) || hasProperty('types', node)) + ) { + return false + } + return check(node) + }, + } }, } diff --git a/lib/rules/state-names.js b/lib/rules/state-names.js index b3941ba..7efbb67 100644 --- a/lib/rules/state-names.js +++ b/lib/rules/state-names.js @@ -5,6 +5,7 @@ const { isReservedXStateWord } = require('../utils/predicates') const snakeCase = require('lodash.snakecase') const camelCase = require('lodash.camelcase') const upperFirst = require('lodash.upperfirst') +const getSelectorPrefix = require('../utils/getSelectorPrefix') function fixName(name, mode) { switch (mode) { @@ -33,16 +34,16 @@ function mustBeQuoted(string) { */ const defaultRegex = '^[a-z]*$' -const stateNodeDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="states"] > ObjectExpression > Property' -const targetDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="states"] Property[key.name="target"][value.type="Literal"]' -const targetArrayStringDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="states"] Property[key.name="target"][value.type="ArrayExpression"] > ArrayExpression > Literal' -const simpleEventTargetDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"] > ObjectExpression > Property[value.type="Literal"]' -const simpleOnDoneOnErrorTargetDeclaration = - 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property:matches([key.name="onDone"], [key.name="onError"])[value.type="Literal"]' +const stateNodeDeclaration = (prefix) => + `${prefix}Property[key.name="states"] > ObjectExpression > Property` +const targetDeclaration = (prefix) => + `${prefix}Property[key.name="states"] Property[key.name="target"][value.type="Literal"]` +const targetArrayStringDeclaration = (prefix) => + `${prefix}Property[key.name="states"] Property[key.name="target"][value.type="ArrayExpression"] > ArrayExpression > Literal` +const simpleEventTargetDeclaration = (prefix) => + `${prefix}Property[key.name="on"] > ObjectExpression > Property[value.type="Literal"]` +const simpleOnDoneOnErrorTargetDeclaration = (prefix) => + `${prefix}Property:matches([key.name="onDone"], [key.name="onError"])[value.type="Literal"]` module.exports = { meta: { @@ -81,6 +82,7 @@ module.exports = { }, create: function (context) { + const prefix = getSelectorPrefix(context.sourceCode) const mode = context.options[0] || 'camelCase' const regexOption = mode === 'regex' @@ -165,7 +167,7 @@ module.exports = { } return { - [stateNodeDeclaration]: function (node) { + [stateNodeDeclaration(prefix)]: function (node) { if (node.computed) { return } @@ -182,10 +184,10 @@ module.exports = { return validate(name, node.key) }, - [targetDeclaration]: validateTarget, - [simpleEventTargetDeclaration]: validateTarget, - [simpleOnDoneOnErrorTargetDeclaration]: validateTarget, - [targetArrayStringDeclaration]: validateTargetLiteral, + [targetDeclaration(prefix)]: validateTarget, + [simpleEventTargetDeclaration(prefix)]: validateTarget, + [simpleOnDoneOnErrorTargetDeclaration(prefix)]: validateTarget, + [targetArrayStringDeclaration(prefix)]: validateTargetLiteral, } }, } diff --git a/lib/utils/isInsideMachineDeclaration.js b/lib/utils/isInsideMachineDeclaration.js deleted file mode 100644 index 47bb3fa..0000000 --- a/lib/utils/isInsideMachineDeclaration.js +++ /dev/null @@ -1,12 +0,0 @@ -const { isCreateMachineCall } = require('./predicates') - -module.exports = function isInsideMachineDeclaration(node) { - let parent = node.parent - while (parent) { - if (isCreateMachineCall(parent)) { - return true - } - parent = parent.parent - } - return false -} diff --git a/tests/lib/rules/event-names.js b/tests/lib/rules/event-names.js index 2bc5a3b..7b832a7 100644 --- a/tests/lib/rules/event-names.js +++ b/tests/lib/rules/event-names.js @@ -345,6 +345,43 @@ const tests = { }, ], }, + { + // If xstate linting is enforced over the entire file, errors are reported even outside of createMachine calls + code: ` + /* eslint-plugin-xstate-include */ + const obj = { + on: { + thisIsNotEvent: 'foo', + 'neither.is.this': 'foo', + } + } + `, + errors: [ + { + messageId: 'invalidEventName', + data: { + eventName: 'thisIsNotEvent', + fixedEventName: 'THIS_IS_NOT_EVENT', + }, + }, + { + messageId: 'invalidEventName', + data: { + eventName: 'neither.is.this', + fixedEventName: 'NEITHER.IS.THIS', + }, + }, + ], + output: ` + /* eslint-plugin-xstate-include */ + const obj = { + on: { + THIS_IS_NOT_EVENT: 'foo', + 'NEITHER.IS.THIS': 'foo', + } + } + `, + }, ], } diff --git a/tests/lib/rules/no-async-guards.js b/tests/lib/rules/no-async-guards.js index 730df2a..0ac276c 100644 --- a/tests/lib/rules/no-async-guards.js +++ b/tests/lib/rules/no-async-guards.js @@ -81,6 +81,21 @@ const tests = { ) ` ), + withVersion( + 5, + ` + createMachine( + {}, + { + guards: { + myGuard: () => {}, + myGuard2: function () {}, + myGuard3() {}, + }, + } + ) + ` + ), ], invalid: [ withVersion(4, { @@ -197,6 +212,67 @@ const tests = { { messageId: 'guardCannotBeAsync' }, ], }), + // lint code outside of createMachine if it is enforced + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const obj = { + on: { + EVENT: { + cond: async () => {}, + }, + }, + invoke: { + src: 'myActor', + onDone: { + cond: async () => {}, + }, + }, + } + `, + errors: [ + { messageId: 'guardCannotBeAsync' }, + { messageId: 'guardCannotBeAsync' }, + ], + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const obj = { + on: { + EVENT: { + guard: async () => {}, + }, + }, + invoke: { + src: 'myActor', + onDone: { + guard: async () => {}, + }, + }, + } + `, + errors: [ + { messageId: 'guardCannotBeAsync' }, + { messageId: 'guardCannotBeAsync' }, + ], + }), + // check implementations outside of createMachine + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const implementations = { + guards: { + guard1: async () => {}, + guard2: async function () {}, + } + } + `, + errors: [ + { messageId: 'guardCannotBeAsync' }, + { messageId: 'guardCannotBeAsync' }, + ], + }), ], } diff --git a/tests/lib/rules/no-auto-forward.js b/tests/lib/rules/no-auto-forward.js index 22c6678..5a06b42 100644 --- a/tests/lib/rules/no-auto-forward.js +++ b/tests/lib/rules/no-auto-forward.js @@ -92,6 +92,31 @@ const tests = { `, errors: [{ messageId: 'autoForwardDeprecated' }], }), + // check the code outside of createMachine if the rule is enforced + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + invoke: { + src: 'myActorLogic', + autoForward: true, + } + } + `, + errors: [{ messageId: 'noAutoForward' }], + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + invoke: { + src: 'myActorLogic', + autoForward: true, + } + } + `, + errors: [{ messageId: 'autoForwardDeprecated' }], + }), ], } diff --git a/tests/lib/rules/no-imperative-action.js b/tests/lib/rules/no-imperative-action.js index 0b9343c..4e83038 100644 --- a/tests/lib/rules/no-imperative-action.js +++ b/tests/lib/rules/no-imperative-action.js @@ -425,6 +425,98 @@ const tests = { }, ], }), + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + actions: { + myAction1: () => assign(), + myAction2: () => { send() }, + myAction3: function() { sendParent() }, + myAction4: () => respond(), + myAction5: () => raise(), + }, + entry: () => send(), + exit: () => send(), + } + `, + errors: [ + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'assign' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'send' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'sendParent' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'respond' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'raise' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'send' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'send' }, + }, + ], + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + actions: { + myAction1: () => assign(), + myAction2: () => { sendTo() }, + myAction3: function() { sendParent() }, + myAction4: () => choose(), + myAction5: () => raise(), + }, + entry: () => sendTo(), + exit: () => sendTo(), + } + `, + errors: [ + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'assign' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'sendTo' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'sendParent' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'choose' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'raise' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'sendTo' }, + }, + { + messageId: 'imperativeActionCreator', + data: { actionCreator: 'sendTo' }, + }, + ], + }), ], } diff --git a/tests/lib/rules/no-infinite-loop.js b/tests/lib/rules/no-infinite-loop.js index a7784e7..aad925e 100644 --- a/tests/lib/rules/no-infinite-loop.js +++ b/tests/lib/rules/no-infinite-loop.js @@ -174,6 +174,19 @@ const tests = { }) ` ), + // outside of createMachine it is not linted by default + withVersion( + 4, + ` + const config = { + states: { + deciding: { + always: {}, + }, + }, + } + ` + ), ], invalid: [ withVersion(4, { @@ -373,6 +386,20 @@ const tests = { { messageId: 'noTargetHasGuardNoAssign' }, ], }), + // outside of createMachine it is not linted by default + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + deciding: { + always: {}, + }, + }, + } + `, + errors: [{ messageId: 'noTargetNoGuardIsSingle' }], + }), ], } @@ -381,4 +408,4 @@ const ruleTester = new RuleTester({ ecmaVersion: 6, }, }) -ruleTester.run('no-infnite-loop', rule, tests) +ruleTester.run('no-infinite-loop', rule, tests) diff --git a/tests/lib/rules/no-inline-implementation.js b/tests/lib/rules/no-inline-implementation.js index 2cbfa6d..f7613ad 100644 --- a/tests/lib/rules/no-inline-implementation.js +++ b/tests/lib/rules/no-inline-implementation.js @@ -87,6 +87,7 @@ const tests = { 4, ` /* eslint no-inline-implementation: [ "warn", { "allowKnownActionCreators": true } ] */ + const { spawn } = require('xstate') createMachine( { states: { @@ -367,6 +368,30 @@ const tests = { }) ` ), + // code outside of createMachine is ignored by default + withVersion( + 4, + ` + const config = { + states: { + active: { + invoke: { + src: () => {}, + }, + entry: () => {}, + on: { + OFF: { + cond: () => {}, + target: 'inactive', + actions: () => {}, + }, + }, + activities: () => {}, + }, + }, + } + ` + ), ], invalid: [ withVersion(4, { @@ -696,6 +721,82 @@ const tests = { { messageId: 'moveActionToOptions' }, ], }), + // reports error ourside of createMachine if there is the include directive + withVersion(4, { + code: ` + /* eslint no-inline-implementation: [ "warn", { "allowKnownActionCreators": true } ] */ + /* eslint-plugin-xstate-include */ + const { spawn } = require('xstate') + const config = { + states: { + active: { + invoke: { + src: () => {}, + }, + entry: () => {}, + on: { + OFF: { + cond: () => {}, + target: 'inactive', + actions: [ + () => {}, + assign({ + ref: () => spawn(() => {}), + }) + ] + }, + }, + activities: () => {}, + }, + }, + } + `, + errors: [ + { messageId: 'moveActorToOptions' }, + { messageId: 'moveActionToOptions' }, + { messageId: 'moveGuardToOptions' }, + { messageId: 'moveActionToOptions' }, + { messageId: 'moveActorToOptions' }, + { messageId: 'moveActivityToOptions' }, + ], + }), + withVersion(5, { + code: ` + /* eslint no-inline-implementation: [ "warn", { "allowKnownActionCreators": true } ] */ + /* eslint-plugin-xstate-include */ + const config = { + states: { + active: { + invoke: { + src: () => {}, + }, + entry: () => {}, + on: { + OFF: { + guard: () => {}, + target: 'inactive', + actions: [ + () => {}, + assign({ + ref: ({ spawn }) => spawn(() => {}), + }) + ] + }, + }, + activities: () => {}, + }, + }, + } + `, + errors: [ + { messageId: 'moveActorToOptions' }, + { messageId: 'moveActionToOptions' }, + { messageId: 'moveGuardToOptions' }, + { messageId: 'moveActionToOptions' }, + { messageId: 'moveActorToOptions' }, + { messageId: 'moveActivityToOptions' }, + ], + }), ], } diff --git a/tests/lib/rules/no-invalid-conditional-action.js b/tests/lib/rules/no-invalid-conditional-action.js index 6c11a3b..e6fb9ff 100644 --- a/tests/lib/rules/no-invalid-conditional-action.js +++ b/tests/lib/rules/no-invalid-conditional-action.js @@ -51,6 +51,61 @@ const tests = { }) ` ), + // no errors outside ofc reateMachine by default + withVersion( + 4, + ` + const config = { + states: { + active: { + on: { + EVENT1: { + actions: choose([{ + cond: 'myGuard', + guard: '???', + invoke: '???', + actions: [], + }]), + }, + }, + entry: choose([{ + cond: 'myGuard', + guard: '???', + invoke: '???', + actions: [], + }]), + } + } + } + ` + ), + withVersion( + 5, + ` + const config = { + states: { + active: { + on: { + EVENT1: { + actions: choose([{ + guard: 'myGuard', + cond: '???', + invoke: '???', + actions: [], + }]), + }, + }, + entry: choose([{ + guard: 'myGuard', + cond: '???', + invoke: '???', + actions: [], + }]), + } + } + } + ` + ), ], invalid: [ // transitions within the choose action creator @@ -236,6 +291,97 @@ const tests = { }, ], }), + // should report errors outside of createMachine with the directive + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + active: { + on: { + EVENT1: { + actions: choose([{ + cond: 'myGuard', + guard: '???', + invoke: '???', + actions: [], + }]), + }, + }, + entry: choose([{ + cond: 'myGuard', + guard: '???', + invoke: '???', + actions: [], + }]), + } + } + } + `, + errors: [ + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'guard' }, + }, + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'invoke' }, + }, + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'guard' }, + }, + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'invoke' }, + }, + ], + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + active: { + on: { + EVENT1: { + actions: choose([{ + guard: 'myGuard', + cond: '???', + invoke: '???', + actions: [], + }]), + }, + }, + entry: choose([{ + guard: 'myGuard', + cond: '???', + invoke: '???', + actions: [], + }]), + } + } + } + `, + errors: [ + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'cond' }, + }, + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'invoke' }, + }, + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'cond' }, + }, + { + messageId: 'invalidConditionalActionProperty', + data: { propName: 'invoke' }, + }, + ], + }), ], } diff --git a/tests/lib/rules/no-invalid-state-props.js b/tests/lib/rules/no-invalid-state-props.js index badd072..e1f8893 100644 --- a/tests/lib/rules/no-invalid-state-props.js +++ b/tests/lib/rules/no-invalid-state-props.js @@ -110,6 +110,37 @@ const tests = { }) ` ), + // no errors reported outside of createMachine by default + withVersion( + 4, + ` + const config = { + id: 'myMachine', + context: {}, + foo: '???', + states: { + idle: { + foo: '???', + }, + }, + } + ` + ), + withVersion( + 5, + ` + const config = { + id: 'myMachine', + context: {}, + foo: '???', + states: { + idle: { + foo: '???', + }, + }, + } + ` + ), ], invalid: [ // unrecognized prop names @@ -249,6 +280,49 @@ const tests = { { messageId: 'invalidHistoryValue', data: { value: 'shallowish' } }, ], }), + // should report errors outside of createMachine if there is the comment directive + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + id: 'myMachine', + context: { + simpson: 10, + }, + foo: '???', + states: { + idle: { + boo: '???', + }, + }, + } + `, + errors: [ + { messageId: 'invalidRootStateProperty', data: { propName: 'foo' } }, + { messageId: 'invalidStateProperty', data: { propName: 'boo' } }, + ], + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + id: 'myMachine', + context: { + simpson: 10, + }, + foo: '???', + states: { + idle: { + boo: '???', + }, + }, + } + `, + errors: [ + { messageId: 'invalidRootStateProperty', data: { propName: 'foo' } }, + { messageId: 'invalidStateProperty', data: { propName: 'boo' } }, + ], + }), ], } diff --git a/tests/lib/rules/no-invalid-transition-props.js b/tests/lib/rules/no-invalid-transition-props.js index 671bd06..96d7827 100644 --- a/tests/lib/rules/no-invalid-transition-props.js +++ b/tests/lib/rules/no-invalid-transition-props.js @@ -171,6 +171,55 @@ const tests = { }) ` ), + // no errors outside of the createMachine calls by default + withVersion( + 4, + ` + const config = { + states: { + idle: { + always: [ + { + foo: '???', + }, + ], + on: { + EVENT: { + foo: '???' + } + }, + }, + }, + onDone: { + foo: '???' + }, + } + ` + ), + withVersion( + 5, + ` + const config = { + states: { + idle: { + always: [ + { + foo: '???', + }, + ], + on: { + EVENT: { + foo: '???' + } + }, + }, + }, + onDone: { + foo: '???' + }, + } + ` + ), ], invalid: [ withVersion(4, { @@ -406,6 +455,85 @@ const tests = { }, ], }), + // errors reported outside of createMachine if there is a comment directive + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + idle: { + always: [ + { + foo: '???', + }, + ], + on: { + EVENT: { + cond: 'myGuard', + boo: '???', + }, + }, + }, + }, + onDone: { + unknown: '???' + }, + } + `, + errors: [ + { + messageId: 'invalidTransitionProperty', + data: { propName: 'foo' }, + }, + { + messageId: 'invalidTransitionProperty', + data: { propName: 'boo' }, + }, + { + messageId: 'invalidTransitionProperty', + data: { propName: 'unknown' }, + }, + ], + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + idle: { + always: [ + { + foo: '???', + }, + ], + on: { + EVENT: { + guard: 'myGuard', + boo: '???', + }, + }, + }, + }, + onDone: { + unknown: '???' + }, + } + `, + errors: [ + { + messageId: 'invalidTransitionProperty', + data: { propName: 'foo' }, + }, + { + messageId: 'invalidTransitionProperty', + data: { propName: 'boo' }, + }, + { + messageId: 'invalidTransitionProperty', + data: { propName: 'unknown' }, + }, + ], + }), ], } diff --git a/tests/lib/rules/no-misplaced-on-transition.js b/tests/lib/rules/no-misplaced-on-transition.js index a9bee0a..1d60a64 100644 --- a/tests/lib/rules/no-misplaced-on-transition.js +++ b/tests/lib/rules/no-misplaced-on-transition.js @@ -38,6 +38,21 @@ const tests = { }, }) `, + // no errors outside of createmachine by default + ` + const config = { + states: { + on: { + EVENT: 'passive', + }, + }, + invoke: { + on: { + EVENT: 'passive', + }, + } + } + `, ], invalid: [ { @@ -70,6 +85,28 @@ const tests = { `, errors: [{ messageId: 'onTransitionInsideInvokeForbidden' }], }, + // errors reported outside of createMachine if there is the comment directive + { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + on: { + EVENT: 'passive', + }, + }, + invoke: { + on: { + EVENT: 'passive', + }, + } + } + `, + errors: [ + { messageId: 'onTransitionInsideStatesForbidden' }, + { messageId: 'onTransitionInsideInvokeForbidden' }, + ], + }, ], } diff --git a/tests/lib/rules/no-ondone-outside-compound-state.js b/tests/lib/rules/no-ondone-outside-compound-state.js index 980f248..63e7676 100644 --- a/tests/lib/rules/no-ondone-outside-compound-state.js +++ b/tests/lib/rules/no-ondone-outside-compound-state.js @@ -59,6 +59,30 @@ const tests = { }, }) `, + // no errors outside of createMachine by default + ` + const config = { + states: { + active: { + type: 'atomic', + onDone: 'idle', + }, + hist: { + type: 'history', + onDone: 'idle', + }, + idle: { + onDone: 'active', + }, + finished: { + type: 'final', + onDone: { + actions: () => {}, + }, + } + }, + } + `, ], invalid: [ { @@ -124,6 +148,39 @@ const tests = { `, errors: [{ messageId: 'onDoneUsedIncorrectly' }], }, + // errors reported outside of createMachine if there is the comment directive + { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + active: { + type: 'atomic', + onDone: 'idle', + }, + hist: { + type: 'history', + onDone: 'idle', + }, + idle: { + onDone: 'active', + }, + finished: { + type: 'final', + onDone: { + actions: () => {}, + }, + } + }, + } + `, + errors: [ + { messageId: 'onDoneOnAtomicStateForbidden' }, + { messageId: 'onDoneOnHistoryStateForbidden' }, + { messageId: 'onDoneUsedIncorrectly' }, + { messageId: 'onDoneOnFinalStateForbidden' }, + ], + }, ], } diff --git a/tests/lib/rules/prefer-always.js b/tests/lib/rules/prefer-always.js index 9c54379..3434421 100644 --- a/tests/lib/rules/prefer-always.js +++ b/tests/lib/rules/prefer-always.js @@ -34,6 +34,41 @@ const tests = { }) ` ), + // no errors outside of createMachine by default + withVersion( + 4, + ` + const config = { + states: { + playing: { + on: { + '': [ + { target: 'win', cond: 'didPlayerWin' }, + { target: 'lose', cond: 'didPlayerLose' }, + ], + }, + }, + }, + } + ` + ), + withVersion( + 5, + ` + const config = { + states: { + playing: { + on: { + '': [ + { target: 'win', guard: 'didPlayerWin' }, + { target: 'lose', guard: 'didPlayerLose' }, + ], + }, + }, + }, + } + ` + ), ], invalid: [ withVersion(4, { @@ -70,6 +105,43 @@ const tests = { `, errors: [{ messageId: 'eventlessTransitionsDeprecated' }], }), + // errors reported outside of createMachine if there is the comment directive + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + playing: { + on: { + '': [ + { target: 'win', cond: 'didPlayerWin' }, + { target: 'lose', cond: 'didPlayerLose' }, + ], + }, + }, + }, + } + `, + errors: [{ messageId: 'preferAlways' }], + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + playing: { + on: { + '': [ + { target: 'win', guard: 'didPlayerWin' }, + { target: 'lose', guard: 'didPlayerLose' }, + ], + }, + }, + }, + } + `, + errors: [{ messageId: 'eventlessTransitionsDeprecated' }], + }), ], } diff --git a/tests/lib/rules/prefer-predictable-action-arguments.js b/tests/lib/rules/prefer-predictable-action-arguments.js index 9499a7b..0119284 100644 --- a/tests/lib/rules/prefer-predictable-action-arguments.js +++ b/tests/lib/rules/prefer-predictable-action-arguments.js @@ -18,6 +18,25 @@ const tests = { createMachine({}) ` ), + // no errors outside of createMachine by default + withVersion( + 4, + ` + const config = { + predictableActionArguments: false, + context: {}, + } + ` + ), + withVersion( + 5, + ` + const config = { + predictableActionArguments: false, + context: {}, + } + ` + ), ], invalid: [ withVersion(4, { @@ -84,6 +103,57 @@ initial: 'ready' }) `, }), + // errors reported outside of createMachine if there is the comment directive + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + context: {}, + } + `, + errors: [{ messageId: 'preferPredictableActionArguments' }], + output: ` + /* eslint-plugin-xstate-include */ + const config = { + predictableActionArguments: true, +context: {}, + } + `, + }), + withVersion(4, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + predictableActionArguments: false, + context: {}, + } + `, + errors: [{ messageId: 'preferPredictableActionArguments' }], + output: ` + /* eslint-plugin-xstate-include */ + const config = { + predictableActionArguments: true, + context: {}, + } + `, + }), + withVersion(5, { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + context: {}, + predictableActionArguments: false + } + `, + errors: [{ messageId: 'deprecatedPredictableActionArguments' }], + output: ` + /* eslint-plugin-xstate-include */ + const config = { + context: {}, + + } + `, + }), ], } diff --git a/tests/lib/rules/state-names.js b/tests/lib/rules/state-names.js index 57bbce7..7e70bc2 100644 --- a/tests/lib/rules/state-names.js +++ b/tests/lib/rules/state-names.js @@ -55,6 +55,25 @@ const tests = { }, }) `, + // no errors outside of createMachine by default + ` + const config = { + states: { + PowerOn: {}, + power_on: {}, + 'power:on': {}, + 'power.on': {}, + entry: {}, + }, + onDone: 'power_on', + on: { + CLICK: 'power_on', + BUMP: { + target: 'power_on', + } + } + } + `, ], invalid: [ @@ -277,6 +296,85 @@ const tests = { }) `, }, + // report errors outside of createMachine if there is the comment directive + { + code: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + PowerOn: {}, + power_on: {}, + 'power:on': {}, + 'power.on': {}, + entry: {}, + someState: { + on: { + CLICK: 'power_shut', + BUMP: { + target: 'power_kill', + }, + }, + }, + }, + onDone: 'power_off', + } + `, + errors: [ + { + messageId: 'invalidStateName', + data: { name: 'PowerOn', fixedName: 'powerOn' }, + }, + { + messageId: 'invalidStateName', + data: { name: 'power_on', fixedName: 'powerOn' }, + }, + { + messageId: 'invalidStateName', + data: { name: 'power:on', fixedName: 'powerOn' }, + }, + { + messageId: 'invalidStateName', + data: { name: 'power.on', fixedName: 'powerOn' }, + }, + { + messageId: 'stateNameIsReservedWord', + data: { name: 'entry' }, + }, + { + messageId: 'invalidStateName', + data: { name: 'power_shut', fixedName: 'powerShut' }, + }, + { + messageId: 'invalidStateName', + data: { name: 'power_kill', fixedName: 'powerKill' }, + }, + { + messageId: 'invalidStateName', + data: { name: 'power_off', fixedName: 'powerOff' }, + }, + ], + output: ` + /* eslint-plugin-xstate-include */ + const config = { + states: { + powerOn: {}, + powerOn: {}, + powerOn: {}, + powerOn: {}, + entry: {}, + someState: { + on: { + CLICK: 'powerShut', + BUMP: { + target: 'powerKill', + }, + }, + }, + }, + onDone: 'powerOff', + } + `, + }, ], }