@sarj/eslint-plugin 13.0.0 → 13.1.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/README.md CHANGED
@@ -8,7 +8,7 @@ Custom ESLint rules for hypermodern TypeScript / React / Next.js projects
8
8
 
9
9
  | Field | Value |
10
10
  | --- | --- |
11
- | Version | `13.0.0` |
11
+ | Version | `13.1.0` |
12
12
  | Registry | npm |
13
13
  | License | MIT |
14
14
  | Runtime | `node >=22.13.0` |
@@ -47,7 +47,7 @@ Custom ESLint rules for hypermodern TypeScript / React / Next.js projects
47
47
  | `eslint:no-insecure-random-id` | Disallow using `Math.random()` to generate identifiers, tokens, or secrets; use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead. | `error` | `none` |
48
48
  | `eslint:no-json-stringify-error` | Disallow `JSON.stringify` on an Error value; it yields `{}` because `message`/`stack` are non-enumerable. | `error` | `none` |
49
49
  | `eslint:no-log-only-catch` | Disallow `catch` clauses that only log (or silently do nothing) and then swallow the error; rethrow or handle it instead. | `error` | `none` |
50
- | `eslint:no-long-comment` | Flag unusually large unstructured prose blocks in implementation code. | `error` | `none` |
50
+ | `eslint:no-long-comment` | Flag unusually large unstructured JSDoc blocks in implementation code. | `error` | `none` |
51
51
  | `eslint:no-offset-pagination` | Disallow OFFSET pagination in embedded SQL; it is O(N) per page and drops or repeats rows under concurrent writes. Use a keyset cursor. | `error` | `none` |
52
52
  | `eslint:no-positional-tuple-return` | Disallow returning a multi-field tuple from an exported function; return a named object so call sites cannot mismatch slots. | `error` | `none` |
53
53
  | `eslint:no-raw-env` | Disallow direct `process.env` and `import.meta.env` reads outside validated boundaries. | `error` | `none` |
package/dist/index.cjs CHANGED
@@ -1142,6 +1142,7 @@ var STEP_NARRATION_RE = /^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|a
1142
1142
  var META_COMMENTARY_RE = /\b(?:keeping (?:it|this) simple|could be (?:refactored|improved|cleaned up|simplified)|refactor(?:ed|ing)? (?:later|this)|not sure (?:if|whether|why|how)|quick[- ](?:and[- ]dirty|fix)|(?:a |bit of a )?hacky|is a hack|temporary (?:solution|workaround|fix|hack)|revisit (?:this|later|below)|clean (?:this|it) up|not ideal|placeholder for now)\b/i;
1143
1143
  var EDITORIAL_PLACEHOLDER_RE = /^(?:(?:implementation omitted|existing code here|your code here|rest of (?:the )?code (?:is )?unchanged|same as above|placeholder implementation)\s*[.!]?|in a real (?:app(?:lication)?|implementation),?\s+(?:this|we|you|it)\s+would\s+(?:call|fetch|generate|download|persist|save|send|store|write)\b[^,;]*[.!]?)$/i;
1144
1144
  var FOR_NOW_RE = /\bfor now\b/i;
1145
+ var JSDOC_DEBT_RE = /^@?(?:todo|fixme)\b/i;
1145
1146
  var DEFERRAL_STOPWORDS = /* @__PURE__ */ new Set([
1146
1147
  "a",
1147
1148
  "an",
@@ -1270,6 +1271,7 @@ var DIAGRAM_ARROW_RE = /[-=~]{2,}>|<[-=~]{2,}/;
1270
1271
  var CODE_KEYWORD_RE = /^(import |export |const |let |var |function\b|class |interface |type \w|enum |return\b|throw |await |async |if\s*\(|for\s*\(|while\s*\(|switch\s*\(|new |console\.)/;
1271
1272
  var CODE_TAIL_RE = /[;{}()]\s*$|=>\s*$|,\s*$/;
1272
1273
  var ASSIGN_RE = /^[A-Za-z_$][\w.$[\]]*\s*(?:=(?![=>])|\+=|-=|\*=)\s*\S.*[;)}\]]\s*$/;
1274
+ var DECLARATION_RE = /^(?:export\s+)?(?:declare\s+)?(?:const|let|var)\s+[A-Za-z_$][\w$]*(?:\s*:\s*[^=]+)?\s*=\s*(?:[A-Za-z_$][\w.$]*(?:\s*\(|\s*$)|["'`]|\[|\{|\d|true\b|false\b|null\b|undefined\b|new\b|await\b|async\b|function\b|class\b)/;
1273
1275
  var CALL_RE = /^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
1274
1276
  var ASSERTION_CODE_RE = /^(?:await\s+)?(?:expect(?:TypeOf)?|assert(?:\.\w+)?)\s*\(/;
1275
1277
  var HTTP_CONTRACT_RE = /\b(?:GET|HEAD|OPTIONS|PATCH|POST|PUT|DELETE)\s+(?:https?:\/\/|\/|\{[A-Za-z_$])/;
@@ -1297,6 +1299,7 @@ function looksLikeCode(text, allowCall = true) {
1297
1299
  if (!t) return false;
1298
1300
  if (PROSE_ASSIGNMENT_RE.test(t)) return false;
1299
1301
  if (CODE_KEYWORD_RE.test(t) && CODE_TAIL_RE.test(t)) return true;
1302
+ if (DECLARATION_RE.test(t)) return true;
1300
1303
  if (ASSIGN_RE.test(t)) return true;
1301
1304
  if (ASSERTION_CODE_RE.test(t)) return true;
1302
1305
  return allowCall && CALL_RE.test(t);
@@ -1626,6 +1629,11 @@ var no_comment_cruft_default = createRule({
1626
1629
  }
1627
1630
  if (wallMembers.has(comment)) continue;
1628
1631
  if (isJsDoc(comment)) {
1632
+ const debt = comment.value.split("\n").map(stripCommentMarker).find((line) => JSDOC_DEBT_RE.test(line));
1633
+ if (debt !== void 0 && !runCitesAReference(comments, i)) {
1634
+ context.report({ node: comment, messageId: "untrackedTodo" });
1635
+ continue;
1636
+ }
1629
1637
  if (isStandalone(comment) && isSectionJsDoc(comment)) {
1630
1638
  context.report({ node: comment, messageId: "sectionBanner" });
1631
1639
  }
@@ -4432,11 +4440,11 @@ function typedFunction(node) {
4432
4440
 
4433
4441
  // src/rules/no-long-comment.ts
4434
4442
  var noLongCommentDocumentation = {
4435
- summary: "Flag unusually large unstructured prose blocks in implementation code.",
4443
+ summary: "Flag unusually large unstructured JSDoc blocks in implementation code.",
4436
4444
  rationale: "Large narrative comments become stale and obscure the local facts that belong beside the code.",
4437
4445
  remediation: "Keep only durable local constraints and express the remaining behavior in code.",
4438
4446
  category: "maintainability",
4439
- limitations: ["Structured API docs, tests, scripts, generated files, and versioned dependencies are excluded."],
4447
+ limitations: ["Only JSDoc blocks are inspected; structured API docs, tests, scripts, generated files, and versioned dependencies are excluded."],
4440
4448
  examples: [
4441
4449
  { id: "local-fact", title: "Keep a concise local fact", outcome: "no-match", files: [{ path: "src/cache.ts", source: "// The cache is process local.\nconst cache = new Map();" }], focusPath: "src/cache.ts", expectedCount: 0, public: true },
4442
4450
  { id: "prose-wall", title: "Avoid an unstructured prose wall", outcome: "match", files: [{ path: "src/chart.ts", source: "/** One. Two. Three. Four. Five. Six. Seven. Eight. */\nconst chart = createChart();" }], focusPath: "src/chart.ts", expectedCount: 1, public: true }
@@ -4464,7 +4472,7 @@ var no_long_comment_default = createRule({
4464
4472
  documentation: noLongCommentDocumentation,
4465
4473
  meta: {
4466
4474
  type: "suggestion",
4467
- docs: { description: "Flag unusually large unstructured prose blocks in implementation code." },
4475
+ docs: { description: "Flag unusually large unstructured JSDoc blocks in implementation code." },
4468
4476
  schema: [],
4469
4477
  messages: {
4470
4478
  tooLong: "Comment is an unusually large prose block \u2014 keep the local facts and clarify the code itself."
@@ -5409,6 +5417,7 @@ var MIN_DISTINCT_SCOPES = 2;
5409
5417
  var PREVIEW_LENGTH = 40;
5410
5418
  var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
5411
5419
  var IDENTIFIER_RE = /^[a-z_][a-z0-9_.]*$/;
5420
+ var URL_PATH_RE = /^\/(?=[^\s]*[A-Za-z0-9])[A-Za-z0-9._~!$&'()*+,;=:@%/?#{}\u005B\u005D-]+$/;
5412
5421
  var FUNCTION_TYPES4 = /* @__PURE__ */ new Set([
5413
5422
  import_utils29.AST_NODE_TYPES.FunctionDeclaration,
5414
5423
  import_utils29.AST_NODE_TYPES.FunctionExpression,
@@ -5416,7 +5425,7 @@ var FUNCTION_TYPES4 = /* @__PURE__ */ new Set([
5416
5425
  ]);
5417
5426
  var noRepeatedStringLiteralDocumentation = {
5418
5427
  summary: "Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant.",
5419
- rationale: "Independent copies of a structured value can diverge and silently change behavior.",
5428
+ rationale: "Independent copies of a query, route template, or identifier can diverge and silently change behavior.",
5420
5429
  remediation: "Extract the repeated value to one module-level constant and reference it from each function.",
5421
5430
  category: "maintainability",
5422
5431
  limitations: ["Test files, short strings, prose, substitutions, module sources, JSX attributes, and repetition within one function are excluded."],
@@ -5426,7 +5435,7 @@ var noRepeatedStringLiteralDocumentation = {
5426
5435
  ]
5427
5436
  };
5428
5437
  function isStructured(value) {
5429
- return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value);
5438
+ return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
5430
5439
  }
5431
5440
  function preview(value) {
5432
5441
  const oneLine = value.replaceAll("\n", " ").trim();
@@ -5445,8 +5454,9 @@ function isScaffolding(node) {
5445
5454
  if (parent === void 0) {
5446
5455
  return true;
5447
5456
  }
5457
+ const isNonComputedPropertyKey = (parent.type === import_utils29.AST_NODE_TYPES.Property || parent.type === import_utils29.AST_NODE_TYPES.PropertyDefinition || parent.type === import_utils29.AST_NODE_TYPES.MethodDefinition || parent.type === import_utils29.AST_NODE_TYPES.AccessorProperty) && parent.key === node && !parent.computed;
5448
5458
  const isRequireSource = parent.type === import_utils29.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils29.AST_NODE_TYPES.Identifier && parent.callee.name === "require";
5449
- return parent.type === import_utils29.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils29.AST_NODE_TYPES.ImportExpression || parent.type === import_utils29.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils29.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils29.AST_NODE_TYPES.TSImportType || parent.type === import_utils29.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils29.AST_NODE_TYPES.TSLiteralType || isRequireSource;
5459
+ return parent.type === import_utils29.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils29.AST_NODE_TYPES.ImportExpression || parent.type === import_utils29.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils29.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils29.AST_NODE_TYPES.TSImportType || parent.type === import_utils29.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils29.AST_NODE_TYPES.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
5450
5460
  }
5451
5461
  var no_repeated_string_literal_default = createRule({
5452
5462
  name: "no-repeated-string-literal",
@@ -5463,7 +5473,7 @@ var no_repeated_string_literal_default = createRule({
5463
5473
  },
5464
5474
  defaultOptions: [],
5465
5475
  create(context) {
5466
- if (isTestFile(context.filename)) {
5476
+ if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
5467
5477
  return {};
5468
5478
  }
5469
5479
  const occurrences = /* @__PURE__ */ new Map();
@@ -5783,10 +5793,14 @@ var no_restated_jsdoc_default = createRule({
5783
5793
  for (const comment of sourceCode.getAllComments()) {
5784
5794
  if (comment.type !== "Block" || !comment.value.startsWith("*")) continue;
5785
5795
  const { description, tags } = parseJsDoc(comment.value);
5786
- if (DIRECTIVE_RE4.test(description)) continue;
5796
+ const describedText = [
5797
+ description,
5798
+ ...tags.filter((tag) => tag.name === "description").map((tag) => tag.text)
5799
+ ].filter((text) => text.length > 0).join("\n");
5800
+ if (DIRECTIVE_RE4.test(describedText)) continue;
5787
5801
  const tagNames = new Set(tags.map((tag) => tag.name));
5788
5802
  if ([...tagNames].some((name) => !MODELLED_TAGS.has(name))) continue;
5789
- if (isProtected(description)) continue;
5803
+ if (isProtected(describedText)) continue;
5790
5804
  const token = sourceCode.getTokenAfter(comment, { includeComments: false });
5791
5805
  if (token === null || token.loc.start.line !== comment.loc.end.line + 1) continue;
5792
5806
  let node = sourceCode.getNodeByRangeIndex(token.range[0]);
@@ -5799,13 +5813,14 @@ var no_restated_jsdoc_default = createRule({
5799
5813
  if (declaration === null) continue;
5800
5814
  const paramTags = tags.filter((tag) => PARAM_TAGS.has(tag.name));
5801
5815
  const returnTags = tags.filter((tag) => RETURN_TAGS.has(tag.name));
5802
- if (description.length === 0 && paramTags.length === 0 && returnTags.length === 0) {
5816
+ if (describedText.length === 0 && paramTags.length === 0 && returnTags.length === 0) {
5803
5817
  continue;
5804
5818
  }
5819
+ if ((paramTags.length > 0 || returnTags.length > 0) && documentsTypedFunction(sourceCode, comment)) continue;
5805
5820
  const nameTokens = tokensOf([declaration.name]);
5806
5821
  const paramTokens = tokensOf(declaration.params);
5807
5822
  const known = /* @__PURE__ */ new Set([...nameTokens, ...paramTokens]);
5808
- let addsNothing = covered(description, known);
5823
+ let addsNothing = covered(describedText, known);
5809
5824
  for (const tag of paramTags) {
5810
5825
  const text = tag.text.replace(/^\{[^}]*\}\s*/, "");
5811
5826
  const match = /^\[?([A-Za-z_$][\w.$]*)\]?\s*-?\s*([\s\S]*)$/.exec(text);
@@ -5813,7 +5828,13 @@ var no_restated_jsdoc_default = createRule({
5813
5828
  addsNothing = false;
5814
5829
  break;
5815
5830
  }
5816
- const own = /* @__PURE__ */ new Set([...splitIdentifier(match[1]?.split(".").pop() ?? ""), ...nameTokens]);
5831
+ const path = match[1] ?? "";
5832
+ const root = path.split(".")[0] ?? "";
5833
+ if (!declaration.params.includes(root)) {
5834
+ addsNothing = false;
5835
+ break;
5836
+ }
5837
+ const own = /* @__PURE__ */ new Set([...splitIdentifier(path.split(".").pop() ?? ""), ...nameTokens]);
5817
5838
  if (!covered(match[2] ?? "", own)) {
5818
5839
  addsNothing = false;
5819
5840
  break;
@@ -6638,10 +6659,10 @@ var no_sentinel_return_on_catch_default = createRule({
6638
6659
  return false;
6639
6660
  }
6640
6661
  if (matcher.isLoggingCall(current)) {
6641
- return true;
6662
+ return caughtName === null || argsIncludeBinding(current.arguments, caughtName);
6642
6663
  }
6643
6664
  const name = calleeName2(current.callee);
6644
- return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
6665
+ return name !== null && REPORT_NAME_RE.test(name) && (caughtName === null || argsIncludeBinding(current.arguments, caughtName));
6645
6666
  });
6646
6667
  }
6647
6668
  return {
@@ -8110,7 +8131,7 @@ var no_type_member_comment_wall_default = createRule({
8110
8131
  claimed.add(comment);
8111
8132
  commented += 1;
8112
8133
  const body2 = commentBody(comment);
8113
- if (body2.length === 0 || carriesValue(body2)) continue;
8134
+ if (body2.length === 0 || carriesValue(body2) || isTagsOnly(body2)) continue;
8114
8135
  if (novelWords(body2, knownTokens(sourceCode.getText(member))) <= options.maxNovelWords) {
8115
8136
  restated += 1;
8116
8137
  }
@@ -14016,7 +14037,7 @@ var rules = {
14016
14037
  };
14017
14038
  var meta = {
14018
14039
  name: "@sarj/eslint-plugin",
14019
- version: "13.0.0"
14040
+ version: "13.1.0"
14020
14041
  };
14021
14042
  var applicationOnlyRules = [
14022
14043
  "no-restricted-library-load",
package/dist/index.d.cts CHANGED
@@ -399,7 +399,7 @@ type FlatPreset = {
399
399
  declare const plugin: {
400
400
  readonly meta: {
401
401
  readonly name: "@sarj/eslint-plugin";
402
- readonly version: "13.0.0";
402
+ readonly version: "13.1.0";
403
403
  };
404
404
  readonly rules: {
405
405
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
package/dist/index.d.ts CHANGED
@@ -399,7 +399,7 @@ type FlatPreset = {
399
399
  declare const plugin: {
400
400
  readonly meta: {
401
401
  readonly name: "@sarj/eslint-plugin";
402
- readonly version: "13.0.0";
402
+ readonly version: "13.1.0";
403
403
  };
404
404
  readonly rules: {
405
405
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
package/dist/index.js CHANGED
@@ -1099,6 +1099,7 @@ var STEP_NARRATION_RE = /^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|a
1099
1099
  var META_COMMENTARY_RE = /\b(?:keeping (?:it|this) simple|could be (?:refactored|improved|cleaned up|simplified)|refactor(?:ed|ing)? (?:later|this)|not sure (?:if|whether|why|how)|quick[- ](?:and[- ]dirty|fix)|(?:a |bit of a )?hacky|is a hack|temporary (?:solution|workaround|fix|hack)|revisit (?:this|later|below)|clean (?:this|it) up|not ideal|placeholder for now)\b/i;
1100
1100
  var EDITORIAL_PLACEHOLDER_RE = /^(?:(?:implementation omitted|existing code here|your code here|rest of (?:the )?code (?:is )?unchanged|same as above|placeholder implementation)\s*[.!]?|in a real (?:app(?:lication)?|implementation),?\s+(?:this|we|you|it)\s+would\s+(?:call|fetch|generate|download|persist|save|send|store|write)\b[^,;]*[.!]?)$/i;
1101
1101
  var FOR_NOW_RE = /\bfor now\b/i;
1102
+ var JSDOC_DEBT_RE = /^@?(?:todo|fixme)\b/i;
1102
1103
  var DEFERRAL_STOPWORDS = /* @__PURE__ */ new Set([
1103
1104
  "a",
1104
1105
  "an",
@@ -1227,6 +1228,7 @@ var DIAGRAM_ARROW_RE = /[-=~]{2,}>|<[-=~]{2,}/;
1227
1228
  var CODE_KEYWORD_RE = /^(import |export |const |let |var |function\b|class |interface |type \w|enum |return\b|throw |await |async |if\s*\(|for\s*\(|while\s*\(|switch\s*\(|new |console\.)/;
1228
1229
  var CODE_TAIL_RE = /[;{}()]\s*$|=>\s*$|,\s*$/;
1229
1230
  var ASSIGN_RE = /^[A-Za-z_$][\w.$[\]]*\s*(?:=(?![=>])|\+=|-=|\*=)\s*\S.*[;)}\]]\s*$/;
1231
+ var DECLARATION_RE = /^(?:export\s+)?(?:declare\s+)?(?:const|let|var)\s+[A-Za-z_$][\w$]*(?:\s*:\s*[^=]+)?\s*=\s*(?:[A-Za-z_$][\w.$]*(?:\s*\(|\s*$)|["'`]|\[|\{|\d|true\b|false\b|null\b|undefined\b|new\b|await\b|async\b|function\b|class\b)/;
1230
1232
  var CALL_RE = /^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
1231
1233
  var ASSERTION_CODE_RE = /^(?:await\s+)?(?:expect(?:TypeOf)?|assert(?:\.\w+)?)\s*\(/;
1232
1234
  var HTTP_CONTRACT_RE = /\b(?:GET|HEAD|OPTIONS|PATCH|POST|PUT|DELETE)\s+(?:https?:\/\/|\/|\{[A-Za-z_$])/;
@@ -1254,6 +1256,7 @@ function looksLikeCode(text, allowCall = true) {
1254
1256
  if (!t) return false;
1255
1257
  if (PROSE_ASSIGNMENT_RE.test(t)) return false;
1256
1258
  if (CODE_KEYWORD_RE.test(t) && CODE_TAIL_RE.test(t)) return true;
1259
+ if (DECLARATION_RE.test(t)) return true;
1257
1260
  if (ASSIGN_RE.test(t)) return true;
1258
1261
  if (ASSERTION_CODE_RE.test(t)) return true;
1259
1262
  return allowCall && CALL_RE.test(t);
@@ -1583,6 +1586,11 @@ var no_comment_cruft_default = createRule({
1583
1586
  }
1584
1587
  if (wallMembers.has(comment)) continue;
1585
1588
  if (isJsDoc(comment)) {
1589
+ const debt = comment.value.split("\n").map(stripCommentMarker).find((line) => JSDOC_DEBT_RE.test(line));
1590
+ if (debt !== void 0 && !runCitesAReference(comments, i)) {
1591
+ context.report({ node: comment, messageId: "untrackedTodo" });
1592
+ continue;
1593
+ }
1586
1594
  if (isStandalone(comment) && isSectionJsDoc(comment)) {
1587
1595
  context.report({ node: comment, messageId: "sectionBanner" });
1588
1596
  }
@@ -4391,11 +4399,11 @@ function typedFunction(node) {
4391
4399
 
4392
4400
  // src/rules/no-long-comment.ts
4393
4401
  var noLongCommentDocumentation = {
4394
- summary: "Flag unusually large unstructured prose blocks in implementation code.",
4402
+ summary: "Flag unusually large unstructured JSDoc blocks in implementation code.",
4395
4403
  rationale: "Large narrative comments become stale and obscure the local facts that belong beside the code.",
4396
4404
  remediation: "Keep only durable local constraints and express the remaining behavior in code.",
4397
4405
  category: "maintainability",
4398
- limitations: ["Structured API docs, tests, scripts, generated files, and versioned dependencies are excluded."],
4406
+ limitations: ["Only JSDoc blocks are inspected; structured API docs, tests, scripts, generated files, and versioned dependencies are excluded."],
4399
4407
  examples: [
4400
4408
  { id: "local-fact", title: "Keep a concise local fact", outcome: "no-match", files: [{ path: "src/cache.ts", source: "// The cache is process local.\nconst cache = new Map();" }], focusPath: "src/cache.ts", expectedCount: 0, public: true },
4401
4409
  { id: "prose-wall", title: "Avoid an unstructured prose wall", outcome: "match", files: [{ path: "src/chart.ts", source: "/** One. Two. Three. Four. Five. Six. Seven. Eight. */\nconst chart = createChart();" }], focusPath: "src/chart.ts", expectedCount: 1, public: true }
@@ -4423,7 +4431,7 @@ var no_long_comment_default = createRule({
4423
4431
  documentation: noLongCommentDocumentation,
4424
4432
  meta: {
4425
4433
  type: "suggestion",
4426
- docs: { description: "Flag unusually large unstructured prose blocks in implementation code." },
4434
+ docs: { description: "Flag unusually large unstructured JSDoc blocks in implementation code." },
4427
4435
  schema: [],
4428
4436
  messages: {
4429
4437
  tooLong: "Comment is an unusually large prose block \u2014 keep the local facts and clarify the code itself."
@@ -5368,6 +5376,7 @@ var MIN_DISTINCT_SCOPES = 2;
5368
5376
  var PREVIEW_LENGTH = 40;
5369
5377
  var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
5370
5378
  var IDENTIFIER_RE = /^[a-z_][a-z0-9_.]*$/;
5379
+ var URL_PATH_RE = /^\/(?=[^\s]*[A-Za-z0-9])[A-Za-z0-9._~!$&'()*+,;=:@%/?#{}\u005B\u005D-]+$/;
5371
5380
  var FUNCTION_TYPES4 = /* @__PURE__ */ new Set([
5372
5381
  AST_NODE_TYPES21.FunctionDeclaration,
5373
5382
  AST_NODE_TYPES21.FunctionExpression,
@@ -5375,7 +5384,7 @@ var FUNCTION_TYPES4 = /* @__PURE__ */ new Set([
5375
5384
  ]);
5376
5385
  var noRepeatedStringLiteralDocumentation = {
5377
5386
  summary: "Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant.",
5378
- rationale: "Independent copies of a structured value can diverge and silently change behavior.",
5387
+ rationale: "Independent copies of a query, route template, or identifier can diverge and silently change behavior.",
5379
5388
  remediation: "Extract the repeated value to one module-level constant and reference it from each function.",
5380
5389
  category: "maintainability",
5381
5390
  limitations: ["Test files, short strings, prose, substitutions, module sources, JSX attributes, and repetition within one function are excluded."],
@@ -5385,7 +5394,7 @@ var noRepeatedStringLiteralDocumentation = {
5385
5394
  ]
5386
5395
  };
5387
5396
  function isStructured(value) {
5388
- return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value);
5397
+ return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
5389
5398
  }
5390
5399
  function preview(value) {
5391
5400
  const oneLine = value.replaceAll("\n", " ").trim();
@@ -5404,8 +5413,9 @@ function isScaffolding(node) {
5404
5413
  if (parent === void 0) {
5405
5414
  return true;
5406
5415
  }
5416
+ const isNonComputedPropertyKey = (parent.type === AST_NODE_TYPES21.Property || parent.type === AST_NODE_TYPES21.PropertyDefinition || parent.type === AST_NODE_TYPES21.MethodDefinition || parent.type === AST_NODE_TYPES21.AccessorProperty) && parent.key === node && !parent.computed;
5407
5417
  const isRequireSource = parent.type === AST_NODE_TYPES21.CallExpression && parent.callee.type === AST_NODE_TYPES21.Identifier && parent.callee.name === "require";
5408
- return parent.type === AST_NODE_TYPES21.ImportDeclaration || parent.type === AST_NODE_TYPES21.ImportExpression || parent.type === AST_NODE_TYPES21.ExportNamedDeclaration || parent.type === AST_NODE_TYPES21.ExportAllDeclaration || parent.type === AST_NODE_TYPES21.TSImportType || parent.type === AST_NODE_TYPES21.JSXAttribute || parent.type === AST_NODE_TYPES21.TSLiteralType || isRequireSource;
5418
+ return parent.type === AST_NODE_TYPES21.ImportDeclaration || parent.type === AST_NODE_TYPES21.ImportExpression || parent.type === AST_NODE_TYPES21.ExportNamedDeclaration || parent.type === AST_NODE_TYPES21.ExportAllDeclaration || parent.type === AST_NODE_TYPES21.TSImportType || parent.type === AST_NODE_TYPES21.JSXAttribute || parent.type === AST_NODE_TYPES21.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
5409
5419
  }
5410
5420
  var no_repeated_string_literal_default = createRule({
5411
5421
  name: "no-repeated-string-literal",
@@ -5422,7 +5432,7 @@ var no_repeated_string_literal_default = createRule({
5422
5432
  },
5423
5433
  defaultOptions: [],
5424
5434
  create(context) {
5425
- if (isTestFile(context.filename)) {
5435
+ if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
5426
5436
  return {};
5427
5437
  }
5428
5438
  const occurrences = /* @__PURE__ */ new Map();
@@ -5742,10 +5752,14 @@ var no_restated_jsdoc_default = createRule({
5742
5752
  for (const comment of sourceCode.getAllComments()) {
5743
5753
  if (comment.type !== "Block" || !comment.value.startsWith("*")) continue;
5744
5754
  const { description, tags } = parseJsDoc(comment.value);
5745
- if (DIRECTIVE_RE4.test(description)) continue;
5755
+ const describedText = [
5756
+ description,
5757
+ ...tags.filter((tag) => tag.name === "description").map((tag) => tag.text)
5758
+ ].filter((text) => text.length > 0).join("\n");
5759
+ if (DIRECTIVE_RE4.test(describedText)) continue;
5746
5760
  const tagNames = new Set(tags.map((tag) => tag.name));
5747
5761
  if ([...tagNames].some((name) => !MODELLED_TAGS.has(name))) continue;
5748
- if (isProtected(description)) continue;
5762
+ if (isProtected(describedText)) continue;
5749
5763
  const token = sourceCode.getTokenAfter(comment, { includeComments: false });
5750
5764
  if (token === null || token.loc.start.line !== comment.loc.end.line + 1) continue;
5751
5765
  let node = sourceCode.getNodeByRangeIndex(token.range[0]);
@@ -5758,13 +5772,14 @@ var no_restated_jsdoc_default = createRule({
5758
5772
  if (declaration === null) continue;
5759
5773
  const paramTags = tags.filter((tag) => PARAM_TAGS.has(tag.name));
5760
5774
  const returnTags = tags.filter((tag) => RETURN_TAGS.has(tag.name));
5761
- if (description.length === 0 && paramTags.length === 0 && returnTags.length === 0) {
5775
+ if (describedText.length === 0 && paramTags.length === 0 && returnTags.length === 0) {
5762
5776
  continue;
5763
5777
  }
5778
+ if ((paramTags.length > 0 || returnTags.length > 0) && documentsTypedFunction(sourceCode, comment)) continue;
5764
5779
  const nameTokens = tokensOf([declaration.name]);
5765
5780
  const paramTokens = tokensOf(declaration.params);
5766
5781
  const known = /* @__PURE__ */ new Set([...nameTokens, ...paramTokens]);
5767
- let addsNothing = covered(description, known);
5782
+ let addsNothing = covered(describedText, known);
5768
5783
  for (const tag of paramTags) {
5769
5784
  const text = tag.text.replace(/^\{[^}]*\}\s*/, "");
5770
5785
  const match = /^\[?([A-Za-z_$][\w.$]*)\]?\s*-?\s*([\s\S]*)$/.exec(text);
@@ -5772,7 +5787,13 @@ var no_restated_jsdoc_default = createRule({
5772
5787
  addsNothing = false;
5773
5788
  break;
5774
5789
  }
5775
- const own = /* @__PURE__ */ new Set([...splitIdentifier(match[1]?.split(".").pop() ?? ""), ...nameTokens]);
5790
+ const path = match[1] ?? "";
5791
+ const root = path.split(".")[0] ?? "";
5792
+ if (!declaration.params.includes(root)) {
5793
+ addsNothing = false;
5794
+ break;
5795
+ }
5796
+ const own = /* @__PURE__ */ new Set([...splitIdentifier(path.split(".").pop() ?? ""), ...nameTokens]);
5776
5797
  if (!covered(match[2] ?? "", own)) {
5777
5798
  addsNothing = false;
5778
5799
  break;
@@ -6597,10 +6618,10 @@ var no_sentinel_return_on_catch_default = createRule({
6597
6618
  return false;
6598
6619
  }
6599
6620
  if (matcher.isLoggingCall(current)) {
6600
- return true;
6621
+ return caughtName === null || argsIncludeBinding(current.arguments, caughtName);
6601
6622
  }
6602
6623
  const name = calleeName2(current.callee);
6603
- return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
6624
+ return name !== null && REPORT_NAME_RE.test(name) && (caughtName === null || argsIncludeBinding(current.arguments, caughtName));
6604
6625
  });
6605
6626
  }
6606
6627
  return {
@@ -8069,7 +8090,7 @@ var no_type_member_comment_wall_default = createRule({
8069
8090
  claimed.add(comment);
8070
8091
  commented += 1;
8071
8092
  const body2 = commentBody(comment);
8072
- if (body2.length === 0 || carriesValue(body2)) continue;
8093
+ if (body2.length === 0 || carriesValue(body2) || isTagsOnly(body2)) continue;
8073
8094
  if (novelWords(body2, knownTokens(sourceCode.getText(member))) <= options.maxNovelWords) {
8074
8095
  restated += 1;
8075
8096
  }
@@ -13987,7 +14008,7 @@ var rules = {
13987
14008
  };
13988
14009
  var meta = {
13989
14010
  name: "@sarj/eslint-plugin",
13990
- version: "13.0.0"
14011
+ version: "13.1.0"
13991
14012
  };
13992
14013
  var applicationOnlyRules = [
13993
14014
  "no-restricted-library-load",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "13.0.0",
3
+ "version": "13.1.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",