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
22 import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";
33 import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";
44 import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";
5+import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js";
56 import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";
7+import noInstanceofError from "./oxlint-plugin-executor/rules/no-instanceof-error.js";
68 import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js";
79 import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check.js";
10+import noPromiseCatch from "./oxlint-plugin-executor/rules/no-promise-catch.js";
811 import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js";
12+import noPromiseReject from "./oxlint-plugin-executor/rules/no-promise-reject.js";
913 import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js";
1014 import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js";
1115 import noTsNocheck from "./oxlint-plugin-executor/rules/no-ts-nocheck.js";
16+import noTryCatchOrThrow from "./oxlint-plugin-executor/rules/no-try-catch-or-throw.js";
17+import noUnknownErrorMessage from "./oxlint-plugin-executor/rules/no-unknown-error-message.js";
1218 import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js";
1319 import noVitestImport from "./oxlint-plugin-executor/rules/no-vitest-import.js";
20+import preferSchemaInferredTypes from "./oxlint-plugin-executor/rules/prefer-schema-inferred-types.js";
21+import preferYieldTaggedError from "./oxlint-plugin-executor/rules/prefer-yield-tagged-error.js";
22+import preferValueInferredExtensionTypes from "./oxlint-plugin-executor/rules/prefer-value-inferred-extension-types.js";
1423 import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js";
1524
1625 export default {
@@ -24,13 +33,22 @@ export default { · 7 unmodified lines
2433 "no-cross-package-relative-imports": noCrossPackageRelativeImports,
2534 "require-reactivity-keys": requireReactivityKeys,
2635 "no-effect-internal-tags": noEffectInternalTags,
36+ "no-error-constructor": noErrorConstructor,
2737 "no-ts-nocheck": noTsNocheck,
2838 "no-inline-object-type-assertion": noInlineObjectTypeAssertion,
39+ "no-instanceof-error": noInstanceofError,
2940 "no-instanceof-tagged-error": noInstanceofTaggedError,
3041 "no-manual-tag-check": noManualTagCheck,
42+ "no-promise-catch": noPromiseCatch,
3143 "no-promise-client-surface": noPromiseClientSurface,
44+ "no-promise-reject": noPromiseReject,
3245 "no-raw-error-throw": noRawErrorThrow,
3346 "no-redundant-error-factory": noRedundantErrorFactory,
47+ "no-try-catch-or-throw": noTryCatchOrThrow,
48+ "no-unknown-error-message": noUnknownErrorMessage,
3449 "no-unknown-shape-probing": noUnknownShapeProbing,
50+ "prefer-schema-inferred-types": preferSchemaInferredTypes,
51+ "prefer-value-inferred-extension-types": preferValueInferredExtensionTypes,
52+ "prefer-yield-tagged-error": preferYieldTaggedError,
3553 },
3654 };
scripts/oxlint-plugin-executor/rules/no-conditional-tests.js+2−1
@@ -57,7 +57,8 @@ export default { · 56 unmodified lines
5757 if (name !== "expect") return;
5858 context.report({
5959 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.",
6162 });
6263 },
6364 FunctionDeclaration: enterFunction,
scripts/oxlint-plugin-executor/rules/no-cross-package-relative-imports.js+1−1
@@ -23,7 +23,7 @@ export default { · 22 unmodified lines
2323
2424 context.report({
2525 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.`,
2727 });
2828 },
2929 };
scripts/oxlint-plugin-executor/rules/no-double-cast.js+1−1
@@ -19,7 +19,7 @@ export default { · 18 unmodified lines
1919 context.report({
2020 node,
2121 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.",
2323 });
2424 },
2525 };
scripts/oxlint-plugin-executor/rules/no-effect-internal-tags.js+1−1
@@ -71,7 +71,7 @@ function reportIfEffectTagComparison( · 70 unmodified lines
7171
7272 context.report({
7373 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.`,
7575 });
7676 }
7777
scripts/oxlint-plugin-executor/rules/no-error-constructor.js+40−0
@@ -0,0 +1,40 @@
1+import { nodeName } from "../utils.js";
2+
3+const errorConstructors = new Set([
4+ "AggregateError",
5+ "Error",
6+ "EvalError",
7+ "RangeError",
8+ "ReferenceError",
9+ "SyntaxError",
10+ "TypeError",
11+ "URIError",
12+]);
13+
14+const 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+
17+const isErrorConstructor = (node) => errorConstructors.has(nodeName(node));
18+
19+export 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 @@
11 import { isIdentifier } from "../utils.js";
22
33 const 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.";
55
66 const isUnknownKeyword = (node) => node?.type === "TSUnknownKeyword";
77
scripts/oxlint-plugin-executor/rules/no-instanceof-error.js+22−0
@@ -0,0 +1,22 @@
1+import { nodeName } from "../utils.js";
2+
3+const message =
4+ "Do not use instanceof Error. Preserve typed failures with Effect tagged-error handling. Skill: wrdn-effect-typed-errors.";
5+
6+export 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 @@
11 import { isIdentifier, nodeName } from "../utils.js";
22
33 const 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.";
55
66 const looksLikeTaggedErrorName = (name) =>
77 typeof name === "string" && name !== "Error" && name.endsWith("Error");
scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js+13−1
@@ -1,7 +1,7 @@
11 import { isIdentifier, isStringLiteral } from "../utils.js";
22
33 const 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.";
55
66 const isTagProperty = (node) =>
77 isIdentifier(node, "_tag") || (isStringLiteral(node) && node.value === "_tag");
@@ -15,6 +15,16 @@ export default { · 7 unmodified lines
1515 },
1616 create(context) {
1717 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+ },
1828 MemberExpression(node) {
1929 if (isTagProperty(node.property)) {
2030 context.report({ node, message });
@@ -23,3 +33,5 @@ export default { · 2 unmodified lines
2333 };
2434 },
2535 };
36+
37+const isTagAccess = (node) => node?.type === "MemberExpression" && isTagProperty(node.property);
scripts/oxlint-plugin-executor/rules/no-promise-catch.js+30−0
@@ -0,0 +1,30 @@
1+import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js";
2+
3+const message =
4+ "Do not use Promise .catch(). Model async failures with Effect.tryPromise and typed Effect error handling. Skill: wrdn-effect-typed-errors.";
5+
6+const 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+
14+export 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 @@
11 import { containsPromiseType, nodeName } from "../utils.js";
22
33 const 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.";
55
66 const isExported = (node) => node?.parent?.type === "ExportNamedDeclaration";
77
scripts/oxlint-plugin-executor/rules/no-promise-reject.js+74−0
@@ -0,0 +1,74 @@
1+import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js";
2+
3+const promiseRejectMessage =
4+ "Do not use Promise.reject(). Model async failures with Effect.fail or Effect.tryPromise. Skill: wrdn-effect-typed-errors.";
5+const rejectCallbackMessage =
6+ "Do not call Promise executor reject(). Model async failures with Effect.fail or Effect.tryPromise. Skill: wrdn-effect-typed-errors.";
7+
8+const 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+
17+const isPromiseConstructor = (node) =>
18+ node?.type === "NewExpression" && isIdentifier(unwrapExpression(node.callee), "Promise");
19+
20+const isFunction = (node) =>
21+ node?.type === "ArrowFunctionExpression" ||
22+ node?.type === "FunctionExpression" ||
23+ node?.type === "FunctionDeclaration";
24+
25+export 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 @@
11 import { isIdentifier } from "../utils.js";
22
33 const 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.";
55
66 const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error");
77
scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js+46−8
@@ -1,27 +1,65 @@
11 import { isIdentifier } from "../utils.js";
22
33 const 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.";
55
66 const isErrorFactoryName = (name) => /^make[A-Z].*Error$/.test(name);
77
8+const isErrorHelperName = (name) =>
9+ isErrorFactoryName(name) || String(name ?? "").endsWith("Error");
10+
11+const 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+
818 const isNewErrorExpression = (node) =>
919 node?.type === "NewExpression" && isIdentifier(node.callee) && node.callee.name.endsWith("Error");
1020
21+const 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+
31+const 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+
39+const 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+
1148 const 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;
1351 if (node?.type !== "BlockStatement") return false;
1452 const statements = node.body ?? [];
1553 return (
1654 statements.length === 1 &&
1755 statements[0]?.type === "ReturnStatement" &&
18− isNewErrorExpression(statements[0].argument)
56+ isRedundantNewErrorExpression(statements[0].argument, parameterNames)
1957 );
2058 };
2159
22−const reportIfRedundantFactory = (context, name, body, node) => {
23− if (isErrorFactoryName(name) && returnsOnlyNewError(body)) {
24− context.report({ node, message });
60+const reportIfRedundantFactory = (context, name, fnNode, reportNode) => {
61+ if (isErrorHelperName(name) && returnsOnlyNewError(fnNode)) {
62+ context.report({ node: reportNode, message });
2563 }
2664 };
2765
@@ -35,7 +73,7 @@ export default { · 7 unmodified lines
3573 create(context) {
3674 return {
3775 FunctionDeclaration(node) {
38− reportIfRedundantFactory(context, node.id?.name, node.body, node);
76+ reportIfRedundantFactory(context, node.id?.name, node, node);
3977 },
4078 VariableDeclarator(node) {
4179 if (!isIdentifier(node.id)) return;
@@ -45,7 +83,7 @@ export default { · 3 unmodified lines
4583 ) {
4684 return;
4785 }
48− reportIfRedundantFactory(context, node.id.name, node.init.body, node);
86+ reportIfRedundantFactory(context, node.id.name, node.init, node);
4987 },
5088 };
5189 },
scripts/oxlint-plugin-executor/rules/no-try-catch-or-throw.js+23−0
@@ -0,0 +1,23 @@
1+const 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.";
3+const 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+
6+export 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
1818
1919 context.report({
2020 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.`,
2222 });
2323 },
2424 };
scripts/oxlint-plugin-executor/rules/no-unknown-error-message.js+49−0
@@ -0,0 +1,49 @@
1+import { getPropertyName, isIdentifier, nodeName, unwrapExpression } from "../utils.js";
2+
3+const stringMessage =
4+ "Do not stringify unknown errors. Keep typed failures in Effect or normalize at a typed boundary. Skill: wrdn-effect-typed-errors.";
5+const messagePropertyMessage =
6+ "Do not read .message from unknown errors. Preserve typed failures with Effect tagged-error handling. Skill: wrdn-effect-typed-errors.";
7+const destructuredMessage =
8+ "Do not destructure .message from unknown errors. Preserve typed failures with Effect tagged-error handling. Skill: wrdn-effect-typed-errors.";
9+
10+const errorLikeNames = new Set(["cause", "e", "err", "error", "reason", "unknownError"]);
11+
12+const isErrorLikeIdentifier = (node) => {
13+ const name = nodeName(unwrapExpression(node));
14+ return errorLikeNames.has(name);
15+};
16+
17+export 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 @@
11 import { isIdentifier, isStringLiteral } from "../utils.js";
22
33 const 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.";
55
66 const isReflectGet = (node) =>
77 node?.type === "MemberExpression" &&
scripts/oxlint-plugin-executor/rules/no-vitest-import.js+1−1
@@ -15,7 +15,7 @@ export default { · 14 unmodified lines
1515 context.report({
1616 node: node.source,
1717 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.",
1919 });
2020 },
2121 };
scripts/oxlint-plugin-executor/rules/prefer-schema-inferred-types.js+66−0
@@ -0,0 +1,66 @@
1+import { getCallName, isIdentifier, typeReferenceName } from "../utils.js";
2+
3+const 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+
6+const schemaSuffixPattern = /(Schema|Model|Struct)$/;
7+
8+const schemaBaseName = (name) => {
9+ const base = name.replace(schemaSuffixPattern, "");
10+ return base.length > 0 && base !== name ? base : undefined;
11+};
12+
13+const isSchemaMemberCall = (node) =>
14+ node?.type === "CallExpression" &&
15+ node.callee?.type === "MemberExpression" &&
16+ isIdentifier(node.callee.object, "Schema");
17+
18+const 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+
26+const isObjectTypeAlias = (node) => node.typeAnnotation?.type === "TSTypeLiteral";
27+
28+const isInferredSchemaType = (node) => {
29+ if (node.typeAnnotation?.type !== "TSTypeReference") return false;
30+ const name = typeReferenceName(node.typeAnnotation);
31+ return name === "Schema.Schema.Type";
32+};
33+
34+export 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 @@
1+import { isIdentifier } from "../utils.js";
2+
3+const 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+
6+const extensionNamePattern = /(?:Plugin)?Extension$/;
7+
8+const isExtensionTypeName = (name) => typeof name === "string" && extensionNamePattern.test(name);
9+
10+const 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+
17+const 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+
23+const 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+
36+const 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+
42+export 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 @@
1+import { getPropertyName, isIdentifier } from "../utils.js";
2+
3+const message =
4+ "Yield tagged errors directly in Effect.gen instead of yielding Effect.fail(new ErrorType(...)). Skill: wrdn-effect-typed-errors.";
5+
6+const isEffectFail = (node) =>
7+ node?.type === "MemberExpression" &&
8+ isIdentifier(node.object, "Effect") &&
9+ getPropertyName(node.property) === "fail";
10+
11+const isTaggedErrorConstruction = (node) =>
12+ node?.type === "NewExpression" &&
13+ isIdentifier(node.callee) &&
14+ node.callee.name !== "Error" &&
15+ node.callee.name.endsWith("Error");
16+
17+const 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+
24+export 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
5656
5757 context.report({
5858 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.`,
6060 });
6161 },
6262 };