Skip to content

Commit

Permalink
feat(system-id): add detection of missing systemId within spawn calls
Browse files Browse the repository at this point in the history
  • Loading branch information
rlaffers committed Oct 5, 2023
1 parent fbc914c commit 92d077d
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 25 deletions.
4 changes: 2 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
99 changes: 76 additions & 23 deletions lib/rules/enforce-system-id.js → lib/rules/system-id.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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.',
Expand All @@ -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(
Expand Down Expand Up @@ -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,
}
},
}
22 changes: 22 additions & 0 deletions lib/utils/predicates.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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,
Expand All @@ -188,4 +208,6 @@ module.exports = {
isKnownActionCreatorCall,
isWithinInvoke,
isReservedXStateWord,
isAssignActionCreatorCall,
isWithinNode,
}

0 comments on commit 92d077d

Please sign in to comment.