Skip to content

Commit bee4ad5

Browse files
committed
feat(no-inline-implementation): allow built-in higher level guards with xstate v5
Parse "guard" as the guard declaration in xstate v5. Always allow the built-in higher level guards to be inlined. Change the option "serviceCreatorRegex" to "actorCreatorRegex". BREAKING CHANGE: Option "serviceCreatorRegex" has been renamed to "actorCreatorRegex".
1 parent 5440a0d commit bee4ad5

File tree

4 files changed

+498
-91
lines changed

4 files changed

+498
-91
lines changed

docs/rules/no-inline-implementation.md

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ Suggest moving implementations of actions, guards, activities and services into
77
Action/guard/activity/service implementation can be quickly prototyped by specifying inline functions directly in the machine config.
88
Although this is convenient, this makes it difficult to debug, test, serialize and accurately visualize actions. It is recommended to refactor inline implementations into the machine options object.
99

10+
### XState v5
11+
12+
In XState v5 some built-in action creators were removed, so this rule will report an error when they are inlined:
13+
- `respond`
14+
- `send`: removed in favor of `raise` and `sendParent`
15+
- `sendUpdate`
16+
- `start`
17+
18+
XState v5 provides some built-in higher level guards: `and`, `or`, `not`, `stateIn`. These are always fine to use.
19+
1020
Examples of **incorrect** code for this rule:
1121

1222
```javascript
@@ -104,42 +114,62 @@ createMachine({
104114
})
105115

106116
// ✅ inlined guard creator calls are ok if they match guardCreatorRegex
107-
/* eslint no-inline-implementation: [ "warn", { "guardCreatorRegex": "^(and|or|not)$" } ] */
117+
/* eslint no-inline-implementation: [ "warn", { "guardCreatorRegex": "^customGuard$" } ] */
108118
createMachine({
109119
states: {
110120
inactive: {
111121
on: {
112122
BUTTON_CLICKED: {
113-
cond: and(['isStartButton', 'isReady'])
123+
cond: customGuard(['isStartButton', 'isReady']),
114124
target: 'active'
115125
}
116126
}
117127
}
118128
}
119129
})
120130

121-
// ✅ inlined guard creator calls are ok if they match actionCreatorRegex
131+
// ✅ inlined built-in guards are ok with XState v5
132+
createMachine({
133+
states: {
134+
inactive: {
135+
on: {
136+
BUTTON_CLICKED: [
137+
{
138+
guard: and(['isStartButton', 'isDoubleClick']),
139+
target: 'active'
140+
},
141+
{
142+
guard: stateIn('mode.active'),
143+
target: 'inactive'
144+
},
145+
]
146+
}
147+
}
148+
}
149+
})
150+
151+
// ✅ inlined action creator calls are ok if they match actionCreatorRegex
122152
/* eslint no-inline-implementation: [ "warn", { "actionCreatorRegex": "^customAction$" } ] */
123153
createMachine({
124154
states: {
125155
inactive: {
126156
on: {
127157
BUTTON_CLICKED: {
128-
target: 'active'
158+
target: 'active',
129159
actions: customAction(),
130160
}
131161
}
132162
}
133163
}
134164
})
135165

136-
// ✅ inlined service creator calls are ok if they match serviceCreatorRegex
137-
/* eslint no-inline-implementation: [ "warn", { "serviceCreatorRegex": "^customService$" } ] */
166+
// ✅ inlined actor creator calls are ok if they match actorCreatorRegex
167+
/* eslint no-inline-implementation: [ "warn", { "actorCreatorRegex": "^customActor" } ] */
138168
createMachine({
139169
states: {
140170
inactive: {
141171
invoke: {
142-
src: createService()
172+
src: customActor()
143173
}
144174
}
145175
}
@@ -153,7 +183,7 @@ createMachine({
153183
| `allowKnownActionCreators` | No | `false` | Inlined action creators are visualized properly (but still difficult to test, debug and serialize). Setting this option to `true` will turn off the rule for [known action creators](https://xstate.js.org/docs/guides/actions.html) used inline. |
154184
| `guardCreatorRegex` | No | `''` | Use a regular expression to allow custom guard creators. |
155185
| `actionCreatorRegex` | No | `''` | Use a regular expression to allow custom action creators. |
156-
| `serviceCreatorRegex` | No | `''` | Use a regular expression to allow custom service creators. |
186+
| `actorCreatorRegex` | No | `''` | Use a regular expression to allow custom actor creators. |
157187

158188
## Example
159189

@@ -182,7 +212,7 @@ createMachine({
182212
{
183213
"xstate/no-inline-implementation": [
184214
"warn",
185-
{ "serviceCreatorRegex": "^customService$" }
215+
{ "actorCreatorRegex": "^customActor$" }
186216
]
187217
}
188218
```

lib/rules/no-inline-implementation.js

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
} = require('../utils/predicates')
1313
const { getTypeProperty } = require('../utils/selectors')
1414
const { anyPass } = require('../utils/combinators')
15+
const getSettings = require('../utils/getSettings')
1516

1617
function isArrayWithFunctionExpressionOrIdentifier(node) {
1718
return (
@@ -20,20 +21,25 @@ function isArrayWithFunctionExpressionOrIdentifier(node) {
2021
)
2122
}
2223

23-
function isInlineAction(node, allowKnownActionCreators, actionCreatorRegex) {
24+
function isInlineAction(
25+
node,
26+
allowKnownActionCreators,
27+
actionCreatorRegex,
28+
version
29+
) {
2430
return (
2531
isFunctionExpression(node) ||
2632
isIdentifier(node) ||
2733
(isCallExpression(node) &&
2834
!(
29-
(allowKnownActionCreators && isKnownActionCreatorCall(node)) ||
35+
(allowKnownActionCreators && isKnownActionCreatorCall(node, version)) ||
3036
isValidCallExpression(node, actionCreatorRegex)
3137
))
3238
)
3339
}
3440

3541
function isValidCallExpression(node, pattern = '') {
36-
if (pattern === '') {
42+
if (pattern === '' || node.callee.type !== 'Identifier') {
3743
return false
3844
}
3945
return new RegExp(pattern).test(node.callee.name)
@@ -81,7 +87,22 @@ const defaultOptions = {
8187
allowKnownActionCreators: false,
8288
actionCreatorRegex: '',
8389
guardCreatorRegex: '',
84-
serviceCreatorRegex: '',
90+
actorCreatorRegex: '',
91+
}
92+
93+
const guardPropName = {
94+
4: 'cond',
95+
5: 'guard',
96+
}
97+
98+
const knownGuards = ['and', 'or', 'not', 'stateIn']
99+
100+
function isKnownGuard(node) {
101+
return (
102+
node.type === 'CallExpression' &&
103+
node.callee.type === 'Identifier' &&
104+
knownGuards.includes(node.callee.name)
105+
)
85106
}
86107

87108
module.exports = {
@@ -112,10 +133,10 @@ module.exports = {
112133
format: 'regex',
113134
default: defaultOptions.guardCreatorRegex,
114135
},
115-
serviceCreatorRegex: {
136+
actorCreatorRegex: {
116137
type: 'string',
117138
format: 'regex',
118-
default: defaultOptions.serviceCreatorRegex,
139+
default: defaultOptions.actorCreatorRegex,
119140
},
120141
},
121142
additionalProperties: false,
@@ -128,74 +149,90 @@ module.exports = {
128149
'Move the action implementation into machine options and refer it by its name here.',
129150
moveActivityToOptions:
130151
'Move the activity implementation into machine options and refer it by its name here.',
131-
moveServiceToOptions:
132-
'Move the service implementation into machine options and refer it by its name here.',
152+
moveActorToOptions:
153+
'Move the actor implementation into machine options and refer it by its name here.',
133154
},
134155
},
135156

136157
create: function (context) {
158+
const { version } = getSettings(context)
137159
const options = context.options[0] || defaultOptions
138-
function checkServiceSrc(node) {
160+
161+
// TODO also check what is passed to spawn
162+
function checkActorSrc(node) {
163+
if (isStringLiteral(node.value)) {
164+
return
165+
}
139166
if (
140-
!isStringLiteral(node.value) &&
141-
!isObjectExpression(node.value) &&
142-
!(
143-
isCallExpression(node.value) &&
144-
isValidCallExpression(node.value, options.serviceCreatorRegex)
145-
)
167+
isCallExpression(node.value) &&
168+
isValidCallExpression(node.value, options.actorCreatorRegex)
146169
) {
147-
context.report({
148-
node,
149-
messageId: 'moveServiceToOptions',
150-
})
151170
return
152171
}
153172
if (node.value.type === 'ObjectExpression') {
154173
const typeProperty = getTypeProperty(node.value)
155174
if (typeProperty && !isStringLiteral(typeProperty.value)) {
156175
context.report({
157176
node: typeProperty,
158-
messageId: 'moveServiceToOptions',
177+
messageId: 'moveActorToOptions',
159178
})
160179
}
161180
}
181+
182+
context.report({
183+
node,
184+
messageId: 'moveActorToOptions',
185+
})
162186
}
163187

164188
function checkTransitionProperty(node) {
165-
if (node.key.name === 'cond') {
189+
if (node.key.name === guardPropName[version]) {
190+
if (isStringLiteral(node.value)) {
191+
return
192+
}
166193
if (
167-
isFunctionExpression(node.value) ||
168-
isIdentifier(node.value) ||
169-
(isCallExpression(node.value) &&
170-
!isValidCallExpression(node.value, options.guardCreatorRegex))
194+
version >= 5 &&
195+
isCallExpression(node.value) &&
196+
isKnownGuard(node.value)
197+
) {
198+
return
199+
}
200+
if (
201+
isCallExpression(node.value) &&
202+
isValidCallExpression(node.value, options.guardCreatorRegex)
171203
) {
172-
context.report({
173-
node,
174-
messageId: 'moveGuardToOptions',
175-
})
176204
return
177205
}
206+
context.report({
207+
node,
208+
messageId: 'moveGuardToOptions',
209+
})
210+
return
178211
}
179212

180213
if (node.key.name === 'actions') {
181214
if (
182215
isInlineAction(
183216
node.value,
184217
options.allowKnownActionCreators,
185-
options.actionCreatorRegex
218+
options.actionCreatorRegex,
219+
version
186220
)
187221
) {
188222
context.report({
189223
node,
190224
messageId: 'moveActionToOptions',
191225
})
192-
} else if (isArrayExpression(node.value)) {
226+
return
227+
}
228+
if (isArrayExpression(node.value)) {
193229
node.value.elements.forEach((element) => {
194230
if (
195231
isInlineAction(
196232
element,
197233
options.allowKnownActionCreators,
198-
options.actionCreatorRegex
234+
options.actionCreatorRegex,
235+
version
199236
)
200237
) {
201238
context.report({
@@ -217,14 +254,13 @@ module.exports = {
217254
[propertyOfChoosableActionObject]: checkTransitionProperty,
218255
[propertyOfChoosableActionObjectAlt]: checkTransitionProperty,
219256

220-
// TODO deprecated in xstate v5
221257
[activitiesProperty]: function (node) {
222258
if (
223259
isFunctionExpression(node.value) ||
224260
isIdentifier(node.value) ||
225261
isArrayWithFunctionExpressionOrIdentifier(node.value) ||
226262
(isCallExpression(node.value) &&
227-
!isValidCallExpression(node.value, options.serviceCreatorRegex))
263+
!isValidCallExpression(node.value, options.actorCreatorRegex))
228264
) {
229265
context.report({
230266
node,
@@ -233,16 +269,17 @@ module.exports = {
233269
}
234270
},
235271

236-
[srcPropertyInsideInvoke]: checkServiceSrc,
272+
[srcPropertyInsideInvoke]: checkActorSrc,
237273

238-
[srcPropertyInsideInvokeArray]: checkServiceSrc,
274+
[srcPropertyInsideInvokeArray]: checkActorSrc,
239275

240276
[entryExitProperty]: function (node) {
241277
if (
242278
isInlineAction(
243279
node.value,
244280
options.allowKnownActionCreators,
245-
options.actionCreatorRegex
281+
options.actionCreatorRegex,
282+
version
246283
)
247284
) {
248285
context.report({
@@ -255,7 +292,8 @@ module.exports = {
255292
isInlineAction(
256293
element,
257294
options.allowKnownActionCreators,
258-
options.actionCreatorRegex
295+
options.actionCreatorRegex,
296+
version
259297
)
260298
) {
261299
context.report({

0 commit comments

Comments
 (0)