From 92d077d119e9c2bd0d6778ec8ab059b993164786 Mon Sep 17 00:00:00 2001 From: Richard Laffers Date: Thu, 5 Oct 2023 17:43:05 +0200 Subject: [PATCH] feat(system-id): add detection of missing systemId within spawn calls --- lib/index.js | 4 +- .../{enforce-system-id.js => system-id.js} | 99 ++++++++++++++----- lib/utils/predicates.js | 22 +++++ 3 files changed, 100 insertions(+), 25 deletions(-) rename lib/rules/{enforce-system-id.js => system-id.js} (50%) diff --git a/lib/index.js b/lib/index.js index 30feca9..5de35af 100644 --- a/lib/index.js +++ b/lib/index.js @@ -28,7 +28,7 @@ module.exports = { 'no-invalid-state-props': require('./rules/no-invalid-state-props'), 'no-async-guard': require('./rules/no-async-guard'), 'no-invalid-conditional-action': require('./rules/no-invalid-conditional-action'), - 'enforce-system-id': require('./rules/enforce-system-id'), + 'system-id': require('./rules/system-id'), }, configs: { // Requires: xstate@5 @@ -80,7 +80,7 @@ module.exports = { 'xstate/no-invalid-state-props': 'error', 'xstate/no-invalid-conditional-action': 'error', 'xstate/no-async-guard': 'error', - 'xstate/enforce-system-id': 'error', + 'xstate/system-id': 'error', }, }, // Requires: xstate@4 diff --git a/lib/rules/enforce-system-id.js b/lib/rules/system-id.js similarity index 50% rename from lib/rules/enforce-system-id.js rename to lib/rules/system-id.js index 95cc720..911af73 100644 --- a/lib/rules/enforce-system-id.js +++ b/lib/rules/system-id.js @@ -3,6 +3,11 @@ const getDocsUrl = require('../utils/getDocsUrl') const getSettings = require('../utils/getSettings') const getSelectorPrefix = require('../utils/getSelectorPrefix') +const { + isAssignActionCreatorCall, + isWithinNode, + isFunctionExpression, +} = require('../utils/predicates') module.exports = { meta: { @@ -16,7 +21,8 @@ module.exports = { fixable: 'code', schema: [], messages: { - missingSystemId: 'Missing "systemId" property in "invoke" block.', + missingSystemId: 'Missing "systemId" property for an invoked actor.', + missingSystemIdSpawn: 'Missing "systemId" property for a spawned actor.', invalidSystemId: 'Property "systemId" should be a non-empty string.', systemIdNotAllowedBeforeVersion5: 'Property "systemId" is not supported in xstate < 5.', @@ -29,6 +35,69 @@ module.exports = { const prefix = getSelectorPrefix(context.sourceCode) const systemIds = new Set() + function checkSpawnExpression(node) { + // check if this spawn call is relevant - must be within a function expression inside the assign action creator + if ( + !isWithinNode( + node, + (ancestor) => + isFunctionExpression(ancestor) && + isWithinNode( + ancestor, + (x) => isAssignActionCreatorCall(x), + (ancestor) => isFunctionExpression(ancestor) + ) + ) + ) { + return + } + + if (node.arguments.length < 2) { + context.report({ + node, + messageId: 'missingSystemIdSpawn', + }) + return + } + const arg2 = node.arguments[1] + if ( + arg2.type !== 'ObjectExpression' || + !arg2.properties.some((prop) => prop.key.name === 'systemId') + ) { + context.report({ + node: arg2, + messageId: 'missingSystemIdSpawn', + }) + return + } + const systemIdProp = arg2.properties.find( + (prop) => prop.key.name === 'systemId' + ) + + if ( + systemIdProp.value.type !== 'Literal' || + typeof systemIdProp.value.value !== 'string' || + systemIdProp.value.value.trim() === '' + ) { + context.report({ + node: systemIdProp, + messageId: 'invalidSystemId', + }) + } + } + + function checkUniqueSystemId(node) { + if (systemIds.has(node.value.value)) { + context.report({ + node, + messageId: 'duplicateSystemId', + data: { systemId: node.value.value }, + }) + } else { + systemIds.add(node.value.value) + } + } + return { [`${prefix}Property[key.name='invoke'] > ObjectExpression`]: (node) => { const systemIdProp = node.properties.find( @@ -61,40 +130,24 @@ module.exports = { context.report({ node: systemIdProp, messageId: 'invalidSystemId', - fix: (fixer) => - fixer.replaceText(systemIdProp.value, "'myActor'"), }) } } else if (version >= 5) { - const { loc } = node.properties[0] - const offset = loc.start.column context.report({ node, messageId: 'missingSystemId', - fix: (fixer) => { - return fixer.insertTextBefore( - node.properties[0], - `systemId: 'myActor',\n${''.padStart(offset, ' ')}` - ) - }, }) } }, [`${prefix}Property[key.name='invoke'] > ObjectExpression > Property[key.name="systemId"]`]: - (node) => { - if (systemIds.has(node.value.value)) { - context.report({ - node, - messageId: 'duplicateSystemId', - data: { systemId: node.value.value }, - }) - } else { - systemIds.add(node.value.value) - } - }, + checkUniqueSystemId, + [`${prefix}CallExpression[callee.name="assign"] CallExpression[callee.name="spawn"] > ObjectExpression > Property[key.name="systemId"]`]: + checkUniqueSystemId, - // TODO check use of systemId in spawns + [`${prefix} CallExpression[callee.name="spawn"]`]: checkSpawnExpression, + [`${prefix} CallExpression[callee.property.name="spawn"]`]: + checkSpawnExpression, } }, } diff --git a/lib/utils/predicates.js b/lib/utils/predicates.js index 95d052d..2ded136 100644 --- a/lib/utils/predicates.js +++ b/lib/utils/predicates.js @@ -143,6 +143,22 @@ function isWithinInvoke(property) { ) } +function isWithinNode(node, predicate, stop = () => false) { + let current = node.parent + while (true) { + if (!current) { + return false + } + if (predicate(current)) { + return true + } + if (stop(current)) { + return false + } + current = current.parent + } +} + // list of property names which have special meaning to XState in some contexts (they are // part of the XState's API) const reservedWords = [ @@ -171,6 +187,10 @@ function isReservedXStateWord(string) { return reservedWords.includes(string) } +function isAssignActionCreatorCall(node) { + return node.type === 'CallExpression' && node.callee.name === 'assign' +} + module.exports = { isFirstArrayItem, propertyHasName, @@ -188,4 +208,6 @@ module.exports = { isKnownActionCreatorCall, isWithinInvoke, isReservedXStateWord, + isAssignActionCreatorCall, + isWithinNode, }