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
|
@@ -2,13 +2,10 @@ 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 { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
5
7
|
import * as Effect from "effect/Effect";
|
|
6
8
|
//#region src/rules/no-unknown-type-aliases.ts
|
|
7
|
-
function referencedAliasName(type) {
|
|
8
|
-
if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
|
|
9
|
-
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
10
|
-
return type.typeArguments === null || type.typeArguments === void 0 || type.typeArguments.params.length === 0 ? type.typeName.name : null;
|
|
11
|
-
}
|
|
12
9
|
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
13
10
|
const noUnknownTypeAliases = define({
|
|
14
11
|
name: "no-unknown-type-aliases",
|
|
@@ -19,29 +16,26 @@ const noUnknownTypeAliases = define({
|
|
|
19
16
|
}),
|
|
20
17
|
create: function* () {
|
|
21
18
|
const context = yield* RuleContext;
|
|
22
|
-
|
|
23
|
-
const resolvesToUnknown = (type
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
let environment = Option.none();
|
|
20
|
+
const resolvesToUnknown = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
|
|
21
|
+
if (resolved.type === "TSUnknownKeyword") return true;
|
|
22
|
+
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
23
|
+
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
24
|
+
}));
|
|
25
|
+
return {
|
|
26
|
+
Program: (node) => {
|
|
27
|
+
environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
|
|
28
|
+
return Effect.void;
|
|
29
|
+
},
|
|
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
|
+
}));
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
node: alias.id,
|
|
41
|
-
messageId: "unknownAlias",
|
|
42
|
-
data: { alias: alias.id.name }
|
|
43
|
-
})) : Effect.void, { discard: true });
|
|
44
|
-
} };
|
|
38
|
+
};
|
|
45
39
|
}
|
|
46
40
|
});
|
|
47
41
|
//#endregion
|
|
@@ -2,37 +2,81 @@ 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";
|
|
6
|
+
import { visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
|
|
5
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";
|
|
6
11
|
import * as Effect from "effect/Effect";
|
|
7
12
|
//#region src/rules/no-unsafe-dictionary-type.ts
|
|
13
|
+
const typeNodeKinds = new Set([
|
|
14
|
+
"JSDocNonNullableType",
|
|
15
|
+
"JSDocNullableType",
|
|
16
|
+
"JSDocUnknownType",
|
|
17
|
+
"TSAnyKeyword",
|
|
18
|
+
"TSArrayType",
|
|
19
|
+
"TSBigIntKeyword",
|
|
20
|
+
"TSBooleanKeyword",
|
|
21
|
+
"TSConditionalType",
|
|
22
|
+
"TSConstructorType",
|
|
23
|
+
"TSFunctionType",
|
|
24
|
+
"TSImportType",
|
|
25
|
+
"TSIndexedAccessType",
|
|
26
|
+
"TSInferType",
|
|
27
|
+
"TSIntersectionType",
|
|
28
|
+
"TSIntrinsicKeyword",
|
|
29
|
+
"TSLiteralType",
|
|
30
|
+
"TSMappedType",
|
|
31
|
+
"TSNamedTupleMember",
|
|
32
|
+
"TSNeverKeyword",
|
|
33
|
+
"TSNullKeyword",
|
|
34
|
+
"TSNumberKeyword",
|
|
35
|
+
"TSObjectKeyword",
|
|
36
|
+
"TSParenthesizedType",
|
|
37
|
+
"TSStringKeyword",
|
|
38
|
+
"TSSymbolKeyword",
|
|
39
|
+
"TSTemplateLiteralType",
|
|
40
|
+
"TSThisType",
|
|
41
|
+
"TSTupleType",
|
|
42
|
+
"TSTypeLiteral",
|
|
43
|
+
"TSTypeOperator",
|
|
44
|
+
"TSTypePredicate",
|
|
45
|
+
"TSTypeQuery",
|
|
46
|
+
"TSTypeReference",
|
|
47
|
+
"TSUndefinedKeyword",
|
|
48
|
+
"TSUnionType",
|
|
49
|
+
"TSUnknownKeyword",
|
|
50
|
+
"TSVoidKeyword"
|
|
51
|
+
]);
|
|
8
52
|
function isTypeNode(node) {
|
|
9
|
-
return
|
|
53
|
+
return typeNodeKinds.has(node.type);
|
|
10
54
|
}
|
|
11
55
|
function typeReferenceName(type) {
|
|
12
|
-
|
|
56
|
+
if (type.typeName.type !== "Identifier") return Option.none();
|
|
57
|
+
return Option.some(type.typeName.name);
|
|
13
58
|
}
|
|
14
59
|
function isInsideTypeAliasDeclaration(node) {
|
|
15
|
-
|
|
16
|
-
while (current !== null && current.type !== "Program") {
|
|
17
|
-
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
18
|
-
current = current.parent;
|
|
19
|
-
}
|
|
20
|
-
return false;
|
|
60
|
+
return Option.isSome(Arr.findFirst(ancestors(node), (ancestor) => ancestor.type === "TSTypeAliasDeclaration"));
|
|
21
61
|
}
|
|
22
62
|
function isPlainAliasConsumerUse(node, environment) {
|
|
23
63
|
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
24
|
-
|
|
25
|
-
|
|
64
|
+
return Option.exists(typeReferenceName(node), (name) => Option.isSome(visibleTypeAlias(name, node, environment.typeAliases))) && !isInsideTypeAliasDeclaration(node);
|
|
65
|
+
}
|
|
66
|
+
function isInsideTypeParameterConstraint(node) {
|
|
67
|
+
let child = node;
|
|
68
|
+
for (const parent of ancestors(node)) {
|
|
69
|
+
if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
|
|
70
|
+
child = parent;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
26
73
|
}
|
|
27
74
|
function shouldReportType(node, environment) {
|
|
75
|
+
if (isInsideTypeParameterConstraint(node)) return false;
|
|
28
76
|
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
29
|
-
if (classifyUnsafeDictionary(node, environment)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
|
|
33
|
-
current = current.parent;
|
|
34
|
-
}
|
|
35
|
-
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);
|
|
36
80
|
}
|
|
37
81
|
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
38
82
|
const noUnsafeDictionaryType = define({
|
|
@@ -44,29 +88,31 @@ const noUnsafeDictionaryType = define({
|
|
|
44
88
|
}),
|
|
45
89
|
create: function* () {
|
|
46
90
|
const context = yield* RuleContext;
|
|
47
|
-
let environment =
|
|
91
|
+
let environment = Option.none();
|
|
48
92
|
const report = (node, value) => context.report(fromId({
|
|
49
93
|
node,
|
|
50
94
|
messageId: "unsafeDictionary",
|
|
51
95
|
data: { value }
|
|
52
96
|
}));
|
|
97
|
+
const reportUnsafe = (node, unsafe) => Option.match(unsafe, {
|
|
98
|
+
onNone: () => Effect.void,
|
|
99
|
+
onSome: (found) => report(node, found.unsafeValue)
|
|
100
|
+
});
|
|
53
101
|
const reportIfUnsafe = (node) => {
|
|
54
|
-
if (environment
|
|
55
|
-
|
|
56
|
-
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));
|
|
57
104
|
};
|
|
58
105
|
return {
|
|
59
106
|
Program: (node) => {
|
|
60
|
-
environment = createTypeEnvironment(node);
|
|
107
|
+
environment = Option.some(createTypeEnvironment(node, context.sourceCode.visitorKeys));
|
|
61
108
|
return Effect.void;
|
|
62
109
|
},
|
|
63
110
|
TSTypeReference: reportIfUnsafe,
|
|
64
111
|
TSTypeLiteral: reportIfUnsafe,
|
|
65
112
|
TSMappedType: reportIfUnsafe,
|
|
66
113
|
TSIndexSignature: (node) => {
|
|
67
|
-
if (environment
|
|
68
|
-
|
|
69
|
-
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));
|
|
70
116
|
}
|
|
71
117
|
};
|
|
72
118
|
}
|
|
@@ -2,11 +2,14 @@ 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";
|
|
6
|
+
import * as Arr from "effect/Array";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
5
8
|
import * as Effect from "effect/Effect";
|
|
6
9
|
//#region src/rules/no-widen-then-assert.ts
|
|
7
10
|
/**
|
|
8
11
|
* Ported from dmmulroy/anti-slop at
|
|
9
|
-
*
|
|
12
|
+
* c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
|
|
10
13
|
*/
|
|
11
14
|
const functionBoundaryTypes = new Set([
|
|
12
15
|
"ArrowFunctionExpression",
|
|
@@ -15,6 +18,7 @@ const functionBoundaryTypes = new Set([
|
|
|
15
18
|
"TSDeclareFunction",
|
|
16
19
|
"TSEmptyBodyFunctionExpression"
|
|
17
20
|
]);
|
|
21
|
+
const sameBoundary = Option.makeEquivalence((left, right) => left === right);
|
|
18
22
|
function unwrapExpressionParentheses(expression) {
|
|
19
23
|
let current = expression;
|
|
20
24
|
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
@@ -26,7 +30,20 @@ function unwrapTypeParentheses(type) {
|
|
|
26
30
|
return current;
|
|
27
31
|
}
|
|
28
32
|
function typeReferenceName(type) {
|
|
29
|
-
|
|
33
|
+
if (type.typeName.type !== "Identifier") return Option.none();
|
|
34
|
+
return Option.some(type.typeName.name);
|
|
35
|
+
}
|
|
36
|
+
function isReferenceNamed(type, name) {
|
|
37
|
+
return Option.contains(typeReferenceName(type), name);
|
|
38
|
+
}
|
|
39
|
+
/** The two type arguments of a reference such as `Record<K, V>`, when it has exactly two. */
|
|
40
|
+
function typeArgumentPair(reference) {
|
|
41
|
+
const parameters = reference.typeArguments?.params ?? [];
|
|
42
|
+
if (parameters.length !== 2) return Option.none();
|
|
43
|
+
return Option.all([Arr.get(parameters, 0), Arr.get(parameters, 1)]);
|
|
44
|
+
}
|
|
45
|
+
function firstTypeArgument(reference) {
|
|
46
|
+
return Arr.head(reference.typeArguments?.params ?? []);
|
|
30
47
|
}
|
|
31
48
|
function isUnknownOrAnyType(type) {
|
|
32
49
|
const unwrapped = unwrapTypeParentheses(type);
|
|
@@ -36,42 +53,45 @@ function isBroadRecordKeyType(type) {
|
|
|
36
53
|
const unwrapped = unwrapTypeParentheses(type);
|
|
37
54
|
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
38
55
|
if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
|
|
39
|
-
return unwrapped.type === "TSTypeReference" &&
|
|
56
|
+
return unwrapped.type === "TSTypeReference" && isReferenceNamed(unwrapped, "PropertyKey");
|
|
57
|
+
}
|
|
58
|
+
function isBroadRecordReference(reference) {
|
|
59
|
+
if (isReferenceNamed(reference, "Readonly")) return Option.exists(firstTypeArgument(reference), isBroadRecordType);
|
|
60
|
+
if (!isReferenceNamed(reference, "Record")) return false;
|
|
61
|
+
return Option.exists(typeArgumentPair(reference), ([key, value]) => isBroadRecordKeyType(key) && isUnknownOrAnyType(value));
|
|
62
|
+
}
|
|
63
|
+
function isBroadIndexSignature(member) {
|
|
64
|
+
return member.parameters.length === 1 && Option.exists(Arr.head(member.parameters), (parameter) => isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation)) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
|
|
40
65
|
}
|
|
41
66
|
function isBroadRecordType(type) {
|
|
42
67
|
const unwrapped = unwrapTypeParentheses(type);
|
|
43
|
-
if (unwrapped.type === "TSTypeReference")
|
|
44
|
-
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
45
|
-
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
46
|
-
return inner !== void 0 && isBroadRecordType(inner);
|
|
47
|
-
}
|
|
48
|
-
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
49
|
-
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
50
|
-
return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
|
|
51
|
-
}
|
|
68
|
+
if (unwrapped.type === "TSTypeReference") return isBroadRecordReference(unwrapped);
|
|
52
69
|
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
|
|
53
|
-
|
|
54
|
-
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
|
55
|
-
return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
|
|
70
|
+
return Option.exists(Arr.head(unwrapped.members), (member) => member.type === "TSIndexSignature" && isBroadIndexSignature(member));
|
|
56
71
|
}
|
|
57
72
|
function broadTypeKind(type) {
|
|
58
73
|
const unwrapped = unwrapTypeParentheses(type);
|
|
59
|
-
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
|
|
60
|
-
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
61
|
-
|
|
74
|
+
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return Option.some("top");
|
|
75
|
+
if (unwrapped.type === "TSObjectKeyword") return Option.some("object");
|
|
76
|
+
if (isBroadRecordType(unwrapped)) return Option.some("record");
|
|
77
|
+
return Option.none();
|
|
78
|
+
}
|
|
79
|
+
function isBroadType(type) {
|
|
80
|
+
return Option.isSome(broadTypeKind(type));
|
|
62
81
|
}
|
|
63
82
|
function assertedExpression(node) {
|
|
64
83
|
return unwrapExpressionParentheses(node.expression);
|
|
65
84
|
}
|
|
66
85
|
function assertionFromExpression(expression) {
|
|
67
86
|
const unwrapped = unwrapExpressionParentheses(expression);
|
|
68
|
-
|
|
87
|
+
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") return Option.some(unwrapped);
|
|
88
|
+
return Option.none();
|
|
69
89
|
}
|
|
70
90
|
function normalizedTypeText(sourceText, type) {
|
|
71
91
|
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
|
|
72
92
|
}
|
|
73
93
|
function typesHaveSameSyntax(sourceText, left, right) {
|
|
74
|
-
return left
|
|
94
|
+
return Option.exists(left, (type) => normalizedTypeText(sourceText, unwrapTypeParentheses(type)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right)));
|
|
75
95
|
}
|
|
76
96
|
function isDefinitelyObjectType(type) {
|
|
77
97
|
const unwrapped = unwrapTypeParentheses(type);
|
|
@@ -92,74 +112,79 @@ function isDefinitelyNarrowerRecordType(type) {
|
|
|
92
112
|
const unwrapped = unwrapTypeParentheses(type);
|
|
93
113
|
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
|
94
114
|
if (unwrapped.type !== "TSTypeReference") return false;
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
100
|
-
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
101
|
-
return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
|
|
115
|
+
if (isReferenceNamed(unwrapped, "Readonly")) return Option.exists(firstTypeArgument(unwrapped), isDefinitelyNarrowerRecordType);
|
|
116
|
+
if (!isReferenceNamed(unwrapped, "Record")) return false;
|
|
117
|
+
return Option.exists(typeArgumentPair(unwrapped), ([, value]) => !isUnknownOrAnyType(value));
|
|
102
118
|
}
|
|
103
119
|
function functionBoundary(node) {
|
|
104
|
-
|
|
105
|
-
while (current !== null && current.type !== "Program") {
|
|
106
|
-
if (functionBoundaryTypes.has(current.type)) return current;
|
|
107
|
-
current = current.parent;
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
120
|
+
return Arr.findFirst(ancestors(node), (ancestor) => functionBoundaryTypes.has(ancestor.type));
|
|
110
121
|
}
|
|
111
122
|
function resolvedVariableForIdentifier(scopes, identifier) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (reference !== void 0) return reference.resolved;
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
123
|
+
const reference = Arr.findFirst(scopes, (scope) => Arr.findFirst(scope.references, (candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end));
|
|
124
|
+
return Option.flatMap(reference, (found) => Option.fromNullishOr(found.resolved));
|
|
117
125
|
}
|
|
118
126
|
function variableDeclarator(variable) {
|
|
119
|
-
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
|
|
120
|
-
return
|
|
127
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return Option.some(definition.node);
|
|
128
|
+
return Option.none();
|
|
129
|
+
}
|
|
130
|
+
function isConstDeclarator(declarator) {
|
|
131
|
+
return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const";
|
|
132
|
+
}
|
|
133
|
+
function isReassigned(variable) {
|
|
134
|
+
return variable.references.some((reference) => reference.isWrite() && !reference.init);
|
|
121
135
|
}
|
|
122
136
|
function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
|
|
123
137
|
const unwrapped = unwrapExpressionParentheses(expression);
|
|
124
138
|
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
|
125
|
-
if (
|
|
126
|
-
return { type: unwrapped.typeAnnotation };
|
|
139
|
+
if (isBroadType(unwrapped.typeAnnotation)) return Option.none();
|
|
140
|
+
return Option.some({ type: Option.some(unwrapped.typeAnnotation) });
|
|
127
141
|
}
|
|
128
|
-
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type:
|
|
129
|
-
if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type:
|
|
130
|
-
if (unwrapped.type
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
142
|
+
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return Option.some({ type: Option.none() });
|
|
143
|
+
if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return Option.some({ type: Option.none() });
|
|
144
|
+
if (unwrapped.type === "Identifier") return identifierValueEvidence(unwrapped, scopes, boundary, visitedVariables);
|
|
145
|
+
return Option.none();
|
|
146
|
+
}
|
|
147
|
+
function identifierValueEvidence(expression, scopes, boundary, visitedVariables) {
|
|
148
|
+
return resolvedVariableForIdentifier(scopes, expression).pipe(Option.filter((variable) => !visitedVariables.has(variable)), Option.flatMap((variable) => Option.match(annotatedIdentifier(variable), {
|
|
149
|
+
onSome: ([identifier, annotation]) => annotatedValueEvidence(identifier, annotation, boundary),
|
|
150
|
+
onNone: () => declaredValueEvidence(variable, scopes, boundary, visitedVariables)
|
|
151
|
+
})));
|
|
152
|
+
}
|
|
153
|
+
/** The first declaration identifier of a variable that carries a type annotation. */
|
|
154
|
+
function annotatedIdentifier(variable) {
|
|
155
|
+
return Arr.findFirst(variable.identifiers, (identifier) => Option.map(Option.fromNullishOr(identifier.typeAnnotation), (annotation) => [identifier, annotation.typeAnnotation]));
|
|
156
|
+
}
|
|
157
|
+
function annotatedValueEvidence(identifier, annotation, boundary) {
|
|
158
|
+
if (!sameBoundary(functionBoundary(identifier), boundary) || isBroadType(annotation)) return Option.none();
|
|
159
|
+
return Option.some({ type: Option.some(annotation) });
|
|
160
|
+
}
|
|
161
|
+
function declaredValueEvidence(variable, scopes, boundary, visitedVariables) {
|
|
162
|
+
return variableDeclarator(variable).pipe(Option.filter((declarator) => isConstDeclarator(declarator) && !isReassigned(variable) && sameBoundary(functionBoundary(declarator), boundary)), Option.flatMap((declarator) => Option.fromNullishOr(declarator.init)), Option.flatMap((init) => knownValueEvidence(init, scopes, boundary, new Set([...visitedVariables, variable]))));
|
|
142
163
|
}
|
|
143
164
|
function widenedBinding(variable, scopes) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
return variableDeclarator(variable).pipe(Option.filter((declarator) => isConstDeclarator(declarator) && declarator.id.type === "Identifier" && !isReassigned(variable)), Option.flatMap((declarator) => Option.flatMap(Option.fromNullishOr(declarator.init), (init) => widenedDeclarator(variable, declarator, init, scopes))));
|
|
166
|
+
}
|
|
167
|
+
function widenedDeclarator(variable, declarator, init, scopes) {
|
|
168
|
+
const identifier = Arr.findFirst(variable.identifiers, (candidate) => candidate.start === declarator.id.start && candidate.end === declarator.id.end);
|
|
147
169
|
const boundary = functionBoundary(declarator);
|
|
148
|
-
const declaredType = identifier
|
|
149
|
-
const initializerAssertion = assertionFromExpression(
|
|
150
|
-
const initializerBroadKind = initializerAssertion
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
const declaredType = identifier.pipe(Option.flatMap((found) => Option.fromNullishOr(found.typeAnnotation)), Option.map((annotation) => annotation.typeAnnotation));
|
|
171
|
+
const initializerAssertion = assertionFromExpression(init);
|
|
172
|
+
const initializerBroadKind = Option.flatMap(initializerAssertion, (assertion) => broadTypeKind(assertion.typeAnnotation));
|
|
173
|
+
const declaredBroadKind = Option.flatMap(declaredType, broadTypeKind);
|
|
174
|
+
const broadKind = Option.orElse(declaredBroadKind, () => initializerBroadKind);
|
|
175
|
+
const originalExpression = Option.match(Option.zipLeft(initializerAssertion, initializerBroadKind), {
|
|
176
|
+
onNone: () => init,
|
|
177
|
+
onSome: assertedExpression
|
|
178
|
+
});
|
|
179
|
+
return Option.flatMap(broadKind, (kind) => Option.map(knownValueEvidence(originalExpression, scopes, boundary, new Set([variable])), (evidence) => ({
|
|
180
|
+
broadKind: kind,
|
|
156
181
|
evidence,
|
|
157
182
|
declaredAt: declarator.end,
|
|
158
183
|
boundary
|
|
159
|
-
};
|
|
184
|
+
})));
|
|
160
185
|
}
|
|
161
186
|
function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
|
|
162
|
-
if (
|
|
187
|
+
if (isBroadType(assertedType)) return false;
|
|
163
188
|
if (broadKind === "top") return true;
|
|
164
189
|
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
|
|
165
190
|
if (broadKind === "object") return isDefinitelyObjectType(assertedType);
|
|
@@ -179,10 +204,8 @@ const noWidenThenAssert = define({
|
|
|
179
204
|
const checkAssertion = (node) => {
|
|
180
205
|
const expression = assertedExpression(node);
|
|
181
206
|
if (expression.type !== "Identifier") return Effect.void;
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
const widened = widenedBinding(variable, scopes);
|
|
185
|
-
if (widened === null || node.start <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) return Effect.void;
|
|
207
|
+
const widened = Option.flatMap(resolvedVariableForIdentifier(scopes, expression), (variable) => widenedBinding(variable, scopes));
|
|
208
|
+
if (!Option.exists(widened, (binding) => node.start > binding.declaredAt && sameBoundary(functionBoundary(node), binding.boundary) && assertionIsNarrower(context.sourceCode.text, binding.broadKind, binding.evidence, node.typeAnnotation))) return Effect.void;
|
|
186
209
|
return context.report(fromId({
|
|
187
210
|
node,
|
|
188
211
|
messageId: "widenThenAssert",
|
|
@@ -3,51 +3,43 @@ 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 { isEffectCall, tagComparisonsInOr, taggedSwitchSubject } from "./_tagged-control-flow.js";
|
|
6
|
+
import * as Arr from "effect/Array";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Predicate from "effect/Predicate";
|
|
6
9
|
import * as Effect from "effect/Effect";
|
|
7
10
|
//#region src/rules/prefer-catch-tag.ts
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const comparisons = tagComparisonsInOr(expression);
|
|
23
|
-
if (comparisons === null || comparisons.length === 0) return false;
|
|
24
|
-
return comparisons.every((comparison) => comparison.subject === parameter.name);
|
|
25
|
-
};
|
|
26
|
-
const isFunction = (argument) => argument?.type === "ArrowFunctionExpression" || argument?.type === "FunctionExpression";
|
|
27
|
-
const hasTagCondition = (node, parameter) => {
|
|
28
|
-
const comparisons = tagComparisonsInOr(node.test);
|
|
29
|
-
if (comparisons === null || comparisons.length === 0) return false;
|
|
30
|
-
return comparisons.every((comparison) => comparison.subject === parameter);
|
|
31
|
-
};
|
|
11
|
+
const isFunction = (argument) => argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression";
|
|
12
|
+
const isIdentifier = (parameter) => parameter.type === "Identifier";
|
|
13
|
+
/** The name of a callback's first parameter when it is a plain identifier. */
|
|
14
|
+
const identifierParameter = (fn) => Arr.head(fn.params).pipe(Option.filter(isIdentifier), Option.map((parameter) => parameter.name));
|
|
15
|
+
/** The expression a callback returns when its body is that expression or a lone `return`. */
|
|
16
|
+
const returnedExpression = (fn) => Option.flatMap(Option.fromNullishOr(fn.body), (body) => {
|
|
17
|
+
if (body.type !== "BlockStatement") return Option.some(body);
|
|
18
|
+
const [statement] = body.body;
|
|
19
|
+
if (body.body.length !== 1 || statement?.type !== "ReturnStatement") return Option.none();
|
|
20
|
+
return Option.fromNullishOr(statement.argument);
|
|
21
|
+
});
|
|
22
|
+
/** Whether `expression` is one or more `parameter._tag === "Tag"` checks joined by `||`. */
|
|
23
|
+
const isTagCheckOn = (expression, parameter) => Option.exists(tagComparisonsInOr(expression), (comparisons) => comparisons.every((comparison) => comparison.subject === parameter));
|
|
24
|
+
const hasManualTagPredicate = (argument) => isFunction(argument) && Option.exists(Option.all([identifierParameter(argument), returnedExpression(argument)]), ([parameter, expression]) => isTagCheckOn(expression, parameter));
|
|
32
25
|
const hasTagSwitch = (node, parameter) => {
|
|
33
|
-
if (taggedSwitchSubject(node)
|
|
34
|
-
return node.cases.some((switchCase) => switchCase.test?.type === "Literal" &&
|
|
26
|
+
if (!Option.contains(taggedSwitchSubject(node), parameter)) return false;
|
|
27
|
+
return node.cases.some((switchCase) => switchCase.test?.type === "Literal" && Predicate.isString(switchCase.test.value));
|
|
35
28
|
};
|
|
36
29
|
const hasManualTagDispatch = (argument) => {
|
|
37
30
|
if (!isFunction(argument) || argument.body?.type !== "BlockStatement") return false;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (statement.type === "
|
|
42
|
-
if (statement.type === "SwitchStatement") return hasTagSwitch(statement, parameter.name);
|
|
31
|
+
const statements = argument.body.body;
|
|
32
|
+
return Option.exists(identifierParameter(argument), (parameter) => statements.some((statement) => {
|
|
33
|
+
if (statement.type === "IfStatement") return isTagCheckOn(statement.test, parameter);
|
|
34
|
+
if (statement.type === "SwitchStatement") return hasTagSwitch(statement, parameter);
|
|
43
35
|
return false;
|
|
44
|
-
});
|
|
36
|
+
}));
|
|
45
37
|
};
|
|
46
38
|
const catchAllHandler = (node) => {
|
|
47
|
-
if (!isEffectCall(node, "catchAll")) return
|
|
39
|
+
if (!isEffectCall(node, "catchAll")) return Option.none();
|
|
48
40
|
let handlerIndex = 1;
|
|
49
41
|
if (node.arguments.length === 1) handlerIndex = 0;
|
|
50
|
-
return node.arguments
|
|
42
|
+
return Arr.get(node.arguments, handlerIndex);
|
|
51
43
|
};
|
|
52
44
|
const preferCatchTag = define({
|
|
53
45
|
name: "prefer-catch-tag",
|
|
@@ -61,9 +53,9 @@ const preferCatchTag = define({
|
|
|
61
53
|
if (isEffectCall(node, "catchIf")) {
|
|
62
54
|
let predicateIndex = 1;
|
|
63
55
|
if (node.arguments.length === 2) predicateIndex = 0;
|
|
64
|
-
const predicate = node.arguments
|
|
65
|
-
if (!
|
|
66
|
-
} else if (!
|
|
56
|
+
const predicate = Arr.get(node.arguments, predicateIndex);
|
|
57
|
+
if (!Option.exists(predicate, hasManualTagPredicate)) return Effect.void;
|
|
58
|
+
} else if (!Option.exists(catchAllHandler(node), hasManualTagDispatch)) return Effect.void;
|
|
67
59
|
return context.report(make({
|
|
68
60
|
node,
|
|
69
61
|
message: "Use Effect.catchTag for one tagged failure or Effect.catchTags for multiple tagged failures."
|