oxlint-plugin-effect 0.14.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 +2 -1
- package/dist/presets/recommended.d.ts +1 -0
- package/dist/presets/recommended.js +1 -0
- package/dist/rules/_anti-slop-array-method.js +41 -33
- package/dist/rules/_anti-slop-dictionary-types.js +172 -129
- package/dist/rules/_anti-slop-function-parameters.js +9 -6
- package/dist/rules/_anti-slop-lexical-type-parameters.js +6 -14
- package/dist/rules/_anti-slop-reflect-method.js +7 -3
- package/dist/rules/_anti-slop-scope.js +8 -6
- package/dist/rules/_anti-slop-type-alias-resolution.js +58 -65
- 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 +63 -44
- package/dist/rules/_tagged-control-flow.js +38 -60
- package/dist/rules/_test-apis.js +47 -0
- package/dist/rules/index.d.ts +2 -1
- package/dist/rules/index.js +2 -0
- package/dist/rules/no-array-filter-map.js +24 -14
- package/dist/rules/no-chained-type-assertions.js +7 -4
- package/dist/rules/no-conditional-empty-object-spread.js +7 -4
- package/dist/rules/no-dynamic-imports.js +33 -25
- package/dist/rules/no-eager-acquire.js +40 -34
- package/dist/rules/no-effect-run-in-tests.js +11 -8
- package/dist/rules/no-globals.js +42 -31
- package/dist/rules/no-known-value-widening.js +64 -39
- package/dist/rules/no-module-level-mutable-state.js +4 -8
- 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 +9 -7
- package/dist/rules/no-per-call-cache-construction.js +2 -2
- package/dist/rules/no-reduce-accumulator-copy.js +65 -53
- package/dist/rules/no-reflect-apply.js +7 -4
- package/dist/rules/no-reflect-get.js +7 -4
- package/dist/rules/no-run-collect-on-unbounded-stream.js +23 -25
- package/dist/rules/no-runtime-typeof.js +9 -5
- package/dist/rules/no-sequential-effect-all.js +18 -11
- package/dist/rules/no-shape-in-symbol-names.js +10 -6
- 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.js +5 -3
- package/dist/rules/no-unknown-returns.js +11 -10
- package/dist/rules/no-unknown-type-aliases.js +13 -9
- package/dist/rules/no-unsafe-dictionary-type.js +22 -27
- package/dist/rules/no-widen-then-assert.js +95 -72
- 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.js +8 -3
- package/dist/vendor/effect-oxlint/Scope.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveVariable } from "./_anti-slop-scope.js";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
2
3
|
//#region src/rules/_anti-slop-reflect-method.ts
|
|
3
4
|
/**
|
|
4
5
|
* Ported from dmmulroy/anti-slop at
|
|
@@ -7,15 +8,18 @@ import { resolveVariable } from "./_anti-slop-scope.js";
|
|
|
7
8
|
function isGlobalReflect(sourceCode, expression) {
|
|
8
9
|
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
9
10
|
if (sourceCode.isGlobalReference(expression)) return true;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
return Option.match(resolveVariable(sourceCode, expression), {
|
|
12
|
+
onNone: () => true,
|
|
13
|
+
onSome: (variable) => variable.defs.length === 0
|
|
14
|
+
});
|
|
12
15
|
}
|
|
13
16
|
/** Reports whether a call target names one method on the global Reflect object. */
|
|
14
17
|
function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
|
|
15
18
|
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
16
19
|
if (!isGlobalReflect(sourceCode, callee.object)) return false;
|
|
17
20
|
const property = callee.property;
|
|
18
|
-
|
|
21
|
+
if (callee.computed) return property.type === "Literal" && property.value === methodName;
|
|
22
|
+
return property.type === "Identifier" && property.name === methodName;
|
|
19
23
|
}
|
|
20
24
|
//#endregion
|
|
21
25
|
export { isGlobalReflectMethodCall };
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
import * as Predicate from "effect/Predicate";
|
|
1
3
|
//#region src/rules/_anti-slop-scope.ts
|
|
2
4
|
/** Resolve an identifier to its binding by walking lexical scopes upward. */
|
|
3
5
|
function resolveVariable(sourceCode, identifier) {
|
|
4
|
-
let scope = sourceCode.getScope(identifier);
|
|
5
|
-
while (scope
|
|
6
|
-
const variable = scope.set.get(identifier.name);
|
|
7
|
-
if (variable
|
|
8
|
-
scope = scope.upper;
|
|
6
|
+
let scope = Option.some(sourceCode.getScope(identifier));
|
|
7
|
+
while (Option.isSome(scope)) {
|
|
8
|
+
const variable = scope.value.set.get(identifier.name);
|
|
9
|
+
if (Predicate.isNotUndefined(variable)) return Option.some(variable);
|
|
10
|
+
scope = Option.fromNullishOr(scope.value.upper);
|
|
9
11
|
}
|
|
10
|
-
return
|
|
12
|
+
return Option.none();
|
|
11
13
|
}
|
|
12
14
|
//#endregion
|
|
13
15
|
export { resolveVariable };
|
|
@@ -1,58 +1,54 @@
|
|
|
1
|
+
import { childNodesAt, isAstNode } from "./_ast-children.js";
|
|
2
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
1
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";
|
|
2
7
|
//#region src/rules/_anti-slop-type-alias-resolution.ts
|
|
3
8
|
const environmentsByProgram = /* @__PURE__ */ new WeakMap();
|
|
4
9
|
function isNode(value) {
|
|
5
|
-
return
|
|
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";
|
|
6
14
|
}
|
|
7
15
|
function enclosingTypeScope(node) {
|
|
8
|
-
|
|
9
|
-
while (current !== null) {
|
|
10
|
-
if (current.type === "Program" || current.type === "BlockStatement" || current.type === "TSModuleBlock" || current.type === "StaticBlock" || current.type === "SwitchStatement") return current;
|
|
11
|
-
current = current.parent;
|
|
12
|
-
}
|
|
13
|
-
return node;
|
|
16
|
+
return Option.getOrElse(Arr.findFirst(ancestors(node), isTypeScope), () => node);
|
|
14
17
|
}
|
|
15
18
|
function declaredTypeBinding(node) {
|
|
16
|
-
if (node.type === "TSTypeAliasDeclaration") return {
|
|
17
|
-
alias: node,
|
|
19
|
+
if (node.type === "TSTypeAliasDeclaration") return Option.some({
|
|
20
|
+
alias: Option.some(node),
|
|
18
21
|
name: node.id.name
|
|
19
|
-
};
|
|
20
|
-
if (node.type === "TSInterfaceDeclaration" || node.type === "TSEnumDeclaration" || node.type === "ClassDeclaration" || node.type === "ClassExpression") return node.id
|
|
21
|
-
alias:
|
|
22
|
-
name:
|
|
23
|
-
};
|
|
24
|
-
if (node.type === "ImportSpecifier" || node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier") return {
|
|
25
|
-
alias:
|
|
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(),
|
|
26
29
|
name: node.local.name
|
|
27
|
-
};
|
|
28
|
-
return
|
|
30
|
+
});
|
|
31
|
+
return Option.none();
|
|
29
32
|
}
|
|
30
33
|
function collectTypeBindings(node, visitorKeys, bindingsByName, aliases) {
|
|
31
34
|
const declared = declaredTypeBinding(node);
|
|
32
|
-
if (declared
|
|
33
|
-
const
|
|
35
|
+
if (Option.isSome(declared)) {
|
|
36
|
+
const { alias, name } = declared.value;
|
|
37
|
+
const bindings = bindingsByName.get(name) ?? [];
|
|
34
38
|
bindings.push({
|
|
35
|
-
|
|
39
|
+
alias,
|
|
40
|
+
name,
|
|
36
41
|
scope: enclosingTypeScope(node)
|
|
37
42
|
});
|
|
38
|
-
bindingsByName.set(
|
|
39
|
-
if (
|
|
40
|
-
}
|
|
41
|
-
const fields = node;
|
|
42
|
-
for (const key of visitorKeys[node.type] ?? []) {
|
|
43
|
-
const value = fields[key];
|
|
44
|
-
if (isNode(value)) {
|
|
45
|
-
collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (!Array.isArray(value)) continue;
|
|
49
|
-
for (const child of value) if (isNode(child)) collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
|
|
43
|
+
bindingsByName.set(name, bindings);
|
|
44
|
+
if (Option.isSome(alias)) aliases.push(alias.value);
|
|
50
45
|
}
|
|
46
|
+
for (const key of visitorKeys[node.type] ?? []) for (const child of childNodesAt(node, key, isNode)) collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
|
|
51
47
|
}
|
|
52
48
|
/** Collect every lexical type alias and competing type binding in a program. */
|
|
53
49
|
function createTypeAliasEnvironment(program, visitorKeys) {
|
|
54
50
|
const cached = environmentsByProgram.get(program);
|
|
55
|
-
if (cached
|
|
51
|
+
if (Predicate.isNotUndefined(cached)) return cached;
|
|
56
52
|
const bindingsByName = /* @__PURE__ */ new Map();
|
|
57
53
|
const aliases = [];
|
|
58
54
|
collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
|
|
@@ -65,14 +61,7 @@ function createTypeAliasEnvironment(program, visitorKeys) {
|
|
|
65
61
|
return environment;
|
|
66
62
|
}
|
|
67
63
|
function ancestorDistance(ancestor, node) {
|
|
68
|
-
|
|
69
|
-
let distance = 0;
|
|
70
|
-
while (current !== null) {
|
|
71
|
-
if (current === ancestor) return distance;
|
|
72
|
-
current = current.parent;
|
|
73
|
-
distance += 1;
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
64
|
+
return Arr.findFirstIndex([node, ...ancestors(node)], (current) => current === ancestor);
|
|
76
65
|
}
|
|
77
66
|
function nearestTypeBindings(name, use, environment) {
|
|
78
67
|
const candidates = environment.bindingsByName.get(name) ?? [];
|
|
@@ -80,61 +69,65 @@ function nearestTypeBindings(name, use, environment) {
|
|
|
80
69
|
let nearest = [];
|
|
81
70
|
for (const candidate of candidates) {
|
|
82
71
|
const distance = ancestorDistance(candidate.scope, use);
|
|
83
|
-
if (distance
|
|
84
|
-
if (distance === nearestDistance) {
|
|
72
|
+
if (Option.isNone(distance) || distance.value > nearestDistance) continue;
|
|
73
|
+
if (distance.value === nearestDistance) {
|
|
85
74
|
nearest.push(candidate);
|
|
86
75
|
continue;
|
|
87
76
|
}
|
|
88
|
-
nearestDistance = distance;
|
|
77
|
+
nearestDistance = distance.value;
|
|
89
78
|
nearest = [candidate];
|
|
90
79
|
}
|
|
91
80
|
return nearest;
|
|
92
81
|
}
|
|
93
82
|
/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
|
|
94
83
|
function visibleTypeAlias(name, use, environment) {
|
|
95
|
-
if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return
|
|
84
|
+
if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return Option.none();
|
|
96
85
|
const bindings = nearestTypeBindings(name, use, environment);
|
|
97
|
-
|
|
86
|
+
if (bindings.length !== 1) return Option.none();
|
|
87
|
+
return Option.flatMap(Arr.head(bindings), (binding) => binding.alias);
|
|
98
88
|
}
|
|
99
89
|
/** Return whether a local declaration shadows a built-in type at this use. */
|
|
100
90
|
function hasVisibleTypeBinding(name, use, environment) {
|
|
101
91
|
return lexicalTypeParameterNames(use, environment.visitorKeys).has(name) || nearestTypeBindings(name, use, environment).length > 0;
|
|
102
92
|
}
|
|
103
93
|
function typeReferenceName(type) {
|
|
104
|
-
|
|
94
|
+
if (type.typeName.type !== "Identifier") return Option.none();
|
|
95
|
+
return Option.some(type.typeName.name);
|
|
105
96
|
}
|
|
106
97
|
function aliasSubstitutions(alias, reference, base) {
|
|
107
98
|
const parameters = alias.typeParameters?.params ?? [];
|
|
108
99
|
const typeArguments = reference.typeArguments?.params ?? [];
|
|
109
100
|
const next = new Map(base);
|
|
110
101
|
for (const [index, parameter] of parameters.entries()) {
|
|
111
|
-
const explicitArgument = typeArguments
|
|
112
|
-
const argument = explicitArgument
|
|
113
|
-
if (argument
|
|
114
|
-
|
|
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;
|
|
115
107
|
next.set(parameter.name.name, {
|
|
116
|
-
type: argument,
|
|
108
|
+
type: argument.value,
|
|
117
109
|
substitutions: new Map(argumentSubstitutions)
|
|
118
110
|
});
|
|
119
111
|
}
|
|
120
|
-
return next;
|
|
112
|
+
return Option.some(next);
|
|
121
113
|
}
|
|
122
114
|
/** Match a type after resolving visible aliases and substituting their type parameters. */
|
|
123
115
|
function resolvedTypeMatches(type, environment, matcher) {
|
|
124
116
|
const evaluate = (current, substitutions, resolvingAliases) => {
|
|
125
117
|
if (current.type === "TSTypeReference") {
|
|
126
118
|
const name = typeReferenceName(current);
|
|
127
|
-
if (name
|
|
128
|
-
const substitution = substitutions.get(name);
|
|
129
|
-
if (substitution
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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);
|
|
138
131
|
}
|
|
139
132
|
}
|
|
140
133
|
}
|
|
@@ -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,19 +139,30 @@ 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
|
-
|
|
153
|
+
if (!Predicate.hasProperty(value, "raw") || !Predicate.isString(value.raw)) return Option.none();
|
|
154
|
+
return Option.liftPredicate(value.raw, isNonEmpty);
|
|
155
|
+
};
|
|
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;
|
|
155
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
|
+
});
|
|
156
166
|
/** Node types that always stand for the same operator tokens. */
|
|
157
167
|
const fixedOperators = new Map([
|
|
158
168
|
["ForStatement", ["for"]],
|
|
@@ -195,35 +205,47 @@ 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
|
};
|
|
@@ -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
|
};
|