@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.
- package/README.md +151 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/ambient.d.ts +6 -0
- package/dist/internal/ambient.d.ts.map +1 -0
- package/dist/internal/ambient.js +118 -0
- package/dist/internal/ambient.js.map +1 -0
- package/dist/internal/ast.d.ts +7 -0
- package/dist/internal/ast.d.ts.map +1 -0
- package/dist/internal/ast.js +62 -0
- package/dist/internal/ast.js.map +1 -0
- package/dist/internal/imports.d.ts +1 -0
- package/dist/internal/imports.d.ts.map +1 -1
- package/dist/internal/imports.js +19 -9
- package/dist/internal/imports.js.map +1 -1
- package/dist/internal/planning.d.ts +3 -0
- package/dist/internal/planning.d.ts.map +1 -1
- package/dist/internal/planning.js +54 -7
- package/dist/internal/planning.js.map +1 -1
- package/dist/internal/rules/noAsyncPlanningCallback.d.ts.map +1 -1
- package/dist/internal/rules/noAsyncPlanningCallback.js +18 -3
- package/dist/internal/rules/noAsyncPlanningCallback.js.map +1 -1
- package/dist/internal/rules/noBrowserApiInPlanning.d.ts +3 -0
- package/dist/internal/rules/noBrowserApiInPlanning.d.ts.map +1 -0
- package/dist/internal/rules/noBrowserApiInPlanning.js +54 -0
- package/dist/internal/rules/noBrowserApiInPlanning.js.map +1 -0
- package/dist/internal/rules/noConflictingInvocationIdentity.d.ts +3 -0
- package/dist/internal/rules/noConflictingInvocationIdentity.d.ts.map +1 -0
- package/dist/internal/rules/noConflictingInvocationIdentity.js +289 -0
- package/dist/internal/rules/noConflictingInvocationIdentity.js.map +1 -0
- package/dist/internal/rules/noNondeterministicPlanning.d.ts +3 -0
- package/dist/internal/rules/noNondeterministicPlanning.d.ts.map +1 -0
- package/dist/internal/rules/noNondeterministicPlanning.js +33 -0
- package/dist/internal/rules/noNondeterministicPlanning.js.map +1 -0
- package/dist/internal/rules/noRedundantResolve.d.ts.map +1 -1
- package/dist/internal/rules/noRedundantResolve.js +41 -15
- package/dist/internal/rules/noRedundantResolve.js.map +1 -1
- package/dist/recommended.d.ts +3 -0
- package/dist/recommended.d.ts.map +1 -1
- package/dist/recommended.js +3 -0
- package/dist/recommended.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/internal/ambient.ts +134 -0
- package/src/internal/ast.ts +83 -0
- package/src/internal/imports.ts +27 -8
- package/src/internal/planning.ts +69 -7
- package/src/internal/rules/noAsyncPlanningCallback.ts +21 -4
- package/src/internal/rules/noBrowserApiInPlanning.ts +57 -0
- package/src/internal/rules/noConflictingInvocationIdentity.ts +343 -0
- package/src/internal/rules/noNondeterministicPlanning.ts +39 -0
- package/src/internal/rules/noRedundantResolve.ts +48 -16
- 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
|
-
|
|
68
|
-
|
|
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
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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":"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,
|
|
1
|
+
{"version":3,"file":"noRedundantResolve.js","sourceRoot":"","sources":["../../../src/internal/rules/noRedundantResolve.ts"],"names":[],"mappings":"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"}
|
package/dist/recommended.d.ts
CHANGED
|
@@ -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
|
|
1
|
+
{"version":3,"file":"recommended.d.ts","sourceRoot":"","sources":["../src/recommended.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,eAAO,MAAM,WAAW;;;;;;;CAOd,CAAA"}
|
package/dist/recommended.js
CHANGED
|
@@ -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
|
};
|
package/dist/recommended.js.map
CHANGED
|
@@ -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
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
|
+
}
|
package/src/internal/imports.ts
CHANGED
|
@@ -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
|
|
42
|
-
|
|
43
|
-
node.property.type === "
|
|
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
|
-
|
|
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 = (
|
package/src/internal/planning.ts
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|