Skip to content

Commit 92d077d

Browse files
committed
feat(system-id): add detection of missing systemId within spawn calls
1 parent fbc914c commit 92d077d

File tree

3 files changed

+100
-25
lines changed

3 files changed

+100
-25
lines changed

lib/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ module.exports = {
2828
'no-invalid-state-props': require('./rules/no-invalid-state-props'),
2929
'no-async-guard': require('./rules/no-async-guard'),
3030
'no-invalid-conditional-action': require('./rules/no-invalid-conditional-action'),
31-
'enforce-system-id': require('./rules/enforce-system-id'),
31+
'system-id': require('./rules/system-id'),
3232
},
3333
configs: {
3434
// Requires: xstate@5
@@ -80,7 +80,7 @@ module.exports = {
8080
'xstate/no-invalid-state-props': 'error',
8181
'xstate/no-invalid-conditional-action': 'error',
8282
'xstate/no-async-guard': 'error',
83-
'xstate/enforce-system-id': 'error',
83+
'xstate/system-id': 'error',
8484
},
8585
},
8686
// Requires: xstate@4

lib/rules/enforce-system-id.js renamed to lib/rules/system-id.js

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
const getDocsUrl = require('../utils/getDocsUrl')
44
const getSettings = require('../utils/getSettings')
55
const getSelectorPrefix = require('../utils/getSelectorPrefix')
6+
const {
7+
isAssignActionCreatorCall,
8+
isWithinNode,
9+
isFunctionExpression,
10+
} = require('../utils/predicates')
611

712
module.exports = {
813
meta: {
@@ -16,7 +21,8 @@ module.exports = {
1621
fixable: 'code',
1722
schema: [],
1823
messages: {
19-
missingSystemId: 'Missing "systemId" property in "invoke" block.',
24+
missingSystemId: 'Missing "systemId" property for an invoked actor.',
25+
missingSystemIdSpawn: 'Missing "systemId" property for a spawned actor.',
2026
invalidSystemId: 'Property "systemId" should be a non-empty string.',
2127
systemIdNotAllowedBeforeVersion5:
2228
'Property "systemId" is not supported in xstate < 5.',
@@ -29,6 +35,69 @@ module.exports = {
2935
const prefix = getSelectorPrefix(context.sourceCode)
3036
const systemIds = new Set()
3137

38+
function checkSpawnExpression(node) {
39+
// check if this spawn call is relevant - must be within a function expression inside the assign action creator
40+
if (
41+
!isWithinNode(
42+
node,
43+
(ancestor) =>
44+
isFunctionExpression(ancestor) &&
45+
isWithinNode(
46+
ancestor,
47+
(x) => isAssignActionCreatorCall(x),
48+
(ancestor) => isFunctionExpression(ancestor)
49+
)
50+
)
51+
) {
52+
return
53+
}
54+
55+
if (node.arguments.length < 2) {
56+
context.report({
57+
node,
58+
messageId: 'missingSystemIdSpawn',
59+
})
60+
return
61+
}
62+
const arg2 = node.arguments[1]
63+
if (
64+
arg2.type !== 'ObjectExpression' ||
65+
!arg2.properties.some((prop) => prop.key.name === 'systemId')
66+
) {
67+
context.report({
68+
node: arg2,
69+
messageId: 'missingSystemIdSpawn',
70+
})
71+
return
72+
}
73+
const systemIdProp = arg2.properties.find(
74+
(prop) => prop.key.name === 'systemId'
75+
)
76+
77+
if (
78+
systemIdProp.value.type !== 'Literal' ||
79+
typeof systemIdProp.value.value !== 'string' ||
80+
systemIdProp.value.value.trim() === ''
81+
) {
82+
context.report({
83+
node: systemIdProp,
84+
messageId: 'invalidSystemId',
85+
})
86+
}
87+
}
88+
89+
function checkUniqueSystemId(node) {
90+
if (systemIds.has(node.value.value)) {
91+
context.report({
92+
node,
93+
messageId: 'duplicateSystemId',
94+
data: { systemId: node.value.value },
95+
})
96+
} else {
97+
systemIds.add(node.value.value)
98+
}
99+
}
100+
32101
return {
33102
[`${prefix}Property[key.name='invoke'] > ObjectExpression`]: (node) => {
34103
const systemIdProp = node.properties.find(
@@ -61,40 +130,24 @@ module.exports = {
61130
context.report({
62131
node: systemIdProp,
63132
messageId: 'invalidSystemId',
64-
fix: (fixer) =>
65-
fixer.replaceText(systemIdProp.value, "'myActor'"),
66133
})
67134
}
68135
} else if (version >= 5) {
69-
const { loc } = node.properties[0]
70-
const offset = loc.start.column
71136
context.report({
72137
node,
73138
messageId: 'missingSystemId',
74-
fix: (fixer) => {
75-
return fixer.insertTextBefore(
76-
node.properties[0],
77-
`systemId: 'myActor',\n${''.padStart(offset, ' ')}`
78-
)
79-
},
80139
})
81140
}
82141
},
83142

84143
[`${prefix}Property[key.name='invoke'] > ObjectExpression > Property[key.name="systemId"]`]:
85-
(node) => {
86-
if (systemIds.has(node.value.value)) {
87-
context.report({
88-
node,
89-
messageId: 'duplicateSystemId',
90-
data: { systemId: node.value.value },
91-
})
92-
} else {
93-
systemIds.add(node.value.value)
94-
}
95-
},
144+
checkUniqueSystemId,
145+
[`${prefix}CallExpression[callee.name="assign"] CallExpression[callee.name="spawn"] > ObjectExpression > Property[key.name="systemId"]`]:
146+
checkUniqueSystemId,
96147

97-
// TODO check use of systemId in spawns
148+
[`${prefix} CallExpression[callee.name="spawn"]`]: checkSpawnExpression,
149+
[`${prefix} CallExpression[callee.property.name="spawn"]`]:
150+
checkSpawnExpression,
98151
}
99152
},
100153
}

lib/utils/predicates.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ function isWithinInvoke(property) {
143143
)
144144
}
145145

146+
function isWithinNode(node, predicate, stop = () => false) {
147+
let current = node.parent
148+
while (true) {
149+
if (!current) {
150+
return false
151+
}
152+
if (predicate(current)) {
153+
return true
154+
}
155+
if (stop(current)) {
156+
return false
157+
}
158+
current = current.parent
159+
}
160+
}
161+
146162
// list of property names which have special meaning to XState in some contexts (they are
147163
// part of the XState's API)
148164
const reservedWords = [
@@ -171,6 +187,10 @@ function isReservedXStateWord(string) {
171187
return reservedWords.includes(string)
172188
}
173189

190+
function isAssignActionCreatorCall(node) {
191+
return node.type === 'CallExpression' && node.callee.name === 'assign'
192+
}
193+
174194
module.exports = {
175195
isFirstArrayItem,
176196
propertyHasName,
@@ -188,4 +208,6 @@ module.exports = {
188208
isKnownActionCreatorCall,
189209
isWithinInvoke,
190210
isReservedXStateWord,
211+
isAssignActionCreatorCall,
212+
isWithinNode,
191213
}

0 commit comments

Comments
 (0)