oxlint-plugin-effect 0.14.0 → 0.16.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 +16 -3
- 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 +3 -1
- package/presets/recommended.json +58 -0
package/README.md
CHANGED
|
@@ -21,7 +21,19 @@ export default {
|
|
|
21
21
|
};
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
A JSON configuration extends the generated preset file. It loads the plugin and enables every recommended rule, so new rules arrive with each release:
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"extends": ["./node_modules/oxlint-plugin-effect/presets/recommended.json"],
|
|
29
|
+
"rules": {
|
|
30
|
+
// Local choices override the preset.
|
|
31
|
+
"effect/noTernary": "off",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every recommended rule has `error` severity. The complexity rules carry their limit as an option, `["error", { "max": 21 }]`.
|
|
25
37
|
|
|
26
38
|
## Recommended Rules
|
|
27
39
|
|
|
@@ -35,7 +47,8 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
|
|
|
35
47
|
| `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
|
|
36
48
|
| `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
|
|
37
49
|
| `effect/noNullish` | Bans null and undefined; permits `Object.create(null)` for prototype-free dictionaries |
|
|
38
|
-
| `effect/noModuleMocks` | Bans Vitest and
|
|
50
|
+
| `effect/noModuleMocks` | Bans Vitest, Jest, and bun:test module mocks, mock functions, and spies; use test layers |
|
|
51
|
+
| `effect/noTestGlobals` | Bans global writes, global stubs, and implicit runner globals in test files |
|
|
39
52
|
| `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
|
|
40
53
|
| `effect/noManagedRuntimeInEffect` | Keeps ManagedRuntime construction at non-Effect host boundaries |
|
|
41
54
|
| `effect/noModuleLevelMutableState` | Bans module-level `let` and `var` outside tests; hold shared state in a Layer-owned `Ref` |
|
|
@@ -203,7 +216,7 @@ bun run codegen
|
|
|
203
216
|
bun run add-rule -- no-example --dry-run
|
|
204
217
|
```
|
|
205
218
|
|
|
206
|
-
`bun run codegen` owns
|
|
219
|
+
`bun run codegen` owns the rule export barrel and both forms of the recommended preset: `src/presets/recommended.ts` and `presets/recommended.json`.
|
|
207
220
|
`bun run gate` fails when either generated file is stale.
|
|
208
221
|
|
|
209
222
|
## License
|
|
@@ -46,6 +46,7 @@ declare const recommended: {
|
|
|
46
46
|
readonly "effect/noShapeInSymbolNames": "error";
|
|
47
47
|
readonly "effect/noSilentCatchAll": "error";
|
|
48
48
|
readonly "effect/noTernary": "error";
|
|
49
|
+
readonly "effect/noTestGlobals": "error";
|
|
49
50
|
readonly "effect/noTestLifecycleHooks": "error";
|
|
50
51
|
readonly "effect/noThrowStatement": "error";
|
|
51
52
|
readonly "effect/noTryCatch": "error";
|
|
@@ -40,6 +40,7 @@ const recommended = {
|
|
|
40
40
|
"effect/noShapeInSymbolNames": "error",
|
|
41
41
|
"effect/noSilentCatchAll": "error",
|
|
42
42
|
"effect/noTernary": "error",
|
|
43
|
+
"effect/noTestGlobals": "error",
|
|
43
44
|
"effect/noTestLifecycleHooks": "error",
|
|
44
45
|
"effect/noThrowStatement": "error",
|
|
45
46
|
"effect/noTryCatch": "error",
|
|
@@ -1,35 +1,40 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Predicate from "effect/Predicate";
|
|
1
4
|
//#region src/rules/_anti-slop-array-method.ts
|
|
2
5
|
/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
|
|
3
6
|
function unwrapArrayExpression(node) {
|
|
4
7
|
while (node.type === "ParenthesizedExpression" || node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") node = node.expression;
|
|
5
8
|
return node;
|
|
6
9
|
}
|
|
10
|
+
function* scopeChain(scope) {
|
|
11
|
+
let current = Option.some(scope);
|
|
12
|
+
while (Option.isSome(current)) {
|
|
13
|
+
yield current.value;
|
|
14
|
+
current = Option.fromNullOr(current.value.upper);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
7
17
|
/** Resolve a local binding by scope, not by identifier spelling. */
|
|
8
18
|
function resolveArrayBinding(sourceCode, node) {
|
|
9
19
|
node = unwrapArrayExpression(node);
|
|
10
|
-
if (node.type !== "Identifier") return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const variable = scope.set.get(node.name);
|
|
14
|
-
if (variable !== void 0) return variable;
|
|
15
|
-
scope = scope.upper;
|
|
16
|
-
}
|
|
17
|
-
return null;
|
|
20
|
+
if (node.type !== "Identifier") return Option.none();
|
|
21
|
+
const name = node.name;
|
|
22
|
+
return Arr.findFirst(scopeChain(sourceCode.getScope(node)), (scope) => Option.fromUndefinedOr(scope.set.get(name)));
|
|
18
23
|
}
|
|
19
24
|
/** Read static method names, including computed string literals, without evaluating expressions. */
|
|
20
25
|
function arrayMethodTarget(node) {
|
|
21
26
|
node = unwrapArrayExpression(node);
|
|
22
|
-
if (node.type !== "MemberExpression") return
|
|
27
|
+
if (node.type !== "MemberExpression") return Option.none();
|
|
23
28
|
const property = node.property;
|
|
24
|
-
if (!node.computed && property.type === "Identifier") return {
|
|
29
|
+
if (!node.computed && property.type === "Identifier") return Option.some({
|
|
25
30
|
name: property.name,
|
|
26
31
|
object: node.object
|
|
27
|
-
};
|
|
28
|
-
if (node.computed && property.type === "Literal" &&
|
|
32
|
+
});
|
|
33
|
+
if (node.computed && property.type === "Literal" && Predicate.isString(property.value)) return Option.some({
|
|
29
34
|
name: property.value,
|
|
30
35
|
object: node.object
|
|
31
|
-
};
|
|
32
|
-
return
|
|
36
|
+
});
|
|
37
|
+
return Option.none();
|
|
33
38
|
}
|
|
34
39
|
function isArrayAnnotation(type) {
|
|
35
40
|
if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
|
|
@@ -41,30 +46,33 @@ function isArrayAnnotation(type) {
|
|
|
41
46
|
function isKnownArrayExpression(sourceCode, node, visited = /* @__PURE__ */ new Set()) {
|
|
42
47
|
node = unwrapArrayExpression(node);
|
|
43
48
|
if (node.type === "ArrayExpression") return true;
|
|
44
|
-
if (node.type === "CallExpression")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"toSpliced"
|
|
55
|
-
].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited);
|
|
56
|
-
}
|
|
49
|
+
if (node.type === "CallExpression") return Option.exists(arrayMethodTarget(node.callee), (method) => [
|
|
50
|
+
"map",
|
|
51
|
+
"filter",
|
|
52
|
+
"flatMap",
|
|
53
|
+
"slice",
|
|
54
|
+
"concat",
|
|
55
|
+
"toSorted",
|
|
56
|
+
"toReversed",
|
|
57
|
+
"toSpliced"
|
|
58
|
+
].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited));
|
|
57
59
|
if (node.type !== "Identifier") return false;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
return Option.exists(resolveArrayBinding(sourceCode, node), (variable) => isKnownArrayVariable(sourceCode, variable, visited));
|
|
61
|
+
}
|
|
62
|
+
function isKnownArrayVariable(sourceCode, variable, visited) {
|
|
63
|
+
if (visited.has(variable)) return false;
|
|
60
64
|
visited.add(variable);
|
|
61
65
|
if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
62
66
|
for (const identifier of variable.identifiers) {
|
|
63
67
|
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
64
|
-
if (annotation
|
|
68
|
+
if (Predicate.isNotUndefined(annotation)) return isArrayAnnotation(annotation);
|
|
65
69
|
}
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
return Option.exists(constInitializer(variable), (init) => isKnownArrayExpression(sourceCode, init, visited));
|
|
71
|
+
}
|
|
72
|
+
/** The initializer of the first `const name = init` definition of a variable. */
|
|
73
|
+
function constInitializer(variable) {
|
|
74
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && Predicate.isNotNull(definition.node.init) && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return Option.some(definition.node.init);
|
|
75
|
+
return Option.none();
|
|
68
76
|
}
|
|
69
77
|
//#endregion
|
|
70
|
-
export { arrayMethodTarget, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression };
|
|
78
|
+
export { arrayMethodTarget, constInitializer, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createTypeAliasEnvironment, hasVisibleTypeBinding, visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
2
5
|
//#region src/rules/_anti-slop-dictionary-types.ts
|
|
3
6
|
const BUILT_INS = new Set([
|
|
4
7
|
"Record",
|
|
@@ -17,13 +20,16 @@ const TRANSPARENT_WRAPPERS = new Set([
|
|
|
17
20
|
"NonNullable"
|
|
18
21
|
]);
|
|
19
22
|
function declaredStatement(statement) {
|
|
20
|
-
|
|
23
|
+
if (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") return Option.fromNullishOr(statement.declaration);
|
|
24
|
+
return Option.some(statement);
|
|
25
|
+
}
|
|
26
|
+
function isInterfaceDeclaration(node) {
|
|
27
|
+
return node.type === "TSInterfaceDeclaration";
|
|
21
28
|
}
|
|
22
29
|
function createTypeEnvironment(program, visitorKeys) {
|
|
23
30
|
const interfaces = /* @__PURE__ */ new Map();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (declaration?.type !== "TSInterfaceDeclaration") continue;
|
|
31
|
+
const declared = Arr.getSomes(program.body.map(declaredStatement));
|
|
32
|
+
for (const declaration of declared.filter(isInterfaceDeclaration)) {
|
|
27
33
|
const declarations = interfaces.get(declaration.id.name) ?? [];
|
|
28
34
|
declarations.push(declaration);
|
|
29
35
|
interfaces.set(declaration.id.name, declarations);
|
|
@@ -34,14 +40,18 @@ function createTypeEnvironment(program, visitorKeys) {
|
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
function typeReferenceName(type) {
|
|
37
|
-
|
|
43
|
+
if (type.typeName.type !== "Identifier") return Option.none();
|
|
44
|
+
return Option.some(type.typeName.name);
|
|
45
|
+
}
|
|
46
|
+
function typeArgument(type, index) {
|
|
47
|
+
return Arr.get(type.typeArguments?.params ?? [], index);
|
|
38
48
|
}
|
|
39
49
|
function isBuiltIn(name, use, environment) {
|
|
40
50
|
return BUILT_INS.has(name) && !hasVisibleTypeBinding(name, use, environment.typeAliases);
|
|
41
51
|
}
|
|
42
52
|
function isUnappliedReferenceTo(type, name) {
|
|
43
53
|
const unwrapped = unwrapTransparentType(type);
|
|
44
|
-
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped)
|
|
54
|
+
return unwrapped.type === "TSTypeReference" && Option.contains(typeReferenceName(unwrapped), name) && (unwrapped.typeArguments?.params.length ?? 0) === 0;
|
|
45
55
|
}
|
|
46
56
|
function unwrapTransparentType(type) {
|
|
47
57
|
let current = type;
|
|
@@ -52,191 +62,224 @@ function isNeverType(type) {
|
|
|
52
62
|
return unwrapTransparentType(type).type === "TSNeverKeyword";
|
|
53
63
|
}
|
|
54
64
|
function isEffectivelyEmptyMember(member) {
|
|
55
|
-
return member.type === "TSPropertySignature" && member.optional === true &&
|
|
65
|
+
return member.type === "TSPropertySignature" && member.optional === true && Predicate.isNotNullish(member.typeAnnotation) && isNeverType(member.typeAnnotation.typeAnnotation);
|
|
56
66
|
}
|
|
57
67
|
function isEffectivelyEmptyTypeLiteral(type) {
|
|
58
68
|
return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
|
|
59
69
|
}
|
|
60
70
|
function isEffectivelyEmptyInterface(declarations) {
|
|
61
71
|
if (declarations.length !== 1) return false;
|
|
62
|
-
|
|
63
|
-
return type !== void 0 && type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
|
|
72
|
+
return Option.exists(Arr.head(declarations), (type) => type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember)));
|
|
64
73
|
}
|
|
65
74
|
function resolvedSubstitutionArgument(type, base, resolving = /* @__PURE__ */ new Set()) {
|
|
66
75
|
const unwrapped = unwrapTransparentType(type);
|
|
67
76
|
if (unwrapped.type !== "TSTypeReference") return type;
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
77
|
+
const referenceName = typeReferenceName(unwrapped);
|
|
78
|
+
if (Option.isNone(referenceName) || resolving.has(referenceName.value)) return type;
|
|
79
|
+
const name = referenceName.value;
|
|
70
80
|
const substitution = base.get(name);
|
|
71
|
-
if (substitution
|
|
81
|
+
if (Predicate.isUndefined(substitution)) return type;
|
|
72
82
|
const nextResolving = new Set(resolving);
|
|
73
83
|
nextResolving.add(name);
|
|
74
84
|
return resolvedSubstitutionArgument(substitution, base, nextResolving);
|
|
75
85
|
}
|
|
76
86
|
function aliasSubstitution(alias, type, base) {
|
|
77
87
|
const parameters = alias.typeParameters?.params ?? [];
|
|
78
|
-
const typeArguments = type.typeArguments?.params ?? [];
|
|
79
88
|
const next = new Map(base);
|
|
80
89
|
for (const [index, parameter] of parameters.entries()) {
|
|
81
|
-
const argument =
|
|
82
|
-
if (argument
|
|
83
|
-
next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
|
|
90
|
+
const argument = Option.orElse(typeArgument(type, index), () => Option.fromNullishOr(parameter.default));
|
|
91
|
+
if (Option.isNone(argument)) return Option.none();
|
|
92
|
+
next.set(parameter.name.name, resolvedSubstitutionArgument(argument.value, next));
|
|
84
93
|
}
|
|
85
|
-
return next;
|
|
94
|
+
return Option.some(next);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The visible alias named by a reference, expanded with its substitutions, unless it is
|
|
98
|
+
* already resolving. (Local change: upstream repeats this step in each reference resolver.)
|
|
99
|
+
*/
|
|
100
|
+
function aliasExpansion(name, reference, environment, substitutions, resolvingAliases) {
|
|
101
|
+
const alias = visibleTypeAlias(name, reference, environment.typeAliases);
|
|
102
|
+
if (Option.isNone(alias) || resolvingAliases.has(name)) return Option.none();
|
|
103
|
+
return Option.map(aliasSubstitution(alias.value, reference, substitutions), (next) => {
|
|
104
|
+
const nextResolving = new Set(resolvingAliases);
|
|
105
|
+
nextResolving.add(name);
|
|
106
|
+
return {
|
|
107
|
+
type: alias.value.typeAnnotation,
|
|
108
|
+
substitutions: next,
|
|
109
|
+
resolvingAliases: nextResolving
|
|
110
|
+
};
|
|
111
|
+
});
|
|
86
112
|
}
|
|
87
113
|
function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
88
114
|
const unwrapped = unwrapTransparentType(type);
|
|
89
|
-
if (unwrapped.type === "TSUnknownKeyword") return "unknown";
|
|
90
|
-
if (unwrapped.type === "TSAnyKeyword") return "any";
|
|
91
|
-
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
92
|
-
if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
|
|
93
|
-
if (unwrapped.type === "TSUnionType")
|
|
115
|
+
if (unwrapped.type === "TSUnknownKeyword") return Option.some("unknown");
|
|
116
|
+
if (unwrapped.type === "TSAnyKeyword") return Option.some("any");
|
|
117
|
+
if (unwrapped.type === "TSObjectKeyword") return Option.some("object");
|
|
118
|
+
if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return Option.some("empty-object");
|
|
119
|
+
if (unwrapped.type === "TSUnionType") {
|
|
120
|
+
if (unwrapped.types.some((member) => Option.isSome(unsafeDirectValue(member, environment, substitutions, resolvingAliases)))) return Option.some("union");
|
|
121
|
+
return Option.none();
|
|
122
|
+
}
|
|
94
123
|
if (unwrapped.type === "TSIntersectionType") {
|
|
95
124
|
const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases));
|
|
96
|
-
if (unsafeMembers.includes("any")) return "any";
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
if (unwrapped.type !== "TSTypeReference") return null;
|
|
100
|
-
const name = typeReferenceName(unwrapped);
|
|
101
|
-
if (name === null) return null;
|
|
102
|
-
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
103
|
-
const wrapped = unwrapped.typeArguments?.params[0];
|
|
104
|
-
return wrapped === void 0 ? null : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
|
|
125
|
+
if (Arr.getSomes(unsafeMembers).includes("any")) return Option.some("any");
|
|
126
|
+
return Option.flatMap(Option.all(unsafeMembers), Arr.head);
|
|
105
127
|
}
|
|
128
|
+
if (unwrapped.type !== "TSTypeReference") return Option.none();
|
|
129
|
+
return unsafeReferenceValue(unwrapped, environment, substitutions, resolvingAliases);
|
|
130
|
+
}
|
|
131
|
+
function unsafeReferenceValue(unwrapped, environment, substitutions, resolvingAliases) {
|
|
132
|
+
const referenceName = typeReferenceName(unwrapped);
|
|
133
|
+
if (Option.isNone(referenceName)) return Option.none();
|
|
134
|
+
const name = referenceName.value;
|
|
135
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.flatMap(typeArgument(unwrapped, 0), (wrapped) => unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases));
|
|
106
136
|
const substitution = substitutions.get(name);
|
|
107
|
-
if (
|
|
137
|
+
if (Predicate.isNotUndefined(substitution)) {
|
|
138
|
+
if (isUnappliedReferenceTo(substitution, name)) return Option.none();
|
|
139
|
+
return unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
|
|
140
|
+
}
|
|
108
141
|
const interfaceDeclarations = environment.interfaces.get(name);
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const nextResolving = new Set(resolvingAliases);
|
|
115
|
-
nextResolving.add(name);
|
|
116
|
-
return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
142
|
+
if (Predicate.isNotUndefined(interfaceDeclarations)) {
|
|
143
|
+
if (isEffectivelyEmptyInterface(interfaceDeclarations)) return Option.some("empty-object");
|
|
144
|
+
return Option.none();
|
|
145
|
+
}
|
|
146
|
+
return Option.flatMap(aliasExpansion(name, unwrapped, environment, substitutions, resolvingAliases), (expansion) => unsafeDirectValue(expansion.type, environment, expansion.substitutions, expansion.resolvingAliases));
|
|
117
147
|
}
|
|
118
148
|
function dictionaryValueTypes(type, environment, substitutions, resolvingAliases) {
|
|
119
149
|
const unwrapped = unwrapTransparentType(type);
|
|
120
|
-
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) =>
|
|
121
|
-
type
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const substitution = substitutions.get(name);
|
|
132
|
-
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
|
|
133
|
-
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
134
|
-
const wrapped = unwrapped.typeArguments?.params[0];
|
|
135
|
-
return wrapped === void 0 ? [] : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
|
|
136
|
-
}
|
|
137
|
-
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
138
|
-
const value = unwrapped.typeArguments?.params[1] ?? null;
|
|
139
|
-
return value === null ? [] : [{
|
|
140
|
-
type: value,
|
|
150
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => {
|
|
151
|
+
if (member.type !== "TSIndexSignature" || Predicate.isNull(member.typeAnnotation)) return [];
|
|
152
|
+
return [{
|
|
153
|
+
type: member.typeAnnotation.typeAnnotation,
|
|
154
|
+
substitutions
|
|
155
|
+
}];
|
|
156
|
+
});
|
|
157
|
+
if (unwrapped.type === "TSMappedType") {
|
|
158
|
+
if (Predicate.isNull(unwrapped.typeAnnotation)) return [];
|
|
159
|
+
return [{
|
|
160
|
+
type: unwrapped.typeAnnotation,
|
|
141
161
|
substitutions
|
|
142
162
|
}];
|
|
143
163
|
}
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
if (unwrapped.type !== "TSTypeReference") return [];
|
|
165
|
+
return referenceValueTypes(unwrapped, environment, substitutions, resolvingAliases);
|
|
166
|
+
}
|
|
167
|
+
function referenceValueTypes(unwrapped, environment, substitutions, resolvingAliases) {
|
|
168
|
+
const referenceName = typeReferenceName(unwrapped);
|
|
169
|
+
if (Option.isNone(referenceName)) return [];
|
|
170
|
+
const name = referenceName.value;
|
|
171
|
+
const substitution = substitutions.get(name);
|
|
172
|
+
if (Predicate.isNotUndefined(substitution)) {
|
|
173
|
+
if (isUnappliedReferenceTo(substitution, name)) return [];
|
|
174
|
+
return dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
|
|
147
175
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.match(typeArgument(unwrapped, 0), {
|
|
177
|
+
onNone: () => [],
|
|
178
|
+
onSome: (wrapped) => dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases)
|
|
179
|
+
});
|
|
180
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return Option.toArray(Option.map(typeArgument(unwrapped, 1), (value) => ({
|
|
181
|
+
type: value,
|
|
182
|
+
substitutions
|
|
183
|
+
})));
|
|
184
|
+
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) return Option.match(typeArgument(unwrapped, 0), {
|
|
185
|
+
onNone: () => [],
|
|
186
|
+
onSome: (source) => dictionaryValueTypes(source, environment, substitutions, resolvingAliases)
|
|
187
|
+
});
|
|
188
|
+
return Option.match(aliasExpansion(name, unwrapped, environment, substitutions, resolvingAliases), {
|
|
189
|
+
onNone: () => [],
|
|
190
|
+
onSome: (expansion) => dictionaryValueTypes(expansion.type, environment, expansion.substitutions, expansion.resolvingAliases)
|
|
191
|
+
});
|
|
155
192
|
}
|
|
193
|
+
const unsafeDictionary = (unsafeValue) => ({
|
|
194
|
+
kind: "unsafe-dictionary",
|
|
195
|
+
unsafeValue
|
|
196
|
+
});
|
|
156
197
|
function classifyUnsafeDictionaryValue(valueType, environment) {
|
|
157
|
-
|
|
158
|
-
return unsafeValue === null ? null : {
|
|
159
|
-
kind: "unsafe-dictionary",
|
|
160
|
-
unsafeValue
|
|
161
|
-
};
|
|
198
|
+
return Option.map(unsafeDirectValue(valueType, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set()), unsafeDictionary);
|
|
162
199
|
}
|
|
163
200
|
function classifyUnsafeDictionary(type, environment) {
|
|
164
|
-
|
|
165
|
-
const unsafeValue = unsafeDirectValue(valueType.type, environment, valueType.substitutions, /* @__PURE__ */ new Set());
|
|
166
|
-
if (unsafeValue !== null) return {
|
|
167
|
-
kind: "unsafe-dictionary",
|
|
168
|
-
unsafeValue
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return null;
|
|
201
|
+
return Arr.findFirst(dictionaryValueTypes(type, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set()), (valueType) => Option.map(unsafeDirectValue(valueType.type, environment, valueType.substitutions, /* @__PURE__ */ new Set()), unsafeDictionary));
|
|
172
202
|
}
|
|
173
203
|
function classifyWideningTarget(type, environment) {
|
|
174
204
|
const unwrapped = unwrapTransparentType(type);
|
|
175
|
-
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
176
|
-
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
177
|
-
if (unwrapped.type === "TSTypeLiteral")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (name === null) return null;
|
|
182
|
-
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
183
|
-
const wrapped = unwrapped.typeArguments?.params[0];
|
|
184
|
-
return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
|
|
205
|
+
if (unwrapped.type === "TSUnknownKeyword") return Option.some({ kind: "unknown" });
|
|
206
|
+
if (unwrapped.type === "TSObjectKeyword") return Option.some({ kind: "object" });
|
|
207
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
208
|
+
if (unwrapped.members.some((member) => member.type === "TSIndexSignature")) return Option.some({ kind: "open dictionary" });
|
|
209
|
+
if (unwrapped.members.length > 0) return Option.some({ kind: "anonymous object" });
|
|
210
|
+
return Option.none();
|
|
185
211
|
}
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if ((
|
|
190
|
-
|
|
191
|
-
|
|
212
|
+
if (unwrapped.type === "TSMappedType") return Option.some({ kind: "open dictionary" });
|
|
213
|
+
if (unwrapped.type !== "TSTypeReference") return Option.none();
|
|
214
|
+
const referenceName = typeReferenceName(unwrapped);
|
|
215
|
+
if (Option.isNone(referenceName)) return Option.none();
|
|
216
|
+
const name = referenceName.value;
|
|
217
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.flatMap(typeArgument(unwrapped, 0), (wrapped) => classifyWideningTarget(wrapped, environment));
|
|
218
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
219
|
+
if (hasBroadRecordKey(unwrapped, environment, /* @__PURE__ */ new Map())) return Option.some({ kind: "open dictionary" });
|
|
220
|
+
return Option.none();
|
|
192
221
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
222
|
+
return Option.flatMap(visibleTypeAlias(name, unwrapped, environment.typeAliases), (alias) => classifyWideningAlias(alias, unwrapped, name, environment));
|
|
223
|
+
}
|
|
224
|
+
function classifyWideningAlias(alias, unwrapped, name, environment) {
|
|
225
|
+
const resolved = Option.flatMap(aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map()), (substitutions) => classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name])));
|
|
226
|
+
if ((alias.typeParameters?.params.length ?? 0) === 0) return resolved;
|
|
227
|
+
if (Option.exists(resolved, (target) => target.kind === "open dictionary")) return Option.some({ kind: "generic container" });
|
|
228
|
+
return Option.none();
|
|
196
229
|
}
|
|
197
230
|
function hasBroadRecordKey(type, environment, substitutions) {
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
return Option.match(typeArgument(type, 0), {
|
|
232
|
+
onNone: () => true,
|
|
233
|
+
onSome: (key) => isBroadMappedKey(key, environment, substitutions)
|
|
234
|
+
});
|
|
200
235
|
}
|
|
201
236
|
function isBroadMappedKey(type, environment, substitutions, visitedAliases = /* @__PURE__ */ new Set()) {
|
|
202
237
|
const unwrapped = unwrapTransparentType(type);
|
|
203
238
|
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
204
239
|
if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
|
|
205
240
|
if (unwrapped.type !== "TSTypeReference") return false;
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
241
|
+
const referenceName = typeReferenceName(unwrapped);
|
|
242
|
+
if (Option.isNone(referenceName)) return false;
|
|
243
|
+
const name = referenceName.value;
|
|
208
244
|
const substitution = substitutions.get(name);
|
|
209
|
-
if (substitution
|
|
245
|
+
if (Predicate.isNotUndefined(substitution) && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
|
|
210
246
|
if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
|
|
211
247
|
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
212
|
-
if (alias
|
|
248
|
+
if (Option.isNone(alias) || (alias.value.typeParameters?.params.length ?? 0) > 0 || visitedAliases.has(name)) return false;
|
|
213
249
|
const nextVisited = new Set(visitedAliases);
|
|
214
250
|
nextVisited.add(name);
|
|
215
|
-
return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
|
|
251
|
+
return isBroadMappedKey(alias.value.typeAnnotation, environment, substitutions, nextVisited);
|
|
216
252
|
}
|
|
217
253
|
function classifyAliasBroadTarget(type, environment, substitutions, resolvingAliases) {
|
|
218
254
|
const unwrapped = unwrapTransparentType(type);
|
|
219
|
-
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
220
|
-
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
221
|
-
if (unwrapped.type === "TSTypeLiteral")
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
255
|
+
if (unwrapped.type === "TSUnknownKeyword") return Option.some({ kind: "unknown" });
|
|
256
|
+
if (unwrapped.type === "TSObjectKeyword") return Option.some({ kind: "object" });
|
|
257
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
258
|
+
if (unwrapped.members.some((member) => member.type === "TSIndexSignature")) return Option.some({ kind: "open dictionary" });
|
|
259
|
+
return Option.none();
|
|
260
|
+
}
|
|
261
|
+
if (unwrapped.type === "TSMappedType") {
|
|
262
|
+
if (isBroadMappedKey(unwrapped.constraint, environment, substitutions)) return Option.some({ kind: "open dictionary" });
|
|
263
|
+
return Option.none();
|
|
264
|
+
}
|
|
265
|
+
if (unwrapped.type !== "TSTypeReference") return Option.none();
|
|
266
|
+
return classifyReferenceBroadTarget(unwrapped, environment, substitutions, resolvingAliases);
|
|
267
|
+
}
|
|
268
|
+
function classifyReferenceBroadTarget(unwrapped, environment, substitutions, resolvingAliases) {
|
|
269
|
+
const referenceName = typeReferenceName(unwrapped);
|
|
270
|
+
if (Option.isNone(referenceName)) return Option.none();
|
|
271
|
+
const name = referenceName.value;
|
|
226
272
|
const substitution = substitutions.get(name);
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return wrapped === void 0 ? null : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
|
|
273
|
+
if (Predicate.isNotUndefined(substitution)) {
|
|
274
|
+
if (isUnappliedReferenceTo(substitution, name)) return Option.none();
|
|
275
|
+
return classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
|
|
231
276
|
}
|
|
232
|
-
if (name
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
nextResolving.add(name);
|
|
239
|
-
return classifyAliasBroadTarget(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
277
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.flatMap(typeArgument(unwrapped, 0), (wrapped) => classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases));
|
|
278
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
279
|
+
if (hasBroadRecordKey(unwrapped, environment, substitutions)) return Option.some({ kind: "open dictionary" });
|
|
280
|
+
return Option.none();
|
|
281
|
+
}
|
|
282
|
+
return Option.flatMap(aliasExpansion(name, unwrapped, environment, substitutions, resolvingAliases), (expansion) => classifyAliasBroadTarget(expansion.type, environment, expansion.substitutions, expansion.resolvingAliases));
|
|
240
283
|
}
|
|
241
284
|
function isKnownEvidenceExpression(expression) {
|
|
242
285
|
let current = expression;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
1
2
|
//#region src/rules/_anti-slop-function-parameters.ts
|
|
2
3
|
/** Return whether a type is or contains TypeScript's absorbing unknown top type. */
|
|
3
4
|
function containsUnknownType(type) {
|
|
@@ -8,20 +9,22 @@ function containsUnknownType(type) {
|
|
|
8
9
|
/** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
|
|
9
10
|
function functionParameterTypeAnnotation(parameter) {
|
|
10
11
|
if (parameter.type === "TSParameterProperty") return functionParameterTypeAnnotation(parameter.parameter);
|
|
11
|
-
if (parameter.type === "RestElement") return parameter.typeAnnotation
|
|
12
|
-
if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation
|
|
13
|
-
return parameter.typeAnnotation;
|
|
12
|
+
if (parameter.type === "RestElement") return Option.orElse(Option.fromNullishOr(parameter.typeAnnotation), () => functionParameterTypeAnnotation(parameter.argument));
|
|
13
|
+
if (parameter.type === "AssignmentPattern") return Option.orElse(Option.fromNullishOr(parameter.typeAnnotation), () => functionParameterTypeAnnotation(parameter.left));
|
|
14
|
+
return Option.fromNullishOr(parameter.typeAnnotation);
|
|
14
15
|
}
|
|
15
16
|
/** Return only a function parameter's local binding, excluding its annotation and default value. */
|
|
16
17
|
function functionParameterBindingName(parameter, sourceCode) {
|
|
17
18
|
if (parameter.type === "TSParameterProperty") return functionParameterBindingName(parameter.parameter, sourceCode);
|
|
18
19
|
if (parameter.type === "AssignmentPattern") return functionParameterBindingName(parameter.left, sourceCode);
|
|
19
20
|
if (parameter.type === "RestElement") return functionParameterBindingName(parameter.argument, sourceCode);
|
|
20
|
-
const
|
|
21
|
-
const annotationOffset = annotation === null || annotation === void 0 ? void 0 : annotation.start - parameter.start;
|
|
21
|
+
const annotationOffset = Option.map(functionParameterTypeAnnotation(parameter), (annotation) => annotation.start - parameter.start);
|
|
22
22
|
if (parameter.type === "Identifier") return parameter.name;
|
|
23
23
|
const sourceText = sourceCode.getText(parameter);
|
|
24
|
-
return
|
|
24
|
+
return Option.match(annotationOffset, {
|
|
25
|
+
onNone: () => sourceText,
|
|
26
|
+
onSome: (offset) => sourceText.slice(0, offset).trimEnd()
|
|
27
|
+
});
|
|
25
28
|
}
|
|
26
29
|
//#endregion
|
|
27
30
|
export { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation };
|