Skip to content

Commit 02976f2

Browse files
committed
feat(spawn-usage): throw error if spawn-usage is used with xstate > 4
Using the rule no longer makes sense in XState v5 since the new API makes it impossible to call it outside of an assign function.
1 parent d461b42 commit 02976f2

File tree

5 files changed

+105
-61
lines changed

5 files changed

+105
-61
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ The default shareable configurations are for XState v5. If you use the older XSt
9696

9797
| Rule | Description | Recommended |
9898
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------ |
99-
| [spawn-usage](docs/rules/spawn-usage.md) | Enforce correct usage of `spawn` | :heavy_check_mark: |
99+
| [spawn-usage](docs/rules/spawn-usage.md) | Enforce correct usage of `spawn`. **Only for XState v4!** | :heavy_check_mark: |
100100
| [no-infinite-loop](docs/rules/no-infinite-loop.md) | Detect infinite loops with eventless transitions | :heavy_check_mark: |
101101
| [no-imperative-action](docs/rules/no-imperative-action.md) | Forbid using action creators imperatively | :heavy_check_mark: |
102102
| [no-ondone-outside-compound-state](docs/rules/no-ondone-outside-compound-state.md) | Forbid onDone transitions on `atomic`, `history` and `final` nodes | :heavy_check_mark: |

docs/rules/spawn-usage.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
Ensure that the `spawn` function imported from xstate is used correctly.
44

5+
** This rule is compatible with XState v4 only! **
6+
57
## Rule Details
68

79
The `spawn` function has to be used in the context of an assignment function. Failing to do so creates an orphaned actor which has no effect.
810

11+
### XState v5
12+
13+
XState v5 changed the way the `spawn` function is accessed. This effectively eliminated the possibility of using the `spawn` function outside of the `assign` function. Therefore, this rule becomes obsolete in XState v5. Do not use it with XState v5.
14+
915
Examples of **incorrect** code for this rule:
1016

1117
```javascript

lib/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ module.exports = {
3838
},
3939
plugins: ['xstate'],
4040
rules: {
41-
'xstate/spawn-usage': 'error',
4241
'xstate/no-infinite-loop': 'error',
4342
'xstate/no-imperative-action': 'error',
4443
'xstate/no-ondone-outside-compound-state': 'error',
@@ -62,7 +61,6 @@ module.exports = {
6261
},
6362
plugins: ['xstate'],
6463
rules: {
65-
'xstate/spawn-usage': 'error',
6664
'xstate/no-infinite-loop': 'error',
6765
'xstate/no-imperative-action': 'error',
6866
'xstate/no-ondone-outside-compound-state': 'error',

lib/rules/spawn-usage.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
'use strict'
2+
/**
3+
* This rule is relevant only for XState v4.
4+
*
5+
*/
26

37
const getDocsUrl = require('../utils/getDocsUrl')
48
const { isFunctionExpression, isIIFE } = require('../utils/predicates')
59
const XStateDetector = require('../utils/XStateDetector')
10+
const getSettings = require('../utils/getSettings')
11+
12+
// TODO instead of the detector, consider using:
13+
// context.getDeclaredVariables(node)
14+
// context.sourceCode.getScope(node).variables
615

716
function isAssignCall(node) {
817
return node.type === 'CallExpression' && node.callee.name === 'assign'
@@ -30,8 +39,6 @@ function isInsideAssignerFunction(node) {
3039
if (isAssignCall(parent)) {
3140
return false
3241
}
33-
// TODO it's possible that a function expression inside assigner function
34-
// does not get called, so nothing is ever spawned
3542
parent = parent.parent
3643
}
3744
return false
@@ -55,6 +62,18 @@ module.exports = {
5562

5663
create: function (context) {
5764
const xstateDetector = new XStateDetector()
65+
const { version } = getSettings(context)
66+
if (version !== 4) {
67+
throw new Error(`Rule "spawn-usage" should be used with XState v4 only! Your XState version: ${version}. Either remove this rule from your ESLint config or set the correct version of XState in the config:
68+
{
69+
"settings": {
70+
"xstate": {
71+
"version": 4
72+
}
73+
}
74+
}
75+
`)
76+
}
5877

5978
return {
6079
...xstateDetector.visitors,

tests/lib/rules/spawn-usage.js

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,120 @@
11
const RuleTester = require('eslint').RuleTester
22
const rule = require('../../../lib/rules/spawn-usage')
3+
const { withVersion } = require('../utils/settings')
34

45
const tests = {
56
valid: [
67
// not imported from xstate - ignore the rule
7-
`
8+
withVersion(
9+
4,
10+
`
811
spawn(x)
9-
`,
1012
`
13+
),
14+
withVersion(
15+
4,
16+
`
1117
import { spawn } from 'xstate'
1218
assign({
1319
ref: () => spawn(x)
1420
})
15-
`,
1621
`
22+
),
23+
withVersion(
24+
4,
25+
`
1726
import { spawn } from 'xstate'
1827
assign({
1928
ref: () => spawn(x)
2029
})
21-
`,
2230
`
31+
),
32+
withVersion(
33+
4,
34+
`
2335
import { spawn } from 'xstate'
2436
assign({
2537
ref: () => spawn(x)
2638
})
27-
`,
2839
`
40+
),
41+
withVersion(
42+
4,
43+
`
2944
import { spawn } from 'xstate'
3045
assign(() => ({
3146
ref: spawn(x, 'id')
3247
}))
33-
`,
3448
`
49+
),
50+
withVersion(
51+
4,
52+
`
3553
import { spawn } from 'xstate'
3654
assign(() => {
3755
return {
3856
ref: spawn(x)
3957
}
4058
})
41-
`,
4259
`
60+
),
61+
withVersion(
62+
4,
63+
`
4364
import { spawn } from 'xstate'
4465
assign(() => {
4566
const ref = spawn(x)
4667
return {
4768
ref,
4869
}
4970
})
50-
`,
5171
`
72+
),
73+
withVersion(
74+
4,
75+
`
5276
import { spawn } from 'xstate'
5377
assign(() => {
5478
const start = () => spawn(x)
5579
return {
5680
ref: start()
5781
}
5882
})
59-
`,
6083
`
84+
),
85+
withVersion(
86+
4,
87+
`
6188
import { spawn } from 'xstate'
6289
assign({
6390
ref: function() { return spawn(x) }
6491
})
65-
`,
6692
`
93+
),
94+
withVersion(
95+
4,
96+
`
6797
import { spawn } from 'xstate'
6898
assign(function() {
6999
return {
70100
ref: spawn(x, 'id')
71101
}
72102
})
73-
`,
74-
// other import types
75103
`
104+
),
105+
// other import types
106+
withVersion(
107+
4,
108+
`
76109
import { spawn as foo } from 'xstate'
77110
assign({
78111
ref: () => foo(x)
79112
})
80-
`,
81113
`
114+
),
115+
withVersion(
116+
4,
117+
`
82118
import xs from 'xstate'
83119
const { spawn } = xs
84120
const foo = xs.spawn
@@ -89,8 +125,11 @@ const tests = {
89125
ref3: () => xs.spawn(x),
90126
ref4: () => xs['spawn'](x),
91127
})
92-
`,
93128
`
129+
),
130+
withVersion(
131+
4,
132+
`
94133
import * as xs from 'xstate'
95134
const { spawn } = xs
96135
const foo = xs.spawn
@@ -101,50 +140,51 @@ const tests = {
101140
ref3: () => xs.spawn(x),
102141
ref4: () => xs['spawn'](x),
103142
})
104-
`,
143+
`
144+
),
105145
],
106146
invalid: [
107-
{
147+
withVersion(4, {
108148
code: `
109149
import { spawn } from 'xstate'
110150
spawn(x)
111151
`,
112152
errors: [{ messageId: 'invalidCallContext' }],
113-
},
114-
{
153+
}),
154+
withVersion(4, {
115155
code: `
116156
import { spawn } from 'xstate'
117157
assign(spawn(x))
118158
`,
119159
errors: [{ messageId: 'invalidCallContext' }],
120-
},
121-
{
160+
}),
161+
withVersion(4, {
122162
code: `
123163
import { spawn } from 'xstate'
124164
assign({
125165
ref: spawn(x)
126166
})
127167
`,
128168
errors: [{ messageId: 'invalidCallContext' }],
129-
},
130-
{
169+
}),
170+
withVersion(4, {
131171
code: `
132172
import { spawn } from 'xstate'
133173
assign((() => ({
134174
ref: spawn(x)
135175
}))())
136176
`,
137177
errors: [{ messageId: 'invalidCallContext' }],
138-
},
178+
}),
139179
// test other import types with a single invalid call
140-
{
180+
withVersion(4, {
141181
code: `
142182
import { spawn as foo } from 'xstate'
143183
foo(x)
144184
`,
145185
errors: [{ messageId: 'invalidCallContext' }],
146-
},
147-
{
186+
}),
187+
withVersion(4, {
148188
code: `
149189
import xs from 'xstate'
150190
const { spawn } = xs
@@ -162,8 +202,8 @@ const tests = {
162202
{ messageId: 'invalidCallContext' },
163203
{ messageId: 'invalidCallContext' },
164204
],
165-
},
166-
{
205+
}),
206+
withVersion(4, {
167207
code: `
168208
import * as xs from 'xstate'
169209
const { spawn } = xs
@@ -181,54 +221,35 @@ const tests = {
181221
{ messageId: 'invalidCallContext' },
182222
{ messageId: 'invalidCallContext' },
183223
],
184-
},
185-
{
224+
}),
225+
withVersion(4, {
186226
code: `
187227
const { spawn } = require('xstate')
188228
spawn(x)
189229
`,
190230
errors: [{ messageId: 'invalidCallContext' }],
191-
},
192-
{
231+
}),
232+
withVersion(4, {
193233
code: `
194234
const spawn = require('xstate').spawn
195235
spawn(x)
196236
`,
197237
errors: [{ messageId: 'invalidCallContext' }],
198-
},
199-
{
238+
}),
239+
withVersion(4, {
200240
code: `
201241
const spawn = require('xstate')['spawn']
202242
spawn(x)
203243
`,
204244
errors: [{ messageId: 'invalidCallContext' }],
205-
},
206-
{
245+
}),
246+
withVersion(4, {
207247
code: `
208248
const xs = require('xstate')
209249
xs.spawn(x)
210250
`,
211251
errors: [{ messageId: 'invalidCallContext' }],
212-
},
213-
// {
214-
// code: `
215-
// import xs from 'xstate'
216-
// xs.spawn(x)
217-
// `,
218-
// errors: [{ messageId: 'invalidCallContext' }],
219-
// },
220-
// TODO extend the rule to catch this use case
221-
// {
222-
// code: `
223-
// assign(() => {
224-
// const start = () => spawn(x)
225-
// return {
226-
// ref: start
227-
// }
228-
// })
229-
// `,
230-
// errors: [{ messageId: 'spawnNeverCalled' }],
231-
// },
252+
}),
232253
],
233254
}
234255

0 commit comments

Comments
 (0)