@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +2 -0
  3. package/README.md +95 -0
  4. package/dist/index.d.ts +13 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +16 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/internal/imports.d.ts +15 -0
  9. package/dist/internal/imports.d.ts.map +1 -0
  10. package/dist/internal/imports.js +60 -0
  11. package/dist/internal/imports.js.map +1 -0
  12. package/dist/internal/planning.d.ts +5 -0
  13. package/dist/internal/planning.d.ts.map +1 -0
  14. package/dist/internal/planning.js +87 -0
  15. package/dist/internal/planning.js.map +1 -0
  16. package/dist/internal/rules/noAsyncPlanningCallback.d.ts +3 -0
  17. package/dist/internal/rules/noAsyncPlanningCallback.d.ts.map +1 -0
  18. package/dist/internal/rules/noAsyncPlanningCallback.js +32 -0
  19. package/dist/internal/rules/noAsyncPlanningCallback.js.map +1 -0
  20. package/dist/internal/rules/noRedundantResolve.d.ts +3 -0
  21. package/dist/internal/rules/noRedundantResolve.d.ts.map +1 -0
  22. package/dist/internal/rules/noRedundantResolve.js +86 -0
  23. package/dist/internal/rules/noRedundantResolve.js.map +1 -0
  24. package/dist/internal/rules/preferInlineHandle.d.ts +3 -0
  25. package/dist/internal/rules/preferInlineHandle.d.ts.map +1 -0
  26. package/dist/internal/rules/preferInlineHandle.js +55 -0
  27. package/dist/internal/rules/preferInlineHandle.js.map +1 -0
  28. package/dist/recommended.d.ts +7 -0
  29. package/dist/recommended.d.ts.map +1 -0
  30. package/dist/recommended.js +7 -0
  31. package/dist/recommended.js.map +1 -0
  32. package/package.json +74 -0
  33. package/src/index.ts +19 -0
  34. package/src/internal/imports.ts +105 -0
  35. package/src/internal/planning.ts +124 -0
  36. package/src/internal/rules/noAsyncPlanningCallback.ts +35 -0
  37. package/src/internal/rules/noRedundantResolve.ts +105 -0
  38. package/src/internal/rules/preferInlineHandle.ts +76 -0
  39. 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
@@ -0,0 +1,2 @@
1
+ Effect Machine Oxlint Plugin is part of the Effect Machine project and is
2
+ distributed under the MIT License.
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.
@@ -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,3 @@
1
+ import type { Rule } from "@oxlint/plugins";
2
+ export declare const noAsyncPlanningCallback: Rule;
3
+ //# sourceMappingURL=noAsyncPlanningCallback.d.ts.map
@@ -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,3 @@
1
+ import type { Rule } from "@oxlint/plugins";
2
+ export declare const noRedundantResolve: Rule;
3
+ //# sourceMappingURL=noRedundantResolve.d.ts.map
@@ -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,3 @@
1
+ import type { Rule } from "@oxlint/plugins";
2
+ export declare const preferInlineHandle: Rule;
3
+ //# sourceMappingURL=preferInlineHandle.d.ts.map
@@ -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
+ }
@@ -0,0 +1,6 @@
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
+ } as const