Schema.is, Schema.decode*, Schema.encode*, Schema.validate*, and Schema.parse* each compile a parser/guard from a schema. Calling them inside a function body rebuilds that function on every invocation. Adds executor/no-inline-schema-compile (oxlint plugin rule) that flags these calls when they appear inside a function body, with two severities: HIGH when the schema argument is itself an inline Schema.X(...) literal (both AST and compile rebuilt), MEDIUM when only the compiler is re-called. Fixes the 44 existing violations across 22 files by hoisting the compiled function (and where applicable, its schema literal) to module scope. Notable consolidations: - oauth-service.ts: 8 sites (5 decode + 3 encode of OAuthProviderState) collapse into two hoisted helpers - jwks-cache.ts, mcp-auth.ts: per-call guard compilation removed from JWT verify error path - React Add*Source / EditOpenApiSource components: shared decodeErrorMessage / isOpenApiSourceBindingValue hoisted migrate-connections.ts has one targeted oxlint-disable for a higher-order helper whose schema is a function parameter; the compile is hoisted into the outer closure but the rule (correctly) flags any Schema compiler call inside a function body.
Showing the changes to scripts (2 of 25 files). Show all
Unified · Split
| @@ -5,6 +5,7 @@ import noEffectEscapeHatch from "./oxlint-plugin-executor/rules/no-effect-escape · 4 unmodified lines | |||
| 5 | import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js"; | 5 | import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js"; |
| 6 | import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js"; | 6 | import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js"; |
| 7 | import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js"; | 7 | import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js"; |
| 8 | import noInlineSchemaCompile from "./oxlint-plugin-executor/rules/no-inline-schema-compile.js"; | ||
| 8 | import noInstanceofError from "./oxlint-plugin-executor/rules/no-instanceof-error.js"; | 9 | import noInstanceofError from "./oxlint-plugin-executor/rules/no-instanceof-error.js"; |
| 9 | import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js"; | 10 | import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js"; |
| 10 | import noJsonParse from "./oxlint-plugin-executor/rules/no-json-parse.js"; | 11 | import noJsonParse from "./oxlint-plugin-executor/rules/no-json-parse.js"; |
| @@ -42,6 +43,7 @@ export default { · 31 unmodified lines | |||
| 42 | "no-error-constructor": noErrorConstructor, | 43 | "no-error-constructor": noErrorConstructor, |
| 43 | "no-ts-nocheck": noTsNocheck, | 44 | "no-ts-nocheck": noTsNocheck, |
| 44 | "no-inline-object-type-assertion": noInlineObjectTypeAssertion, | 45 | "no-inline-object-type-assertion": noInlineObjectTypeAssertion, |
| 46 | "no-inline-schema-compile": noInlineSchemaCompile, | ||
| 45 | "no-instanceof-error": noInstanceofError, | 47 | "no-instanceof-error": noInstanceofError, |
| 46 | "no-instanceof-tagged-error": noInstanceofTaggedError, | 48 | "no-instanceof-tagged-error": noInstanceofTaggedError, |
| 47 | "no-json-parse": noJsonParse, | 49 | "no-json-parse": noJsonParse, |
| @@ -0,0 +1,104 @@ | |||
| 1 | import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js"; | ||
| 2 | |||
| 3 | // Schema methods that compile a parser/guard from a schema. Calling these | ||
| 4 | // allocates a new function per invocation; the result should be hoisted to | ||
| 5 | // module (or at least closure) scope. | ||
| 6 | const COMPILER_METHODS = new Set([ | ||
| 7 | "is", | ||
| 8 | "asserts", | ||
| 9 | "decode", | ||
| 10 | "decodeSync", | ||
| 11 | "decodePromise", | ||
| 12 | "decodeOption", | ||
| 13 | "decodeEither", | ||
| 14 | "decodeUnknown", | ||
| 15 | "decodeUnknownSync", | ||
| 16 | "decodeUnknownPromise", | ||
| 17 | "decodeUnknownOption", | ||
| 18 | "decodeUnknownEither", | ||
| 19 | "encode", | ||
| 20 | "encodeSync", | ||
| 21 | "encodePromise", | ||
| 22 | "encodeOption", | ||
| 23 | "encodeEither", | ||
| 24 | "encodeUnknown", | ||
| 25 | "encodeUnknownSync", | ||
| 26 | "encodeUnknownPromise", | ||
| 27 | "encodeUnknownOption", | ||
| 28 | "encodeUnknownEither", | ||
| 29 | "validate", | ||
| 30 | "validateSync", | ||
| 31 | "validatePromise", | ||
| 32 | "validateOption", | ||
| 33 | "validateEither", | ||
| 34 | "parse", | ||
| 35 | "parseSync", | ||
| 36 | "parsePromise", | ||
| 37 | "parseOption", | ||
| 38 | "parseEither", | ||
| 39 | ]); | ||
| 40 | |||
| 41 | const getSchemaCompilerMethod = (callee) => { | ||
| 42 | const expression = unwrapExpression(callee); | ||
| 43 | if (expression?.type !== "MemberExpression") return undefined; | ||
| 44 | const object = unwrapExpression(expression.object); | ||
| 45 | if (!isIdentifier(object, "Schema")) return undefined; | ||
| 46 | const method = getPropertyName(expression.property); | ||
| 47 | return method && COMPILER_METHODS.has(method) ? method : undefined; | ||
| 48 | }; | ||
| 49 | |||
| 50 | const isNestedSchemaCall = (node) => { | ||
| 51 | const expression = unwrapExpression(node); | ||
| 52 | if (expression?.type !== "CallExpression") return false; | ||
| 53 | const callee = unwrapExpression(expression.callee); | ||
| 54 | if (callee?.type !== "MemberExpression") return false; | ||
| 55 | const object = unwrapExpression(callee.object); | ||
| 56 | return isIdentifier(object, "Schema"); | ||
| 57 | }; | ||
| 58 | |||
| 59 | const messageHigh = (method) => | ||
| 60 | `Hoist Schema.${method}(...) to module scope: both the inline schema literal and the compiled function are rebuilt on every call. Move the compiled function to a module-level const.`; | ||
| 61 | |||
| 62 | const messageMedium = (method) => | ||
| 63 | `Hoist Schema.${method}(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.`; | ||
| 64 | |||
| 65 | export default { | ||
| 66 | meta: { | ||
| 67 | type: "problem", | ||
| 68 | docs: { | ||
| 69 | description: | ||
| 70 | "Disallow Schema compiler calls (Schema.is/decode*/encode*/validate*/parse*) inside function bodies; hoist them to module scope.", | ||
| 71 | }, | ||
| 72 | }, | ||
| 73 | create(context) { | ||
| 74 | let functionDepth = 0; | ||
| 75 | |||
| 76 | const enterFunction = () => { | ||
| 77 | functionDepth++; | ||
| 78 | }; | ||
| 79 | const exitFunction = () => { | ||
| 80 | functionDepth--; | ||
| 81 | }; | ||
| 82 | |||
| 83 | return { | ||
| 84 | FunctionDeclaration: enterFunction, | ||
| 85 | "FunctionDeclaration:exit": exitFunction, | ||
| 86 | FunctionExpression: enterFunction, | ||
| 87 | "FunctionExpression:exit": exitFunction, | ||
| 88 | ArrowFunctionExpression: enterFunction, | ||
| 89 | "ArrowFunctionExpression:exit": exitFunction, | ||
| 90 | |||
| 91 | CallExpression(node) { | ||
| 92 | if (functionDepth === 0) return; | ||
| 93 | const method = getSchemaCompilerMethod(node.callee); | ||
| 94 | if (!method) return; | ||
| 95 | const firstArg = node.arguments[0]; | ||
| 96 | const high = firstArg && isNestedSchemaCall(firstArg); | ||
| 97 | context.report({ | ||
| 98 | node: node.callee, | ||
| 99 | message: high ? messageHigh(method) : messageMedium(method), | ||
| 100 | }); | ||
| 101 | }, | ||
| 102 | }; | ||
| 103 | }, | ||
| 104 | }; | ||