@sarj/eslint-plugin 15.17.10 → 15.17.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -678,10 +678,11 @@ var TEST_MODULES = /* @__PURE__ */ new Set(["@jest/globals", "@playwright/test",
678
678
  var DUPLICATE_TEST_BODY_DOCUMENTATION = {
679
679
  summary: "Disallow substantial sibling tests with the same body shape; express their differing inputs as a parameterized case table.",
680
680
  rationale: "Copy-pasted test bodies hide the cases that differ and allow equivalent assertions to drift independently.",
681
- remediation: "Move the varying inputs and expected values into a case table consumed by `test.each(...)` or `it.each(...)`.",
681
+ remediation: "Consider a case table with one named test or subtest per case; preserve setup lifetime, test modifiers, and each case's assertions rather than deleting coverage.",
682
682
  category: "testing",
683
683
  limitations: [
684
- "The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments."
684
+ "The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments.",
685
+ "Matching normalized body shapes do not prove runtime equivalence or independent setup; parameterization is a manual review, not an automatic deletion."
685
686
  ],
686
687
  examples: [
687
688
  {
@@ -866,6 +867,10 @@ function isDuplicateTestFrameworkIdentifier(identifier, sourceCode) {
866
867
  const variable = import_utils3.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
867
868
  if (variable === null || variable.defs.length === 0) return true;
868
869
  return variable.defs.some((definition) => {
870
+ if (definition.node.type === import_utils3.AST_NODE_TYPES.ImportDefaultSpecifier) return definition.node.parent.source.value === "node:test";
871
+ if (definition.node.type !== import_utils3.AST_NODE_TYPES.ImportSpecifier) return false;
872
+ const imported = definition.node.imported;
873
+ if (!TEST_CALLERS.has(imported.type === import_utils3.AST_NODE_TYPES.Identifier ? imported.name : String(imported.value))) return false;
869
874
  let current = definition.node;
870
875
  while (current != null && current.type !== import_utils3.AST_NODE_TYPES.ImportDeclaration) current = current.parent;
871
876
  return current?.type === import_utils3.AST_NODE_TYPES.ImportDeclaration && typeof current.source.value === "string" && TEST_MODULES.has(current.source.value);
@@ -2609,12 +2614,33 @@ var import_utils13 = require("@typescript-eslint/utils");
2609
2614
  // src/rules/_sql.ts
2610
2615
  var import_utils12 = require("@typescript-eslint/utils");
2611
2616
  function stripSqlNoise(text) {
2612
- const out = [...text];
2617
+ return scanSqlNoise(text);
2618
+ }
2619
+ function sqlSingleQuotedRanges(text) {
2620
+ const ranges = [];
2621
+ scanSqlNoise(text, (start, end) => ranges.push([start, end]));
2622
+ return ranges;
2623
+ }
2624
+ function scanSqlNoise(text, onSingleQuoted) {
2625
+ const out = text.split("");
2613
2626
  const n = text.length;
2614
2627
  let i = 0;
2615
2628
  while (i < n) {
2616
2629
  const ch = text[i];
2630
+ if (ch === "$" && !/[\w$]/u.test(text[i - 1] ?? "")) {
2631
+ const delimiter = /^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/u.exec(text.slice(i))?.[0];
2632
+ if (delimiter !== void 0) {
2633
+ const closing = text.indexOf(delimiter, i + delimiter.length);
2634
+ const end = closing < 0 ? n : closing + delimiter.length;
2635
+ while (i < end) {
2636
+ if (text[i] !== "\n") out[i] = " ";
2637
+ i += 1;
2638
+ }
2639
+ continue;
2640
+ }
2641
+ }
2617
2642
  if (ch === "'" || ch === '"') {
2643
+ const start = i;
2618
2644
  out[i] = " ";
2619
2645
  i += 1;
2620
2646
  while (i < n) {
@@ -2628,6 +2654,7 @@ function stripSqlNoise(text) {
2628
2654
  }
2629
2655
  out[i] = " ";
2630
2656
  i += 1;
2657
+ if (ch === "'") onSingleQuoted?.(start, i);
2631
2658
  break;
2632
2659
  }
2633
2660
  if (c !== "\n") {
@@ -2648,17 +2675,20 @@ function stripSqlNoise(text) {
2648
2675
  out[i] = " ";
2649
2676
  out[i + 1] = " ";
2650
2677
  i += 2;
2651
- while (i < n && !(text[i] === "*" && text[i + 1] === "/")) {
2678
+ let depth = 1;
2679
+ while (i < n && depth > 0) {
2680
+ if (text[i] === "/" && text[i + 1] === "*" || text[i] === "*" && text[i + 1] === "/") {
2681
+ depth += text[i] === "/" ? 1 : -1;
2682
+ out[i] = " ";
2683
+ out[i + 1] = " ";
2684
+ i += 2;
2685
+ continue;
2686
+ }
2652
2687
  if (text[i] !== "\n") {
2653
2688
  out[i] = " ";
2654
2689
  }
2655
2690
  i += 1;
2656
2691
  }
2657
- if (i < n) {
2658
- out[i] = " ";
2659
- out[i + 1] = " ";
2660
- i += 2;
2661
- }
2662
2692
  continue;
2663
2693
  }
2664
2694
  i += 1;
@@ -2756,8 +2786,9 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
2756
2786
  remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
2757
2787
  category: "security",
2758
2788
  limitations: [
2759
- "The rule reports only visibly quoted runtime values; dynamic identifiers and unquoted fragments require provenance that syntax-only linting cannot prove.",
2760
- "Static fragments and parameterizing tagged templates are exempt."
2789
+ "Only single-quoted SQL values are inspected. Double-quoted identifiers, comments, dollar strings, and unquoted fragments are excluded; this is not a general SQL injection detector.",
2790
+ "Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
2791
+ "The bounded lexer recognizes doubled quotes, comments, and PostgreSQL dollar strings; dialect-specific escape modes and SQL generated through other APIs require separate security review."
2761
2792
  ],
2762
2793
  examples: [
2763
2794
  {
@@ -2798,15 +2829,23 @@ function isStaticFragment(expression) {
2798
2829
  return false;
2799
2830
  }
2800
2831
  function runtimeInterpolations(template) {
2832
+ const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
2833
+ const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
2834
+ let offset = 0;
2801
2835
  return template.expressions.filter(
2802
- (expression, index) => !isStaticFragment(expression) && endsWithSqlQuote(template.quasis[index]?.value.raw ?? "") && startsWithSqlQuote(template.quasis[index + 1]?.value.raw ?? "")
2836
+ (expression, index) => {
2837
+ offset += parts[index]?.length ?? 0;
2838
+ const inValue = ranges.some(([start, end]) => start < offset && offset < end);
2839
+ offset += RUNTIME_MARKER.length;
2840
+ return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
2841
+ }
2803
2842
  );
2804
2843
  }
2805
2844
  function endsWithSqlQuote(text) {
2806
- return /['"]\s*$/u.test(text);
2845
+ return /'\s*$/u.test(text);
2807
2846
  }
2808
2847
  function startsWithSqlQuote(text) {
2809
- return /^\s*['"]/u.test(text);
2848
+ return /^\s*'/u.test(text);
2810
2849
  }
2811
2850
  function staticLiteralText(node) {
2812
2851
  if (node.type === import_utils13.AST_NODE_TYPES.Literal && typeof node.value === "string") {
@@ -2828,7 +2867,13 @@ function runtimeConcatOperands(node) {
2828
2867
  if (!hasStringLiteral) {
2829
2868
  return [];
2830
2869
  }
2870
+ const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
2871
+ const ranges = sqlSingleQuotedRanges(parts.join(""));
2872
+ let offset = 0;
2831
2873
  return operands.filter((operand, index) => {
2874
+ const inValue = ranges.some(([start, end]) => start < offset && offset < end);
2875
+ offset += parts[index]?.length ?? 0;
2876
+ if (!inValue) return false;
2832
2877
  if (isStaticFragment(operand)) return false;
2833
2878
  const before = operands[index - 1];
2834
2879
  const after = operands[index + 1];
@@ -2921,9 +2966,11 @@ var no_dynamic_sql_default = createRule({
2921
2966
  var import_utils14 = require("@typescript-eslint/utils");
2922
2967
  var NO_ENUM_DOCUMENTATION = {
2923
2968
  summary: "Disallow TypeScript `enum`; use string-literal unions or `as const` objects instead.",
2924
- rationale: "TypeScript enums emit runtime objects and numeric enums accept values outside their declared members, adding behavior where a type-only model is sufficient.",
2925
- remediation: "Replace the enum with a string-literal union or an `as const` object and derive its value type from that object.",
2969
+ rationale: "Literal unions keep type-only domains explicit, while constant objects make runtime values deliberate. This policy also avoids compiler-dependent const-enum inlining contracts.",
2970
+ remediation: "Use a literal union or an `as const` object after checking runtime member access, numeric reverse mappings, serialized values, and public consumers.",
2926
2971
  category: "maintainability",
2972
+ limitations: ["This is an explicit style policy for regular and const enums, not a claim that every enum emits an object. Generated files and configured exclusions are preserved; migration is manual."],
2973
+ references: ["https://www.typescriptlang.org/docs/handbook/enums.html"],
2927
2974
  examples: [
2928
2975
  {
2929
2976
  id: "string-literal-union",
@@ -3003,17 +3050,17 @@ var no_enum_default = createRule({
3003
3050
  // src/rules/no-fat-try-blocks.ts
3004
3051
  var import_utils15 = require("@typescript-eslint/utils");
3005
3052
  var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
3006
- summary: "Disallow `try` blocks containing more than three top-level operations that can throw.",
3053
+ summary: "Review try blocks exceeding the configured count of syntactically selected operations.",
3007
3054
  rationale: "A broad `try` block obscures which operation failed and encourages one catch clause to recover from unrelated errors.",
3008
3055
  remediation: "Keep only the operations that share one recovery policy inside the `try` block and move other work outside it.",
3009
3056
  category: "correctness",
3010
3057
  limitations: [
3011
- "The rule uses syntax to identify throwing operations and exempts generated files, finally blocks, rethrows, and terminal error boundaries."
3058
+ "The default threshold is three selected top-level operations, not a proof of every possible throw. A shared recovery policy may legitimately cover several operations; generated files, catchless finally blocks, rethrows, and terminal error boundaries are excluded."
3012
3059
  ],
3013
3060
  examples: [
3014
3061
  {
3015
3062
  id: "focused-try-block",
3016
- title: "A try block contains three throwing operations",
3063
+ title: "Three selected operations stay within the default threshold",
3017
3064
  outcome: "no-match",
3018
3065
  files: [{
3019
3066
  path: "src/load.ts",
@@ -3025,7 +3072,7 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
3025
3072
  },
3026
3073
  {
3027
3074
  id: "broad-try-block",
3028
- title: "A try block contains four throwing operations",
3075
+ title: "Review whether four selected operations share one recovery policy",
3029
3076
  outcome: "match",
3030
3077
  files: [{
3031
3078
  path: "src/load.ts",
@@ -3395,7 +3442,7 @@ var no_fat_try_blocks_default = createRule({
3395
3442
  meta: {
3396
3443
  type: "problem",
3397
3444
  docs: {
3398
- description: "Disallow `try` blocks containing more than three top-level operations that can throw."
3445
+ description: "Review try blocks exceeding the configured count of syntactically selected operations."
3399
3446
  },
3400
3447
  schema: [
3401
3448
  {
@@ -3407,7 +3454,7 @@ var no_fat_try_blocks_default = createRule({
3407
3454
  }
3408
3455
  ],
3409
3456
  messages: {
3410
- fatTryBlock: "This `try` block has {{count}} statements that can throw (max {{max}}). Isolate the throwing statement(s); move non-throwing work outside the `try`."
3457
+ fatTryBlock: "This `try` block has {{count}} syntactically selected operations (max {{max}}). Review whether they share one recovery policy; move unrelated work outside the boundary."
3411
3458
  }
3412
3459
  },
3413
3460
  defaultOptions: [{ max: MAX_TRY_BODY_STATEMENTS }],
@@ -3454,7 +3501,8 @@ var NO_HAND_ROLLED_SLEEP_DOCUMENTATION = {
3454
3501
  remediation: "Use `node:timers/promises` with an abort signal for delays, or pass `AbortSignal.timeout(...)` to the timed operation.",
3455
3502
  category: "correctness",
3456
3503
  limitations: [
3457
- "The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions."
3504
+ "The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions.",
3505
+ "Locally shadowed constructors/timers and value-returning timers are excluded. Only recognized browser markers are excluded; choose a runtime-compatible cancellation API for other browser modules."
3458
3506
  ],
3459
3507
  examples: [
3460
3508
  {
@@ -3598,6 +3646,21 @@ var no_hand_rolled_sleep_default = createRule({
3598
3646
  return {};
3599
3647
  }
3600
3648
  const checkClientModules = optionsArg?.checkClientModules ?? false;
3649
+ const bindingOf = (identifier) => import_utils16.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
3650
+ const isGlobal = (identifier) => (bindingOf(identifier)?.defs.length ?? 0) === 0;
3651
+ const isBuiltinTimer = (callee) => {
3652
+ if (!isSetTimeoutCallee(callee)) return false;
3653
+ if (callee.type === import_utils16.AST_NODE_TYPES.MemberExpression && callee.object.type === import_utils16.AST_NODE_TYPES.Identifier) return isGlobal(callee.object);
3654
+ if (callee.type !== import_utils16.AST_NODE_TYPES.Identifier) return false;
3655
+ const binding = bindingOf(callee);
3656
+ return binding === null || binding.defs.length === 0 || binding.defs.every((definition) => definition.node.type === import_utils16.AST_NODE_TYPES.ImportSpecifier && definition.node.imported.type === import_utils16.AST_NODE_TYPES.Identifier && definition.node.imported.name === "setTimeout" && definition.node.parent.type === import_utils16.AST_NODE_TYPES.ImportDeclaration && ["node:timers", "timers"].includes(String(definition.node.parent.source.value)));
3657
+ };
3658
+ const settlesParameter = (callback, executor, index) => {
3659
+ const parameter = executor.params[index];
3660
+ if (parameter?.type !== import_utils16.AST_NODE_TYPES.Identifier) return false;
3661
+ const callee = callback.type === import_utils16.AST_NODE_TYPES.Identifier ? callback : callback.type === import_utils16.AST_NODE_TYPES.ArrowFunctionExpression || callback.type === import_utils16.AST_NODE_TYPES.FunctionExpression ? soleCall(callback)?.callee : null;
3662
+ return callee?.type === import_utils16.AST_NODE_TYPES.Identifier && bindingOf(callee) === bindingOf(parameter);
3663
+ };
3601
3664
  function isClientModule2() {
3602
3665
  if (/\.[cm]?[jt]sx$/.test(filename)) {
3603
3666
  return true;
@@ -3623,7 +3686,7 @@ var no_hand_rolled_sleep_default = createRule({
3623
3686
  };
3624
3687
  return {
3625
3688
  NewExpression(node) {
3626
- if (node.callee.type !== import_utils16.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
3689
+ if (node.callee.type !== import_utils16.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise" || !isGlobal(node.callee)) {
3627
3690
  return;
3628
3691
  }
3629
3692
  const executor = node.arguments[0];
@@ -3631,7 +3694,7 @@ var no_hand_rolled_sleep_default = createRule({
3631
3694
  return;
3632
3695
  }
3633
3696
  const call = soleCall(executor);
3634
- if (call === null || !isSetTimeoutCallee(call.callee)) {
3697
+ if (call === null || !isBuiltinTimer(call.callee)) {
3635
3698
  return;
3636
3699
  }
3637
3700
  const [callback, delay] = call.arguments;
@@ -3639,14 +3702,14 @@ var no_hand_rolled_sleep_default = createRule({
3639
3702
  return;
3640
3703
  }
3641
3704
  const resolveName = parameterName(executor, 0);
3642
- if (resolveName !== null && settlesWithoutValue(callback, resolveName)) {
3705
+ if (resolveName !== null && call.arguments.length === 2 && settlesWithoutValue(callback, resolveName) && settlesParameter(callback, executor, 0)) {
3643
3706
  if (reportsSleepHere()) {
3644
3707
  context.report({ node, messageId: "handRolledSleep" });
3645
3708
  }
3646
3709
  return;
3647
3710
  }
3648
3711
  const rejectName = parameterName(executor, 1);
3649
- if (rejectName !== null && isRaceArm(node) && rejectsInCallback(callback, rejectName)) {
3712
+ if (rejectName !== null && isRaceArm(node) && settlesParameter(callback, executor, 1) && rejectsInCallback(callback, rejectName)) {
3650
3713
  context.report({ node, messageId: "handRolledTimeoutRace" });
3651
3714
  }
3652
3715
  }
@@ -3745,7 +3808,7 @@ var NO_INSECURE_RANDOM_ID_DOCUMENTATION = {
3745
3808
  rationale: "Math.random is predictable and lacks the entropy required for security-sensitive values.",
3746
3809
  remediation: "Generate the value with crypto.randomUUID or crypto.getRandomValues.",
3747
3810
  category: "security",
3748
- limitations: ["Ambiguous identifiers and test files are excluded to avoid flagging sampling and fixture data."],
3811
+ limitations: ["Names select security-sensitive bindings heuristically; they do not prove sensitivity. Sampling in unrelated bindings or branch tests, locally shadowed Math objects, ambiguous identifiers and test files are excluded. This is not interprocedural data-flow analysis."],
3749
3812
  examples: [
3750
3813
  { id: "cryptographic-id", title: "Use the Web Crypto API", outcome: "no-match", files: [{ path: "src/session.ts", source: "const sessionToken = crypto.randomUUID();" }], focusPath: "src/session.ts", expectedCount: 0, public: true },
3751
3814
  { id: "predictable-token", title: "Do not derive a token from Math.random", outcome: "match", files: [{ path: "src/session.ts", source: "const sessionToken = Math.random();" }], focusPath: "src/session.ts", expectedCount: 1, public: true }
@@ -3826,6 +3889,7 @@ function findEnclosingNames(node) {
3826
3889
  if (directBinding && parent.id.type === "Identifier") {
3827
3890
  names.push(parent.id.name);
3828
3891
  }
3892
+ return names;
3829
3893
  }
3830
3894
  if (parent.type === "Property" && parent.value === current) {
3831
3895
  const key = parent.key;
@@ -3860,7 +3924,7 @@ function findEnclosingNames(node) {
3860
3924
  if (directBinding && parent.id !== null) names.push(parent.id.name);
3861
3925
  return names;
3862
3926
  }
3863
- if (parent.type === "ExpressionStatement") {
3927
+ if (parent.type === "ExpressionStatement" || parent.type === "IfStatement" || parent.type === "ForStatement" || parent.type === "WhileStatement" || parent.type === "DoWhileStatement" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") {
3864
3928
  return names;
3865
3929
  }
3866
3930
  current = parent;
@@ -3952,6 +4016,7 @@ var no_insecure_random_id_default = createRule({
3952
4016
  if (!isMathRandomCall(node)) {
3953
4017
  return;
3954
4018
  }
4019
+ if ((import_utils18.ASTUtils.findVariable(context.sourceCode.getScope(node), "Math")?.defs.length ?? 0) > 0) return;
3955
4020
  const names = findEnclosingNames(node);
3956
4021
  if (names.some(isStrongSecurityName)) {
3957
4022
  context.report({ node, messageId: "insecureRandomId" });
@@ -4816,6 +4881,39 @@ var no_log_only_catch_default = createRule({
4816
4881
  const matcher = createLogMatcher(loggingOptions);
4817
4882
  const filename = context.filename;
4818
4883
  const sourceCode = context.sourceCode;
4884
+ function hasCoercionValidation(node) {
4885
+ const owner = node.parent;
4886
+ const statement = owner.block.body[0];
4887
+ if (node.body.body.length !== 0 || owner.finalizer !== null || owner.block.body.length !== 1 || statement?.type !== import_utils24.AST_NODE_TYPES.ExpressionStatement || statement.expression.type !== import_utils24.AST_NODE_TYPES.AssignmentExpression || statement.expression.operator !== "=") return false;
4888
+ const { left, right } = statement.expression;
4889
+ if (left.type !== import_utils24.AST_NODE_TYPES.MemberExpression || left.computed || left.object.type !== import_utils24.AST_NODE_TYPES.Identifier || right.type !== import_utils24.AST_NODE_TYPES.CallExpression || right.optional || right.callee.type !== import_utils24.AST_NODE_TYPES.Identifier || !["String", "Number", "Boolean", "BigInt"].includes(right.callee.name) || right.arguments.length !== 1) return false;
4890
+ const argument = right.arguments[0];
4891
+ if (argument === void 0 || sourceCode.getText(left) !== sourceCode.getText(argument)) return false;
4892
+ const global = import_utils24.ASTUtils.findVariable(sourceCode.getScope(right.callee), right.callee.name);
4893
+ if (global !== null && global.defs.length > 0) return false;
4894
+ const root = import_utils24.ASTUtils.findVariable(sourceCode.getScope(left.object), left.object.name);
4895
+ if (root === null || root.references.some((reference) => reference.isWrite() && !reference.init)) return false;
4896
+ let current = owner;
4897
+ let slot = statementSlot(current);
4898
+ while (slot === null && current.parent !== void 0 && !FUNCTION_TYPES2.has(current.parent.type)) {
4899
+ current = current.parent;
4900
+ slot = statementSlot(current);
4901
+ }
4902
+ let next = slot?.list[slot.index + 1];
4903
+ let target = sourceCode.getText(left);
4904
+ if (next?.type === import_utils24.AST_NODE_TYPES.VariableDeclaration && next.kind === "const" && next.declarations.length === 1) {
4905
+ const alias = next.declarations[0];
4906
+ if (alias?.id.type !== import_utils24.AST_NODE_TYPES.Identifier || alias.init === null || sourceCode.getText(alias.init) !== target) return false;
4907
+ target = alias.id.name;
4908
+ next = slot?.list[slot.index + 2];
4909
+ }
4910
+ if (next?.type !== import_utils24.AST_NODE_TYPES.IfStatement) return false;
4911
+ let condition = next.test;
4912
+ while (condition.type === import_utils24.AST_NODE_TYPES.LogicalExpression && condition.operator === "&&") condition = condition.left;
4913
+ if (condition.type !== import_utils24.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(condition.operator)) return false;
4914
+ const test = condition.left;
4915
+ return test.type === import_utils24.AST_NODE_TYPES.UnaryExpression && test.operator === "typeof" && sourceCode.getText(test.argument) === target && condition.right.type === import_utils24.AST_NODE_TYPES.Literal && condition.right.value === right.callee.name.toLowerCase();
4916
+ }
4819
4917
  function isLoggingCallStatement(statement) {
4820
4918
  if (statement.type !== "ExpressionStatement") {
4821
4919
  return false;
@@ -4842,17 +4940,11 @@ var no_log_only_catch_default = createRule({
4842
4940
  CatchClause(node) {
4843
4941
  const statements = node.body.body;
4844
4942
  const isDocumented = sourceCode.getCommentsInside(node.body).length > 0 || hasAdjacentRationale(node);
4845
- if (statements.length === 0) {
4846
- if (isDocumented) {
4847
- return;
4848
- }
4849
- if (fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
4850
- return;
4851
- }
4852
- context.report({ node, messageId: "emptyCatch" });
4943
+ if (isDocumented || hasCoercionValidation(node) || fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
4853
4944
  return;
4854
4945
  }
4855
- if (isDocumented) {
4946
+ if (statements.length === 0) {
4947
+ context.report({ node, messageId: "emptyCatch" });
4856
4948
  return;
4857
4949
  }
4858
4950
  const everyStatementIsLogging = statements.every(
@@ -4870,7 +4962,7 @@ var no_log_only_catch_default = createRule({
4870
4962
  var import_utils25 = require("@typescript-eslint/utils");
4871
4963
  var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
4872
4964
  summary: "Disallow a bare return from a test catch block when it skips a later assertion.",
4873
- rationale: "The caught failure turns into a passing test without executing the assertion that follows it.",
4965
+ rationale: "An unasserted catch return can swallow a failure and skip later assertions; the complete test result also depends on other assertions and hooks.",
4874
4966
  remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
4875
4967
  category: "testing",
4876
4968
  filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
@@ -4957,7 +5049,7 @@ var no_bare_return_from_test_catch_default = createRule({
4957
5049
  type: "problem",
4958
5050
  docs: { description: "Disallow a bare return from a test catch block when it skips a later assertion." },
4959
5051
  schema: [],
4960
- messages: { bareReturnFromTestCatch: "This bare return turns the caught failure into a passing test and skips a later assertion. Rethrow, assert on the error, or explicitly skip the test." }
5052
+ messages: { bareReturnFromTestCatch: "This bare return can swallow the caught failure and skips a later assertion. Rethrow, assert on the error, or explicitly skip the test." }
4961
5053
  },
4962
5054
  defaultOptions: [],
4963
5055
  create(context) {
@@ -4976,6 +5068,23 @@ var no_bare_return_from_test_catch_default = createRule({
4976
5068
  if (current === null || current === void 0) break;
4977
5069
  }
4978
5070
  if (catchClause === null || catchClause.parent.finalizer !== null) return;
5071
+ const parameter = catchClause.param;
5072
+ if (parameter?.type === import_utils25.AST_NODE_TYPES.Identifier && node.parent === catchClause.body) {
5073
+ const errorBinding = import_utils25.ASTUtils.findVariable(context.sourceCode.getScope(parameter), parameter.name);
5074
+ const assertedError = catchClause.body.body.some((statement) => {
5075
+ if (statement.range[1] >= node.range[0] || statement.type !== import_utils25.AST_NODE_TYPES.ExpressionStatement) return false;
5076
+ const expression = statement.expression;
5077
+ if (expression.type !== import_utils25.AST_NODE_TYPES.CallExpression || !isAssertion(expression, context)) return false;
5078
+ const root = rootIdentifier2(expression.callee);
5079
+ if (root === null) return false;
5080
+ const assertionName = importedName3(root, context, ASSERTION_MODULES);
5081
+ let operand = expression.callee.type === import_utils25.AST_NODE_TYPES.MemberExpression ? expression.callee.object : null;
5082
+ if (operand?.type === import_utils25.AST_NODE_TYPES.MemberExpression && staticMemberName2(operand) === "not") operand = operand.object;
5083
+ if (assertionName !== "assert" && (assertionName !== "expect" || operand?.type !== import_utils25.AST_NODE_TYPES.CallExpression || operand.callee !== root)) return false;
5084
+ return walkOwnScope(expression, (current) => current.type === import_utils25.AST_NODE_TYPES.Identifier && errorBinding?.references.some((reference) => reference.identifier === current) === true);
5085
+ });
5086
+ if (assertedError) return;
5087
+ }
4979
5088
  if (walkOwnScope(catchClause.body, (current) => current.type === import_utils25.AST_NODE_TYPES.ThrowStatement || isExplicitSkip(current, context))) return;
4980
5089
  if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
4981
5090
  context.report({ node, messageId: "bareReturnFromTestCatch" });
@@ -4987,13 +5096,13 @@ var no_bare_return_from_test_catch_default = createRule({
4987
5096
  // src/rules/no-bespoke-api-case-conversion.ts
4988
5097
  var import_utils26 = require("@typescript-eslint/utils");
4989
5098
  var NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION = {
4990
- summary: "Disallow hand-written snake_case/camelCase object-key translation at a proven API adapter boundary.",
4991
- rationale: "A second, hand-maintained representation of an API wire contract drifts from the generated client and makes backend field renames compile successfully while failing at runtime.",
5099
+ summary: "Review direct snake_case/camelCase mirror mappings on explicitly API-typed adapter values.",
5100
+ rationale: "Duplicating wire-name translation can drift from an API client contract. When the SDK owns application-facing names, centralizing conversion avoids maintaining another mirror by hand.",
4992
5101
  remediation: "Move wire-name ownership and case conversion into the generated SDK/model layer; keep application adapters on the generated typed surface.",
4993
5102
  category: "architecture",
4994
5103
  autofix: "none",
4995
5104
  limitations: [
4996
- "Only files named adapter/adapters that import an API, client, SDK, contract, or generated module are checked.",
5105
+ "Only adapter/adapters files and receivers explicitly annotated with a scope-resolved type imported from an API/client/SDK/contract/generated module are checked. Unrelated imports, local type shadows, reassigned receivers and inferred receiver types are excluded.",
4997
5106
  "Only object properties that directly translate the same identifier between snake_case and lowerCamelCase are reported.",
4998
5107
  "Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and indirect conversions are intentionally excluded."
4999
5108
  ],
@@ -5065,7 +5174,7 @@ var no_bespoke_api_case_conversion_default = createRule({
5065
5174
  docs: { description: NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION.summary },
5066
5175
  schema: [],
5067
5176
  messages: {
5068
- noBespokeApiCaseConversion: "This API adapter manually translates `{{wireName}}` and `{{applicationName}}`; make the generated SDK/model layer own wire-name conversion."
5177
+ noBespokeApiCaseConversion: "This API-typed adapter value mirrors `{{wireName}}` and `{{applicationName}}`. If the SDK owns application-facing names, move this conversion to its model boundary."
5069
5178
  }
5070
5179
  },
5071
5180
  defaultOptions: [],
@@ -5075,16 +5184,33 @@ var no_bespoke_api_case_conversion_default = createRule({
5075
5184
  if (!ADAPTER_BASENAME_RE.test(basename) || isGeneratedFile(filename, context.sourceCode.text) || isTestFile(filename, ["fixtureTree"])) {
5076
5185
  return {};
5077
5186
  }
5078
- const provenApiBoundary = context.sourceCode.ast.body.some(
5079
- (statement) => statement.type === import_utils26.AST_NODE_TYPES.ImportDeclaration && API_BOUNDARY_IMPORT_RE.test(statement.source.value)
5080
- );
5081
- if (!provenApiBoundary) return {};
5187
+ const hasApiReceiver = (value) => {
5188
+ let current = value;
5189
+ while (true) {
5190
+ if (current.type === import_utils26.AST_NODE_TYPES.MemberExpression) current = current.object;
5191
+ else if (current.type === import_utils26.AST_NODE_TYPES.TSAsExpression || current.type === import_utils26.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils26.AST_NODE_TYPES.TSTypeAssertion) current = current.expression;
5192
+ else break;
5193
+ }
5194
+ if (current.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
5195
+ const binding = import_utils26.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
5196
+ if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
5197
+ const identifier = binding.defs[0]?.name;
5198
+ if (identifier?.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
5199
+ const annotation = identifier.typeAnnotation?.typeAnnotation;
5200
+ if (annotation?.type !== import_utils26.AST_NODE_TYPES.TSTypeReference) return false;
5201
+ let typeName = annotation.typeName;
5202
+ while (typeName.type === import_utils26.AST_NODE_TYPES.TSQualifiedName) typeName = typeName.left;
5203
+ if (typeName.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
5204
+ const typeBinding = import_utils26.ASTUtils.findVariable(context.sourceCode.getScope(typeName), typeName.name);
5205
+ return typeBinding?.defs.length === 1 && typeBinding.defs[0]?.type === "ImportBinding" && typeBinding.defs[0].parent.type === import_utils26.AST_NODE_TYPES.ImportDeclaration && API_BOUNDARY_IMPORT_RE.test(typeBinding.defs[0].parent.source.value);
5206
+ };
5082
5207
  return {
5083
5208
  Property(node) {
5084
5209
  if (node.computed || node.method || node.shorthand) return;
5085
5210
  const key = propertyName(node.key);
5086
5211
  const value = memberName3(node.value);
5087
5212
  if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
5213
+ if (!hasApiReceiver(node.value)) return;
5088
5214
  const wireName = SNAKE_CASE_RE.test(key) ? key : value;
5089
5215
  const applicationName = wireName === key ? value : key;
5090
5216
  context.report({
@@ -5207,7 +5333,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
5207
5333
  limitations: [
5208
5334
  "Only ESLint disable comments and TypeScript expect-error directives are checked.",
5209
5335
  "The rule uses a small anchored vocabulary and does not score prose quality generally.",
5210
- "Generated files and descriptions containing any concrete context are excluded."
5336
+ "Generated files and descriptions outside that exact vocabulary are excluded; an unflagged reason is not proof of a justified suppression. Missing descriptions remain owned by the upstream description requirement."
5211
5337
  ],
5212
5338
  examples: [
5213
5339
  {
@@ -5217,7 +5343,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
5217
5343
  files: [
5218
5344
  {
5219
5345
  path: "src/adapter.ts",
5220
- source: "// @ts-expect-error -- vendor types omit the runtime requestId field\nreturn response.requestId;"
5346
+ source: "function requestId(response: object) {\n // @ts-expect-error -- vendor types omit the runtime requestId field\n return response.requestId;\n}"
5221
5347
  }
5222
5348
  ],
5223
5349
  focusPath: "src/adapter.ts",
@@ -5231,7 +5357,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
5231
5357
  files: [
5232
5358
  {
5233
5359
  path: "src/adapter.ts",
5234
- source: "// @ts-expect-error -- false positive\nreturn response.requestId;"
5360
+ source: "function requestId(response: object) {\n // @ts-expect-error -- false positive\n return response.requestId;\n}"
5235
5361
  }
5236
5362
  ],
5237
5363
  focusPath: "src/adapter.ts",
@@ -5464,11 +5590,11 @@ var no_generic_single_export_module_default = createRule({
5464
5590
  // src/rules/no-offset-pagination.ts
5465
5591
  var import_utils29 = require("@typescript-eslint/utils");
5466
5592
  var NO_OFFSET_PAGINATION_DOCUMENTATION = {
5467
- summary: "Disallow OFFSET pagination in embedded SQL; it is O(N) per page and drops or repeats rows under concurrent writes. Use a keyset cursor.",
5593
+ summary: "Prefer keyset pagination for embedded SQL queries using OFFSET.",
5468
5594
  rationale: "Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.",
5469
- remediation: "Page with a stable ordered key and a cursor predicate.",
5595
+ remediation: "Consider a keyset cursor that preserves the query's complete ordering, tie-breakers, and filters.",
5470
5596
  category: "performance",
5471
- limitations: ["Only embedded SQL is inspected; test files and non-pagination OFFSET syntax are excluded."],
5597
+ limitations: ["A SELECT/FROM query shape or adjacent LIMIT/OFFSET fragment is required. Isolated OFFSET fragments and test files are excluded; this lexical context does not prove a database execution sink. Performance and concurrent-write behavior depend on indexes, ordering, isolation, and dialect; bounded pages and random page access can justify OFFSET."],
5472
5598
  examples: [
5473
5599
  { id: "keyset-pagination", title: "Page from a stable cursor", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id FROM runs WHERE id > ? ORDER BY id LIMIT ?`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
5474
5600
  { id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
@@ -5476,17 +5602,18 @@ var NO_OFFSET_PAGINATION_DOCUMENTATION = {
5476
5602
  };
5477
5603
  var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
5478
5604
  var OFFSET_GATE = /offset/i;
5605
+ var PAGINATION_CONTEXT = /\bSELECT\b[\s\S]*\bFROM\b[\s\S]*\bOFFSET\b|\bLIMIT\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)\s+OFFSET\b/i;
5479
5606
  var no_offset_pagination_default = createRule({
5480
5607
  name: "no-offset-pagination",
5481
5608
  documentation: NO_OFFSET_PAGINATION_DOCUMENTATION,
5482
5609
  meta: {
5483
5610
  type: "problem",
5484
5611
  docs: {
5485
- description: "Disallow OFFSET pagination in embedded SQL; it is O(N) per page and drops or repeats rows under concurrent writes. Use a keyset cursor."
5612
+ description: "Prefer keyset pagination for embedded SQL queries using OFFSET."
5486
5613
  },
5487
5614
  schema: [],
5488
5615
  messages: {
5489
- noOffsetPagination: "OFFSET pagination scans and discards every skipped row (O(N) per page) and shifts under concurrent inserts, so rows get repeated or missed. Use a keyset cursor: `WHERE id > ? ORDER BY id LIMIT ?`."
5616
+ noOffsetPagination: "Review OFFSET pagination for large or changing result sets. If a keyset cursor fits the access pattern, preserve the query's complete ordering, tie-breakers, and filters; bounded pages or random page access may justify OFFSET."
5490
5617
  }
5491
5618
  },
5492
5619
  defaultOptions: [],
@@ -5495,7 +5622,7 @@ var no_offset_pagination_default = createRule({
5495
5622
  return {};
5496
5623
  }
5497
5624
  return createSqlListener((sql, node) => {
5498
- if (!OFFSET_PAGINATION.test(sql)) {
5625
+ if (!PAGINATION_CONTEXT.test(sql) || !OFFSET_PAGINATION.test(sql)) {
5499
5626
  return;
5500
5627
  }
5501
5628
  context.report({ node, messageId: "noOffsetPagination" });
@@ -6526,7 +6653,7 @@ var NO_REPEATED_STRING_LITERAL_DOCUMENTATION = {
6526
6653
  ]
6527
6654
  };
6528
6655
  function isStructured(value) {
6529
- return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
6656
+ return SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
6530
6657
  }
6531
6658
  function preview(value) {
6532
6659
  const oneLine = value.replaceAll("\n", " ").trim();
@@ -6547,7 +6674,7 @@ function isScaffolding(node) {
6547
6674
  }
6548
6675
  const isNonComputedPropertyKey = (parent.type === import_utils35.AST_NODE_TYPES.Property || parent.type === import_utils35.AST_NODE_TYPES.PropertyDefinition || parent.type === import_utils35.AST_NODE_TYPES.MethodDefinition || parent.type === import_utils35.AST_NODE_TYPES.AccessorProperty) && parent.key === node && !parent.computed;
6549
6676
  const isRequireSource = parent.type === import_utils35.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils35.AST_NODE_TYPES.Identifier && parent.callee.name === "require";
6550
- return parent.type === import_utils35.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ImportExpression || parent.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils35.AST_NODE_TYPES.TSImportType || parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
6677
+ return parent.type === import_utils35.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ImportExpression || parent.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils35.AST_NODE_TYPES.TSImportType || parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.JSXExpressionContainer && parent.parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
6551
6678
  }
6552
6679
  var no_repeated_string_literal_default = createRule({
6553
6680
  name: "no-repeated-string-literal",
@@ -7467,11 +7594,11 @@ var no_server_env_in_client_component_default = createRule({
7467
7594
  // src/rules/no-select-star.ts
7468
7595
  var import_utils41 = require("@typescript-eslint/utils");
7469
7596
  var NO_SELECT_STAR_DOCUMENTATION = {
7470
- summary: "Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently.",
7597
+ summary: "Prefer explicit column projections over SELECT * in embedded SQL.",
7471
7598
  rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
7472
7599
  remediation: "List every required column explicitly in the projection.",
7473
7600
  category: "correctness",
7474
- limitations: ["Only statically visible embedded SQL is checked; function arguments such as COUNT(*) and stars inside EXISTS are excluded."],
7601
+ limitations: ["Only statically visible embedded SQL is checked; quoted strings (including PostgreSQL dollar strings), comments, function arguments such as COUNT(*), and stars inside EXISTS are excluded. This is a bounded lexical scan, not a complete SQL parser."],
7475
7602
  examples: [
7476
7603
  { id: "explicit-projection", title: "Select the required columns", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id, status FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
7477
7604
  { id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
@@ -7522,7 +7649,7 @@ var no_select_star_default = createRule({
7522
7649
  meta: {
7523
7650
  type: "problem",
7524
7651
  docs: {
7525
- description: "Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently."
7652
+ description: "Prefer explicit column projections over SELECT * in embedded SQL."
7526
7653
  },
7527
7654
  schema: [],
7528
7655
  messages: {
@@ -8006,7 +8133,7 @@ var NO_SILENT_PROMISE_CATCH_DOCUMENTATION = {
8006
8133
  rationale: "A swallowed rejection hides failures and gives callers an indistinguishable fallback value.",
8007
8134
  remediation: "Log, rethrow, or explicitly recover from the rejection; explain intentional teardown suppression.",
8008
8135
  category: "correctness",
8009
- limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded."],
8136
+ limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded.", "Recognized imported Zod construction chains and their stable local aliases are excluded. Other untyped catch-like APIs are not proven to be Promises."],
8010
8137
  examples: [
8011
8138
  { id: "reported-rejection", title: "Report the rejection", outcome: "no-match", files: [{ path: "src/load.ts", source: "load().catch((error) => logger.error({ error }, 'load failed'));" }], focusPath: "src/load.ts", expectedCount: 0, public: true },
8012
8139
  { id: "silent-rejection", title: "Do not swallow the rejection", outcome: "match", files: [{ path: "src/load.ts", source: "load().catch(() => null);" }], focusPath: "src/load.ts", expectedCount: 1, public: true }
@@ -8020,6 +8147,43 @@ var BODY_PARSE_METHODS = /* @__PURE__ */ new Set([
8020
8147
  "json",
8021
8148
  "text"
8022
8149
  ]);
8150
+ var ZOD_CONSTRUCTORS = /* @__PURE__ */ new Set([
8151
+ "any",
8152
+ "array",
8153
+ "bigint",
8154
+ "boolean",
8155
+ "custom",
8156
+ "date",
8157
+ "enum",
8158
+ "literal",
8159
+ "map",
8160
+ "never",
8161
+ "null",
8162
+ "number",
8163
+ "object",
8164
+ "record",
8165
+ "set",
8166
+ "string",
8167
+ "tuple",
8168
+ "undefined",
8169
+ "union",
8170
+ "unknown"
8171
+ ]);
8172
+ var ZOD_CHAIN_METHODS = /* @__PURE__ */ new Set([
8173
+ "array",
8174
+ "catch",
8175
+ "default",
8176
+ "describe",
8177
+ "max",
8178
+ "min",
8179
+ "nullable",
8180
+ "nullish",
8181
+ "optional",
8182
+ "readonly",
8183
+ "refine",
8184
+ "superRefine",
8185
+ "transform"
8186
+ ]);
8023
8187
  function isBodyParseCall(node) {
8024
8188
  return node.type === import_utils43.AST_NODE_TYPES.CallExpression && node.arguments.length === 0 && node.callee.type === import_utils43.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils43.AST_NODE_TYPES.Identifier && BODY_PARSE_METHODS.has(node.callee.property.name);
8025
8189
  }
@@ -8093,6 +8257,26 @@ var no_silent_promise_catch_default = createRule({
8093
8257
  if (isTestFile(context.filename) || isScriptFile(context.filename)) {
8094
8258
  return {};
8095
8259
  }
8260
+ function isZodSchema(node, seen = /* @__PURE__ */ new Set()) {
8261
+ if (seen.has(node)) return false;
8262
+ seen.add(node);
8263
+ if (node.type === import_utils43.AST_NODE_TYPES.Identifier) {
8264
+ const binding = import_utils43.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
8265
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
8266
+ const [definition] = binding.defs;
8267
+ return binding.defs.length === 1 && definition?.node.type === import_utils43.AST_NODE_TYPES.VariableDeclarator && definition.node.init !== null && isZodSchema(definition.node.init, seen);
8268
+ }
8269
+ if (node.type !== import_utils43.AST_NODE_TYPES.CallExpression || node.callee.type !== import_utils43.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.property.type !== import_utils43.AST_NODE_TYPES.Identifier) return false;
8270
+ const { object, property } = node.callee;
8271
+ if (object.type === import_utils43.AST_NODE_TYPES.Identifier && ZOD_CONSTRUCTORS.has(property.name)) {
8272
+ const binding = import_utils43.ASTUtils.findVariable(context.sourceCode.getScope(object), object.name);
8273
+ if (binding?.defs.some((definition) => {
8274
+ const specifier = definition.node;
8275
+ return (specifier.type === import_utils43.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils43.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils43.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils43.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") && specifier.parent.type === import_utils43.AST_NODE_TYPES.ImportDeclaration && isZodModule(String(specifier.parent.source.value));
8276
+ })) return true;
8277
+ }
8278
+ return ZOD_CHAIN_METHODS.has(property.name) && isZodSchema(object, seen);
8279
+ }
8096
8280
  const hasExplanatoryComment = (call, handler) => {
8097
8281
  const sourceCode = context.sourceCode;
8098
8282
  if (sourceCode.getCommentsInside(handler).some(isExplanatory)) {
@@ -8117,6 +8301,7 @@ var no_silent_promise_catch_default = createRule({
8117
8301
  const method = node.callee.property.name;
8118
8302
  const handlerIndex = method === "catch" ? 0 : method === "then" ? 1 : null;
8119
8303
  if (handlerIndex === null) return;
8304
+ if (method === "catch" && isZodSchema(node.callee.object)) return;
8120
8305
  if (isBodyParseCall(node.callee.object)) {
8121
8306
  return;
8122
8307
  }
@@ -8151,14 +8336,14 @@ var no_silent_promise_catch_default = createRule({
8151
8336
  // src/rules/no-sleep-in-test-body.ts
8152
8337
  var import_utils44 = require("@typescript-eslint/utils");
8153
8338
  var NO_SLEEP_IN_TEST_BODY_DOCUMENTATION = {
8154
- summary: "Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers.",
8339
+ summary: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers.",
8155
8340
  rationale: "Wall-clock delays make test correctness depend on scheduler and machine speed.",
8156
- remediation: "Await the observable signal or advance deterministic fake timers.",
8341
+ remediation: "Await the observable signal, or advance fake timers when supported and restore real timers in finally or a teardown hook.",
8157
8342
  category: "testing",
8158
8343
  filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
8159
- limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes and parameterized delays are excluded."],
8344
+ limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks are excluded."],
8160
8345
  examples: [
8161
- { id: "fake-timer", title: "Advance time deterministically", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { vi.useFakeTimers(); const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true },
8346
+ { id: "fake-timer", title: "Advance controlled time and restore real timers", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { vi.useFakeTimers(); try { const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; } finally { vi.useRealTimers(); } });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true },
8162
8347
  { id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true }
8163
8348
  ]
8164
8349
  };
@@ -8177,9 +8362,6 @@ var FUNCTION_TYPES5 = /* @__PURE__ */ new Set([
8177
8362
  function isNonzeroNumericLiteral(node) {
8178
8363
  return node?.type === import_utils44.AST_NODE_TYPES.Literal && typeof node.value === "number" && node.value !== 0;
8179
8364
  }
8180
- function isTimedSetTimeout(node) {
8181
- return node.type === import_utils44.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
8182
- }
8183
8365
  function isPromiseSleep(node) {
8184
8366
  if (node.callee.type !== import_utils44.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
8185
8367
  return false;
@@ -8189,12 +8371,14 @@ function isPromiseSleep(node) {
8189
8371
  return false;
8190
8372
  }
8191
8373
  const body2 = executor.body;
8192
- if (body2.type !== import_utils44.AST_NODE_TYPES.BlockStatement) {
8193
- return isTimedSetTimeout(body2);
8194
- }
8195
- return body2.body.some(
8196
- (stmt) => stmt.type === import_utils44.AST_NODE_TYPES.ExpressionStatement && isTimedSetTimeout(stmt.expression)
8197
- );
8374
+ const resolve2 = executor.params[0];
8375
+ if (executor.params.length !== 1 || resolve2?.type !== import_utils44.AST_NODE_TYPES.Identifier || resolve2.name === "setTimeout") return false;
8376
+ const statement = body2.type === import_utils44.AST_NODE_TYPES.BlockStatement && body2.body.length === 1 ? body2.body[0] : null;
8377
+ const timer = body2.type !== import_utils44.AST_NODE_TYPES.BlockStatement ? body2 : statement?.type === import_utils44.AST_NODE_TYPES.ExpressionStatement ? statement.expression : null;
8378
+ return timer?.type === import_utils44.AST_NODE_TYPES.CallExpression && isTimedSetTimeout(timer) && timer.arguments[0]?.type === import_utils44.AST_NODE_TYPES.Identifier && timer.arguments[0].name === resolve2.name;
8379
+ }
8380
+ function isTimedSetTimeout(node) {
8381
+ return node.type === import_utils44.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
8198
8382
  }
8199
8383
  function isHelperSleep(node) {
8200
8384
  return node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
@@ -8248,11 +8432,11 @@ var no_sleep_in_test_body_default = createRule({
8248
8432
  meta: {
8249
8433
  type: "problem",
8250
8434
  docs: {
8251
- description: "Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers."
8435
+ description: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers."
8252
8436
  },
8253
8437
  schema: [],
8254
8438
  messages: {
8255
- noSleepInTestBody: "A fixed sleep in a test body asserts on wall-clock time and flakes under CI load. Await the promise the code returns, or drive time with `vi.useFakeTimers()` + `await vi.advanceTimersByTimeAsync(ms)`."
8439
+ noSleepInTestBody: "A fixed sleep depends on wall-clock timing and can be flaky under load. Await observable completion or use controlled fake timers, restoring real timers afterward."
8256
8440
  }
8257
8441
  },
8258
8442
  defaultOptions: [],
@@ -8273,11 +8457,17 @@ var no_sleep_in_test_body_default = createRule({
8273
8457
  return {
8274
8458
  NewExpression(node) {
8275
8459
  if (isPromiseSleep(node)) {
8460
+ const constructor = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), "Promise");
8461
+ const timer = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), "setTimeout");
8462
+ if ((constructor?.defs.length ?? 0) > 0 || (timer?.defs.length ?? 0) > 0) return;
8276
8463
  report2(node);
8277
8464
  }
8278
8465
  },
8279
8466
  CallExpression(node) {
8280
8467
  if (isHelperSleep(node)) {
8468
+ if (node.callee.type !== import_utils44.AST_NODE_TYPES.Identifier) return;
8469
+ const variable = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), node.callee.name);
8470
+ if (variable?.defs.some((definition) => definition.type !== "ImportBinding")) return;
8281
8471
  report2(node);
8282
8472
  }
8283
8473
  }
@@ -8298,7 +8488,7 @@ var NO_STORAGE_IN_STATELESS_MODULES_DOCUMENTATION = {
8298
8488
  rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
8299
8489
  remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
8300
8490
  category: "architecture",
8301
- limitations: ["The rule is disabled until module path patterns are configured, recognizes only configured storage method names, and requires storage-like receiver evidence for the overloaded `put` method."],
8491
+ limitations: ["This opt-in architectural policy requires configured module paths and storage method names. Overloaded `put` requires storage-like receiver evidence; `prepare` requires SQL-shaped literal text or a conventional database receiver for dynamic text. These syntax heuristics do not prove database provenance or identify the system of record."],
8302
8492
  examples: [
8303
8493
  { id: "system-of-record", title: "Read from the system of record", outcome: "no-match", files: [{ path: "src/engineer-digest/post.ts", source: "const issues = await linear.listIssues();" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 0, public: true },
8304
8494
  { id: "private-storage", title: "Do not write private state in a stateless module", outcome: "match", files: [{ path: "src/engineer-digest/post.ts", source: "await kv.put('digest:last', timestamp);" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 1, public: true }
@@ -8332,6 +8522,17 @@ function storageMethodName(node, methods) {
8332
8522
  if (name === "put" && !isStorageLikeReceiver(callee.object)) {
8333
8523
  return null;
8334
8524
  }
8525
+ if (name === "prepare") {
8526
+ const argument = node.arguments[0];
8527
+ const text = argument === void 0 ? null : sqlTextOf(argument);
8528
+ if (text !== null) {
8529
+ if (!/^\s*(?:SELECT|WITH|INSERT|UPDATE|DELETE|REPLACE|CREATE|ALTER|DROP|PRAGMA|EXPLAIN)\b/iu.test(stripSqlNoise(text))) return null;
8530
+ } else {
8531
+ const receiver = callee.object;
8532
+ const receiverName = receiver.type === import_utils45.AST_NODE_TYPES.Identifier ? receiver.name : receiver.type === import_utils45.AST_NODE_TYPES.MemberExpression && !receiver.computed && receiver.property.type === import_utils45.AST_NODE_TYPES.Identifier ? receiver.property.name : "";
8533
+ if (!/^(?:db|database|connection)$/iu.test(receiverName)) return null;
8534
+ }
8535
+ }
8335
8536
  return name;
8336
8537
  }
8337
8538
  function isStorageLikeReceiver(node) {
@@ -8671,12 +8872,12 @@ var no_string_concat_in_loop_default = createRule({
8671
8872
  // src/rules/no-tautological-expect.ts
8672
8873
  var import_utils47 = require("@typescript-eslint/utils");
8673
8874
  var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
8674
- summary: "Disallow an assertion whose operands are all literals; its outcome is fixed before the code runs, so it can never fail.",
8875
+ summary: "Disallow supported literal-only assertions that are statically known to pass.",
8675
8876
  rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
8676
8877
  remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
8677
8878
  category: "testing",
8678
8879
  limitations: [
8679
- "Only direct supported `expect` matcher calls in recognized test files are inspected."
8880
+ "Only direct supported `expect` matcher calls in recognized test files are inspected. Local expect bindings, regular expressions, and unsupported coercions are excluded; failing constant assertions are not tautologies."
8680
8881
  ],
8681
8882
  examples: [
8682
8883
  {
@@ -8713,11 +8914,11 @@ var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
8713
8914
  function isLiteral(node) {
8714
8915
  switch (node.type) {
8715
8916
  case import_utils47.AST_NODE_TYPES.Literal:
8716
- return true;
8917
+ return !("regex" in node);
8717
8918
  case import_utils47.AST_NODE_TYPES.TemplateLiteral:
8718
8919
  return node.expressions.length === 0;
8719
8920
  case import_utils47.AST_NODE_TYPES.UnaryExpression:
8720
- return NUMERIC_SIGNS.has(node.operator) && isLiteral(node.argument);
8921
+ return NUMERIC_SIGNS.has(node.operator) && node.argument.type === import_utils47.AST_NODE_TYPES.Literal && typeof node.argument.value === "number";
8721
8922
  case import_utils47.AST_NODE_TYPES.ArrayExpression:
8722
8923
  return node.elements.every((element) => element !== null && isLiteral(element));
8723
8924
  case import_utils47.AST_NODE_TYPES.ObjectExpression:
@@ -8731,6 +8932,43 @@ function isLiteral(node) {
8731
8932
  function isStructuralLiteral(node) {
8732
8933
  return node.type === import_utils47.AST_NODE_TYPES.ArrayExpression || node.type === import_utils47.AST_NODE_TYPES.ObjectExpression;
8733
8934
  }
8935
+ function passesZeroArgumentMatcher(node, matcher) {
8936
+ let value;
8937
+ switch (node.type) {
8938
+ case import_utils47.AST_NODE_TYPES.Literal:
8939
+ value = node.value;
8940
+ break;
8941
+ case import_utils47.AST_NODE_TYPES.TemplateLiteral:
8942
+ value = node.quasis[0]?.value.cooked;
8943
+ break;
8944
+ case import_utils47.AST_NODE_TYPES.UnaryExpression:
8945
+ if (node.argument.type !== import_utils47.AST_NODE_TYPES.Literal || typeof node.argument.value !== "number") return false;
8946
+ value = node.operator === "-" ? -node.argument.value : node.argument.value;
8947
+ break;
8948
+ case import_utils47.AST_NODE_TYPES.ArrayExpression:
8949
+ case import_utils47.AST_NODE_TYPES.ObjectExpression:
8950
+ value = {};
8951
+ break;
8952
+ default:
8953
+ return false;
8954
+ }
8955
+ switch (matcher) {
8956
+ case "toBeDefined":
8957
+ return value !== void 0;
8958
+ case "toBeUndefined":
8959
+ return value === void 0;
8960
+ case "toBeNull":
8961
+ return value === null;
8962
+ case "toBeTruthy":
8963
+ return Boolean(value);
8964
+ case "toBeFalsy":
8965
+ return !value;
8966
+ case "toBeNaN":
8967
+ return typeof value === "number" && Number.isNaN(value);
8968
+ default:
8969
+ return false;
8970
+ }
8971
+ }
8734
8972
  function expectOperand(callee) {
8735
8973
  const receiver = callee.object;
8736
8974
  if (receiver.type !== import_utils47.AST_NODE_TYPES.CallExpression || receiver.callee.type !== import_utils47.AST_NODE_TYPES.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
@@ -8744,12 +8982,12 @@ var no_tautological_expect_default = createRule({
8744
8982
  meta: {
8745
8983
  type: "problem",
8746
8984
  docs: {
8747
- description: "Disallow an assertion whose operands are all literals; its outcome is fixed before the code runs, so it can never fail."
8985
+ description: "Disallow supported literal-only assertions that are statically known to pass."
8748
8986
  },
8749
8987
  schema: [],
8750
8988
  messages: {
8751
- tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares a literal with an identical literal \u2014 it passes even if the code under test is deleted. Assert on a value the code produced, or delete the test.",
8752
- tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` asserts on a literal, so its outcome is fixed before the code runs. Assert on a value the code produced, or delete the test."
8989
+ tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares an identical literal and does not observe behavior. Assert on a produced value or remove only the redundant assertion, preserving other coverage.",
8990
+ tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
8753
8991
  }
8754
8992
  },
8755
8993
  defaultOptions: [],
@@ -8771,11 +9009,20 @@ var no_tautological_expect_default = createRule({
8771
9009
  return;
8772
9010
  }
8773
9011
  const matcher = callee.property.name;
9012
+ if (callee.object.type !== import_utils47.AST_NODE_TYPES.CallExpression || callee.object.callee.type !== import_utils47.AST_NODE_TYPES.Identifier) return;
9013
+ const expectIdentifier = callee.object.callee;
9014
+ const variable = import_utils47.ASTUtils.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
9015
+ if (variable !== null && variable.defs.some((definition) => {
9016
+ if (definition.node.type !== import_utils47.AST_NODE_TYPES.ImportSpecifier) return true;
9017
+ const declaration = definition.node.parent;
9018
+ const imported = definition.node.imported;
9019
+ return declaration.type !== import_utils47.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === import_utils47.AST_NODE_TYPES.Identifier ? imported.name : imported.value) !== "expect";
9020
+ })) return;
8774
9021
  const operand = expectOperand(callee);
8775
9022
  if (operand === null || !isLiteral(operand)) {
8776
9023
  return;
8777
9024
  }
8778
- if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0) {
9025
+ if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher)) {
8779
9026
  context.report({
8780
9027
  node,
8781
9028
  messageId: "tautologicalMatcher",
@@ -10027,15 +10274,16 @@ var MOCK_MODULES = /* @__PURE__ */ new Set([
10027
10274
  var NO_UNSAFE_MOCK_CASTING_DOCUMENTATION = {
10028
10275
  summary: "Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.",
10029
10276
  rationale: "A type assertion can claim an unmocked value is a mock and bypass checking between the original callable and the mock API.",
10030
- remediation: "Use the test framework's `mocked` helper to obtain the typed mock reference.",
10277
+ remediation: "Create the mock or spy first, then use the framework's mocked helper to preserve the original value's type. The helper does not create or verify a runtime mock.",
10031
10278
  category: "testing",
10032
- limitations: ["Only mock types imported from Vitest or Jest modules are inspected."],
10279
+ limitations: ["Only mock types imported from Vitest or Jest modules are inspected. mocked is a type helper, not runtime validation or a replacement for mock setup."],
10280
+ references: ["https://vitest.dev/api/vi.html#vi-mocked"],
10033
10281
  examples: [
10034
10282
  {
10035
10283
  id: "typed-mock-helper",
10036
10284
  title: "Use the framework helper",
10037
10285
  outcome: "no-match",
10038
- files: [{ path: "src/client.test.ts", source: "const m = vi.mocked(myFn);" }],
10286
+ files: [{ path: "src/client.test.ts", source: "import { vi } from 'vitest'; const client = { read: () => 'value' }; vi.spyOn(client, 'read'); const m = vi.mocked(client.read);" }],
10039
10287
  focusPath: "src/client.test.ts",
10040
10288
  expectedCount: 0,
10041
10289
  public: true
@@ -10061,7 +10309,7 @@ var no_unsafe_mock_casting_default = createRule({
10061
10309
  },
10062
10310
  schema: [],
10063
10311
  messages: {
10064
- unsafeMockCast: "Do not cast to a Mock type. Use `vi.mocked(fn)` or `jest.mocked(fn)` instead to preserve type safety."
10312
+ unsafeMockCast: "Avoid a broad Mock cast. After creating the mock or spy, use `vi.mocked(fn)` or `jest.mocked(fn)` to retain its original type; the helper does not create a runtime mock."
10065
10313
  }
10066
10314
  },
10067
10315
  defaultOptions: [],
@@ -10123,7 +10371,7 @@ var import_utils55 = require("@typescript-eslint/utils");
10123
10371
  var ts2 = __toESM(require("typescript"), 1);
10124
10372
  var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
10125
10373
  summary: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.',
10126
- rationale: "Wrapping a TypeScript enum preserves its emitted runtime object and duplicates the schema's value definition across two constructs.",
10374
+ rationale: "The project prefers literal-first schema definitions. nativeEnum also accepts plain enum-like objects, so this is an explicit declaration policy, not proof that every call duplicates a TypeScript enum's runtime object.",
10127
10375
  remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
10128
10376
  category: "maintainability",
10129
10377
  autofix: "none",
@@ -10301,7 +10549,7 @@ var TEST_LOOPS_OVER_LITERAL_CASES_DOCUMENTATION = {
10301
10549
  remediation: "Create one named parameterized test or runner-aware subtest for each literal case.",
10302
10550
  category: "testing",
10303
10551
  filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**"],
10304
- limitations: ["Only inline literal for-of cases containing framework assertions are reported."],
10552
+ limitations: ["Only inline literal for-of cases containing framework assertions are reported. References to setup or parameters owned by the enclosing test, and loops followed by statements in the same or enclosing block, are excluded because they can belong to an ordered scenario. External helper purity is not inferred."],
10305
10553
  examples: [
10306
10554
  { id: "parameterized-cases", title: "Use a parameterized test", outcome: "no-match", files: [{ path: "src/parser.test.ts", source: "test.each(['a', 'b'])('parses %s', (value) => { expect(parse(value)).toBe(value); });" }], focusPath: "src/parser.test.ts", expectedCount: 0, public: true },
10307
10555
  { id: "looped-cases", title: "Do not hide cases in a loop", outcome: "match", files: [{ path: "src/parser.test.ts", source: "test('parses', () => { for (const value of ['a', 'b']) { expect(parse(value)).toBe(value); } });" }], focusPath: "src/parser.test.ts", expectedCount: 1, public: true }
@@ -10448,7 +10696,7 @@ var test_loops_over_literal_cases_default = createRule({
10448
10696
  },
10449
10697
  schema: [],
10450
10698
  messages: {
10451
- literalCaseLoop: "This loop asserts over {{count}} inline cases, but the runner sees one test and stops at the first failure. Create one named test or subtest per case; use `test.each(...)` or `it.each(...)` where supported."
10699
+ literalCaseLoop: "This loop asserts over {{count}} inline cases in one test; a thrown assertion may prevent later cases from running. Create one named test or subtest per independent case; use `test.each(...)` or `it.each(...)` where supported."
10452
10700
  }
10453
10701
  },
10454
10702
  defaultOptions: [],
@@ -10473,6 +10721,9 @@ var test_loops_over_literal_cases_default = createRule({
10473
10721
  if (enclosing === null || !isTestBody2(enclosing, isFrameworkTest)) {
10474
10722
  return;
10475
10723
  }
10724
+ for (let current = node; current !== void 0 && current !== enclosing; current = current.parent) {
10725
+ if (current.parent?.type === import_utils56.AST_NODE_TYPES.BlockStatement && current.parent.body.at(-1) !== current) return;
10726
+ }
10476
10727
  const cases = unwrapExpression(node.right);
10477
10728
  const callbackParameters = new Set(
10478
10729
  enclosing.params.flatMap((parameter) => parameter.type === import_utils56.AST_NODE_TYPES.Identifier ? [parameter.name] : [])
@@ -10482,6 +10733,16 @@ var test_loops_over_literal_cases_default = createRule({
10482
10733
  ) || !walkOwnScope2(node.body, (current) => isAssertion2(current, isFrameworkAssertion)) || walkOwnScope2(node.body, (current) => opensSubtest(current, callbackParameters)) || walkOwnScope2(node.body, (current) => LOOP_CARRIED_CONTROL.has(current.type))) {
10483
10734
  return;
10484
10735
  }
10736
+ const capturesSetup = walkOwnScope2(node.body, (current) => {
10737
+ if (current.type !== import_utils56.AST_NODE_TYPES.Identifier) return false;
10738
+ const variable = import_utils56.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
10739
+ if (variable === null || !variable.references.some((reference) => reference.identifier === current)) return false;
10740
+ return variable.defs.some((definition) => {
10741
+ const declaration = definition.name;
10742
+ return declaration.range[0] >= enclosing.range[0] && declaration.range[1] <= enclosing.range[1] && (declaration.range[0] < node.range[0] || declaration.range[1] > node.range[1]);
10743
+ });
10744
+ });
10745
+ if (capturesSetup) return;
10485
10746
  context.report({
10486
10747
  node,
10487
10748
  messageId: "literalCaseLoop",
@@ -11102,7 +11363,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
11102
11363
  category: "correctness",
11103
11364
  autofix: "none",
11104
11365
  limitations: [
11105
- "Only direct identifier keys in application-owned z.object/z.strictObject schemas are checked.",
11366
+ "Only direct identifier keys with recognizable numeric Zod leaves in application-owned z.object/z.strictObject schemas are checked; aliases and transformations are not inferred.",
11106
11367
  "The rule covers control timings such as timeout, delay, interval, backoff, TTL, lease, heartbeat, debounce, and throttle; observed durations and business-domain periods are excluded.",
11107
11368
  "Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and non-Zod schemas are excluded."
11108
11369
  ],
@@ -11114,7 +11375,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
11114
11375
  files: [
11115
11376
  {
11116
11377
  path: "src/request.ts",
11117
- source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(1) });"
11378
+ source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(1000).max(300000).default(30000) });"
11118
11379
  }
11119
11380
  ],
11120
11381
  focusPath: "src/request.ts",
@@ -11128,7 +11389,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
11128
11389
  files: [
11129
11390
  {
11130
11391
  path: "src/request.ts",
11131
- source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1) });"
11392
+ source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1).max(300).default(30) });"
11132
11393
  }
11133
11394
  ],
11134
11395
  focusPath: "src/request.ts",
@@ -11160,6 +11421,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
11160
11421
  }
11161
11422
  const zodNamespaces = /* @__PURE__ */ new Set();
11162
11423
  const objectFactories = /* @__PURE__ */ new Set();
11424
+ const numberFactories = /* @__PURE__ */ new Set();
11163
11425
  function binding(identifier) {
11164
11426
  return import_utils63.ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier.name);
11165
11427
  }
@@ -11179,10 +11441,27 @@ var prefer_millisecond_control_duration_schema_default = createRule({
11179
11441
  const variable = binding(callee.object);
11180
11442
  return variable !== null && zodNamespaces.has(variable);
11181
11443
  }
11444
+ function isNumericSchema(node) {
11445
+ if (node.type !== import_utils63.AST_NODE_TYPES.CallExpression) return false;
11446
+ const callee = node.callee;
11447
+ if (callee.type === import_utils63.AST_NODE_TYPES.Identifier) {
11448
+ const variable = binding(callee);
11449
+ return variable !== null && numberFactories.has(variable);
11450
+ }
11451
+ if (callee.type !== import_utils63.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils63.AST_NODE_TYPES.Identifier) return false;
11452
+ if (callee.object.type === import_utils63.AST_NODE_TYPES.Identifier) {
11453
+ const variable = binding(callee.object);
11454
+ return callee.property.name === "number" && variable !== null && zodNamespaces.has(variable);
11455
+ }
11456
+ return ["int", "min", "max", "positive", "nonnegative", "finite", "multipleOf", "optional", "nullable", "nullish", "default", "describe", "brand", "readonly"].includes(callee.property.name) && isNumericSchema(callee.object);
11457
+ }
11182
11458
  return {
11183
11459
  ImportDeclaration(node) {
11184
11460
  if (!isZodModule(node.source.value)) return;
11185
11461
  for (const specifier of node.specifiers) {
11462
+ if (specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && specifier.imported.name === "number") {
11463
+ record(numberFactories, specifier.local);
11464
+ }
11186
11465
  if (specifier.type === import_utils63.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils63.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
11187
11466
  record(zodNamespaces, specifier.local);
11188
11467
  } else if (specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && (specifier.imported.name === "object" || specifier.imported.name === "strictObject")) {
@@ -11195,7 +11474,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
11195
11474
  const shape = node.arguments[0];
11196
11475
  if (shape?.type !== import_utils63.AST_NODE_TYPES.ObjectExpression) return;
11197
11476
  for (const member of shape.properties) {
11198
- if (member.type !== import_utils63.AST_NODE_TYPES.Property) continue;
11477
+ if (member.type !== import_utils63.AST_NODE_TYPES.Property || !isNumericSchema(member.value)) continue;
11199
11478
  const key = directIdentifierKey(member);
11200
11479
  if (key === null || !CONTROL_SECONDS_RE.test(key.name) && !CONTROL_SECONDS_CAMEL_RE.test(key.name)) {
11201
11480
  continue;
@@ -11215,7 +11494,7 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
11215
11494
  remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
11216
11495
  category: "correctness",
11217
11496
  limitations: [
11218
- "The rule skips generated files, test files, JavaScript files, and collections that are deliberately mutated in their declaring module."
11497
+ "Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven."
11219
11498
  ],
11220
11499
  examples: [
11221
11500
  {
@@ -11369,7 +11648,7 @@ var prefer_immutable_module_constant_default = createRule({
11369
11648
  }
11370
11649
  const exportedNames2 = /* @__PURE__ */ new Set();
11371
11650
  const typeAliases2 = /* @__PURE__ */ new Map();
11372
- const mutatesThroughConstAlias = (root) => {
11651
+ const mutatesThroughAlias = (root) => {
11373
11652
  const pending = [root];
11374
11653
  const seen = /* @__PURE__ */ new Set();
11375
11654
  while (pending.length > 0) {
@@ -11381,7 +11660,7 @@ var prefer_immutable_module_constant_default = createRule({
11381
11660
  if (identifier.type !== import_utils64.AST_NODE_TYPES.Identifier) continue;
11382
11661
  if (referenceMutates(identifier, isUnshadowedGlobal3)) return true;
11383
11662
  const declarator = identifier.parent;
11384
- if (declarator.type !== import_utils64.AST_NODE_TYPES.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== import_utils64.AST_NODE_TYPES.Identifier || declarator.parent.type !== import_utils64.AST_NODE_TYPES.VariableDeclaration || declarator.parent.kind !== "const") {
11663
+ if (declarator.type !== import_utils64.AST_NODE_TYPES.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== import_utils64.AST_NODE_TYPES.Identifier || declarator.parent.type !== import_utils64.AST_NODE_TYPES.VariableDeclaration) {
11385
11664
  continue;
11386
11665
  }
11387
11666
  const alias = sourceCode.getDeclaredVariables(declarator)[0];
@@ -11430,7 +11709,7 @@ var prefer_immutable_module_constant_default = createRule({
11430
11709
  return;
11431
11710
  }
11432
11711
  const variable = sourceCode.getDeclaredVariables(node)[0];
11433
- if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughConstAlias(variable)) {
11712
+ if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughAlias(variable)) {
11434
11713
  return;
11435
11714
  }
11436
11715
  context.report({
@@ -11857,7 +12136,7 @@ var PREFER_MODULE_LEVEL_CONSTANT_DOCUMENTATION = {
11857
12136
  rationale: "Recreating immutable lookup data on every call wastes allocations and obscures its constant nature.",
11858
12137
  remediation: "Declare immutable literal collections and non-stateful regular expressions once at module scope.",
11859
12138
  category: "performance",
11860
- limitations: ["Collections that are small, mutated, escape the function, or depend on local values are not reported."],
12139
+ limitations: ["Small collections, observed direct or nested mutations, nested aliases, direct escapes, and dependencies on local values are excluded. Directly invoked function expressions are excluded rather than assuming they run repeatedly. Callback effects and indirect escapes are not analyzed interprocedurally; review those before hoisting."],
11861
12140
  examples: [
11862
12141
  { id: "hoisted-collection", title: "Hoist a constant collection", outcome: "no-match", files: [{ path: "src/keys.ts", source: "const KEYS = ['a', 'b', 'c'] as const; function isAllowed(key: string) { return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 0, public: true },
11863
12142
  { id: "local-collection", title: "Do not recreate a constant collection", outcome: "match", files: [{ path: "src/keys.ts", source: "function isAllowed(key: string) { const KEYS = ['a', 'b', 'c']; return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 1, public: true }
@@ -12012,12 +12291,16 @@ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
12012
12291
  ]
12013
12292
  );
12014
12293
  function isSafeRead(identifier) {
12015
- const parent = identifier.parent;
12294
+ let parent = identifier.parent;
12016
12295
  if (parent.type === import_utils66.AST_NODE_TYPES.MemberExpression) {
12017
12296
  if (parent.object !== identifier) {
12018
12297
  return true;
12019
12298
  }
12299
+ while (parent.parent.type === import_utils66.AST_NODE_TYPES.MemberExpression && parent.parent.object === parent) {
12300
+ parent = parent.parent;
12301
+ }
12020
12302
  const grandparent = parent.parent;
12303
+ if (grandparent.type === import_utils66.AST_NODE_TYPES.VariableDeclarator || grandparent.type === import_utils66.AST_NODE_TYPES.SpreadElement) return false;
12021
12304
  if (grandparent.type === import_utils66.AST_NODE_TYPES.AssignmentExpression && grandparent.left === parent) {
12022
12305
  return false;
12023
12306
  }
@@ -12027,7 +12310,7 @@ function isSafeRead(identifier) {
12027
12310
  if (grandparent.type === import_utils66.AST_NODE_TYPES.UnaryExpression && grandparent.operator === "delete") {
12028
12311
  return false;
12029
12312
  }
12030
- if (!parent.computed && parent.property.type === import_utils66.AST_NODE_TYPES.Identifier && MUTATING_METHODS2.has(parent.property.name) && grandparent.type === import_utils66.AST_NODE_TYPES.CallExpression && grandparent.callee === parent) {
12313
+ if (grandparent.type === import_utils66.AST_NODE_TYPES.CallExpression && grandparent.callee === parent && (parent.computed ? parent.property.type !== import_utils66.AST_NODE_TYPES.Literal || typeof parent.property.value !== "string" || MUTATING_METHODS2.has(parent.property.value) : parent.property.type === import_utils66.AST_NODE_TYPES.Identifier && MUTATING_METHODS2.has(parent.property.name))) {
12031
12314
  return false;
12032
12315
  }
12033
12316
  return true;
@@ -12135,9 +12418,15 @@ var prefer_module_level_constant_default = createRule({
12135
12418
  if (node.id.type !== import_utils66.AST_NODE_TYPES.Identifier || node.init === null) {
12136
12419
  return;
12137
12420
  }
12138
- if (enclosingFunction3(node) === null) {
12421
+ const owner = enclosingFunction3(node);
12422
+ if (owner === null) {
12139
12423
  return;
12140
12424
  }
12425
+ let expression = owner;
12426
+ while (expression.parent !== void 0 && unwrap4(expression.parent) === expression) {
12427
+ expression = expression.parent;
12428
+ }
12429
+ if (expression.parent?.type === import_utils66.AST_NODE_TYPES.CallExpression && expression.parent.callee === expression) return;
12141
12430
  const candidate2 = classify(node.init, checkRegex);
12142
12431
  if (candidate2 === null) {
12143
12432
  return;
@@ -12162,10 +12451,10 @@ var prefer_module_level_constant_default = createRule({
12162
12451
  var import_utils67 = require("@typescript-eslint/utils");
12163
12452
  var PREFER_MODULE_LEVEL_SCHEMA_DOCUMENTATION = {
12164
12453
  summary: "Declare a Zod schema at module scope when it closes over nothing in the enclosing function",
12165
- rationale: "A closed schema created inside a function is rebuilt on every call and cannot be reused or exported for inference.",
12454
+ rationale: "A schema created inside a function is rebuilt on each call. Module scope can enable reuse across callers; local schemas already support local type inference.",
12166
12455
  remediation: "Move the closed schema declaration to module scope and reference it from the function.",
12167
12456
  category: "performance",
12168
- limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded."],
12457
+ limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded.", "Eager calls outside the recognized Zod construction chain and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, schema identity, error customization, and module initialization order before moving construction."],
12169
12458
  examples: [
12170
12459
  { id: "module-schema", title: "Declare the schema once", outcome: "no-match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; const ZBody = z.object({ id: z.string(), name: z.string() }); export function handle(raw: unknown) { return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 0, public: true },
12171
12460
  { id: "local-schema", title: "Do not rebuild a closed schema", outcome: "match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; export function handle(raw: unknown) { const ZBody = z.object({ id: z.string(), name: z.string() }); return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 1, public: true }
@@ -12182,6 +12471,36 @@ var DEFAULT_FACTORIES = [
12182
12471
  "union"
12183
12472
  ];
12184
12473
  var DEFAULT_MIN_PROPERTIES = 2;
12474
+ var CONSTRUCTION_FACTORIES = /* @__PURE__ */ new Set([
12475
+ ...DEFAULT_FACTORIES,
12476
+ "any",
12477
+ "array",
12478
+ "bigint",
12479
+ "boolean",
12480
+ "custom",
12481
+ "date",
12482
+ "enum",
12483
+ "instanceof",
12484
+ "lazy",
12485
+ "literal",
12486
+ "map",
12487
+ "nan",
12488
+ "nativeEnum",
12489
+ "never",
12490
+ "null",
12491
+ "nullable",
12492
+ "nullish",
12493
+ "number",
12494
+ "optional",
12495
+ "preprocess",
12496
+ "promise",
12497
+ "set",
12498
+ "string",
12499
+ "symbol",
12500
+ "undefined",
12501
+ "unknown",
12502
+ "void"
12503
+ ]);
12185
12504
  var MEMO_CALLEES = /* @__PURE__ */ new Set([
12186
12505
  "lazy",
12187
12506
  "memo",
@@ -12258,7 +12577,7 @@ function outermostEnclosingFunction(node) {
12258
12577
  }
12259
12578
  return outermost;
12260
12579
  }
12261
- function subtreeSome(root, predicate) {
12580
+ function subtreeSome(root, predicate, skipDeferredFunctions = false) {
12262
12581
  let found = false;
12263
12582
  const visit = (value) => {
12264
12583
  if (found || value === null || typeof value !== "object") {
@@ -12274,6 +12593,7 @@ function subtreeSome(root, predicate) {
12274
12593
  if (typeof candidate2.type !== "string") {
12275
12594
  return;
12276
12595
  }
12596
+ if (skipDeferredFunctions && FUNCTION_TYPES8.has(candidate2.type)) return;
12277
12597
  if (predicate(candidate2)) {
12278
12598
  found = true;
12279
12599
  return;
@@ -12372,6 +12692,15 @@ var prefer_module_level_schema_default = createRule({
12372
12692
  function isZodCall(node) {
12373
12693
  return node.type === import_utils67.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils67.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils67.AST_NODE_TYPES.Identifier && zodNamespaces.has(node.callee.object.name);
12374
12694
  }
12695
+ function isSchemaConstruction(node) {
12696
+ const callee = node.callee;
12697
+ if (callee.type !== import_utils67.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils67.AST_NODE_TYPES.Identifier || TERMINAL_METHODS.has(callee.property.name)) return false;
12698
+ if (callee.object.type === import_utils67.AST_NODE_TYPES.CallExpression) return isSchemaConstruction(callee.object);
12699
+ return isZodCall(node) && CONSTRUCTION_FACTORIES.has(callee.property.name);
12700
+ }
12701
+ function hasEagerComputation(node) {
12702
+ return subtreeSome(node, (inner) => inner.type === import_utils67.AST_NODE_TYPES.NewExpression || inner.type === import_utils67.AST_NODE_TYPES.TaggedTemplateExpression || inner.type === import_utils67.AST_NODE_TYPES.CallExpression && !isSchemaConstruction(inner), true);
12703
+ }
12375
12704
  function isCovered(node) {
12376
12705
  let current = node.parent ?? void 0;
12377
12706
  while (current !== void 0) {
@@ -12501,6 +12830,7 @@ var prefer_module_level_schema_default = createRule({
12501
12830
  return;
12502
12831
  }
12503
12832
  const outermost = outermostSchemaExpression(expression);
12833
+ if (hasEagerComputation(outermost)) return;
12504
12834
  if (outermost !== expression && (readsReceiver(outermost) || buildsLocalizedText(outermost) || !closesOverNothing(outermost, enclosing))) {
12505
12835
  return;
12506
12836
  }
@@ -12638,7 +12968,8 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
12638
12968
  limitations: [
12639
12969
  "Composite object/record/tuple/union schemas are owned by prefer-module-level-schema.",
12640
12970
  "Schemas that depend on function-local or mutable state, localized text, receiver state, lazy construction, or recognized memoization are excluded.",
12641
- "Literal string z.enum domains are owned by prefer-shared-zod-enum."
12971
+ "Literal string z.enum domains are owned by prefer-shared-zod-enum.",
12972
+ "Eager calls outside recognized Zod construction chains and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, error customization, schema identity, and module initialization order before moving construction."
12642
12973
  ],
12643
12974
  examples: [
12644
12975
  {
@@ -12680,7 +13011,7 @@ function collectReferences2(scope, output) {
12680
13011
  output.push(...scope.references);
12681
13012
  for (const child of scope.childScopes) collectReferences2(child, output);
12682
13013
  }
12683
- function subtreeSome2(root, predicate) {
13014
+ function subtreeSome2(root, predicate, skipDeferredFunctions = false) {
12684
13015
  let found = false;
12685
13016
  const visit = (value) => {
12686
13017
  if (found || value === null || typeof value !== "object") return;
@@ -12690,6 +13021,7 @@ function subtreeSome2(root, predicate) {
12690
13021
  }
12691
13022
  const candidate2 = value;
12692
13023
  if (typeof candidate2.type !== "string") return;
13024
+ if (skipDeferredFunctions && FUNCTION_TYPES9.has(candidate2.type)) return;
12693
13025
  if (predicate(candidate2)) {
12694
13026
  found = true;
12695
13027
  return;
@@ -12810,6 +13142,15 @@ var prefer_module_level_refined_schema_default = createRule({
12810
13142
  return names[1] ?? null;
12811
13143
  return null;
12812
13144
  }
13145
+ function isSchemaConstruction(node) {
13146
+ const callee = node.callee;
13147
+ if (callee.type !== import_utils68.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils68.AST_NODE_TYPES.Identifier || NON_SCHEMA_TERMINALS.has(callee.property.name)) return false;
13148
+ if (callee.object.type === import_utils68.AST_NODE_TYPES.CallExpression) return isSchemaConstruction(callee.object);
13149
+ return factoryName(node, FACTORIES) !== null || factoryName(node, COMPOSITE_FACTORIES) !== null;
13150
+ }
13151
+ function hasEagerComputation(node) {
13152
+ return subtreeSome2(node, (inner) => inner.type === import_utils68.AST_NODE_TYPES.NewExpression || inner.type === import_utils68.AST_NODE_TYPES.TaggedTemplateExpression || inner.type === import_utils68.AST_NODE_TYPES.CallExpression && !isSchemaConstruction(inner), true);
13153
+ }
12813
13154
  function isSharedEnumDomain(node, factory) {
12814
13155
  if (factory !== "enum") return false;
12815
13156
  const [argument] = node.arguments;
@@ -12878,7 +13219,7 @@ var prefer_module_level_refined_schema_default = createRule({
12878
13219
  const enclosing = outermostEnclosingFunction2(node);
12879
13220
  if (enclosing === void 0) return;
12880
13221
  const expression = schemaExpression2(node);
12881
- if (readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
13222
+ if (hasEagerComputation(expression) || readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
12882
13223
  return;
12883
13224
  context.report({ node, messageId: "hoistRefinedSchema" });
12884
13225
  }
@@ -12906,7 +13247,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
12906
13247
  outcome: "no-match",
12907
13248
  files: [{
12908
13249
  path: "src/schema.ts",
12909
- source: "import { z } from 'zod'; export const Version = z.literal([1, 2, 3]);"
13250
+ source: "import { z } from 'zod/v4'; export const Version = z.literal([1, 2, 3]);"
12910
13251
  }],
12911
13252
  focusPath: "src/schema.ts",
12912
13253
  expectedCount: 0,
@@ -12918,7 +13259,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
12918
13259
  outcome: "match",
12919
13260
  files: [{
12920
13261
  path: "src/schema.ts",
12921
- source: "import { z } from 'zod'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
13262
+ source: "import { z } from 'zod/v4'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
12922
13263
  }],
12923
13264
  focusPath: "src/schema.ts",
12924
13265
  expectedCount: 1,
@@ -13077,11 +13418,11 @@ var import_utils71 = require("@typescript-eslint/utils");
13077
13418
  var PREFER_NAMED_COMPLEX_RETURN_TYPE_DOCUMENTATION = {
13078
13419
  summary: "Prefer a named contract for structurally complex function return types.",
13079
13420
  rationale: "A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.",
13080
- remediation: "Extract the return annotation to a named type or interface and reference that contract from the signature.",
13421
+ remediation: "Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.",
13081
13422
  category: "maintainability",
13082
13423
  limitations: [
13083
13424
  "Only explicit object types with at least three members and unions with at least three object variants are reported.",
13084
- "Generic wrappers such as Promise and Readonly are unwrapped one level at a time; inferred return types are outside this rule."
13425
+ "Any single-argument generic wrapper is traversed to find nested shapes, not assumed semantically transparent. Inferred return types are outside this rule; extraction is manual and must preserve locally bound type parameters."
13085
13426
  ],
13086
13427
  examples: [
13087
13428
  { id: "named-result", title: "Name a multi-state result", outcome: "no-match", files: [{ path: "src/queue.ts", source: "type ClaimResult = { state: 'idle' } | { state: 'waiting'; retryAt: number } | { state: 'claimed'; id: string }; export function claim(): ClaimResult { return { state: 'idle' }; }" }], focusPath: "src/queue.ts", expectedCount: 0, public: true },
@@ -13237,12 +13578,14 @@ var import_utils73 = require("@typescript-eslint/utils");
13237
13578
  var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
13238
13579
  summary: "Prefer the modern one-shot node:crypto hash API when streaming state is unnecessary.",
13239
13580
  rationale: "A createHash-update-digest chain allocates mutable streaming state for a single in-memory value; Node's built-in hash function expresses the one-shot operation directly and can use its optimized fast path.",
13240
- remediation: "Import hash from node:crypto and replace a single-update chain with hash(algorithm, value, encoding). Keep createHash for streams or multiple incremental updates.",
13581
+ remediation: "On a supported Node runtime, consider hash(algorithm, value, encoding). Preserve the output encoding explicitly: digest() returns a Buffer, while hash defaults to hex. Keep createHash for streams or multiple updates.",
13241
13582
  category: "performance",
13242
13583
  limitations: [
13243
13584
  "Only bindings and inline calls with statically proven provenance from crypto or node:crypto are analyzed; arbitrary assignments and dynamic module specifiers are excluded.",
13244
- "Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid."
13585
+ "Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid.",
13586
+ "Runtime support and output encoding require manual review; no autofix or guaranteed speedup is promised."
13245
13587
  ],
13588
+ references: ["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],
13246
13589
  examples: [
13247
13590
  { id: "one-shot-hash", title: "Use Node's one-shot hash API", outcome: "no-match", files: [{ path: "case.ts", source: "import { hash } from 'node:crypto'; export const digest = hash('sha256', 'value', 'hex');" }], focusPath: "case.ts", expectedCount: 0, public: true },
13248
13591
  { id: "mutable-one-shot-chain", title: "Avoid mutable state for one value", outcome: "match", files: [{ path: "case.ts", source: "import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true }
@@ -13371,6 +13714,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
13371
13714
  category: "performance",
13372
13715
  limitations: [
13373
13716
  "Tests and generated files are excluded.",
13717
+ "This recommendation applies to Node-compatible runtimes. Changing a synchronous API to a promise changes its caller contract; review startup-only work and APIs that require synchronous execution rather than mechanically adding await.",
13374
13718
  "ESLint rule implementations under src/rules are excluded because visitor creation and execution are synchronous by contract.",
13375
13719
  "Only statically identifiable node:fs loads are inspected; filesystem objects passed through arbitrary functions or assignments require type-aware analysis."
13376
13720
  ],
@@ -13389,14 +13733,14 @@ function memberName5(node) {
13389
13733
  function unwrapAwait2(node) {
13390
13734
  return node.type === import_utils74.AST_NODE_TYPES.AwaitExpression ? node.argument : node;
13391
13735
  }
13392
- function isFsLoader(node) {
13736
+ function isFsLoader(node, isGlobal) {
13393
13737
  const expression = unwrapAwait2(node);
13394
13738
  if (expression.type === import_utils74.AST_NODE_TYPES.ImportExpression) return isFsSpecifier(expression.source);
13395
13739
  if (expression.type !== import_utils74.AST_NODE_TYPES.CallExpression || expression.arguments.length !== 1) return false;
13396
13740
  const [argument] = expression.arguments;
13397
13741
  if (argument === void 0 || argument.type === import_utils74.AST_NODE_TYPES.SpreadElement || !isFsSpecifier(argument)) return false;
13398
- if (expression.callee.type === import_utils74.AST_NODE_TYPES.Identifier) return expression.callee.name === "require";
13399
- return expression.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && memberName5(expression.callee) === "getBuiltinModule";
13742
+ if (expression.callee.type === import_utils74.AST_NODE_TYPES.Identifier) return expression.callee.name === "require" && isGlobal(expression.callee);
13743
+ return expression.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName5(expression.callee) === "getBuiltinModule";
13400
13744
  }
13401
13745
  function isFsSpecifier(node) {
13402
13746
  return node.type === import_utils74.AST_NODE_TYPES.Literal && (node.value === "node:fs" || node.value === "fs");
@@ -13423,13 +13767,26 @@ var prefer_node_fs_promises_default = createRule({
13423
13767
  if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text) || normalizedFilename.includes("src/rules/"))
13424
13768
  return {};
13425
13769
  const namespaces = /* @__PURE__ */ new Set();
13770
+ const bindingOf = (node) => import_utils74.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
13771
+ const isGlobal = (node) => {
13772
+ const binding = bindingOf(node);
13773
+ return binding === null || binding.defs.length === 0;
13774
+ };
13775
+ const isNamespace = (node) => {
13776
+ const binding = bindingOf(node);
13777
+ return binding !== null && namespaces.has(binding) && !binding.references.some((reference) => reference.isWrite() && reference.init !== true);
13778
+ };
13779
+ const recordNamespace = (node) => {
13780
+ const binding = bindingOf(node);
13781
+ if (binding !== null) namespaces.add(binding);
13782
+ };
13426
13783
  return {
13427
13784
  ImportDeclaration(node) {
13428
13785
  if (node.source.value !== "node:fs" && node.source.value !== "fs") return;
13429
13786
  const synchronousImports = [];
13430
13787
  for (const specifier of node.specifiers) {
13431
13788
  if (specifier.type === import_utils74.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils74.AST_NODE_TYPES.ImportDefaultSpecifier) {
13432
- namespaces.add(specifier.local.name);
13789
+ recordNamespace(specifier.local);
13433
13790
  continue;
13434
13791
  }
13435
13792
  if (specifier.type === import_utils74.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils74.AST_NODE_TYPES.Identifier && specifier.imported.name.endsWith("Sync")) synchronousImports.push(specifier.imported.name);
@@ -13443,10 +13800,10 @@ var prefer_node_fs_promises_default = createRule({
13443
13800
  }
13444
13801
  },
13445
13802
  VariableDeclarator(node) {
13446
- if (node.init === null || !isFsLoader(node.init) && (node.init.type !== import_utils74.AST_NODE_TYPES.Identifier || !namespaces.has(node.init.name)))
13803
+ if (node.init === null || !isFsLoader(node.init, isGlobal) && (node.init.type !== import_utils74.AST_NODE_TYPES.Identifier || !isNamespace(node.init)))
13447
13804
  return;
13448
13805
  if (node.id.type === import_utils74.AST_NODE_TYPES.Identifier) {
13449
- namespaces.add(node.id.name);
13806
+ recordNamespace(node.id);
13450
13807
  return;
13451
13808
  }
13452
13809
  if (node.id.type !== import_utils74.AST_NODE_TYPES.ObjectPattern) return;
@@ -13467,7 +13824,7 @@ var prefer_node_fs_promises_default = createRule({
13467
13824
  const name = memberName5(node);
13468
13825
  if (name?.endsWith("Sync") !== true) return;
13469
13826
  const object = unwrapAwait2(node.object);
13470
- if (object.type === import_utils74.AST_NODE_TYPES.Identifier && namespaces.has(object.name) || isFsLoader(object)) {
13827
+ if (object.type === import_utils74.AST_NODE_TYPES.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
13471
13828
  context.report({ node, messageId: "preferAsyncFs", data: { name } });
13472
13829
  }
13473
13830
  }
@@ -13478,11 +13835,11 @@ var prefer_node_fs_promises_default = createRule({
13478
13835
  // src/rules/prefer-non-nullable-collection.ts
13479
13836
  var import_utils75 = require("@typescript-eslint/utils");
13480
13837
  var PREFER_NON_NULLABLE_COLLECTION_DOCUMENTATION = {
13481
- summary: "Suggest non-null arrays only when local control flow proves the nullish state is equivalent to an empty collection.",
13838
+ summary: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard.",
13482
13839
  rationale: "A redundant nullish collection state spreads defaults and guards through consumers without carrying information.",
13483
13840
  remediation: "Use a non-null collection type and normalize omitted input to an empty collection at the boundary.",
13484
13841
  category: "maintainability",
13485
- limitations: ["The rule requires local evidence that nullish and empty values are treated identically and skips exported wire shapes."],
13842
+ limitations: ["Recognized defaults and guards are manual modeling prompts, not proof of equivalence for every caller or later use. Exported wire shapes and unknown object escapes are excluded; preserve meaningful null states and boundary compatibility."],
13486
13843
  examples: [
13487
13844
  { id: "non-null-array", title: "Model an always-present collection", outcome: "no-match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] } function search({ items }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 0, public: true },
13488
13845
  { id: "defaulted-nullish-array", title: "Do not retain a redundant nullish state", outcome: "match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] | undefined } function search({ items = [] }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 1, public: true }
@@ -13551,22 +13908,6 @@ function sameAccess(node, access) {
13551
13908
  }
13552
13909
  return node.type === import_utils75.AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === import_utils75.AST_NODE_TYPES.Identifier && node.object.name === access.object && node.property.type === import_utils75.AST_NODE_TYPES.Identifier && node.property.name === access.property;
13553
13910
  }
13554
- function isNullGuard(node, access) {
13555
- if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
13556
- if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(node.operator)) {
13557
- return false;
13558
- }
13559
- const nullish = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === null || value.type === import_utils75.AST_NODE_TYPES.Identifier && value.name === "undefined";
13560
- return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
13561
- }
13562
- function isEmptyGuard(node, access) {
13563
- if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
13564
- if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
13565
- return false;
13566
- }
13567
- const zero = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === 0;
13568
- return memberLengthOf(node.left, access) && zero(node.right) || memberLengthOf(node.right, access) && zero(node.left);
13569
- }
13570
13911
  function memberLengthOf(node, access) {
13571
13912
  const target = node.type === import_utils75.AST_NODE_TYPES.ChainExpression ? node.expression : node;
13572
13913
  return target.type === import_utils75.AST_NODE_TYPES.MemberExpression && !target.computed && target.property.type === import_utils75.AST_NODE_TYPES.Identifier && target.property.name === "length" && sameAccess(target.object, access);
@@ -13581,7 +13922,24 @@ function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
13581
13922
  const terminating = first.consequent.type === import_utils75.AST_NODE_TYPES.ReturnStatement || first.consequent.type === import_utils75.AST_NODE_TYPES.ThrowStatement || first.consequent.type === import_utils75.AST_NODE_TYPES.BlockStatement && first.consequent.body.length === 1 && (first.consequent.body[0]?.type === import_utils75.AST_NODE_TYPES.ReturnStatement || first.consequent.body[0]?.type === import_utils75.AST_NODE_TYPES.ThrowStatement);
13582
13923
  if (!terminating) return false;
13583
13924
  if (contains(first.consequent, visitorKeys, (node) => sameAccess(node, access))) return false;
13584
- return contains(first.test, visitorKeys, (node) => isNullGuard(node, access)) && contains(first.test, visitorKeys, (node) => isEmptyGuard(node, access));
13925
+ if (first.test.type === import_utils75.AST_NODE_TYPES.UnaryExpression && first.test.operator === "!" && optionalMemberLengthOf(first.test.argument, access)) return true;
13926
+ return first.test.type === import_utils75.AST_NODE_TYPES.LogicalExpression && first.test.operator === "||" && isNullGuard(first.test.left, access) && isEmptyGuard(first.test.right, access);
13927
+ }
13928
+ function isNullGuard(node, access) {
13929
+ if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
13930
+ if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(node.operator)) {
13931
+ return false;
13932
+ }
13933
+ const nullish = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === null || value.type === import_utils75.AST_NODE_TYPES.Identifier && value.name === "undefined";
13934
+ return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
13935
+ }
13936
+ function isEmptyGuard(node, access) {
13937
+ if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
13938
+ if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
13939
+ return false;
13940
+ }
13941
+ const zero = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === 0;
13942
+ return memberLengthOf(node.left, access) && zero(node.right) || node.operator !== "<=" && memberLengthOf(node.right, access) && zero(node.left);
13585
13943
  }
13586
13944
  function contains(node, visitorKeys, predicate) {
13587
13945
  if (predicate(node)) return true;
@@ -13619,7 +13977,7 @@ function memberIsOnlyCoalesced(context, object, property, fn) {
13619
13977
  if (!belongsToFunction(reference.identifier, fn)) return [null];
13620
13978
  const parent = reference.identifier.parent;
13621
13979
  if (parent?.type === import_utils75.AST_NODE_TYPES.MemberExpression && !parent.computed && parent.object === reference.identifier && parent.property.type === import_utils75.AST_NODE_TYPES.Identifier && parent.property.name === property) return [parent];
13622
- return [];
13980
+ return parent?.type === import_utils75.AST_NODE_TYPES.MemberExpression && parent.object === reference.identifier ? [] : [null];
13623
13981
  });
13624
13982
  return accesses.length > 0 && accesses.every((access) => access !== null && directlyCoalesced(access));
13625
13983
  }
@@ -13629,11 +13987,11 @@ var prefer_non_nullable_collection_default = createRule({
13629
13987
  meta: {
13630
13988
  type: "suggestion",
13631
13989
  docs: {
13632
- description: "Suggest non-null arrays only when local control flow proves the nullish state is equivalent to an empty collection."
13990
+ description: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard."
13633
13991
  },
13634
13992
  schema: [],
13635
13993
  messages: {
13636
- preferNonNullableCollection: "`{{name}}` is locally treated exactly like `[]`; make it a non-null array and normalize omitted input at the boundary."
13994
+ preferNonNullableCollection: "`{{name}}` uses an empty-array default or shared null-or-empty guard; consider a non-null array after checking that the nullish state carries no separate meaning."
13637
13995
  }
13638
13996
  },
13639
13997
  defaultOptions: [],
@@ -13882,12 +14240,12 @@ var ts4 = __toESM(require("typescript"), 1);
13882
14240
  var PREFER_AWAIT_IN_ASYNC_RETURN_DOCUMENTATION = {
13883
14241
  summary: "Prefer explicit `await` when an async function directly returns one typed Promise `.then` transform.",
13884
14242
  rationale: "Mixing a directly returned Promise callback into otherwise async control flow makes sequencing and failures harder to read.",
13885
- remediation: "Await the Promise, then return the transformed value with ordinary async statements.",
14243
+ remediation: "Consider awaiting the Promise and returning the transformed value with ordinary async statements. Preserve catch boundaries, callback behavior, and observable scheduling when rewriting manually.",
13886
14244
  category: "maintainability",
13887
14245
  since: "15.6.3",
13888
14246
  limitations: [
13889
14247
  "Only a single directly returned `.then` call with an inline callback is checked.",
13890
- "The receiver must be proven Promise-like by TypeScript; untyped files and larger chains are intentionally ignored.",
14248
+ "The receiver must be proven Promise-like by TypeScript; an earlier chain can still produce that receiver. Untyped receivers, rejection handlers and named callbacks are excluded. This is not the upstream return-await policy and no scheduling equivalence is promised.",
13891
14249
  "Direct loader callbacks passed to resolved `React.lazy` and `next/dynamic` imports are excluded because returning the module Promise is their framework contract."
13892
14250
  ],
13893
14251
  examples: [
@@ -14028,12 +14386,17 @@ var import_utils78 = require("@typescript-eslint/utils");
14028
14386
  var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
14029
14387
  summary: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access.",
14030
14388
  rationale: "External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.",
14031
- remediation: "Parse the payload through a schema or establish a recognized runtime validation guard before reading fields.",
14389
+ remediation: "For external payloads, parse through a schema or establish runtime validation before reading fields. Review validator implementations separately rather than recursively requiring another schema.",
14032
14390
  category: "correctness",
14033
- limitations: ["Test fixtures, generated clients, local JSON files, and recognized validation guards are excluded."],
14391
+ limitations: [
14392
+ "A JSON.parse call alone does not prove external input; exact native JSON stringify/parse round trips are excluded. Validator implementations and other non-network parsing can still require manual review rather than a schema rewrite.",
14393
+ "JSON.parse must resolve to the global JSON object. json() receivers require an unshadowed global Request/Response annotation or construction, or stable local aliases of global fetch results; imported, inferred and unknown response types are deliberately not inferred.",
14394
+ "Named validators remain conventions, not proof of their implementation. Their exemptions are confined to a valid branch or a preceding same-block validation statement; ignored predicate results and deferred callbacks do not validate later reads.",
14395
+ "Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
14396
+ ],
14034
14397
  examples: [
14035
- { id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
14036
- { id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
14398
+ { id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
14399
+ { id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
14037
14400
  ]
14038
14401
  };
14039
14402
  var unwrap6 = (node) => {
@@ -14058,7 +14421,7 @@ var isSchemaParseReference = (node) => {
14058
14421
  const inner = unwrap6(node);
14059
14422
  return inner !== null && inner.type === import_utils78.AST_NODE_TYPES.MemberExpression && !inner.computed && inner.property.type === import_utils78.AST_NODE_TYPES.Identifier && (inner.property.name === "parse" || inner.property.name === "safeParse");
14060
14423
  };
14061
- var isRawPayloadSource = (node, isKnownLocalText) => {
14424
+ var isRawPayloadSource = (node, context, isKnownLocalText) => {
14062
14425
  let current = unwrap6(node);
14063
14426
  if (current === null) return false;
14064
14427
  if (current.type === import_utils78.AST_NODE_TYPES.AwaitExpression) {
@@ -14076,13 +14439,31 @@ var isRawPayloadSource = (node, isKnownLocalText) => {
14076
14439
  return false;
14077
14440
  }
14078
14441
  if (property.name === "json") {
14079
- return true;
14442
+ return !callee.computed && current.arguments.length === 0 && isResponseSource(callee.object, context);
14080
14443
  }
14081
14444
  if (PROMISE_CHAIN_METHODS.has(property.name)) {
14082
- return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object);
14445
+ return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object, context, isKnownLocalText);
14083
14446
  }
14084
14447
  const object = unwrap6(callee.object);
14085
- return property.name === "parse" && object !== null && object.type === import_utils78.AST_NODE_TYPES.Identifier && object.name === "JSON" && !isLocalFileRead(current.arguments[0]) && isKnownLocalText?.(current.arguments[0]) !== true;
14448
+ const input = unwrap6(current.arguments[0]);
14449
+ if (input?.type === import_utils78.AST_NODE_TYPES.CallExpression && input.arguments.length === 1 && input.callee.type === import_utils78.AST_NODE_TYPES.MemberExpression && !input.callee.computed && input.callee.object.type === import_utils78.AST_NODE_TYPES.Identifier && input.callee.object.name === "JSON" && input.callee.property.type === import_utils78.AST_NODE_TYPES.Identifier && input.callee.property.name === "stringify" && (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(input.callee.object), "JSON")?.defs.length ?? 0) === 0) return false;
14450
+ return property.name === "parse" && object !== null && object.type === import_utils78.AST_NODE_TYPES.Identifier && object.name === "JSON" && (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(object), "JSON")?.defs.length ?? 0) === 0 && !isLocalFileRead(current.arguments[0]) && isKnownLocalText?.(current.arguments[0]) !== true;
14451
+ };
14452
+ var isResponseSource = (node, context, seen = /* @__PURE__ */ new Set()) => {
14453
+ let current = unwrap6(node);
14454
+ if (current?.type === import_utils78.AST_NODE_TYPES.AwaitExpression) current = unwrap6(current.argument);
14455
+ if (current === null || seen.has(current)) return false;
14456
+ seen.add(current);
14457
+ const isGlobal = (identifier) => (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier.name)?.defs.length ?? 0) === 0;
14458
+ if (current.type === import_utils78.AST_NODE_TYPES.CallExpression) return current.callee.type === import_utils78.AST_NODE_TYPES.Identifier && current.callee.name === "fetch" && isGlobal(current.callee);
14459
+ if (current.type === import_utils78.AST_NODE_TYPES.NewExpression) return current.callee.type === import_utils78.AST_NODE_TYPES.Identifier && ["Request", "Response"].includes(current.callee.name) && isGlobal(current.callee);
14460
+ if (current.type !== import_utils78.AST_NODE_TYPES.Identifier) return false;
14461
+ const binding = import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
14462
+ if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
14463
+ const definition = binding.defs[0];
14464
+ const annotation = definition?.name.type === import_utils78.AST_NODE_TYPES.Identifier ? definition.name.typeAnnotation?.typeAnnotation : void 0;
14465
+ if (annotation?.type === import_utils78.AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === import_utils78.AST_NODE_TYPES.Identifier && ["Request", "Response"].includes(annotation.typeName.name) && isGlobal(annotation.typeName)) return true;
14466
+ return definition?.type === "Variable" && definition.parent.kind === "const" && definition.node.init !== null && isResponseSource(definition.node.init, context, seen);
14086
14467
  };
14087
14468
  var FILE_READ_RE = /^(readFile|readFileSync|readJson|readJsonSync|readJSON)$/;
14088
14469
  var isDirectLocalFileRead = (node) => {
@@ -14348,7 +14729,7 @@ var prefer_schema_for_api_payload_default = createRule({
14348
14729
  },
14349
14730
  schema: [],
14350
14731
  messages: {
14351
- unparsedJsonAccess: "Property access on an unvalidated payload (`response.json()` / `JSON.parse()`) without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
14732
+ unparsedJsonAccess: "Review property access on parsed JSON without a recognized validation boundary. For external payloads, validate before reading fields; validator implementations need manual review, not a recursive schema rewrite."
14352
14733
  }
14353
14734
  },
14354
14735
  defaultOptions: [],
@@ -14359,6 +14740,40 @@ var prefer_schema_for_api_payload_default = createRule({
14359
14740
  const unvalidatedVariables = /* @__PURE__ */ new Set();
14360
14741
  const aliasGroups = /* @__PURE__ */ new Map();
14361
14742
  const localFileTextVariables = /* @__PURE__ */ new Set();
14743
+ const namedGuards = /* @__PURE__ */ new Map();
14744
+ const guardDominates = (use, call) => {
14745
+ if (context.sourceCode.getScope(use).variableScope !== context.sourceCode.getScope(call).variableScope) return false;
14746
+ let guard = call;
14747
+ let positive = true;
14748
+ while (guard.parent.type === import_utils78.AST_NODE_TYPES.UnaryExpression && guard.parent.operator === "!") {
14749
+ positive = !positive;
14750
+ guard = guard.parent;
14751
+ }
14752
+ while (positive && guard.parent.type === import_utils78.AST_NODE_TYPES.LogicalExpression && guard.parent.operator === "&&") guard = guard.parent;
14753
+ const branch = guard.parent;
14754
+ if (branch.type === import_utils78.AST_NODE_TYPES.IfStatement && branch.test === guard) {
14755
+ if (positive && nodeWithin2(use, branch.consequent)) return true;
14756
+ if (!positive && branch.alternate !== null && nodeWithin2(use, branch.alternate)) return true;
14757
+ const terminal = branch.consequent.type === import_utils78.AST_NODE_TYPES.BlockStatement ? branch.consequent.body.at(-1) : branch.consequent;
14758
+ if (!positive && (terminal?.type === import_utils78.AST_NODE_TYPES.ThrowStatement || terminal?.type === import_utils78.AST_NODE_TYPES.ReturnStatement)) {
14759
+ let statement2 = use;
14760
+ while (statement2.parent !== void 0 && statement2.parent !== branch.parent && statement2.parent.type !== import_utils78.AST_NODE_TYPES.Program) {
14761
+ if (statement2.type === import_utils78.AST_NODE_TYPES.FunctionDeclaration || statement2.type === import_utils78.AST_NODE_TYPES.FunctionExpression || statement2.type === import_utils78.AST_NODE_TYPES.ArrowFunctionExpression) return false;
14762
+ statement2 = statement2.parent;
14763
+ }
14764
+ return statement2.parent === branch.parent && statement2.range[0] > branch.range[1];
14765
+ }
14766
+ }
14767
+ if (branch.type === import_utils78.AST_NODE_TYPES.ConditionalExpression && branch.test === guard) return nodeWithin2(use, positive ? branch.consequent : branch.alternate);
14768
+ if (positive && branch.type === import_utils78.AST_NODE_TYPES.WhileStatement && branch.test === guard) return nodeWithin2(use, branch.body);
14769
+ if (call.parent.type !== import_utils78.AST_NODE_TYPES.ExpressionStatement || call.callee.type !== import_utils78.AST_NODE_TYPES.Identifier || /^(?:is|has)[A-Z]/u.test(call.callee.name)) return false;
14770
+ let statement = use;
14771
+ while (statement.parent !== void 0 && statement.parent !== call.parent.parent && statement.parent.type !== import_utils78.AST_NODE_TYPES.Program) {
14772
+ if (statement.type === import_utils78.AST_NODE_TYPES.FunctionDeclaration || statement.type === import_utils78.AST_NODE_TYPES.FunctionExpression || statement.type === import_utils78.AST_NODE_TYPES.ArrowFunctionExpression) return false;
14773
+ statement = statement.parent;
14774
+ }
14775
+ return statement.parent === call.parent.parent && statement.range[0] > call.parent.range[1];
14776
+ };
14362
14777
  const localFileTextRef = (node, scope) => {
14363
14778
  const unwrapped = unwrap6(node);
14364
14779
  if (unwrapped?.type !== import_utils78.AST_NODE_TYPES.Identifier) return null;
@@ -14375,6 +14790,7 @@ var prefer_schema_for_api_payload_default = createRule({
14375
14790
  };
14376
14791
  const clearBinding = (variable) => {
14377
14792
  unvalidatedVariables.delete(variable);
14793
+ namedGuards.delete(variable);
14378
14794
  const group = aliasGroups.get(variable);
14379
14795
  aliasGroups.delete(variable);
14380
14796
  group?.delete(variable);
@@ -14409,10 +14825,24 @@ var prefer_schema_for_api_payload_default = createRule({
14409
14825
  };
14410
14826
  const isFullyNarrowedPattern = (declarator) => {
14411
14827
  const declared = context.sourceCode.getDeclaredVariables(declarator);
14828
+ const statement = declarator.parent;
14829
+ const block = statement.parent;
14830
+ const followsRejectingGuard = (identifier) => {
14831
+ if (block.type !== import_utils78.AST_NODE_TYPES.BlockStatement && block.type !== import_utils78.AST_NODE_TYPES.Program) return false;
14832
+ if (context.sourceCode.getScope(identifier).variableScope !== context.sourceCode.getScope(declarator).variableScope) return false;
14833
+ return block.body.some((candidate2) => {
14834
+ if (candidate2.type !== import_utils78.AST_NODE_TYPES.IfStatement || candidate2.range[1] >= identifier.range[0] || bindingValidationPolarity(candidate2.test, identifier.name) !== "valid-when-false") return false;
14835
+ const terminal = candidate2.consequent.type === import_utils78.AST_NODE_TYPES.BlockStatement ? candidate2.consequent.body.at(-1) : candidate2.consequent;
14836
+ return terminal?.type === import_utils78.AST_NODE_TYPES.ThrowStatement || terminal?.type === import_utils78.AST_NODE_TYPES.ReturnStatement;
14837
+ });
14838
+ };
14412
14839
  return declared.length > 0 && declared.every(
14413
- (variable) => variable.references.some(
14414
- (reference) => isValidationRead(reference.identifier)
14415
- )
14840
+ (variable) => variable.references.some((reference) => isValidationRead(reference.identifier)) && variable.references.every((reference) => {
14841
+ const identifier = reference.identifier;
14842
+ if (reference.init === true) return true;
14843
+ if (reference.isWrite() || identifier.type !== import_utils78.AST_NODE_TYPES.Identifier) return false;
14844
+ return isValidationRead(identifier) || isUseWithinValidatedBranch(identifier, identifier.name) || followsRejectingGuard(identifier);
14845
+ })
14416
14846
  );
14417
14847
  };
14418
14848
  const trackInitializer = (declarator, scope) => {
@@ -14420,7 +14850,7 @@ var prefer_schema_for_api_payload_default = createRule({
14420
14850
  const variable = declaredVars[0];
14421
14851
  if (variable === void 0) return;
14422
14852
  const localText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
14423
- if (isRawPayloadSource(declarator.init, localText)) {
14853
+ if (isRawPayloadSource(declarator.init, context, localText)) {
14424
14854
  trackRawBinding(variable);
14425
14855
  return;
14426
14856
  }
@@ -14441,6 +14871,7 @@ var prefer_schema_for_api_payload_default = createRule({
14441
14871
  if (node.id.type === import_utils78.AST_NODE_TYPES.ObjectPattern || node.id.type === import_utils78.AST_NODE_TYPES.ArrayPattern) {
14442
14872
  if (isRawPayloadSource(
14443
14873
  node.init,
14874
+ context,
14444
14875
  (candidate2) => localFileTextRef(candidate2, scope) !== null
14445
14876
  )) {
14446
14877
  if (!isFullyNarrowedPattern(node)) {
@@ -14460,7 +14891,7 @@ var prefer_schema_for_api_payload_default = createRule({
14460
14891
  if (variable === null) return;
14461
14892
  const isLocalText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
14462
14893
  updateLocalFileText(variable, node.right, scope);
14463
- if (isRawPayloadSource(node.right, isLocalText)) {
14894
+ if (isRawPayloadSource(node.right, context, isLocalText)) {
14464
14895
  trackRawBinding(variable);
14465
14896
  } else {
14466
14897
  const source = unvalidatedVariableRef(node.right, scope, unvalidatedVariables);
@@ -14472,6 +14903,7 @@ var prefer_schema_for_api_payload_default = createRule({
14472
14903
  if (node.left.type === import_utils78.AST_NODE_TYPES.ObjectPattern || node.left.type === import_utils78.AST_NODE_TYPES.ArrayPattern) {
14473
14904
  if (isRawPayloadSource(
14474
14905
  node.right,
14906
+ context,
14475
14907
  (candidate2) => localFileTextRef(candidate2, scope) !== null
14476
14908
  )) {
14477
14909
  context.report({
@@ -14501,7 +14933,13 @@ var prefer_schema_for_api_payload_default = createRule({
14501
14933
  continue;
14502
14934
  }
14503
14935
  const variable = findVariable2(scope, unwrapped.name);
14504
- if (variable !== null) clearAliasGroup(variable);
14936
+ if (variable !== null) {
14937
+ for (const alias of aliasGroups.get(variable) ?? [variable]) {
14938
+ const guards = namedGuards.get(alias) ?? [];
14939
+ guards.push(node);
14940
+ namedGuards.set(alias, guards);
14941
+ }
14942
+ }
14505
14943
  }
14506
14944
  },
14507
14945
  MemberExpression(node) {
@@ -14511,6 +14949,7 @@ var prefer_schema_for_api_payload_default = createRule({
14511
14949
  const obj = unwrap6(node.object);
14512
14950
  if (isRawPayloadSource(
14513
14951
  obj,
14952
+ context,
14514
14953
  (candidate2) => localFileTextRef(candidate2, scope) !== null
14515
14954
  )) {
14516
14955
  const parent = node.parent;
@@ -14522,6 +14961,7 @@ var prefer_schema_for_api_payload_default = createRule({
14522
14961
  }
14523
14962
  const variable = obj?.type === import_utils78.AST_NODE_TYPES.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
14524
14963
  if (variable !== null && obj?.type === import_utils78.AST_NODE_TYPES.Identifier) {
14964
+ if (namedGuards.get(variable)?.some((call) => guardDominates(node, call))) return;
14525
14965
  if (isUseWithinValidatedBranch(node, obj.name)) {
14526
14966
  return;
14527
14967
  }
@@ -14547,7 +14987,7 @@ var PREFER_SHARED_ZOD_ENUM_DOCUMENTATION = {
14547
14987
  rationale: "Inline or repeated literal domains hide a reusable contract and allow equivalent fields to drift independently.",
14548
14988
  remediation: "Declare a module-level named Zod enum schema and reuse it at each field or contract site.",
14549
14989
  category: "maintainability",
14550
- limitations: ["Only direct z.enum calls with string-literal arrays are inspected; computed domains require review."],
14990
+ limitations: ["Only direct z.enum calls with string-literal arrays are inspected; computed domains require review. Equal values do not prove a shared business domain: retain local schemas when ownership, error customization, or future evolution differs, and review initialization order before extraction."],
14551
14991
  examples: [
14552
14992
  { id: "shared-provider", title: "Reuse a named enum schema", outcome: "no-match", files: [{ path: "src/provider.ts", source: "import { z } from 'zod'; const ProviderSchema = z.enum(['agy', 'claude', 'sol']); const JobSchema = z.object({ provider: ProviderSchema }); const StatusSchema = z.object({ provider: ProviderSchema.optional() });" }], focusPath: "src/provider.ts", expectedCount: 0, public: true },
14553
14993
  { id: "inline-provider", title: "Do not inline enum domains in object fields", outcome: "match", files: [{ path: "src/provider.ts", source: "import { z } from 'zod'; const JobSchema = z.object({ provider: z.enum(['agy', 'claude', 'sol']) });" }], focusPath: "src/provider.ts", expectedCount: 1, public: true }
@@ -14589,16 +15029,22 @@ var prefer_shared_zod_enum_default = createRule({
14589
15029
  create(context) {
14590
15030
  if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14591
15031
  const zodBindings = /* @__PURE__ */ new Set();
15032
+ const bindingOf = (node) => import_utils79.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
14592
15033
  const seen = /* @__PURE__ */ new Set();
14593
15034
  return {
14594
15035
  ImportDeclaration(node) {
14595
15036
  if (!isZodModule(node.source.value)) return;
14596
15037
  for (const specifier of node.specifiers) {
14597
- if (specifier.type === import_utils79.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils79.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") zodBindings.add(specifier.local.name);
15038
+ if (specifier.type === import_utils79.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils79.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
15039
+ const binding = bindingOf(specifier.local);
15040
+ if (binding !== null) zodBindings.add(binding);
15041
+ }
14598
15042
  }
14599
15043
  },
14600
15044
  CallExpression(node) {
14601
- if (node.callee.type !== import_utils79.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.object.type !== import_utils79.AST_NODE_TYPES.Identifier || !zodBindings.has(node.callee.object.name) || node.callee.property.type !== import_utils79.AST_NODE_TYPES.Identifier || node.callee.property.name !== "enum") return;
15045
+ if (node.callee.type !== import_utils79.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.object.type !== import_utils79.AST_NODE_TYPES.Identifier || node.callee.property.type !== import_utils79.AST_NODE_TYPES.Identifier || node.callee.property.name !== "enum") return;
15046
+ const binding = bindingOf(node.callee.object);
15047
+ if (binding === null || !zodBindings.has(binding)) return;
14602
15048
  const domain = literalDomain(node);
14603
15049
  if (domain === null) return;
14604
15050
  const key = JSON.stringify(domain);
@@ -15576,6 +16022,13 @@ var prefer_whole_object_assertion_default = createRule({
15576
16022
  return null;
15577
16023
  }
15578
16024
  const actual = expectCall.arguments[0];
16025
+ const variable = import_utils83.ASTUtils.findVariable(sourceCode.getScope(expectCall.callee), expectCall.callee.name);
16026
+ if (variable !== null && variable.defs.some((definition) => {
16027
+ if (definition.node.type !== import_utils83.AST_NODE_TYPES.ImportSpecifier) return true;
16028
+ const declaration = definition.node.parent;
16029
+ const imported = definition.node.imported;
16030
+ return declaration.type !== import_utils83.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === import_utils83.AST_NODE_TYPES.Identifier ? imported.name : imported.value) !== "expect";
16031
+ })) return null;
15579
16032
  if (actual === void 0 || actual.type !== import_utils83.AST_NODE_TYPES.MemberExpression || actual.optional) {
15580
16033
  return null;
15581
16034
  }
@@ -15737,12 +16190,12 @@ var prefer_whole_object_assertion_default = createRule({
15737
16190
  // src/rules/repeated-static-call-cases.ts
15738
16191
  var import_utils84 = require("@typescript-eslint/utils");
15739
16192
  var REPEATED_STATIC_CALL_CASES_DOCUMENTATION = {
15740
- summary: "Report three or more consecutive literal call assertions that should be independently named test cases.",
15741
- rationale: "Copy-pasted cases obscure the input table and stop later cases from being reported after the first failure.",
15742
- remediation: "Replace the repeated assertions with a named `test.each` or `it.each` table.",
16193
+ summary: "Review three or more consecutive static-input call assertions as potential named cases.",
16194
+ rationale: "Copy-pasted cases obscure the input table, and a thrown assertion can stop later cases from being reported.",
16195
+ remediation: "If the calls are independent, use the runner's named parameterized cases or subtests. Preserve ordered state-transition scenarios as one test.",
15743
16196
  category: "testing",
15744
16197
  filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
15745
- limitations: ["Only consecutive top-level assertions with direct calls and entirely static inputs and expected values are reported."],
16198
+ limitations: ["Only consecutive top-level assertions with direct calls and static inputs and expected values are checked. Test-local callees and fixtures are excluded; imported or outer functions can still be stateful, so manual independence review is required. Not every runner supports test.each."],
15746
16199
  examples: [
15747
16200
  { id: "parameterized", title: "Name each case", outcome: "no-match", files: [{ path: "src/parser.test.ts", source: "test.each([['a', true], ['b', false], ['c', true]])('parses %s', (input, expected) => { expect(parse(input)).toBe(expected); });" }], focusPath: "src/parser.test.ts", expectedCount: 0, public: true },
15748
16201
  { id: "repeated", title: "Do not repeat literal cases", outcome: "match", files: [{ path: "src/parser.test.ts", source: "test('parses', () => { expect(parse('a')).toBe(true); expect(parse('b')).toBe(false); expect(parse('c')).toBe(true); });" }], focusPath: "src/parser.test.ts", expectedCount: 1, public: true }
@@ -15823,7 +16276,7 @@ function staticShape(node) {
15823
16276
  return "dynamic";
15824
16277
  }
15825
16278
  }
15826
- function assertionShape(statement, context) {
16279
+ function assertionShape(statement, context, callback) {
15827
16280
  if (statement.type !== import_utils84.AST_NODE_TYPES.ExpressionStatement || statement.expression.type !== import_utils84.AST_NODE_TYPES.CallExpression) return null;
15828
16281
  const matcherCall = statement.expression;
15829
16282
  if (matcherCall.callee.type !== import_utils84.AST_NODE_TYPES.MemberExpression || matcherCall.callee.computed || matcherCall.callee.property.type !== import_utils84.AST_NODE_TYPES.Identifier || matcherCall.arguments.length !== 1) return null;
@@ -15835,6 +16288,10 @@ function assertionShape(statement, context) {
15835
16288
  const expected = matcherCall.arguments[0];
15836
16289
  if (observed?.type !== import_utils84.AST_NODE_TYPES.CallExpression || observed.callee.type !== import_utils84.AST_NODE_TYPES.Identifier || observed.arguments.length === 0 || observed.arguments.some((arg) => arg.type === import_utils84.AST_NODE_TYPES.SpreadElement || !isStatic(arg)) || expected?.type === import_utils84.AST_NODE_TYPES.SpreadElement || expected === void 0 || !isStatic(expected)) return null;
15837
16290
  const skeleton = `${observed.callee.name}/${observed.arguments.map((item) => staticShape(item)).join(",")}/${chain.modifiers.join(".")}/${matcher}/${staticShape(expected)}`;
16291
+ const binding = import_utils84.ASTUtils.findVariable(context.sourceCode.getScope(observed.callee), observed.callee.name);
16292
+ if (binding?.defs.some(
16293
+ (definition) => definition.node.range[0] >= callback.range[0] && definition.node.range[1] <= callback.range[1]
16294
+ )) return null;
15838
16295
  const values = [...observed.arguments, expected].map((item) => context.sourceCode.getText(item)).join("\0");
15839
16296
  return { statement, skeleton, values };
15840
16297
  }
@@ -15854,9 +16311,9 @@ var repeated_static_call_cases_default = createRule({
15854
16311
  documentation: REPEATED_STATIC_CALL_CASES_DOCUMENTATION,
15855
16312
  meta: {
15856
16313
  type: "suggestion",
15857
- docs: { description: "Report three or more consecutive literal call assertions that should be independently named test cases." },
16314
+ docs: { description: REPEATED_STATIC_CALL_CASES_DOCUMENTATION.summary },
15858
16315
  schema: [],
15859
- messages: { repeatedStaticCallCases: "These {{count}} consecutive assertions repeat the same call with static cases. Use a named `test.each` or `it.each` table so every case is independently reported." }
16316
+ messages: { repeatedStaticCallCases: "These {{count}} consecutive assertions repeat a call with static inputs. If independent, use named parameterized cases or subtests; preserve ordered scenarios as one test." }
15860
16317
  },
15861
16318
  defaultOptions: [],
15862
16319
  create(context) {
@@ -15891,7 +16348,7 @@ var repeated_static_call_cases_default = createRule({
15891
16348
  run = [];
15892
16349
  };
15893
16350
  for (const statement of node.body.body) {
15894
- const shape = assertionShape(statement, context);
16351
+ const shape = assertionShape(statement, context, node);
15895
16352
  if (shape === null || run.length > 0 && run[0]?.skeleton !== shape.skeleton) flush();
15896
16353
  if (shape !== null) run.push(shape);
15897
16354
  }
@@ -16910,10 +17367,12 @@ var require_fetch_timeout_default = createRule({
16910
17367
  var import_utils88 = require("@typescript-eslint/utils");
16911
17368
  var REQUIRE_INTERFACE_FOR_EXPORTED_CLASS_DOCUMENTATION = {
16912
17369
  summary: "Require exported concrete classes with public behavior to declare a contract.",
16913
- rationale: "Consumers coupled only to a concrete class cannot substitute implementations or state the supported public capability independently of implementation details.",
17370
+ rationale: "An explicit contract names the intended public capability separately from implementation details. TypeScript already supports structural compatibility; this is an architecture policy, not a prerequisite for substitution.",
16914
17371
  remediation: "Declare a focused interface and add an implements clause, or inherit from an intentional base contract.",
16915
17372
  category: "architecture",
16916
17373
  limitations: [
17374
+ "JavaScript files are excluded because implements is TypeScript syntax; imported framework base contracts still require manual policy review.",
17375
+ "An exported injected service may also receive require-port-for-service: its service-boundary error and this general exported-contract warning intentionally enforce distinct policy scopes.",
16917
17376
  "The warning-stage rule checks module-level class declarations and direct class-expression values exported directly, through local export specifiers, or through a default identifier; re-exports and expressions wrapped in other calls require review.",
16918
17377
  "An extends clause satisfies the contract only when its target is a locally declared abstract class; imported base-class contracts require an explicit implements clause.",
16919
17378
  "Static factories and data-only classes without public instance methods are outside the contract requirement."
@@ -16985,7 +17444,7 @@ var require_interface_for_exported_class_default = createRule({
16985
17444
  },
16986
17445
  defaultOptions: [],
16987
17446
  create(context) {
16988
- if (isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
17447
+ if (/\.(?:js|jsx|mjs|cjs)$/iu.test(context.filename) || isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
16989
17448
  return {
16990
17449
  "Program:exit"(program) {
16991
17450
  const classes = /* @__PURE__ */ new Map();
@@ -17646,12 +18105,13 @@ var MODEL_EXECUTION_METHODS = /* @__PURE__ */ new Set([
17646
18105
  var DATABASE_NAMES = /^(?:db|database|connection|pool|prisma|query|transaction|tx)$/iu;
17647
18106
  var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
17648
18107
  summary: "Keep SQL reads and writes inside a class that receives its database dependency.",
17649
- rationale: "Free-function database access hides connection ownership and makes transaction, retry, observability, and test boundaries inconsistent.",
18108
+ rationale: "An injected repository class is the preferred ownership boundary for database access under this architectural policy; free functions can also express explicit dependencies.",
17650
18109
  remediation: "Move the query into a repository or store class and inject the pool, connection, transaction, or typed database binding through its constructor.",
17651
18110
  category: "architecture",
17652
18111
  limitations: [
17653
18112
  "The rule recognizes conventional database receiver names, Cloudflare DB bindings, direct pool.query calls, explicit query-builder terminals, and Prisma-style model delegates; unusually named or heavily aliased clients require architectural review.",
17654
18113
  "Query construction without a recognized execution terminal is intentionally not reported.",
18114
+ "Stable local Map, WeakMap, and URLSearchParams instances are excluded. Other conventional receiver names are heuristics, not proof of a database API.",
17655
18115
  "Constructor injection inherited from a base class or transformed through a wrapper is not inferred by this syntax-only rule."
17656
18116
  ],
17657
18117
  examples: [
@@ -17857,11 +18317,23 @@ var require_sql_access_class_default = createRule({
17857
18317
  create(context) {
17858
18318
  if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text))
17859
18319
  return {};
18320
+ function knownNonDatabase(node, seen = /* @__PURE__ */ new Set()) {
18321
+ if (seen.has(node)) return false;
18322
+ seen.add(node);
18323
+ if (node.type === import_utils90.AST_NODE_TYPES.Identifier) {
18324
+ const binding = import_utils90.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
18325
+ if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
18326
+ const definition = binding.defs[0];
18327
+ return definition?.type === "Variable" && definition.node.init !== null && knownNonDatabase(definition.node.init, seen);
18328
+ }
18329
+ return node.type === import_utils90.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils90.AST_NODE_TYPES.Identifier && ["Map", "WeakMap", "URLSearchParams"].includes(node.callee.name) && (import_utils90.ASTUtils.findVariable(context.sourceCode.getScope(node.callee), node.callee.name)?.defs.length ?? 0) === 0;
18330
+ }
17860
18331
  return {
17861
18332
  CallExpression(node) {
17862
18333
  if (node.callee.type !== import_utils90.AST_NODE_TYPES.MemberExpression)
17863
18334
  return;
17864
18335
  const method = memberName6(node.callee);
18336
+ if (knownNonDatabase(node.callee.object)) return;
17865
18337
  if (method === null || !isDatabaseOperation(method, node.callee.object))
17866
18338
  return;
17867
18339
  const owner = owningClass2(node);
@@ -17882,6 +18354,8 @@ var REQUIRE_STATIC_NEXT_MATCHER_DOCUMENTATION = {
17882
18354
  rationale: "Next.js must statically analyze matcher values at build time; computed values are ignored.",
17883
18355
  remediation: "Write matcher strings, arrays, and object fields as literals in the exported config.",
17884
18356
  category: "correctness",
18357
+ limitations: ["Only directly exported matcher configuration in middleware/proxy entry files is inspected. Literal matcher validity is left to Next.js; this rule checks static syntax, not the complete framework schema."],
18358
+ references: ["https://nextjs.org/docs/app/api-reference/file-conventions/proxy"],
17885
18359
  examples: [
17886
18360
  { id: "literal-matcher", title: "Use a literal matcher", outcome: "no-match", files: [{ path: "src/middleware.ts", source: 'export const config = { matcher: "/api/:path*" };' }], focusPath: "src/middleware.ts", expectedCount: 0, public: true },
17887
18361
  { id: "computed-matcher", title: "Do not compute the matcher", outcome: "match", files: [{ path: "src/middleware.ts", source: 'const matcher = "/api/:path*"; export const config = { matcher };' }], focusPath: "src/middleware.ts", expectedCount: 1, public: true }
@@ -17929,7 +18403,7 @@ var require_static_next_matcher_default = createRule({
17929
18403
  },
17930
18404
  schema: [],
17931
18405
  messages: {
17932
- dynamicMatcher: "Next.js matcher values must contain only literal arrays and objects. Calls, identifiers, concatenation, interpolated templates, and spreads are not statically analyzable and fail the production build."
18406
+ dynamicMatcher: "Keep Next.js matcher strings, arrays and object fields literal in the exported config. Dynamic values such as variables are not supported by its build-time static analysis and can be ignored."
17933
18407
  }
17934
18408
  },
17935
18409
  defaultOptions: [],
@@ -18110,7 +18584,8 @@ var REQUIRE_ZOD_FORM_VALIDATION_DOCUMENTATION = {
18110
18584
  category: "security",
18111
18585
  limitations: [
18112
18586
  "Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.",
18113
- "Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer."
18587
+ "Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer.",
18588
+ "An enclosing parse does not validate an earlier call or deferred callback that consumes its input. Direct object/array construction, unshadowed Object.fromEntries and native Number/String/Boolean coercion remain supported; arbitrary preprocessing needs an explicitly reviewed boundary."
18114
18589
  ],
18115
18590
  examples: [
18116
18591
  { id: "validated-form-value", title: "Validate the form value", outcome: "no-match", files: [{ path: "src/action.ts", source: "const input = UserSchema.parse({ name: formData.get('name') });" }], focusPath: "src/action.ts", expectedCount: 0, public: true },
@@ -18225,6 +18700,15 @@ var require_zod_form_validation_default = createRule({
18225
18700
  let parent = node.parent;
18226
18701
  while (parent !== null && parent !== void 0) {
18227
18702
  if (isZodParseCall(parent)) return parent;
18703
+ if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils94.AST_NODE_TYPES.Identifier && ["Number", "String", "Boolean"].includes(parent.callee.name) && parent.arguments.length === 1 && (resolvedBinding(parent.callee)?.defs.length ?? 0) === 0) {
18704
+ parent = parent.parent;
18705
+ continue;
18706
+ }
18707
+ if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils94.AST_NODE_TYPES.MemberExpression && !parent.callee.computed && parent.callee.object.type === import_utils94.AST_NODE_TYPES.Identifier && parent.callee.object.name === "Object" && parent.callee.property.type === import_utils94.AST_NODE_TYPES.Identifier && parent.callee.property.name === "fromEntries" && (resolvedBinding(parent.callee.object)?.defs.length ?? 0) === 0) {
18708
+ parent = parent.parent;
18709
+ continue;
18710
+ }
18711
+ if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression || parent.type === import_utils94.AST_NODE_TYPES.NewExpression || parent.type === import_utils94.AST_NODE_TYPES.TaggedTemplateExpression || parent.type === import_utils94.AST_NODE_TYPES.ArrowFunctionExpression || parent.type === import_utils94.AST_NODE_TYPES.FunctionExpression || parent.type === import_utils94.AST_NODE_TYPES.FunctionDeclaration) return null;
18228
18712
  parent = parent.parent;
18229
18713
  }
18230
18714
  return null;
@@ -18404,13 +18888,14 @@ var require_zod_form_validation_default = createRule({
18404
18888
  // src/rules/store-insert-requires-on-conflict.ts
18405
18889
  var import_utils95 = require("@typescript-eslint/utils");
18406
18890
  var STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION = {
18407
- summary: "Require embedded inserts in explicitly replayable callables to carry conflict handling.",
18408
- rationale: "A callable named as an enqueue, seed, migration, schedule, ensure, or upsert promises replay safety.",
18409
- remediation: "Add an appropriate `ON CONFLICT` action or supported replay-safe insert form.",
18891
+ summary: "Review conflict handling for embedded inserts in replay-named callables.",
18892
+ rationale: "Names such as seed, enqueue, or upsert suggest that repeated execution deserves a conflict-policy review, but do not prove a replay contract.",
18893
+ remediation: "Choose conflict handling appropriate to the schema and SQL dialect, or document why this insertion must fail on a duplicate.",
18410
18894
  category: "correctness",
18895
+ limitations: ["Only the nearest statically named callable is considered; top-level inserts and anonymous callbacks are excluded. Recognized conflict syntax does not prove idempotence or concurrency safety: WHERE NOT EXISTS can race, and INSERT OR REPLACE can delete an existing row. Review unique constraints and dialect semantics manually."],
18411
18896
  examples: [
18412
- { id: "conflict-safe-insert", title: "Handle a replayed insert", outcome: "no-match", files: [{ path: "src/store.ts", source: "db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run();" }], focusPath: "src/store.ts", expectedCount: 0, public: true },
18413
- { id: "bare-insert", title: "Do not issue a replay-unsafe insert", outcome: "match", files: [{ path: "src/store.ts", source: "db.prepare(`INSERT INTO runs (id) VALUES (?)`).run();" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
18897
+ { id: "conflict-safe-insert", title: "Review the conflict policy for a replayed insert", outcome: "no-match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(); }" }], focusPath: "src/store.ts", expectedCount: 0, public: true },
18898
+ { id: "bare-insert", title: "Review a bare insert in a replay-named callable", outcome: "match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
18414
18899
  ]
18415
18900
  };
18416
18901
  var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
@@ -18422,14 +18907,18 @@ function owningCallableName(node) {
18422
18907
  return current.id?.name ?? null;
18423
18908
  }
18424
18909
  if (current.type === "MethodDefinition") {
18425
- return current.key.type === "Identifier" ? current.key.name : null;
18910
+ return !current.computed && current.key.type === "Identifier" ? current.key.name : null;
18426
18911
  }
18427
18912
  if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "VariableDeclarator" && current.parent.id.type === "Identifier") {
18428
18913
  return current.parent.id.name;
18429
18914
  }
18430
- if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && current.parent.key.type === "Identifier") {
18915
+ if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && !current.parent.computed && current.parent.key.type === "Identifier") {
18431
18916
  return current.parent.key.name;
18432
18917
  }
18918
+ if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") {
18919
+ const parent = current.parent;
18920
+ return parent.type === "MethodDefinition" && !parent.computed && parent.key.type === "Identifier" ? parent.key.name : null;
18921
+ }
18433
18922
  }
18434
18923
  return null;
18435
18924
  }
@@ -18440,11 +18929,11 @@ var store_insert_requires_on_conflict_default = createRule({
18440
18929
  meta: {
18441
18930
  type: "problem",
18442
18931
  docs: {
18443
- description: "Require embedded inserts in explicitly replayable callables to carry conflict handling."
18932
+ description: "Review conflict handling for embedded inserts in replay-named callables."
18444
18933
  },
18445
18934
  schema: [],
18446
18935
  messages: {
18447
- storeInsertRequiresOnConflict: "This INSERT is not replay-safe: a cron re-run or queue redelivery duplicates the row (or fails the handler on a unique-constraint violation). Add `ON CONFLICT (...) DO UPDATE` / `DO NOTHING` (or `INSERT OR IGNORE`)."
18936
+ storeInsertRequiresOnConflict: "This INSERT is inside a replay-named callable without recognized conflict handling. Review duplicate execution, unique constraints, and the appropriate conflict policy for your SQL dialect."
18448
18937
  }
18449
18938
  },
18450
18939
  defaultOptions: [],
@@ -18457,7 +18946,7 @@ var store_insert_requires_on_conflict_default = createRule({
18457
18946
  return;
18458
18947
  }
18459
18948
  const owner = owningCallableName(node);
18460
- if (owner !== null && !REPLAY_CONTRACT_NAME.test(owner)) {
18949
+ if (owner === null || !REPLAY_CONTRACT_NAME.test(owner)) {
18461
18950
  return;
18462
18951
  }
18463
18952
  context.report({ node, messageId: "storeInsertRequiresOnConflict" });
@@ -18470,15 +18959,13 @@ var import_utils96 = require("@typescript-eslint/utils");
18470
18959
  var STEPDOWN_DOCUMENTATION = {
18471
18960
  summary: "Place a private helper below its sole direct same-scope caller.",
18472
18961
  rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
18473
- remediation: "Move the private helper below its sole caller.",
18962
+ remediation: "Consider moving the private helper below its sole caller after reviewing initialization and reflection dependencies.",
18474
18963
  category: "maintainability",
18475
- autofix: "safe",
18476
18964
  limitations: [
18477
18965
  "Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.",
18478
- "Class helpers must be private; their sole caller may be public, protected, or private so the fix also satisfies accessibility ordering.",
18966
+ "Class helpers must be private; their sole caller may be public, protected, or private.",
18479
18967
  "Runtime class-field, static-block, computed-member, and decorator barriers are never crossed.",
18480
- "Autofix is limited to comment-free class-member boundaries; module functions and ambiguous comment ownership remain report-only.",
18481
- "Overlapping helper chains remain report-only so ESLint never leaves a partially reordered class after exhausting its fix-pass limit."
18968
+ "This rule is report-only: reordering methods can change reflective property order, and moving module declarations can change initialization behavior or introduce temporal-dead-zone failures. Review module cycles and eager callers manually."
18482
18969
  ],
18483
18970
  examples: [
18484
18971
  { id: "caller-before-helper", title: "Place the caller first", outcome: "no-match", files: [{ path: "src/run.ts", source: "function run() { return load(); }\nfunction load() { return 1; }" }], focusPath: "src/run.ts", expectedCount: 0, public: true },
@@ -18488,7 +18975,7 @@ var STEPDOWN_DOCUMENTATION = {
18488
18975
  function isFunction(node) {
18489
18976
  return node.type === import_utils96.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils96.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils96.AST_NODE_TYPES.FunctionExpression;
18490
18977
  }
18491
- function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true, makeFix) {
18978
+ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true) {
18492
18979
  const byName = new Map(scopeDefinitions.map((definition) => [definition.name, definition]));
18493
18980
  const cycles = cycleComponents(calls);
18494
18981
  const callers = /* @__PURE__ */ new Map();
@@ -18507,12 +18994,10 @@ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned,
18507
18994
  if (callerName2 === void 0 || cycles.has(helper.name) && cycles.get(helper.name) === cycles.get(callerName2)) continue;
18508
18995
  const caller = byName.get(callerName2);
18509
18996
  if (caller === void 0 || helper.node.range[0] >= caller.node.range[0] || !canMove(helper, caller)) continue;
18510
- const fix = makeFix?.(helper, caller);
18511
18997
  context.report({
18512
18998
  node: helper.node,
18513
18999
  messageId: "helperAboveOnlyCaller",
18514
- data: { helper: helper.name, caller: callerName2 },
18515
- ...fix === void 0 ? {} : { fix }
19000
+ data: { helper: helper.name, caller: callerName2 }
18516
19001
  });
18517
19002
  }
18518
19003
  }
@@ -18839,37 +19324,7 @@ function classScope(context, node, computedReferenceNames) {
18839
19324
  if (helperIndex === void 0 || callerIndex === void 0) return false;
18840
19325
  return runtimeBarrierPrefix[callerIndex + 1] === runtimeBarrierPrefix[helperIndex + 1];
18841
19326
  };
18842
- const incoming = /* @__PURE__ */ new Map();
18843
- for (const [caller, callees] of calls) {
18844
- for (const callee of callees) {
18845
- if (callee === caller) continue;
18846
- const callers = incoming.get(callee) ?? /* @__PURE__ */ new Set();
18847
- callers.add(caller);
18848
- incoming.set(callee, callers);
18849
- }
18850
- }
18851
- reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove, (helper, caller) => {
18852
- if (!canMove(helper, caller)) return void 0;
18853
- const helperCallsAnother = [...calls.get(helper.name) ?? []].some((callee) => callee !== helper.name);
18854
- const callerIsAnotherHelper = [...incoming.get(caller.name) ?? []].some((name) => name !== helper.name);
18855
- if (helperCallsAnother || callerIsAnotherHelper) return void 0;
18856
- const helperMember = helper.node;
18857
- const callerMember = caller.node;
18858
- const helperIndex = memberIndexes.get(helperMember);
18859
- if (helperIndex === void 0) return void 0;
18860
- const next = node.body.body[helperIndex + 1];
18861
- const suffixEnd = next?.range[0] ?? node.body.range[1] - 1;
18862
- const suffix = context.sourceCode.text.slice(helperMember.range[1], suffixEnd);
18863
- if (!/^\s*$/u.test(suffix)) return void 0;
18864
- const previous = node.body.body[helperIndex - 1];
18865
- const prefixStart = previous?.range[1] ?? node.body.range[0] + 1;
18866
- if (!/^\s*$/u.test(context.sourceCode.text.slice(prefixStart, helperMember.range[0]))) return void 0;
18867
- const helperText = context.sourceCode.getText(helperMember);
18868
- return (fixer) => [
18869
- fixer.removeRange([helperMember.range[0], suffixEnd]),
18870
- fixer.insertTextAfter(callerMember, `${suffix}${helperText}`)
18871
- ];
18872
- });
19327
+ reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove);
18873
19328
  }
18874
19329
  function isClassRuntimeBarrier(member) {
18875
19330
  switch (member.type) {
@@ -18891,7 +19346,6 @@ var stepdown_default = createRule({
18891
19346
  type: "suggestion",
18892
19347
  docs: { description: "Place a private helper below its sole direct same-scope caller." },
18893
19348
  schema: [],
18894
- fixable: "code",
18895
19349
  messages: {
18896
19350
  helperAboveOnlyCaller: "Private helper `{{helper}}` is defined above its only caller `{{caller}}`; move it below the caller."
18897
19351
  }
@@ -18964,7 +19418,7 @@ var SOURCE_COUPLED_TEST_DOCUMENTATION = {
18964
19418
  remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
18965
19419
  category: "testing",
18966
19420
  limitations: [
18967
- "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
19421
+ "The rule follows stable lexical bindings, static source paths, awaited reads, and common text operations. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
18968
19422
  "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
18969
19423
  ],
18970
19424
  examples: [
@@ -19003,6 +19457,11 @@ function stringValue(node) {
19003
19457
  const current = unwrap7(node);
19004
19458
  if (current.type === import_utils97.AST_NODE_TYPES.Literal && typeof current.value === "string") return current.value;
19005
19459
  if (current.type === import_utils97.AST_NODE_TYPES.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
19460
+ if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
19461
+ const left = stringValue(current.left);
19462
+ const right = stringValue(current.right);
19463
+ return left === null || right === null ? null : left + right;
19464
+ }
19006
19465
  return null;
19007
19466
  }
19008
19467
  function importSource(node) {
@@ -19032,14 +19491,19 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
19032
19491
  const scopes = [newScope()];
19033
19492
  const reportedOrigins = /* @__PURE__ */ new Set();
19034
19493
  const currentScope = () => scopes.at(-1) ?? scopes[0];
19035
- const visible = (kind, name2) => {
19494
+ const bindingOf = (node) => import_utils97.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
19495
+ const visible = (kind, node) => {
19496
+ const name2 = bindingOf(node);
19497
+ if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
19036
19498
  for (let index = scopes.length - 1; index >= 0; index--) {
19037
19499
  const scope = scopes[index];
19038
19500
  if (scope.declared.has(name2)) return scope[kind].has(name2);
19039
19501
  }
19040
19502
  return false;
19041
19503
  };
19042
- const visibleRawOrigins = (name2) => {
19504
+ const visibleRawOrigins = (node) => {
19505
+ const name2 = bindingOf(node);
19506
+ if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return /* @__PURE__ */ new Set();
19043
19507
  for (let index = scopes.length - 1; index >= 0; index--) {
19044
19508
  const scope = scopes[index];
19045
19509
  if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
@@ -19050,15 +19514,14 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
19050
19514
  const current = unwrap7(node);
19051
19515
  const value = stringValue(current);
19052
19516
  if (value !== null) return sourceSuffixRe.test(value);
19053
- if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visible("paths", current.name);
19054
- if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
19055
- return sourcePath(current.left) || sourcePath(current.right);
19056
- }
19057
- if (current.type === import_utils97.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
19517
+ if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visible("paths", current);
19058
19518
  if (current.type === import_utils97.AST_NODE_TYPES.CallExpression || current.type === import_utils97.AST_NODE_TYPES.NewExpression) {
19059
- return current.arguments.some((argument) => argument.type !== import_utils97.AST_NODE_TYPES.SpreadElement && sourcePath(argument));
19519
+ const callee = current.callee;
19520
+ const first = current.arguments[0];
19521
+ if (first === void 0 || first.type === import_utils97.AST_NODE_TYPES.SpreadElement) return false;
19522
+ if (current.type === import_utils97.AST_NODE_TYPES.NewExpression && callee.type === import_utils97.AST_NODE_TYPES.Identifier && callee.name === "URL" && (bindingOf(callee)?.defs.length ?? 0) === 0) return sourcePath(first);
19523
+ if (callee.type === import_utils97.AST_NODE_TYPES.Identifier && bindingOf(callee)?.defs.some((definition) => definition.node.type === import_utils97.AST_NODE_TYPES.ImportSpecifier && definition.node.imported.type === import_utils97.AST_NODE_TYPES.Identifier && definition.node.imported.name === "fileURLToPath" && definition.node.parent.type === import_utils97.AST_NODE_TYPES.ImportDeclaration && ["node:url", "url"].includes(String(definition.node.parent.source.value)))) return sourcePath(first);
19060
19524
  }
19061
- if (current.type === import_utils97.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
19062
19525
  return false;
19063
19526
  };
19064
19527
  const rawRead = (node) => {
@@ -19066,16 +19529,16 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
19066
19529
  if (current.type !== import_utils97.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
19067
19530
  const callee = unwrap7(current.callee);
19068
19531
  if (callee.type === import_utils97.AST_NODE_TYPES.Identifier) {
19069
- return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
19532
+ return visible("fsReaders", callee) && sourcePath(current.arguments[0]);
19070
19533
  }
19071
19534
  if (callee.type !== import_utils97.AST_NODE_TYPES.MemberExpression) return false;
19072
19535
  const name2 = staticMemberName7(callee);
19073
19536
  const object = unwrap7(callee.object);
19074
- return name2 !== null && FS_READERS.has(name2) && object.type === import_utils97.AST_NODE_TYPES.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
19537
+ return name2 !== null && FS_READERS.has(name2) && object.type === import_utils97.AST_NODE_TYPES.Identifier && visible("fsObjects", object) && sourcePath(current.arguments[0]);
19075
19538
  };
19076
19539
  const rawOrigins = (node) => {
19077
19540
  const current = unwrap7(node);
19078
- if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current.name);
19541
+ if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current);
19079
19542
  if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
19080
19543
  if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
19081
19544
  if (current.type === import_utils97.AST_NODE_TYPES.MemberExpression && staticMemberName7(current) === "length") return rawOrigins(current.object);
@@ -19116,14 +19579,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
19116
19579
  if (receiver.type !== import_utils97.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
19117
19580
  return new Set(node.arguments.flatMap((argument) => argument.type === import_utils97.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
19118
19581
  };
19119
- const rawRegexExtractionOrigins = (node) => {
19120
- const callee = unwrap7(node.callee);
19121
- if (callee.type !== import_utils97.AST_NODE_TYPES.MemberExpression || staticMemberName7(callee) !== "matchAll" || node.arguments.length !== 1) return /* @__PURE__ */ new Set();
19122
- const argument = node.arguments[0];
19123
- if (argument?.type !== import_utils97.AST_NODE_TYPES.Literal || !(argument.value instanceof RegExp)) return /* @__PURE__ */ new Set();
19124
- return rawOrigins(callee.object);
19125
- };
19126
- const declare = (name2, state) => {
19582
+ const declare = (node, state) => {
19583
+ const name2 = bindingOf(node);
19584
+ if (name2 === null) return;
19127
19585
  const scope = currentScope();
19128
19586
  scope.declared.add(name2);
19129
19587
  scope.collections.delete(name2);
@@ -19143,18 +19601,8 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
19143
19601
  const current = unwrap7(node);
19144
19602
  return current.type === import_utils97.AST_NODE_TYPES.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== import_utils97.AST_NODE_TYPES.SpreadElement && sourcePath(element));
19145
19603
  };
19146
- const declaredNames2 = (node) => {
19147
- const current = unwrap7(node);
19148
- if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return [current.name];
19149
- if (current.type === import_utils97.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
19150
- if (current.type === import_utils97.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
19151
- if (current.type === import_utils97.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
19152
- if (current.type === import_utils97.AST_NODE_TYPES.ObjectPattern) return current.properties.flatMap((property) => property.type === import_utils97.AST_NODE_TYPES.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
19153
- return [];
19154
- };
19155
- const enterFunction = (node) => {
19604
+ const enterFunction = () => {
19156
19605
  scopes.push(newScope());
19157
- for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
19158
19606
  };
19159
19607
  const exitFunction = () => {
19160
19608
  scopes.pop();
@@ -19166,9 +19614,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
19166
19614
  for (const specifier of node.specifiers) {
19167
19615
  if (specifier.type === import_utils97.AST_NODE_TYPES.ImportSpecifier) {
19168
19616
  const imported = specifier.imported.type === import_utils97.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
19169
- if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
19617
+ if (FS_READERS.has(imported)) declare(specifier.local, { fsReader: true });
19170
19618
  } else {
19171
- declare(specifier.local.name, { fsObject: true });
19619
+ declare(specifier.local, { fsObject: true });
19172
19620
  }
19173
19621
  }
19174
19622
  },
@@ -19177,35 +19625,34 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
19177
19625
  VariableDeclarator(node) {
19178
19626
  if (node.init === null) return;
19179
19627
  const required = requireSource(node.init);
19628
+ const initializer = unwrap7(node.init);
19629
+ if (required !== null && initializer.type === import_utils97.AST_NODE_TYPES.CallExpression && initializer.callee.type === import_utils97.AST_NODE_TYPES.Identifier && (bindingOf(initializer.callee)?.defs.length ?? 0) > 0) return;
19180
19630
  if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils97.AST_NODE_TYPES.Identifier) {
19181
- declare(node.id.name, { fsObject: true });
19631
+ declare(node.id, { fsObject: true });
19182
19632
  return;
19183
19633
  }
19184
19634
  if (node.id.type === import_utils97.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
19185
19635
  for (const property of node.id.properties) {
19186
19636
  if (property.type !== import_utils97.AST_NODE_TYPES.Property || property.value.type !== import_utils97.AST_NODE_TYPES.Identifier) continue;
19187
19637
  const key = property.key.type === import_utils97.AST_NODE_TYPES.Identifier ? property.key.name : property.key.type === import_utils97.AST_NODE_TYPES.Literal ? String(property.key.value) : "";
19188
- if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
19638
+ if (FS_READERS.has(key)) declare(property.value, { fsReader: true });
19189
19639
  }
19190
19640
  return;
19191
19641
  }
19192
19642
  if (node.id.type !== import_utils97.AST_NODE_TYPES.Identifier) return;
19193
- declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
19643
+ declare(node.id, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
19194
19644
  },
19195
19645
  AssignmentExpression(node) {
19196
- if (node.left.type === import_utils97.AST_NODE_TYPES.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
19646
+ if (node.left.type === import_utils97.AST_NODE_TYPES.Identifier) declare(node.left, {});
19197
19647
  },
19198
19648
  ForOfStatement(node) {
19199
19649
  const right = unwrap7(node.right);
19200
- const collection = right.type === import_utils97.AST_NODE_TYPES.Identifier && visible("collections", right.name);
19650
+ const collection = right.type === import_utils97.AST_NODE_TYPES.Identifier && visible("collections", right);
19201
19651
  const left = node.left.type === import_utils97.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
19202
- if (collection && left?.type === import_utils97.AST_NODE_TYPES.Identifier) declare(left.name, { path: true });
19652
+ if (collection && left?.type === import_utils97.AST_NODE_TYPES.Identifier) declare(left, { path: true });
19203
19653
  },
19204
19654
  CallExpression(node) {
19205
- const origins = /* @__PURE__ */ new Set([
19206
- ...rawAssertionOrigins(node),
19207
- ...rawRegexExtractionOrigins(node)
19208
- ]);
19655
+ const origins = rawAssertionOrigins(node);
19209
19656
  if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
19210
19657
  for (const origin of origins) reportedOrigins.add(origin);
19211
19658
  context.report({ node, messageId: "rawSourceOracle" });
@@ -19348,8 +19795,8 @@ var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
19348
19795
  remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
19349
19796
  category: "testing",
19350
19797
  limitations: [
19351
- "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
19352
- "The warning-stage rule remains suppressible for calibration; promotion may make the locked policy non-suppressible."
19798
+ "The rule follows stable lexical bindings and static source paths. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
19799
+ "When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
19353
19800
  ],
19354
19801
  examples: [
19355
19802
  {
@@ -19824,7 +20271,7 @@ var RULES = {
19824
20271
  };
19825
20272
  var meta = {
19826
20273
  name: "@sarj/eslint-plugin",
19827
- version: "15.17.10"
20274
+ version: "15.17.11"
19828
20275
  };
19829
20276
  var APPLICATION_ONLY_RULES = [];
19830
20277
  var LIBRARY_IMPORT_POLICY = ["error", {