@sarj/eslint-plugin 15.17.6 → 15.17.8

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
@@ -6869,22 +6869,23 @@ var no_restated_comment_default = createRule({
6869
6869
  // src/rules/no-restated-jsdoc.ts
6870
6870
  var import_utils39 = require("@typescript-eslint/utils");
6871
6871
  var NO_RESTATED_JSDOC_DOCUMENTATION = {
6872
- summary: "Flag a JSDoc block whose description and tags only re-spell the signature they document.",
6872
+ summary: "Flag JSDoc prose that appears to repeat declaration names without adding behavioral information.",
6873
6873
  rationale: "Signature-only JSDoc duplicates type information and drifts without helping callers.",
6874
6874
  remediation: "Delete the block or document behavior, constraints, failures, or context the signature cannot express.",
6875
6875
  category: "maintainability",
6876
6876
  aliases: ["jsdoc-restates-signature"],
6877
6877
  autofix: "suggestion",
6878
- limitations: ["Generated files, detached blocks, unknown tags, empty blocks, and JSDoc with information absent from the signature are excluded."],
6878
+ limitations: ["Generated files, detached blocks, intervening comments, unknown tags, explicit JSDoc type payloads, empty blocks, and JSDoc with information absent from the signature are excluded.", "Negation, conditions, constraints, sentinel values, numeric details, and quoted text conservatively preserve the block, even when a declaration name contains the same words."],
6879
6879
  examples: [
6880
6880
  { id: "behavioral-jsdoc", title: "Document behavior absent from the signature", outcome: "no-match", files: [{ path: "src/users.ts", source: "/** Get the user while bypassing the read replica. */\nexport function getUser(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 0, public: true },
6881
- { id: "signature-jsdoc", title: "Remove JSDoc that only repeats the signature", outcome: "match", files: [{ path: "src/users.ts", source: "/** Get the user by id. */\nexport function getUserById(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 1, public: true }
6881
+ { id: "signature-jsdoc", title: "Remove JSDoc that only repeats the signature", outcome: "match", files: [{ path: "src/users.ts", source: "/** Get the user by id. */\nexport function getUserById(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 1, public: true },
6882
+ { id: "negated-behavior", scenarioId: "negation", title: "Keep behavior even when its words resemble the declaration", outcome: "no-match", files: [{ path: "src/users.ts", source: "/** Does not cache the user. */\nexport function cacheUser(user: unknown) { return user; }" }], focusPath: "src/users.ts", expectedCount: 0, public: true },
6883
+ { id: "repeated-behavior-name", scenarioId: "negation", title: "Review prose that merely repeats the declaration name", outcome: "match", files: [{ path: "src/users.ts", source: "/** Cache the user. */\nexport function cacheUser(user: unknown) { return user; }" }], focusPath: "src/users.ts", expectedCount: 1, public: true }
6882
6884
  ]
6883
6885
  };
6884
6886
  var MODELLED_TAGS = /* @__PURE__ */ new Set([
6885
6887
  "arg",
6886
6888
  "argument",
6887
- "async",
6888
6889
  "description",
6889
6890
  "param",
6890
6891
  "return",
@@ -6903,7 +6904,8 @@ var STOPWORDS2 = new Set(
6903
6904
  returns returning result optional required default true false null undefined
6904
6905
  string number boolean array list promise`.split(/\s+/)
6905
6906
  );
6906
- var WORD_RE2 = /[A-Za-z]+/g;
6907
+ var WORD_RE2 = new RegExp("\\p{L}+", "gu");
6908
+ var BEHAVIORAL_PROSE_RE = /\b(?:not|no|never|if|when|unless|must|should|may|can|could|would|optional|required|default|true|false|null|undefined|before|after|until|once|again|only|always)\b|\d|["'`<>=+*/%&|!~^-]/i;
6907
6909
  function parseJsDoc(value) {
6908
6910
  const lines = value.replace(/^\*/, "").split("\n").map((line) => line.replace(/^\s*\*?\s?/, ""));
6909
6911
  const description = [];
@@ -6923,12 +6925,13 @@ function parseJsDoc(value) {
6923
6925
  return { description: description.join("\n").trim(), tags };
6924
6926
  }
6925
6927
  function covered(text, known) {
6928
+ if (BEHAVIORAL_PROSE_RE.test(text)) return false;
6926
6929
  const stems = /* @__PURE__ */ new Set();
6927
6930
  for (const token of known) stems.add(stem(token));
6928
6931
  return proseTokens(text).every((word) => known.has(word) || stems.has(stem(word)));
6929
6932
  }
6930
6933
  function proseTokens(text) {
6931
- return (text.match(WORD_RE2) ?? []).map((word) => word.toLowerCase()).filter((word) => word.length > 1 && !STOPWORDS2.has(word));
6934
+ return (text.match(WORD_RE2) ?? []).map((word) => word.toLowerCase()).filter((word) => !STOPWORDS2.has(word));
6932
6935
  }
6933
6936
  function declarationNames(node) {
6934
6937
  switch (node.type) {
@@ -6985,11 +6988,11 @@ var no_restated_jsdoc_default = createRule({
6985
6988
  type: "suggestion",
6986
6989
  hasSuggestions: true,
6987
6990
  docs: {
6988
- description: "Flag a JSDoc block whose description and tags only re-spell the signature they document."
6991
+ description: "Flag JSDoc prose that appears to repeat declaration names without adding behavioral information."
6989
6992
  },
6990
6993
  schema: [],
6991
6994
  messages: {
6992
- restatesSignature: "JSDoc only re-spells the signature \u2014 delete it; if the signature still needs explanation, improve its names or types. Keep constraints, failures, and rationale.",
6995
+ restatesSignature: "JSDoc appears to repeat declaration names \u2014 consider removing repetition. Keep type contracts, constraints, failures, and rationale.",
6993
6996
  deleteBlock: "Delete the JSDoc block."
6994
6997
  }
6995
6998
  },
@@ -7012,8 +7015,9 @@ var no_restated_jsdoc_default = createRule({
7012
7015
  const tagNames = new Set(tags.map((tag) => tag.name));
7013
7016
  if ([...tagNames].some((name) => !MODELLED_TAGS.has(name))) continue;
7014
7017
  if (isProtected(describedText)) continue;
7015
- const token = sourceCode.getTokenAfter(comment, { includeComments: false });
7018
+ const token = sourceCode.getTokenAfter(comment, { includeComments: true });
7016
7019
  if (token === null || token.loc.start.line !== comment.loc.end.line + 1) continue;
7020
+ if (token.type === "Line" || token.type === "Block") continue;
7017
7021
  let node = sourceCode.getNodeByRangeIndex(token.range[0]);
7018
7022
  let declaration = null;
7019
7023
  while (node != null && node.type !== import_utils39.AST_NODE_TYPES.Program) {
@@ -7024,6 +7028,8 @@ var no_restated_jsdoc_default = createRule({
7024
7028
  if (declaration === null) continue;
7025
7029
  const paramTags = tags.filter((tag) => PARAM_TAGS.has(tag.name));
7026
7030
  const returnTags = tags.filter((tag) => RETURN_TAGS.has(tag.name));
7031
+ if ([...paramTags, ...returnTags].some((tag) => /^\s*\{/.test(tag.text))) continue;
7032
+ if (paramTags.some((tag) => /^\s*(?:\[|[A-Za-z_$][\w$]*\.)/.test(tag.text))) continue;
7027
7033
  if (describedText.length === 0 && paramTags.length === 0 && returnTags.length === 0) {
7028
7034
  continue;
7029
7035
  }
@@ -10074,8 +10080,8 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
10074
10080
  rationale: "Wrapping a TypeScript enum preserves its emitted runtime object and duplicates the schema's value definition across two constructs.",
10075
10081
  remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
10076
10082
  category: "maintainability",
10077
- autofix: "safe",
10078
- limitations: ["Automatic fixes are limited to inline object literals whose unique values are all string literals."],
10083
+ autofix: "none",
10084
+ limitations: ["Migration is manual: replacing an enum-like object with a value array changes the public schema.enum keys and can affect consumers."],
10079
10085
  examples: [
10080
10086
  {
10081
10087
  id: "zod-literal-enum",
@@ -10093,8 +10099,7 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
10093
10099
  files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.nativeEnum({ Active: "active", Inactive: "inactive" });' }],
10094
10100
  focusPath: "src/status.ts",
10095
10101
  expectedCount: 1,
10096
- public: true,
10097
- fixedFiles: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.enum(["active", "inactive"]);' }]
10102
+ public: true
10098
10103
  }
10099
10104
  ]
10100
10105
  };
@@ -10119,26 +10124,6 @@ function unwrap2(node) {
10119
10124
  }
10120
10125
  return node;
10121
10126
  }
10122
- function stringValueTexts(node, sourceCode) {
10123
- const texts = [];
10124
- for (const prop of node.properties) {
10125
- if (prop.type !== import_utils56.AST_NODE_TYPES.Property) {
10126
- return null;
10127
- }
10128
- if (prop.computed || prop.shorthand || prop.method || prop.kind !== "init") {
10129
- return null;
10130
- }
10131
- const value = prop.value;
10132
- if (value.type !== import_utils56.AST_NODE_TYPES.Literal || typeof value.value !== "string") {
10133
- return null;
10134
- }
10135
- const text = sourceCode.getText(value);
10136
- if (!texts.includes(text)) {
10137
- texts.push(text);
10138
- }
10139
- }
10140
- return texts.length > 0 ? texts : null;
10141
- }
10142
10127
  function resolvesToLocalEnum(node, scope) {
10143
10128
  let current = scope;
10144
10129
  while (current !== null) {
@@ -10170,7 +10155,6 @@ var no_zod_native_enum_default = createRule({
10170
10155
  documentation: NO_ZOD_NATIVE_ENUM_DOCUMENTATION,
10171
10156
  meta: {
10172
10157
  type: "suggestion",
10173
- fixable: "code",
10174
10158
  docs: {
10175
10159
  description: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.'
10176
10160
  },
@@ -10215,30 +10199,6 @@ var no_zod_native_enum_default = createRule({
10215
10199
  }
10216
10200
  return false;
10217
10201
  }
10218
- function buildFix(node) {
10219
- const callee = node.callee;
10220
- if (callee.type !== import_utils56.AST_NODE_TYPES.MemberExpression || callee.property.type !== import_utils56.AST_NODE_TYPES.Identifier) {
10221
- return null;
10222
- }
10223
- const arg = node.arguments[0];
10224
- if (arg === void 0 || node.arguments.length !== 1 || arg.type === import_utils56.AST_NODE_TYPES.SpreadElement) {
10225
- return null;
10226
- }
10227
- const inner = unwrap2(arg);
10228
- if (inner.type !== import_utils56.AST_NODE_TYPES.ObjectExpression) {
10229
- return null;
10230
- }
10231
- const values = stringValueTexts(inner, sourceCode);
10232
- if (values === null) {
10233
- return null;
10234
- }
10235
- const property = callee.property;
10236
- const replacementArg = `[${values.join(", ")}]`;
10237
- return (fixer) => [
10238
- fixer.replaceText(property, "enum"),
10239
- fixer.replaceText(arg, replacementArg)
10240
- ];
10241
- }
10242
10202
  return {
10243
10203
  ImportDeclaration(node) {
10244
10204
  if (!isZodModule2(node.source.value)) {
@@ -10259,11 +10219,9 @@ var no_zod_native_enum_default = createRule({
10259
10219
  },
10260
10220
  CallExpression(node) {
10261
10221
  if (isZodMemberCall(node, "nativeEnum")) {
10262
- const fix = buildFix(node);
10263
10222
  context.report({
10264
10223
  node,
10265
- messageId: "nativeEnum",
10266
- ...fix === null ? {} : { fix }
10224
+ messageId: "nativeEnum"
10267
10225
  });
10268
10226
  return;
10269
10227
  }
@@ -12870,11 +12828,11 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
12870
12828
  rationale: "One multi-value literal expresses the same closed value domain without repeated schema wrappers.",
12871
12829
  remediation: "Replace the union with z.literal([value1, value2, ...]).",
12872
12830
  category: "maintainability",
12873
- autofix: "safe",
12831
+ autofix: "none",
12874
12832
  limitations: [
12875
12833
  "Bare zod imports are analyzed only when the rule option explicitly declares zodMajorVersion: 4; explicit zod/v4 entrypoints are self-declaring.",
12876
12834
  "All-string domains are left to zod/prefer-enum-over-literal-union.",
12877
- "A finding containing comments is not autofixed because moving its trivia is ambiguous."
12835
+ "Migration is manual: ZodLiteral and ZodUnion expose different introspection APIs and validation error shapes even when they accept the same values."
12878
12836
  ],
12879
12837
  examples: [
12880
12838
  {
@@ -12897,10 +12855,6 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
12897
12855
  path: "src/schema.ts",
12898
12856
  source: "import { z } from 'zod'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
12899
12857
  }],
12900
- fixedFiles: [{
12901
- path: "src/schema.ts",
12902
- source: "import { z } from 'zod'; export const Version = z.literal([1, 2, 3]);"
12903
- }],
12904
12858
  focusPath: "src/schema.ts",
12905
12859
  expectedCount: 1,
12906
12860
  public: true
@@ -12930,7 +12884,6 @@ var prefer_multi_value_zod_literal_default = createRule({
12930
12884
  documentation: PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION,
12931
12885
  meta: {
12932
12886
  type: "suggestion",
12933
- fixable: "code",
12934
12887
  docs: {
12935
12888
  description: "Use the Zod 4 multi-value literal API instead of a union of literal schemas."
12936
12889
  },
@@ -12994,15 +12947,10 @@ var prefer_multi_value_zod_literal_default = createRule({
12994
12947
  }
12995
12948
  if (values.every(isStaticString)) return;
12996
12949
  const namespace = node.callee.object.name;
12997
- const hasComments = context.sourceCode.getCommentsInside(node).length > 0;
12998
12950
  context.report({
12999
12951
  node,
13000
12952
  messageId: "useMultiValueLiteral",
13001
- data: { zod: namespace },
13002
- fix: hasComments ? null : (fixer) => fixer.replaceText(
13003
- node,
13004
- `${namespace}.literal([${values.map((value) => context.sourceCode.getText(value)).join(", ")}])`
13005
- )
12953
+ data: { zod: namespace }
13006
12954
  });
13007
12955
  }
13008
12956
  };
@@ -15433,13 +15381,14 @@ var MIN_RUN_LENGTH = 2;
15433
15381
  var PREFER_WHOLE_OBJECT_ASSERTION_DOCUMENTATION = {
15434
15382
  summary: "Collapse consecutive assertions on one object into a whole-object assertion so related mismatches are reported together.",
15435
15383
  rationale: "One whole-object assertion presents related expectations together and produces a complete structural diff.",
15436
- remediation: "Replace consecutive member assertions with one `toMatchObject` assertion.",
15384
+ remediation: "Consider one `toMatchObject` assertion for ordinary data objects. Preserve missing-property checks, identity, and getter or proxy behavior when deciding whether to combine assertions.",
15437
15385
  category: "testing",
15438
15386
  aliases: ["strict-test-assertions"],
15439
- autofix: "safe",
15387
+ autofix: "none",
15388
+ limitations: ["No automatic rewrite: whole-object matching can require previously absent properties and change observable getter or proxy reads."],
15440
15389
  examples: [
15441
15390
  { id: "whole-object", title: "Assert the object once", outcome: "no-match", files: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });" }], focusPath: "src/user.test.ts", expectedCount: 0, public: true },
15442
- { id: "member-run", title: "Do not split one object across assertions", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });\n" }] }
15391
+ { id: "member-run", title: "Consider grouping related data properties", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true }
15443
15392
  ]
15444
15393
  };
15445
15394
  function literalText(node, getText) {
@@ -15495,9 +15444,8 @@ var prefer_whole_object_assertion_default = createRule({
15495
15444
  docs: {
15496
15445
  description: "Collapse consecutive assertions on one object into a whole-object assertion so related mismatches are reported together."
15497
15446
  },
15498
- fixable: "code",
15499
15447
  messages: {
15500
- combineAssertions: "These {{count}} assertions each check one property of `{{receiver}}` against a literal, so the first mismatch hides the rest. Assert the object once: `expect({{receiver}}).toMatchObject({ \u2026 })`.",
15448
+ combineAssertions: "Consider combining these {{count}} assertions on `{{receiver}}` with `toMatchObject` when structural matching preserves property presence and getter or proxy behavior.",
15501
15449
  assertArrayOnce: "These {{count}} assertions check `{{receiver}}[0]`\u2026`{{receiver}}[{{last}}]` one at a time, which never checks how long `{{receiver}}` is \u2014 extra elements pass unnoticed. Assert the array once: `expect({{receiver}}).{{matcher}}([ \u2026 ])`."
15502
15450
  },
15503
15451
  schema: []
@@ -15568,11 +15516,6 @@ var prefer_whole_object_assertion_default = createRule({
15568
15516
  expectedIsLiteral: literal !== null
15569
15517
  };
15570
15518
  }
15571
- function hasInterveningComment(run) {
15572
- return run.some(
15573
- (assertion, index) => sourceCode.getCommentsInside(assertion.statement).length > 0 || index > 0 && sourceCode.getCommentsBefore(assertion.statement).length > 0
15574
- );
15575
- }
15576
15519
  function reportPropertyRun(run) {
15577
15520
  const tree = /* @__PURE__ */ new Map();
15578
15521
  const paths = [];
@@ -15619,16 +15562,10 @@ var prefer_whole_object_assertion_default = createRule({
15619
15562
  return;
15620
15563
  }
15621
15564
  const receiverText = `${sourceCode.getText(first.receiver)}${commonPrefix.map((name) => `.${name}`).join("")}`;
15622
- const renderTree = (value) => [...value.entries()].map(([name, child]) => `${name}: ${child instanceof Map ? `{ ${renderTree(child)} }` : child}`).join(", ");
15623
- const properties = renderTree(tree);
15624
15565
  context.report({
15625
15566
  node: first.statement,
15626
15567
  messageId: "combineAssertions",
15627
- data: { count: String(run.length), receiver: receiverText },
15628
- fix: hasInterveningComment(run) ? null : (fixer) => [
15629
- fixer.replaceText(first.statement, `expect(${receiverText}).toMatchObject({ ${properties} });`),
15630
- ...run.slice(1).map((assertion) => fixer.remove(assertion.statement))
15631
- ]
15568
+ data: { count: String(run.length), receiver: receiverText }
15632
15569
  });
15633
15570
  }
15634
15571
  function reportIndexRun(run) {
@@ -19752,7 +19689,7 @@ var RULES = {
19752
19689
  };
19753
19690
  var meta = {
19754
19691
  name: "@sarj/eslint-plugin",
19755
- version: "15.17.6"
19692
+ version: "15.17.8"
19756
19693
  };
19757
19694
  var APPLICATION_ONLY_RULES = [];
19758
19695
  var LIBRARY_IMPORT_POLICY = ["error", {
@@ -19763,6 +19700,7 @@ var ADVISORY_RULES = [
19763
19700
  "@sarj/excessive-commentary",
19764
19701
  "@sarj/no-bespoke-api-case-conversion",
19765
19702
  "@sarj/no-restated-comment",
19703
+ "@sarj/no-restated-jsdoc",
19766
19704
  "@sarj/prefer-millisecond-control-duration-schema",
19767
19705
  "@sarj/prefer-module-level-refined-schema",
19768
19706
  "@sarj/prefer-multi-value-zod-literal",
@@ -19773,6 +19711,7 @@ var ADVISORY_RULES = [
19773
19711
  "@sarj/prefer-nullish-filter-predicate",
19774
19712
  "@sarj/prefer-shared-zod-enum",
19775
19713
  "@sarj/prefer-switch-for-repeated-equality",
19714
+ "@sarj/prefer-whole-object-assertion",
19776
19715
  "@sarj/require-interface-for-exported-class",
19777
19716
  "@sarj/require-sql-access-class",
19778
19717
  "@sarj/sole-export-matches-filename"
@@ -19811,7 +19750,7 @@ var RECOMMENDED_RULES = {
19811
19750
  "@sarj/no-repeated-string-literal": "error",
19812
19751
  "@sarj/no-router-refresh-polling": "error",
19813
19752
  "@sarj/no-restated-comment": "warn",
19814
- "@sarj/no-restated-jsdoc": "error",
19753
+ "@sarj/no-restated-jsdoc": "warn",
19815
19754
  "@sarj/no-secret-in-log": "error",
19816
19755
  "@sarj/no-server-env-in-client-component": "error",
19817
19756
  "@sarj/no-select-star": "error",
@@ -19851,7 +19790,7 @@ var RECOMMENDED_RULES = {
19851
19790
  "@sarj/prefer-switch-for-repeated-equality": "warn",
19852
19791
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
19853
19792
  "@sarj/prefer-server-actions": "error",
19854
- "@sarj/prefer-whole-object-assertion": "error",
19793
+ "@sarj/prefer-whole-object-assertion": "warn",
19855
19794
  "@sarj/repeated-static-call-cases": "error",
19856
19795
  "@sarj/prefer-zod-infer": "error",
19857
19796
  "@sarj/require-assert-never": "error",
@@ -19907,7 +19846,7 @@ var STRICT_RULES = {
19907
19846
  "@sarj/no-repeated-string-literal": "error",
19908
19847
  "@sarj/no-router-refresh-polling": "error",
19909
19848
  "@sarj/no-restated-comment": "warn",
19910
- "@sarj/no-restated-jsdoc": "error",
19849
+ "@sarj/no-restated-jsdoc": "warn",
19911
19850
  "@sarj/no-secret-in-log": "error",
19912
19851
  "@sarj/no-server-env-in-client-component": "error",
19913
19852
  "@sarj/no-select-star": "error",
@@ -19948,7 +19887,7 @@ var STRICT_RULES = {
19948
19887
  "@sarj/prefer-switch-for-repeated-equality": "warn",
19949
19888
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
19950
19889
  "@sarj/prefer-server-actions": "error",
19951
- "@sarj/prefer-whole-object-assertion": "error",
19890
+ "@sarj/prefer-whole-object-assertion": "warn",
19952
19891
  "@sarj/repeated-static-call-cases": "error",
19953
19892
  "@sarj/prefer-zod-infer": "error",
19954
19893
  "@sarj/require-assert-never": "error",
package/dist/index.d.cts CHANGED
@@ -320,7 +320,7 @@ declare const RULES: {
320
320
  /** @deprecated All repositories use one policy; retained for import compatibility. */
321
321
  declare const APPLICATION_ONLY_RULES: readonly [];
322
322
  /** Rules staged as non-blocking warnings while corpus adoption evidence accumulates. */
323
- declare const ADVISORY_RULES: readonly ["@sarj/excessive-commentary", "@sarj/no-bespoke-api-case-conversion", "@sarj/no-restated-comment", "@sarj/prefer-millisecond-control-duration-schema", "@sarj/prefer-module-level-refined-schema", "@sarj/prefer-multi-value-zod-literal", "@sarj/prefer-named-callback-domain", "@sarj/prefer-named-complex-return-type", "@sarj/prefer-node-crypto-hash", "@sarj/prefer-node-fs-promises", "@sarj/prefer-nullish-filter-predicate", "@sarj/prefer-shared-zod-enum", "@sarj/prefer-switch-for-repeated-equality", "@sarj/require-interface-for-exported-class", "@sarj/require-sql-access-class", "@sarj/sole-export-matches-filename"];
323
+ declare const ADVISORY_RULES: readonly ["@sarj/excessive-commentary", "@sarj/no-bespoke-api-case-conversion", "@sarj/no-restated-comment", "@sarj/no-restated-jsdoc", "@sarj/prefer-millisecond-control-duration-schema", "@sarj/prefer-module-level-refined-schema", "@sarj/prefer-multi-value-zod-literal", "@sarj/prefer-named-callback-domain", "@sarj/prefer-named-complex-return-type", "@sarj/prefer-node-crypto-hash", "@sarj/prefer-node-fs-promises", "@sarj/prefer-nullish-filter-predicate", "@sarj/prefer-shared-zod-enum", "@sarj/prefer-switch-for-repeated-equality", "@sarj/prefer-whole-object-assertion", "@sarj/require-interface-for-exported-class", "@sarj/require-sql-access-class", "@sarj/sole-export-matches-filename"];
324
324
  declare const RECOMMENDED_RULES: {
325
325
  readonly "no-restricted-imports": readonly ["error", {
326
326
  readonly paths: {
@@ -538,7 +538,7 @@ declare const RECOMMENDED_RULES: {
538
538
  readonly "@sarj/no-repeated-string-literal": "error";
539
539
  readonly "@sarj/no-router-refresh-polling": "error";
540
540
  readonly "@sarj/no-restated-comment": "warn";
541
- readonly "@sarj/no-restated-jsdoc": "error";
541
+ readonly "@sarj/no-restated-jsdoc": "warn";
542
542
  readonly "@sarj/no-secret-in-log": "error";
543
543
  readonly "@sarj/no-server-env-in-client-component": "error";
544
544
  readonly "@sarj/no-select-star": "error";
@@ -582,7 +582,7 @@ declare const RECOMMENDED_RULES: {
582
582
  readonly requireSemanticTokens: true;
583
583
  }];
584
584
  readonly "@sarj/prefer-server-actions": "error";
585
- readonly "@sarj/prefer-whole-object-assertion": "error";
585
+ readonly "@sarj/prefer-whole-object-assertion": "warn";
586
586
  readonly "@sarj/repeated-static-call-cases": "error";
587
587
  readonly "@sarj/prefer-zod-infer": "error";
588
588
  readonly "@sarj/require-assert-never": "error";
@@ -821,7 +821,7 @@ declare const STRICT_RULES: {
821
821
  readonly "@sarj/no-repeated-string-literal": "error";
822
822
  readonly "@sarj/no-router-refresh-polling": "error";
823
823
  readonly "@sarj/no-restated-comment": "warn";
824
- readonly "@sarj/no-restated-jsdoc": "error";
824
+ readonly "@sarj/no-restated-jsdoc": "warn";
825
825
  readonly "@sarj/no-secret-in-log": "error";
826
826
  readonly "@sarj/no-server-env-in-client-component": "error";
827
827
  readonly "@sarj/no-select-star": "error";
@@ -866,7 +866,7 @@ declare const STRICT_RULES: {
866
866
  readonly requireSemanticTokens: true;
867
867
  }];
868
868
  readonly "@sarj/prefer-server-actions": "error";
869
- readonly "@sarj/prefer-whole-object-assertion": "error";
869
+ readonly "@sarj/prefer-whole-object-assertion": "warn";
870
870
  readonly "@sarj/repeated-static-call-cases": "error";
871
871
  readonly "@sarj/prefer-zod-infer": "error";
872
872
  readonly "@sarj/require-assert-never": "error";
@@ -893,7 +893,7 @@ type FlatPreset = {
893
893
  declare const PLUGIN: {
894
894
  readonly meta: {
895
895
  readonly name: "@sarj/eslint-plugin";
896
- readonly version: "15.17.6";
896
+ readonly version: "15.17.8";
897
897
  };
898
898
  readonly rules: {
899
899
  readonly "excessive-commentary": DocumentedRule<readonly [], "excessive">;
package/dist/index.d.ts CHANGED
@@ -320,7 +320,7 @@ declare const RULES: {
320
320
  /** @deprecated All repositories use one policy; retained for import compatibility. */
321
321
  declare const APPLICATION_ONLY_RULES: readonly [];
322
322
  /** Rules staged as non-blocking warnings while corpus adoption evidence accumulates. */
323
- declare const ADVISORY_RULES: readonly ["@sarj/excessive-commentary", "@sarj/no-bespoke-api-case-conversion", "@sarj/no-restated-comment", "@sarj/prefer-millisecond-control-duration-schema", "@sarj/prefer-module-level-refined-schema", "@sarj/prefer-multi-value-zod-literal", "@sarj/prefer-named-callback-domain", "@sarj/prefer-named-complex-return-type", "@sarj/prefer-node-crypto-hash", "@sarj/prefer-node-fs-promises", "@sarj/prefer-nullish-filter-predicate", "@sarj/prefer-shared-zod-enum", "@sarj/prefer-switch-for-repeated-equality", "@sarj/require-interface-for-exported-class", "@sarj/require-sql-access-class", "@sarj/sole-export-matches-filename"];
323
+ declare const ADVISORY_RULES: readonly ["@sarj/excessive-commentary", "@sarj/no-bespoke-api-case-conversion", "@sarj/no-restated-comment", "@sarj/no-restated-jsdoc", "@sarj/prefer-millisecond-control-duration-schema", "@sarj/prefer-module-level-refined-schema", "@sarj/prefer-multi-value-zod-literal", "@sarj/prefer-named-callback-domain", "@sarj/prefer-named-complex-return-type", "@sarj/prefer-node-crypto-hash", "@sarj/prefer-node-fs-promises", "@sarj/prefer-nullish-filter-predicate", "@sarj/prefer-shared-zod-enum", "@sarj/prefer-switch-for-repeated-equality", "@sarj/prefer-whole-object-assertion", "@sarj/require-interface-for-exported-class", "@sarj/require-sql-access-class", "@sarj/sole-export-matches-filename"];
324
324
  declare const RECOMMENDED_RULES: {
325
325
  readonly "no-restricted-imports": readonly ["error", {
326
326
  readonly paths: {
@@ -538,7 +538,7 @@ declare const RECOMMENDED_RULES: {
538
538
  readonly "@sarj/no-repeated-string-literal": "error";
539
539
  readonly "@sarj/no-router-refresh-polling": "error";
540
540
  readonly "@sarj/no-restated-comment": "warn";
541
- readonly "@sarj/no-restated-jsdoc": "error";
541
+ readonly "@sarj/no-restated-jsdoc": "warn";
542
542
  readonly "@sarj/no-secret-in-log": "error";
543
543
  readonly "@sarj/no-server-env-in-client-component": "error";
544
544
  readonly "@sarj/no-select-star": "error";
@@ -582,7 +582,7 @@ declare const RECOMMENDED_RULES: {
582
582
  readonly requireSemanticTokens: true;
583
583
  }];
584
584
  readonly "@sarj/prefer-server-actions": "error";
585
- readonly "@sarj/prefer-whole-object-assertion": "error";
585
+ readonly "@sarj/prefer-whole-object-assertion": "warn";
586
586
  readonly "@sarj/repeated-static-call-cases": "error";
587
587
  readonly "@sarj/prefer-zod-infer": "error";
588
588
  readonly "@sarj/require-assert-never": "error";
@@ -821,7 +821,7 @@ declare const STRICT_RULES: {
821
821
  readonly "@sarj/no-repeated-string-literal": "error";
822
822
  readonly "@sarj/no-router-refresh-polling": "error";
823
823
  readonly "@sarj/no-restated-comment": "warn";
824
- readonly "@sarj/no-restated-jsdoc": "error";
824
+ readonly "@sarj/no-restated-jsdoc": "warn";
825
825
  readonly "@sarj/no-secret-in-log": "error";
826
826
  readonly "@sarj/no-server-env-in-client-component": "error";
827
827
  readonly "@sarj/no-select-star": "error";
@@ -866,7 +866,7 @@ declare const STRICT_RULES: {
866
866
  readonly requireSemanticTokens: true;
867
867
  }];
868
868
  readonly "@sarj/prefer-server-actions": "error";
869
- readonly "@sarj/prefer-whole-object-assertion": "error";
869
+ readonly "@sarj/prefer-whole-object-assertion": "warn";
870
870
  readonly "@sarj/repeated-static-call-cases": "error";
871
871
  readonly "@sarj/prefer-zod-infer": "error";
872
872
  readonly "@sarj/require-assert-never": "error";
@@ -893,7 +893,7 @@ type FlatPreset = {
893
893
  declare const PLUGIN: {
894
894
  readonly meta: {
895
895
  readonly name: "@sarj/eslint-plugin";
896
- readonly version: "15.17.6";
896
+ readonly version: "15.17.8";
897
897
  };
898
898
  readonly rules: {
899
899
  readonly "excessive-commentary": DocumentedRule<readonly [], "excessive">;
package/dist/index.js CHANGED
@@ -6828,22 +6828,23 @@ var no_restated_comment_default = createRule({
6828
6828
  // src/rules/no-restated-jsdoc.ts
6829
6829
  import { AST_NODE_TYPES as AST_NODE_TYPES29 } from "@typescript-eslint/utils";
6830
6830
  var NO_RESTATED_JSDOC_DOCUMENTATION = {
6831
- summary: "Flag a JSDoc block whose description and tags only re-spell the signature they document.",
6831
+ summary: "Flag JSDoc prose that appears to repeat declaration names without adding behavioral information.",
6832
6832
  rationale: "Signature-only JSDoc duplicates type information and drifts without helping callers.",
6833
6833
  remediation: "Delete the block or document behavior, constraints, failures, or context the signature cannot express.",
6834
6834
  category: "maintainability",
6835
6835
  aliases: ["jsdoc-restates-signature"],
6836
6836
  autofix: "suggestion",
6837
- limitations: ["Generated files, detached blocks, unknown tags, empty blocks, and JSDoc with information absent from the signature are excluded."],
6837
+ limitations: ["Generated files, detached blocks, intervening comments, unknown tags, explicit JSDoc type payloads, empty blocks, and JSDoc with information absent from the signature are excluded.", "Negation, conditions, constraints, sentinel values, numeric details, and quoted text conservatively preserve the block, even when a declaration name contains the same words."],
6838
6838
  examples: [
6839
6839
  { id: "behavioral-jsdoc", title: "Document behavior absent from the signature", outcome: "no-match", files: [{ path: "src/users.ts", source: "/** Get the user while bypassing the read replica. */\nexport function getUser(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 0, public: true },
6840
- { id: "signature-jsdoc", title: "Remove JSDoc that only repeats the signature", outcome: "match", files: [{ path: "src/users.ts", source: "/** Get the user by id. */\nexport function getUserById(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 1, public: true }
6840
+ { id: "signature-jsdoc", title: "Remove JSDoc that only repeats the signature", outcome: "match", files: [{ path: "src/users.ts", source: "/** Get the user by id. */\nexport function getUserById(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 1, public: true },
6841
+ { id: "negated-behavior", scenarioId: "negation", title: "Keep behavior even when its words resemble the declaration", outcome: "no-match", files: [{ path: "src/users.ts", source: "/** Does not cache the user. */\nexport function cacheUser(user: unknown) { return user; }" }], focusPath: "src/users.ts", expectedCount: 0, public: true },
6842
+ { id: "repeated-behavior-name", scenarioId: "negation", title: "Review prose that merely repeats the declaration name", outcome: "match", files: [{ path: "src/users.ts", source: "/** Cache the user. */\nexport function cacheUser(user: unknown) { return user; }" }], focusPath: "src/users.ts", expectedCount: 1, public: true }
6841
6843
  ]
6842
6844
  };
6843
6845
  var MODELLED_TAGS = /* @__PURE__ */ new Set([
6844
6846
  "arg",
6845
6847
  "argument",
6846
- "async",
6847
6848
  "description",
6848
6849
  "param",
6849
6850
  "return",
@@ -6862,7 +6863,8 @@ var STOPWORDS2 = new Set(
6862
6863
  returns returning result optional required default true false null undefined
6863
6864
  string number boolean array list promise`.split(/\s+/)
6864
6865
  );
6865
- var WORD_RE2 = /[A-Za-z]+/g;
6866
+ var WORD_RE2 = new RegExp("\\p{L}+", "gu");
6867
+ var BEHAVIORAL_PROSE_RE = /\b(?:not|no|never|if|when|unless|must|should|may|can|could|would|optional|required|default|true|false|null|undefined|before|after|until|once|again|only|always)\b|\d|["'`<>=+*/%&|!~^-]/i;
6866
6868
  function parseJsDoc(value) {
6867
6869
  const lines = value.replace(/^\*/, "").split("\n").map((line) => line.replace(/^\s*\*?\s?/, ""));
6868
6870
  const description = [];
@@ -6882,12 +6884,13 @@ function parseJsDoc(value) {
6882
6884
  return { description: description.join("\n").trim(), tags };
6883
6885
  }
6884
6886
  function covered(text, known) {
6887
+ if (BEHAVIORAL_PROSE_RE.test(text)) return false;
6885
6888
  const stems = /* @__PURE__ */ new Set();
6886
6889
  for (const token of known) stems.add(stem(token));
6887
6890
  return proseTokens(text).every((word) => known.has(word) || stems.has(stem(word)));
6888
6891
  }
6889
6892
  function proseTokens(text) {
6890
- return (text.match(WORD_RE2) ?? []).map((word) => word.toLowerCase()).filter((word) => word.length > 1 && !STOPWORDS2.has(word));
6893
+ return (text.match(WORD_RE2) ?? []).map((word) => word.toLowerCase()).filter((word) => !STOPWORDS2.has(word));
6891
6894
  }
6892
6895
  function declarationNames(node) {
6893
6896
  switch (node.type) {
@@ -6944,11 +6947,11 @@ var no_restated_jsdoc_default = createRule({
6944
6947
  type: "suggestion",
6945
6948
  hasSuggestions: true,
6946
6949
  docs: {
6947
- description: "Flag a JSDoc block whose description and tags only re-spell the signature they document."
6950
+ description: "Flag JSDoc prose that appears to repeat declaration names without adding behavioral information."
6948
6951
  },
6949
6952
  schema: [],
6950
6953
  messages: {
6951
- restatesSignature: "JSDoc only re-spells the signature \u2014 delete it; if the signature still needs explanation, improve its names or types. Keep constraints, failures, and rationale.",
6954
+ restatesSignature: "JSDoc appears to repeat declaration names \u2014 consider removing repetition. Keep type contracts, constraints, failures, and rationale.",
6952
6955
  deleteBlock: "Delete the JSDoc block."
6953
6956
  }
6954
6957
  },
@@ -6971,8 +6974,9 @@ var no_restated_jsdoc_default = createRule({
6971
6974
  const tagNames = new Set(tags.map((tag) => tag.name));
6972
6975
  if ([...tagNames].some((name) => !MODELLED_TAGS.has(name))) continue;
6973
6976
  if (isProtected(describedText)) continue;
6974
- const token = sourceCode.getTokenAfter(comment, { includeComments: false });
6977
+ const token = sourceCode.getTokenAfter(comment, { includeComments: true });
6975
6978
  if (token === null || token.loc.start.line !== comment.loc.end.line + 1) continue;
6979
+ if (token.type === "Line" || token.type === "Block") continue;
6976
6980
  let node = sourceCode.getNodeByRangeIndex(token.range[0]);
6977
6981
  let declaration = null;
6978
6982
  while (node != null && node.type !== AST_NODE_TYPES29.Program) {
@@ -6983,6 +6987,8 @@ var no_restated_jsdoc_default = createRule({
6983
6987
  if (declaration === null) continue;
6984
6988
  const paramTags = tags.filter((tag) => PARAM_TAGS.has(tag.name));
6985
6989
  const returnTags = tags.filter((tag) => RETURN_TAGS.has(tag.name));
6990
+ if ([...paramTags, ...returnTags].some((tag) => /^\s*\{/.test(tag.text))) continue;
6991
+ if (paramTags.some((tag) => /^\s*(?:\[|[A-Za-z_$][\w$]*\.)/.test(tag.text))) continue;
6986
6992
  if (describedText.length === 0 && paramTags.length === 0 && returnTags.length === 0) {
6987
6993
  continue;
6988
6994
  }
@@ -10040,8 +10046,8 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
10040
10046
  rationale: "Wrapping a TypeScript enum preserves its emitted runtime object and duplicates the schema's value definition across two constructs.",
10041
10047
  remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
10042
10048
  category: "maintainability",
10043
- autofix: "safe",
10044
- limitations: ["Automatic fixes are limited to inline object literals whose unique values are all string literals."],
10049
+ autofix: "none",
10050
+ limitations: ["Migration is manual: replacing an enum-like object with a value array changes the public schema.enum keys and can affect consumers."],
10045
10051
  examples: [
10046
10052
  {
10047
10053
  id: "zod-literal-enum",
@@ -10059,8 +10065,7 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
10059
10065
  files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.nativeEnum({ Active: "active", Inactive: "inactive" });' }],
10060
10066
  focusPath: "src/status.ts",
10061
10067
  expectedCount: 1,
10062
- public: true,
10063
- fixedFiles: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.enum(["active", "inactive"]);' }]
10068
+ public: true
10064
10069
  }
10065
10070
  ]
10066
10071
  };
@@ -10085,26 +10090,6 @@ function unwrap2(node) {
10085
10090
  }
10086
10091
  return node;
10087
10092
  }
10088
- function stringValueTexts(node, sourceCode) {
10089
- const texts = [];
10090
- for (const prop of node.properties) {
10091
- if (prop.type !== AST_NODE_TYPES41.Property) {
10092
- return null;
10093
- }
10094
- if (prop.computed || prop.shorthand || prop.method || prop.kind !== "init") {
10095
- return null;
10096
- }
10097
- const value = prop.value;
10098
- if (value.type !== AST_NODE_TYPES41.Literal || typeof value.value !== "string") {
10099
- return null;
10100
- }
10101
- const text = sourceCode.getText(value);
10102
- if (!texts.includes(text)) {
10103
- texts.push(text);
10104
- }
10105
- }
10106
- return texts.length > 0 ? texts : null;
10107
- }
10108
10093
  function resolvesToLocalEnum(node, scope) {
10109
10094
  let current = scope;
10110
10095
  while (current !== null) {
@@ -10136,7 +10121,6 @@ var no_zod_native_enum_default = createRule({
10136
10121
  documentation: NO_ZOD_NATIVE_ENUM_DOCUMENTATION,
10137
10122
  meta: {
10138
10123
  type: "suggestion",
10139
- fixable: "code",
10140
10124
  docs: {
10141
10125
  description: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.'
10142
10126
  },
@@ -10181,30 +10165,6 @@ var no_zod_native_enum_default = createRule({
10181
10165
  }
10182
10166
  return false;
10183
10167
  }
10184
- function buildFix(node) {
10185
- const callee = node.callee;
10186
- if (callee.type !== AST_NODE_TYPES41.MemberExpression || callee.property.type !== AST_NODE_TYPES41.Identifier) {
10187
- return null;
10188
- }
10189
- const arg = node.arguments[0];
10190
- if (arg === void 0 || node.arguments.length !== 1 || arg.type === AST_NODE_TYPES41.SpreadElement) {
10191
- return null;
10192
- }
10193
- const inner = unwrap2(arg);
10194
- if (inner.type !== AST_NODE_TYPES41.ObjectExpression) {
10195
- return null;
10196
- }
10197
- const values = stringValueTexts(inner, sourceCode);
10198
- if (values === null) {
10199
- return null;
10200
- }
10201
- const property = callee.property;
10202
- const replacementArg = `[${values.join(", ")}]`;
10203
- return (fixer) => [
10204
- fixer.replaceText(property, "enum"),
10205
- fixer.replaceText(arg, replacementArg)
10206
- ];
10207
- }
10208
10168
  return {
10209
10169
  ImportDeclaration(node) {
10210
10170
  if (!isZodModule2(node.source.value)) {
@@ -10225,11 +10185,9 @@ var no_zod_native_enum_default = createRule({
10225
10185
  },
10226
10186
  CallExpression(node) {
10227
10187
  if (isZodMemberCall(node, "nativeEnum")) {
10228
- const fix = buildFix(node);
10229
10188
  context.report({
10230
10189
  node,
10231
- messageId: "nativeEnum",
10232
- ...fix === null ? {} : { fix }
10190
+ messageId: "nativeEnum"
10233
10191
  });
10234
10192
  return;
10235
10193
  }
@@ -12848,11 +12806,11 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
12848
12806
  rationale: "One multi-value literal expresses the same closed value domain without repeated schema wrappers.",
12849
12807
  remediation: "Replace the union with z.literal([value1, value2, ...]).",
12850
12808
  category: "maintainability",
12851
- autofix: "safe",
12809
+ autofix: "none",
12852
12810
  limitations: [
12853
12811
  "Bare zod imports are analyzed only when the rule option explicitly declares zodMajorVersion: 4; explicit zod/v4 entrypoints are self-declaring.",
12854
12812
  "All-string domains are left to zod/prefer-enum-over-literal-union.",
12855
- "A finding containing comments is not autofixed because moving its trivia is ambiguous."
12813
+ "Migration is manual: ZodLiteral and ZodUnion expose different introspection APIs and validation error shapes even when they accept the same values."
12856
12814
  ],
12857
12815
  examples: [
12858
12816
  {
@@ -12875,10 +12833,6 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
12875
12833
  path: "src/schema.ts",
12876
12834
  source: "import { z } from 'zod'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
12877
12835
  }],
12878
- fixedFiles: [{
12879
- path: "src/schema.ts",
12880
- source: "import { z } from 'zod'; export const Version = z.literal([1, 2, 3]);"
12881
- }],
12882
12836
  focusPath: "src/schema.ts",
12883
12837
  expectedCount: 1,
12884
12838
  public: true
@@ -12908,7 +12862,6 @@ var prefer_multi_value_zod_literal_default = createRule({
12908
12862
  documentation: PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION,
12909
12863
  meta: {
12910
12864
  type: "suggestion",
12911
- fixable: "code",
12912
12865
  docs: {
12913
12866
  description: "Use the Zod 4 multi-value literal API instead of a union of literal schemas."
12914
12867
  },
@@ -12972,15 +12925,10 @@ var prefer_multi_value_zod_literal_default = createRule({
12972
12925
  }
12973
12926
  if (values.every(isStaticString)) return;
12974
12927
  const namespace = node.callee.object.name;
12975
- const hasComments = context.sourceCode.getCommentsInside(node).length > 0;
12976
12928
  context.report({
12977
12929
  node,
12978
12930
  messageId: "useMultiValueLiteral",
12979
- data: { zod: namespace },
12980
- fix: hasComments ? null : (fixer) => fixer.replaceText(
12981
- node,
12982
- `${namespace}.literal([${values.map((value) => context.sourceCode.getText(value)).join(", ")}])`
12983
- )
12931
+ data: { zod: namespace }
12984
12932
  });
12985
12933
  }
12986
12934
  };
@@ -15419,13 +15367,14 @@ var MIN_RUN_LENGTH = 2;
15419
15367
  var PREFER_WHOLE_OBJECT_ASSERTION_DOCUMENTATION = {
15420
15368
  summary: "Collapse consecutive assertions on one object into a whole-object assertion so related mismatches are reported together.",
15421
15369
  rationale: "One whole-object assertion presents related expectations together and produces a complete structural diff.",
15422
- remediation: "Replace consecutive member assertions with one `toMatchObject` assertion.",
15370
+ remediation: "Consider one `toMatchObject` assertion for ordinary data objects. Preserve missing-property checks, identity, and getter or proxy behavior when deciding whether to combine assertions.",
15423
15371
  category: "testing",
15424
15372
  aliases: ["strict-test-assertions"],
15425
- autofix: "safe",
15373
+ autofix: "none",
15374
+ limitations: ["No automatic rewrite: whole-object matching can require previously absent properties and change observable getter or proxy reads."],
15426
15375
  examples: [
15427
15376
  { id: "whole-object", title: "Assert the object once", outcome: "no-match", files: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });" }], focusPath: "src/user.test.ts", expectedCount: 0, public: true },
15428
- { id: "member-run", title: "Do not split one object across assertions", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });\n" }] }
15377
+ { id: "member-run", title: "Consider grouping related data properties", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true }
15429
15378
  ]
15430
15379
  };
15431
15380
  function literalText(node, getText) {
@@ -15481,9 +15430,8 @@ var prefer_whole_object_assertion_default = createRule({
15481
15430
  docs: {
15482
15431
  description: "Collapse consecutive assertions on one object into a whole-object assertion so related mismatches are reported together."
15483
15432
  },
15484
- fixable: "code",
15485
15433
  messages: {
15486
- combineAssertions: "These {{count}} assertions each check one property of `{{receiver}}` against a literal, so the first mismatch hides the rest. Assert the object once: `expect({{receiver}}).toMatchObject({ \u2026 })`.",
15434
+ combineAssertions: "Consider combining these {{count}} assertions on `{{receiver}}` with `toMatchObject` when structural matching preserves property presence and getter or proxy behavior.",
15487
15435
  assertArrayOnce: "These {{count}} assertions check `{{receiver}}[0]`\u2026`{{receiver}}[{{last}}]` one at a time, which never checks how long `{{receiver}}` is \u2014 extra elements pass unnoticed. Assert the array once: `expect({{receiver}}).{{matcher}}([ \u2026 ])`."
15488
15436
  },
15489
15437
  schema: []
@@ -15554,11 +15502,6 @@ var prefer_whole_object_assertion_default = createRule({
15554
15502
  expectedIsLiteral: literal !== null
15555
15503
  };
15556
15504
  }
15557
- function hasInterveningComment(run) {
15558
- return run.some(
15559
- (assertion, index) => sourceCode.getCommentsInside(assertion.statement).length > 0 || index > 0 && sourceCode.getCommentsBefore(assertion.statement).length > 0
15560
- );
15561
- }
15562
15505
  function reportPropertyRun(run) {
15563
15506
  const tree = /* @__PURE__ */ new Map();
15564
15507
  const paths = [];
@@ -15605,16 +15548,10 @@ var prefer_whole_object_assertion_default = createRule({
15605
15548
  return;
15606
15549
  }
15607
15550
  const receiverText = `${sourceCode.getText(first.receiver)}${commonPrefix.map((name) => `.${name}`).join("")}`;
15608
- const renderTree = (value) => [...value.entries()].map(([name, child]) => `${name}: ${child instanceof Map ? `{ ${renderTree(child)} }` : child}`).join(", ");
15609
- const properties = renderTree(tree);
15610
15551
  context.report({
15611
15552
  node: first.statement,
15612
15553
  messageId: "combineAssertions",
15613
- data: { count: String(run.length), receiver: receiverText },
15614
- fix: hasInterveningComment(run) ? null : (fixer) => [
15615
- fixer.replaceText(first.statement, `expect(${receiverText}).toMatchObject({ ${properties} });`),
15616
- ...run.slice(1).map((assertion) => fixer.remove(assertion.statement))
15617
- ]
15554
+ data: { count: String(run.length), receiver: receiverText }
15618
15555
  });
15619
15556
  }
15620
15557
  function reportIndexRun(run) {
@@ -19747,7 +19684,7 @@ var RULES = {
19747
19684
  };
19748
19685
  var meta = {
19749
19686
  name: "@sarj/eslint-plugin",
19750
- version: "15.17.6"
19687
+ version: "15.17.8"
19751
19688
  };
19752
19689
  var APPLICATION_ONLY_RULES = [];
19753
19690
  var LIBRARY_IMPORT_POLICY = ["error", {
@@ -19758,6 +19695,7 @@ var ADVISORY_RULES = [
19758
19695
  "@sarj/excessive-commentary",
19759
19696
  "@sarj/no-bespoke-api-case-conversion",
19760
19697
  "@sarj/no-restated-comment",
19698
+ "@sarj/no-restated-jsdoc",
19761
19699
  "@sarj/prefer-millisecond-control-duration-schema",
19762
19700
  "@sarj/prefer-module-level-refined-schema",
19763
19701
  "@sarj/prefer-multi-value-zod-literal",
@@ -19768,6 +19706,7 @@ var ADVISORY_RULES = [
19768
19706
  "@sarj/prefer-nullish-filter-predicate",
19769
19707
  "@sarj/prefer-shared-zod-enum",
19770
19708
  "@sarj/prefer-switch-for-repeated-equality",
19709
+ "@sarj/prefer-whole-object-assertion",
19771
19710
  "@sarj/require-interface-for-exported-class",
19772
19711
  "@sarj/require-sql-access-class",
19773
19712
  "@sarj/sole-export-matches-filename"
@@ -19806,7 +19745,7 @@ var RECOMMENDED_RULES = {
19806
19745
  "@sarj/no-repeated-string-literal": "error",
19807
19746
  "@sarj/no-router-refresh-polling": "error",
19808
19747
  "@sarj/no-restated-comment": "warn",
19809
- "@sarj/no-restated-jsdoc": "error",
19748
+ "@sarj/no-restated-jsdoc": "warn",
19810
19749
  "@sarj/no-secret-in-log": "error",
19811
19750
  "@sarj/no-server-env-in-client-component": "error",
19812
19751
  "@sarj/no-select-star": "error",
@@ -19846,7 +19785,7 @@ var RECOMMENDED_RULES = {
19846
19785
  "@sarj/prefer-switch-for-repeated-equality": "warn",
19847
19786
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
19848
19787
  "@sarj/prefer-server-actions": "error",
19849
- "@sarj/prefer-whole-object-assertion": "error",
19788
+ "@sarj/prefer-whole-object-assertion": "warn",
19850
19789
  "@sarj/repeated-static-call-cases": "error",
19851
19790
  "@sarj/prefer-zod-infer": "error",
19852
19791
  "@sarj/require-assert-never": "error",
@@ -19902,7 +19841,7 @@ var STRICT_RULES = {
19902
19841
  "@sarj/no-repeated-string-literal": "error",
19903
19842
  "@sarj/no-router-refresh-polling": "error",
19904
19843
  "@sarj/no-restated-comment": "warn",
19905
- "@sarj/no-restated-jsdoc": "error",
19844
+ "@sarj/no-restated-jsdoc": "warn",
19906
19845
  "@sarj/no-secret-in-log": "error",
19907
19846
  "@sarj/no-server-env-in-client-component": "error",
19908
19847
  "@sarj/no-select-star": "error",
@@ -19943,7 +19882,7 @@ var STRICT_RULES = {
19943
19882
  "@sarj/prefer-switch-for-repeated-equality": "warn",
19944
19883
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
19945
19884
  "@sarj/prefer-server-actions": "error",
19946
- "@sarj/prefer-whole-object-assertion": "error",
19885
+ "@sarj/prefer-whole-object-assertion": "warn",
19947
19886
  "@sarj/repeated-static-call-cases": "error",
19948
19887
  "@sarj/prefer-zod-infer": "error",
19949
19888
  "@sarj/require-assert-never": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.17.6",
3
+ "version": "15.17.8",
4
4
  "packageManager": "npm@12.0.2",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",