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,26 +2,23 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
-
import { isInsideCatchAllHandler, tagComparison, taggedSwitchSubject } from "./_tagged-control-flow.js";
|
|
5
|
+
import { hasOneTaggedSubject, isInsideCatchAllHandler, tagComparison, 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-match-tags-exhaustive.ts
|
|
8
11
|
const statementReturns = (statement) => {
|
|
9
12
|
if (statement.type === "ReturnStatement") return true;
|
|
10
13
|
if (statement.type !== "BlockStatement") return false;
|
|
11
|
-
|
|
12
|
-
if (last === void 0) return false;
|
|
13
|
-
return statementReturns(last);
|
|
14
|
+
return Option.exists(Arr.last(statement.body), statementReturns);
|
|
14
15
|
};
|
|
15
|
-
const caseReturns = (switchCase) =>
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
return statementReturns(last);
|
|
19
|
-
};
|
|
20
|
-
const isCompleteTransformationShape = (node) => {
|
|
21
|
-
if (node.cases.length < 2 || taggedSwitchSubject(node) === null) return false;
|
|
16
|
+
const caseReturns = (switchCase) => Option.exists(Arr.last(switchCase.consequent), statementReturns);
|
|
17
|
+
const isCompleteSwitchTransformation = (node) => {
|
|
18
|
+
if (node.cases.length < 2 || Option.isNone(taggedSwitchSubject(node))) return false;
|
|
22
19
|
for (const switchCase of node.cases) {
|
|
23
|
-
if (switchCase.test
|
|
24
|
-
if (
|
|
20
|
+
if (switchCase.test?.type !== "Literal") return false;
|
|
21
|
+
if (!Predicate.isString(switchCase.test.value) || !caseReturns(switchCase)) return false;
|
|
25
22
|
}
|
|
26
23
|
return true;
|
|
27
24
|
};
|
|
@@ -30,45 +27,36 @@ const isTerminalStatement = (node) => {
|
|
|
30
27
|
if (node.parent?.type !== "BlockStatement") return true;
|
|
31
28
|
return node.parent.body.at(-1) === node;
|
|
32
29
|
};
|
|
33
|
-
const
|
|
34
|
-
|
|
30
|
+
const isPlainIf = (statement) => statement.type === "IfStatement" && Predicate.isNull(statement.alternate);
|
|
31
|
+
/** The tag a branch compares, when the branch always returns. */
|
|
32
|
+
const returningTagComparison = (branch) => Option.filter(tagComparison(branch.test), () => statementReturns(branch.consequent));
|
|
33
|
+
/** Whether at least two returning branches each compare one subject against a distinct tag. */
|
|
34
|
+
const isDistinctTagDispatch = (branches) => Option.exists(Option.all(branches.map(returningTagComparison)), (comparisons) => hasOneTaggedSubject(comparisons) && new Set(comparisons.map((comparison) => comparison.tag)).size === comparisons.length);
|
|
35
|
+
/** The if statement and each `else if` after it; none when the chain ends in a plain else. */
|
|
36
|
+
const elseIfChain = (node) => {
|
|
37
|
+
const chain = [node];
|
|
35
38
|
let current = node;
|
|
36
|
-
|
|
37
|
-
const tags = /* @__PURE__ */ new Set();
|
|
38
|
-
let branchCount = 0;
|
|
39
|
-
while (true) {
|
|
40
|
-
const comparison = tagComparison(current.test);
|
|
41
|
-
if (comparison === null || !statementReturns(current.consequent)) return false;
|
|
42
|
-
if (subject !== void 0 && comparison.subject !== subject) return false;
|
|
43
|
-
if (tags.has(comparison.tag)) return false;
|
|
44
|
-
subject = comparison.subject;
|
|
45
|
-
tags.add(comparison.tag);
|
|
46
|
-
branchCount += 1;
|
|
47
|
-
if (current.alternate === null) break;
|
|
48
|
-
if (current.alternate.type !== "IfStatement") return false;
|
|
39
|
+
while (current.alternate?.type === "IfStatement") {
|
|
49
40
|
current = current.alternate;
|
|
41
|
+
chain.push(current);
|
|
50
42
|
}
|
|
51
|
-
|
|
43
|
+
if (Predicate.isNotNull(current.alternate)) return Option.none();
|
|
44
|
+
return Option.some(chain);
|
|
45
|
+
};
|
|
46
|
+
const isCompleteIfTransformation = (node) => {
|
|
47
|
+
if (isElseIf(node) || !isTerminalStatement(node)) return false;
|
|
48
|
+
return Option.exists(elseIfChain(node), isDistinctTagDispatch);
|
|
52
49
|
};
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
/** Whether `previous` is a returning tag check on the same subject, so `node` does not start a run. */
|
|
51
|
+
const continuesRun = (previous, node) => Option.exists(previous, (statement) => isPlainIf(statement) && statementReturns(statement.consequent) && Option.exists(Option.all([tagComparison(statement.test), tagComparison(node.test)]), ([before, current]) => before.subject === current.subject));
|
|
52
|
+
const isCompleteSequentialIfTransformation = (node) => {
|
|
53
|
+
if (!isPlainIf(node) || node.parent?.type !== "BlockStatement") return false;
|
|
55
54
|
const statements = node.parent.body;
|
|
56
55
|
const index = statements.indexOf(node);
|
|
57
56
|
if (index < 0) return false;
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
if (previous?.type === "IfStatement" && previous.alternate === null) {
|
|
62
|
-
if (tagComparison(previous.test)?.subject === comparison.subject && statementReturns(previous.consequent)) return false;
|
|
63
|
-
}
|
|
64
|
-
const tags = new Set([comparison.tag]);
|
|
65
|
-
for (const statement of statements.slice(index + 1)) {
|
|
66
|
-
if (statement.type !== "IfStatement" || statement.alternate !== null) return false;
|
|
67
|
-
const next = tagComparison(statement.test);
|
|
68
|
-
if (next === null || next.subject !== comparison.subject || tags.has(next.tag) || !statementReturns(statement.consequent)) return false;
|
|
69
|
-
tags.add(next.tag);
|
|
70
|
-
}
|
|
71
|
-
return tags.size >= 2;
|
|
57
|
+
const run = statements.slice(index);
|
|
58
|
+
if (!run.every(isPlainIf) || continuesRun(Arr.get(statements, index - 1), node)) return false;
|
|
59
|
+
return isDistinctTagDispatch(run);
|
|
72
60
|
};
|
|
73
61
|
const preferMatchTagsExhaustive = define({
|
|
74
62
|
name: "prefer-match-tags-exhaustive",
|
|
@@ -80,14 +68,14 @@ const preferMatchTagsExhaustive = define({
|
|
|
80
68
|
const context = yield* RuleContext;
|
|
81
69
|
return {
|
|
82
70
|
SwitchStatement: (node) => {
|
|
83
|
-
if (!
|
|
71
|
+
if (!isCompleteSwitchTransformation(node) || isInsideCatchAllHandler(node)) return Effect.void;
|
|
84
72
|
return context.report(make({
|
|
85
73
|
node,
|
|
86
74
|
message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new case."
|
|
87
75
|
}));
|
|
88
76
|
},
|
|
89
77
|
IfStatement: (node) => {
|
|
90
|
-
if (!
|
|
78
|
+
if (!isCompleteIfTransformation(node) && !isCompleteSequentialIfTransformation(node) || isInsideCatchAllHandler(node)) return Effect.void;
|
|
91
79
|
return context.report(make({
|
|
92
80
|
node,
|
|
93
81
|
message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new branch."
|
|
@@ -3,15 +3,14 @@ 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 { effectCallbackLocation, hasOneTaggedSubject, tagComparisonsInOr } from "./_tagged-control-flow.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
//#region src/rules/prefer-predicate-is-tagged.ts
|
|
8
|
-
const isCatchIfPredicate = (node) => {
|
|
9
|
-
const location = effectCallbackLocation(node, "catchIf");
|
|
10
|
-
if (location === null) return false;
|
|
9
|
+
const isCatchIfPredicate = (node) => Option.exists(effectCallbackLocation(node, "catchIf"), (location) => {
|
|
11
10
|
let predicateIndex = 1;
|
|
12
11
|
if (location.argumentCount === 2) predicateIndex = 0;
|
|
13
12
|
return location.index === predicateIndex;
|
|
14
|
-
};
|
|
13
|
+
});
|
|
15
14
|
const preferPredicateIsTagged = define({
|
|
16
15
|
name: "prefer-predicate-is-tagged",
|
|
17
16
|
meta: meta({
|
|
@@ -24,8 +23,7 @@ const preferPredicateIsTagged = define({
|
|
|
24
23
|
if (node.operator !== "||") return Effect.void;
|
|
25
24
|
if (node.parent?.type === "LogicalExpression" && node.parent.operator === "||") return Effect.void;
|
|
26
25
|
if (isCatchIfPredicate(node)) return Effect.void;
|
|
27
|
-
|
|
28
|
-
if (comparisons === null || !hasOneTaggedSubject(comparisons)) return Effect.void;
|
|
26
|
+
if (!Option.exists(tagComparisonsInOr(node), hasOneTaggedSubject)) return Effect.void;
|
|
29
27
|
return context.report(make({
|
|
30
28
|
node,
|
|
31
29
|
message: "Use a named Predicate refinement composed from Predicate.isTagged for multiple tags."
|
|
@@ -2,33 +2,44 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { expressionArgument } from "./_call-arguments.js";
|
|
5
6
|
import { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
7
|
+
import * as Arr from "effect/Array";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Predicate from "effect/Predicate";
|
|
6
10
|
import * as Effect from "effect/Effect";
|
|
7
11
|
//#region src/rules/prefer-service-of.ts
|
|
12
|
+
const isObjectExpression = (node) => node.type === "ObjectExpression";
|
|
13
|
+
const returnedObjectStatement = (statement) => {
|
|
14
|
+
if (statement.type !== "ReturnStatement" || statement.argument?.type !== "ObjectExpression") return Option.none();
|
|
15
|
+
return Option.some(statement.argument);
|
|
16
|
+
};
|
|
17
|
+
/** The object a function argument returns: its expression body or its first returned object. */
|
|
8
18
|
const returnedObject = (node) => {
|
|
9
|
-
if (node
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
if (
|
|
13
|
-
|
|
19
|
+
if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return Option.none();
|
|
20
|
+
const body = node.body;
|
|
21
|
+
if (Predicate.isNull(body)) return Option.none();
|
|
22
|
+
if (body.type === "ObjectExpression") return Option.some(body);
|
|
23
|
+
if (body.type !== "BlockStatement") return Option.none();
|
|
24
|
+
return Arr.findFirst(body.body, returnedObjectStatement);
|
|
25
|
+
};
|
|
26
|
+
/** The object an `Effect.succeed(...)` or `Effect.gen(...)` implementation produces. */
|
|
27
|
+
const effectImplementationObject = (implementation, effectNamespaces) => {
|
|
28
|
+
if (implementation.type !== "CallExpression" || implementation.callee.type === "Super") return Option.none();
|
|
29
|
+
if (isStaticMember(implementation.callee, effectNamespaces, "succeed")) return Option.filter(Arr.head(implementation.arguments), isObjectExpression);
|
|
30
|
+
if (isStaticCall(implementation, effectNamespaces, "gen")) return Option.flatMap(Arr.head(implementation.arguments), returnedObject);
|
|
31
|
+
return Option.none();
|
|
32
|
+
};
|
|
33
|
+
const layerImplementationObject = (callee, implementation, layerNamespaces, effectNamespaces) => {
|
|
34
|
+
if (isStaticMember(callee, layerNamespaces, "succeed")) return Option.liftPredicate(implementation, isObjectExpression);
|
|
35
|
+
if (isStaticMember(callee, layerNamespaces, "sync")) return returnedObject(implementation);
|
|
36
|
+
if (!isStaticMember(callee, layerNamespaces, "effect")) return Option.none();
|
|
37
|
+
return effectImplementationObject(implementation, effectNamespaces);
|
|
14
38
|
};
|
|
15
39
|
const implementationObject = (node, layerNamespaces, effectNamespaces) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (isStaticMember(node.callee, layerNamespaces, "succeed")) {
|
|
20
|
-
if (implementation.type === "ObjectExpression") return implementation;
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (isStaticMember(node.callee, layerNamespaces, "sync")) return returnedObject(implementation);
|
|
24
|
-
if (!isStaticMember(node.callee, layerNamespaces, "effect")) return void 0;
|
|
25
|
-
if (implementation.type !== "CallExpression" || implementation.callee.type === "Super") return;
|
|
26
|
-
if (isStaticMember(implementation.callee, effectNamespaces, "succeed")) {
|
|
27
|
-
const value = implementation.arguments[0];
|
|
28
|
-
if (value?.type === "ObjectExpression") return value;
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (isStaticCall(implementation, effectNamespaces, "gen")) return returnedObject(implementation.arguments[0]);
|
|
40
|
+
const callee = node.callee;
|
|
41
|
+
if (callee.type === "Super") return Option.none();
|
|
42
|
+
return Option.flatMap(expressionArgument(node, 1), (implementation) => layerImplementationObject(callee, implementation, layerNamespaces, effectNamespaces));
|
|
32
43
|
};
|
|
33
44
|
const preferServiceOf = define({
|
|
34
45
|
name: "prefer-service-of",
|
|
@@ -52,11 +63,13 @@ const preferServiceOf = define({
|
|
|
52
63
|
const service = node.arguments[0];
|
|
53
64
|
if (service?.type !== "Identifier") return Effect.void;
|
|
54
65
|
const implementation = implementationObject(node, visibleNamespaces(ctx, node, layerNamespaces), visibleNamespaces(ctx, node, effectNamespaces));
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
return Option.match(implementation, {
|
|
67
|
+
onNone: () => Effect.void,
|
|
68
|
+
onSome: (object) => ctx.report(make({
|
|
69
|
+
node: object,
|
|
70
|
+
message: `Wrap this implementation with ${service.name}.of(...) so it stays checked against the service interface.`
|
|
71
|
+
}))
|
|
72
|
+
});
|
|
60
73
|
}
|
|
61
74
|
};
|
|
62
75
|
}
|
|
@@ -3,9 +3,11 @@ 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 { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
|
+
import * as Arr from "effect/Array";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
6
8
|
import * as Effect from "effect/Effect";
|
|
7
9
|
//#region src/rules/require-named-effect-fn.ts
|
|
8
|
-
const isFunction = (node) => node
|
|
10
|
+
const isFunction = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
|
|
9
11
|
const requireNamedEffectFn = define({
|
|
10
12
|
name: "require-named-effect-fn",
|
|
11
13
|
meta: meta({
|
|
@@ -24,7 +26,7 @@ const requireNamedEffectFn = define({
|
|
|
24
26
|
CallExpression: (node) => {
|
|
25
27
|
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
26
28
|
const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
|
|
27
|
-
if (!isStaticMember(node.callee, namespaces, "fn") || !
|
|
29
|
+
if (!isStaticMember(node.callee, namespaces, "fn") || !Option.exists(Arr.head(node.arguments), isFunction)) return Effect.void;
|
|
28
30
|
return ctx.report(make({
|
|
29
31
|
node,
|
|
30
32
|
message: "Name this operation with Effect.fn(\"Domain.operation\") for stable tracing and diagnostics."
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
let targetText = padded;
|
|
28
|
+
let reason = "";
|
|
29
|
+
if (separator !== -1) {
|
|
30
|
+
targetText = padded.slice(0, separator);
|
|
31
|
+
reason = padded.slice(separator + 4);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: kindOf(name),
|
|
35
|
+
name,
|
|
36
|
+
targets: targetText.split(/[\s,]+/u).filter((target) => target.length > 0),
|
|
37
|
+
reason: reason.trim()
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const checkDirective = (directive) => {
|
|
42
|
+
const findings = [];
|
|
43
|
+
if (directive.kind !== "typescript" && directive.targets.length === 0) {
|
|
44
|
+
let example = "ruleName:off";
|
|
45
|
+
if (directive.kind === "lint") example = "rule-name";
|
|
46
|
+
findings.push({
|
|
47
|
+
messageId: "missingTarget",
|
|
48
|
+
data: {
|
|
49
|
+
directive: directive.name,
|
|
50
|
+
example
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (directive.kind === "effect-diagnostics") for (const target of directive.targets) {
|
|
55
|
+
if (!target.startsWith("effect/")) continue;
|
|
56
|
+
findings.push({
|
|
57
|
+
messageId: "effectPrefix",
|
|
58
|
+
data: {
|
|
59
|
+
target,
|
|
60
|
+
bare: target.slice(7)
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (directive.reason.length === 0) findings.push({
|
|
65
|
+
messageId: "missingReason",
|
|
66
|
+
data: { directive: directive.name }
|
|
67
|
+
});
|
|
68
|
+
return findings;
|
|
69
|
+
};
|
|
70
|
+
const requireSuppressionReason = define({
|
|
71
|
+
name: "require-suppression-reason",
|
|
72
|
+
meta: meta({
|
|
73
|
+
type: "problem",
|
|
74
|
+
description: "Require oxlint, ESLint, Effect diagnostics, and TypeScript suppressions to name what they disable and give a reason after ` -- `.",
|
|
75
|
+
messages: {
|
|
76
|
+
missingTarget: "`{{directive}}` must name what it disables, for example `{{directive}} {{example}} -- reason`. A blanket suppression hides every future finding on the code it covers.",
|
|
77
|
+
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.",
|
|
78
|
+
effectPrefix: "`@effect-diagnostics` ignores `{{target}}`: @effect/tsgo does not accept the `effect/` prefix. Write the bare rule name, `{{bare}}`."
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
create: function* () {
|
|
82
|
+
const ctx = yield* RuleContext;
|
|
83
|
+
const checkComment = (comment) => Effect.forEach(parseDirectives(comment.value).flatMap(checkDirective), (finding) => ctx.report(fromId({
|
|
84
|
+
node: comment,
|
|
85
|
+
messageId: finding.messageId,
|
|
86
|
+
data: finding.data
|
|
87
|
+
})), { discard: true });
|
|
88
|
+
return { Program: () => Effect.forEach(ctx.sourceCode.getAllComments().filter((comment) => comment.type !== "Shebang"), checkComment, { discard: true }) };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
//#endregion
|
|
92
|
+
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.15.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",
|