History

Add Effect lint skills and diagnostics

Rhys Sullivan committed 6 months ago

Commit 48272141 parent 726574534 files · +1297 −21Browse files

Showing the changes to scripts (24 of 34 files). Show all

Unified · Split

24 files
  1. scripts/oxlint-plugin-executor.js +18 −0
  2. scripts/oxlint-plugin-executor/rules/no-conditional-tests.js +2 −1
  3. scripts/oxlint-plugin-executor/rules/no-cross-package-relative-imports.js +1 −1
  4. scripts/oxlint-plugin-executor/rules/no-double-cast.js +1 −1
  5. scripts/oxlint-plugin-executor/rules/no-effect-internal-tags.js +1 −1
  6. scripts/oxlint-plugin-executor/rules/no-error-constructor.js +40 −0
  7. scripts/oxlint-plugin-executor/rules/no-inline-object-type-assertion.js +1 −1
  8. scripts/oxlint-plugin-executor/rules/no-instanceof-error.js +22 −0
  9. scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js +1 −1
  10. scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js +13 −1
  11. scripts/oxlint-plugin-executor/rules/no-promise-catch.js +30 −0
  12. scripts/oxlint-plugin-executor/rules/no-promise-client-surface.js +1 −1
  13. scripts/oxlint-plugin-executor/rules/no-promise-reject.js +74 −0
  14. scripts/oxlint-plugin-executor/rules/no-raw-error-throw.js +1 −1
  15. scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js +46 −8
  16. scripts/oxlint-plugin-executor/rules/no-try-catch-or-throw.js +23 −0
  17. scripts/oxlint-plugin-executor/rules/no-ts-nocheck.js +1 −1
  18. scripts/oxlint-plugin-executor/rules/no-unknown-error-message.js +49 −0
  19. scripts/oxlint-plugin-executor/rules/no-unknown-shape-probing.js +1 −1
  20. scripts/oxlint-plugin-executor/rules/no-vitest-import.js +1 −1
  21. scripts/oxlint-plugin-executor/rules/prefer-schema-inferred-types.js +66 −0
  22. scripts/oxlint-plugin-executor/rules/prefer-value-inferred-extension-types.js +81 −0
  23. scripts/oxlint-plugin-executor/rules/prefer-yield-tagged-error.js +40 −0
  24. scripts/oxlint-plugin-executor/rules/require-reactivity-keys.js +1 −1
scripts/oxlint-plugin-executor.js+18−0
@@ -2,15 +2,24 @@ import noConditionalTests from "./oxlint-plugin-executor/rules/no-conditional-te · 1 unmodified line
2import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";2import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";
3import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";3import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";
4import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";4import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";
5import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js";
5import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";6import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";
7import noInstanceofError from "./oxlint-plugin-executor/rules/no-instanceof-error.js";
6import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js";8import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js";
7import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check.js";9import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check.js";
10import noPromiseCatch from "./oxlint-plugin-executor/rules/no-promise-catch.js";
8import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js";11import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js";
12import noPromiseReject from "./oxlint-plugin-executor/rules/no-promise-reject.js";
9import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js";13import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js";
10import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js";14import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js";
11import noTsNocheck from "./oxlint-plugin-executor/rules/no-ts-nocheck.js";15import noTsNocheck from "./oxlint-plugin-executor/rules/no-ts-nocheck.js";
16import noTryCatchOrThrow from "./oxlint-plugin-executor/rules/no-try-catch-or-throw.js";
17import noUnknownErrorMessage from "./oxlint-plugin-executor/rules/no-unknown-error-message.js";
12import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js";18import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js";
13import noVitestImport from "./oxlint-plugin-executor/rules/no-vitest-import.js";19import noVitestImport from "./oxlint-plugin-executor/rules/no-vitest-import.js";
20import preferSchemaInferredTypes from "./oxlint-plugin-executor/rules/prefer-schema-inferred-types.js";
21import preferYieldTaggedError from "./oxlint-plugin-executor/rules/prefer-yield-tagged-error.js";
22import preferValueInferredExtensionTypes from "./oxlint-plugin-executor/rules/prefer-value-inferred-extension-types.js";
14import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js";23import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js";
15 24
16export default {25export default {
@@ -24,13 +33,22 @@ export default { · 7 unmodified lines
24 "no-cross-package-relative-imports": noCrossPackageRelativeImports,33 "no-cross-package-relative-imports": noCrossPackageRelativeImports,
25 "require-reactivity-keys": requireReactivityKeys,34 "require-reactivity-keys": requireReactivityKeys,
26 "no-effect-internal-tags": noEffectInternalTags,35 "no-effect-internal-tags": noEffectInternalTags,
36 "no-error-constructor": noErrorConstructor,
27 "no-ts-nocheck": noTsNocheck,37 "no-ts-nocheck": noTsNocheck,
28 "no-inline-object-type-assertion": noInlineObjectTypeAssertion,38 "no-inline-object-type-assertion": noInlineObjectTypeAssertion,
39 "no-instanceof-error": noInstanceofError,
29 "no-instanceof-tagged-error": noInstanceofTaggedError,40 "no-instanceof-tagged-error": noInstanceofTaggedError,
30 "no-manual-tag-check": noManualTagCheck,41 "no-manual-tag-check": noManualTagCheck,
42 "no-promise-catch": noPromiseCatch,
31 "no-promise-client-surface": noPromiseClientSurface,43 "no-promise-client-surface": noPromiseClientSurface,
44 "no-promise-reject": noPromiseReject,
32 "no-raw-error-throw": noRawErrorThrow,45 "no-raw-error-throw": noRawErrorThrow,
33 "no-redundant-error-factory": noRedundantErrorFactory,46 "no-redundant-error-factory": noRedundantErrorFactory,
47 "no-try-catch-or-throw": noTryCatchOrThrow,
48 "no-unknown-error-message": noUnknownErrorMessage,
34 "no-unknown-shape-probing": noUnknownShapeProbing,49 "no-unknown-shape-probing": noUnknownShapeProbing,
50 "prefer-schema-inferred-types": preferSchemaInferredTypes,
51 "prefer-value-inferred-extension-types": preferValueInferredExtensionTypes,
52 "prefer-yield-tagged-error": preferYieldTaggedError,
35 },53 },
36};54};
scripts/oxlint-plugin-executor/rules/no-conditional-tests.js+2−1
@@ -57,7 +57,8 @@ export default { · 56 unmodified lines
57 if (name !== "expect") return;57 if (name !== "expect") return;
58 context.report({58 context.report({
59 node,59 node,
60 message: "Avoid conditional expect calls; split the test or assert both branches explicitly.",60 message:
61 "Avoid conditional expect calls; split the test or assert both branches explicitly. Skill: wrdn-effect-vitest-tests.",
61 });62 });
62 },63 },
63 FunctionDeclaration: enterFunction,64 FunctionDeclaration: enterFunction,
scripts/oxlint-plugin-executor/rules/no-cross-package-relative-imports.js+1−1
@@ -23,7 +23,7 @@ export default { · 22 unmodified lines
23 23
24 context.report({24 context.report({
25 node: node.source,25 node: node.source,
26 message: `Import ${target.name} via its package export instead of a relative path.`,26 message: `Import ${target.name} via its package export instead of a relative path. Skill: wrdn-package-boundaries.`,
27 });27 });
28 },28 },
29 };29 };
scripts/oxlint-plugin-executor/rules/no-double-cast.js+1−1
@@ -19,7 +19,7 @@ export default { · 18 unmodified lines
19 context.report({19 context.report({
20 node,20 node,
21 message:21 message:
22 "Avoid double casts through unknown/any; use a typed boundary, schema decode, or a narrow allow comment with a reason.",22 "Avoid double casts through unknown/any; use a typed boundary, schema decode, or a narrow allow comment with a reason. Skill: wrdn-effect-schema-boundaries.",
23 });23 });
24 },24 },
25 };25 };
scripts/oxlint-plugin-executor/rules/no-effect-internal-tags.js+1−1
@@ -71,7 +71,7 @@ function reportIfEffectTagComparison( · 70 unmodified lines
71 71
72 context.report({72 context.report({
73 node: access,73 node: access,
74 message: `Use Effect's public helpers instead of checking internal _tag "${tag}".`,74 message: `Use Effect's public helpers instead of checking internal _tag "${tag}". Skill: wrdn-effect-typed-errors.`,
75 });75 });
76}76}
77 77
scripts/oxlint-plugin-executor/rules/no-error-constructor.js+40−0
@@ -0,0 +1,40 @@
1import { nodeName } from "../utils.js";
2
3const errorConstructors = new Set([
4 "AggregateError",
5 "Error",
6 "EvalError",
7 "RangeError",
8 "ReferenceError",
9 "SyntaxError",
10 "TypeError",
11 "URIError",
12]);
13
14const message =
15 "Do not construct built-in Error objects in Effect domain code. Use typed domain errors and Effect.fail instead; at true adapter boundaries use a narrow suppression with a boundary reason. Skill: wrdn-effect-typed-errors.";
16
17const isErrorConstructor = (node) => errorConstructors.has(nodeName(node));
18
19export default {
20 meta: {
21 type: "problem",
22 docs: {
23 description: "Disallow built-in Error constructors.",
24 },
25 },
26 create(context) {
27 return {
28 NewExpression(node) {
29 if (isErrorConstructor(node.callee)) {
30 context.report({ node, message });
31 }
32 },
33 CallExpression(node) {
34 if (isErrorConstructor(node.callee)) {
35 context.report({ node, message });
36 }
37 },
38 };
39 },
40};
scripts/oxlint-plugin-executor/rules/no-inline-object-type-assertion.js+1−1
@@ -1,7 +1,7 @@
1import { isIdentifier } from "../utils.js";1import { isIdentifier } from "../utils.js";
2 2
3const message =3const message =
4 "Do not assert against inline object-shaped types. Use a named type, Schema, or a proper type guard.";4 "Do not assert against inline object-shaped types. Use a named type, Schema, or a proper type guard. Skill: wrdn-effect-schema-boundaries.";
5 5
6const isUnknownKeyword = (node) => node?.type === "TSUnknownKeyword";6const isUnknownKeyword = (node) => node?.type === "TSUnknownKeyword";
7 7
scripts/oxlint-plugin-executor/rules/no-instanceof-error.js+22−0
@@ -0,0 +1,22 @@
1import { nodeName } from "../utils.js";
2
3const message =
4 "Do not use instanceof Error. Preserve typed failures with Effect tagged-error handling. Skill: wrdn-effect-typed-errors.";
5
6export default {
7 meta: {
8 type: "problem",
9 docs: {
10 description: "Disallow instanceof Error checks.",
11 },
12 },
13 create(context) {
14 return {
15 BinaryExpression(node) {
16 if (node.operator === "instanceof" && nodeName(node.right) === "Error") {
17 context.report({ node, message });
18 }
19 },
20 };
21 },
22};
scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js+1−1
@@ -1,7 +1,7 @@
1import { isIdentifier, nodeName } from "../utils.js";1import { isIdentifier, nodeName } from "../utils.js";
2 2
3const message =3const message =
4 "Do not use instanceof for tagged errors. Use Effect.catchTag, Effect.catchTags, or a _tag-based guard.";4 "Do not use instanceof for tagged errors. Use Effect.catchTag, Effect.catchTags, or a _tag-based guard. Skill: wrdn-effect-typed-errors.";
5 5
6const looksLikeTaggedErrorName = (name) =>6const looksLikeTaggedErrorName = (name) =>
7 typeof name === "string" && name !== "Error" && name.endsWith("Error");7 typeof name === "string" && name !== "Error" && name.endsWith("Error");
scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js+13−1
@@ -1,7 +1,7 @@
1import { isIdentifier, isStringLiteral } from "../utils.js";1import { isIdentifier, isStringLiteral } from "../utils.js";
2 2
3const message =3const message =
4 "Do not inspect _tag manually. Use Effect.catchTag, Effect.catchTags, Predicate.isTagged, or another Effect tagged-error API.";4 "Do not inspect _tag manually. Use Effect.catchTag/catchTags for error handling, Predicate.isTagged for guards, or public Effect helpers for Effect data. Skill: wrdn-effect-typed-errors.";
5 5
6const isTagProperty = (node) =>6const isTagProperty = (node) =>
7 isIdentifier(node, "_tag") || (isStringLiteral(node) && node.value === "_tag");7 isIdentifier(node, "_tag") || (isStringLiteral(node) && node.value === "_tag");
@@ -15,6 +15,16 @@ export default { · 7 unmodified lines
15 },15 },
16 create(context) {16 create(context) {
17 return {17 return {
18 BinaryExpression(node) {
19 if (node.operator === "in" && isTagProperty(node.left)) {
20 context.report({ node, message });
21 return;
22 }
23 if (!["===", "!==", "==", "!="].includes(node.operator)) return;
24 if (isTagAccess(node.left) || isTagAccess(node.right)) {
25 context.report({ node, message });
26 }
27 },
18 MemberExpression(node) {28 MemberExpression(node) {
19 if (isTagProperty(node.property)) {29 if (isTagProperty(node.property)) {
20 context.report({ node, message });30 context.report({ node, message });
@@ -23,3 +33,5 @@ export default { · 2 unmodified lines
23 };33 };
24 },34 },
25};35};
36
37const isTagAccess = (node) => node?.type === "MemberExpression" && isTagProperty(node.property);
scripts/oxlint-plugin-executor/rules/no-promise-catch.js+30−0
@@ -0,0 +1,30 @@
1import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js";
2
3const message =
4 "Do not use Promise .catch(). Model async failures with Effect.tryPromise and typed Effect error handling. Skill: wrdn-effect-typed-errors.";
5
6const isCatchMember = (node) => {
7 const expression = unwrapExpression(node);
8 if (isIdentifier(unwrapExpression(expression?.object), "Effect")) return false;
9 return (
10 expression?.type === "MemberExpression" && getPropertyName(expression.property) === "catch"
11 );
12};
13
14export default {
15 meta: {
16 type: "problem",
17 docs: {
18 description: "Disallow Promise-style .catch() error handling.",
19 },
20 },
21 create(context) {
22 return {
23 CallExpression(node) {
24 if (isCatchMember(node.callee)) {
25 context.report({ node, message });
26 }
27 },
28 };
29 },
30};
scripts/oxlint-plugin-executor/rules/no-promise-client-surface.js+1−1
@@ -1,7 +1,7 @@
1import { containsPromiseType, nodeName } from "../utils.js";1import { containsPromiseType, nodeName } from "../utils.js";
2 2
3const message =3const message =
4 "Do not expose Promise-shaped client surfaces. Wrap third-party SDK promises at the adapter boundary and expose Effect methods.";4 "Do not expose Promise-shaped client surfaces. Wrap third-party SDK promises at the adapter boundary and expose Effect methods. Skill: effect-client-wrapper.";
5 5
6const isExported = (node) => node?.parent?.type === "ExportNamedDeclaration";6const isExported = (node) => node?.parent?.type === "ExportNamedDeclaration";
7 7
scripts/oxlint-plugin-executor/rules/no-promise-reject.js+74−0
@@ -0,0 +1,74 @@
1import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js";
2
3const promiseRejectMessage =
4 "Do not use Promise.reject(). Model async failures with Effect.fail or Effect.tryPromise. Skill: wrdn-effect-typed-errors.";
5const rejectCallbackMessage =
6 "Do not call Promise executor reject(). Model async failures with Effect.fail or Effect.tryPromise. Skill: wrdn-effect-typed-errors.";
7
8const isPromiseReject = (node) => {
9 const expression = unwrapExpression(node);
10 return (
11 expression?.type === "MemberExpression" &&
12 isIdentifier(unwrapExpression(expression.object), "Promise") &&
13 getPropertyName(expression.property) === "reject"
14 );
15};
16
17const isPromiseConstructor = (node) =>
18 node?.type === "NewExpression" && isIdentifier(unwrapExpression(node.callee), "Promise");
19
20const isFunction = (node) =>
21 node?.type === "ArrowFunctionExpression" ||
22 node?.type === "FunctionExpression" ||
23 node?.type === "FunctionDeclaration";
24
25export default {
26 meta: {
27 type: "problem",
28 docs: {
29 description: "Disallow Promise rejection APIs.",
30 },
31 },
32 create(context) {
33 const promiseExecutors = new WeakSet();
34 const rejectNames = [];
35
36 const enterFunction = (node) => {
37 if (!promiseExecutors.has(node)) return;
38 const rejectParam = node.params?.[1];
39 if (isIdentifier(rejectParam)) {
40 rejectNames.push(rejectParam.name);
41 } else {
42 rejectNames.push(undefined);
43 }
44 };
45
46 const exitFunction = (node) => {
47 if (promiseExecutors.has(node)) rejectNames.pop();
48 };
49
50 return {
51 NewExpression(node) {
52 if (!isPromiseConstructor(node)) return;
53 const executor = node.arguments?.[0];
54 if (isFunction(executor)) promiseExecutors.add(executor);
55 },
56 CallExpression(node) {
57 if (isPromiseReject(node.callee)) {
58 context.report({ node, message: promiseRejectMessage });
59 return;
60 }
61
62 if (isIdentifier(node.callee) && rejectNames.includes(node.callee.name)) {
63 context.report({ node, message: rejectCallbackMessage });
64 }
65 },
66 FunctionDeclaration: enterFunction,
67 "FunctionDeclaration:exit": exitFunction,
68 FunctionExpression: enterFunction,
69 "FunctionExpression:exit": exitFunction,
70 ArrowFunctionExpression: enterFunction,
71 "ArrowFunctionExpression:exit": exitFunction,
72 };
73 },
74};
scripts/oxlint-plugin-executor/rules/no-raw-error-throw.js+1−1
@@ -1,7 +1,7 @@
1import { isIdentifier } from "../utils.js";1import { isIdentifier } from "../utils.js";
2 2
3const message =3const message =
4 "Do not throw raw Error objects in Effect code. Return Effect.fail with a tagged error or assert directly in tests.";4 "Do not throw raw Error objects in Effect code. Return Effect.fail with a tagged error or assert directly in tests. Skill: wrdn-effect-typed-errors.";
5 5
6const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error");6const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error");
7 7
scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js+46−8
@@ -1,27 +1,65 @@
1import { isIdentifier } from "../utils.js";1import { isIdentifier } from "../utils.js";
2 2
3const message =3const message =
4 "Do not add redundant make*Error wrappers that only construct a tagged error. Construct the tagged error directly.";4 "Do not add redundant helpers that only construct a tagged error. Construct the tagged error directly. Skill: wrdn-effect-typed-errors.";
5 5
6const isErrorFactoryName = (name) => /^make[A-Z].*Error$/.test(name);6const isErrorFactoryName = (name) => /^make[A-Z].*Error$/.test(name);
7 7
8const isErrorHelperName = (name) =>
9 isErrorFactoryName(name) || String(name ?? "").endsWith("Error");
10
11const parameterName = (param) => {
12 if (isIdentifier(param)) return param.name;
13 if (param?.type === "AssignmentPattern" && isIdentifier(param.left)) return param.left.name;
14 if (param?.type === "RestElement" && isIdentifier(param.argument)) return param.argument.name;
15 return undefined;
16};
17
8const isNewErrorExpression = (node) =>18const isNewErrorExpression = (node) =>
9 node?.type === "NewExpression" && isIdentifier(node.callee) && node.callee.name.endsWith("Error");19 node?.type === "NewExpression" && isIdentifier(node.callee) && node.callee.name.endsWith("Error");
10 20
21const isForwardedValue = (node, parameterNames) => {
22 if (node?.type === "Literal" || node?.type === "StringLiteral") return true;
23 if (node?.type === "Identifier") return parameterNames.has(node.name);
24 return (
25 node?.type === "MemberExpression" &&
26 isIdentifier(node.object) &&
27 parameterNames.has(node.object.name)
28 );
29};
30
31const isObjectWithOnlyForwardedFields = (node, parameterNames) => {
32 if (node?.type !== "ObjectExpression") return true;
33 return (node.properties ?? []).every((property) => {
34 if (property.type === "SpreadElement") return false;
35 return isForwardedValue(property.value, parameterNames);
36 });
37};
38
39const isRedundantNewErrorExpression = (node, parameterNames) => {
40 if (!isNewErrorExpression(node)) return false;
41 if ((node.arguments ?? []).length === 0) return true;
42 if (node.arguments.length > 1) return false;
43 const argument = node.arguments[0];
44 if (argument?.type === "Identifier") return parameterNames.has(argument.name);
45 return isObjectWithOnlyForwardedFields(argument, parameterNames);
46};
47
11const returnsOnlyNewError = (node) => {48const returnsOnlyNewError = (node) => {
12 if (isNewErrorExpression(node)) return true;49 const parameterNames = new Set((node?.params ?? []).map(parameterName).filter(Boolean));
50 if (isRedundantNewErrorExpression(node?.body ?? node, parameterNames)) return true;
13 if (node?.type !== "BlockStatement") return false;51 if (node?.type !== "BlockStatement") return false;
14 const statements = node.body ?? [];52 const statements = node.body ?? [];
15 return (53 return (
16 statements.length === 1 &&54 statements.length === 1 &&
17 statements[0]?.type === "ReturnStatement" &&55 statements[0]?.type === "ReturnStatement" &&
18 isNewErrorExpression(statements[0].argument)56 isRedundantNewErrorExpression(statements[0].argument, parameterNames)
19 );57 );
20};58};
21 59
22const reportIfRedundantFactory = (context, name, body, node) => {60const reportIfRedundantFactory = (context, name, fnNode, reportNode) => {
23 if (isErrorFactoryName(name) && returnsOnlyNewError(body)) {61 if (isErrorHelperName(name) && returnsOnlyNewError(fnNode)) {
24 context.report({ node, message });62 context.report({ node: reportNode, message });
25 }63 }
26};64};
27 65
@@ -35,7 +73,7 @@ export default { · 7 unmodified lines
35 create(context) {73 create(context) {
36 return {74 return {
37 FunctionDeclaration(node) {75 FunctionDeclaration(node) {
38 reportIfRedundantFactory(context, node.id?.name, node.body, node);76 reportIfRedundantFactory(context, node.id?.name, node, node);
39 },77 },
40 VariableDeclarator(node) {78 VariableDeclarator(node) {
41 if (!isIdentifier(node.id)) return;79 if (!isIdentifier(node.id)) return;
@@ -45,7 +83,7 @@ export default { · 3 unmodified lines
45 ) {83 ) {
46 return;84 return;
47 }85 }
48 reportIfRedundantFactory(context, node.id.name, node.init.body, node);86 reportIfRedundantFactory(context, node.id.name, node.init, node);
49 },87 },
50 };88 };
51 },89 },
scripts/oxlint-plugin-executor/rules/no-try-catch-or-throw.js+23−0
@@ -0,0 +1,23 @@
1const tryCatchMessage =
2 "Do not use try/catch blocks in Effect domain code. Model failures with Effect instead; at true adapter boundaries use a narrow suppression with a boundary reason. Skill: wrdn-effect-typed-errors; React useAtomSet mutation handlers use wrdn-effect-promise-exit.";
3const throwMessage =
4 "Do not throw errors in Effect domain code. Model failures with Effect.fail or typed error values instead; at true adapter boundaries use a narrow suppression with a boundary reason. Skill: wrdn-effect-typed-errors.";
5
6export default {
7 meta: {
8 type: "problem",
9 docs: {
10 description: "Disallow try/catch blocks and throw statements.",
11 },
12 },
13 create(context) {
14 return {
15 TryStatement(node) {
16 context.report({ node, message: tryCatchMessage });
17 },
18 ThrowStatement(node) {
19 context.report({ node, message: throwMessage });
20 },
21 };
22 },
23};
scripts/oxlint-plugin-executor/rules/no-ts-nocheck.js+1−1
@@ -18,7 +18,7 @@ export default { · 17 unmodified lines
18 18
19 context.report({19 context.report({
20 node,20 node,
21 message: `Do not use ${directiveName}; fix the types or narrow the file scope.`,21 message: `Do not use ${directiveName}; fix the types or narrow the file scope. Skill: wrdn-typescript-type-safety.`,
22 });22 });
23 },23 },
24 };24 };
scripts/oxlint-plugin-executor/rules/no-unknown-error-message.js+49−0
@@ -0,0 +1,49 @@
1import { getPropertyName, isIdentifier, nodeName, unwrapExpression } from "../utils.js";
2
3const stringMessage =
4 "Do not stringify unknown errors. Keep typed failures in Effect or normalize at a typed boundary. Skill: wrdn-effect-typed-errors.";
5const messagePropertyMessage =
6 "Do not read .message from unknown errors. Preserve typed failures with Effect tagged-error handling. Skill: wrdn-effect-typed-errors.";
7const destructuredMessage =
8 "Do not destructure .message from unknown errors. Preserve typed failures with Effect tagged-error handling. Skill: wrdn-effect-typed-errors.";
9
10const errorLikeNames = new Set(["cause", "e", "err", "error", "reason", "unknownError"]);
11
12const isErrorLikeIdentifier = (node) => {
13 const name = nodeName(unwrapExpression(node));
14 return errorLikeNames.has(name);
15};
16
17export default {
18 meta: {
19 type: "problem",
20 docs: {
21 description: "Disallow common unknown-error string and message normalization patterns.",
22 },
23 },
24 create(context) {
25 return {
26 CallExpression(node) {
27 if (!isIdentifier(unwrapExpression(node.callee), "String")) return;
28 if (node.arguments.some(isErrorLikeIdentifier)) {
29 context.report({ node, message: stringMessage });
30 }
31 },
32 MemberExpression(node) {
33 if (getPropertyName(node.property) !== "message") return;
34 if (isErrorLikeIdentifier(node.object)) {
35 context.report({ node, message: messagePropertyMessage });
36 }
37 },
38 VariableDeclarator(node) {
39 if (node.id?.type !== "ObjectPattern" || !isErrorLikeIdentifier(node.init)) return;
40 for (const property of node.id.properties ?? []) {
41 if (property.type !== "Property") continue;
42 if (getPropertyName(property.key) === "message") {
43 context.report({ node: property, message: destructuredMessage });
44 }
45 }
46 },
47 };
48 },
49};
scripts/oxlint-plugin-executor/rules/no-unknown-shape-probing.js+1−1
@@ -1,7 +1,7 @@
1import { isIdentifier, isStringLiteral } from "../utils.js";1import { isIdentifier, isStringLiteral } from "../utils.js";
2 2
3const message =3const message =
4 "Do not probe unknown object shapes in domain code. Normalize at a boundary with Schema, a typed adapter, or a named guard.";4 "Do not probe unknown object shapes in domain code. Normalize at a boundary with Schema, a typed adapter, or a named guard. Skill: wrdn-effect-schema-boundaries.";
5 5
6const isReflectGet = (node) =>6const isReflectGet = (node) =>
7 node?.type === "MemberExpression" &&7 node?.type === "MemberExpression" &&
scripts/oxlint-plugin-executor/rules/no-vitest-import.js+1−1
@@ -15,7 +15,7 @@ export default { · 14 unmodified lines
15 context.report({15 context.report({
16 node: node.source,16 node: node.source,
17 message:17 message:
18 "Import test helpers from @effect/vitest or @effect/vitest/utils instead of vitest.",18 "Import test helpers from @effect/vitest or @effect/vitest/utils instead of vitest. Skill: wrdn-effect-vitest-tests.",
19 });19 });
20 },20 },
21 };21 };
scripts/oxlint-plugin-executor/rules/prefer-schema-inferred-types.js+66−0
@@ -0,0 +1,66 @@
1import { getCallName, isIdentifier, typeReferenceName } from "../utils.js";
2
3const message =
4 "This object type duplicates a nearby Effect Schema. Export an inferred type from the schema instead. Skill: wrdn-effect-schema-inferred-types.";
5
6const schemaSuffixPattern = /(Schema|Model|Struct)$/;
7
8const schemaBaseName = (name) => {
9 const base = name.replace(schemaSuffixPattern, "");
10 return base.length > 0 && base !== name ? base : undefined;
11};
12
13const isSchemaMemberCall = (node) =>
14 node?.type === "CallExpression" &&
15 node.callee?.type === "MemberExpression" &&
16 isIdentifier(node.callee.object, "Schema");
17
18const isSchemaModelExpression = (node) => {
19 if (isSchemaMemberCall(node)) return true;
20 if (node?.type === "CallExpression" && getCallName(node.callee) === "pipe") {
21 return isSchemaModelExpression(node.callee.object);
22 }
23 return false;
24};
25
26const isObjectTypeAlias = (node) => node.typeAnnotation?.type === "TSTypeLiteral";
27
28const isInferredSchemaType = (node) => {
29 if (node.typeAnnotation?.type !== "TSTypeReference") return false;
30 const name = typeReferenceName(node.typeAnnotation);
31 return name === "Schema.Schema.Type";
32};
33
34export default {
35 meta: {
36 type: "problem",
37 docs: {
38 description: message,
39 },
40 },
41 create(context) {
42 const schemaBases = new Set();
43 const candidates = [];
44
45 return {
46 VariableDeclarator(node) {
47 if (!isIdentifier(node.id) || !isSchemaModelExpression(node.init)) return;
48 const base = schemaBaseName(node.id.name);
49 if (base) schemaBases.add(base);
50 },
51 TSInterfaceDeclaration(node) {
52 candidates.push({ node, name: node.id?.name });
53 },
54 TSTypeAliasDeclaration(node) {
55 if (!isObjectTypeAlias(node) || isInferredSchemaType(node)) return;
56 candidates.push({ node, name: node.id?.name });
57 },
58 "Program:exit"() {
59 for (const candidate of candidates) {
60 if (!candidate.name || !schemaBases.has(candidate.name)) continue;
61 context.report({ node: candidate.node, message });
62 }
63 },
64 };
65 },
66};
scripts/oxlint-plugin-executor/rules/prefer-value-inferred-extension-types.js+81−0
@@ -0,0 +1,81 @@
1import { isIdentifier } from "../utils.js";
2
3const message =
4 "Do not duplicate plugin extension object shapes. Derive the extension type from the extension factory return value. Skill: wrdn-effect-value-inferred-types.";
5
6const extensionNamePattern = /(?:Plugin)?Extension$/;
7
8const isExtensionTypeName = (name) => typeof name === "string" && extensionNamePattern.test(name);
9
10const isExtensionProperty = (node) =>
11 node?.type === "Property" &&
12 !node.computed &&
13 ((node.key?.type === "Identifier" && node.key.name === "extension") ||
14 ((node.key?.type === "Literal" || node.key?.type === "StringLiteral") &&
15 node.key.value === "extension"));
16
17const isSatisfiesExtension = (node, extensionTypeNames) =>
18 node?.type === "TSSatisfiesExpression" &&
19 node.typeAnnotation?.type === "TSTypeReference" &&
20 isIdentifier(node.typeAnnotation.typeName) &&
21 extensionTypeNames.has(node.typeAnnotation.typeName.name);
22
23const returnsSatisfiesExtension = (node, extensionTypeNames) => {
24 if (!node) return false;
25 if (isSatisfiesExtension(node, extensionTypeNames)) return true;
26 if (node.type === "BlockStatement") {
27 return (node.body ?? []).some(
28 (statement) =>
29 statement.type === "ReturnStatement" &&
30 isSatisfiesExtension(statement.argument, extensionTypeNames),
31 );
32 }
33 return false;
34};
35
36const isAnnotatedExtensionFunction = (node, extensionTypeNames) =>
37 (node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression") &&
38 node.returnType?.typeAnnotation?.type === "TSTypeReference" &&
39 isIdentifier(node.returnType.typeAnnotation.typeName) &&
40 extensionTypeNames.has(node.returnType.typeAnnotation.typeName.name);
41
42export default {
43 meta: {
44 type: "problem",
45 docs: {
46 description: message,
47 },
48 },
49 create(context) {
50 const extensionTypeNames = new Set();
51 const extensionProperties = [];
52
53 return {
54 TSInterfaceDeclaration(node) {
55 if (isExtensionTypeName(node.id?.name)) {
56 extensionTypeNames.add(node.id.name);
57 }
58 },
59 TSTypeAliasDeclaration(node) {
60 if (isExtensionTypeName(node.id?.name) && node.typeAnnotation?.type === "TSTypeLiteral") {
61 extensionTypeNames.add(node.id.name);
62 }
63 },
64 Property(node) {
65 if (!isExtensionProperty(node)) return;
66 extensionProperties.push(node);
67 },
68 "Program:exit"() {
69 for (const node of extensionProperties) {
70 const value = node.value;
71 if (
72 isAnnotatedExtensionFunction(value, extensionTypeNames) ||
73 returnsSatisfiesExtension(value?.body, extensionTypeNames)
74 ) {
75 context.report({ node, message });
76 }
77 }
78 },
79 };
80 },
81};
scripts/oxlint-plugin-executor/rules/prefer-yield-tagged-error.js+40−0
@@ -0,0 +1,40 @@
1import { getPropertyName, isIdentifier } from "../utils.js";
2
3const message =
4 "Yield tagged errors directly in Effect.gen instead of yielding Effect.fail(new ErrorType(...)). Skill: wrdn-effect-typed-errors.";
5
6const isEffectFail = (node) =>
7 node?.type === "MemberExpression" &&
8 isIdentifier(node.object, "Effect") &&
9 getPropertyName(node.property) === "fail";
10
11const isTaggedErrorConstruction = (node) =>
12 node?.type === "NewExpression" &&
13 isIdentifier(node.callee) &&
14 node.callee.name !== "Error" &&
15 node.callee.name.endsWith("Error");
16
17const isYieldedEffectFailOfTaggedError = (node) =>
18 node?.type === "YieldExpression" &&
19 node.delegate === true &&
20 node.argument?.type === "CallExpression" &&
21 isEffectFail(node.argument.callee) &&
22 isTaggedErrorConstruction(node.argument.arguments?.[0]);
23
24export default {
25 meta: {
26 type: "problem",
27 docs: {
28 description: message,
29 },
30 },
31 create(context) {
32 return {
33 YieldExpression(node) {
34 if (isYieldedEffectFailOfTaggedError(node)) {
35 context.report({ node, message });
36 }
37 },
38 };
39 },
40};
scripts/oxlint-plugin-executor/rules/require-reactivity-keys.js+1−1
@@ -56,7 +56,7 @@ export default { · 55 unmodified lines
56 56
57 context.report({57 context.report({
58 node: call,58 node: call,
59 message: `Mutation ${mutation.mutationName} must pass reactivityKeys at the call site.`,59 message: `Mutation ${mutation.mutationName} must pass reactivityKeys at the call site. Skill: wrdn-effect-atom-reactivity-keys.`,
60 });60 });
61 },61 },
62 };62 };