diff --git a/README.md b/README.md index 3247234..d16a5c0 100644 --- a/README.md +++ b/README.md @@ -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: | diff --git a/docs/rules/spawn-usage.md b/docs/rules/spawn-usage.md index 2103110..f4f5cfc 100644 --- a/docs/rules/spawn-usage.md +++ b/docs/rules/spawn-usage.md @@ -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 diff --git a/lib/index.js b/lib/index.js index 7a86f8a..409beb8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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', @@ -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', diff --git a/lib/rules/spawn-usage.js b/lib/rules/spawn-usage.js index 697b6fd..ab6c78c 100644 --- a/lib/rules/spawn-usage.js +++ b/lib/rules/spawn-usage.js @@ -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' @@ -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 @@ -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, diff --git a/tests/lib/rules/spawn-usage.js b/tests/lib/rules/spawn-usage.js index a846696..58f7b7c 100644 --- a/tests/lib/rules/spawn-usage.js +++ b/tests/lib/rules/spawn-usage.js @@ -1,45 +1,66 @@ 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) @@ -47,8 +68,11 @@ const tests = { ref, } }) - `, ` + ), + withVersion( + 4, + ` import { spawn } from 'xstate' assign(() => { const start = () => spawn(x) @@ -56,29 +80,41 @@ const tests = { 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 @@ -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 @@ -101,24 +140,25 @@ 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({ @@ -126,8 +166,8 @@ const tests = { }) `, errors: [{ messageId: 'invalidCallContext' }], - }, - { + }), + withVersion(4, { code: ` import { spawn } from 'xstate' assign((() => ({ @@ -135,16 +175,16 @@ const tests = { }))()) `, 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 @@ -162,8 +202,8 @@ const tests = { { messageId: 'invalidCallContext' }, { messageId: 'invalidCallContext' }, ], - }, - { + }), + withVersion(4, { code: ` import * as xs from 'xstate' const { spawn } = xs @@ -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' }], - // }, + }), ], }