@typeonce/oxlint-plugin-effect-machine 0.26.2 → 0.27.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 (55) hide show
  1. package/README.md +151 -9
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/ambient.d.ts +6 -0
  7. package/dist/internal/ambient.d.ts.map +1 -0
  8. package/dist/internal/ambient.js +118 -0
  9. package/dist/internal/ambient.js.map +1 -0
  10. package/dist/internal/ast.d.ts +7 -0
  11. package/dist/internal/ast.d.ts.map +1 -0
  12. package/dist/internal/ast.js +62 -0
  13. package/dist/internal/ast.js.map +1 -0
  14. package/dist/internal/imports.d.ts +1 -0
  15. package/dist/internal/imports.d.ts.map +1 -1
  16. package/dist/internal/imports.js +19 -9
  17. package/dist/internal/imports.js.map +1 -1
  18. package/dist/internal/planning.d.ts +3 -0
  19. package/dist/internal/planning.d.ts.map +1 -1
  20. package/dist/internal/planning.js +54 -7
  21. package/dist/internal/planning.js.map +1 -1
  22. package/dist/internal/rules/noAsyncPlanningCallback.d.ts.map +1 -1
  23. package/dist/internal/rules/noAsyncPlanningCallback.js +18 -3
  24. package/dist/internal/rules/noAsyncPlanningCallback.js.map +1 -1
  25. package/dist/internal/rules/noBrowserApiInPlanning.d.ts +3 -0
  26. package/dist/internal/rules/noBrowserApiInPlanning.d.ts.map +1 -0
  27. package/dist/internal/rules/noBrowserApiInPlanning.js +54 -0
  28. package/dist/internal/rules/noBrowserApiInPlanning.js.map +1 -0
  29. package/dist/internal/rules/noConflictingInvocationIdentity.d.ts +3 -0
  30. package/dist/internal/rules/noConflictingInvocationIdentity.d.ts.map +1 -0
  31. package/dist/internal/rules/noConflictingInvocationIdentity.js +289 -0
  32. package/dist/internal/rules/noConflictingInvocationIdentity.js.map +1 -0
  33. package/dist/internal/rules/noNondeterministicPlanning.d.ts +3 -0
  34. package/dist/internal/rules/noNondeterministicPlanning.d.ts.map +1 -0
  35. package/dist/internal/rules/noNondeterministicPlanning.js +33 -0
  36. package/dist/internal/rules/noNondeterministicPlanning.js.map +1 -0
  37. package/dist/internal/rules/noRedundantResolve.d.ts.map +1 -1
  38. package/dist/internal/rules/noRedundantResolve.js +41 -15
  39. package/dist/internal/rules/noRedundantResolve.js.map +1 -1
  40. package/dist/recommended.d.ts +3 -0
  41. package/dist/recommended.d.ts.map +1 -1
  42. package/dist/recommended.js +3 -0
  43. package/dist/recommended.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/index.ts +6 -0
  46. package/src/internal/ambient.ts +134 -0
  47. package/src/internal/ast.ts +83 -0
  48. package/src/internal/imports.ts +27 -8
  49. package/src/internal/planning.ts +69 -7
  50. package/src/internal/rules/noAsyncPlanningCallback.ts +21 -4
  51. package/src/internal/rules/noBrowserApiInPlanning.ts +57 -0
  52. package/src/internal/rules/noConflictingInvocationIdentity.ts +343 -0
  53. package/src/internal/rules/noNondeterministicPlanning.ts +39 -0
  54. package/src/internal/rules/noRedundantResolve.ts +48 -16
  55. package/src/recommended.ts +3 -0
@@ -32,6 +32,19 @@ const isDefaultTargetConstruction = (node, binding) => node?.type === "CallExpre
32
32
  staticMemberName(node.callee) === "from" &&
33
33
  node.callee.object.type === "Identifier" &&
34
34
  node.callee.object.name === binding;
35
+ const isEmptyResolver = (node) => node.body?.type === "BlockStatement" && node.body.body.length === 0;
36
+ const isTargetlessReceiver = (node) => node.type === "MemberExpression" && staticMemberName(node) === "none";
37
+ const isReenterOnlyOptions = (node) => {
38
+ if (node?.type !== "ObjectExpression" || node.properties.length !== 1)
39
+ return false;
40
+ const property = node.properties[0];
41
+ return property?.type === "Property" &&
42
+ !property.computed &&
43
+ property.key.type === "Identifier" &&
44
+ property.key.name === "reenter" &&
45
+ property.value.type === "Literal" &&
46
+ property.value.value === true;
47
+ };
35
48
  export const noRedundantResolve = {
36
49
  meta: {
37
50
  type: "suggestion",
@@ -42,7 +55,9 @@ export const noRedundantResolve = {
42
55
  fixable: "code",
43
56
  schema: [],
44
57
  messages: {
45
- redundantResolver: "Remove this resolver. The selected target already applies default construction."
58
+ redundantResolver: "Remove this resolver. The selected target already applies default construction, so use the target selector directly.",
59
+ redundantReenterResolver: "Replace this resolver with .reenter(). It applies the same default construction while explicitly reentering the selected state.",
60
+ redundantTargetlessResolver: "Remove this empty resolver. A targetless transition performs the same work as to.none; use to.none directly."
46
61
  }
47
62
  },
48
63
  create(context) {
@@ -51,7 +66,7 @@ export const noRedundantResolve = {
51
66
  ImportDeclaration: (node) => recordMachineImport(bindings, node),
52
67
  CallExpression(node) {
53
68
  if (!hasMachineImport(bindings) ||
54
- node.arguments.length !== 1 ||
69
+ (node.arguments.length !== 1 && node.arguments.length !== 2) ||
55
70
  node.callee.type !== "MemberExpression" ||
56
71
  staticMemberName(node.callee) !== "resolve")
57
72
  return;
@@ -63,21 +78,32 @@ export const noRedundantResolve = {
63
78
  return;
64
79
  if (!isPlanningCallback(callback, bindings))
65
80
  return;
81
+ const receiver = node.callee.object;
66
82
  const binding = targetBinding(callback);
67
- if (binding === undefined ||
68
- !isDefaultTargetConstruction(returnedExpression(callback), binding))
83
+ const defaultConstruction = binding !== undefined &&
84
+ isDefaultTargetConstruction(returnedExpression(callback), binding);
85
+ const targetless = isTargetlessReceiver(receiver) && isEmptyResolver(callback);
86
+ if (!defaultConstruction && !targetless)
69
87
  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
- }
88
+ const options = node.arguments[1];
89
+ if (options?.type === "SpreadElement")
90
+ return;
91
+ const reenter = options === undefined ? false : isReenterOnlyOptions(options);
92
+ if (options !== undefined && !reenter)
93
+ return;
94
+ const messageId = reenter
95
+ ? "redundantReenterResolver"
96
+ : targetless
97
+ ? "redundantTargetlessResolver"
98
+ : "redundantResolver";
99
+ const replacement = `${context.sourceCode.getText(receiver)}${reenter ? ".reenter()" : ""}`;
100
+ context.report({
101
+ node,
102
+ messageId,
103
+ ...(context.sourceCode.getCommentsInside(node).length === 0
104
+ ? { fix: (fixer) => fixer.replaceText(node, replacement) }
105
+ : undefined)
106
+ });
81
107
  },
82
108
  VariableDeclarator: (node) => recordMachineDefinition(bindings, node)
83
109
  };
@@ -1 +1 @@
1
- {"version":3,"file":"noRedundantResolve.js","sourceRoot":"","sources":["../../../src/internal/rules/noRedundantResolve.ts"],"names":[],"mappings":"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"}
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,eAAe,GAAG,CACtB,IAAsD,EAC7C,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,gBAAgB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAA;AAEjF,MAAM,oBAAoB,GAAG,CAAC,IAAuB,EAAW,EAAE,CAChE,IAAI,CAAC,IAAI,KAAK,kBAAkB,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,MAAM,CAAA;AAEvE,MAAM,oBAAoB,GAAG,CAAC,IAAmC,EAAW,EAAE;IAC5E,IAAI,IAAI,EAAE,IAAI,KAAK,kBAAkB,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACnF,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IACnC,OAAO,QAAQ,EAAE,IAAI,KAAK,UAAU;QAClC,CAAC,QAAQ,CAAC,QAAQ;QAClB,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,YAAY;QAClC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS;QAC/B,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS;QACjC,QAAQ,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAA;AACjC,CAAC,CAAA;AAED,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,EACf,sHAAsH;YACxH,wBAAwB,EACtB,iIAAiI;YACnI,2BAA2B,EACzB,8GAA8G;SACjH;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,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC;oBAC5D,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,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAA;gBACnC,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;gBACvC,MAAM,mBAAmB,GAAG,OAAO,KAAK,SAAS;oBAC/C,2BAA2B,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAA;gBACpE,MAAM,UAAU,GAAG,oBAAoB,CAAC,QAAQ,CAAC,IAAI,eAAe,CAAC,QAAQ,CAAC,CAAA;gBAC9E,IAAI,CAAC,mBAAmB,IAAI,CAAC,UAAU;oBAAE,OAAM;gBAE/C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;gBACjC,IAAI,OAAO,EAAE,IAAI,KAAK,eAAe;oBAAE,OAAM;gBAC7C,MAAM,OAAO,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAA;gBAC7E,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,OAAO;oBAAE,OAAM;gBAE7C,MAAM,SAAS,GAAG,OAAO;oBACvB,CAAC,CAAC,0BAA0B;oBAC5B,CAAC,CAAC,UAAU;wBACZ,CAAC,CAAC,6BAA6B;wBAC/B,CAAC,CAAC,mBAAmB,CAAA;gBACvB,MAAM,WAAW,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;gBAC3F,OAAO,CAAC,MAAM,CAAC;oBACb,IAAI;oBACJ,SAAS;oBACT,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;wBACzD,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE;wBAC1D,CAAC,CAAC,SAAS,CAAC;iBACf,CAAC,CAAA;YACJ,CAAC;YACD,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC;SACtE,CAAA;IACH,CAAC;CACF,CAAA"}
@@ -1,6 +1,9 @@
1
1
  /** Rules recommended for every Effect Machine model. */
2
2
  export declare const recommended: {
3
3
  readonly "effect-machine/no-async-planning-callback": "error";
4
+ readonly "effect-machine/no-browser-api-in-planning": "error";
5
+ readonly "effect-machine/no-conflicting-invocation-identity": "error";
6
+ readonly "effect-machine/no-nondeterministic-planning": "error";
4
7
  readonly "effect-machine/no-redundant-resolve": "error";
5
8
  readonly "effect-machine/prefer-inline-handle": "error";
6
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"recommended.d.ts","sourceRoot":"","sources":["../src/recommended.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,eAAO,MAAM,WAAW;;;;CAId,CAAA"}
1
+ {"version":3,"file":"recommended.d.ts","sourceRoot":"","sources":["../src/recommended.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,eAAO,MAAM,WAAW;;;;;;;CAOd,CAAA"}
@@ -1,6 +1,9 @@
1
1
  /** Rules recommended for every Effect Machine model. */
2
2
  export const recommended = {
3
3
  "effect-machine/no-async-planning-callback": "error",
4
+ "effect-machine/no-browser-api-in-planning": "error",
5
+ "effect-machine/no-conflicting-invocation-identity": "error",
6
+ "effect-machine/no-nondeterministic-planning": "error",
4
7
  "effect-machine/no-redundant-resolve": "error",
5
8
  "effect-machine/prefer-inline-handle": "error"
6
9
  };
@@ -1 +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"}
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,2CAA2C,EAAE,OAAO;IACpD,mDAAmD,EAAE,OAAO;IAC5D,6CAA6C,EAAE,OAAO;IACtD,qCAAqC,EAAE,OAAO;IAC9C,qCAAqC,EAAE,OAAO;CACtC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/oxlint-plugin-effect-machine",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "description": "Oxlint rules for Effect Machine models",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { Plugin } from "@oxlint/plugins"
2
2
  import { noAsyncPlanningCallback } from "./internal/rules/noAsyncPlanningCallback.js"
3
+ import { noBrowserApiInPlanning } from "./internal/rules/noBrowserApiInPlanning.js"
4
+ import { noConflictingInvocationIdentity } from "./internal/rules/noConflictingInvocationIdentity.js"
5
+ import { noNondeterministicPlanning } from "./internal/rules/noNondeterministicPlanning.js"
3
6
  import { noRedundantResolve } from "./internal/rules/noRedundantResolve.js"
4
7
  import { preferInlineHandle } from "./internal/rules/preferInlineHandle.js"
5
8
 
@@ -9,6 +12,9 @@ const plugin = {
9
12
  },
10
13
  rules: {
11
14
  "no-async-planning-callback": noAsyncPlanningCallback,
15
+ "no-browser-api-in-planning": noBrowserApiInPlanning,
16
+ "no-conflicting-invocation-identity": noConflictingInvocationIdentity,
17
+ "no-nondeterministic-planning": noNondeterministicPlanning,
12
18
  "no-redundant-resolve": noRedundantResolve,
13
19
  "prefer-inline-handle": preferInlineHandle
14
20
  }
@@ -0,0 +1,134 @@
1
+ import type { Context, ESTree } from "@oxlint/plugins"
2
+ import { canonicalGlobalPath } from "./ast.js"
3
+
4
+ const scheduledFunctions = new Set([
5
+ "fetch",
6
+ "queueMicrotask",
7
+ "requestAnimationFrame",
8
+ "requestIdleCallback",
9
+ "setImmediate",
10
+ "setInterval",
11
+ "setTimeout"
12
+ ])
13
+
14
+ const promiseMethods = new Set([
15
+ "all",
16
+ "allSettled",
17
+ "any",
18
+ "race",
19
+ "reject",
20
+ "resolve",
21
+ "try"
22
+ ])
23
+
24
+ export const asyncOperation = (
25
+ context: Context,
26
+ node: ESTree.CallExpression | ESTree.NewExpression
27
+ ): string | undefined => {
28
+ const path = canonicalGlobalPath(context, node.callee)
29
+ if (path === undefined) return undefined
30
+ if (node.type === "NewExpression") {
31
+ return path.length === 1 && path[0] === "Promise" ? "new Promise(...)" : undefined
32
+ }
33
+ if (path.length === 1 && scheduledFunctions.has(path[0]!)) return `${path[0]}(...)`
34
+ if (path.length === 2 && path[0] === "Promise" && promiseMethods.has(path[1]!)) {
35
+ return `Promise.${path[1]}(...)`
36
+ }
37
+ return path.length === 2 && path[0] === "process" && path[1] === "nextTick"
38
+ ? "process.nextTick(...)"
39
+ : undefined
40
+ }
41
+
42
+ export const nondeterministicOperation = (
43
+ context: Context,
44
+ node: ESTree.CallExpression | ESTree.NewExpression
45
+ ): string | undefined => {
46
+ const path = canonicalGlobalPath(context, node.callee)
47
+ if (path === undefined) return undefined
48
+ if (node.type === "NewExpression") {
49
+ return path.length === 1 && path[0] === "Date" && node.arguments.length === 0
50
+ ? "new Date()"
51
+ : undefined
52
+ }
53
+ if (path.length === 1 && path[0] === "Date") return "Date()"
54
+ if (path.length === 2 && path[0] === "Date" && path[1] === "now") return "Date.now()"
55
+ if (path.length === 2 && path[0] === "Math" && path[1] === "random") return "Math.random()"
56
+ if (
57
+ path.length === 2 &&
58
+ path[0] === "crypto" &&
59
+ (path[1] === "getRandomValues" || path[1] === "randomUUID")
60
+ ) return `crypto.${path[1]}(...)`
61
+ if (path.length === 2 && path[0] === "performance" && path[1] === "now") {
62
+ return "performance.now()"
63
+ }
64
+ if (path.length >= 3 && path[0] === "Temporal" && path[1] === "Now") {
65
+ return `${path.join(".")}()`
66
+ }
67
+ if (
68
+ path[0] === "process" &&
69
+ (path.join(".") === "process.hrtime" ||
70
+ path.join(".") === "process.hrtime.bigint" ||
71
+ path.join(".") === "process.uptime")
72
+ ) return `${path.join(".")}()`
73
+ return undefined
74
+ }
75
+
76
+ export const nondeterministicProperty = (
77
+ context: Context,
78
+ node: ESTree.MemberExpression
79
+ ): string | undefined => {
80
+ const path = canonicalGlobalPath(context, node)
81
+ return path?.length === 2 && path[0] === "performance" && path[1] === "timeOrigin"
82
+ ? "performance.timeOrigin"
83
+ : undefined
84
+ }
85
+
86
+ const browserRoots = new Set([
87
+ "caches",
88
+ "cookieStore",
89
+ "document",
90
+ "history",
91
+ "indexedDB",
92
+ "localStorage",
93
+ "location",
94
+ "navigator",
95
+ "screen",
96
+ "self",
97
+ "sessionStorage",
98
+ "visualViewport",
99
+ "window"
100
+ ])
101
+
102
+ const browserFunctions = new Set([
103
+ "addEventListener",
104
+ "alert",
105
+ "confirm",
106
+ "dispatchEvent",
107
+ "getComputedStyle",
108
+ "matchMedia",
109
+ "open",
110
+ "postMessage",
111
+ "prompt",
112
+ "removeEventListener"
113
+ ])
114
+
115
+ const browserConstructors = new Set([
116
+ "BroadcastChannel",
117
+ "EventSource",
118
+ "SharedWorker",
119
+ "WebSocket",
120
+ "Worker",
121
+ "XMLHttpRequest"
122
+ ])
123
+
124
+ export const browserApi = (
125
+ context: Context,
126
+ node: ESTree.Expression
127
+ ): string | undefined => {
128
+ const path = canonicalGlobalPath(context, node)
129
+ if (path === undefined || path.length === 0) return undefined
130
+ if (browserRoots.has(path[0]!) || browserFunctions.has(path[0]!) || browserConstructors.has(path[0]!)) {
131
+ return path.join(".")
132
+ }
133
+ return undefined
134
+ }
@@ -0,0 +1,83 @@
1
+ import type { Context, ESTree, Scope, Variable } from "@oxlint/plugins"
2
+ import { staticMemberName } from "./imports.js"
3
+
4
+ export const unwrapExpression = (node: ESTree.Expression): ESTree.Expression => {
5
+ let current = node
6
+ while (
7
+ current.type === "ChainExpression" ||
8
+ current.type === "ParenthesizedExpression" ||
9
+ current.type === "TSAsExpression" ||
10
+ current.type === "TSNonNullExpression" ||
11
+ current.type === "TSSatisfiesExpression" ||
12
+ current.type === "TSTypeAssertion"
13
+ ) current = current.expression
14
+ return current
15
+ }
16
+
17
+ const referenceIn = (
18
+ scope: Scope,
19
+ node: ESTree.IdentifierReference
20
+ ) =>
21
+ scope.references.find((reference) => reference.identifier === node) ??
22
+ scope.through.find((reference) => reference.identifier === node)
23
+
24
+ export const resolvedVariable = (
25
+ context: Context,
26
+ node: ESTree.IdentifierReference
27
+ ): Variable | undefined => {
28
+ let scope: Scope | null = context.sourceCode.getScope(node)
29
+ while (scope !== null) {
30
+ const reference = referenceIn(scope, node)
31
+ if (reference !== undefined) return reference.resolved ?? undefined
32
+ scope = scope.upper
33
+ }
34
+ return undefined
35
+ }
36
+
37
+ export const isUnshadowedGlobal = (
38
+ context: Context,
39
+ node: ESTree.Expression,
40
+ name: string
41
+ ): node is ESTree.IdentifierReference => {
42
+ const expression = unwrapExpression(node)
43
+ if (expression.type !== "Identifier" || expression.name !== name) return false
44
+ if (context.sourceCode.isGlobalReference(expression)) return true
45
+
46
+ let scope: Scope | null = context.sourceCode.getScope(expression)
47
+ while (scope !== null) {
48
+ const reference = referenceIn(scope, expression)
49
+ if (reference !== undefined) return reference.resolved === null
50
+ scope = scope.upper
51
+ }
52
+ return false
53
+ }
54
+
55
+ export const globalPath = (
56
+ context: Context,
57
+ node: ESTree.Expression
58
+ ): ReadonlyArray<string> | undefined => {
59
+ const expression = unwrapExpression(node)
60
+ if (expression.type === "Identifier") {
61
+ return isUnshadowedGlobal(context, expression, expression.name)
62
+ ? [expression.name]
63
+ : undefined
64
+ }
65
+ if (expression.type !== "MemberExpression") return undefined
66
+
67
+ const member = staticMemberName(expression)
68
+ if (member === undefined) return undefined
69
+ const owner = globalPath(context, expression.object)
70
+ return owner === undefined ? undefined : [...owner, member]
71
+ }
72
+
73
+ const ambientQualifiers = new Set(["globalThis", "self", "window"])
74
+
75
+ export const canonicalGlobalPath = (
76
+ context: Context,
77
+ node: ESTree.Expression
78
+ ): ReadonlyArray<string> | undefined => {
79
+ const path = globalPath(context, node)
80
+ return path !== undefined && path.length > 1 && ambientQualifiers.has(path[0]!)
81
+ ? path.slice(1)
82
+ : path
83
+ }
@@ -37,12 +37,16 @@ export const recordMachineImport = (
37
37
  export const hasMachineImport = (bindings: MachineBindings): boolean =>
38
38
  bindings.machine.size > 0 || bindings.namespaces.size > 0
39
39
 
40
- export const staticMemberName = (node: ESTree.Node): string | undefined =>
41
- node.type === "MemberExpression" &&
42
- !node.computed &&
43
- node.property.type === "Identifier"
40
+ export const staticMemberName = (node: ESTree.Node): string | undefined => {
41
+ if (node.type !== "MemberExpression") return undefined
42
+ return node.computed
43
+ ? node.property.type === "Literal" && typeof node.property.value === "string"
44
+ ? node.property.value
45
+ : undefined
46
+ : node.property.type === "Identifier"
44
47
  ? node.property.name
45
48
  : undefined
49
+ }
46
50
 
47
51
  const isNamespaceMachine = (
48
52
  node: ESTree.Node,
@@ -55,6 +59,24 @@ const isNamespaceMachine = (
55
59
  node.property.type === "Identifier" &&
56
60
  node.property.name === "Machine"
57
61
 
62
+ const isMachineReference = (
63
+ node: ESTree.Node,
64
+ bindings: MachineBindings
65
+ ): boolean =>
66
+ node.type === "Identifier"
67
+ ? bindings.machine.has(node.name)
68
+ : isNamespaceMachine(node, bindings)
69
+
70
+ export const isMachineMemberCall = (
71
+ node: ESTree.Node,
72
+ member: string,
73
+ bindings: MachineBindings
74
+ ): node is ESTree.CallExpression =>
75
+ node.type === "CallExpression" &&
76
+ node.callee.type === "MemberExpression" &&
77
+ staticMemberName(node.callee) === member &&
78
+ isMachineReference(node.callee.object, bindings)
79
+
58
80
  export const isMachineMakeCall = (
59
81
  node: ESTree.CallExpression,
60
82
  bindings: MachineBindings
@@ -64,10 +86,7 @@ export const isMachineMakeCall = (
64
86
  staticMemberName(node.callee) !== "make"
65
87
  ) return false
66
88
 
67
- const receiver = node.callee.object
68
- return receiver.type === "Identifier"
69
- ? bindings.machine.has(receiver.name)
70
- : isNamespaceMachine(receiver, bindings)
89
+ return isMachineReference(node.callee.object, bindings)
71
90
  }
72
91
 
73
92
  export const recordMachineDefinition = (
@@ -1,4 +1,5 @@
1
1
  import type { ESTree } from "@oxlint/plugins"
2
+ import { unwrapExpression } from "./ast.js"
2
3
  import { isMachineHandleCall, isMachineMakeCall, type MachineBindings, staticMemberName } from "./imports.js"
3
4
 
4
5
  export type PlanningFunction = ESTree.ArrowFunctionExpression | ESTree.Function
@@ -16,7 +17,10 @@ const statePlanningProperties = new Set([
16
17
  "choice",
17
18
  "entry",
18
19
  "exit",
19
- "invoke"
20
+ "initialize",
21
+ "invoke",
22
+ "onDone",
23
+ "output"
20
24
  ])
21
25
 
22
26
  const propertyName = (node: ESTree.Node): string | undefined => {
@@ -74,6 +78,26 @@ const isEventHandlerProperty = (
74
78
  isStateConfig(onProperty.parent, bindings)
75
79
  }
76
80
 
81
+ const isHistoryDefaultProperty = (
82
+ node: ESTree.Node,
83
+ bindings: MachineBindings
84
+ ): boolean => {
85
+ if (node.type !== "Property" || propertyName(node) !== "default") return false
86
+ const historyEntry = node.parent
87
+ if (
88
+ historyEntry.type !== "ObjectExpression" ||
89
+ historyEntry.parent.type !== "Property" ||
90
+ historyEntry.parent.parent.type !== "ObjectExpression"
91
+ ) return false
92
+ const historyEntries = historyEntry.parent.parent
93
+ if (
94
+ historyEntries.parent.type !== "Property" ||
95
+ propertyName(historyEntries.parent) !== "history" ||
96
+ historyEntries.parent.parent.type !== "ObjectExpression"
97
+ ) return false
98
+ return isStateConfig(historyEntries.parent.parent, bindings)
99
+ }
100
+
77
101
  const isPropertyPlanningCallback = (
78
102
  node: PlanningFunction,
79
103
  bindings: MachineBindings
@@ -89,10 +113,11 @@ const isPropertyPlanningCallback = (
89
113
  return name === "initial"
90
114
  ? isMachineMakeConfig(property.parent, bindings)
91
115
  : (name !== undefined && statePlanningProperties.has(name) && isStateConfig(property.parent, bindings)) ||
92
- isEventHandlerProperty(property, bindings)
116
+ isEventHandlerProperty(property, bindings) ||
117
+ isHistoryDefaultProperty(property, bindings)
93
118
  }
94
119
 
95
- const enclosingFunction = (node: ESTree.Node): PlanningFunction | undefined => {
120
+ export const enclosingFunction = (node: ESTree.Node): PlanningFunction | undefined => {
96
121
  let current: ESTree.Node | null = node.parent
97
122
  while (current !== null && current.type !== "Program") {
98
123
  if (
@@ -104,6 +129,40 @@ const enclosingFunction = (node: ESTree.Node): PlanningFunction | undefined => {
104
129
  return undefined
105
130
  }
106
131
 
132
+ const selectorRoot = (node: ESTree.Expression): string | undefined => {
133
+ let expression = unwrapExpression(node)
134
+ while (expression.type === "CallExpression" || expression.type === "MemberExpression") {
135
+ expression = unwrapExpression(
136
+ expression.type === "CallExpression"
137
+ ? expression.callee
138
+ : expression.object
139
+ )
140
+ }
141
+ return expression.type === "Identifier" ? expression.name : undefined
142
+ }
143
+
144
+ export const enclosingPlanningCallback = (
145
+ node: ESTree.Node,
146
+ bindings: MachineBindings
147
+ ): PlanningFunction | undefined => {
148
+ const callback = enclosingFunction(node)
149
+ return callback !== undefined && isPlanningCallback(callback, bindings)
150
+ ? callback
151
+ : undefined
152
+ }
153
+
154
+ export const isInvokePlanningCallback = (
155
+ node: PlanningFunction,
156
+ bindings: MachineBindings
157
+ ): boolean => {
158
+ const property = node.parent
159
+ return property.type === "Property" &&
160
+ property.value === node &&
161
+ propertyName(property) === "invoke" &&
162
+ property.parent.type === "ObjectExpression" &&
163
+ isStateConfig(property.parent, bindings)
164
+ }
165
+
107
166
  export const isPlanningCallback = (
108
167
  node: PlanningFunction,
109
168
  bindings: MachineBindings
@@ -112,12 +171,15 @@ export const isPlanningCallback = (
112
171
 
113
172
  const parent = node.parent
114
173
  if (parent.type === "CallExpression" && parent.arguments.includes(node)) {
115
- const method = parent.callee.type === "MemberExpression"
116
- ? staticMemberName(parent.callee)
117
- : undefined
174
+ if (parent.callee.type !== "MemberExpression") return false
175
+ const method = staticMemberName(parent.callee)
118
176
  if (method !== undefined && directPlanningMethods.has(method)) {
119
177
  const owner = enclosingFunction(parent)
120
- return owner !== undefined && isPlanningCallback(owner, bindings)
178
+ const selector = owner?.params[0]
179
+ return owner !== undefined &&
180
+ selector?.type === "Identifier" &&
181
+ selectorRoot(parent.callee.object) === selector.name &&
182
+ isPlanningCallback(owner, bindings)
121
183
  }
122
184
  }
123
185
  return false
@@ -1,6 +1,7 @@
1
- import type { Rule } from "@oxlint/plugins"
1
+ import type { ESTree, Rule } from "@oxlint/plugins"
2
+ import { asyncOperation } from "../ambient.js"
2
3
  import { hasMachineImport, makeMachineBindings, recordMachineDefinition, recordMachineImport } from "../imports.js"
3
- import { isPlanningCallback, type PlanningFunction } from "../planning.js"
4
+ import { enclosingPlanningCallback, isPlanningCallback, type PlanningFunction } from "../planning.js"
4
5
 
5
6
  export const noAsyncPlanningCallback: Rule = {
6
7
  meta: {
@@ -11,7 +12,10 @@ export const noAsyncPlanningCallback: Rule = {
11
12
  },
12
13
  schema: [],
13
14
  messages: {
14
- asyncPlanning: "Planning callbacks must be synchronous. Move asynchronous work into state-owned invocation."
15
+ asyncOperation:
16
+ "{{operation}} starts asynchronous work during synchronous planning. Move it into the owning state's invoke declaration using from.effect(...), from.stream(...), or from.timer(...), then handle completion or failure with onDone/onFailure.",
17
+ asyncPlanning:
18
+ "Planning callbacks must be synchronous. Remove async and move the asynchronous work into the owning state's invoke declaration, then transition from onDone/onFailure."
15
19
  }
16
20
  },
17
21
  create(context) {
@@ -25,11 +29,24 @@ export const noAsyncPlanningCallback: Rule = {
25
29
  context.report({ node, messageId: "asyncPlanning" })
26
30
  }
27
31
  }
32
+ const inspectOperation = (
33
+ node: ESTree.CallExpression | ESTree.NewExpression
34
+ ): void => {
35
+ if (!hasMachineImport(bindings)) return
36
+ const callback = enclosingPlanningCallback(node, bindings)
37
+ if (callback === undefined || callback.async) return
38
+ const operation = asyncOperation(context, node)
39
+ if (operation !== undefined) {
40
+ context.report({ node, messageId: "asyncOperation", data: { operation } })
41
+ }
42
+ }
28
43
  return {
29
44
  ImportDeclaration: (node) => recordMachineImport(bindings, node),
30
45
  VariableDeclarator: (node) => recordMachineDefinition(bindings, node),
31
46
  ArrowFunctionExpression: inspect,
32
- FunctionExpression: inspect
47
+ FunctionExpression: inspect,
48
+ CallExpression: inspectOperation,
49
+ NewExpression: inspectOperation
33
50
  }
34
51
  }
35
52
  }
@@ -0,0 +1,57 @@
1
+ import type { ESTree, Rule } from "@oxlint/plugins"
2
+ import { browserApi } from "../ambient.js"
3
+ import { hasMachineImport, makeMachineBindings, recordMachineDefinition, recordMachineImport } from "../imports.js"
4
+ import { enclosingPlanningCallback } from "../planning.js"
5
+
6
+ const isOutermostAccess = (node: ESTree.Expression): boolean => {
7
+ const parent = node.parent
8
+ if (parent.type === "MemberExpression" && parent.object === node) return false
9
+ if (
10
+ (parent.type === "CallExpression" || parent.type === "NewExpression") &&
11
+ parent.callee === node
12
+ ) return false
13
+ return true
14
+ }
15
+
16
+ export const noBrowserApiInPlanning: Rule = {
17
+ meta: {
18
+ type: "problem",
19
+ docs: {
20
+ description: "Keep browser state and side effects out of Effect Machine planning.",
21
+ recommended: true
22
+ },
23
+ schema: [],
24
+ messages: {
25
+ browserApi:
26
+ "{{api}} reads browser state or performs a browser side effect during synchronous planning. If it affects the workflow, run it in a state-owned invocation and transition from its outcome. If it only affects presentation, keep it in the UI adapter."
27
+ }
28
+ },
29
+ create(context) {
30
+ const bindings = makeMachineBindings()
31
+ const inspect = (node: ESTree.Expression): void => {
32
+ if (
33
+ !hasMachineImport(bindings) ||
34
+ !isOutermostAccess(node) ||
35
+ enclosingPlanningCallback(node, bindings) === undefined
36
+ ) return
37
+ const api = browserApi(context, node)
38
+ if (api !== undefined) context.report({ node, messageId: "browserApi", data: { api } })
39
+ }
40
+ const inspectOperation = (node: ESTree.CallExpression | ESTree.NewExpression): void => {
41
+ if (
42
+ !hasMachineImport(bindings) ||
43
+ enclosingPlanningCallback(node, bindings) === undefined
44
+ ) return
45
+ const api = browserApi(context, node.callee)
46
+ if (api !== undefined) context.report({ node, messageId: "browserApi", data: { api } })
47
+ }
48
+ return {
49
+ ImportDeclaration: (node) => recordMachineImport(bindings, node),
50
+ Identifier: (node) => inspect(node as ESTree.IdentifierReference),
51
+ MemberExpression: inspect,
52
+ CallExpression: inspectOperation,
53
+ NewExpression: inspectOperation,
54
+ VariableDeclarator: (node) => recordMachineDefinition(bindings, node)
55
+ }
56
+ }
57
+ }