From 03a58f6c9f7fa0211b3f8eb2ae68cebfcf811fa3 Mon Sep 17 00:00:00 2001 From: Richard Laffers Date: Sun, 25 Apr 2021 15:54:02 +0200 Subject: [PATCH] feat(no-auto-forward): add rule no-auto-forward --- README.md | 4 +- docs/rules/no-auto-forward.md | 60 ++++++++++++++++++++++++++ lib/index.js | 2 + lib/rules/no-auto-forward.js | 46 ++++++++++++++++++++ tests/lib/rules/no-auto-forward.js | 67 ++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 docs/rules/no-auto-forward.md create mode 100644 lib/rules/no-auto-forward.js create mode 100644 tests/lib/rules/no-auto-forward.js diff --git a/README.md b/README.md index 7d1d43e..6088576 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ Then configure the rules you want to use under the rules section. "xstate/prefer-always": "error", "xstate/event-names": ["warn", "macroCase"], "xstate/state-names": ["warn", "camelCase"], - "xstate/no-inline-implementation": "warn" + "xstate/no-inline-implementation": "warn", + "xstate/no-auto-forward": "warn" } } ``` @@ -88,6 +89,7 @@ There is also an `all` configuration which includes every available rule. It enf | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | ------------------ | | [no-inline-implementation](docs/rules/no-inline-implementation.md) | Suggest refactoring guards, actions and services into machine options | | | [prefer-always](docs/rules/prefer-always.md) | Suggest using the `always` syntax for transient (eventless) transitions | :heavy_check_mark: | +| [no-auto-forward](docs/rules/no-auto-forward.md) | Forbid auto-forwarding events to invoked services or spawned actors | | ### Stylistic Issues diff --git a/docs/rules/no-auto-forward.md b/docs/rules/no-auto-forward.md new file mode 100644 index 0000000..b1161c0 --- /dev/null +++ b/docs/rules/no-auto-forward.md @@ -0,0 +1,60 @@ +# Forbid auto-forwarding events + +Prefer sending events explicitly to child actors/services. + +## Rule Details + +Avoid blindly forwarding all events to invoked services or spawned actors - it may lead to unexpected behavior or infinite loops. The official documentation [suggests sending events explicitly](https://xstate.js.org/docs/guides/communication.html#the-invoke-property) with the [`forwardTo`](https://xstate.js.org/docs/guides/actions.html#forward-to-action) or `send` action creators. + +Examples of **incorrect** code for this rule: + +```javascript +// ❌ auto-forwarding events to an invoked service +createMachine({ + states: { + playing: { + invoke: { + src: 'game', + autoForward: true, + }, + }, + }, +}) + +// ❌ auto-forwarding events to a spawned actor +createMachine({ + states: { + initializing: { + entry: assign({ + gameRef: () => spawn(game, { autoForward: true }), + }), + }, + }, +}) +``` + +Examples of **correct** code for this rule: + +```javascript +// ✅ no auto-forward +createMachine{{ + states: { + playing: { + invoke: { + src: 'game', + }, + }, + }, +}} + +// ✅ autoForward set to false +createMachine({ + states: { + initializing: { + entry: assign({ + gameRef: () => spawn(game, { autoForward: false }), + }), + }, + }, +}) +``` diff --git a/lib/index.js b/lib/index.js index 7dd4fa2..62e9f08 100644 --- a/lib/index.js +++ b/lib/index.js @@ -22,6 +22,7 @@ module.exports = { 'invoke-usage': require('./rules/invoke-usage'), 'entry-exit-action': require('./rules/entry-exit-action'), 'prefer-always': require('./rules/prefer-always'), + 'no-auto-forward': require('./rules/no-auto-forward'), }, configs: { recommended: { @@ -50,6 +51,7 @@ module.exports = { 'xstate/event-names': ['warn', 'macroCase'], 'xstate/state-names': ['warn', 'camelCase'], 'xstate/no-inline-implementation': 'warn', + 'xstate/no-auto-forward': 'warn', 'xstate/prefer-always': 'error', }, }, diff --git a/lib/rules/no-auto-forward.js b/lib/rules/no-auto-forward.js new file mode 100644 index 0000000..9eb0eb7 --- /dev/null +++ b/lib/rules/no-auto-forward.js @@ -0,0 +1,46 @@ +'use strict' + +const getDocsUrl = require('../utils/getDocsUrl') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Forbid auto-forwarding events to child actors', + category: 'Best Practices', + url: getDocsUrl('no-auto-forward'), + recommended: 'warn', + }, + schema: [], + messages: { + noAutoForward: + 'Forwarding all events may lead to unexpected behavior and/or infinite loops. Prefer using `forwardTo` action creator to send events explicitly.', + }, + }, + + create: function (context) { + return { + 'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="invoke"] > ObjectExpression > Property[key.name="autoForward"]': function ( + node + ) { + if (node.value.value === true) { + context.report({ + node, + messageId: 'noAutoForward', + }) + } + }, + + 'CallExpression[callee.name=/^createMachine$|^Machine$/] CallExpression[callee.name="spawn"] > ObjectExpression > Property[key.name="autoForward"]': function ( + node + ) { + if (node.value.value === true) { + context.report({ + node, + messageId: 'noAutoForward', + }) + } + }, + } + }, +} diff --git a/tests/lib/rules/no-auto-forward.js b/tests/lib/rules/no-auto-forward.js new file mode 100644 index 0000000..9eacebb --- /dev/null +++ b/tests/lib/rules/no-auto-forward.js @@ -0,0 +1,67 @@ +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-auto-forward') + +const tests = { + valid: [ + ` + createMachine({ + states: { + playing: { + invoke: { + src: 'game', + }, + }, + }, + }) + `, + ` + createMachine({ + states: { + initializing: { + entry: assign({ + gameRef: () => spawn(game, { autoForward: false }), + }), + }, + }, + }) + `, + ], + invalid: [ + { + code: ` + createMachine({ + states: { + playing: { + invoke: { + src: 'game', + autoForward: true, + }, + }, + }, + }) + `, + errors: [{ messageId: 'noAutoForward' }], + }, + { + code: ` + createMachine({ + states: { + initializing: { + entry: assign({ + gameRef: () => spawn(game, { autoForward: true }), + }), + }, + }, + }) + `, + errors: [{ messageId: 'noAutoForward' }], + }, + ], +} + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + }, +}) +ruleTester.run('no-auto-forward', rule, tests)