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 | import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js"; | 2 | import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js"; |
| 3 | import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js"; | 3 | import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js"; |
| 4 | import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js"; | 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 | import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js"; | 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 | import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js"; | 8 | import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js"; |
| 7 | import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check.js"; | 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 | import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js"; | 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 | import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js"; | 13 | import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js"; |
| 10 | import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js"; | 14 | import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js"; |
| 11 | import noTsNocheck from "./oxlint-plugin-executor/rules/no-ts-nocheck.js"; | 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 | import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js"; | 18 | import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js"; |
| 13 | import noVitestImport from "./oxlint-plugin-executor/rules/no-vitest-import.js"; | 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 | import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js"; | 23 | import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js"; |
| 15 | 24 | ||
| 16 | export default { | 25 | export 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 | }; |
| @@ -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, |
| @@ -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 | }; |
| @@ -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 | }; |
| @@ -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 | ||
| @@ -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 | import { isIdentifier } from "../utils.js"; | 1 | import { isIdentifier } from "../utils.js"; |
| 2 | 2 | ||
| 3 | const message = | 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 | const isUnknownKeyword = (node) => node?.type === "TSUnknownKeyword"; | 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 | import { isIdentifier, nodeName } from "../utils.js"; | 1 | import { isIdentifier, nodeName } from "../utils.js"; |
| 2 | 2 | ||
| 3 | const message = | 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 | const looksLikeTaggedErrorName = (name) => | 6 | const looksLikeTaggedErrorName = (name) => |
| 7 | typeof name === "string" && name !== "Error" && name.endsWith("Error"); | 7 | typeof name === "string" && name !== "Error" && name.endsWith("Error"); |
| @@ -1,7 +1,7 @@ | |||
| 1 | import { isIdentifier, isStringLiteral } from "../utils.js"; | 1 | import { isIdentifier, isStringLiteral } from "../utils.js"; |
| 2 | 2 | ||
| 3 | const message = | 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 | const isTagProperty = (node) => | 6 | const 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 | |||
| 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 | import { containsPromiseType, nodeName } from "../utils.js"; | 1 | import { containsPromiseType, nodeName } from "../utils.js"; |
| 2 | 2 | ||
| 3 | const message = | 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 | const isExported = (node) => node?.parent?.type === "ExportNamedDeclaration"; | 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 | import { isIdentifier } from "../utils.js"; | 1 | import { isIdentifier } from "../utils.js"; |
| 2 | 2 | ||
| 3 | const message = | 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 | const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error"); | 6 | const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error"); |
| 7 | 7 | ||
| @@ -1,27 +1,65 @@ | |||
| 1 | import { isIdentifier } from "../utils.js"; | 1 | import { isIdentifier } from "../utils.js"; |
| 2 | 2 | ||
| 3 | const message = | 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 | const isErrorFactoryName = (name) => /^make[A-Z].*Error$/.test(name); | 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 | const isNewErrorExpression = (node) => | 18 | const 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 | ||
| 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 | const returnsOnlyNewError = (node) => { | 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 | 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 | ||
| 22 | const reportIfRedundantFactory = (context, name, body, node) => { | 60 | const 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 | }, |
| @@ -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 | 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 | }; |
| @@ -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 | import { isIdentifier, isStringLiteral } from "../utils.js"; | 1 | import { isIdentifier, isStringLiteral } from "../utils.js"; |
| 2 | 2 | ||
| 3 | const message = | 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 | const isReflectGet = (node) => | 6 | const isReflectGet = (node) => |
| 7 | node?.type === "MemberExpression" && | 7 | node?.type === "MemberExpression" && |
| @@ -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 | }; |
| @@ -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 | 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 | }; |