Skip to content

Commit

Permalink
feat(spawn-usage): throw error if spawn-usage is used with xstate > 4
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rlaffers committed Aug 17, 2023
1 parent d461b42 commit 02976f2
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 61 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ The default shareable configurations are for XState v5. If you use the older XSt

| Rule | Description | Recommended |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------ |
| [spawn-usage](docs/rules/spawn-usage.md) | Enforce correct usage of `spawn` | :heavy_check_mark: |
| [spawn-usage](docs/rules/spawn-usage.md) | Enforce correct usage of `spawn`. **Only for XState v4!** | :heavy_check_mark: |
| [no-infinite-loop](docs/rules/no-infinite-loop.md) | Detect infinite loops with eventless transitions | :heavy_check_mark: |
| [no-imperative-action](docs/rules/no-imperative-action.md) | Forbid using action creators imperatively | :heavy_check_mark: |
| [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: |
Expand Down
6 changes: 6 additions & 0 deletions docs/rules/spawn-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

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

** This rule is compatible with XState v4 only! **

## Rule Details

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.

### XState v5

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.

Examples of **incorrect** code for this rule:

```javascript
Expand Down
2 changes: 0 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ module.exports = {
},
plugins: ['xstate'],
rules: {
'xstate/spawn-usage': 'error',
'xstate/no-infinite-loop': 'error',
'xstate/no-imperative-action': 'error',
'xstate/no-ondone-outside-compound-state': 'error',
Expand All @@ -62,7 +61,6 @@ module.exports = {
},
plugins: ['xstate'],
rules: {
'xstate/spawn-usage': 'error',
'xstate/no-infinite-loop': 'error',
'xstate/no-imperative-action': 'error',
'xstate/no-ondone-outside-compound-state': 'error',
Expand Down
23 changes: 21 additions & 2 deletions lib/rules/spawn-usage.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
'use strict'
/**
* This rule is relevant only for XState v4.
*
*/

const getDocsUrl = require('../utils/getDocsUrl')
const { isFunctionExpression, isIIFE } = require('../utils/predicates')
const XStateDetector = require('../utils/XStateDetector')
const getSettings = require('../utils/getSettings')

// TODO instead of the detector, consider using:
// context.getDeclaredVariables(node)
// context.sourceCode.getScope(node).variables

function isAssignCall(node) {
return node.type === 'CallExpression' && node.callee.name === 'assign'
Expand Down Expand Up @@ -30,8 +39,6 @@ function isInsideAssignerFunction(node) {
if (isAssignCall(parent)) {
return false
}
// TODO it's possible that a function expression inside assigner function
// does not get called, so nothing is ever spawned
parent = parent.parent
}
return false
Expand All @@ -55,6 +62,18 @@ module.exports = {

create: function (context) {
const xstateDetector = new XStateDetector()
const { version } = getSettings(context)
if (version !== 4) {
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:
{
"settings": {
"xstate": {
"version": 4
}
}
}
`)
}

return {
...xstateDetector.visitors,
Expand Down
133 changes: 77 additions & 56 deletions tests/lib/rules/spawn-usage.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,120 @@
const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/spawn-usage')
const { withVersion } = require('../utils/settings')

const tests = {
valid: [
// not imported from xstate - ignore the rule
`
withVersion(
4,
`
spawn(x)
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign({
ref: () => spawn(x)
})
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign({
ref: () => spawn(x)
})
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign({
ref: () => spawn(x)
})
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign(() => ({
ref: spawn(x, 'id')
}))
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign(() => {
return {
ref: spawn(x)
}
})
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign(() => {
const ref = spawn(x)
return {
ref,
}
})
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign(() => {
const start = () => spawn(x)
return {
ref: start()
}
})
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign({
ref: function() { return spawn(x) }
})
`,
`
),
withVersion(
4,
`
import { spawn } from 'xstate'
assign(function() {
return {
ref: spawn(x, 'id')
}
})
`,
// other import types
`
),
// other import types
withVersion(
4,
`
import { spawn as foo } from 'xstate'
assign({
ref: () => foo(x)
})
`,
`
),
withVersion(
4,
`
import xs from 'xstate'
const { spawn } = xs
const foo = xs.spawn
Expand All @@ -89,8 +125,11 @@ const tests = {
ref3: () => xs.spawn(x),
ref4: () => xs['spawn'](x),
})
`,
`
),
withVersion(
4,
`
import * as xs from 'xstate'
const { spawn } = xs
const foo = xs.spawn
Expand All @@ -101,50 +140,51 @@ const tests = {
ref3: () => xs.spawn(x),
ref4: () => xs['spawn'](x),
})
`,
`
),
],
invalid: [
{
withVersion(4, {
code: `
import { spawn } from 'xstate'
spawn(x)
`,
errors: [{ messageId: 'invalidCallContext' }],
},
{
}),
withVersion(4, {
code: `
import { spawn } from 'xstate'
assign(spawn(x))
`,
errors: [{ messageId: 'invalidCallContext' }],
},
{
}),
withVersion(4, {
code: `
import { spawn } from 'xstate'
assign({
ref: spawn(x)
})
`,
errors: [{ messageId: 'invalidCallContext' }],
},
{
}),
withVersion(4, {
code: `
import { spawn } from 'xstate'
assign((() => ({
ref: spawn(x)
}))())
`,
errors: [{ messageId: 'invalidCallContext' }],
},
}),
// test other import types with a single invalid call
{
withVersion(4, {
code: `
import { spawn as foo } from 'xstate'
foo(x)
`,
errors: [{ messageId: 'invalidCallContext' }],
},
{
}),
withVersion(4, {
code: `
import xs from 'xstate'
const { spawn } = xs
Expand All @@ -162,8 +202,8 @@ const tests = {
{ messageId: 'invalidCallContext' },
{ messageId: 'invalidCallContext' },
],
},
{
}),
withVersion(4, {
code: `
import * as xs from 'xstate'
const { spawn } = xs
Expand All @@ -181,54 +221,35 @@ const tests = {
{ messageId: 'invalidCallContext' },
{ messageId: 'invalidCallContext' },
],
},
{
}),
withVersion(4, {
code: `
const { spawn } = require('xstate')
spawn(x)
`,
errors: [{ messageId: 'invalidCallContext' }],
},
{
}),
withVersion(4, {
code: `
const spawn = require('xstate').spawn
spawn(x)
`,
errors: [{ messageId: 'invalidCallContext' }],
},
{
}),
withVersion(4, {
code: `
const spawn = require('xstate')['spawn']
spawn(x)
`,
errors: [{ messageId: 'invalidCallContext' }],
},
{
}),
withVersion(4, {
code: `
const xs = require('xstate')
xs.spawn(x)
`,
errors: [{ messageId: 'invalidCallContext' }],
},
// {
// code: `
// import xs from 'xstate'
// xs.spawn(x)
// `,
// errors: [{ messageId: 'invalidCallContext' }],
// },
// TODO extend the rule to catch this use case
// {
// code: `
// assign(() => {
// const start = () => spawn(x)
// return {
// ref: start
// }
// })
// `,
// errors: [{ messageId: 'spawnNeverCalled' }],
// },
}),
],
}

Expand Down

0 comments on commit 02976f2

Please sign in to comment.