@typeonce/oxlint-plugin-effect-machine 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +2 -0
- package/README.md +95 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/imports.d.ts +15 -0
- package/dist/internal/imports.d.ts.map +1 -0
- package/dist/internal/imports.js +60 -0
- package/dist/internal/imports.js.map +1 -0
- package/dist/internal/planning.d.ts +5 -0
- package/dist/internal/planning.d.ts.map +1 -0
- package/dist/internal/planning.js +87 -0
- package/dist/internal/planning.js.map +1 -0
- package/dist/internal/rules/noAsyncPlanningCallback.d.ts +3 -0
- package/dist/internal/rules/noAsyncPlanningCallback.d.ts.map +1 -0
- package/dist/internal/rules/noAsyncPlanningCallback.js +32 -0
- package/dist/internal/rules/noAsyncPlanningCallback.js.map +1 -0
- package/dist/internal/rules/noRedundantResolve.d.ts +3 -0
- package/dist/internal/rules/noRedundantResolve.d.ts.map +1 -0
- package/dist/internal/rules/noRedundantResolve.js +86 -0
- package/dist/internal/rules/noRedundantResolve.js.map +1 -0
- package/dist/internal/rules/preferInlineHandle.d.ts +3 -0
- package/dist/internal/rules/preferInlineHandle.d.ts.map +1 -0
- package/dist/internal/rules/preferInlineHandle.js +55 -0
- package/dist/internal/rules/preferInlineHandle.js.map +1 -0
- package/dist/recommended.d.ts +7 -0
- package/dist/recommended.d.ts.map +1 -0
- package/dist/recommended.js +7 -0
- package/dist/recommended.js.map +1 -0
- package/package.json +74 -0
- package/src/index.ts +19 -0
- package/src/internal/imports.ts +105 -0
- package/src/internal/planning.ts +124 -0
- package/src/internal/rules/noAsyncPlanningCallback.ts +35 -0
- package/src/internal/rules/noRedundantResolve.ts +105 -0
- package/src/internal/rules/preferInlineHandle.ts +76 -0
- package/src/recommended.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sandro Maglione
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Effect Machine Oxlint plugin
|
|
2
|
+
|
|
3
|
+
`@typeonce/oxlint-plugin-effect-machine` checks Effect Machine definitions for
|
|
4
|
+
redundant resolvers, asynchronous planning, and one-use intermediate machine
|
|
5
|
+
definitions.
|
|
6
|
+
|
|
7
|
+
The plugin uses Oxlint's JavaScript plugin interface. Custom JavaScript plugins
|
|
8
|
+
are currently alpha in Oxlint, so keep Oxlint and this package on versions that
|
|
9
|
+
have been tested together.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Install the plugin and the matching Effect Machine release:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add -D oxlint @typeonce/oxlint-plugin-effect-machine
|
|
17
|
+
pnpm add @typeonce/effect-machine
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
All Effect Machine packages use the same version.
|
|
21
|
+
|
|
22
|
+
## Configure
|
|
23
|
+
|
|
24
|
+
Load the plugin and its recommended rules from `oxlint.config.ts`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { recommended } from "@typeonce/oxlint-plugin-effect-machine/recommended"
|
|
28
|
+
import { defineConfig } from "oxlint"
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
jsPlugins: ["@typeonce/oxlint-plugin-effect-machine"],
|
|
32
|
+
rules: recommended
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
JSON configurations can list the same rules directly:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"jsPlugins": ["@typeonce/oxlint-plugin-effect-machine"],
|
|
41
|
+
"rules": {
|
|
42
|
+
"effect-machine/no-async-planning-callback": "error",
|
|
43
|
+
"effect-machine/no-redundant-resolve": "error",
|
|
44
|
+
"effect-machine/prefer-inline-handle": "error"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The rules are syntax-based. They recognize `Machine.make(...)`, direct chained
|
|
50
|
+
`.handle(...)` calls, and `.handle(...)` calls on definitions declared in the
|
|
51
|
+
same module. They do not resolve a machine definition imported from another
|
|
52
|
+
module.
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
### `effect-machine/no-redundant-resolve`
|
|
57
|
+
|
|
58
|
+
Removes a resolver whose only result is empty default construction:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Before
|
|
62
|
+
const handlers = {
|
|
63
|
+
Start: (to) => to.full.Running().resolve(({ target }) => target.from())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// After `oxlint --fix`
|
|
67
|
+
const handlers = {
|
|
68
|
+
Start: (to) => to.full.Running()
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The fixer does not run when the resolver has options, comments, construction
|
|
73
|
+
input, or any other work.
|
|
74
|
+
|
|
75
|
+
### `effect-machine/no-async-planning-callback`
|
|
76
|
+
|
|
77
|
+
Rejects asynchronous transition, lifecycle, initial, choice, entry, exit, and
|
|
78
|
+
invocation-planning callbacks. A machine plans synchronously. Put asynchronous
|
|
79
|
+
work in state-owned `invoke` sources instead.
|
|
80
|
+
|
|
81
|
+
### `effect-machine/prefer-inline-handle`
|
|
82
|
+
|
|
83
|
+
Reports a private top-level `Machine.make(...)` definition when its only use is
|
|
84
|
+
one `.handle(...)` call:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// Before
|
|
88
|
+
const definition = Machine.make({/* ... */})
|
|
89
|
+
export const machine = definition.handle({/* ... */})
|
|
90
|
+
|
|
91
|
+
// After
|
|
92
|
+
export const machine = Machine.make({/* ... */}).handle({/* ... */})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Definitions that are exported or reused remain valid.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare const plugin: {
|
|
2
|
+
meta: {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
rules: {
|
|
6
|
+
"no-async-planning-callback": import("@oxlint/plugins").Rule;
|
|
7
|
+
"no-redundant-resolve": import("@oxlint/plugins").Rule;
|
|
8
|
+
"prefer-inline-handle": import("@oxlint/plugins").Rule;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export { recommended } from "./recommended.js";
|
|
12
|
+
export default plugin;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,MAAM;;;;;;;;;CASM,CAAA;AAElB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,eAAe,MAAM,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { noAsyncPlanningCallback } from "./internal/rules/noAsyncPlanningCallback.js";
|
|
2
|
+
import { noRedundantResolve } from "./internal/rules/noRedundantResolve.js";
|
|
3
|
+
import { preferInlineHandle } from "./internal/rules/preferInlineHandle.js";
|
|
4
|
+
const plugin = {
|
|
5
|
+
meta: {
|
|
6
|
+
name: "effect-machine"
|
|
7
|
+
},
|
|
8
|
+
rules: {
|
|
9
|
+
"no-async-planning-callback": noAsyncPlanningCallback,
|
|
10
|
+
"no-redundant-resolve": noRedundantResolve,
|
|
11
|
+
"prefer-inline-handle": preferInlineHandle
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export { recommended } from "./recommended.js";
|
|
15
|
+
export default plugin;
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAA;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAA;AAE3E,MAAM,MAAM,GAAG;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,gBAAgB;KACvB;IACD,KAAK,EAAE;QACL,4BAA4B,EAAE,uBAAuB;QACrD,sBAAsB,EAAE,kBAAkB;QAC1C,sBAAsB,EAAE,kBAAkB;KAC3C;CACe,CAAA;AAElB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,eAAe,MAAM,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
export interface MachineBindings {
|
|
3
|
+
readonly definitions: Set<string>;
|
|
4
|
+
readonly machine: Set<string>;
|
|
5
|
+
readonly namespaces: Set<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare const makeMachineBindings: () => MachineBindings;
|
|
8
|
+
export declare const recordMachineImport: (bindings: MachineBindings, node: ESTree.ImportDeclaration) => void;
|
|
9
|
+
export declare const hasMachineImport: (bindings: MachineBindings) => boolean;
|
|
10
|
+
export declare const staticMemberName: (node: ESTree.Node) => string | undefined;
|
|
11
|
+
export declare const isMachineMakeCall: (node: ESTree.CallExpression, bindings: MachineBindings) => boolean;
|
|
12
|
+
export declare const recordMachineDefinition: (bindings: MachineBindings, node: ESTree.VariableDeclarator) => void;
|
|
13
|
+
export declare const isMachineHandleCall: (node: ESTree.CallExpression, bindings: MachineBindings) => boolean;
|
|
14
|
+
export declare const isMemberCall: (node: ESTree.Node, member: string) => node is ESTree.CallExpression;
|
|
15
|
+
//# sourceMappingURL=imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/internal/imports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAI7C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACjC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACjC;AAED,eAAO,MAAM,mBAAmB,QAAO,eAIrC,CAAA;AAIF,eAAO,MAAM,mBAAmB,GAC9B,UAAU,eAAe,EACzB,MAAM,MAAM,CAAC,iBAAiB,KAC7B,IAaF,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,UAAU,eAAe,KAAG,OACF,CAAA;AAE3D,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,CAAC,IAAI,KAAG,MAAM,GAAG,SAK/C,CAAA;AAaf,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,CAAC,cAAc,EAC3B,UAAU,eAAe,KACxB,OAUF,CAAA;AAED,eAAO,MAAM,uBAAuB,GAClC,UAAU,eAAe,EACzB,MAAM,MAAM,CAAC,kBAAkB,KAC9B,IAMF,CAAA;AAED,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,CAAC,cAAc,EAC3B,UAAU,eAAe,KACxB,OAUF,CAAA;AAED,eAAO,MAAM,YAAY,GACvB,MAAM,MAAM,CAAC,IAAI,EACjB,QAAQ,MAAM,KACb,IAAI,IAAI,MAAM,CAAC,cAGwB,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const effectMachineModule = "@typeonce/effect-machine";
|
|
2
|
+
export const makeMachineBindings = () => ({
|
|
3
|
+
definitions: new Set(),
|
|
4
|
+
machine: new Set(),
|
|
5
|
+
namespaces: new Set()
|
|
6
|
+
});
|
|
7
|
+
const moduleExportName = (node) => node.type === "Identifier" ? node.name : node.value;
|
|
8
|
+
export const recordMachineImport = (bindings, node) => {
|
|
9
|
+
if (node.source.value !== effectMachineModule)
|
|
10
|
+
return;
|
|
11
|
+
for (const specifier of node.specifiers) {
|
|
12
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
13
|
+
bindings.namespaces.add(specifier.local.name);
|
|
14
|
+
}
|
|
15
|
+
else if (specifier.type === "ImportSpecifier" &&
|
|
16
|
+
moduleExportName(specifier.imported) === "Machine") {
|
|
17
|
+
bindings.machine.add(specifier.local.name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export const hasMachineImport = (bindings) => bindings.machine.size > 0 || bindings.namespaces.size > 0;
|
|
22
|
+
export const staticMemberName = (node) => node.type === "MemberExpression" &&
|
|
23
|
+
!node.computed &&
|
|
24
|
+
node.property.type === "Identifier"
|
|
25
|
+
? node.property.name
|
|
26
|
+
: undefined;
|
|
27
|
+
const isNamespaceMachine = (node, bindings) => node.type === "MemberExpression" &&
|
|
28
|
+
!node.computed &&
|
|
29
|
+
node.object.type === "Identifier" &&
|
|
30
|
+
bindings.namespaces.has(node.object.name) &&
|
|
31
|
+
node.property.type === "Identifier" &&
|
|
32
|
+
node.property.name === "Machine";
|
|
33
|
+
export const isMachineMakeCall = (node, bindings) => {
|
|
34
|
+
if (node.callee.type !== "MemberExpression" ||
|
|
35
|
+
staticMemberName(node.callee) !== "make")
|
|
36
|
+
return false;
|
|
37
|
+
const receiver = node.callee.object;
|
|
38
|
+
return receiver.type === "Identifier"
|
|
39
|
+
? bindings.machine.has(receiver.name)
|
|
40
|
+
: isNamespaceMachine(receiver, bindings);
|
|
41
|
+
};
|
|
42
|
+
export const recordMachineDefinition = (bindings, node) => {
|
|
43
|
+
if (node.id.type === "Identifier" &&
|
|
44
|
+
node.init?.type === "CallExpression" &&
|
|
45
|
+
isMachineMakeCall(node.init, bindings))
|
|
46
|
+
bindings.definitions.add(node.id.name);
|
|
47
|
+
};
|
|
48
|
+
export const isMachineHandleCall = (node, bindings) => {
|
|
49
|
+
if (node.callee.type !== "MemberExpression" ||
|
|
50
|
+
staticMemberName(node.callee) !== "handle")
|
|
51
|
+
return false;
|
|
52
|
+
const receiver = node.callee.object;
|
|
53
|
+
return receiver.type === "CallExpression"
|
|
54
|
+
? isMachineMakeCall(receiver, bindings)
|
|
55
|
+
: receiver.type === "Identifier" && bindings.definitions.has(receiver.name);
|
|
56
|
+
};
|
|
57
|
+
export const isMemberCall = (node, member) => node.type === "CallExpression" &&
|
|
58
|
+
node.callee.type === "MemberExpression" &&
|
|
59
|
+
staticMemberName(node.callee) === member;
|
|
60
|
+
//# sourceMappingURL=imports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.js","sourceRoot":"","sources":["../../src/internal/imports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
import { type MachineBindings } from "./imports.js";
|
|
3
|
+
export type PlanningFunction = ESTree.ArrowFunctionExpression | ESTree.Function;
|
|
4
|
+
export declare const isPlanningCallback: (node: PlanningFunction, bindings: MachineBindings) => boolean;
|
|
5
|
+
//# sourceMappingURL=planning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planning.d.ts","sourceRoot":"","sources":["../../src/internal/planning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAA0C,KAAK,eAAe,EAAoB,MAAM,cAAc,CAAA;AAE7G,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,uBAAuB,GAAG,MAAM,CAAC,QAAQ,CAAA;AAuG/E,eAAO,MAAM,kBAAkB,GAC7B,MAAM,gBAAgB,EACtB,UAAU,eAAe,KACxB,OAcF,CAAA"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isMachineHandleCall, isMachineMakeCall, staticMemberName } from "./imports.js";
|
|
2
|
+
const directPlanningMethods = new Set([
|
|
3
|
+
"onDone",
|
|
4
|
+
"onElement",
|
|
5
|
+
"onFailure",
|
|
6
|
+
"onSnapshot",
|
|
7
|
+
"resolve"
|
|
8
|
+
]);
|
|
9
|
+
const statePlanningProperties = new Set([
|
|
10
|
+
"always",
|
|
11
|
+
"choice",
|
|
12
|
+
"entry",
|
|
13
|
+
"exit",
|
|
14
|
+
"invoke"
|
|
15
|
+
]);
|
|
16
|
+
const propertyName = (node) => {
|
|
17
|
+
if (node.type !== "Property" || node.computed)
|
|
18
|
+
return undefined;
|
|
19
|
+
return node.key.type === "Identifier" || node.key.type === "Literal"
|
|
20
|
+
? String(node.key.type === "Identifier" ? node.key.name : node.key.value)
|
|
21
|
+
: undefined;
|
|
22
|
+
};
|
|
23
|
+
const isCallArgument = (node, predicate) => node.parent.type === "CallExpression" &&
|
|
24
|
+
node.parent.arguments.includes(node) &&
|
|
25
|
+
predicate(node.parent);
|
|
26
|
+
const isMachineMakeConfig = (node, bindings) => isCallArgument(node, (call) => isMachineMakeCall(call, bindings));
|
|
27
|
+
const isMachineHandleConfig = (node, bindings) => isCallArgument(node, (call) => isMachineHandleCall(call, bindings));
|
|
28
|
+
const isStateConfig = (node, bindings) => {
|
|
29
|
+
if (node.parent.type !== "Property" || node.parent.parent.type !== "ObjectExpression") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const stateCollection = node.parent.parent;
|
|
33
|
+
if (isMachineHandleConfig(stateCollection, bindings))
|
|
34
|
+
return true;
|
|
35
|
+
if (stateCollection.parent.type !== "Property" ||
|
|
36
|
+
propertyName(stateCollection.parent) !== "states" ||
|
|
37
|
+
stateCollection.parent.parent.type !== "ObjectExpression")
|
|
38
|
+
return false;
|
|
39
|
+
return isStateConfig(stateCollection.parent.parent, bindings);
|
|
40
|
+
};
|
|
41
|
+
const isEventHandlerProperty = (node, bindings) => {
|
|
42
|
+
if (node.type !== "Property" || node.parent.type !== "ObjectExpression")
|
|
43
|
+
return false;
|
|
44
|
+
const onProperty = node.parent.parent;
|
|
45
|
+
return onProperty.type === "Property" &&
|
|
46
|
+
propertyName(onProperty) === "on" &&
|
|
47
|
+
onProperty.parent.type === "ObjectExpression" &&
|
|
48
|
+
isStateConfig(onProperty.parent, bindings);
|
|
49
|
+
};
|
|
50
|
+
const isPropertyPlanningCallback = (node, bindings) => {
|
|
51
|
+
const property = node.parent;
|
|
52
|
+
if (property.type !== "Property" ||
|
|
53
|
+
property.value !== node ||
|
|
54
|
+
property.parent.type !== "ObjectExpression")
|
|
55
|
+
return false;
|
|
56
|
+
const name = propertyName(property);
|
|
57
|
+
return name === "initial"
|
|
58
|
+
? isMachineMakeConfig(property.parent, bindings)
|
|
59
|
+
: (name !== undefined && statePlanningProperties.has(name) && isStateConfig(property.parent, bindings)) ||
|
|
60
|
+
isEventHandlerProperty(property, bindings);
|
|
61
|
+
};
|
|
62
|
+
const enclosingFunction = (node) => {
|
|
63
|
+
let current = node.parent;
|
|
64
|
+
while (current !== null && current.type !== "Program") {
|
|
65
|
+
if (current.type === "ArrowFunctionExpression" ||
|
|
66
|
+
current.type === "FunctionExpression")
|
|
67
|
+
return current;
|
|
68
|
+
current = current.parent;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
export const isPlanningCallback = (node, bindings) => {
|
|
73
|
+
if (isPropertyPlanningCallback(node, bindings))
|
|
74
|
+
return true;
|
|
75
|
+
const parent = node.parent;
|
|
76
|
+
if (parent.type === "CallExpression" && parent.arguments.includes(node)) {
|
|
77
|
+
const method = parent.callee.type === "MemberExpression"
|
|
78
|
+
? staticMemberName(parent.callee)
|
|
79
|
+
: undefined;
|
|
80
|
+
if (method !== undefined && directPlanningMethods.has(method)) {
|
|
81
|
+
const owner = enclosingFunction(parent);
|
|
82
|
+
return owner !== undefined && isPlanningCallback(owner, bindings);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=planning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planning.js","sourceRoot":"","sources":["../../src/internal/planning.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noAsyncPlanningCallback.d.ts","sourceRoot":"","sources":["../../../src/internal/rules/noAsyncPlanningCallback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAI3C,eAAO,MAAM,uBAAuB,EAAE,IA8BrC,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { hasMachineImport, makeMachineBindings, recordMachineDefinition, recordMachineImport } from "../imports.js";
|
|
2
|
+
import { isPlanningCallback } from "../planning.js";
|
|
3
|
+
export const noAsyncPlanningCallback = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Keep Effect Machine planning callbacks synchronous.",
|
|
8
|
+
recommended: true
|
|
9
|
+
},
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
asyncPlanning: "Planning callbacks must be synchronous. Move asynchronous work into state-owned invocation."
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const bindings = makeMachineBindings();
|
|
17
|
+
const inspect = (node) => {
|
|
18
|
+
if (hasMachineImport(bindings) &&
|
|
19
|
+
node.async &&
|
|
20
|
+
isPlanningCallback(node, bindings)) {
|
|
21
|
+
context.report({ node, messageId: "asyncPlanning" });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
ImportDeclaration: (node) => recordMachineImport(bindings, node),
|
|
26
|
+
VariableDeclarator: (node) => recordMachineDefinition(bindings, node),
|
|
27
|
+
ArrowFunctionExpression: inspect,
|
|
28
|
+
FunctionExpression: inspect
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=noAsyncPlanningCallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noAsyncPlanningCallback.js","sourceRoot":"","sources":["../../../src/internal/rules/noAsyncPlanningCallback.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnH,OAAO,EAAE,kBAAkB,EAAyB,MAAM,gBAAgB,CAAA;AAE1E,MAAM,CAAC,MAAM,uBAAuB,GAAS;IAC3C,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,qDAAqD;YAClE,WAAW,EAAE,IAAI;SAClB;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,aAAa,EAAE,6FAA6F;SAC7G;KACF;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAA;QACtC,MAAM,OAAO,GAAG,CAAC,IAAsB,EAAQ,EAAE;YAC/C,IACE,gBAAgB,CAAC,QAAQ,CAAC;gBAC1B,IAAI,CAAC,KAAK;gBACV,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,EAClC,CAAC;gBACD,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC,CAAA;YACtD,CAAC;QACH,CAAC,CAAA;QACD,OAAO;YACL,iBAAiB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC;YAChE,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC;YACrE,uBAAuB,EAAE,OAAO;YAChC,kBAAkB,EAAE,OAAO;SAC5B,CAAA;IACH,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noRedundantResolve.d.ts","sourceRoot":"","sources":["../../../src/internal/rules/noRedundantResolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAmDnD,eAAO,MAAM,kBAAkB,EAAE,IAqDhC,CAAA"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { hasMachineImport, makeMachineBindings, recordMachineDefinition, recordMachineImport, staticMemberName } from "../imports.js";
|
|
2
|
+
import { isPlanningCallback } from "../planning.js";
|
|
3
|
+
const returnedExpression = (node) => {
|
|
4
|
+
if (node.body === null)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (node.body.type !== "BlockStatement")
|
|
7
|
+
return node.body;
|
|
8
|
+
if (node.body.body.length !== 1 || node.body.body[0]?.type !== "ReturnStatement") {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return node.body.body[0].argument;
|
|
12
|
+
};
|
|
13
|
+
const targetBinding = (node) => {
|
|
14
|
+
if (node.params.length !== 1)
|
|
15
|
+
return undefined;
|
|
16
|
+
const parameter = node.params[0];
|
|
17
|
+
if (parameter?.type !== "ObjectPattern" || parameter.properties.length !== 1) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const property = parameter.properties[0];
|
|
21
|
+
if (property?.type !== "Property" ||
|
|
22
|
+
property.computed ||
|
|
23
|
+
property.key.type !== "Identifier" ||
|
|
24
|
+
property.key.name !== "target" ||
|
|
25
|
+
property.value.type !== "Identifier")
|
|
26
|
+
return undefined;
|
|
27
|
+
return property.value.name;
|
|
28
|
+
};
|
|
29
|
+
const isDefaultTargetConstruction = (node, binding) => node?.type === "CallExpression" &&
|
|
30
|
+
node.arguments.length === 0 &&
|
|
31
|
+
node.callee.type === "MemberExpression" &&
|
|
32
|
+
staticMemberName(node.callee) === "from" &&
|
|
33
|
+
node.callee.object.type === "Identifier" &&
|
|
34
|
+
node.callee.object.name === binding;
|
|
35
|
+
export const noRedundantResolve = {
|
|
36
|
+
meta: {
|
|
37
|
+
type: "suggestion",
|
|
38
|
+
docs: {
|
|
39
|
+
description: "Remove resolvers that only apply default target construction.",
|
|
40
|
+
recommended: true
|
|
41
|
+
},
|
|
42
|
+
fixable: "code",
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
redundantResolver: "Remove this resolver. The selected target already applies default construction."
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
create(context) {
|
|
49
|
+
const bindings = makeMachineBindings();
|
|
50
|
+
return {
|
|
51
|
+
ImportDeclaration: (node) => recordMachineImport(bindings, node),
|
|
52
|
+
CallExpression(node) {
|
|
53
|
+
if (!hasMachineImport(bindings) ||
|
|
54
|
+
node.arguments.length !== 1 ||
|
|
55
|
+
node.callee.type !== "MemberExpression" ||
|
|
56
|
+
staticMemberName(node.callee) !== "resolve")
|
|
57
|
+
return;
|
|
58
|
+
const callback = node.arguments[0];
|
|
59
|
+
if (callback?.type !== "ArrowFunctionExpression" &&
|
|
60
|
+
callback?.type !== "FunctionExpression")
|
|
61
|
+
return;
|
|
62
|
+
if (callback.async || callback.generator)
|
|
63
|
+
return;
|
|
64
|
+
if (!isPlanningCallback(callback, bindings))
|
|
65
|
+
return;
|
|
66
|
+
const binding = targetBinding(callback);
|
|
67
|
+
if (binding === undefined ||
|
|
68
|
+
!isDefaultTargetConstruction(returnedExpression(callback), binding))
|
|
69
|
+
return;
|
|
70
|
+
const receiver = node.callee.object;
|
|
71
|
+
if (context.sourceCode.getCommentsInside(node).length === 0) {
|
|
72
|
+
context.report({
|
|
73
|
+
node,
|
|
74
|
+
messageId: "redundantResolver",
|
|
75
|
+
fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(receiver))
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
context.report({ node, messageId: "redundantResolver" });
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
VariableDeclarator: (node) => recordMachineDefinition(bindings, node)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=noRedundantResolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noRedundantResolve.js","sourceRoot":"","sources":["../../../src/internal/rules/noRedundantResolve.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD,MAAM,kBAAkB,GAAG,CACzB,IAAsD,EAChB,EAAE;IACxC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAA;IACxC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,IAAI,CAAC,IAAI,CAAA;IACzD,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,iBAAiB,EAAE,CAAC;QACjF,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;AACnC,CAAC,CAAA;AAED,MAAM,aAAa,GAAG,CACpB,IAAsD,EAClC,EAAE;IACtB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAChC,IAAI,SAAS,EAAE,IAAI,KAAK,eAAe,IAAI,SAAS,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7E,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IACxC,IACE,QAAQ,EAAE,IAAI,KAAK,UAAU;QAC7B,QAAQ,CAAC,QAAQ;QACjB,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,YAAY;QAClC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,QAAQ;QAC9B,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,YAAY;QACpC,OAAO,SAAS,CAAA;IAClB,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAA;AAC5B,CAAC,CAAA;AAED,MAAM,2BAA2B,GAAG,CAClC,IAA0C,EAC1C,OAAe,EACN,EAAE,CACX,IAAI,EAAE,IAAI,KAAK,gBAAgB;IAC/B,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,kBAAkB;IACvC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,MAAM;IACxC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY;IACxC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,CAAA;AAErC,MAAM,CAAC,MAAM,kBAAkB,GAAS;IACtC,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EAAE,+DAA+D;YAC5E,WAAW,EAAE,IAAI;SAClB;QACD,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,iBAAiB,EAAE,iFAAiF;SACrG;KACF;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAA;QACtC,OAAO;YACL,iBAAiB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC;YAChE,cAAc,CAAC,IAAI;gBACjB,IACE,CAAC,gBAAgB,CAAC,QAAQ,CAAC;oBAC3B,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;oBAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,kBAAkB;oBACvC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,SAAS;oBAC3C,OAAM;gBAER,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;gBAClC,IACE,QAAQ,EAAE,IAAI,KAAK,yBAAyB;oBAC5C,QAAQ,EAAE,IAAI,KAAK,oBAAoB;oBACvC,OAAM;gBACR,IAAI,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,SAAS;oBAAE,OAAM;gBAChD,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC;oBAAE,OAAM;gBAEnD,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;gBACvC,IACE,OAAO,KAAK,SAAS;oBACrB,CAAC,2BAA2B,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;oBACnE,OAAM;gBAER,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAA;gBACnC,IAAI,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC5D,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,mBAAmB;wBAC9B,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;qBAC9E,CAAC,CAAA;gBACJ,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC,CAAA;gBAC1D,CAAC;YACH,CAAC;YACD,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC;SACtE,CAAA;IACH,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preferInlineHandle.d.ts","sourceRoot":"","sources":["../../../src/internal/rules/preferInlineHandle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,IAAI,EAAY,MAAM,iBAAiB,CAAA;AAoCtE,eAAO,MAAM,kBAAkB,EAAE,IAuChC,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { hasMachineImport, isMachineMakeCall, isMemberCall, makeMachineBindings, recordMachineDefinition, recordMachineImport } from "../imports.js";
|
|
2
|
+
const isTopLevelPrivateDeclaration = (node) => node.parent.type === "VariableDeclaration" && node.parent.parent.type === "Program";
|
|
3
|
+
const declaredVariable = (context, candidate) => context.sourceCode.getDeclaredVariables(candidate.declaration)
|
|
4
|
+
.find((variable) => variable.name === candidate.name.name);
|
|
5
|
+
const isOnlyHandleReference = (variable) => {
|
|
6
|
+
const references = variable.references.filter((reference) => reference.isRead());
|
|
7
|
+
if (references.length !== 1)
|
|
8
|
+
return false;
|
|
9
|
+
const identifier = references[0]?.identifier;
|
|
10
|
+
if (identifier?.parent.type !== "MemberExpression")
|
|
11
|
+
return false;
|
|
12
|
+
const member = identifier.parent;
|
|
13
|
+
if (member.object !== identifier || !isMemberCall(member.parent, "handle"))
|
|
14
|
+
return false;
|
|
15
|
+
return member.parent.callee === member;
|
|
16
|
+
};
|
|
17
|
+
export const preferInlineHandle = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: "suggestion",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Chain handle directly from one-use Machine.make definitions.",
|
|
22
|
+
recommended: true
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
inlineHandle: "Chain .handle(...) directly from Machine.make(...). This one-use definition adds no reusable model."
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
const bindings = makeMachineBindings();
|
|
31
|
+
const candidates = [];
|
|
32
|
+
return {
|
|
33
|
+
ImportDeclaration: (node) => recordMachineImport(bindings, node),
|
|
34
|
+
VariableDeclarator(node) {
|
|
35
|
+
recordMachineDefinition(bindings, node);
|
|
36
|
+
if (!hasMachineImport(bindings) ||
|
|
37
|
+
node.id.type !== "Identifier" ||
|
|
38
|
+
node.init?.type !== "CallExpression" ||
|
|
39
|
+
!isTopLevelPrivateDeclaration(node) ||
|
|
40
|
+
!isMachineMakeCall(node.init, bindings))
|
|
41
|
+
return;
|
|
42
|
+
candidates.push({ declaration: node, name: node.id });
|
|
43
|
+
},
|
|
44
|
+
"Program:exit"() {
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
const variable = declaredVariable(context, candidate);
|
|
47
|
+
if (variable !== undefined && isOnlyHandleReference(variable)) {
|
|
48
|
+
context.report({ node: candidate.name, messageId: "inlineHandle" });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=preferInlineHandle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preferInlineHandle.js","sourceRoot":"","sources":["../../../src/internal/rules/preferInlineHandle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Rules recommended for every Effect Machine model. */
|
|
2
|
+
export declare const recommended: {
|
|
3
|
+
readonly "effect-machine/no-async-planning-callback": "error";
|
|
4
|
+
readonly "effect-machine/no-redundant-resolve": "error";
|
|
5
|
+
readonly "effect-machine/prefer-inline-handle": "error";
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=recommended.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommended.d.ts","sourceRoot":"","sources":["../src/recommended.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,eAAO,MAAM,WAAW;;;;CAId,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Rules recommended for every Effect Machine model. */
|
|
2
|
+
export const recommended = {
|
|
3
|
+
"effect-machine/no-async-planning-callback": "error",
|
|
4
|
+
"effect-machine/no-redundant-resolve": "error",
|
|
5
|
+
"effect-machine/prefer-inline-handle": "error"
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=recommended.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommended.js","sourceRoot":"","sources":["../src/recommended.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,2CAA2C,EAAE,OAAO;IACpD,qCAAqC,EAAE,OAAO;IAC9C,qCAAqC,EAAE,OAAO;CACtC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typeonce/oxlint-plugin-effect-machine",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Oxlint rules for Effect Machine models",
|
|
5
|
+
"author": "Sandro Maglione",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/typeonce-dev/effect-machine.git",
|
|
9
|
+
"directory": "packages/oxlint-plugin"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/typeonce-dev/effect-machine/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/typeonce-dev/effect-machine/tree/main/packages/oxlint-plugin#readme",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"src/**/*.ts",
|
|
20
|
+
"dist/**/*.js",
|
|
21
|
+
"dist/**/*.js.map",
|
|
22
|
+
"dist/**/*.d.ts",
|
|
23
|
+
"dist/**/*.d.ts.map",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"NOTICE"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./src/index.ts",
|
|
31
|
+
"import": "./src/index.ts"
|
|
32
|
+
},
|
|
33
|
+
"./recommended": {
|
|
34
|
+
"types": "./src/recommended.ts",
|
|
35
|
+
"import": "./src/recommended.ts"
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json",
|
|
38
|
+
"./internal/*": null
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true,
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./recommended": {
|
|
49
|
+
"types": "./dist/recommended.d.ts",
|
|
50
|
+
"import": "./dist/recommended.js"
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json",
|
|
53
|
+
"./internal/*": null
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -b tsconfig.build.json",
|
|
58
|
+
"check": "tsc -b tsconfig.json"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"oxlint": ">=1.80.0 <2"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@oxlint/plugins": "1.80.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"oxlint": "1.80.0",
|
|
68
|
+
"typescript": "6.0.3",
|
|
69
|
+
"vitest": "4.1.10"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=20.19.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Plugin } from "@oxlint/plugins"
|
|
2
|
+
import { noAsyncPlanningCallback } from "./internal/rules/noAsyncPlanningCallback.js"
|
|
3
|
+
import { noRedundantResolve } from "./internal/rules/noRedundantResolve.js"
|
|
4
|
+
import { preferInlineHandle } from "./internal/rules/preferInlineHandle.js"
|
|
5
|
+
|
|
6
|
+
const plugin = {
|
|
7
|
+
meta: {
|
|
8
|
+
name: "effect-machine"
|
|
9
|
+
},
|
|
10
|
+
rules: {
|
|
11
|
+
"no-async-planning-callback": noAsyncPlanningCallback,
|
|
12
|
+
"no-redundant-resolve": noRedundantResolve,
|
|
13
|
+
"prefer-inline-handle": preferInlineHandle
|
|
14
|
+
}
|
|
15
|
+
} satisfies Plugin
|
|
16
|
+
|
|
17
|
+
export { recommended } from "./recommended.js"
|
|
18
|
+
|
|
19
|
+
export default plugin
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
2
|
+
|
|
3
|
+
const effectMachineModule = "@typeonce/effect-machine"
|
|
4
|
+
|
|
5
|
+
export interface MachineBindings {
|
|
6
|
+
readonly definitions: Set<string>
|
|
7
|
+
readonly machine: Set<string>
|
|
8
|
+
readonly namespaces: Set<string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const makeMachineBindings = (): MachineBindings => ({
|
|
12
|
+
definitions: new Set(),
|
|
13
|
+
machine: new Set(),
|
|
14
|
+
namespaces: new Set()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const moduleExportName = (node: ESTree.ModuleExportName): string => node.type === "Identifier" ? node.name : node.value
|
|
18
|
+
|
|
19
|
+
export const recordMachineImport = (
|
|
20
|
+
bindings: MachineBindings,
|
|
21
|
+
node: ESTree.ImportDeclaration
|
|
22
|
+
): void => {
|
|
23
|
+
if (node.source.value !== effectMachineModule) return
|
|
24
|
+
|
|
25
|
+
for (const specifier of node.specifiers) {
|
|
26
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
27
|
+
bindings.namespaces.add(specifier.local.name)
|
|
28
|
+
} else if (
|
|
29
|
+
specifier.type === "ImportSpecifier" &&
|
|
30
|
+
moduleExportName(specifier.imported) === "Machine"
|
|
31
|
+
) {
|
|
32
|
+
bindings.machine.add(specifier.local.name)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const hasMachineImport = (bindings: MachineBindings): boolean =>
|
|
38
|
+
bindings.machine.size > 0 || bindings.namespaces.size > 0
|
|
39
|
+
|
|
40
|
+
export const staticMemberName = (node: ESTree.Node): string | undefined =>
|
|
41
|
+
node.type === "MemberExpression" &&
|
|
42
|
+
!node.computed &&
|
|
43
|
+
node.property.type === "Identifier"
|
|
44
|
+
? node.property.name
|
|
45
|
+
: undefined
|
|
46
|
+
|
|
47
|
+
const isNamespaceMachine = (
|
|
48
|
+
node: ESTree.Node,
|
|
49
|
+
bindings: MachineBindings
|
|
50
|
+
): boolean =>
|
|
51
|
+
node.type === "MemberExpression" &&
|
|
52
|
+
!node.computed &&
|
|
53
|
+
node.object.type === "Identifier" &&
|
|
54
|
+
bindings.namespaces.has(node.object.name) &&
|
|
55
|
+
node.property.type === "Identifier" &&
|
|
56
|
+
node.property.name === "Machine"
|
|
57
|
+
|
|
58
|
+
export const isMachineMakeCall = (
|
|
59
|
+
node: ESTree.CallExpression,
|
|
60
|
+
bindings: MachineBindings
|
|
61
|
+
): boolean => {
|
|
62
|
+
if (
|
|
63
|
+
node.callee.type !== "MemberExpression" ||
|
|
64
|
+
staticMemberName(node.callee) !== "make"
|
|
65
|
+
) return false
|
|
66
|
+
|
|
67
|
+
const receiver = node.callee.object
|
|
68
|
+
return receiver.type === "Identifier"
|
|
69
|
+
? bindings.machine.has(receiver.name)
|
|
70
|
+
: isNamespaceMachine(receiver, bindings)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const recordMachineDefinition = (
|
|
74
|
+
bindings: MachineBindings,
|
|
75
|
+
node: ESTree.VariableDeclarator
|
|
76
|
+
): void => {
|
|
77
|
+
if (
|
|
78
|
+
node.id.type === "Identifier" &&
|
|
79
|
+
node.init?.type === "CallExpression" &&
|
|
80
|
+
isMachineMakeCall(node.init, bindings)
|
|
81
|
+
) bindings.definitions.add(node.id.name)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const isMachineHandleCall = (
|
|
85
|
+
node: ESTree.CallExpression,
|
|
86
|
+
bindings: MachineBindings
|
|
87
|
+
): boolean => {
|
|
88
|
+
if (
|
|
89
|
+
node.callee.type !== "MemberExpression" ||
|
|
90
|
+
staticMemberName(node.callee) !== "handle"
|
|
91
|
+
) return false
|
|
92
|
+
|
|
93
|
+
const receiver = node.callee.object
|
|
94
|
+
return receiver.type === "CallExpression"
|
|
95
|
+
? isMachineMakeCall(receiver, bindings)
|
|
96
|
+
: receiver.type === "Identifier" && bindings.definitions.has(receiver.name)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const isMemberCall = (
|
|
100
|
+
node: ESTree.Node,
|
|
101
|
+
member: string
|
|
102
|
+
): node is ESTree.CallExpression =>
|
|
103
|
+
node.type === "CallExpression" &&
|
|
104
|
+
node.callee.type === "MemberExpression" &&
|
|
105
|
+
staticMemberName(node.callee) === member
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
2
|
+
import { isMachineHandleCall, isMachineMakeCall, type MachineBindings, staticMemberName } from "./imports.js"
|
|
3
|
+
|
|
4
|
+
export type PlanningFunction = ESTree.ArrowFunctionExpression | ESTree.Function
|
|
5
|
+
|
|
6
|
+
const directPlanningMethods = new Set([
|
|
7
|
+
"onDone",
|
|
8
|
+
"onElement",
|
|
9
|
+
"onFailure",
|
|
10
|
+
"onSnapshot",
|
|
11
|
+
"resolve"
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
const statePlanningProperties = new Set([
|
|
15
|
+
"always",
|
|
16
|
+
"choice",
|
|
17
|
+
"entry",
|
|
18
|
+
"exit",
|
|
19
|
+
"invoke"
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
const propertyName = (node: ESTree.Node): string | undefined => {
|
|
23
|
+
if (node.type !== "Property" || node.computed) return undefined
|
|
24
|
+
return node.key.type === "Identifier" || node.key.type === "Literal"
|
|
25
|
+
? String(node.key.type === "Identifier" ? node.key.name : node.key.value)
|
|
26
|
+
: undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isCallArgument = (
|
|
30
|
+
node: ESTree.ObjectExpression,
|
|
31
|
+
predicate: (call: ESTree.CallExpression) => boolean
|
|
32
|
+
): boolean =>
|
|
33
|
+
node.parent.type === "CallExpression" &&
|
|
34
|
+
node.parent.arguments.includes(node) &&
|
|
35
|
+
predicate(node.parent)
|
|
36
|
+
|
|
37
|
+
const isMachineMakeConfig = (
|
|
38
|
+
node: ESTree.ObjectExpression,
|
|
39
|
+
bindings: MachineBindings
|
|
40
|
+
): boolean => isCallArgument(node, (call) => isMachineMakeCall(call, bindings))
|
|
41
|
+
|
|
42
|
+
const isMachineHandleConfig = (
|
|
43
|
+
node: ESTree.ObjectExpression,
|
|
44
|
+
bindings: MachineBindings
|
|
45
|
+
): boolean => isCallArgument(node, (call) => isMachineHandleCall(call, bindings))
|
|
46
|
+
|
|
47
|
+
const isStateConfig = (
|
|
48
|
+
node: ESTree.ObjectExpression,
|
|
49
|
+
bindings: MachineBindings
|
|
50
|
+
): boolean => {
|
|
51
|
+
if (node.parent.type !== "Property" || node.parent.parent.type !== "ObjectExpression") {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const stateCollection = node.parent.parent
|
|
56
|
+
if (isMachineHandleConfig(stateCollection, bindings)) return true
|
|
57
|
+
if (
|
|
58
|
+
stateCollection.parent.type !== "Property" ||
|
|
59
|
+
propertyName(stateCollection.parent) !== "states" ||
|
|
60
|
+
stateCollection.parent.parent.type !== "ObjectExpression"
|
|
61
|
+
) return false
|
|
62
|
+
return isStateConfig(stateCollection.parent.parent, bindings)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isEventHandlerProperty = (
|
|
66
|
+
node: ESTree.Node,
|
|
67
|
+
bindings: MachineBindings
|
|
68
|
+
): boolean => {
|
|
69
|
+
if (node.type !== "Property" || node.parent.type !== "ObjectExpression") return false
|
|
70
|
+
const onProperty = node.parent.parent
|
|
71
|
+
return onProperty.type === "Property" &&
|
|
72
|
+
propertyName(onProperty) === "on" &&
|
|
73
|
+
onProperty.parent.type === "ObjectExpression" &&
|
|
74
|
+
isStateConfig(onProperty.parent, bindings)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isPropertyPlanningCallback = (
|
|
78
|
+
node: PlanningFunction,
|
|
79
|
+
bindings: MachineBindings
|
|
80
|
+
): boolean => {
|
|
81
|
+
const property = node.parent
|
|
82
|
+
if (
|
|
83
|
+
property.type !== "Property" ||
|
|
84
|
+
property.value !== node ||
|
|
85
|
+
property.parent.type !== "ObjectExpression"
|
|
86
|
+
) return false
|
|
87
|
+
|
|
88
|
+
const name = propertyName(property)
|
|
89
|
+
return name === "initial"
|
|
90
|
+
? isMachineMakeConfig(property.parent, bindings)
|
|
91
|
+
: (name !== undefined && statePlanningProperties.has(name) && isStateConfig(property.parent, bindings)) ||
|
|
92
|
+
isEventHandlerProperty(property, bindings)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const enclosingFunction = (node: ESTree.Node): PlanningFunction | undefined => {
|
|
96
|
+
let current: ESTree.Node | null = node.parent
|
|
97
|
+
while (current !== null && current.type !== "Program") {
|
|
98
|
+
if (
|
|
99
|
+
current.type === "ArrowFunctionExpression" ||
|
|
100
|
+
current.type === "FunctionExpression"
|
|
101
|
+
) return current
|
|
102
|
+
current = current.parent
|
|
103
|
+
}
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const isPlanningCallback = (
|
|
108
|
+
node: PlanningFunction,
|
|
109
|
+
bindings: MachineBindings
|
|
110
|
+
): boolean => {
|
|
111
|
+
if (isPropertyPlanningCallback(node, bindings)) return true
|
|
112
|
+
|
|
113
|
+
const parent = node.parent
|
|
114
|
+
if (parent.type === "CallExpression" && parent.arguments.includes(node)) {
|
|
115
|
+
const method = parent.callee.type === "MemberExpression"
|
|
116
|
+
? staticMemberName(parent.callee)
|
|
117
|
+
: undefined
|
|
118
|
+
if (method !== undefined && directPlanningMethods.has(method)) {
|
|
119
|
+
const owner = enclosingFunction(parent)
|
|
120
|
+
return owner !== undefined && isPlanningCallback(owner, bindings)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Rule } from "@oxlint/plugins"
|
|
2
|
+
import { hasMachineImport, makeMachineBindings, recordMachineDefinition, recordMachineImport } from "../imports.js"
|
|
3
|
+
import { isPlanningCallback, type PlanningFunction } from "../planning.js"
|
|
4
|
+
|
|
5
|
+
export const noAsyncPlanningCallback: Rule = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Keep Effect Machine planning callbacks synchronous.",
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
asyncPlanning: "Planning callbacks must be synchronous. Move asynchronous work into state-owned invocation."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const bindings = makeMachineBindings()
|
|
19
|
+
const inspect = (node: PlanningFunction): void => {
|
|
20
|
+
if (
|
|
21
|
+
hasMachineImport(bindings) &&
|
|
22
|
+
node.async &&
|
|
23
|
+
isPlanningCallback(node, bindings)
|
|
24
|
+
) {
|
|
25
|
+
context.report({ node, messageId: "asyncPlanning" })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
ImportDeclaration: (node) => recordMachineImport(bindings, node),
|
|
30
|
+
VariableDeclarator: (node) => recordMachineDefinition(bindings, node),
|
|
31
|
+
ArrowFunctionExpression: inspect,
|
|
32
|
+
FunctionExpression: inspect
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ESTree, Rule } from "@oxlint/plugins"
|
|
2
|
+
import {
|
|
3
|
+
hasMachineImport,
|
|
4
|
+
makeMachineBindings,
|
|
5
|
+
recordMachineDefinition,
|
|
6
|
+
recordMachineImport,
|
|
7
|
+
staticMemberName
|
|
8
|
+
} from "../imports.js"
|
|
9
|
+
import { isPlanningCallback } from "../planning.js"
|
|
10
|
+
|
|
11
|
+
const returnedExpression = (
|
|
12
|
+
node: ESTree.ArrowFunctionExpression | ESTree.Function
|
|
13
|
+
): ESTree.Expression | null | undefined => {
|
|
14
|
+
if (node.body === null) return undefined
|
|
15
|
+
if (node.body.type !== "BlockStatement") return node.body
|
|
16
|
+
if (node.body.body.length !== 1 || node.body.body[0]?.type !== "ReturnStatement") {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
return node.body.body[0].argument
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const targetBinding = (
|
|
23
|
+
node: ESTree.ArrowFunctionExpression | ESTree.Function
|
|
24
|
+
): string | undefined => {
|
|
25
|
+
if (node.params.length !== 1) return undefined
|
|
26
|
+
const parameter = node.params[0]
|
|
27
|
+
if (parameter?.type !== "ObjectPattern" || parameter.properties.length !== 1) {
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
const property = parameter.properties[0]
|
|
31
|
+
if (
|
|
32
|
+
property?.type !== "Property" ||
|
|
33
|
+
property.computed ||
|
|
34
|
+
property.key.type !== "Identifier" ||
|
|
35
|
+
property.key.name !== "target" ||
|
|
36
|
+
property.value.type !== "Identifier"
|
|
37
|
+
) return undefined
|
|
38
|
+
return property.value.name
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isDefaultTargetConstruction = (
|
|
42
|
+
node: ESTree.Expression | null | undefined,
|
|
43
|
+
binding: string
|
|
44
|
+
): boolean =>
|
|
45
|
+
node?.type === "CallExpression" &&
|
|
46
|
+
node.arguments.length === 0 &&
|
|
47
|
+
node.callee.type === "MemberExpression" &&
|
|
48
|
+
staticMemberName(node.callee) === "from" &&
|
|
49
|
+
node.callee.object.type === "Identifier" &&
|
|
50
|
+
node.callee.object.name === binding
|
|
51
|
+
|
|
52
|
+
export const noRedundantResolve: Rule = {
|
|
53
|
+
meta: {
|
|
54
|
+
type: "suggestion",
|
|
55
|
+
docs: {
|
|
56
|
+
description: "Remove resolvers that only apply default target construction.",
|
|
57
|
+
recommended: true
|
|
58
|
+
},
|
|
59
|
+
fixable: "code",
|
|
60
|
+
schema: [],
|
|
61
|
+
messages: {
|
|
62
|
+
redundantResolver: "Remove this resolver. The selected target already applies default construction."
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
create(context) {
|
|
66
|
+
const bindings = makeMachineBindings()
|
|
67
|
+
return {
|
|
68
|
+
ImportDeclaration: (node) => recordMachineImport(bindings, node),
|
|
69
|
+
CallExpression(node) {
|
|
70
|
+
if (
|
|
71
|
+
!hasMachineImport(bindings) ||
|
|
72
|
+
node.arguments.length !== 1 ||
|
|
73
|
+
node.callee.type !== "MemberExpression" ||
|
|
74
|
+
staticMemberName(node.callee) !== "resolve"
|
|
75
|
+
) return
|
|
76
|
+
|
|
77
|
+
const callback = node.arguments[0]
|
|
78
|
+
if (
|
|
79
|
+
callback?.type !== "ArrowFunctionExpression" &&
|
|
80
|
+
callback?.type !== "FunctionExpression"
|
|
81
|
+
) return
|
|
82
|
+
if (callback.async || callback.generator) return
|
|
83
|
+
if (!isPlanningCallback(callback, bindings)) return
|
|
84
|
+
|
|
85
|
+
const binding = targetBinding(callback)
|
|
86
|
+
if (
|
|
87
|
+
binding === undefined ||
|
|
88
|
+
!isDefaultTargetConstruction(returnedExpression(callback), binding)
|
|
89
|
+
) return
|
|
90
|
+
|
|
91
|
+
const receiver = node.callee.object
|
|
92
|
+
if (context.sourceCode.getCommentsInside(node).length === 0) {
|
|
93
|
+
context.report({
|
|
94
|
+
node,
|
|
95
|
+
messageId: "redundantResolver",
|
|
96
|
+
fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(receiver))
|
|
97
|
+
})
|
|
98
|
+
} else {
|
|
99
|
+
context.report({ node, messageId: "redundantResolver" })
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
VariableDeclarator: (node) => recordMachineDefinition(bindings, node)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Context, ESTree, Rule, Variable } from "@oxlint/plugins"
|
|
2
|
+
import {
|
|
3
|
+
hasMachineImport,
|
|
4
|
+
isMachineMakeCall,
|
|
5
|
+
isMemberCall,
|
|
6
|
+
makeMachineBindings,
|
|
7
|
+
recordMachineDefinition,
|
|
8
|
+
recordMachineImport
|
|
9
|
+
} from "../imports.js"
|
|
10
|
+
|
|
11
|
+
interface Candidate {
|
|
12
|
+
readonly declaration: ESTree.VariableDeclarator
|
|
13
|
+
readonly name: ESTree.BindingIdentifier
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isTopLevelPrivateDeclaration = (node: ESTree.VariableDeclarator): boolean =>
|
|
17
|
+
node.parent.type === "VariableDeclaration" && node.parent.parent.type === "Program"
|
|
18
|
+
|
|
19
|
+
const declaredVariable = (
|
|
20
|
+
context: Context,
|
|
21
|
+
candidate: Candidate
|
|
22
|
+
): Variable | undefined =>
|
|
23
|
+
context.sourceCode.getDeclaredVariables(candidate.declaration)
|
|
24
|
+
.find((variable) => variable.name === candidate.name.name)
|
|
25
|
+
|
|
26
|
+
const isOnlyHandleReference = (variable: Variable): boolean => {
|
|
27
|
+
const references = variable.references.filter((reference) => reference.isRead())
|
|
28
|
+
if (references.length !== 1) return false
|
|
29
|
+
|
|
30
|
+
const identifier = references[0]?.identifier
|
|
31
|
+
if (identifier?.parent.type !== "MemberExpression") return false
|
|
32
|
+
const member = identifier.parent
|
|
33
|
+
if (member.object !== identifier || !isMemberCall(member.parent, "handle")) return false
|
|
34
|
+
return member.parent.callee === member
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const preferInlineHandle: Rule = {
|
|
38
|
+
meta: {
|
|
39
|
+
type: "suggestion",
|
|
40
|
+
docs: {
|
|
41
|
+
description: "Chain handle directly from one-use Machine.make definitions.",
|
|
42
|
+
recommended: true
|
|
43
|
+
},
|
|
44
|
+
schema: [],
|
|
45
|
+
messages: {
|
|
46
|
+
inlineHandle:
|
|
47
|
+
"Chain .handle(...) directly from Machine.make(...). This one-use definition adds no reusable model."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
create(context) {
|
|
51
|
+
const bindings = makeMachineBindings()
|
|
52
|
+
const candidates: Array<Candidate> = []
|
|
53
|
+
return {
|
|
54
|
+
ImportDeclaration: (node) => recordMachineImport(bindings, node),
|
|
55
|
+
VariableDeclarator(node) {
|
|
56
|
+
recordMachineDefinition(bindings, node)
|
|
57
|
+
if (
|
|
58
|
+
!hasMachineImport(bindings) ||
|
|
59
|
+
node.id.type !== "Identifier" ||
|
|
60
|
+
node.init?.type !== "CallExpression" ||
|
|
61
|
+
!isTopLevelPrivateDeclaration(node) ||
|
|
62
|
+
!isMachineMakeCall(node.init, bindings)
|
|
63
|
+
) return
|
|
64
|
+
candidates.push({ declaration: node, name: node.id })
|
|
65
|
+
},
|
|
66
|
+
"Program:exit"() {
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const variable = declaredVariable(context, candidate)
|
|
69
|
+
if (variable !== undefined && isOnlyHandleReference(variable)) {
|
|
70
|
+
context.report({ node: candidate.name, messageId: "inlineHandle" })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|