oxlint-plugin-effect 0.13.0 → 0.15.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 +26 -14
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/dist/presets/recommended.d.ts +10 -0
- package/dist/presets/recommended.js +11 -1
- package/dist/rules/_anti-slop-array-method.js +78 -0
- package/dist/rules/_anti-slop-dictionary-types.js +198 -150
- package/dist/rules/_anti-slop-function-parameters.js +30 -0
- package/dist/rules/_anti-slop-lexical-type-parameters.js +25 -0
- package/dist/rules/_anti-slop-reflect-method.js +25 -0
- package/dist/rules/_anti-slop-scope.js +15 -0
- package/dist/rules/_anti-slop-type-alias-resolution.js +139 -0
- package/dist/rules/_ast-ancestors.js +14 -0
- package/dist/rules/_ast-children.js +18 -0
- package/dist/rules/_call-arguments.js +11 -0
- package/dist/rules/_effect-context.js +25 -20
- package/dist/rules/_function-metrics.js +94 -75
- package/dist/rules/_tagged-control-flow.js +38 -60
- package/dist/rules/_test-apis.js +47 -0
- package/dist/rules/_test-files.js +8 -0
- package/dist/rules/index.d.ts +11 -1
- package/dist/rules/index.js +21 -1
- package/dist/rules/no-array-filter-map.d.ts +7 -0
- package/dist/rules/no-array-filter-map.js +49 -0
- package/dist/rules/no-chained-type-assertions.js +7 -4
- package/dist/rules/no-conditional-empty-object-spread.d.ts +1 -1
- package/dist/rules/no-conditional-empty-object-spread.js +7 -47
- package/dist/rules/no-dynamic-imports.js +33 -25
- package/dist/rules/no-eager-acquire.d.ts +6 -0
- package/dist/rules/no-eager-acquire.js +98 -0
- package/dist/rules/no-effect-run-in-tests.d.ts +6 -0
- package/dist/rules/no-effect-run-in-tests.js +60 -0
- package/dist/rules/no-globals.js +42 -31
- package/dist/rules/no-known-value-widening.js +67 -50
- package/dist/rules/no-module-level-mutable-state.d.ts +6 -0
- package/dist/rules/no-module-level-mutable-state.js +47 -0
- package/dist/rules/no-module-mocks.js +37 -48
- package/dist/rules/no-new-promise.js +6 -2
- package/dist/rules/no-node-builtin-import.js +56 -33
- package/dist/rules/no-nullish.js +5 -1
- package/dist/rules/no-object-parameters.js +16 -30
- package/dist/rules/no-per-call-cache-construction.js +2 -2
- package/dist/rules/no-reduce-accumulator-copy.d.ts +7 -0
- package/dist/rules/no-reduce-accumulator-copy.js +106 -0
- package/dist/rules/no-reflect-apply.d.ts +7 -0
- package/dist/rules/no-reflect-apply.js +28 -0
- package/dist/rules/no-reflect-get.d.ts +7 -0
- package/dist/rules/no-reflect-get.js +28 -0
- package/dist/rules/no-run-collect-on-unbounded-stream.js +23 -25
- package/dist/rules/no-runtime-typeof.js +21 -4
- package/dist/rules/no-sequential-effect-all.js +18 -11
- package/dist/rules/no-shape-in-symbol-names.js +15 -5
- package/dist/rules/no-silent-catch-all.js +20 -14
- package/dist/rules/no-test-globals.d.ts +6 -0
- package/dist/rules/no-test-globals.js +147 -0
- package/dist/rules/no-test-lifecycle-hooks.js +4 -2
- package/dist/rules/no-unbounded-concurrency.js +22 -17
- package/dist/rules/no-unbounded-retry.js +47 -62
- package/dist/rules/no-unknown-parameters.d.ts +1 -1
- package/dist/rules/no-unknown-parameters.js +26 -17
- package/dist/rules/no-unknown-returns.d.ts +7 -0
- package/dist/rules/no-unknown-returns.js +74 -0
- package/dist/rules/no-unknown-type-aliases.js +21 -27
- package/dist/rules/no-unsafe-dictionary-type.js +71 -25
- package/dist/rules/no-widen-then-assert.js +96 -73
- package/dist/rules/prefer-catch-tag.js +29 -37
- package/dist/rules/prefer-match-tags-exhaustive.js +35 -47
- package/dist/rules/prefer-predicate-is-tagged.js +4 -6
- package/dist/rules/prefer-service-of.js +39 -26
- package/dist/rules/require-named-effect-fn.js +4 -2
- package/dist/rules/require-suppression-reason.d.ts +6 -0
- package/dist/rules/require-suppression-reason.js +92 -0
- package/dist/vendor/effect-oxlint/Diagnostic.js +1 -1
- package/dist/vendor/effect-oxlint/Scope.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { childNodesAt, isAstNode } from "./_ast-children.js";
|
|
2
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
3
|
+
import { lexicalTypeParameterNames } from "./_anti-slop-lexical-type-parameters.js";
|
|
4
|
+
import * as Arr from "effect/Array";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import * as Predicate from "effect/Predicate";
|
|
7
|
+
//#region src/rules/_anti-slop-type-alias-resolution.ts
|
|
8
|
+
const environmentsByProgram = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function isNode(value) {
|
|
10
|
+
return isAstNode(value);
|
|
11
|
+
}
|
|
12
|
+
function isTypeScope(node) {
|
|
13
|
+
return node.type === "Program" || node.type === "BlockStatement" || node.type === "TSModuleBlock" || node.type === "StaticBlock" || node.type === "SwitchStatement";
|
|
14
|
+
}
|
|
15
|
+
function enclosingTypeScope(node) {
|
|
16
|
+
return Option.getOrElse(Arr.findFirst(ancestors(node), isTypeScope), () => node);
|
|
17
|
+
}
|
|
18
|
+
function declaredTypeBinding(node) {
|
|
19
|
+
if (node.type === "TSTypeAliasDeclaration") return Option.some({
|
|
20
|
+
alias: Option.some(node),
|
|
21
|
+
name: node.id.name
|
|
22
|
+
});
|
|
23
|
+
if (node.type === "TSInterfaceDeclaration" || node.type === "TSEnumDeclaration" || node.type === "ClassDeclaration" || node.type === "ClassExpression") return Option.map(Option.fromNullishOr(node.id), (id) => ({
|
|
24
|
+
alias: Option.none(),
|
|
25
|
+
name: id.name
|
|
26
|
+
}));
|
|
27
|
+
if (node.type === "ImportSpecifier" || node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier") return Option.some({
|
|
28
|
+
alias: Option.none(),
|
|
29
|
+
name: node.local.name
|
|
30
|
+
});
|
|
31
|
+
return Option.none();
|
|
32
|
+
}
|
|
33
|
+
function collectTypeBindings(node, visitorKeys, bindingsByName, aliases) {
|
|
34
|
+
const declared = declaredTypeBinding(node);
|
|
35
|
+
if (Option.isSome(declared)) {
|
|
36
|
+
const { alias, name } = declared.value;
|
|
37
|
+
const bindings = bindingsByName.get(name) ?? [];
|
|
38
|
+
bindings.push({
|
|
39
|
+
alias,
|
|
40
|
+
name,
|
|
41
|
+
scope: enclosingTypeScope(node)
|
|
42
|
+
});
|
|
43
|
+
bindingsByName.set(name, bindings);
|
|
44
|
+
if (Option.isSome(alias)) aliases.push(alias.value);
|
|
45
|
+
}
|
|
46
|
+
for (const key of visitorKeys[node.type] ?? []) for (const child of childNodesAt(node, key, isNode)) collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
|
|
47
|
+
}
|
|
48
|
+
/** Collect every lexical type alias and competing type binding in a program. */
|
|
49
|
+
function createTypeAliasEnvironment(program, visitorKeys) {
|
|
50
|
+
const cached = environmentsByProgram.get(program);
|
|
51
|
+
if (Predicate.isNotUndefined(cached)) return cached;
|
|
52
|
+
const bindingsByName = /* @__PURE__ */ new Map();
|
|
53
|
+
const aliases = [];
|
|
54
|
+
collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
|
|
55
|
+
const environment = {
|
|
56
|
+
aliases,
|
|
57
|
+
bindingsByName,
|
|
58
|
+
visitorKeys
|
|
59
|
+
};
|
|
60
|
+
environmentsByProgram.set(program, environment);
|
|
61
|
+
return environment;
|
|
62
|
+
}
|
|
63
|
+
function ancestorDistance(ancestor, node) {
|
|
64
|
+
return Arr.findFirstIndex([node, ...ancestors(node)], (current) => current === ancestor);
|
|
65
|
+
}
|
|
66
|
+
function nearestTypeBindings(name, use, environment) {
|
|
67
|
+
const candidates = environment.bindingsByName.get(name) ?? [];
|
|
68
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
69
|
+
let nearest = [];
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
const distance = ancestorDistance(candidate.scope, use);
|
|
72
|
+
if (Option.isNone(distance) || distance.value > nearestDistance) continue;
|
|
73
|
+
if (distance.value === nearestDistance) {
|
|
74
|
+
nearest.push(candidate);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
nearestDistance = distance.value;
|
|
78
|
+
nearest = [candidate];
|
|
79
|
+
}
|
|
80
|
+
return nearest;
|
|
81
|
+
}
|
|
82
|
+
/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
|
|
83
|
+
function visibleTypeAlias(name, use, environment) {
|
|
84
|
+
if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return Option.none();
|
|
85
|
+
const bindings = nearestTypeBindings(name, use, environment);
|
|
86
|
+
if (bindings.length !== 1) return Option.none();
|
|
87
|
+
return Option.flatMap(Arr.head(bindings), (binding) => binding.alias);
|
|
88
|
+
}
|
|
89
|
+
/** Return whether a local declaration shadows a built-in type at this use. */
|
|
90
|
+
function hasVisibleTypeBinding(name, use, environment) {
|
|
91
|
+
return lexicalTypeParameterNames(use, environment.visitorKeys).has(name) || nearestTypeBindings(name, use, environment).length > 0;
|
|
92
|
+
}
|
|
93
|
+
function typeReferenceName(type) {
|
|
94
|
+
if (type.typeName.type !== "Identifier") return Option.none();
|
|
95
|
+
return Option.some(type.typeName.name);
|
|
96
|
+
}
|
|
97
|
+
function aliasSubstitutions(alias, reference, base) {
|
|
98
|
+
const parameters = alias.typeParameters?.params ?? [];
|
|
99
|
+
const typeArguments = reference.typeArguments?.params ?? [];
|
|
100
|
+
const next = new Map(base);
|
|
101
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
102
|
+
const explicitArgument = Arr.get(typeArguments, index);
|
|
103
|
+
const argument = Option.orElse(explicitArgument, () => Option.fromNullishOr(parameter.default));
|
|
104
|
+
if (Option.isNone(argument)) return Option.none();
|
|
105
|
+
let argumentSubstitutions = base;
|
|
106
|
+
if (Option.isNone(explicitArgument)) argumentSubstitutions = next;
|
|
107
|
+
next.set(parameter.name.name, {
|
|
108
|
+
type: argument.value,
|
|
109
|
+
substitutions: new Map(argumentSubstitutions)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return Option.some(next);
|
|
113
|
+
}
|
|
114
|
+
/** Match a type after resolving visible aliases and substituting their type parameters. */
|
|
115
|
+
function resolvedTypeMatches(type, environment, matcher) {
|
|
116
|
+
const evaluate = (current, substitutions, resolvingAliases) => {
|
|
117
|
+
if (current.type === "TSTypeReference") {
|
|
118
|
+
const name = typeReferenceName(current);
|
|
119
|
+
if (Option.isSome(name)) {
|
|
120
|
+
const substitution = substitutions.get(name.value);
|
|
121
|
+
if (Predicate.isNotUndefined(substitution) && !current.typeArguments?.params.length) return evaluate(substitution.type, substitution.substitutions, resolvingAliases);
|
|
122
|
+
const expansion = Option.flatMap(Option.filter(visibleTypeAlias(name.value, current, environment), (alias) => !resolvingAliases.has(alias)), (alias) => Option.map(aliasSubstitutions(alias, current, substitutions), (next) => ({
|
|
123
|
+
alias,
|
|
124
|
+
next
|
|
125
|
+
})));
|
|
126
|
+
if (Option.isSome(expansion)) {
|
|
127
|
+
const { alias, next } = expansion.value;
|
|
128
|
+
const nextResolving = new Set(resolvingAliases);
|
|
129
|
+
nextResolving.add(alias);
|
|
130
|
+
return evaluate(alias.typeAnnotation, next, nextResolving);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return matcher(current, (child) => evaluate(child, substitutions, resolvingAliases));
|
|
135
|
+
};
|
|
136
|
+
return evaluate(type, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { createTypeAliasEnvironment, hasVisibleTypeBinding, resolvedTypeMatches, visibleTypeAlias };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
//#region src/rules/_ast-ancestors.ts
|
|
3
|
+
/** The node's parent, or none at the Program root. */
|
|
4
|
+
const parentOf = (node) => Option.fromNullishOr(node.parent);
|
|
5
|
+
/** The node's ancestors, nearest first, ending at the Program root. */
|
|
6
|
+
function* ancestors(node) {
|
|
7
|
+
let current = parentOf(node);
|
|
8
|
+
while (Option.isSome(current)) {
|
|
9
|
+
yield current.value;
|
|
10
|
+
current = parentOf(current.value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ancestors, parentOf };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as Predicate from "effect/Predicate";
|
|
2
|
+
//#region src/rules/_ast-children.ts
|
|
3
|
+
/**
|
|
4
|
+
* Child-node access by property name, for AST walkers driven by visitor keys
|
|
5
|
+
* or object keys. Reads through the property descriptor, so no walker has to
|
|
6
|
+
* view a node as an open dictionary.
|
|
7
|
+
*/
|
|
8
|
+
/** Whether a value is an AST node: an object whose `type` is a string. */
|
|
9
|
+
const isAstNode = (value) => Predicate.hasProperty(value, "type") && Predicate.isString(value.type);
|
|
10
|
+
/** The nodes held at `node[key]`: the node itself, the nodes of an array, or none. */
|
|
11
|
+
const childNodesAt = (node, key, isNode) => {
|
|
12
|
+
const value = Object.getOwnPropertyDescriptor(node, key)?.value;
|
|
13
|
+
if (isNode(value)) return [value];
|
|
14
|
+
if (Array.isArray(value)) return value.filter(isNode);
|
|
15
|
+
return [];
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { childNodesAt, isAstNode };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
//#region src/rules/_call-arguments.ts
|
|
4
|
+
const isExpression = (argument) => argument.type !== "SpreadElement";
|
|
5
|
+
/** The call's argument at `index`, or none when it is missing or spread. */
|
|
6
|
+
const expressionArgument = (node, index) => Option.filter(Arr.get(node.arguments, index), isExpression);
|
|
7
|
+
const isStaticProperty = (property, name) => property.type === "Property" && !property.computed && (property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name);
|
|
8
|
+
/** Every non-computed property of an object literal named `name`, in source order. */
|
|
9
|
+
const staticProperties = (node, name) => node.properties.filter((property) => isStaticProperty(property, name));
|
|
10
|
+
//#endregion
|
|
11
|
+
export { expressionArgument, staticProperties };
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
1
2
|
import { isStaticCall, isStaticMember } from "./_effect-namespaces.js";
|
|
3
|
+
import * as Arr from "effect/Array";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
2
5
|
//#region src/rules/_effect-context.ts
|
|
3
|
-
const isFunction = (node) => node
|
|
6
|
+
const isFunction = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
|
|
7
|
+
/** `Effect.gen(...)`, `Effect.fn(...)`, or `Effect.fnUntraced(...)` called with the program directly. */
|
|
8
|
+
const directProgramKind = (callee, effectNamespaces) => {
|
|
9
|
+
if (isStaticMember(callee, effectNamespaces, "gen")) return Option.some("gen");
|
|
10
|
+
if (isStaticMember(callee, effectNamespaces, "fn")) return Option.some("fn");
|
|
11
|
+
if (isStaticMember(callee, effectNamespaces, "fnUntraced")) return Option.some("fnUntraced");
|
|
12
|
+
return Option.none();
|
|
13
|
+
};
|
|
14
|
+
/** `Effect.fn("name")(...)` or `Effect.fnUntraced(options)(...)` called with the program. */
|
|
15
|
+
const curriedProgramKind = (callee, effectNamespaces) => {
|
|
16
|
+
if (isStaticCall(callee, effectNamespaces, "fn")) return Option.some("fn");
|
|
17
|
+
if (isStaticCall(callee, effectNamespaces, "fnUntraced")) return Option.some("fnUntraced");
|
|
18
|
+
return Option.none();
|
|
19
|
+
};
|
|
20
|
+
/** The kind of Effect program a function is the body of, when it is an argument of one. */
|
|
4
21
|
const effectProgramKind = (node, effectNamespaces) => {
|
|
5
22
|
const parent = node.parent;
|
|
6
|
-
if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return
|
|
7
|
-
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
if (parent.callee.type !== "CallExpression") return void 0;
|
|
11
|
-
if (isStaticCall(parent.callee, effectNamespaces, "fn")) return "fn";
|
|
12
|
-
if (isStaticCall(parent.callee, effectNamespaces, "fnUntraced")) return "fnUntraced";
|
|
13
|
-
};
|
|
14
|
-
const enclosingEffectProgram = (node, effectNamespaces) => {
|
|
15
|
-
let current = node.parent;
|
|
16
|
-
while (current !== void 0 && current !== null) {
|
|
17
|
-
if (isFunction(current)) {
|
|
18
|
-
const kind = effectProgramKind(current, effectNamespaces);
|
|
19
|
-
if (kind === void 0) return void 0;
|
|
20
|
-
return [current, kind];
|
|
21
|
-
}
|
|
22
|
-
current = current.parent;
|
|
23
|
-
}
|
|
23
|
+
if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return Option.none();
|
|
24
|
+
const callee = parent.callee;
|
|
25
|
+
if (callee.type === "Super") return Option.none();
|
|
26
|
+
return Option.orElse(directProgramKind(callee, effectNamespaces), () => curriedProgramKind(callee, effectNamespaces));
|
|
24
27
|
};
|
|
25
|
-
|
|
28
|
+
/** The nearest enclosing function, when it is the body of an Effect program. */
|
|
29
|
+
const enclosingEffectProgram = (node, effectNamespaces) => Option.flatMap(Arr.findFirst(ancestors(node), isFunction), (program) => Option.map(effectProgramKind(program, effectNamespaces), (kind) => [program, kind]));
|
|
30
|
+
const isInsideEffectProgram = (node, effectNamespaces) => Option.isSome(enclosingEffectProgram(node, effectNamespaces));
|
|
26
31
|
//#endregion
|
|
27
32
|
export { enclosingEffectProgram, isInsideEffectProgram };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { childNodesAt, isAstNode } from "./_ast-children.js";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
1
5
|
//#region src/rules/_function-metrics.ts
|
|
2
|
-
const isNode = (value) =>
|
|
6
|
+
const isNode = (value) => isAstNode(value);
|
|
3
7
|
const isFunctionType = (type) => type === "ArrowFunctionExpression" || type === "FunctionDeclaration" || type === "FunctionExpression";
|
|
4
8
|
const isFunctionNode = (node) => isFunctionType(node.type);
|
|
5
9
|
/** Slots that hold the parent link, source positions, or type-level syntax. */
|
|
@@ -26,12 +30,7 @@ const transparentTypeWrappers = new Set([
|
|
|
26
30
|
"TSInstantiationExpression"
|
|
27
31
|
]);
|
|
28
32
|
const isTypeOnly = (node) => node.type.startsWith("TS") && !transparentTypeWrappers.has(node.type);
|
|
29
|
-
const slot = (node, key) =>
|
|
30
|
-
const value = node[key];
|
|
31
|
-
if (isNode(value)) return [value];
|
|
32
|
-
if (Array.isArray(value)) return value.filter(isNode);
|
|
33
|
-
return [];
|
|
34
|
-
};
|
|
33
|
+
const slot = (node, key) => childNodesAt(node, key, isNode);
|
|
35
34
|
/** Runtime child nodes of `node`, in source order of their slots. */
|
|
36
35
|
const children = (node) => {
|
|
37
36
|
if (transparentTypeWrappers.has(node.type)) return slot(node, "expression");
|
|
@@ -44,8 +43,7 @@ const children = (node) => {
|
|
|
44
43
|
};
|
|
45
44
|
/** The nodes that form the body of a function unit: its parameters and its body. */
|
|
46
45
|
const unitRoots = (fn) => {
|
|
47
|
-
|
|
48
|
-
return [...slot(node, "params"), ...slot(node, "body")];
|
|
46
|
+
return [...slot(fn, "params"), ...slot(fn, "body")];
|
|
49
47
|
};
|
|
50
48
|
/**
|
|
51
49
|
* Flatten a logical expression tree into its operator sequence, left to right.
|
|
@@ -81,7 +79,8 @@ const cognitiveComplexity = (fn) => {
|
|
|
81
79
|
for (const child of slot(node, key)) visit(child, nesting);
|
|
82
80
|
};
|
|
83
81
|
const visitIf = (node, nesting, isElseIf) => {
|
|
84
|
-
total +=
|
|
82
|
+
total += 1;
|
|
83
|
+
if (!isElseIf) total += nesting;
|
|
85
84
|
visitSlot(node, "test", nesting);
|
|
86
85
|
visitSlot(node, "consequent", nesting + 1);
|
|
87
86
|
for (const alternate of slot(node, "alternate")) if (alternate.type === "IfStatement") visitIf(alternate, nesting, true);
|
|
@@ -140,50 +139,61 @@ const halsteadDifficulty = (counts) => {
|
|
|
140
139
|
};
|
|
141
140
|
const functionOperator = (node) => {
|
|
142
141
|
if (node.type === "ArrowFunctionExpression") return "=>";
|
|
143
|
-
|
|
142
|
+
if (node.generator === true) return "function*";
|
|
143
|
+
return "function";
|
|
144
144
|
};
|
|
145
145
|
const literalOperand = (node) => {
|
|
146
|
-
if (
|
|
147
|
-
if (
|
|
146
|
+
if (Predicate.isString(node.raw)) return node.raw;
|
|
147
|
+
if (Predicate.isString(node.bigint)) return `${node.bigint}n`;
|
|
148
148
|
return String(node.value);
|
|
149
149
|
};
|
|
150
|
+
const isNonEmpty = (text) => text.length > 0;
|
|
150
151
|
const templateOperand = (node) => {
|
|
151
152
|
const value = node.value;
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
153
|
+
if (!Predicate.hasProperty(value, "raw") || !Predicate.isString(value.raw)) return Option.none();
|
|
154
|
+
return Option.liftPredicate(value.raw, isNonEmpty);
|
|
155
155
|
};
|
|
156
|
-
/**
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
WhileStatement: ["while"],
|
|
161
|
-
DoWhileStatement: ["do-while"],
|
|
162
|
-
SwitchStatement: ["switch"],
|
|
163
|
-
ReturnStatement: ["return"],
|
|
164
|
-
ThrowStatement: ["throw"],
|
|
165
|
-
CatchClause: ["catch"],
|
|
166
|
-
BreakStatement: ["break"],
|
|
167
|
-
ContinueStatement: ["continue"],
|
|
168
|
-
LabeledStatement: [":"],
|
|
169
|
-
DebuggerStatement: ["debugger"],
|
|
170
|
-
WithStatement: ["with"],
|
|
171
|
-
AssignmentPattern: ["="],
|
|
172
|
-
NewExpression: ["new"],
|
|
173
|
-
ArrayExpression: ["[]"],
|
|
174
|
-
ArrayPattern: ["[]"],
|
|
175
|
-
ObjectExpression: ["{}"],
|
|
176
|
-
ObjectPattern: ["{}"],
|
|
177
|
-
SpreadElement: ["..."],
|
|
178
|
-
RestElement: ["..."],
|
|
179
|
-
ConditionalExpression: ["?:"],
|
|
180
|
-
SequenceExpression: [","],
|
|
181
|
-
AwaitExpression: ["await"],
|
|
182
|
-
ImportExpression: ["import()"],
|
|
183
|
-
TemplateLiteral: ["`"],
|
|
184
|
-
JSXElement: ["<>"],
|
|
185
|
-
JSXFragment: ["<>"]
|
|
156
|
+
/** `tokens` when `present`, otherwise none: the operator tokens of an optional clause. */
|
|
157
|
+
const tokensIf = (present, ...tokens) => {
|
|
158
|
+
if (!present) return [];
|
|
159
|
+
return tokens;
|
|
186
160
|
};
|
|
161
|
+
/** The `name` of the node held in a slot, or empty when the slot holds none. */
|
|
162
|
+
const slotName = (node, key) => Option.match(Arr.head(slot(node, key)), {
|
|
163
|
+
onNone: () => "",
|
|
164
|
+
onSome: (child) => String(child.name)
|
|
165
|
+
});
|
|
166
|
+
/** Node types that always stand for the same operator tokens. */
|
|
167
|
+
const fixedOperators = new Map([
|
|
168
|
+
["ForStatement", ["for"]],
|
|
169
|
+
["ForInStatement", ["for-in"]],
|
|
170
|
+
["WhileStatement", ["while"]],
|
|
171
|
+
["DoWhileStatement", ["do-while"]],
|
|
172
|
+
["SwitchStatement", ["switch"]],
|
|
173
|
+
["ReturnStatement", ["return"]],
|
|
174
|
+
["ThrowStatement", ["throw"]],
|
|
175
|
+
["CatchClause", ["catch"]],
|
|
176
|
+
["BreakStatement", ["break"]],
|
|
177
|
+
["ContinueStatement", ["continue"]],
|
|
178
|
+
["LabeledStatement", [":"]],
|
|
179
|
+
["DebuggerStatement", ["debugger"]],
|
|
180
|
+
["WithStatement", ["with"]],
|
|
181
|
+
["AssignmentPattern", ["="]],
|
|
182
|
+
["NewExpression", ["new"]],
|
|
183
|
+
["ArrayExpression", ["[]"]],
|
|
184
|
+
["ArrayPattern", ["[]"]],
|
|
185
|
+
["ObjectExpression", ["{}"]],
|
|
186
|
+
["ObjectPattern", ["{}"]],
|
|
187
|
+
["SpreadElement", ["..."]],
|
|
188
|
+
["RestElement", ["..."]],
|
|
189
|
+
["ConditionalExpression", ["?:"]],
|
|
190
|
+
["SequenceExpression", [","]],
|
|
191
|
+
["AwaitExpression", ["await"]],
|
|
192
|
+
["ImportExpression", ["import()"]],
|
|
193
|
+
["TemplateLiteral", ["`"]],
|
|
194
|
+
["JSXElement", ["<>"]],
|
|
195
|
+
["JSXFragment", ["<>"]]
|
|
196
|
+
]);
|
|
187
197
|
/** Node types whose operator token carries the operator itself. */
|
|
188
198
|
const operatorCarriers = new Set([
|
|
189
199
|
"BinaryExpression",
|
|
@@ -195,41 +205,53 @@ const operatorCarriers = new Set([
|
|
|
195
205
|
/** Statement-level nodes whose operator tokens depend on the node's shape. */
|
|
196
206
|
const statementOperators = (node) => {
|
|
197
207
|
switch (node.type) {
|
|
198
|
-
case "IfStatement": return isNode(node.alternate)
|
|
199
|
-
case "ForOfStatement":
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
case "IfStatement": return ["if", ...tokensIf(isNode(node.alternate), "else")];
|
|
209
|
+
case "ForOfStatement":
|
|
210
|
+
if (node.await === true) return ["for-await-of"];
|
|
211
|
+
return ["for-of"];
|
|
212
|
+
case "SwitchCase":
|
|
213
|
+
if (isNode(node.test)) return ["case"];
|
|
214
|
+
return ["default"];
|
|
215
|
+
case "TryStatement": return ["try", ...tokensIf(isNode(node.finalizer), "finally")];
|
|
202
216
|
case "VariableDeclaration": return [String(node.kind)];
|
|
203
|
-
case "VariableDeclarator": return isNode(node.init)
|
|
217
|
+
case "VariableDeclarator": return tokensIf(isNode(node.init), "=");
|
|
204
218
|
case "ClassDeclaration":
|
|
205
|
-
case "ClassExpression": return isNode(node.superClass)
|
|
206
|
-
case "PropertyDefinition": return isNode(node.value)
|
|
219
|
+
case "ClassExpression": return ["class", ...tokensIf(isNode(node.superClass), "extends")];
|
|
220
|
+
case "PropertyDefinition": return tokensIf(isNode(node.value), "=");
|
|
207
221
|
default: return [];
|
|
208
222
|
}
|
|
209
223
|
};
|
|
224
|
+
/** `?.` calls and accesses carry a leading `?`. */
|
|
225
|
+
const optionalMark = (node) => {
|
|
226
|
+
if (node.optional === true) return "?";
|
|
227
|
+
return "";
|
|
228
|
+
};
|
|
229
|
+
const memberAccess = (node) => {
|
|
230
|
+
if (node.computed === true) return "[]";
|
|
231
|
+
return ".";
|
|
232
|
+
};
|
|
210
233
|
/** Expression-level nodes whose operator tokens depend on the node's shape. */
|
|
211
234
|
const expressionOperators = (node) => {
|
|
212
235
|
if (operatorCarriers.has(node.type)) return [String(node.operator)];
|
|
213
236
|
switch (node.type) {
|
|
214
|
-
case "CallExpression": return
|
|
215
|
-
case "MemberExpression": {
|
|
216
|
-
const access = node.computed === true ? "[]" : ".";
|
|
217
|
-
return node.optional === true ? [`?${access}`] : [access];
|
|
218
|
-
}
|
|
237
|
+
case "CallExpression": return [`${optionalMark(node)}()`];
|
|
238
|
+
case "MemberExpression": return [`${optionalMark(node)}${memberAccess(node)}`];
|
|
219
239
|
case "Property":
|
|
220
240
|
if (node.kind === "get" || node.kind === "set") return [String(node.kind)];
|
|
221
|
-
return node.shorthand
|
|
222
|
-
case "YieldExpression":
|
|
223
|
-
|
|
241
|
+
return tokensIf(node.shorthand !== true && node.method !== true, ":");
|
|
242
|
+
case "YieldExpression":
|
|
243
|
+
if (node.delegate === true) return ["yield*"];
|
|
244
|
+
return ["yield"];
|
|
245
|
+
case "JSXAttribute": return tokensIf(isNode(node.value), "=");
|
|
224
246
|
case "ArrowFunctionExpression":
|
|
225
247
|
case "FunctionDeclaration":
|
|
226
|
-
case "FunctionExpression": return node.async === true
|
|
248
|
+
case "FunctionExpression": return [...tokensIf(node.async === true, "async"), functionOperator(node)];
|
|
227
249
|
default: return [];
|
|
228
250
|
}
|
|
229
251
|
};
|
|
230
252
|
/** Every operator token a node stands for. */
|
|
231
253
|
const operatorsOf = (node) => [
|
|
232
|
-
...fixedOperators
|
|
254
|
+
...fixedOperators.get(node.type) ?? [],
|
|
233
255
|
...statementOperators(node),
|
|
234
256
|
...expressionOperators(node)
|
|
235
257
|
];
|
|
@@ -237,18 +259,15 @@ const operatorsOf = (node) => [
|
|
|
237
259
|
const operandOf = (node) => {
|
|
238
260
|
switch (node.type) {
|
|
239
261
|
case "Identifier":
|
|
240
|
-
case "JSXIdentifier": return String(node.name);
|
|
241
|
-
case "PrivateIdentifier": return `#${String(node.name)}
|
|
242
|
-
case "Literal": return literalOperand(node);
|
|
262
|
+
case "JSXIdentifier": return Option.some(String(node.name));
|
|
263
|
+
case "PrivateIdentifier": return Option.some(`#${String(node.name)}`);
|
|
264
|
+
case "Literal": return Option.some(literalOperand(node));
|
|
243
265
|
case "TemplateElement": return templateOperand(node);
|
|
244
|
-
case "ThisExpression": return "this";
|
|
245
|
-
case "Super": return "super";
|
|
246
|
-
case "MetaProperty": return `${
|
|
247
|
-
case "JSXText":
|
|
248
|
-
|
|
249
|
-
return text.length > 0 ? text : null;
|
|
250
|
-
}
|
|
251
|
-
default: return null;
|
|
266
|
+
case "ThisExpression": return Option.some("this");
|
|
267
|
+
case "Super": return Option.some("super");
|
|
268
|
+
case "MetaProperty": return Option.some(`${slotName(node, "meta")}.${slotName(node, "property")}`);
|
|
269
|
+
case "JSXText": return Option.liftPredicate(String(node.value).trim(), isNonEmpty);
|
|
270
|
+
default: return Option.none();
|
|
252
271
|
}
|
|
253
272
|
};
|
|
254
273
|
/**
|
|
@@ -266,7 +285,7 @@ const halstead = (fn) => {
|
|
|
266
285
|
if (isTypeOnly(node)) return;
|
|
267
286
|
for (const operator of operatorsOf(node)) tally(operators, operator);
|
|
268
287
|
const operand = operandOf(node);
|
|
269
|
-
if (operand
|
|
288
|
+
if (Option.isSome(operand)) tally(operands, operand.value);
|
|
270
289
|
if (isFunctionType(node.type) || node.type === "MetaProperty") return;
|
|
271
290
|
for (const child of children(node)) visit(child);
|
|
272
291
|
};
|
|
@@ -1,79 +1,57 @@
|
|
|
1
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
1
5
|
//#region src/rules/_tagged-control-flow.ts
|
|
2
6
|
const expressionKey = (node) => {
|
|
3
|
-
if (node.type === "Identifier") return node.name;
|
|
4
|
-
if (node.type === "ThisExpression") return "this";
|
|
5
|
-
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
return `${object}.${node.property.name}`;
|
|
7
|
+
if (node.type === "Identifier") return Option.some(node.name);
|
|
8
|
+
if (node.type === "ThisExpression") return Option.some("this");
|
|
9
|
+
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return Option.none();
|
|
10
|
+
const property = node.property.name;
|
|
11
|
+
return Option.map(expressionKey(node.object), (object) => `${object}.${property}`);
|
|
9
12
|
};
|
|
10
13
|
const stringLiteral = (node) => {
|
|
11
|
-
if (node.type !== "Literal") return
|
|
12
|
-
|
|
13
|
-
return node.value;
|
|
14
|
+
if (node.type !== "Literal" || !Predicate.isString(node.value)) return Option.none();
|
|
15
|
+
return Option.some(node.value);
|
|
14
16
|
};
|
|
15
17
|
const taggedSubject = (node) => {
|
|
16
|
-
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier" || node.property.name !== "_tag") return
|
|
18
|
+
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier" || node.property.name !== "_tag") return Option.none();
|
|
17
19
|
return expressionKey(node.object);
|
|
18
20
|
};
|
|
21
|
+
/** `subjectSide._tag === "Tag"` read from one orientation of the comparison. */
|
|
22
|
+
const orientedComparison = (subjectSide, tagSide) => Option.all({
|
|
23
|
+
subject: taggedSubject(subjectSide),
|
|
24
|
+
tag: stringLiteral(tagSide)
|
|
25
|
+
});
|
|
19
26
|
const tagComparison = (node) => {
|
|
20
|
-
if (node.type !== "BinaryExpression" || node.operator !== "===") return
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (leftSubject !== null && rightTag !== null) return {
|
|
24
|
-
subject: leftSubject,
|
|
25
|
-
tag: rightTag
|
|
26
|
-
};
|
|
27
|
-
const rightSubject = taggedSubject(node.right);
|
|
28
|
-
const leftTag = stringLiteral(node.left);
|
|
29
|
-
if (rightSubject === null || leftTag === null) return null;
|
|
30
|
-
return {
|
|
31
|
-
subject: rightSubject,
|
|
32
|
-
tag: leftTag
|
|
33
|
-
};
|
|
27
|
+
if (node.type !== "BinaryExpression" || node.operator !== "===") return Option.none();
|
|
28
|
+
const { left, right } = node;
|
|
29
|
+
return Option.orElse(orientedComparison(left, right), () => orientedComparison(right, left));
|
|
34
30
|
};
|
|
35
31
|
const tagComparisonsInOr = (node) => {
|
|
36
|
-
if (node.type === "LogicalExpression" && node.operator === "||")
|
|
37
|
-
|
|
38
|
-
const right = tagComparisonsInOr(node.right);
|
|
39
|
-
if (left === null || right === null) return null;
|
|
40
|
-
return [...left, ...right];
|
|
41
|
-
}
|
|
42
|
-
const comparison = tagComparison(node);
|
|
43
|
-
if (comparison === null) return null;
|
|
44
|
-
return [comparison];
|
|
45
|
-
};
|
|
46
|
-
const hasOneTaggedSubject = (comparisons) => {
|
|
47
|
-
const first = comparisons[0];
|
|
48
|
-
if (first === void 0 || comparisons.length < 2) return false;
|
|
49
|
-
return comparisons.every((comparison) => comparison.subject === first.subject);
|
|
32
|
+
if (node.type === "LogicalExpression" && node.operator === "||") return Option.zipWith(tagComparisonsInOr(node.left), tagComparisonsInOr(node.right), (left, right) => [...left, ...right]);
|
|
33
|
+
return Option.map(tagComparison(node), (comparison) => [comparison]);
|
|
50
34
|
};
|
|
35
|
+
const hasOneTaggedSubject = (comparisons) => comparisons.length >= 2 && Option.exists(Arr.head(comparisons), (first) => comparisons.every((comparison) => comparison.subject === first.subject));
|
|
51
36
|
const isEffectCall = (node, operation) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.object.type === "Identifier" && node.callee.object.name === "Effect" && node.callee.property.type === "Identifier" && node.callee.property.name === operation;
|
|
52
|
-
const isFunction = (node) => node
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
current = current.parent;
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
};
|
|
70
|
-
const isInsideCatchAllHandler = (node) => {
|
|
71
|
-
const location = effectCallbackLocation(node, "catchAll");
|
|
72
|
-
if (location === null) return false;
|
|
37
|
+
const isFunction = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
|
|
38
|
+
const callbackLocation = (callback, operation) => {
|
|
39
|
+
const call = callback.parent;
|
|
40
|
+
if (call?.type !== "CallExpression" || !isEffectCall(call, operation)) return Option.none();
|
|
41
|
+
const index = call.arguments.indexOf(callback);
|
|
42
|
+
if (index < 0) return Option.none();
|
|
43
|
+
return Option.some({
|
|
44
|
+
argumentCount: call.arguments.length,
|
|
45
|
+
index
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
/** Where the nearest enclosing function sits among the arguments of an `Effect.<operation>` call. */
|
|
49
|
+
const effectCallbackLocation = (node, operation) => Option.flatMap(Arr.findFirst(ancestors(node), isFunction), (callback) => callbackLocation(callback, operation));
|
|
50
|
+
const isInsideCatchAllHandler = (node) => Option.exists(effectCallbackLocation(node, "catchAll"), (location) => {
|
|
73
51
|
let handlerIndex = 1;
|
|
74
52
|
if (location.argumentCount === 1) handlerIndex = 0;
|
|
75
53
|
return location.index === handlerIndex;
|
|
76
|
-
};
|
|
54
|
+
});
|
|
77
55
|
const taggedSwitchSubject = (node) => taggedSubject(node.discriminant);
|
|
78
56
|
//#endregion
|
|
79
57
|
export { effectCallbackLocation, hasOneTaggedSubject, isEffectCall, isInsideCatchAllHandler, tagComparison, tagComparisonsInOr, taggedSwitchSubject };
|