@sarj/eslint-plugin 15.3.0 → 15.5.0

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
@@ -104,6 +104,7 @@ ${missing.join("\n")}`);
104
104
  function publicExample(example) {
105
105
  return {
106
106
  id: example.id,
107
+ scenarioId: example.scenarioId ?? "primary",
107
108
  title: example.title,
108
109
  outcome: example.outcome,
109
110
  files: example.files.map(publicFile),
@@ -148,9 +149,14 @@ function nativeSpec(config, documentation) {
148
149
  const examples = [...documentation.examples ?? []];
149
150
  examples.forEach(validateExample);
150
151
  assertUnique(examples.map((example) => example.id), "rule example IDs");
151
- const publicOutcomes = new Set(examples.filter((example) => example.public === true).map((example) => example.outcome));
152
- if (publicOutcomes.size > 0 && !(publicOutcomes.has("match") && publicOutcomes.has("no-match"))) {
153
- throw new TypeError("published rule examples must include matching and non-matching cases");
152
+ const publicExamples = examples.filter((example) => example.public === true);
153
+ const scenarios = new Set(publicExamples.map((example) => example.scenarioId ?? "primary"));
154
+ for (const scenario of scenarios) {
155
+ const pair = publicExamples.filter((example) => (example.scenarioId ?? "primary") === scenario);
156
+ const outcomes = new Set(pair.map((example) => example.outcome));
157
+ if (pair.length !== 2 || !outcomes.has("match") || !outcomes.has("no-match")) {
158
+ throw new TypeError(`published example scenario ${scenario} must contain both matching and non-matching cases exactly once`);
159
+ }
154
160
  }
155
161
  const messageIds = Object.keys(meta2.messages).sort();
156
162
  const schema = optionsSchema(meta2.schema);
@@ -171,7 +177,7 @@ function nativeSpec(config, documentation) {
171
177
  references,
172
178
  since: documentation.since ?? null,
173
179
  examples,
174
- publicExamples: examples.filter((example) => example.public === true),
180
+ publicExamples,
175
181
  messageIds,
176
182
  optionsSchema: schema
177
183
  };
@@ -191,6 +197,9 @@ function validateExample(example) {
191
197
  if (!KEBAB_CASE.test(example.id)) {
192
198
  throw new TypeError("example ID must be lowercase kebab-case");
193
199
  }
200
+ if (!KEBAB_CASE.test(example.scenarioId ?? "primary")) {
201
+ throw new TypeError("example scenario must be lowercase kebab-case");
202
+ }
194
203
  if (example.title.trim().length === 0) {
195
204
  throw new TypeError("example title must not be empty");
196
205
  }
@@ -598,6 +607,9 @@ function isAssertionStatement(statement) {
598
607
  const expression = statement.expression;
599
608
  return expression.type === import_utils3.AST_NODE_TYPES.CallExpression && isAssertionCall(expression);
600
609
  }
610
+ function isTypeOnlyContractStatement(statement) {
611
+ return statement.type === import_utils3.AST_NODE_TYPES.TSTypeAliasDeclaration || statement.type === import_utils3.AST_NODE_TYPES.TSInterfaceDeclaration;
612
+ }
601
613
  function normalizedLiteral(node) {
602
614
  if ("regex" in node) {
603
615
  return ["Literal", "regex", node.regex.pattern, node.regex.flags];
@@ -655,7 +667,7 @@ var duplicate_test_body_default = createRule({
655
667
  return;
656
668
  }
657
669
  const body2 = candidate.body;
658
- if (body2.body.type !== import_utils3.AST_NODE_TYPES.BlockStatement || body2.body.body.length < MIN_STATEMENTS || body2.body.body.every(isAssertionStatement)) {
670
+ if (body2.body.type !== import_utils3.AST_NODE_TYPES.BlockStatement || body2.body.body.length < MIN_STATEMENTS || body2.body.body.every(isAssertionStatement) || body2.body.body.some(isTypeOnlyContractStatement)) {
659
671
  return;
660
672
  }
661
673
  const comments = context.sourceCode.getCommentsInside(body2.body).map((comment) => [comment.type, comment.value]);
@@ -3550,7 +3562,11 @@ var NUMBER_METHODS = /* @__PURE__ */ new Set([
3550
3562
  "lt",
3551
3563
  "lte",
3552
3564
  "max",
3553
- "min"
3565
+ "min",
3566
+ "negative",
3567
+ "nonnegative",
3568
+ "nonpositive",
3569
+ "positive"
3554
3570
  ]);
3555
3571
  var LENGTH_METHODS = /* @__PURE__ */ new Set(["length", "max", "min"]);
3556
3572
  var RESHAPING_METHODS = /* @__PURE__ */ new Set([
@@ -3687,13 +3703,29 @@ var no_impossible_zod_literal_bounds_default = createRule({
3687
3703
  let upper = null;
3688
3704
  for (const { method, node } of chain.calls) {
3689
3705
  if (!allowed.has(method)) return null;
3690
- const value = finiteNumber(node.arguments[0]);
3706
+ const semantic = method === "positive" ? { exclusive: true, lower: true, value: 0 } : method === "nonnegative" ? { exclusive: false, lower: true, value: 0 } : method === "negative" ? { exclusive: true, lower: false, value: 0 } : method === "nonpositive" ? { exclusive: false, lower: false, value: 0 } : null;
3707
+ const value = semantic?.value ?? finiteNumber(node.arguments[0]);
3691
3708
  if (value === null) return null;
3692
3709
  if (chain.kind !== "number" && (!Number.isInteger(value) || value < 0)) {
3693
3710
  return null;
3694
3711
  }
3695
3712
  const label = `${method}(${String(value)})`;
3696
- if (method === "length") {
3713
+ if (semantic !== null) {
3714
+ if (node.arguments.length !== 0) return null;
3715
+ if (semantic.lower) {
3716
+ lower = strongerLower(lower, {
3717
+ exclusive: semantic.exclusive,
3718
+ label: `${method}()`,
3719
+ value
3720
+ });
3721
+ } else {
3722
+ upper = strongerUpper(upper, {
3723
+ exclusive: semantic.exclusive,
3724
+ label: `${method}()`,
3725
+ value
3726
+ });
3727
+ }
3728
+ } else if (method === "length") {
3697
3729
  lower = strongerLower(lower, { exclusive: false, label, value });
3698
3730
  upper = strongerUpper(upper, { exclusive: false, label, value });
3699
3731
  } else if (method === "gt" || method === "gte" || method === "min") {
@@ -6806,6 +6838,13 @@ function nearestEnclosingFunction(node) {
6806
6838
  }
6807
6839
  return null;
6808
6840
  }
6841
+ function isImmediatelyConsumedSleep(node) {
6842
+ const parent = node.parent;
6843
+ if (parent?.type === import_utils34.AST_NODE_TYPES.AwaitExpression || parent?.type === import_utils34.AST_NODE_TYPES.ReturnStatement || parent?.type === import_utils34.AST_NODE_TYPES.ExpressionStatement) {
6844
+ return true;
6845
+ }
6846
+ return parent?.type === import_utils34.AST_NODE_TYPES.ArrowFunctionExpression && parent.body === node;
6847
+ }
6809
6848
  function isTestBody(fn) {
6810
6849
  const call = fn.parent;
6811
6850
  if (call?.type !== import_utils34.AST_NODE_TYPES.CallExpression || !call.arguments.some((argument) => argument === fn)) {
@@ -6848,6 +6887,9 @@ var no_sleep_in_test_body_default = createRule({
6848
6887
  return {};
6849
6888
  }
6850
6889
  const report = (node) => {
6890
+ if (!isImmediatelyConsumedSleep(node)) {
6891
+ return;
6892
+ }
6851
6893
  const enclosing = nearestEnclosingFunction(node);
6852
6894
  if (enclosing === null || !isTestBody(enclosing)) {
6853
6895
  return;
@@ -7145,6 +7187,27 @@ function isSmallStaticForLoop(node) {
7145
7187
  const iterations = node.test.right.value - declaration.init.value + (node.test.operator === "<=" ? 1 : 0);
7146
7188
  return iterations >= 0 && iterations <= 8;
7147
7189
  }
7190
+ function isStringSeededReduce(node) {
7191
+ if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "reduce" || node.arguments.length !== 2) {
7192
+ return false;
7193
+ }
7194
+ const [callback, initial] = node.arguments;
7195
+ if (node.callee.object.type === "ArrayExpression" && node.callee.object.elements.length <= 8) {
7196
+ return false;
7197
+ }
7198
+ if (callback === void 0 || initial === void 0 || callback.type === "SpreadElement" || initial.type === "SpreadElement" || callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression" || callback.params[0]?.type !== "Identifier" || !isStringLiteralInit(initial)) {
7199
+ return false;
7200
+ }
7201
+ const accumulator = callback.params[0].name;
7202
+ if (callback.body.type !== "BlockStatement") {
7203
+ return isConcatOntoTarget(callback.body, accumulator);
7204
+ }
7205
+ if (callback.body.body.length !== 1 || callback.body.body[0]?.type !== "ReturnStatement") {
7206
+ return false;
7207
+ }
7208
+ const returned = callback.body.body[0].argument;
7209
+ return returned !== null && isConcatOntoTarget(returned, accumulator);
7210
+ }
7148
7211
  var no_string_concat_in_loop_default = createRule({
7149
7212
  name: "no-string-concat-in-loop",
7150
7213
  documentation: noStringConcatInLoopDocumentation,
@@ -7155,7 +7218,8 @@ var no_string_concat_in_loop_default = createRule({
7155
7218
  },
7156
7219
  schema: [],
7157
7220
  messages: {
7158
- noStringConcatInLoop: 'Avoid building a string with `+=` inside a loop \u2014 this is O(n^2). Push the parts onto an array and use `arr.join("")` after the loop.'
7221
+ noStringConcatInLoop: 'Avoid building a string with `+=` inside a loop \u2014 this is O(n^2). Push the parts onto an array and use `arr.join("")` after the loop.',
7222
+ noStringReduce: "Avoid concatenating a growing string in `reduce` \u2014 this is O(n^2). Map the fragments and join them once instead."
7159
7223
  }
7160
7224
  },
7161
7225
  defaultOptions: [],
@@ -7165,6 +7229,11 @@ var no_string_concat_in_loop_default = createRule({
7165
7229
  }
7166
7230
  const reported = /* @__PURE__ */ new WeakMap();
7167
7231
  return {
7232
+ CallExpression(node) {
7233
+ if (isStringSeededReduce(node)) {
7234
+ context.report({ node, messageId: "noStringReduce" });
7235
+ }
7236
+ },
7168
7237
  AssignmentExpression(node) {
7169
7238
  if (node.left.type !== "Identifier") {
7170
7239
  return;
@@ -7269,6 +7338,9 @@ function isLiteral(node) {
7269
7338
  return false;
7270
7339
  }
7271
7340
  }
7341
+ function isStructuralLiteral(node) {
7342
+ return node.type === import_utils37.AST_NODE_TYPES.ArrayExpression || node.type === import_utils37.AST_NODE_TYPES.ObjectExpression;
7343
+ }
7272
7344
  function expectOperand(callee) {
7273
7345
  const receiver = callee.object;
7274
7346
  if (receiver.type !== import_utils37.AST_NODE_TYPES.CallExpression || receiver.callee.type !== import_utils37.AST_NODE_TYPES.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
@@ -7325,6 +7397,9 @@ var no_tautological_expect_default = createRule({
7325
7397
  if (!EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === void 0 || !isLiteral(expected)) {
7326
7398
  return;
7327
7399
  }
7400
+ if (matcher === "toBe" && (isStructuralLiteral(operand) || isStructuralLiteral(expected))) {
7401
+ return;
7402
+ }
7328
7403
  if (context.sourceCode.getText(operand) !== context.sourceCode.getText(expected)) {
7329
7404
  return;
7330
7405
  }
@@ -7838,7 +7913,13 @@ var no_declaration_comment_wall_default = createRule({
7838
7913
  const lead = ownsItsLine ? endingOn.get(member.loc.start.line - 1) : void 0;
7839
7914
  if (lead !== void 0) {
7840
7915
  const before = sourceCode.getTokenBefore(lead, { includeComments: false });
7841
- if (before === null || before.loc.end.line < lead.loc.start.line) return lead;
7916
+ if (before === null || before.loc.end.line < lead.loc.start.line) {
7917
+ const previousLine = endingOn.get(lead.loc.start.line - 1);
7918
+ if (lead.type === import_utils40.AST_TOKEN_TYPES.Line && previousLine?.type === import_utils40.AST_TOKEN_TYPES.Line && previousLine.loc.start.column === lead.loc.start.column) {
7919
+ return void 0;
7920
+ }
7921
+ return lead;
7922
+ }
7842
7923
  }
7843
7924
  const trail = startingOn.get(member.loc.end.line);
7844
7925
  return trail !== void 0 && trail.range[0] > member.range[0] ? trail : void 0;
@@ -8140,7 +8221,13 @@ var no_type_member_comment_wall_default = createRule({
8140
8221
  const lead = ownsItsLine ? endingOn.get(member.loc.start.line - 1) : void 0;
8141
8222
  if (lead !== void 0) {
8142
8223
  const before = sourceCode.getTokenBefore(lead, { includeComments: false });
8143
- if (before === null || before.loc.end.line < lead.loc.start.line) return lead;
8224
+ if (before === null || before.loc.end.line < lead.loc.start.line) {
8225
+ const previousLine = endingOn.get(lead.loc.start.line - 1);
8226
+ if (lead.type === import_utils42.AST_TOKEN_TYPES.Line && previousLine?.type === import_utils42.AST_TOKEN_TYPES.Line && previousLine.loc.start.column === lead.loc.start.column) {
8227
+ return void 0;
8228
+ }
8229
+ return lead;
8230
+ }
8144
8231
  }
8145
8232
  const trail = startingOn.get(member.loc.end.line);
8146
8233
  return trail !== void 0 && trail.range[0] > member.range[0] ? trail : void 0;
@@ -8437,6 +8524,10 @@ var no_unnecessary_use_client_default = createRule({
8437
8524
  var import_utils44 = require("@typescript-eslint/utils");
8438
8525
  var MOCK_TYPE_NAMES = /* @__PURE__ */ new Set([
8439
8526
  "Mock",
8527
+ "Mocked",
8528
+ "MockedClass",
8529
+ "MockedFunction",
8530
+ "MockedObject",
8440
8531
  "MockInstance",
8441
8532
  "SpyInstance"
8442
8533
  ]);
@@ -8680,9 +8771,9 @@ var no_zod_native_enum_default = createRule({
8680
8771
  }
8681
8772
  function isZodMemberCall(node, api) {
8682
8773
  const callee = node.callee;
8683
- if (callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils45.AST_NODE_TYPES.Identifier && callee.property.type === import_utils45.AST_NODE_TYPES.Identifier) {
8774
+ if (callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && callee.object.type === import_utils45.AST_NODE_TYPES.Identifier && (callee.property.type === import_utils45.AST_NODE_TYPES.Identifier && !callee.computed && callee.property.name === api || callee.computed && callee.property.type === import_utils45.AST_NODE_TYPES.Literal && callee.property.value === api)) {
8684
8775
  const binding = resolvedBinding(callee.object);
8685
- return binding !== null && zodNamespaceBindings.has(binding) && callee.property.name === api;
8776
+ return binding !== null && zodNamespaceBindings.has(binding);
8686
8777
  }
8687
8778
  if (callee.type === import_utils45.AST_NODE_TYPES.Identifier) {
8688
8779
  const binding = resolvedBinding(callee);
@@ -9526,6 +9617,27 @@ var prefer_immutable_module_constant_default = createRule({
9526
9617
  }
9527
9618
  const exportedNames2 = /* @__PURE__ */ new Set();
9528
9619
  const typeAliases2 = /* @__PURE__ */ new Map();
9620
+ const mutatesThroughConstAlias = (root) => {
9621
+ const pending = [root];
9622
+ const seen = /* @__PURE__ */ new Set();
9623
+ while (pending.length > 0) {
9624
+ const variable = pending.pop();
9625
+ if (variable === void 0 || seen.has(variable)) continue;
9626
+ seen.add(variable);
9627
+ for (const reference of variable.references) {
9628
+ const identifier = reference.identifier;
9629
+ if (identifier.type !== import_utils51.AST_NODE_TYPES.Identifier) continue;
9630
+ if (referenceMutates(identifier, isUnshadowedGlobal)) return true;
9631
+ const declarator = identifier.parent;
9632
+ if (declarator.type !== import_utils51.AST_NODE_TYPES.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== import_utils51.AST_NODE_TYPES.Identifier || declarator.parent.type !== import_utils51.AST_NODE_TYPES.VariableDeclaration || declarator.parent.kind !== "const") {
9633
+ continue;
9634
+ }
9635
+ const alias = sourceCode.getDeclaredVariables(declarator)[0];
9636
+ if (alias !== void 0) pending.push(alias);
9637
+ }
9638
+ }
9639
+ return false;
9640
+ };
9529
9641
  return {
9530
9642
  Program(node) {
9531
9643
  for (const statement of node.body) {
@@ -9566,9 +9678,7 @@ var prefer_immutable_module_constant_default = createRule({
9566
9678
  return;
9567
9679
  }
9568
9680
  const variable = sourceCode.getDeclaredVariables(node)[0];
9569
- if (!directlyExported && !exportedNames2.has(node.id.name) && variable?.references.some(
9570
- (reference) => reference.identifier.type === import_utils51.AST_NODE_TYPES.Identifier && referenceMutates(reference.identifier, isUnshadowedGlobal)
9571
- ) === true) {
9681
+ if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughConstAlias(variable)) {
9572
9682
  return;
9573
9683
  }
9574
9684
  context.report({
@@ -10347,6 +10457,10 @@ var prefer_module_level_schema_default = createRule({
10347
10457
  }
10348
10458
  for (const definition of resolved.defs) {
10349
10459
  if (definition.type === "ImportBinding") {
10460
+ const parent = reference.identifier.parent;
10461
+ if (parent?.type === import_utils54.AST_NODE_TYPES.CallExpression && parent.callee === reference.identifier && !zodNamespaces.has(reference.identifier.name)) {
10462
+ return false;
10463
+ }
10350
10464
  continue;
10351
10465
  }
10352
10466
  if (definition.node.type === import_utils54.AST_NODE_TYPES.VariableDeclarator && definition.node.parent.type === import_utils54.AST_NODE_TYPES.VariableDeclaration && definition.node.parent.kind !== "const") {
@@ -12178,10 +12292,7 @@ var SHAPE_PRESERVING_METHODS = /* @__PURE__ */ new Set([
12178
12292
  ]);
12179
12293
  var OPTIONAL_MODIFIERS = /* @__PURE__ */ new Set([
12180
12294
  "optional",
12181
- "nullish",
12182
- "default",
12183
- "prefault",
12184
- "catch"
12295
+ "nullish"
12185
12296
  ]);
12186
12297
  var NULLABLE_MODIFIERS = /* @__PURE__ */ new Set(["nullable", "nullish"]);
12187
12298
  var RESHAPING_MODIFIERS = /* @__PURE__ */ new Set([
@@ -13450,6 +13561,7 @@ var ZOD_PARSE_METHODS = /* @__PURE__ */ new Set([
13450
13561
  "parseAsync",
13451
13562
  "safeParseAsync"
13452
13563
  ]);
13564
+ var FORM_VALUE_METHODS = /* @__PURE__ */ new Set(["get", "getAll"]);
13453
13565
  var zodReceiverRoot = (node) => {
13454
13566
  let current = node;
13455
13567
  while (true) {
@@ -13524,7 +13636,7 @@ var require_zod_form_validation_default = createRule({
13524
13636
  };
13525
13637
  const isFormSourceIdentifier = (node) => {
13526
13638
  if (node.type !== import_utils66.AST_NODE_TYPES.Identifier) return false;
13527
- if (/formdata/i.test(node.name)) return true;
13639
+ const conventionalName = /formdata/i.test(node.name);
13528
13640
  let scope = context.sourceCode.getScope(node);
13529
13641
  while (scope !== null) {
13530
13642
  const variable = scope.set.get(node.name);
@@ -13533,16 +13645,16 @@ var require_zod_form_validation_default = createRule({
13533
13645
  if (def !== void 0 && def.type === "Variable" && def.node.type === import_utils66.AST_NODE_TYPES.VariableDeclarator && def.node.init !== null) {
13534
13646
  return isFormDataMethodCall(def.node.init);
13535
13647
  }
13536
- return false;
13648
+ return def?.type === "Parameter" && conventionalName;
13537
13649
  }
13538
13650
  scope = scope.upper;
13539
13651
  }
13540
- return false;
13652
+ return conventionalName;
13541
13653
  };
13542
13654
  const isFormDataGetCall = (node) => {
13543
13655
  const callee = node.callee;
13544
13656
  if (callee.type !== import_utils66.AST_NODE_TYPES.MemberExpression) return false;
13545
- if (callee.property.type !== import_utils66.AST_NODE_TYPES.Identifier || callee.property.name !== "get") {
13657
+ if (callee.property.type !== import_utils66.AST_NODE_TYPES.Identifier || !FORM_VALUE_METHODS.has(callee.property.name)) {
13546
13658
  return false;
13547
13659
  }
13548
13660
  return isFormSourceIdentifier(callee.object);
@@ -13636,6 +13748,40 @@ var require_zod_form_validation_default = createRule({
13636
13748
  }
13637
13749
  return ["===", "!==", "==", "!="].includes(parent.operator) && (parent.right.type === import_utils66.AST_NODE_TYPES.Literal && parent.right.value === null || parent.right.type === import_utils66.AST_NODE_TYPES.Identifier && parent.right.name === "undefined");
13638
13750
  };
13751
+ const isDescendantOf = (node, ancestor) => {
13752
+ let current = node;
13753
+ while (current !== void 0 && current !== null) {
13754
+ if (current === ancestor) return true;
13755
+ current = current.parent;
13756
+ }
13757
+ return false;
13758
+ };
13759
+ const blockTerminates = (node) => {
13760
+ if (node.type === import_utils66.AST_NODE_TYPES.ReturnStatement || node.type === import_utils66.AST_NODE_TYPES.ThrowStatement) {
13761
+ return true;
13762
+ }
13763
+ if (node.type !== import_utils66.AST_NODE_TYPES.BlockStatement || node.body.length === 0) return false;
13764
+ const last = node.body.at(-1);
13765
+ return last !== void 0 && blockTerminates(last);
13766
+ };
13767
+ const narrowingIf = (identifier) => {
13768
+ const comparison = identifier.parent;
13769
+ if (comparison?.type !== import_utils66.AST_NODE_TYPES.BinaryExpression || comparison.operator !== "instanceof" || comparison.left !== identifier || comparison.right.type !== import_utils66.AST_NODE_TYPES.Identifier || comparison.right.name !== "File" && comparison.right.name !== "Blob") {
13770
+ return null;
13771
+ }
13772
+ const maybeNegation = comparison.parent;
13773
+ const negated = maybeNegation?.type === import_utils66.AST_NODE_TYPES.UnaryExpression && maybeNegation.operator === "!";
13774
+ const test = negated ? maybeNegation : comparison;
13775
+ const branch = test.parent;
13776
+ return branch?.type === import_utils66.AST_NODE_TYPES.IfStatement && branch.test === test ? { branch, positive: !negated } : null;
13777
+ };
13778
+ const useDominatedByNarrowing = (use, narrowings) => narrowings.some(({ branch, positive }) => {
13779
+ if (positive) return isDescendantOf(use, branch.consequent);
13780
+ if (!blockTerminates(branch.consequent)) return false;
13781
+ const branchStatement = containingStatement(branch);
13782
+ const useStatement = containingStatement(use);
13783
+ return branchStatement !== null && useStatement !== null && branchStatement.parent === useStatement.parent && branchStatement.range[1] < useStatement.range[0];
13784
+ });
13639
13785
  const statementWithinBlock = (node, block) => {
13640
13786
  let current = node;
13641
13787
  while (current.parent !== void 0 && current.parent !== block) {
@@ -13650,14 +13796,16 @@ var require_zod_form_validation_default = createRule({
13650
13796
  (identifier) => identifier.type === import_utils66.AST_NODE_TYPES.Identifier
13651
13797
  );
13652
13798
  if (references.length === 0) return false;
13653
- if (references.some(isInstanceofNarrowing)) return true;
13799
+ const narrowings = references.map(narrowingIf).filter(
13800
+ (value) => value !== null
13801
+ );
13654
13802
  const validationStatements = references.map((reference) => guaranteedValidationStatement(declarator, reference)).filter(
13655
13803
  (statement) => statement !== null
13656
13804
  );
13657
13805
  const declarationStatement = containingStatement(declarator);
13658
13806
  const declarationBlock = declarationStatement?.parent;
13659
13807
  return references.every((reference) => {
13660
- if (zodParseAncestor(reference) !== null || isSafePrevalidationInspection(reference)) {
13808
+ if (zodParseAncestor(reference) !== null || isSafePrevalidationInspection(reference) || useDominatedByNarrowing(reference, narrowings)) {
13661
13809
  return true;
13662
13810
  }
13663
13811
  if (declarationBlock === void 0) return false;
@@ -14187,7 +14335,13 @@ var NON_SCHEMA_TERMINALS = /* @__PURE__ */ new Set([
14187
14335
  "safeDecodeAsync",
14188
14336
  "toJSONSchema",
14189
14337
  "registry",
14190
- "implement"
14338
+ "implement",
14339
+ "flattenError",
14340
+ "formatError",
14341
+ "isNullable",
14342
+ "isOptional",
14343
+ "prettifyError",
14344
+ "treeifyError"
14191
14345
  ]);
14192
14346
  var terminalMethodName = (callee) => !callee.computed && callee.property.type === import_utils69.AST_NODE_TYPES.Identifier ? callee.property.name : null;
14193
14347
  var calleeChainRoot = (node) => {
@@ -14436,7 +14590,7 @@ var rules = {
14436
14590
  };
14437
14591
  var meta = {
14438
14592
  name: "@sarj/eslint-plugin",
14439
- version: "15.3.0"
14593
+ version: "15.5.0"
14440
14594
  };
14441
14595
  var applicationOnlyRules = [
14442
14596
  "no-restricted-library-load",
package/dist/index.d.cts CHANGED
@@ -25,6 +25,7 @@ interface ExampleFile {
25
25
  /** A reviewed example. It remains private unless `public: true` is explicit. */
26
26
  interface RuleExample {
27
27
  readonly id: string;
28
+ readonly scenarioId?: string;
28
29
  readonly title: string;
29
30
  readonly outcome: ExampleOutcome;
30
31
  readonly files: readonly ExampleFile[];
@@ -224,7 +225,7 @@ declare const rules: {
224
225
  readonly "no-silent-promise-catch": DocumentedRule<readonly [], "silentCatch">;
225
226
  readonly "no-sleep-in-test-body": DocumentedRule<readonly [], "noSleepInTestBody">;
226
227
  readonly "no-storage-in-stateless-modules": DocumentedRule<readonly [RuleOptions$3?], "storageInStatelessModule">;
227
- readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop">;
228
+ readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop" | "noStringReduce">;
228
229
  readonly "no-tautological-expect": DocumentedRule<readonly [], "tautologicalComparison" | "tautologicalMatcher">;
229
230
  readonly "no-typed-doc-sections": DocumentedRule<readonly [], "typedSection">;
230
231
  readonly "no-trailing-value-narration": DocumentedRule<readonly [], "deleteNarration" | "narratesValue">;
@@ -414,7 +415,7 @@ type FlatPreset = {
414
415
  declare const plugin: {
415
416
  readonly meta: {
416
417
  readonly name: "@sarj/eslint-plugin";
417
- readonly version: "15.3.0";
418
+ readonly version: "15.5.0";
418
419
  };
419
420
  readonly rules: {
420
421
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -454,7 +455,7 @@ declare const plugin: {
454
455
  readonly "no-silent-promise-catch": DocumentedRule<readonly [], "silentCatch">;
455
456
  readonly "no-sleep-in-test-body": DocumentedRule<readonly [], "noSleepInTestBody">;
456
457
  readonly "no-storage-in-stateless-modules": DocumentedRule<readonly [RuleOptions$3?], "storageInStatelessModule">;
457
- readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop">;
458
+ readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop" | "noStringReduce">;
458
459
  readonly "no-tautological-expect": DocumentedRule<readonly [], "tautologicalComparison" | "tautologicalMatcher">;
459
460
  readonly "no-typed-doc-sections": DocumentedRule<readonly [], "typedSection">;
460
461
  readonly "no-trailing-value-narration": DocumentedRule<readonly [], "deleteNarration" | "narratesValue">;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ interface ExampleFile {
25
25
  /** A reviewed example. It remains private unless `public: true` is explicit. */
26
26
  interface RuleExample {
27
27
  readonly id: string;
28
+ readonly scenarioId?: string;
28
29
  readonly title: string;
29
30
  readonly outcome: ExampleOutcome;
30
31
  readonly files: readonly ExampleFile[];
@@ -224,7 +225,7 @@ declare const rules: {
224
225
  readonly "no-silent-promise-catch": DocumentedRule<readonly [], "silentCatch">;
225
226
  readonly "no-sleep-in-test-body": DocumentedRule<readonly [], "noSleepInTestBody">;
226
227
  readonly "no-storage-in-stateless-modules": DocumentedRule<readonly [RuleOptions$3?], "storageInStatelessModule">;
227
- readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop">;
228
+ readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop" | "noStringReduce">;
228
229
  readonly "no-tautological-expect": DocumentedRule<readonly [], "tautologicalComparison" | "tautologicalMatcher">;
229
230
  readonly "no-typed-doc-sections": DocumentedRule<readonly [], "typedSection">;
230
231
  readonly "no-trailing-value-narration": DocumentedRule<readonly [], "deleteNarration" | "narratesValue">;
@@ -414,7 +415,7 @@ type FlatPreset = {
414
415
  declare const plugin: {
415
416
  readonly meta: {
416
417
  readonly name: "@sarj/eslint-plugin";
417
- readonly version: "15.3.0";
418
+ readonly version: "15.5.0";
418
419
  };
419
420
  readonly rules: {
420
421
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -454,7 +455,7 @@ declare const plugin: {
454
455
  readonly "no-silent-promise-catch": DocumentedRule<readonly [], "silentCatch">;
455
456
  readonly "no-sleep-in-test-body": DocumentedRule<readonly [], "noSleepInTestBody">;
456
457
  readonly "no-storage-in-stateless-modules": DocumentedRule<readonly [RuleOptions$3?], "storageInStatelessModule">;
457
- readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop">;
458
+ readonly "no-string-concat-in-loop": DocumentedRule<readonly [], "noStringConcatInLoop" | "noStringReduce">;
458
459
  readonly "no-tautological-expect": DocumentedRule<readonly [], "tautologicalComparison" | "tautologicalMatcher">;
459
460
  readonly "no-typed-doc-sections": DocumentedRule<readonly [], "typedSection">;
460
461
  readonly "no-trailing-value-narration": DocumentedRule<readonly [], "deleteNarration" | "narratesValue">;
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ ${missing.join("\n")}`);
61
61
  function publicExample(example) {
62
62
  return {
63
63
  id: example.id,
64
+ scenarioId: example.scenarioId ?? "primary",
64
65
  title: example.title,
65
66
  outcome: example.outcome,
66
67
  files: example.files.map(publicFile),
@@ -105,9 +106,14 @@ function nativeSpec(config, documentation) {
105
106
  const examples = [...documentation.examples ?? []];
106
107
  examples.forEach(validateExample);
107
108
  assertUnique(examples.map((example) => example.id), "rule example IDs");
108
- const publicOutcomes = new Set(examples.filter((example) => example.public === true).map((example) => example.outcome));
109
- if (publicOutcomes.size > 0 && !(publicOutcomes.has("match") && publicOutcomes.has("no-match"))) {
110
- throw new TypeError("published rule examples must include matching and non-matching cases");
109
+ const publicExamples = examples.filter((example) => example.public === true);
110
+ const scenarios = new Set(publicExamples.map((example) => example.scenarioId ?? "primary"));
111
+ for (const scenario of scenarios) {
112
+ const pair = publicExamples.filter((example) => (example.scenarioId ?? "primary") === scenario);
113
+ const outcomes = new Set(pair.map((example) => example.outcome));
114
+ if (pair.length !== 2 || !outcomes.has("match") || !outcomes.has("no-match")) {
115
+ throw new TypeError(`published example scenario ${scenario} must contain both matching and non-matching cases exactly once`);
116
+ }
111
117
  }
112
118
  const messageIds = Object.keys(meta2.messages).sort();
113
119
  const schema = optionsSchema(meta2.schema);
@@ -128,7 +134,7 @@ function nativeSpec(config, documentation) {
128
134
  references,
129
135
  since: documentation.since ?? null,
130
136
  examples,
131
- publicExamples: examples.filter((example) => example.public === true),
137
+ publicExamples,
132
138
  messageIds,
133
139
  optionsSchema: schema
134
140
  };
@@ -148,6 +154,9 @@ function validateExample(example) {
148
154
  if (!KEBAB_CASE.test(example.id)) {
149
155
  throw new TypeError("example ID must be lowercase kebab-case");
150
156
  }
157
+ if (!KEBAB_CASE.test(example.scenarioId ?? "primary")) {
158
+ throw new TypeError("example scenario must be lowercase kebab-case");
159
+ }
151
160
  if (example.title.trim().length === 0) {
152
161
  throw new TypeError("example title must not be empty");
153
162
  }
@@ -555,6 +564,9 @@ function isAssertionStatement(statement) {
555
564
  const expression = statement.expression;
556
565
  return expression.type === AST_NODE_TYPES2.CallExpression && isAssertionCall(expression);
557
566
  }
567
+ function isTypeOnlyContractStatement(statement) {
568
+ return statement.type === AST_NODE_TYPES2.TSTypeAliasDeclaration || statement.type === AST_NODE_TYPES2.TSInterfaceDeclaration;
569
+ }
558
570
  function normalizedLiteral(node) {
559
571
  if ("regex" in node) {
560
572
  return ["Literal", "regex", node.regex.pattern, node.regex.flags];
@@ -612,7 +624,7 @@ var duplicate_test_body_default = createRule({
612
624
  return;
613
625
  }
614
626
  const body2 = candidate.body;
615
- if (body2.body.type !== AST_NODE_TYPES2.BlockStatement || body2.body.body.length < MIN_STATEMENTS || body2.body.body.every(isAssertionStatement)) {
627
+ if (body2.body.type !== AST_NODE_TYPES2.BlockStatement || body2.body.body.length < MIN_STATEMENTS || body2.body.body.every(isAssertionStatement) || body2.body.body.some(isTypeOnlyContractStatement)) {
616
628
  return;
617
629
  }
618
630
  const comments = context.sourceCode.getCommentsInside(body2.body).map((comment) => [comment.type, comment.value]);
@@ -3512,7 +3524,11 @@ var NUMBER_METHODS = /* @__PURE__ */ new Set([
3512
3524
  "lt",
3513
3525
  "lte",
3514
3526
  "max",
3515
- "min"
3527
+ "min",
3528
+ "negative",
3529
+ "nonnegative",
3530
+ "nonpositive",
3531
+ "positive"
3516
3532
  ]);
3517
3533
  var LENGTH_METHODS = /* @__PURE__ */ new Set(["length", "max", "min"]);
3518
3534
  var RESHAPING_METHODS = /* @__PURE__ */ new Set([
@@ -3649,13 +3665,29 @@ var no_impossible_zod_literal_bounds_default = createRule({
3649
3665
  let upper = null;
3650
3666
  for (const { method, node } of chain.calls) {
3651
3667
  if (!allowed.has(method)) return null;
3652
- const value = finiteNumber(node.arguments[0]);
3668
+ const semantic = method === "positive" ? { exclusive: true, lower: true, value: 0 } : method === "nonnegative" ? { exclusive: false, lower: true, value: 0 } : method === "negative" ? { exclusive: true, lower: false, value: 0 } : method === "nonpositive" ? { exclusive: false, lower: false, value: 0 } : null;
3669
+ const value = semantic?.value ?? finiteNumber(node.arguments[0]);
3653
3670
  if (value === null) return null;
3654
3671
  if (chain.kind !== "number" && (!Number.isInteger(value) || value < 0)) {
3655
3672
  return null;
3656
3673
  }
3657
3674
  const label = `${method}(${String(value)})`;
3658
- if (method === "length") {
3675
+ if (semantic !== null) {
3676
+ if (node.arguments.length !== 0) return null;
3677
+ if (semantic.lower) {
3678
+ lower = strongerLower(lower, {
3679
+ exclusive: semantic.exclusive,
3680
+ label: `${method}()`,
3681
+ value
3682
+ });
3683
+ } else {
3684
+ upper = strongerUpper(upper, {
3685
+ exclusive: semantic.exclusive,
3686
+ label: `${method}()`,
3687
+ value
3688
+ });
3689
+ }
3690
+ } else if (method === "length") {
3659
3691
  lower = strongerLower(lower, { exclusive: false, label, value });
3660
3692
  upper = strongerUpper(upper, { exclusive: false, label, value });
3661
3693
  } else if (method === "gt" || method === "gte" || method === "min") {
@@ -6768,6 +6800,13 @@ function nearestEnclosingFunction(node) {
6768
6800
  }
6769
6801
  return null;
6770
6802
  }
6803
+ function isImmediatelyConsumedSleep(node) {
6804
+ const parent = node.parent;
6805
+ if (parent?.type === AST_NODE_TYPES25.AwaitExpression || parent?.type === AST_NODE_TYPES25.ReturnStatement || parent?.type === AST_NODE_TYPES25.ExpressionStatement) {
6806
+ return true;
6807
+ }
6808
+ return parent?.type === AST_NODE_TYPES25.ArrowFunctionExpression && parent.body === node;
6809
+ }
6771
6810
  function isTestBody(fn) {
6772
6811
  const call = fn.parent;
6773
6812
  if (call?.type !== AST_NODE_TYPES25.CallExpression || !call.arguments.some((argument) => argument === fn)) {
@@ -6810,6 +6849,9 @@ var no_sleep_in_test_body_default = createRule({
6810
6849
  return {};
6811
6850
  }
6812
6851
  const report = (node) => {
6852
+ if (!isImmediatelyConsumedSleep(node)) {
6853
+ return;
6854
+ }
6813
6855
  const enclosing = nearestEnclosingFunction(node);
6814
6856
  if (enclosing === null || !isTestBody(enclosing)) {
6815
6857
  return;
@@ -7107,6 +7149,27 @@ function isSmallStaticForLoop(node) {
7107
7149
  const iterations = node.test.right.value - declaration.init.value + (node.test.operator === "<=" ? 1 : 0);
7108
7150
  return iterations >= 0 && iterations <= 8;
7109
7151
  }
7152
+ function isStringSeededReduce(node) {
7153
+ if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "reduce" || node.arguments.length !== 2) {
7154
+ return false;
7155
+ }
7156
+ const [callback, initial] = node.arguments;
7157
+ if (node.callee.object.type === "ArrayExpression" && node.callee.object.elements.length <= 8) {
7158
+ return false;
7159
+ }
7160
+ if (callback === void 0 || initial === void 0 || callback.type === "SpreadElement" || initial.type === "SpreadElement" || callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression" || callback.params[0]?.type !== "Identifier" || !isStringLiteralInit(initial)) {
7161
+ return false;
7162
+ }
7163
+ const accumulator = callback.params[0].name;
7164
+ if (callback.body.type !== "BlockStatement") {
7165
+ return isConcatOntoTarget(callback.body, accumulator);
7166
+ }
7167
+ if (callback.body.body.length !== 1 || callback.body.body[0]?.type !== "ReturnStatement") {
7168
+ return false;
7169
+ }
7170
+ const returned = callback.body.body[0].argument;
7171
+ return returned !== null && isConcatOntoTarget(returned, accumulator);
7172
+ }
7110
7173
  var no_string_concat_in_loop_default = createRule({
7111
7174
  name: "no-string-concat-in-loop",
7112
7175
  documentation: noStringConcatInLoopDocumentation,
@@ -7117,7 +7180,8 @@ var no_string_concat_in_loop_default = createRule({
7117
7180
  },
7118
7181
  schema: [],
7119
7182
  messages: {
7120
- noStringConcatInLoop: 'Avoid building a string with `+=` inside a loop \u2014 this is O(n^2). Push the parts onto an array and use `arr.join("")` after the loop.'
7183
+ noStringConcatInLoop: 'Avoid building a string with `+=` inside a loop \u2014 this is O(n^2). Push the parts onto an array and use `arr.join("")` after the loop.',
7184
+ noStringReduce: "Avoid concatenating a growing string in `reduce` \u2014 this is O(n^2). Map the fragments and join them once instead."
7121
7185
  }
7122
7186
  },
7123
7187
  defaultOptions: [],
@@ -7127,6 +7191,11 @@ var no_string_concat_in_loop_default = createRule({
7127
7191
  }
7128
7192
  const reported = /* @__PURE__ */ new WeakMap();
7129
7193
  return {
7194
+ CallExpression(node) {
7195
+ if (isStringSeededReduce(node)) {
7196
+ context.report({ node, messageId: "noStringReduce" });
7197
+ }
7198
+ },
7130
7199
  AssignmentExpression(node) {
7131
7200
  if (node.left.type !== "Identifier") {
7132
7201
  return;
@@ -7231,6 +7300,9 @@ function isLiteral(node) {
7231
7300
  return false;
7232
7301
  }
7233
7302
  }
7303
+ function isStructuralLiteral(node) {
7304
+ return node.type === AST_NODE_TYPES27.ArrayExpression || node.type === AST_NODE_TYPES27.ObjectExpression;
7305
+ }
7234
7306
  function expectOperand(callee) {
7235
7307
  const receiver = callee.object;
7236
7308
  if (receiver.type !== AST_NODE_TYPES27.CallExpression || receiver.callee.type !== AST_NODE_TYPES27.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
@@ -7287,6 +7359,9 @@ var no_tautological_expect_default = createRule({
7287
7359
  if (!EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === void 0 || !isLiteral(expected)) {
7288
7360
  return;
7289
7361
  }
7362
+ if (matcher === "toBe" && (isStructuralLiteral(operand) || isStructuralLiteral(expected))) {
7363
+ return;
7364
+ }
7290
7365
  if (context.sourceCode.getText(operand) !== context.sourceCode.getText(expected)) {
7291
7366
  return;
7292
7367
  }
@@ -7600,7 +7675,7 @@ var no_trailing_value_narration_default = createRule({
7600
7675
  });
7601
7676
 
7602
7677
  // src/rules/no-declaration-comment-wall.ts
7603
- import { AST_NODE_TYPES as AST_NODE_TYPES29 } from "@typescript-eslint/utils";
7678
+ import { AST_NODE_TYPES as AST_NODE_TYPES29, AST_TOKEN_TYPES } from "@typescript-eslint/utils";
7604
7679
 
7605
7680
  // src/rules/_comment-wall.ts
7606
7681
  import { AST_NODE_TYPES as AST_NODE_TYPES28 } from "@typescript-eslint/utils";
@@ -7800,7 +7875,13 @@ var no_declaration_comment_wall_default = createRule({
7800
7875
  const lead = ownsItsLine ? endingOn.get(member.loc.start.line - 1) : void 0;
7801
7876
  if (lead !== void 0) {
7802
7877
  const before = sourceCode.getTokenBefore(lead, { includeComments: false });
7803
- if (before === null || before.loc.end.line < lead.loc.start.line) return lead;
7878
+ if (before === null || before.loc.end.line < lead.loc.start.line) {
7879
+ const previousLine = endingOn.get(lead.loc.start.line - 1);
7880
+ if (lead.type === AST_TOKEN_TYPES.Line && previousLine?.type === AST_TOKEN_TYPES.Line && previousLine.loc.start.column === lead.loc.start.column) {
7881
+ return void 0;
7882
+ }
7883
+ return lead;
7884
+ }
7804
7885
  }
7805
7886
  const trail = startingOn.get(member.loc.end.line);
7806
7887
  return trail !== void 0 && trail.range[0] > member.range[0] ? trail : void 0;
@@ -8039,7 +8120,7 @@ var no_union_in_comment_default = createRule({
8039
8120
  });
8040
8121
 
8041
8122
  // src/rules/no-type-member-comment-wall.ts
8042
- import { AST_NODE_TYPES as AST_NODE_TYPES31 } from "@typescript-eslint/utils";
8123
+ import { AST_NODE_TYPES as AST_NODE_TYPES31, AST_TOKEN_TYPES as AST_TOKEN_TYPES2 } from "@typescript-eslint/utils";
8043
8124
  var noTypeMemberCommentWallDocumentation = {
8044
8125
  summary: "Flag an object type whose member comments mostly re-spell the members' own names and types.",
8045
8126
  rationale: "Repetitive member comments add scanning cost while hiding the comments that describe facts absent from the type.",
@@ -8102,7 +8183,13 @@ var no_type_member_comment_wall_default = createRule({
8102
8183
  const lead = ownsItsLine ? endingOn.get(member.loc.start.line - 1) : void 0;
8103
8184
  if (lead !== void 0) {
8104
8185
  const before = sourceCode.getTokenBefore(lead, { includeComments: false });
8105
- if (before === null || before.loc.end.line < lead.loc.start.line) return lead;
8186
+ if (before === null || before.loc.end.line < lead.loc.start.line) {
8187
+ const previousLine = endingOn.get(lead.loc.start.line - 1);
8188
+ if (lead.type === AST_TOKEN_TYPES2.Line && previousLine?.type === AST_TOKEN_TYPES2.Line && previousLine.loc.start.column === lead.loc.start.column) {
8189
+ return void 0;
8190
+ }
8191
+ return lead;
8192
+ }
8106
8193
  }
8107
8194
  const trail = startingOn.get(member.loc.end.line);
8108
8195
  return trail !== void 0 && trail.range[0] > member.range[0] ? trail : void 0;
@@ -8402,6 +8489,10 @@ import {
8402
8489
  } from "@typescript-eslint/utils";
8403
8490
  var MOCK_TYPE_NAMES = /* @__PURE__ */ new Set([
8404
8491
  "Mock",
8492
+ "Mocked",
8493
+ "MockedClass",
8494
+ "MockedFunction",
8495
+ "MockedObject",
8405
8496
  "MockInstance",
8406
8497
  "SpyInstance"
8407
8498
  ]);
@@ -8649,9 +8740,9 @@ var no_zod_native_enum_default = createRule({
8649
8740
  }
8650
8741
  function isZodMemberCall(node, api) {
8651
8742
  const callee = node.callee;
8652
- if (callee.type === AST_NODE_TYPES34.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES34.Identifier && callee.property.type === AST_NODE_TYPES34.Identifier) {
8743
+ if (callee.type === AST_NODE_TYPES34.MemberExpression && callee.object.type === AST_NODE_TYPES34.Identifier && (callee.property.type === AST_NODE_TYPES34.Identifier && !callee.computed && callee.property.name === api || callee.computed && callee.property.type === AST_NODE_TYPES34.Literal && callee.property.value === api)) {
8653
8744
  const binding = resolvedBinding(callee.object);
8654
- return binding !== null && zodNamespaceBindings.has(binding) && callee.property.name === api;
8745
+ return binding !== null && zodNamespaceBindings.has(binding);
8655
8746
  }
8656
8747
  if (callee.type === AST_NODE_TYPES34.Identifier) {
8657
8748
  const binding = resolvedBinding(callee);
@@ -9495,6 +9586,27 @@ var prefer_immutable_module_constant_default = createRule({
9495
9586
  }
9496
9587
  const exportedNames2 = /* @__PURE__ */ new Set();
9497
9588
  const typeAliases2 = /* @__PURE__ */ new Map();
9589
+ const mutatesThroughConstAlias = (root) => {
9590
+ const pending = [root];
9591
+ const seen = /* @__PURE__ */ new Set();
9592
+ while (pending.length > 0) {
9593
+ const variable = pending.pop();
9594
+ if (variable === void 0 || seen.has(variable)) continue;
9595
+ seen.add(variable);
9596
+ for (const reference of variable.references) {
9597
+ const identifier = reference.identifier;
9598
+ if (identifier.type !== AST_NODE_TYPES39.Identifier) continue;
9599
+ if (referenceMutates(identifier, isUnshadowedGlobal)) return true;
9600
+ const declarator = identifier.parent;
9601
+ if (declarator.type !== AST_NODE_TYPES39.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== AST_NODE_TYPES39.Identifier || declarator.parent.type !== AST_NODE_TYPES39.VariableDeclaration || declarator.parent.kind !== "const") {
9602
+ continue;
9603
+ }
9604
+ const alias = sourceCode.getDeclaredVariables(declarator)[0];
9605
+ if (alias !== void 0) pending.push(alias);
9606
+ }
9607
+ }
9608
+ return false;
9609
+ };
9498
9610
  return {
9499
9611
  Program(node) {
9500
9612
  for (const statement of node.body) {
@@ -9535,9 +9647,7 @@ var prefer_immutable_module_constant_default = createRule({
9535
9647
  return;
9536
9648
  }
9537
9649
  const variable = sourceCode.getDeclaredVariables(node)[0];
9538
- if (!directlyExported && !exportedNames2.has(node.id.name) && variable?.references.some(
9539
- (reference) => reference.identifier.type === AST_NODE_TYPES39.Identifier && referenceMutates(reference.identifier, isUnshadowedGlobal)
9540
- ) === true) {
9650
+ if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughConstAlias(variable)) {
9541
9651
  return;
9542
9652
  }
9543
9653
  context.report({
@@ -10316,6 +10426,10 @@ var prefer_module_level_schema_default = createRule({
10316
10426
  }
10317
10427
  for (const definition of resolved.defs) {
10318
10428
  if (definition.type === "ImportBinding") {
10429
+ const parent = reference.identifier.parent;
10430
+ if (parent?.type === AST_NODE_TYPES42.CallExpression && parent.callee === reference.identifier && !zodNamespaces.has(reference.identifier.name)) {
10431
+ return false;
10432
+ }
10319
10433
  continue;
10320
10434
  }
10321
10435
  if (definition.node.type === AST_NODE_TYPES42.VariableDeclarator && definition.node.parent.type === AST_NODE_TYPES42.VariableDeclaration && definition.node.parent.kind !== "const") {
@@ -12147,10 +12261,7 @@ var SHAPE_PRESERVING_METHODS = /* @__PURE__ */ new Set([
12147
12261
  ]);
12148
12262
  var OPTIONAL_MODIFIERS = /* @__PURE__ */ new Set([
12149
12263
  "optional",
12150
- "nullish",
12151
- "default",
12152
- "prefault",
12153
- "catch"
12264
+ "nullish"
12154
12265
  ]);
12155
12266
  var NULLABLE_MODIFIERS = /* @__PURE__ */ new Set(["nullable", "nullish"]);
12156
12267
  var RESHAPING_MODIFIERS = /* @__PURE__ */ new Set([
@@ -13425,6 +13536,7 @@ var ZOD_PARSE_METHODS = /* @__PURE__ */ new Set([
13425
13536
  "parseAsync",
13426
13537
  "safeParseAsync"
13427
13538
  ]);
13539
+ var FORM_VALUE_METHODS = /* @__PURE__ */ new Set(["get", "getAll"]);
13428
13540
  var zodReceiverRoot = (node) => {
13429
13541
  let current = node;
13430
13542
  while (true) {
@@ -13499,7 +13611,7 @@ var require_zod_form_validation_default = createRule({
13499
13611
  };
13500
13612
  const isFormSourceIdentifier = (node) => {
13501
13613
  if (node.type !== AST_NODE_TYPES53.Identifier) return false;
13502
- if (/formdata/i.test(node.name)) return true;
13614
+ const conventionalName = /formdata/i.test(node.name);
13503
13615
  let scope = context.sourceCode.getScope(node);
13504
13616
  while (scope !== null) {
13505
13617
  const variable = scope.set.get(node.name);
@@ -13508,16 +13620,16 @@ var require_zod_form_validation_default = createRule({
13508
13620
  if (def !== void 0 && def.type === "Variable" && def.node.type === AST_NODE_TYPES53.VariableDeclarator && def.node.init !== null) {
13509
13621
  return isFormDataMethodCall(def.node.init);
13510
13622
  }
13511
- return false;
13623
+ return def?.type === "Parameter" && conventionalName;
13512
13624
  }
13513
13625
  scope = scope.upper;
13514
13626
  }
13515
- return false;
13627
+ return conventionalName;
13516
13628
  };
13517
13629
  const isFormDataGetCall = (node) => {
13518
13630
  const callee = node.callee;
13519
13631
  if (callee.type !== AST_NODE_TYPES53.MemberExpression) return false;
13520
- if (callee.property.type !== AST_NODE_TYPES53.Identifier || callee.property.name !== "get") {
13632
+ if (callee.property.type !== AST_NODE_TYPES53.Identifier || !FORM_VALUE_METHODS.has(callee.property.name)) {
13521
13633
  return false;
13522
13634
  }
13523
13635
  return isFormSourceIdentifier(callee.object);
@@ -13611,6 +13723,40 @@ var require_zod_form_validation_default = createRule({
13611
13723
  }
13612
13724
  return ["===", "!==", "==", "!="].includes(parent.operator) && (parent.right.type === AST_NODE_TYPES53.Literal && parent.right.value === null || parent.right.type === AST_NODE_TYPES53.Identifier && parent.right.name === "undefined");
13613
13725
  };
13726
+ const isDescendantOf = (node, ancestor) => {
13727
+ let current = node;
13728
+ while (current !== void 0 && current !== null) {
13729
+ if (current === ancestor) return true;
13730
+ current = current.parent;
13731
+ }
13732
+ return false;
13733
+ };
13734
+ const blockTerminates = (node) => {
13735
+ if (node.type === AST_NODE_TYPES53.ReturnStatement || node.type === AST_NODE_TYPES53.ThrowStatement) {
13736
+ return true;
13737
+ }
13738
+ if (node.type !== AST_NODE_TYPES53.BlockStatement || node.body.length === 0) return false;
13739
+ const last = node.body.at(-1);
13740
+ return last !== void 0 && blockTerminates(last);
13741
+ };
13742
+ const narrowingIf = (identifier) => {
13743
+ const comparison = identifier.parent;
13744
+ if (comparison?.type !== AST_NODE_TYPES53.BinaryExpression || comparison.operator !== "instanceof" || comparison.left !== identifier || comparison.right.type !== AST_NODE_TYPES53.Identifier || comparison.right.name !== "File" && comparison.right.name !== "Blob") {
13745
+ return null;
13746
+ }
13747
+ const maybeNegation = comparison.parent;
13748
+ const negated = maybeNegation?.type === AST_NODE_TYPES53.UnaryExpression && maybeNegation.operator === "!";
13749
+ const test = negated ? maybeNegation : comparison;
13750
+ const branch = test.parent;
13751
+ return branch?.type === AST_NODE_TYPES53.IfStatement && branch.test === test ? { branch, positive: !negated } : null;
13752
+ };
13753
+ const useDominatedByNarrowing = (use, narrowings) => narrowings.some(({ branch, positive }) => {
13754
+ if (positive) return isDescendantOf(use, branch.consequent);
13755
+ if (!blockTerminates(branch.consequent)) return false;
13756
+ const branchStatement = containingStatement(branch);
13757
+ const useStatement = containingStatement(use);
13758
+ return branchStatement !== null && useStatement !== null && branchStatement.parent === useStatement.parent && branchStatement.range[1] < useStatement.range[0];
13759
+ });
13614
13760
  const statementWithinBlock = (node, block) => {
13615
13761
  let current = node;
13616
13762
  while (current.parent !== void 0 && current.parent !== block) {
@@ -13625,14 +13771,16 @@ var require_zod_form_validation_default = createRule({
13625
13771
  (identifier) => identifier.type === AST_NODE_TYPES53.Identifier
13626
13772
  );
13627
13773
  if (references.length === 0) return false;
13628
- if (references.some(isInstanceofNarrowing)) return true;
13774
+ const narrowings = references.map(narrowingIf).filter(
13775
+ (value) => value !== null
13776
+ );
13629
13777
  const validationStatements = references.map((reference) => guaranteedValidationStatement(declarator, reference)).filter(
13630
13778
  (statement) => statement !== null
13631
13779
  );
13632
13780
  const declarationStatement = containingStatement(declarator);
13633
13781
  const declarationBlock = declarationStatement?.parent;
13634
13782
  return references.every((reference) => {
13635
- if (zodParseAncestor(reference) !== null || isSafePrevalidationInspection(reference)) {
13783
+ if (zodParseAncestor(reference) !== null || isSafePrevalidationInspection(reference) || useDominatedByNarrowing(reference, narrowings)) {
13636
13784
  return true;
13637
13785
  }
13638
13786
  if (declarationBlock === void 0) return false;
@@ -14165,7 +14313,13 @@ var NON_SCHEMA_TERMINALS = /* @__PURE__ */ new Set([
14165
14313
  "safeDecodeAsync",
14166
14314
  "toJSONSchema",
14167
14315
  "registry",
14168
- "implement"
14316
+ "implement",
14317
+ "flattenError",
14318
+ "formatError",
14319
+ "isNullable",
14320
+ "isOptional",
14321
+ "prettifyError",
14322
+ "treeifyError"
14169
14323
  ]);
14170
14324
  var terminalMethodName = (callee) => !callee.computed && callee.property.type === AST_NODE_TYPES55.Identifier ? callee.property.name : null;
14171
14325
  var calleeChainRoot = (node) => {
@@ -14414,7 +14568,7 @@ var rules = {
14414
14568
  };
14415
14569
  var meta = {
14416
14570
  name: "@sarj/eslint-plugin",
14417
- version: "15.3.0"
14571
+ version: "15.5.0"
14418
14572
  };
14419
14573
  var applicationOnlyRules = [
14420
14574
  "no-restricted-library-load",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.3.0",
3
+ "version": "15.5.0",
4
4
  "packageManager": "npm@11.19.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",