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
|
@@ -2,18 +2,24 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { expressionArgument, staticProperties } from "./_call-arguments.js";
|
|
5
6
|
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
7
|
+
import * as Arr from "effect/Array";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
6
9
|
import * as Effect from "effect/Effect";
|
|
7
10
|
//#region src/rules/no-unbounded-concurrency.ts
|
|
11
|
+
/** The first `concurrency: "unbounded"` value in an options object literal. */
|
|
8
12
|
const unboundedConcurrency = (node) => {
|
|
9
|
-
if (node
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if (node.type !== "ObjectExpression") return Option.none();
|
|
14
|
+
return Option.map(Arr.findFirst(staticProperties(node, "concurrency"), (property) => property.value.type === "Literal" && property.value.value === "unbounded"), (property) => property.value);
|
|
15
|
+
};
|
|
16
|
+
const fixedCollection = (node) => node.type === "ArrayExpression" || node.type === "ObjectExpression";
|
|
17
|
+
/** Where `Effect.all` and `Effect.forEach` take their options. */
|
|
18
|
+
const optionsIndex = (callee, effectNamespaces) => {
|
|
19
|
+
if (isStaticMember(callee, effectNamespaces, "forEach")) return Option.some(2);
|
|
20
|
+
if (isStaticMember(callee, effectNamespaces, "all")) return Option.some(1);
|
|
21
|
+
return Option.none();
|
|
15
22
|
};
|
|
16
|
-
const fixedCollection = (node) => node?.type === "ArrayExpression" || node?.type === "ObjectExpression";
|
|
17
23
|
const noUnboundedConcurrency = define({
|
|
18
24
|
name: "no-unbounded-concurrency",
|
|
19
25
|
meta: meta({
|
|
@@ -32,16 +38,15 @@ const noUnboundedConcurrency = define({
|
|
|
32
38
|
CallExpression: (node) => {
|
|
33
39
|
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
34
40
|
const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}));
|
|
41
|
+
if (Option.exists(Arr.head(node.arguments), fixedCollection)) return Effect.void;
|
|
42
|
+
const unbounded = Option.flatMap(optionsIndex(node.callee, namespaces), (index) => Option.flatMap(expressionArgument(node, index), unboundedConcurrency));
|
|
43
|
+
return Option.match(unbounded, {
|
|
44
|
+
onNone: () => Effect.void,
|
|
45
|
+
onSome: (value) => ctx.report(make({
|
|
46
|
+
node: value,
|
|
47
|
+
message: "Bound concurrency for a collection that can grow. Use a finite concurrency value."
|
|
48
|
+
}))
|
|
49
|
+
});
|
|
45
50
|
}
|
|
46
51
|
};
|
|
47
52
|
}
|
|
@@ -2,7 +2,10 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { expressionArgument, staticProperties } from "./_call-arguments.js";
|
|
5
6
|
import { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
7
|
+
import * as Arr from "effect/Array";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
6
9
|
import * as Effect from "effect/Effect";
|
|
7
10
|
//#region src/rules/no-unbounded-retry.ts
|
|
8
11
|
const unboundedConstructors = new Set([
|
|
@@ -12,68 +15,49 @@ const unboundedConstructors = new Set([
|
|
|
12
15
|
]);
|
|
13
16
|
const boundedCombinators = new Set(["take"]);
|
|
14
17
|
const staticPropertyName = (node) => {
|
|
15
|
-
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return;
|
|
16
|
-
return node.property.name;
|
|
17
|
-
};
|
|
18
|
-
const hasOwnProperty = (node, name) => {
|
|
19
|
-
for (const property of node.properties) {
|
|
20
|
-
if (property.type !== "Property" || property.computed) continue;
|
|
21
|
-
if (property.key.type === "Identifier" && property.key.name === name) return true;
|
|
22
|
-
if (property.key.type === "Literal" && property.key.value === name) return true;
|
|
23
|
-
}
|
|
24
|
-
return false;
|
|
25
|
-
};
|
|
26
|
-
const propertyValue = (node, name) => {
|
|
27
|
-
for (const property of node.properties) {
|
|
28
|
-
if (property.type !== "Property" || property.computed) continue;
|
|
29
|
-
if (property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name) return property.value;
|
|
30
|
-
}
|
|
18
|
+
if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return Option.none();
|
|
19
|
+
return Option.some(node.property.name);
|
|
31
20
|
};
|
|
32
21
|
const isBoundedOperation = (node, scheduleNamespaces) => {
|
|
33
22
|
if (node.type !== "CallExpression" || node.callee.type === "Super") return false;
|
|
34
|
-
const
|
|
35
|
-
if (property
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
const bound = node.arguments[0];
|
|
39
|
-
return bound !== void 0 && bound.type !== "SpreadElement" && isStaticCall(bound, scheduleNamespaces, "recurs");
|
|
23
|
+
const callee = node.callee;
|
|
24
|
+
if (Option.exists(staticPropertyName(callee), (property) => boundedCombinators.has(property) && isStaticMember(callee, scheduleNamespaces, property))) return true;
|
|
25
|
+
if (!isStaticMember(callee, scheduleNamespaces, "both")) return false;
|
|
26
|
+
return Option.exists(expressionArgument(node, 0), (bound) => isStaticCall(bound, scheduleNamespaces, "recurs"));
|
|
40
27
|
};
|
|
41
28
|
const unboundedSchedule = (node, scheduleNamespaces) => {
|
|
42
|
-
if (node.type === "MemberExpression" && isStaticMember(node, scheduleNamespaces, "forever")) return node;
|
|
43
|
-
if (node.type !== "CallExpression" || node.callee.type === "Super") return
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
if (property
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
if (node.type === "MemberExpression" && isStaticMember(node, scheduleNamespaces, "forever")) return Option.some(node);
|
|
30
|
+
if (node.type !== "CallExpression" || node.callee.type === "Super") return Option.none();
|
|
31
|
+
const callee = node.callee;
|
|
32
|
+
const property = staticPropertyName(callee);
|
|
33
|
+
if (Option.exists(property, (name) => unboundedConstructors.has(name) && isStaticMember(callee, scheduleNamespaces, name))) return Option.some(node);
|
|
34
|
+
if (!Option.contains(property, "pipe") || callee.type !== "MemberExpression") return Option.none();
|
|
35
|
+
return Option.filter(unboundedSchedule(callee.object, scheduleNamespaces), () => !node.arguments.some((operation) => isBoundedOperation(operation, scheduleNamespaces)));
|
|
36
|
+
};
|
|
37
|
+
/** Arguments a data-last retry call takes; one more means the data-first form. */
|
|
38
|
+
const retryArity = (callee, effectNamespaces, streamNamespaces, httpClientNamespaces) => {
|
|
39
|
+
if (isStaticMember(callee, effectNamespaces, "retryOrElse")) return Option.some(2);
|
|
40
|
+
if (isStaticMember(callee, effectNamespaces, "retry") || isStaticMember(callee, streamNamespaces, "retry") || isStaticMember(callee, httpClientNamespaces, "retryTransient")) return Option.some(1);
|
|
41
|
+
return Option.none();
|
|
42
|
+
};
|
|
43
|
+
/** The policy argument: first in the data-last form, second in the data-first form. */
|
|
44
|
+
const policyArgument = (node, arity) => {
|
|
45
|
+
if (node.arguments.length < arity) return Option.none();
|
|
46
|
+
let policyIndex = 1;
|
|
47
|
+
if (node.arguments.length === arity) policyIndex = 0;
|
|
48
|
+
return Arr.get(node.arguments, policyIndex);
|
|
49
|
+
};
|
|
50
|
+
/** The schedule a policy argument retries on, unless it sets `times`. */
|
|
51
|
+
const policySchedule = (policy) => {
|
|
52
|
+
if (policy.type === "SpreadElement") return Option.none();
|
|
53
|
+
if (policy.type !== "ObjectExpression") return Option.some(policy);
|
|
54
|
+
if (Arr.isReadonlyArrayNonEmpty(staticProperties(policy, "times"))) return Option.none();
|
|
55
|
+
return Option.map(Arr.head(staticProperties(policy, "schedule")), (property) => property.value);
|
|
51
56
|
};
|
|
52
57
|
const retryPolicy = (node, effectNamespaces, streamNamespaces, httpClientNamespaces) => {
|
|
53
|
-
if (node.callee.type === "Super") return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (node.arguments.length === 1) policyIndex = 0;
|
|
57
|
-
if (node.arguments.length >= 2) policyIndex = 1;
|
|
58
|
-
}
|
|
59
|
-
if (isStaticMember(node.callee, effectNamespaces, "retryOrElse")) {
|
|
60
|
-
if (node.arguments.length === 2) policyIndex = 0;
|
|
61
|
-
if (node.arguments.length >= 3) policyIndex = 1;
|
|
62
|
-
}
|
|
63
|
-
if (isStaticMember(node.callee, streamNamespaces, "retry")) {
|
|
64
|
-
if (node.arguments.length === 1) policyIndex = 0;
|
|
65
|
-
if (node.arguments.length >= 2) policyIndex = 1;
|
|
66
|
-
}
|
|
67
|
-
if (isStaticMember(node.callee, httpClientNamespaces, "retryTransient")) {
|
|
68
|
-
if (node.arguments.length === 1) policyIndex = 0;
|
|
69
|
-
if (node.arguments.length >= 2) policyIndex = 1;
|
|
70
|
-
}
|
|
71
|
-
if (policyIndex === void 0) return void 0;
|
|
72
|
-
const policy = node.arguments[policyIndex];
|
|
73
|
-
if (policy?.type === "SpreadElement") return void 0;
|
|
74
|
-
if (policy?.type !== "ObjectExpression") return policy;
|
|
75
|
-
if (hasOwnProperty(policy, "times")) return void 0;
|
|
76
|
-
return propertyValue(policy, "schedule");
|
|
58
|
+
if (node.callee.type === "Super") return Option.none();
|
|
59
|
+
const arity = retryArity(node.callee, effectNamespaces, streamNamespaces, httpClientNamespaces);
|
|
60
|
+
return Option.flatMap(Option.flatMap(arity, (count) => policyArgument(node, count)), policySchedule);
|
|
77
61
|
};
|
|
78
62
|
const noUnboundedRetry = define({
|
|
79
63
|
name: "no-unbounded-retry",
|
|
@@ -99,13 +83,14 @@ const noUnboundedRetry = define({
|
|
|
99
83
|
CallExpression: (node) => {
|
|
100
84
|
if (node.type !== "CallExpression") return Effect.void;
|
|
101
85
|
const policy = retryPolicy(node, visibleNamespaces(ctx, node, effectNamespaces), visibleNamespaces(ctx, node, streamNamespaces), visibleNamespaces(ctx, node, httpClientNamespaces));
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
86
|
+
const unbounded = Option.flatMap(policy, (schedule) => unboundedSchedule(schedule, visibleNamespaces(ctx, node, scheduleNamespaces)));
|
|
87
|
+
return Option.match(unbounded, {
|
|
88
|
+
onNone: () => Effect.void,
|
|
89
|
+
onSome: (schedule) => ctx.report(make({
|
|
90
|
+
node: schedule,
|
|
91
|
+
message: "Bound retry attempts or elapsed time. Add times or compose the schedule with Schedule.recurs or Schedule.take."
|
|
92
|
+
}))
|
|
93
|
+
});
|
|
109
94
|
}
|
|
110
95
|
};
|
|
111
96
|
}
|
|
@@ -3,6 +3,7 @@ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
5
|
import { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation } from "./_anti-slop-function-parameters.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
//#region src/rules/no-unknown-parameters.ts
|
|
8
9
|
function isTypePredicateSubject(owner, parameterName) {
|
|
@@ -34,12 +35,13 @@ const noUnknownParameters = define({
|
|
|
34
35
|
const context = yield* RuleContext;
|
|
35
36
|
const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
|
|
36
37
|
const annotation = functionParameterTypeAnnotation(parameter);
|
|
37
|
-
if (annotation
|
|
38
|
-
|
|
38
|
+
if (Option.isNone(annotation)) return Effect.void;
|
|
39
|
+
const type = annotation.value.typeAnnotation;
|
|
40
|
+
if (!containsUnknownType(type)) return Effect.void;
|
|
39
41
|
const name = functionParameterBindingName(parameter, context.sourceCode);
|
|
40
42
|
if (name === "cause" || isTypePredicateSubject(node, name) || parameter === node.params[0] && isRejectionHandler(node)) return Effect.void;
|
|
41
43
|
return context.report(fromId({
|
|
42
|
-
node:
|
|
44
|
+
node: type,
|
|
43
45
|
messageId: "unknownParameter",
|
|
44
46
|
data: { parameter: name }
|
|
45
47
|
}));
|
|
@@ -2,7 +2,11 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
5
6
|
import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
|
|
7
|
+
import * as Arr from "effect/Array";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Predicate from "effect/Predicate";
|
|
6
10
|
import * as Effect from "effect/Effect";
|
|
7
11
|
//#region src/rules/no-unknown-returns.ts
|
|
8
12
|
/**
|
|
@@ -13,12 +17,10 @@ import * as Effect from "effect/Effect";
|
|
|
13
17
|
*/
|
|
14
18
|
function isInsideTypePattern(node) {
|
|
15
19
|
let child = node;
|
|
16
|
-
|
|
17
|
-
while (parent !== null && parent.type !== "Program") {
|
|
20
|
+
for (const parent of ancestors(node)) {
|
|
18
21
|
if (parent.type === "TSConditionalType" && parent.extendsType === child) return true;
|
|
19
22
|
if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
|
|
20
23
|
child = parent;
|
|
21
|
-
parent = parent.parent;
|
|
22
24
|
}
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
@@ -32,18 +34,17 @@ const noUnknownReturns = define({
|
|
|
32
34
|
}),
|
|
33
35
|
create: function* () {
|
|
34
36
|
const context = yield* RuleContext;
|
|
35
|
-
let environment =
|
|
36
|
-
const resolvesToUnknown = (type) => environment
|
|
37
|
+
let environment = Option.none();
|
|
38
|
+
const resolvesToUnknown = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
|
|
37
39
|
if (resolved.type === "TSUnknownKeyword") return true;
|
|
38
40
|
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
39
41
|
if (resolved.type === "TSUnionType") return resolved.types.some(matches);
|
|
40
42
|
if (resolved.type !== "TSTypeReference" || resolved.typeName.type !== "Identifier" || resolved.typeName.name !== "Promise" && resolved.typeName.name !== "PromiseLike") return false;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
43
|
+
return Option.exists(Arr.head(resolved.typeArguments?.params ?? []), matches);
|
|
44
|
+
}));
|
|
44
45
|
const checkReturnType = (node) => {
|
|
45
46
|
const annotation = node.returnType;
|
|
46
|
-
if (annotation
|
|
47
|
+
if (Predicate.isNullish(annotation)) return Effect.void;
|
|
47
48
|
if (!resolvesToUnknown(annotation.typeAnnotation)) return Effect.void;
|
|
48
49
|
if (isInsideTypePattern(node)) return Effect.void;
|
|
49
50
|
return context.report(fromId({
|
|
@@ -53,7 +54,7 @@ const noUnknownReturns = define({
|
|
|
53
54
|
};
|
|
54
55
|
return {
|
|
55
56
|
Program: (node) => {
|
|
56
|
-
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
57
|
+
environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
|
|
57
58
|
return Effect.void;
|
|
58
59
|
},
|
|
59
60
|
ArrowFunctionExpression: checkReturnType,
|
|
@@ -3,6 +3,7 @@ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
5
|
import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
//#region src/rules/no-unknown-type-aliases.ts
|
|
8
9
|
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
@@ -15,22 +16,25 @@ const noUnknownTypeAliases = define({
|
|
|
15
16
|
}),
|
|
16
17
|
create: function* () {
|
|
17
18
|
const context = yield* RuleContext;
|
|
18
|
-
let environment =
|
|
19
|
-
const resolvesToUnknown = (type) => environment
|
|
19
|
+
let environment = Option.none();
|
|
20
|
+
const resolvesToUnknown = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
|
|
20
21
|
if (resolved.type === "TSUnknownKeyword") return true;
|
|
21
22
|
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
22
23
|
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
23
|
-
});
|
|
24
|
+
}));
|
|
24
25
|
return {
|
|
25
26
|
Program: (node) => {
|
|
26
|
-
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
27
|
+
environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
|
|
27
28
|
return Effect.void;
|
|
28
29
|
},
|
|
29
|
-
TSTypeAliasDeclaration: (node) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
TSTypeAliasDeclaration: (node) => {
|
|
31
|
+
if (!resolvesToUnknown(node.typeAnnotation)) return Effect.void;
|
|
32
|
+
return context.report(fromId({
|
|
33
|
+
node: node.id,
|
|
34
|
+
messageId: "unknownAlias",
|
|
35
|
+
data: { alias: node.id.name }
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
});
|
|
@@ -2,8 +2,12 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
5
6
|
import { visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
|
|
6
7
|
import { classifyUnsafeDictionary, classifyUnsafeDictionaryValue, createTypeEnvironment } from "./_anti-slop-dictionary-types.js";
|
|
8
|
+
import * as Arr from "effect/Array";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import * as Predicate from "effect/Predicate";
|
|
7
11
|
import * as Effect from "effect/Effect";
|
|
8
12
|
//#region src/rules/no-unsafe-dictionary-type.ts
|
|
9
13
|
const typeNodeKinds = new Set([
|
|
@@ -49,41 +53,30 @@ function isTypeNode(node) {
|
|
|
49
53
|
return typeNodeKinds.has(node.type);
|
|
50
54
|
}
|
|
51
55
|
function typeReferenceName(type) {
|
|
52
|
-
|
|
56
|
+
if (type.typeName.type !== "Identifier") return Option.none();
|
|
57
|
+
return Option.some(type.typeName.name);
|
|
53
58
|
}
|
|
54
59
|
function isInsideTypeAliasDeclaration(node) {
|
|
55
|
-
|
|
56
|
-
while (current !== null && current.type !== "Program") {
|
|
57
|
-
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
58
|
-
current = current.parent;
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
60
|
+
return Option.isSome(Arr.findFirst(ancestors(node), (ancestor) => ancestor.type === "TSTypeAliasDeclaration"));
|
|
61
61
|
}
|
|
62
62
|
function isPlainAliasConsumerUse(node, environment) {
|
|
63
63
|
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
64
|
-
|
|
65
|
-
return name !== null && visibleTypeAlias(name, node, environment.typeAliases) !== null && !isInsideTypeAliasDeclaration(node);
|
|
64
|
+
return Option.exists(typeReferenceName(node), (name) => Option.isSome(visibleTypeAlias(name, node, environment.typeAliases))) && !isInsideTypeAliasDeclaration(node);
|
|
66
65
|
}
|
|
67
66
|
function isInsideTypeParameterConstraint(node) {
|
|
68
67
|
let child = node;
|
|
69
|
-
|
|
70
|
-
while (parent !== null && parent.type !== "Program") {
|
|
68
|
+
for (const parent of ancestors(node)) {
|
|
71
69
|
if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
|
|
72
70
|
child = parent;
|
|
73
|
-
parent = child.parent;
|
|
74
71
|
}
|
|
75
72
|
return false;
|
|
76
73
|
}
|
|
77
74
|
function shouldReportType(node, environment) {
|
|
78
75
|
if (isInsideTypeParameterConstraint(node)) return false;
|
|
79
76
|
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
80
|
-
if (classifyUnsafeDictionary(node, environment)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
|
|
84
|
-
current = current.parent;
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
77
|
+
if (Option.isNone(classifyUnsafeDictionary(node, environment))) return false;
|
|
78
|
+
const unsafeAncestor = Arr.findFirst(ancestors(node), (ancestor) => isTypeNode(ancestor) && Option.isSome(classifyUnsafeDictionary(ancestor, environment)));
|
|
79
|
+
return Option.isNone(unsafeAncestor);
|
|
87
80
|
}
|
|
88
81
|
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
89
82
|
const noUnsafeDictionaryType = define({
|
|
@@ -95,29 +88,31 @@ const noUnsafeDictionaryType = define({
|
|
|
95
88
|
}),
|
|
96
89
|
create: function* () {
|
|
97
90
|
const context = yield* RuleContext;
|
|
98
|
-
let environment =
|
|
91
|
+
let environment = Option.none();
|
|
99
92
|
const report = (node, value) => context.report(fromId({
|
|
100
93
|
node,
|
|
101
94
|
messageId: "unsafeDictionary",
|
|
102
95
|
data: { value }
|
|
103
96
|
}));
|
|
97
|
+
const reportUnsafe = (node, unsafe) => Option.match(unsafe, {
|
|
98
|
+
onNone: () => Effect.void,
|
|
99
|
+
onSome: (found) => report(node, found.unsafeValue)
|
|
100
|
+
});
|
|
104
101
|
const reportIfUnsafe = (node) => {
|
|
105
|
-
if (environment
|
|
106
|
-
|
|
107
|
-
return unsafe === null ? Effect.void : report(node, unsafe.unsafeValue);
|
|
102
|
+
if (Option.isNone(environment) || !shouldReportType(node, environment.value)) return Effect.void;
|
|
103
|
+
return reportUnsafe(node, classifyUnsafeDictionary(node, environment.value));
|
|
108
104
|
};
|
|
109
105
|
return {
|
|
110
106
|
Program: (node) => {
|
|
111
|
-
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
|
|
107
|
+
environment = Option.some(createTypeEnvironment(node, context.sourceCode.visitorKeys));
|
|
112
108
|
return Effect.void;
|
|
113
109
|
},
|
|
114
110
|
TSTypeReference: reportIfUnsafe,
|
|
115
111
|
TSTypeLiteral: reportIfUnsafe,
|
|
116
112
|
TSMappedType: reportIfUnsafe,
|
|
117
113
|
TSIndexSignature: (node) => {
|
|
118
|
-
if (environment
|
|
119
|
-
|
|
120
|
-
return unsafe === null ? Effect.void : report(node, unsafe.unsafeValue);
|
|
114
|
+
if (Option.isNone(environment) || Predicate.isNull(node.typeAnnotation) || node.parent?.type === "TSTypeLiteral") return Effect.void;
|
|
115
|
+
return reportUnsafe(node, classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment.value));
|
|
121
116
|
}
|
|
122
117
|
};
|
|
123
118
|
}
|