Showing the changes to scripts (24 of 34 files). Show all
Unified · Split
| @@ -2,15 +2,24 @@ import noConditionalTests from "./oxlint-plugin-executor/rules/no-conditional-te · 1 unmodified line | ||
| 2 | 2 | import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js"; |
| 3 | 3 | import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js"; |
| 4 | 4 | import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js"; |
| 5 | +import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js"; | |
| 5 | 6 | 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"; | |
| 6 | 8 | import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js"; |
| 7 | 9 | import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check.js"; |
| 10 | +import noPromiseCatch from "./oxlint-plugin-executor/rules/no-promise-catch.js"; | |
| 8 | 11 | import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js"; |
| 12 | +import noPromiseReject from "./oxlint-plugin-executor/rules/no-promise-reject.js"; | |
| 9 | 13 | import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js"; |
| 10 | 14 | import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js"; |
| 11 | 15 | 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"; | |
| 12 | 18 | import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js"; |
| 13 | 19 | 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"; | |
| 14 | 23 | import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js"; |
| 15 | 24 | |
| 16 | 25 | export default { |
| @@ -24,13 +33,22 @@ export default { · 7 unmodified lines | ||
| 24 | 33 | "no-cross-package-relative-imports": noCrossPackageRelativeImports, |
| 25 | 34 | "require-reactivity-keys": requireReactivityKeys, |
| 26 | 35 | "no-effect-internal-tags": noEffectInternalTags, |
| 36 | + "no-error-constructor": noErrorConstructor, | |
| 27 | 37 | "no-ts-nocheck": noTsNocheck, |
| 28 | 38 | "no-inline-object-type-assertion": noInlineObjectTypeAssertion, |
| 39 | + "no-instanceof-error": noInstanceofError, | |
| 29 | 40 | "no-instanceof-tagged-error": noInstanceofTaggedError, |
| 30 | 41 | "no-manual-tag-check": noManualTagCheck, |
| 42 | + "no-promise-catch": noPromiseCatch, | |
| 31 | 43 | "no-promise-client-surface": noPromiseClientSurface, |
| 44 | + "no-promise-reject": noPromiseReject, | |
| 32 | 45 | "no-raw-error-throw": noRawErrorThrow, |
| 33 | 46 | "no-redundant-error-factory": noRedundantErrorFactory, |
| 47 | + "no-try-catch-or-throw": noTryCatchOrThrow, | |
| 48 | + "no-unknown-error-message": noUnknownErrorMessage, | |
| 34 | 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 | }; |
| @@ -57,7 +57,8 @@ export default { · 56 unmodified lines | ||
| 57 | 57 | if (name !== "expect") return; |
| 58 | 58 | context.report({ |
| 59 | 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 | 64 | FunctionDeclaration: enterFunction, |
| @@ -23,7 +23,7 @@ export default { · 22 unmodified lines | ||
| 23 | 23 | |
| 24 | 24 | context.report({ |
| 25 | 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 | }; |
| @@ -19,7 +19,7 @@ export default { · 18 unmodified lines | ||
| 19 | 19 | context.report({ |
| 20 | 20 | node, |
| 21 | 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 | }; |
| @@ -71,7 +71,7 @@ function reportIfEffectTagComparison( · 70 unmodified lines | ||
| 71 | 71 | |
| 72 | 72 | context.report({ |
| 73 | 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 | |
| @@ -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 | +}; | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { isIdentifier } from "../utils.js"; |
| 2 | 2 | |
| 3 | 3 | 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."; | |
| 5 | 5 | |
| 6 | 6 | const isUnknownKeyword = (node) => node?.type === "TSUnknownKeyword"; |
| 7 | 7 | |
| @@ -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 | +}; | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { isIdentifier, nodeName } from "../utils.js"; |
| 2 | 2 | |
| 3 | 3 | 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."; | |
| 5 | 5 | |
| 6 | 6 | const looksLikeTaggedErrorName = (name) => |
| 7 | 7 | typeof name === "string" && name !== "Error" && name.endsWith("Error"); |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { isIdentifier, isStringLiteral } from "../utils.js"; |
| 2 | 2 | |
| 3 | 3 | 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."; | |
| 5 | 5 | |
| 6 | 6 | const isTagProperty = (node) => |
| 7 | 7 | isIdentifier(node, "_tag") || (isStringLiteral(node) && node.value === "_tag"); |
| @@ -15,6 +15,16 @@ export default { · 7 unmodified lines | ||
| 15 | 15 | }, |
| 16 | 16 | create(context) { |
| 17 | 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 | 28 | MemberExpression(node) { |
| 19 | 29 | if (isTagProperty(node.property)) { |
| 20 | 30 | context.report({ node, message }); |
| @@ -23,3 +33,5 @@ export default { · 2 unmodified lines | ||
| 23 | 33 | }; |
| 24 | 34 | }, |
| 25 | 35 | }; |
| 36 | + | |
| 37 | +const isTagAccess = (node) => node?.type === "MemberExpression" && isTagProperty(node.property); | |
| @@ -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 | +}; | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { containsPromiseType, nodeName } from "../utils.js"; |
| 2 | 2 | |
| 3 | 3 | 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."; | |
| 5 | 5 | |
| 6 | 6 | const isExported = (node) => node?.parent?.type === "ExportNamedDeclaration"; |
| 7 | 7 | |
| @@ -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 | +}; | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { isIdentifier } from "../utils.js"; |
| 2 | 2 | |
| 3 | 3 | 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."; | |
| 5 | 5 | |
| 6 | 6 | const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error"); |
| 7 | 7 | |
| @@ -1,27 +1,65 @@ | ||
| 1 | 1 | import { isIdentifier } from "../utils.js"; |
| 2 | 2 | |
| 3 | 3 | 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."; | |
| 5 | 5 | |
| 6 | 6 | const isErrorFactoryName = (name) => /^make[A-Z].*Error$/.test(name); |
| 7 | 7 | |
| 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 | + | |
| 8 | 18 | const isNewErrorExpression = (node) => |
| 9 | 19 | node?.type === "NewExpression" && isIdentifier(node.callee) && node.callee.name.endsWith("Error"); |
| 10 | 20 | |
| 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 | + | |
| 11 | 48 | 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; | |
| 13 | 51 | if (node?.type !== "BlockStatement") return false; |
| 14 | 52 | const statements = node.body ?? []; |
| 15 | 53 | return ( |
| 16 | 54 | statements.length === 1 && |
| 17 | 55 | statements[0]?.type === "ReturnStatement" && |
| 18 | − isNewErrorExpression(statements[0].argument) | |
| 56 | + isRedundantNewErrorExpression(statements[0].argument, parameterNames) | |
| 19 | 57 | ); |
| 20 | 58 | }; |
| 21 | 59 | |
| 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 }); | |
| 25 | 63 | } |
| 26 | 64 | }; |
| 27 | 65 | |
| @@ -35,7 +73,7 @@ export default { · 7 unmodified lines | ||
| 35 | 73 | create(context) { |
| 36 | 74 | return { |
| 37 | 75 | FunctionDeclaration(node) { |
| 38 | − reportIfRedundantFactory(context, node.id?.name, node.body, node); | |
| 76 | + reportIfRedundantFactory(context, node.id?.name, node, node); | |
| 39 | 77 | }, |
| 40 | 78 | VariableDeclarator(node) { |
| 41 | 79 | if (!isIdentifier(node.id)) return; |
| @@ -45,7 +83,7 @@ export default { · 3 unmodified lines | ||
| 45 | 83 | ) { |
| 46 | 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 | }, |
| @@ -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 | +}; | |
| @@ -18,7 +18,7 @@ export default { · 17 unmodified lines | ||
| 18 | 18 | |
| 19 | 19 | context.report({ |
| 20 | 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 | }; |
| @@ -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 | +}; | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { isIdentifier, isStringLiteral } from "../utils.js"; |
| 2 | 2 | |
| 3 | 3 | 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."; | |
| 5 | 5 | |
| 6 | 6 | const isReflectGet = (node) => |
| 7 | 7 | node?.type === "MemberExpression" && |
| @@ -15,7 +15,7 @@ export default { · 14 unmodified lines | ||
| 15 | 15 | context.report({ |
| 16 | 16 | node: node.source, |
| 17 | 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 | }; |
| @@ -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 | +}; | |
| @@ -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 | +}; | |
| @@ -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 | +}; | |
| @@ -56,7 +56,7 @@ export default { · 55 unmodified lines | ||
| 56 | 56 | |
| 57 | 57 | context.report({ |
| 58 | 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 | }; |