Skip to content

Commit

Permalink
feat: add a comment directive tu enforce this plugin
Browse files Browse the repository at this point in the history
By default, this plugin does not lint code outside of createMachine call epxressions. This commit
add a comment direective eslint-plugin-xstate-include which enforces these rules over any file.

fix #20
  • Loading branch information
rlaffers committed Aug 26, 2023
1 parent 3136344 commit 48cd089
Show file tree
Hide file tree
Showing 31 changed files with 1,454 additions and 316 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,17 @@ If you do not use shareable configs, you need to manually specify the XState ver
| ---------------------------------------- | ------------------------------------------------------------------------ | ----------- |
| [event-names](docs/rules/event-names.md) | Suggest consistent formatting of event names | |
| [state-names](docs/rules/state-names.md) | Suggest consistent formatting of state names and prevent confusing names | |

## Comment Directives

By default, the plugin lints only code within the `createMachine` or `Machine` calls. However, if your machine configuration is imported from another file, you will need to enable this plugin's rules by adding a comment directive to the top of the file:

```js
/* eslint-plugin-xstate-include */
// 💡 This machine config will no w be linted too.
export machine = {
initial: 'active',
context: {},
// etc
}
```
12 changes: 8 additions & 4 deletions lib/rules/event-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const getDocsUrl = require('../utils/getDocsUrl')
const { isStringLiteral } = require('../utils/predicates')
const { getTypeProperty } = require('../utils/selectors')
const { init, last } = require('../utils/arrays')
const getSelectorPrefix = require('../utils/getSelectorPrefix')

const toMacroCase = (string) => {
const words = string.split('.').filter(Boolean)
Expand Down Expand Up @@ -61,8 +62,10 @@ function containsWildcardOrDot(name) {
return wildcardOrDot.test(name)
}

const selectorSendEvent =
'CallExpression[callee.name=/^createMachine$|^Machine$/] CallExpression[callee.name=/^send$|^sendParent$|^respond$|^raise$/]'
const selectorSendEvent = (prefix) =>
prefix === ''
? 'CallExpression[callee.name=/^send$|^sendTo$|^sendParent$|^respond$|^raise$|^forwardTo$/]'
: `${prefix}CallExpression[callee.name=/^send$|^sendTo$|^sendParent$|^respond$|^raise$|^forwardTo$/]`

/**
* Default regular expression for the regex option.
Expand Down Expand Up @@ -107,6 +110,7 @@ module.exports = {
},

create: function (context) {
const prefix = getSelectorPrefix(context.sourceCode)
const mode = context.options[0] || 'macroCase'
const regexOption =
mode === 'regex'
Expand All @@ -115,7 +119,7 @@ module.exports = {
const regex = regexOption !== null ? new RegExp(regexOption) : null

return {
'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="on"] > ObjectExpression > Property':
[`${prefix}Property[key.name="on"] > ObjectExpression > Property`]:
function (node) {
// key names [varName] are dynamic values and cannot be linted
if (node.computed) {
Expand Down Expand Up @@ -156,7 +160,7 @@ module.exports = {
}
},

[selectorSendEvent]: function (node) {
[selectorSendEvent(prefix)]: function (node) {
const eventArg = node.arguments[0]
if (!eventArg) {
return
Expand Down
18 changes: 10 additions & 8 deletions lib/rules/invoke-usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
isCreateMachineCall,
isCallExpression,
} = require('../utils/predicates')
const getSelectorPrefix = require('../utils/getSelectorPrefix')

module.exports = {
meta: {
Expand All @@ -30,15 +31,16 @@ module.exports = {
},

create: function (context) {
const prefix = getSelectorPrefix(context.sourceCode)

return {
'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="invoke"]':
function (node) {
if (node.value.type === 'ArrayExpression') {
node.value.elements.forEach((node) => testInvokeDefinition(node))
} else {
testInvokeDefinition(node.value)
}
},
[`${prefix}Property[key.name="invoke"]`]: function (node) {
if (node.value.type === 'ArrayExpression') {
node.value.elements.forEach((node) => testInvokeDefinition(node))
} else {
testInvokeDefinition(node.value)
}
},
}

function testInvokeDefinition(node) {
Expand Down
63 changes: 38 additions & 25 deletions lib/rules/no-async-guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const getDocsUrl = require('../utils/getDocsUrl')
const { isFunctionExpression } = require('../utils/predicates')
const getSettings = require('../utils/getSettings')
const getSelectorPrefix = require('../utils/getSelectorPrefix')

function isAsyncFunctionExpression(node) {
return isFunctionExpression(node) && node.async
Expand All @@ -25,31 +26,43 @@ module.exports = {

create: function (context) {
const { version } = getSettings(context)
return {
'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:first-child Property[key.name=/^cond|guard$/]':
function (node) {
if (version === 4 && node.key.name !== 'cond') {
return
}
if (version > 4 && node.key.name !== 'guard') {
return
}
if (isAsyncFunctionExpression(node.value)) {
context.report({
node: node.value,
messageId: 'guardCannotBeAsync',
})
}
},
'CallExpression[callee.name=/^createMachine$|^Machine$/] > ObjectExpression:nth-child(2) > Property[key.name="guards"] > ObjectExpression > Property':
function (node) {
if (isAsyncFunctionExpression(node.value)) {
context.report({
node: node.value,
messageId: 'guardCannotBeAsync',
})
}
},
const prefix = getSelectorPrefix(context.sourceCode)

function checkInlineGuard(node) {
if (version === 4 && node.key.name !== 'cond') {
return
}
if (version > 4 && node.key.name !== 'guard') {
return
}
if (isAsyncFunctionExpression(node.value)) {
context.report({
node: node.value,
messageId: 'guardCannotBeAsync',
})
}
}

function checkGuardImplementation(node) {
if (isAsyncFunctionExpression(node.value)) {
context.report({
node: node.value,
messageId: 'guardCannotBeAsync',
})
}
}

return prefix === ''
? {
'Property[key.name=/^cond|guard$/]': checkInlineGuard,
'Property[key.name="guards"] > ObjectExpression > Property':
checkGuardImplementation,
}
: {
[`${prefix}> ObjectExpression:first-child Property[key.name=/^cond|guard$/]`]:
checkInlineGuard,
[`${prefix}> ObjectExpression:nth-child(2) > Property[key.name="guards"] > ObjectExpression > Property`]:
checkGuardImplementation,
}
},
}
6 changes: 4 additions & 2 deletions lib/rules/no-auto-forward.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const getDocsUrl = require('../utils/getDocsUrl')
const getSettings = require('../utils/getSettings')
const getSelectorPrefix = require('../utils/getSelectorPrefix')

module.exports = {
meta: {
Expand All @@ -22,9 +23,10 @@ module.exports = {
},

create: function (context) {
const prefix = getSelectorPrefix(context.sourceCode)
const { version } = getSettings(context)
return {
'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="invoke"] > ObjectExpression > Property[key.name="autoForward"]':
[`${prefix}Property[key.name="invoke"] > ObjectExpression > Property[key.name="autoForward"]`]:
function (node) {
if (version !== 4) {
context.report({
Expand All @@ -41,7 +43,7 @@ module.exports = {
}
},

'CallExpression[callee.name=/^createMachine$|^Machine$/] CallExpression[callee.name="spawn"] > ObjectExpression > Property[key.name="autoForward"]':
[`${prefix}CallExpression[callee.name="spawn"] > ObjectExpression > Property[key.name="autoForward"]`]:
function (node) {
if (version !== 4) {
context.report({
Expand Down
10 changes: 6 additions & 4 deletions lib/rules/no-imperative-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const getDocsUrl = require('../utils/getDocsUrl')
const getSettings = require('../utils/getSettings')
const getSelectorPrefix = require('../utils/getSelectorPrefix')

const {
isKnownActionCreatorCall,
Expand Down Expand Up @@ -60,9 +61,10 @@ module.exports = {
},

create: function (context) {
const prefix = getSelectorPrefix(context.sourceCode)
const { version } = getSettings(context)
return {
'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^entry$|^exit$/] ArrowFunctionExpression CallExpression':
[`${prefix}Property[key.name=/^entry$|^exit$/] ArrowFunctionExpression CallExpression`]:
function (node) {
if (
isKnownActionCreatorCall(node, version) &&
Expand All @@ -76,7 +78,7 @@ module.exports = {
}
},

'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name=/^entry$|^exit$/] FunctionExpression CallExpression':
[`${prefix}Property[key.name=/^entry$|^exit$/] FunctionExpression CallExpression`]:
function (node) {
if (
isKnownActionCreatorCall(node, version) &&
Expand All @@ -90,7 +92,7 @@ module.exports = {
}
},

'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="actions"] ArrowFunctionExpression CallExpression':
[`${prefix}Property[key.name="actions"] ArrowFunctionExpression CallExpression`]:
function (node) {
if (
isKnownActionCreatorCall(node, version) &&
Expand All @@ -104,7 +106,7 @@ module.exports = {
}
},

'CallExpression[callee.name=/^createMachine$|^Machine$/] Property[key.name="actions"] FunctionExpression CallExpression':
[`${prefix}Property[key.name="actions"] FunctionExpression CallExpression`]:
function (node) {
if (
isKnownActionCreatorCall(node, version) &&
Expand Down
15 changes: 6 additions & 9 deletions lib/rules/no-infinite-loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const { pipe, map, fromMaybe, Just, Nothing } = require('sanctuary')
const { allPass, complement } = require('../utils/combinators')
const getDocsUrl = require('../utils/getDocsUrl')
const isInsideMachineDeclaration = require('../utils/isInsideMachineDeclaration')
const {
isFirstArrayItem,
propertyHasName,
Expand All @@ -12,6 +11,7 @@ const {
isStringLiteralOrIdentifier,
} = require('../utils/predicates')
const getSettings = require('../utils/getSettings')
const getSelectorPrefix = require('../utils/getSelectorPrefix')

function isEventlessTransitionDeclaration(node) {
return node.type === 'Property' && node.key.name === 'always'
Expand Down Expand Up @@ -159,14 +159,14 @@ module.exports = {
},

create: function (context) {
const prefix = getSelectorPrefix(context.sourceCode)
const { version } = getSettings(context)
return {
// always: {}
// always: { actions: whatever}
'Property[key.name="always"] > ObjectExpression': function (node) {
if (!isInsideMachineDeclaration(node)) {
return
}
[`${prefix}Property[key.name="always"] > ObjectExpression`]: function (
node
) {
if (
!hasProperty('target', node) &&
!isConditionalTransition(node, version)
Expand All @@ -180,11 +180,8 @@ module.exports = {

// always: [{}]
// always: [{ actions: whatever }]
'Property[key.name="always"] > ArrayExpression > ObjectExpression':
[`${prefix}Property[key.name="always"] > ArrayExpression > ObjectExpression`]:
function (node) {
if (!isInsideMachineDeclaration(node)) {
return
}
if (
!hasProperty('target', node) &&
!isConditionalTransition(node, version)
Expand Down
Loading

0 comments on commit 48cd089

Please sign in to comment.