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
|
@@ -2,13 +2,9 @@ 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";
|
|
5
6
|
import * as Effect from "effect/Effect";
|
|
6
7
|
//#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
8
|
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
13
9
|
const noUnknownTypeAliases = define({
|
|
14
10
|
name: "no-unknown-type-aliases",
|
|
@@ -19,29 +15,23 @@ const noUnknownTypeAliases = define({
|
|
|
19
15
|
}),
|
|
20
16
|
create: function* () {
|
|
21
17
|
const context = yield* RuleContext;
|
|
22
|
-
|
|
23
|
-
const resolvesToUnknown = (type
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (const statement of node.body) {
|
|
36
|
-
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
37
|
-
if (declaration?.type === "TSTypeAliasDeclaration") aliases.set(declaration.id.name, declaration);
|
|
38
|
-
}
|
|
39
|
-
return Effect.forEach(aliases.values(), (alias) => resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name])) ? context.report(fromId({
|
|
40
|
-
node: alias.id,
|
|
18
|
+
let environment = null;
|
|
19
|
+
const resolvesToUnknown = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
20
|
+
if (resolved.type === "TSUnknownKeyword") return true;
|
|
21
|
+
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
22
|
+
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
Program: (node) => {
|
|
26
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
27
|
+
return Effect.void;
|
|
28
|
+
},
|
|
29
|
+
TSTypeAliasDeclaration: (node) => resolvesToUnknown(node.typeAnnotation) ? context.report(fromId({
|
|
30
|
+
node: node.id,
|
|
41
31
|
messageId: "unknownAlias",
|
|
42
|
-
data: { alias:
|
|
43
|
-
})) : Effect.void
|
|
44
|
-
}
|
|
32
|
+
data: { alias: node.id.name }
|
|
33
|
+
})) : Effect.void
|
|
34
|
+
};
|
|
45
35
|
}
|
|
46
36
|
});
|
|
47
37
|
//#endregion
|
|
@@ -2,11 +2,51 @@ 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 { visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
|
|
5
6
|
import { classifyUnsafeDictionary, classifyUnsafeDictionaryValue, createTypeEnvironment } from "./_anti-slop-dictionary-types.js";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
//#region src/rules/no-unsafe-dictionary-type.ts
|
|
9
|
+
const typeNodeKinds = new Set([
|
|
10
|
+
"JSDocNonNullableType",
|
|
11
|
+
"JSDocNullableType",
|
|
12
|
+
"JSDocUnknownType",
|
|
13
|
+
"TSAnyKeyword",
|
|
14
|
+
"TSArrayType",
|
|
15
|
+
"TSBigIntKeyword",
|
|
16
|
+
"TSBooleanKeyword",
|
|
17
|
+
"TSConditionalType",
|
|
18
|
+
"TSConstructorType",
|
|
19
|
+
"TSFunctionType",
|
|
20
|
+
"TSImportType",
|
|
21
|
+
"TSIndexedAccessType",
|
|
22
|
+
"TSInferType",
|
|
23
|
+
"TSIntersectionType",
|
|
24
|
+
"TSIntrinsicKeyword",
|
|
25
|
+
"TSLiteralType",
|
|
26
|
+
"TSMappedType",
|
|
27
|
+
"TSNamedTupleMember",
|
|
28
|
+
"TSNeverKeyword",
|
|
29
|
+
"TSNullKeyword",
|
|
30
|
+
"TSNumberKeyword",
|
|
31
|
+
"TSObjectKeyword",
|
|
32
|
+
"TSParenthesizedType",
|
|
33
|
+
"TSStringKeyword",
|
|
34
|
+
"TSSymbolKeyword",
|
|
35
|
+
"TSTemplateLiteralType",
|
|
36
|
+
"TSThisType",
|
|
37
|
+
"TSTupleType",
|
|
38
|
+
"TSTypeLiteral",
|
|
39
|
+
"TSTypeOperator",
|
|
40
|
+
"TSTypePredicate",
|
|
41
|
+
"TSTypeQuery",
|
|
42
|
+
"TSTypeReference",
|
|
43
|
+
"TSUndefinedKeyword",
|
|
44
|
+
"TSUnionType",
|
|
45
|
+
"TSUnknownKeyword",
|
|
46
|
+
"TSVoidKeyword"
|
|
47
|
+
]);
|
|
8
48
|
function isTypeNode(node) {
|
|
9
|
-
return
|
|
49
|
+
return typeNodeKinds.has(node.type);
|
|
10
50
|
}
|
|
11
51
|
function typeReferenceName(type) {
|
|
12
52
|
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
@@ -22,9 +62,20 @@ function isInsideTypeAliasDeclaration(node) {
|
|
|
22
62
|
function isPlainAliasConsumerUse(node, environment) {
|
|
23
63
|
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
24
64
|
const name = typeReferenceName(node);
|
|
25
|
-
return name !== null && environment.
|
|
65
|
+
return name !== null && visibleTypeAlias(name, node, environment.typeAliases) !== null && !isInsideTypeAliasDeclaration(node);
|
|
66
|
+
}
|
|
67
|
+
function isInsideTypeParameterConstraint(node) {
|
|
68
|
+
let child = node;
|
|
69
|
+
let parent = child.parent;
|
|
70
|
+
while (parent !== null && parent.type !== "Program") {
|
|
71
|
+
if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
|
|
72
|
+
child = parent;
|
|
73
|
+
parent = child.parent;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
26
76
|
}
|
|
27
77
|
function shouldReportType(node, environment) {
|
|
78
|
+
if (isInsideTypeParameterConstraint(node)) return false;
|
|
28
79
|
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
29
80
|
if (classifyUnsafeDictionary(node, environment) === null) return false;
|
|
30
81
|
let current = node.parent;
|
|
@@ -57,7 +108,7 @@ const noUnsafeDictionaryType = define({
|
|
|
57
108
|
};
|
|
58
109
|
return {
|
|
59
110
|
Program: (node) => {
|
|
60
|
-
environment = createTypeEnvironment(node);
|
|
111
|
+
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
|
|
61
112
|
return Effect.void;
|
|
62
113
|
},
|
|
63
114
|
TSTypeReference: reportIfUnsafe,
|
|
@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect";
|
|
|
6
6
|
//#region src/rules/no-widen-then-assert.ts
|
|
7
7
|
/**
|
|
8
8
|
* Ported from dmmulroy/anti-slop at
|
|
9
|
-
*
|
|
9
|
+
* c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
|
|
10
10
|
*/
|
|
11
11
|
const functionBoundaryTypes = new Set([
|
|
12
12
|
"ArrowFunctionExpression",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/require-suppression-reason.ts
|
|
7
|
+
/**
|
|
8
|
+
* A directive at the start of a comment line, after any JSDoc `*` gutter.
|
|
9
|
+
* The rules and reason run to the next directive or the end of the comment.
|
|
10
|
+
*/
|
|
11
|
+
const directivePattern = /(?:^|\n)[\t *]*(?<name>(?:oxlint|eslint)-disable(?:-next-line|-line)?|@effect-diagnostics(?:-next-line)?|@ts-(?:expect-error|ignore|nocheck))(?=$|[\s:])/gu;
|
|
12
|
+
/** ESLint-style reason separator: `--` with whitespace on both sides. */
|
|
13
|
+
const reasonSeparator = /\s--\s/u;
|
|
14
|
+
const kindOf = (name) => {
|
|
15
|
+
if (name.startsWith("@ts-")) return "typescript";
|
|
16
|
+
if (name.startsWith("@effect-diagnostics")) return "effect-diagnostics";
|
|
17
|
+
return "lint";
|
|
18
|
+
};
|
|
19
|
+
const parseDirectives = (value) => {
|
|
20
|
+
const matches = [...value.matchAll(directivePattern)];
|
|
21
|
+
return matches.map((match, index) => {
|
|
22
|
+
const name = match.groups?.["name"] ?? "";
|
|
23
|
+
const start = match.index + match[0].length;
|
|
24
|
+
const end = matches[index + 1]?.index ?? value.length;
|
|
25
|
+
const padded = ` ${value.slice(start, end).replaceAll(/\n[\t *]*/gu, " ").trim()} `;
|
|
26
|
+
const separator = padded.search(reasonSeparator);
|
|
27
|
+
const targetText = separator === -1 ? padded : padded.slice(0, separator);
|
|
28
|
+
const reason = separator === -1 ? "" : padded.slice(separator + 4);
|
|
29
|
+
return {
|
|
30
|
+
kind: kindOf(name),
|
|
31
|
+
name,
|
|
32
|
+
targets: targetText.split(/[\s,]+/u).filter((target) => target.length > 0),
|
|
33
|
+
reason: reason.trim()
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const checkDirective = (directive) => {
|
|
38
|
+
const findings = [];
|
|
39
|
+
if (directive.kind !== "typescript" && directive.targets.length === 0) {
|
|
40
|
+
const example = directive.kind === "lint" ? "rule-name" : "ruleName:off";
|
|
41
|
+
findings.push({
|
|
42
|
+
messageId: "missingTarget",
|
|
43
|
+
data: {
|
|
44
|
+
directive: directive.name,
|
|
45
|
+
example
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (directive.kind === "effect-diagnostics") for (const target of directive.targets) {
|
|
50
|
+
if (!target.startsWith("effect/")) continue;
|
|
51
|
+
findings.push({
|
|
52
|
+
messageId: "effectPrefix",
|
|
53
|
+
data: {
|
|
54
|
+
target,
|
|
55
|
+
bare: target.slice(7)
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (directive.reason.length === 0) findings.push({
|
|
60
|
+
messageId: "missingReason",
|
|
61
|
+
data: { directive: directive.name }
|
|
62
|
+
});
|
|
63
|
+
return findings;
|
|
64
|
+
};
|
|
65
|
+
const requireSuppressionReason = define({
|
|
66
|
+
name: "require-suppression-reason",
|
|
67
|
+
meta: meta({
|
|
68
|
+
type: "problem",
|
|
69
|
+
description: "Require oxlint, ESLint, Effect diagnostics, and TypeScript suppressions to name what they disable and give a reason after ` -- `.",
|
|
70
|
+
messages: {
|
|
71
|
+
missingTarget: "`{{directive}}` must name what it disables, for example `{{directive}} {{example}} -- reason`. A blanket suppression hides every future finding on the code it covers.",
|
|
72
|
+
missingReason: "`{{directive}}` needs a reason after ` -- ` that says why the check does not apply here, so a reader can tell when the suppression can be removed.",
|
|
73
|
+
effectPrefix: "`@effect-diagnostics` ignores `{{target}}`: @effect/tsgo does not accept the `effect/` prefix. Write the bare rule name, `{{bare}}`."
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
create: function* () {
|
|
77
|
+
const ctx = yield* RuleContext;
|
|
78
|
+
const checkComment = (comment) => Effect.forEach(parseDirectives(comment.value).flatMap(checkDirective), (finding) => ctx.report(fromId({
|
|
79
|
+
node: comment,
|
|
80
|
+
messageId: finding.messageId,
|
|
81
|
+
data: finding.data
|
|
82
|
+
})), { discard: true });
|
|
83
|
+
return { Program: () => Effect.forEach(ctx.sourceCode.getAllComments().filter((comment) => comment.type !== "Shebang"), checkComment, { discard: true }) };
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
//#endregion
|
|
87
|
+
export { requireSuppressionReason };
|
|
@@ -112,4 +112,4 @@ const extractFixes = (result) => {
|
|
|
112
112
|
*/
|
|
113
113
|
const composeFixes = (...fixes) => (fixer) => Arr.flatMap(fixes, (fn) => Array.from(extractFixes(fn(fixer))));
|
|
114
114
|
//#endregion
|
|
115
|
-
export { Diagnostic_exports, fromId, make
|
|
115
|
+
export { Diagnostic_exports, fromId, make };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-effect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "oxlint plugin with Effect-specific lint rules",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
"lint": "concurrently -n ox,effect -c yellow,blue \"oxlint\" \"bun run lint:effect\"",
|
|
46
46
|
"lint:ox": "oxlint",
|
|
47
47
|
"lint:effect": "effect-language-service diagnostics --project tsconfig.json --lspconfig \"$(cat .effect-lsp.json)\"",
|
|
48
|
+
"lint:self": "oxlint -c oxlint.self.config.ts src scripts tests",
|
|
48
49
|
"lint:fix": "oxlint --fix",
|
|
49
50
|
"fmt": "oxfmt",
|
|
50
51
|
"fmt:check": "oxfmt --check",
|
|
51
52
|
"test": "bun run build && bun test",
|
|
52
|
-
"gate": "concurrently -n type,style,generated,test -c blue,yellow,magenta,green \"bun run typecheck\" \"bun run lint\" \"bun run codegen:check\" \"bun
|
|
53
|
+
"gate": "bun run build && concurrently -n type,style,self,generated,test -c blue,yellow,cyan,magenta,green \"bun run typecheck\" \"bun run lint\" \"bun run lint:self\" \"bun run codegen:check\" \"bun test\"",
|
|
53
54
|
"codegen": "bun run scripts/codegen.ts",
|
|
54
55
|
"codegen:check": "bun run scripts/codegen.ts --check",
|
|
55
56
|
"add-rule": "bun run scripts/add-rule.ts",
|