oxlint-plugin-effect 0.13.0 → 0.14.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 +24 -13
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/dist/presets/recommended.d.ts +9 -0
- package/dist/presets/recommended.js +10 -1
- package/dist/rules/_anti-slop-array-method.js +70 -0
- package/dist/rules/_anti-slop-dictionary-types.js +58 -53
- package/dist/rules/_anti-slop-function-parameters.js +27 -0
- package/dist/rules/_anti-slop-lexical-type-parameters.js +33 -0
- package/dist/rules/_anti-slop-reflect-method.js +21 -0
- package/dist/rules/_anti-slop-scope.js +13 -0
- package/dist/rules/_anti-slop-type-alias-resolution.js +146 -0
- package/dist/rules/_function-metrics.js +31 -31
- package/dist/rules/_test-files.js +8 -0
- package/dist/rules/index.d.ts +10 -1
- package/dist/rules/index.js +19 -1
- package/dist/rules/no-array-filter-map.d.ts +7 -0
- package/dist/rules/no-array-filter-map.js +39 -0
- package/dist/rules/no-conditional-empty-object-spread.d.ts +1 -1
- package/dist/rules/no-conditional-empty-object-spread.js +8 -51
- package/dist/rules/no-eager-acquire.d.ts +6 -0
- package/dist/rules/no-eager-acquire.js +92 -0
- package/dist/rules/no-effect-run-in-tests.d.ts +6 -0
- package/dist/rules/no-effect-run-in-tests.js +57 -0
- package/dist/rules/no-known-value-widening.js +6 -14
- package/dist/rules/no-module-level-mutable-state.d.ts +6 -0
- package/dist/rules/no-module-level-mutable-state.js +51 -0
- package/dist/rules/no-object-parameters.js +11 -27
- package/dist/rules/no-reduce-accumulator-copy.d.ts +7 -0
- package/dist/rules/no-reduce-accumulator-copy.js +94 -0
- package/dist/rules/no-reflect-apply.d.ts +7 -0
- package/dist/rules/no-reflect-apply.js +25 -0
- package/dist/rules/no-reflect-get.d.ts +7 -0
- package/dist/rules/no-reflect-get.js +25 -0
- package/dist/rules/no-runtime-typeof.js +14 -1
- package/dist/rules/no-shape-in-symbol-names.js +7 -1
- package/dist/rules/no-unknown-parameters.d.ts +1 -1
- package/dist/rules/no-unknown-parameters.js +23 -16
- package/dist/rules/no-unknown-returns.d.ts +7 -0
- package/dist/rules/no-unknown-returns.js +73 -0
- package/dist/rules/no-unknown-type-aliases.js +17 -27
- package/dist/rules/no-unsafe-dictionary-type.js +54 -3
- package/dist/rules/no-widen-then-assert.js +1 -1
- package/dist/rules/require-suppression-reason.d.ts +6 -0
- package/dist/rules/require-suppression-reason.js +87 -0
- package/dist/vendor/effect-oxlint/Diagnostic.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -34,10 +34,13 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
|
|
|
34
34
|
| `effect/noThrowStatement` | Bans every throw statement |
|
|
35
35
|
| `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
|
|
36
36
|
| `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
|
|
37
|
-
| `effect/noNullish` | Bans null and undefined; permits `Object.create(null)` for prototype-free dictionaries
|
|
37
|
+
| `effect/noNullish` | Bans null and undefined; permits `Object.create(null)` for prototype-free dictionaries |
|
|
38
38
|
| `effect/noModuleMocks` | Bans Vitest and Jest module mocks and method spies; use Effect service test layers |
|
|
39
39
|
| `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
|
|
40
40
|
| `effect/noManagedRuntimeInEffect` | Keeps ManagedRuntime construction at non-Effect host boundaries |
|
|
41
|
+
| `effect/noModuleLevelMutableState` | Bans module-level `let` and `var` outside tests; hold shared state in a Layer-owned `Ref` |
|
|
42
|
+
| `effect/noEagerAcquire` | Bans `acquireRelease` acquires that build or capture the resource before acquire runs |
|
|
43
|
+
| `effect/noEffectRunInTests` | Bans `Effect.run*` and `ManagedRuntime.make` in tests; use `it.effect` or `it.layer` |
|
|
41
44
|
| `effect/noInlineProvide` | Keeps dependency provisioning at explicit composition boundaries |
|
|
42
45
|
| `effect/noNestedEffectGen` | Flattens directly yielded nested generators |
|
|
43
46
|
| `effect/noPerCallCacheConstruction` | Constructs shared caches once in their owning layer |
|
|
@@ -55,26 +58,34 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
|
|
|
55
58
|
| `effect/preferPredicateIsTagged` | Replaces combined `_tag` comparisons with a named `Predicate` refinement |
|
|
56
59
|
| `effect/preferServiceOf` | Checks inline Layer implementations through `Service.of` |
|
|
57
60
|
| `effect/requireNamedEffectFn` | Requires stable names for `Effect.fn` operations |
|
|
61
|
+
| `effect/requireSuppressionReason` | Requires lint, Effect, and TS suppressions to name their target and give a `--` reason |
|
|
58
62
|
| `effect/noGlobals` | Bans ambient capabilities with direct Effect replacements; allows `process.std*.isTTY` |
|
|
59
63
|
| `effect/noNodeBuiltinImport` | Bans fully replaced Node modules and replaced operations from partial modules |
|
|
60
64
|
|
|
65
|
+
`effect/requireSuppressionReason` also rejects `@effect-diagnostics effect/name:off`: @effect/tsgo ignores the `effect/` prefix, so write the bare rule name. A blanket directive that covers its own line, such as a bare `// oxlint-disable-line` or a file-leading `/* eslint-disable */`, suppresses this rule's report too; oxlint applies the directive before the rule can report it.
|
|
66
|
+
|
|
61
67
|
## Anti-Slop Rules
|
|
62
68
|
|
|
63
69
|
The recommended preset also includes these rules from
|
|
64
70
|
[`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop):
|
|
65
71
|
|
|
66
|
-
| Rule | Contract
|
|
67
|
-
| --------------------------------------- |
|
|
68
|
-
| `effect/
|
|
69
|
-
| `effect/
|
|
70
|
-
| `effect/
|
|
71
|
-
| `effect/
|
|
72
|
-
| `effect/
|
|
73
|
-
| `effect/
|
|
74
|
-
| `effect/
|
|
75
|
-
| `effect/
|
|
76
|
-
| `effect/
|
|
77
|
-
| `effect/
|
|
72
|
+
| Rule | Contract |
|
|
73
|
+
| --------------------------------------- | -------------------------------------------------------------------------------- |
|
|
74
|
+
| `effect/noArrayFilterMap` | Bans adjacent array `filter`/`map` passes over a known array |
|
|
75
|
+
| `effect/noChainedTypeAssertions` | Bans nested type assertions that invent type evidence |
|
|
76
|
+
| `effect/noConditionalEmptyObjectSpread` | Bans conditional spreads that use an empty object to omit fields |
|
|
77
|
+
| `effect/noKnownValueWidening` | Bans broad target types that discard known value evidence |
|
|
78
|
+
| `effect/noObjectParameters` | Bans the broad `object` type on function inputs |
|
|
79
|
+
| `effect/noReduceAccumulatorCopy` | Bans copying a reducer accumulator on every iteration |
|
|
80
|
+
| `effect/noReflectApply` | Bans `Reflect.apply`; call typed functions directly |
|
|
81
|
+
| `effect/noReflectGet` | Bans `Reflect.get`; use typed property access |
|
|
82
|
+
| `effect/noRuntimeTypeof` | Requires boundary parsing instead of `typeof` narrowing; allows existence probes |
|
|
83
|
+
| `effect/noShapeInSymbolNames` | Bans `shape` in symbol names |
|
|
84
|
+
| `effect/noUnknownParameters` | Bans `unknown` inputs except `cause`, type guards, and rejection reasons |
|
|
85
|
+
| `effect/noUnknownReturns` | Bans return contracts of `unknown` or `Promise<unknown>` |
|
|
86
|
+
| `effect/noUnknownTypeAliases` | Bans aliases that only hide `unknown` |
|
|
87
|
+
| `effect/noUnsafeDictionaryType` | Bans dictionaries with unsafe broad value types |
|
|
88
|
+
| `effect/noWidenThenAssert` | Bans local flows that widen known values and then assert them back |
|
|
78
89
|
|
|
79
90
|
See `THIRD_PARTY_NOTICES.md` for the source revision and license.
|
|
80
91
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Third-Party Notices
|
|
2
2
|
|
|
3
3
|
The anti-slop rules include code derived from
|
|
4
|
-
[`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop)
|
|
5
|
-
`
|
|
4
|
+
[`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop), synced to commit
|
|
5
|
+
`c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`. The rejection-handler exemption in
|
|
6
|
+
`noUnknownParameters` comes from the vendored copy in
|
|
7
|
+
[`joelhooks/rat-stack`](https://github.com/joelhooks/rat-stack).
|
|
6
8
|
|
|
7
9
|
MIT License
|
|
8
10
|
|
|
@@ -14,17 +14,21 @@ declare const recommended: {
|
|
|
14
14
|
readonly "effect/maxHalsteadDifficulty": readonly ["error", {
|
|
15
15
|
readonly max: 79;
|
|
16
16
|
}];
|
|
17
|
+
readonly "effect/noArrayFilterMap": "error";
|
|
17
18
|
readonly "effect/noAs": "error";
|
|
18
19
|
readonly "effect/noAsyncFunction": "error";
|
|
19
20
|
readonly "effect/noChainedTypeAssertions": "error";
|
|
20
21
|
readonly "effect/noConditionalEmptyObjectSpread": "error";
|
|
21
22
|
readonly "effect/noDynamicImports": "error";
|
|
23
|
+
readonly "effect/noEagerAcquire": "error";
|
|
22
24
|
readonly "effect/noEffectBind": "error";
|
|
23
25
|
readonly "effect/noEffectDo": "error";
|
|
26
|
+
readonly "effect/noEffectRunInTests": "error";
|
|
24
27
|
readonly "effect/noGlobals": "error";
|
|
25
28
|
readonly "effect/noInlineProvide": "error";
|
|
26
29
|
readonly "effect/noKnownValueWidening": "error";
|
|
27
30
|
readonly "effect/noManagedRuntimeInEffect": "error";
|
|
31
|
+
readonly "effect/noModuleLevelMutableState": "error";
|
|
28
32
|
readonly "effect/noModuleMocks": "error";
|
|
29
33
|
readonly "effect/noNestedEffectGen": "error";
|
|
30
34
|
readonly "effect/noNewError": "error";
|
|
@@ -33,6 +37,9 @@ declare const recommended: {
|
|
|
33
37
|
readonly "effect/noNullish": "error";
|
|
34
38
|
readonly "effect/noObjectParameters": "error";
|
|
35
39
|
readonly "effect/noPerCallCacheConstruction": "error";
|
|
40
|
+
readonly "effect/noReduceAccumulatorCopy": "error";
|
|
41
|
+
readonly "effect/noReflectApply": "error";
|
|
42
|
+
readonly "effect/noReflectGet": "error";
|
|
36
43
|
readonly "effect/noRunCollectOnUnboundedStream": "error";
|
|
37
44
|
readonly "effect/noRuntimeTypeof": "error";
|
|
38
45
|
readonly "effect/noSequentialEffectAll": "error";
|
|
@@ -45,6 +52,7 @@ declare const recommended: {
|
|
|
45
52
|
readonly "effect/noUnboundedConcurrency": "error";
|
|
46
53
|
readonly "effect/noUnboundedRetry": "error";
|
|
47
54
|
readonly "effect/noUnknownParameters": "error";
|
|
55
|
+
readonly "effect/noUnknownReturns": "error";
|
|
48
56
|
readonly "effect/noUnknownTypeAliases": "error";
|
|
49
57
|
readonly "effect/noUnsafeDictionaryType": "error";
|
|
50
58
|
readonly "effect/noWidenThenAssert": "error";
|
|
@@ -54,6 +62,7 @@ declare const recommended: {
|
|
|
54
62
|
readonly "effect/preferPredicateIsTagged": "error";
|
|
55
63
|
readonly "effect/preferServiceOf": "error";
|
|
56
64
|
readonly "effect/requireNamedEffectFn": "error";
|
|
65
|
+
readonly "effect/requireSuppressionReason": "error";
|
|
57
66
|
};
|
|
58
67
|
//#endregion
|
|
59
68
|
export { recommended };
|
|
@@ -8,17 +8,21 @@ const recommended = {
|
|
|
8
8
|
complexity: ["error", { max: 21 }],
|
|
9
9
|
"effect/maxCognitiveComplexity": ["error", { max: 21 }],
|
|
10
10
|
"effect/maxHalsteadDifficulty": ["error", { max: 79 }],
|
|
11
|
+
"effect/noArrayFilterMap": "error",
|
|
11
12
|
"effect/noAs": "error",
|
|
12
13
|
"effect/noAsyncFunction": "error",
|
|
13
14
|
"effect/noChainedTypeAssertions": "error",
|
|
14
15
|
"effect/noConditionalEmptyObjectSpread": "error",
|
|
15
16
|
"effect/noDynamicImports": "error",
|
|
17
|
+
"effect/noEagerAcquire": "error",
|
|
16
18
|
"effect/noEffectBind": "error",
|
|
17
19
|
"effect/noEffectDo": "error",
|
|
20
|
+
"effect/noEffectRunInTests": "error",
|
|
18
21
|
"effect/noGlobals": "error",
|
|
19
22
|
"effect/noInlineProvide": "error",
|
|
20
23
|
"effect/noKnownValueWidening": "error",
|
|
21
24
|
"effect/noManagedRuntimeInEffect": "error",
|
|
25
|
+
"effect/noModuleLevelMutableState": "error",
|
|
22
26
|
"effect/noModuleMocks": "error",
|
|
23
27
|
"effect/noNestedEffectGen": "error",
|
|
24
28
|
"effect/noNewError": "error",
|
|
@@ -27,6 +31,9 @@ const recommended = {
|
|
|
27
31
|
"effect/noNullish": "error",
|
|
28
32
|
"effect/noObjectParameters": "error",
|
|
29
33
|
"effect/noPerCallCacheConstruction": "error",
|
|
34
|
+
"effect/noReduceAccumulatorCopy": "error",
|
|
35
|
+
"effect/noReflectApply": "error",
|
|
36
|
+
"effect/noReflectGet": "error",
|
|
30
37
|
"effect/noRunCollectOnUnboundedStream": "error",
|
|
31
38
|
"effect/noRuntimeTypeof": "error",
|
|
32
39
|
"effect/noSequentialEffectAll": "error",
|
|
@@ -39,6 +46,7 @@ const recommended = {
|
|
|
39
46
|
"effect/noUnboundedConcurrency": "error",
|
|
40
47
|
"effect/noUnboundedRetry": "error",
|
|
41
48
|
"effect/noUnknownParameters": "error",
|
|
49
|
+
"effect/noUnknownReturns": "error",
|
|
42
50
|
"effect/noUnknownTypeAliases": "error",
|
|
43
51
|
"effect/noUnsafeDictionaryType": "error",
|
|
44
52
|
"effect/noWidenThenAssert": "error",
|
|
@@ -47,7 +55,8 @@ const recommended = {
|
|
|
47
55
|
"effect/preferMatchTagsExhaustive": "error",
|
|
48
56
|
"effect/preferPredicateIsTagged": "error",
|
|
49
57
|
"effect/preferServiceOf": "error",
|
|
50
|
-
"effect/requireNamedEffectFn": "error"
|
|
58
|
+
"effect/requireNamedEffectFn": "error",
|
|
59
|
+
"effect/requireSuppressionReason": "error"
|
|
51
60
|
};
|
|
52
61
|
//#endregion
|
|
53
62
|
export { recommended };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/rules/_anti-slop-array-method.ts
|
|
2
|
+
/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
|
|
3
|
+
function unwrapArrayExpression(node) {
|
|
4
|
+
while (node.type === "ParenthesizedExpression" || node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") node = node.expression;
|
|
5
|
+
return node;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve a local binding by scope, not by identifier spelling. */
|
|
8
|
+
function resolveArrayBinding(sourceCode, node) {
|
|
9
|
+
node = unwrapArrayExpression(node);
|
|
10
|
+
if (node.type !== "Identifier") return null;
|
|
11
|
+
let scope = sourceCode.getScope(node);
|
|
12
|
+
while (scope !== null) {
|
|
13
|
+
const variable = scope.set.get(node.name);
|
|
14
|
+
if (variable !== void 0) return variable;
|
|
15
|
+
scope = scope.upper;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
/** Read static method names, including computed string literals, without evaluating expressions. */
|
|
20
|
+
function arrayMethodTarget(node) {
|
|
21
|
+
node = unwrapArrayExpression(node);
|
|
22
|
+
if (node.type !== "MemberExpression") return null;
|
|
23
|
+
const property = node.property;
|
|
24
|
+
if (!node.computed && property.type === "Identifier") return {
|
|
25
|
+
name: property.name,
|
|
26
|
+
object: node.object
|
|
27
|
+
};
|
|
28
|
+
if (node.computed && property.type === "Literal" && typeof property.value === "string") return {
|
|
29
|
+
name: property.value,
|
|
30
|
+
object: node.object
|
|
31
|
+
};
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function isArrayAnnotation(type) {
|
|
35
|
+
if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
|
|
36
|
+
if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation);
|
|
37
|
+
if (type.type === "TSTypeOperator" && type.operator === "readonly") return isArrayAnnotation(type.typeAnnotation);
|
|
38
|
+
return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray");
|
|
39
|
+
}
|
|
40
|
+
/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
|
|
41
|
+
function isKnownArrayExpression(sourceCode, node, visited = /* @__PURE__ */ new Set()) {
|
|
42
|
+
node = unwrapArrayExpression(node);
|
|
43
|
+
if (node.type === "ArrayExpression") return true;
|
|
44
|
+
if (node.type === "CallExpression") {
|
|
45
|
+
const method = arrayMethodTarget(node.callee);
|
|
46
|
+
return method !== null && [
|
|
47
|
+
"map",
|
|
48
|
+
"filter",
|
|
49
|
+
"flatMap",
|
|
50
|
+
"slice",
|
|
51
|
+
"concat",
|
|
52
|
+
"toSorted",
|
|
53
|
+
"toReversed",
|
|
54
|
+
"toSpliced"
|
|
55
|
+
].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited);
|
|
56
|
+
}
|
|
57
|
+
if (node.type !== "Identifier") return false;
|
|
58
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
59
|
+
if (variable === null || visited.has(variable)) return false;
|
|
60
|
+
visited.add(variable);
|
|
61
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
62
|
+
for (const identifier of variable.identifiers) {
|
|
63
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
64
|
+
if (annotation !== void 0) return isArrayAnnotation(annotation);
|
|
65
|
+
}
|
|
66
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return isKnownArrayExpression(sourceCode, definition.node.init, visited);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { arrayMethodTarget, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createTypeAliasEnvironment, hasVisibleTypeBinding, visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
|
|
1
2
|
//#region src/rules/_anti-slop-dictionary-types.ts
|
|
2
3
|
const BUILT_INS = new Set([
|
|
3
4
|
"Record",
|
|
@@ -18,48 +19,25 @@ const TRANSPARENT_WRAPPERS = new Set([
|
|
|
18
19
|
function declaredStatement(statement) {
|
|
19
20
|
return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
|
|
20
21
|
}
|
|
21
|
-
function createTypeEnvironment(program) {
|
|
22
|
-
const aliases = /* @__PURE__ */ new Map();
|
|
22
|
+
function createTypeEnvironment(program, visitorKeys) {
|
|
23
23
|
const interfaces = /* @__PURE__ */ new Map();
|
|
24
|
-
const shadowedBuiltIns = /* @__PURE__ */ new Set();
|
|
25
24
|
for (const statement of program.body) {
|
|
26
25
|
const declaration = declaredStatement(statement);
|
|
27
|
-
if (declaration?.type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (declaration?.type === "TSTypeAliasDeclaration") {
|
|
32
|
-
if (aliases.get(declaration.id.name) === void 0) aliases.set(declaration.id.name, declaration);
|
|
33
|
-
else shadowedBuiltIns.add(declaration.id.name);
|
|
34
|
-
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (declaration?.type === "TSInterfaceDeclaration") {
|
|
38
|
-
const declarations = interfaces.get(declaration.id.name) ?? [];
|
|
39
|
-
declarations.push(declaration);
|
|
40
|
-
interfaces.set(declaration.id.name, declarations);
|
|
41
|
-
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (declaration?.type === "TSEnumDeclaration") {
|
|
45
|
-
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if ((declaration?.type === "ClassDeclaration" || declaration?.type === "FunctionDeclaration") && declaration.id !== null) {
|
|
49
|
-
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
50
|
-
}
|
|
26
|
+
if (declaration?.type !== "TSInterfaceDeclaration") continue;
|
|
27
|
+
const declarations = interfaces.get(declaration.id.name) ?? [];
|
|
28
|
+
declarations.push(declaration);
|
|
29
|
+
interfaces.set(declaration.id.name, declarations);
|
|
51
30
|
}
|
|
52
31
|
return {
|
|
53
|
-
aliases,
|
|
54
32
|
interfaces,
|
|
55
|
-
|
|
33
|
+
typeAliases: createTypeAliasEnvironment(program, visitorKeys)
|
|
56
34
|
};
|
|
57
35
|
}
|
|
58
36
|
function typeReferenceName(type) {
|
|
59
37
|
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
60
38
|
}
|
|
61
|
-
function isBuiltIn(name, environment) {
|
|
62
|
-
return BUILT_INS.has(name) && !environment.
|
|
39
|
+
function isBuiltIn(name, use, environment) {
|
|
40
|
+
return BUILT_INS.has(name) && !hasVisibleTypeBinding(name, use, environment.typeAliases);
|
|
63
41
|
}
|
|
64
42
|
function isUnappliedReferenceTo(type, name) {
|
|
65
43
|
const unwrapped = unwrapTransparentType(type);
|
|
@@ -84,13 +62,16 @@ function isEffectivelyEmptyInterface(declarations) {
|
|
|
84
62
|
const [type] = declarations;
|
|
85
63
|
return type !== void 0 && type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
|
|
86
64
|
}
|
|
87
|
-
function resolvedSubstitutionArgument(type, base) {
|
|
65
|
+
function resolvedSubstitutionArgument(type, base, resolving = /* @__PURE__ */ new Set()) {
|
|
88
66
|
const unwrapped = unwrapTransparentType(type);
|
|
89
67
|
if (unwrapped.type !== "TSTypeReference") return type;
|
|
90
68
|
const name = typeReferenceName(unwrapped);
|
|
91
|
-
if (name === null) return type;
|
|
69
|
+
if (name === null || resolving.has(name)) return type;
|
|
92
70
|
const substitution = base.get(name);
|
|
93
|
-
|
|
71
|
+
if (substitution === void 0) return type;
|
|
72
|
+
const nextResolving = new Set(resolving);
|
|
73
|
+
nextResolving.add(name);
|
|
74
|
+
return resolvedSubstitutionArgument(substitution, base, nextResolving);
|
|
94
75
|
}
|
|
95
76
|
function aliasSubstitution(alias, type, base) {
|
|
96
77
|
const parameters = alias.typeParameters?.params ?? [];
|
|
@@ -118,7 +99,7 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
|
118
99
|
if (unwrapped.type !== "TSTypeReference") return null;
|
|
119
100
|
const name = typeReferenceName(unwrapped);
|
|
120
101
|
if (name === null) return null;
|
|
121
|
-
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
102
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
122
103
|
const wrapped = unwrapped.typeArguments?.params[0];
|
|
123
104
|
return wrapped === void 0 ? null : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
|
|
124
105
|
}
|
|
@@ -126,8 +107,8 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
|
126
107
|
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
|
|
127
108
|
const interfaceDeclarations = environment.interfaces.get(name);
|
|
128
109
|
if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
|
|
129
|
-
const alias = environment.
|
|
130
|
-
if (alias ===
|
|
110
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
111
|
+
if (alias === null || resolvingAliases.has(name)) return null;
|
|
131
112
|
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
132
113
|
if (nextSubstitutions === null) return null;
|
|
133
114
|
const nextResolving = new Set(resolvingAliases);
|
|
@@ -149,23 +130,23 @@ function dictionaryValueTypes(type, environment, substitutions, resolvingAliases
|
|
|
149
130
|
if (name === null) return [];
|
|
150
131
|
const substitution = substitutions.get(name);
|
|
151
132
|
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
|
|
152
|
-
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
133
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
153
134
|
const wrapped = unwrapped.typeArguments?.params[0];
|
|
154
135
|
return wrapped === void 0 ? [] : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
|
|
155
136
|
}
|
|
156
|
-
if (name === "Record" && isBuiltIn(name, environment)) {
|
|
137
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
157
138
|
const value = unwrapped.typeArguments?.params[1] ?? null;
|
|
158
139
|
return value === null ? [] : [{
|
|
159
140
|
type: value,
|
|
160
141
|
substitutions
|
|
161
142
|
}];
|
|
162
143
|
}
|
|
163
|
-
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, environment)) {
|
|
144
|
+
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) {
|
|
164
145
|
const source = unwrapped.typeArguments?.params[0];
|
|
165
146
|
return source === void 0 ? [] : dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
|
|
166
147
|
}
|
|
167
|
-
const alias = environment.
|
|
168
|
-
if (alias ===
|
|
148
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
149
|
+
if (alias === null || resolvingAliases.has(name)) return [];
|
|
169
150
|
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
170
151
|
if (nextSubstitutions === null) return [];
|
|
171
152
|
const nextResolving = new Set(resolvingAliases);
|
|
@@ -189,9 +170,6 @@ function classifyUnsafeDictionary(type, environment) {
|
|
|
189
170
|
}
|
|
190
171
|
return null;
|
|
191
172
|
}
|
|
192
|
-
function resolvesToDictionary(type, environment, substitutions, resolvingAliases) {
|
|
193
|
-
return dictionaryValueTypes(type, environment, substitutions, resolvingAliases).length > 0;
|
|
194
|
-
}
|
|
195
173
|
function classifyWideningTarget(type, environment) {
|
|
196
174
|
const unwrapped = unwrapTransparentType(type);
|
|
197
175
|
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
@@ -201,32 +179,59 @@ function classifyWideningTarget(type, environment) {
|
|
|
201
179
|
if (unwrapped.type !== "TSTypeReference") return null;
|
|
202
180
|
const name = typeReferenceName(unwrapped);
|
|
203
181
|
if (name === null) return null;
|
|
204
|
-
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
182
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
205
183
|
const wrapped = unwrapped.typeArguments?.params[0];
|
|
206
184
|
return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
|
|
207
185
|
}
|
|
208
|
-
if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
|
|
209
|
-
const alias = environment.
|
|
210
|
-
if (alias ===
|
|
186
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, /* @__PURE__ */ new Map()) ? { kind: "open dictionary" } : null;
|
|
187
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
188
|
+
if (alias === null) return null;
|
|
211
189
|
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
212
190
|
const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
|
|
213
|
-
return substitutions
|
|
191
|
+
return (substitutions === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name])))?.kind === "open dictionary" ? { kind: "generic container" } : null;
|
|
214
192
|
}
|
|
215
193
|
const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
|
|
216
194
|
if (substitutions === null) return null;
|
|
217
195
|
return classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name]));
|
|
218
196
|
}
|
|
197
|
+
function hasBroadRecordKey(type, environment, substitutions) {
|
|
198
|
+
const key = type.typeArguments?.params[0];
|
|
199
|
+
return key === void 0 || isBroadMappedKey(key, environment, substitutions);
|
|
200
|
+
}
|
|
201
|
+
function isBroadMappedKey(type, environment, substitutions, visitedAliases = /* @__PURE__ */ new Set()) {
|
|
202
|
+
const unwrapped = unwrapTransparentType(type);
|
|
203
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
204
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
|
|
205
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
206
|
+
const name = typeReferenceName(unwrapped);
|
|
207
|
+
if (name === null) return false;
|
|
208
|
+
const substitution = substitutions.get(name);
|
|
209
|
+
if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
|
|
210
|
+
if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
|
|
211
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
212
|
+
if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0 || visitedAliases.has(name)) return false;
|
|
213
|
+
const nextVisited = new Set(visitedAliases);
|
|
214
|
+
nextVisited.add(name);
|
|
215
|
+
return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
|
|
216
|
+
}
|
|
219
217
|
function classifyAliasBroadTarget(type, environment, substitutions, resolvingAliases) {
|
|
220
218
|
const unwrapped = unwrapTransparentType(type);
|
|
221
219
|
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
222
220
|
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
221
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
|
|
222
|
+
if (unwrapped.type === "TSMappedType") return isBroadMappedKey(unwrapped.constraint, environment, substitutions) ? { kind: "open dictionary" } : null;
|
|
223
223
|
if (unwrapped.type !== "TSTypeReference") return null;
|
|
224
224
|
const name = typeReferenceName(unwrapped);
|
|
225
225
|
if (name === null) return null;
|
|
226
226
|
const substitution = substitutions.get(name);
|
|
227
|
-
if (substitution !== void 0) return classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
|
|
228
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
229
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
230
|
+
return wrapped === void 0 ? null : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
|
|
231
|
+
}
|
|
232
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, substitutions) ? { kind: "open dictionary" } : null;
|
|
233
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
234
|
+
if (alias === null || resolvingAliases.has(name)) return null;
|
|
230
235
|
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
231
236
|
if (nextSubstitutions === null) return null;
|
|
232
237
|
const nextResolving = new Set(resolvingAliases);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/rules/_anti-slop-function-parameters.ts
|
|
2
|
+
/** Return whether a type is or contains TypeScript's absorbing unknown top type. */
|
|
3
|
+
function containsUnknownType(type) {
|
|
4
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
5
|
+
if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation);
|
|
6
|
+
return type.type === "TSUnionType" && type.types.some(containsUnknownType);
|
|
7
|
+
}
|
|
8
|
+
/** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
|
|
9
|
+
function functionParameterTypeAnnotation(parameter) {
|
|
10
|
+
if (parameter.type === "TSParameterProperty") return functionParameterTypeAnnotation(parameter.parameter);
|
|
11
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument);
|
|
12
|
+
if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left);
|
|
13
|
+
return parameter.typeAnnotation;
|
|
14
|
+
}
|
|
15
|
+
/** Return only a function parameter's local binding, excluding its annotation and default value. */
|
|
16
|
+
function functionParameterBindingName(parameter, sourceCode) {
|
|
17
|
+
if (parameter.type === "TSParameterProperty") return functionParameterBindingName(parameter.parameter, sourceCode);
|
|
18
|
+
if (parameter.type === "AssignmentPattern") return functionParameterBindingName(parameter.left, sourceCode);
|
|
19
|
+
if (parameter.type === "RestElement") return functionParameterBindingName(parameter.argument, sourceCode);
|
|
20
|
+
const annotation = functionParameterTypeAnnotation(parameter);
|
|
21
|
+
const annotationOffset = annotation === null || annotation === void 0 ? void 0 : annotation.start - parameter.start;
|
|
22
|
+
if (parameter.type === "Identifier") return parameter.name;
|
|
23
|
+
const sourceText = sourceCode.getText(parameter);
|
|
24
|
+
return annotationOffset === void 0 ? sourceText : sourceText.slice(0, annotationOffset).trimEnd();
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/rules/_anti-slop-lexical-type-parameters.ts
|
|
2
|
+
function isNode(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
4
|
+
}
|
|
5
|
+
function collectInferTypeParameterNames(node, visitorKeys, names) {
|
|
6
|
+
if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
|
|
7
|
+
const record = node;
|
|
8
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
9
|
+
const value = record[key];
|
|
10
|
+
if (isNode(value)) {
|
|
11
|
+
collectInferTypeParameterNames(value, visitorKeys, names);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (!Array.isArray(value)) continue;
|
|
15
|
+
for (const child of value) if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Collect type binders that are in scope at a node and can shadow module aliases. */
|
|
19
|
+
function lexicalTypeParameterNames(node, visitorKeys) {
|
|
20
|
+
const names = /* @__PURE__ */ new Set();
|
|
21
|
+
let descendant = node;
|
|
22
|
+
let current = node;
|
|
23
|
+
while (current !== null && current.type !== "Program") {
|
|
24
|
+
if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
|
|
25
|
+
if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
|
|
26
|
+
if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
|
|
27
|
+
descendant = current;
|
|
28
|
+
current = current.parent;
|
|
29
|
+
}
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { lexicalTypeParameterNames };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolveVariable } from "./_anti-slop-scope.js";
|
|
2
|
+
//#region src/rules/_anti-slop-reflect-method.ts
|
|
3
|
+
/**
|
|
4
|
+
* Ported from dmmulroy/anti-slop at
|
|
5
|
+
* c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
|
|
6
|
+
*/
|
|
7
|
+
function isGlobalReflect(sourceCode, expression) {
|
|
8
|
+
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
9
|
+
if (sourceCode.isGlobalReference(expression)) return true;
|
|
10
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
11
|
+
return variable === null || variable.defs.length === 0;
|
|
12
|
+
}
|
|
13
|
+
/** Reports whether a call target names one method on the global Reflect object. */
|
|
14
|
+
function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
|
|
15
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
16
|
+
if (!isGlobalReflect(sourceCode, callee.object)) return false;
|
|
17
|
+
const property = callee.property;
|
|
18
|
+
return callee.computed ? property.type === "Literal" && property.value === methodName : property.type === "Identifier" && property.name === methodName;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { isGlobalReflectMethodCall };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/rules/_anti-slop-scope.ts
|
|
2
|
+
/** Resolve an identifier to its binding by walking lexical scopes upward. */
|
|
3
|
+
function resolveVariable(sourceCode, identifier) {
|
|
4
|
+
let scope = sourceCode.getScope(identifier);
|
|
5
|
+
while (scope !== null) {
|
|
6
|
+
const variable = scope.set.get(identifier.name);
|
|
7
|
+
if (variable !== void 0) return variable;
|
|
8
|
+
scope = scope.upper;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { resolveVariable };
|