@sarj/eslint-plugin 9.17.0 → 10.0.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
@@ -26,7 +26,7 @@ import sarj from "@sarj/eslint-plugin";
26
26
  export default [sarj.configs.recommended];
27
27
  ```
28
28
 
29
- 67 rules. Each source under `src/rules/` states one concise claim and links to
29
+ 66 rules. Each source under `src/rules/` states one concise claim and links to
30
30
  its paired tests. The definition and named test cases are the complete rule
31
31
  specification, and `meta.docs.url` points directly to those executable examples.
32
32
 
package/dist/index.cjs CHANGED
@@ -928,6 +928,8 @@ var CODE_TAIL_RE = /[;{}()]\s*$|=>\s*$|,\s*$/;
928
928
  var ASSIGN_RE = /^[A-Za-z_$][\w.$[\]]*\s*(?:=(?![=>])|\+=|-=|\*=)\s*\S.*[;)}\]]\s*$/;
929
929
  var CALL_RE = /^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
930
930
  var ASSERTION_CODE_RE = /^(?:await\s+)?(?:expect(?:TypeOf)?|assert(?:\.\w+)?)\s*\(/;
931
+ var HTTP_CONTRACT_RE = /\b(?:GET|HEAD|OPTIONS|PATCH|POST|PUT|DELETE)\s+(?:https?:\/\/|\/|\{[A-Za-z_$])/;
932
+ var PROSE_ASSIGNMENT_RE = /^[A-Za-z_$][\w.$[\]]*\s*=\s*[A-Za-z][A-Za-z '-]{2,}\s+\((?:how|what|when|where|which|who|why)\b[^)]*\)[.!]?$/i;
931
933
  var PSEUDOCODE_RE = /%\w+%|\[opt\]|(?:^|\s)<[A-Za-z]\w*>|…|\.\.\./;
932
934
  function isBanner(text) {
933
935
  const t = text.trim();
@@ -946,6 +948,7 @@ function isRegionMarker(text) {
946
948
  function looksLikeCode(text, allowCall = true) {
947
949
  const t = text.trim();
948
950
  if (!t) return false;
951
+ if (PROSE_ASSIGNMENT_RE.test(t)) return false;
949
952
  if (CODE_KEYWORD_RE.test(t) && CODE_TAIL_RE.test(t)) return true;
950
953
  if (ASSIGN_RE.test(t)) return true;
951
954
  if (ASSERTION_CODE_RE.test(t)) return true;
@@ -1084,6 +1087,21 @@ function runCitesAReference(comments, index) {
1084
1087
  function areAdjacentLineComments(a, b) {
1085
1088
  return a !== void 0 && b !== void 0 && a.type === "Line" && b.type === "Line" && b.loc.start.line === a.loc.end.line + 1;
1086
1089
  }
1090
+ function lineRunStart(comments, index) {
1091
+ let start = index;
1092
+ while (start > 0 && areAdjacentLineComments(comments[start - 1], comments[start])) {
1093
+ start -= 1;
1094
+ }
1095
+ return start;
1096
+ }
1097
+ function runDocumentsHttpContract(comments, index) {
1098
+ const start = lineRunStart(comments, index);
1099
+ for (let i = start; i < comments.length; i++) {
1100
+ if (i > start && !areAdjacentLineComments(comments[i - 1], comments[i])) break;
1101
+ if (HTTP_CONTRACT_RE.test(stripCommentMarker(comments[i]?.value ?? ""))) return true;
1102
+ }
1103
+ return false;
1104
+ }
1087
1105
  function isInsideCommentRun(comments, index) {
1088
1106
  const comment = comments[index];
1089
1107
  return areAdjacentLineComments(comments[index - 1], comment) || areAdjacentLineComments(comment, comments[index + 1]);
@@ -1119,7 +1137,7 @@ var no_comment_cruft_default = createRule({
1119
1137
  schema: [],
1120
1138
  messages: {
1121
1139
  commentedOutCode: "Commented-out code \u2014 delete it; git history remembers.",
1122
- sectionBanner: "Section-banner / region comment \u2014 structure code with functions, not ASCII rules.",
1140
+ sectionBanner: "Section-banner / region comment \u2014 remove the decoration or replace it with a named code boundary.",
1123
1141
  fileHeaderPreamble: "File-header comment preamble \u2014 use a brief doc comment for the why, not a block of `//` lines.",
1124
1142
  commentWall: "Statement comment wall ({{count}} narrated steps) \u2014 delete the walkthrough and name the operations in code; keep only constraints or rationale.",
1125
1143
  redundantNarration: "Comment narrates the code \u2014 delete it or say *why*, not *what*. Code is self-documenting.",
@@ -1142,6 +1160,17 @@ var no_comment_cruft_default = createRule({
1142
1160
  function isJsDoc(comment) {
1143
1161
  return comment.type === "Block" && /^\*/.test(comment.value);
1144
1162
  }
1163
+ function isJsxOnlyComment(comment) {
1164
+ for (let node = sourceCode.getNodeByRangeIndex(
1165
+ comment.range[0]
1166
+ ); node != null; node = node.parent) {
1167
+ if (node.type === import_utils7.AST_NODE_TYPES.JSXExpressionContainer) {
1168
+ return node.expression.type === import_utils7.AST_NODE_TYPES.JSXEmptyExpression;
1169
+ }
1170
+ if (node.type === import_utils7.AST_NODE_TYPES.Program) return false;
1171
+ }
1172
+ return false;
1173
+ }
1145
1174
  function findCommentWalls(comments) {
1146
1175
  const attached = /* @__PURE__ */ new Map();
1147
1176
  for (const comment of comments) {
@@ -1223,6 +1252,8 @@ var no_comment_cruft_default = createRule({
1223
1252
  const enumerated = comments.filter(
1224
1253
  (c) => c.type === "Line" && ENUMERATION_RE.test(stripCommentMarker(c.value))
1225
1254
  );
1255
+ const reportedBannerRuns = /* @__PURE__ */ new Set();
1256
+ const reportedCodeRuns = /* @__PURE__ */ new Set();
1226
1257
  for (let i = 0; i < comments.length; i++) {
1227
1258
  const comment = comments[i];
1228
1259
  if (comment === void 0) continue;
@@ -1242,9 +1273,14 @@ var no_comment_cruft_default = createRule({
1242
1273
  }
1243
1274
  continue;
1244
1275
  }
1245
- if (!isStandalone(comment)) continue;
1246
1276
  if (LICENSE_RE.test(comment.value)) continue;
1247
1277
  const texts = comment.value.split("\n").map(stripCommentMarker).filter((l) => l.length > 0 && !isDirective(l));
1278
+ if (!isStandalone(comment)) {
1279
+ if (isJsxOnlyComment(comment) && texts.some(isBanner)) {
1280
+ context.report({ node: comment, messageId: "sectionBanner" });
1281
+ }
1282
+ continue;
1283
+ }
1248
1284
  const firstText = texts[0];
1249
1285
  if (firstText !== void 0 && /^(?:todo|fixme)\b/i.test(firstText)) {
1250
1286
  if (!runCitesAReference(comments, i)) {
@@ -1259,16 +1295,23 @@ var no_comment_cruft_default = createRule({
1259
1295
  context.report({ node: comment, messageId: "placeholderImplementation" });
1260
1296
  continue;
1261
1297
  }
1298
+ const runStart = lineRunStart(comments, i);
1262
1299
  if (texts.some(isBanner)) {
1263
- context.report({ node: comment, messageId: "sectionBanner" });
1300
+ if (!reportedBannerRuns.has(runStart)) {
1301
+ context.report({ node: comment, messageId: "sectionBanner" });
1302
+ reportedBannerRuns.add(runStart);
1303
+ }
1264
1304
  continue;
1265
1305
  }
1266
1306
  const prev = comments[i - 1];
1267
1307
  const precedingProse = prev !== void 0 && prev.type === "Line" && prev.loc.end.line === comment.loc.start.line - 1 && isProse(stripCommentMarker(prev.value));
1268
1308
  const container = sourceCode.getNodeByRangeIndex(comment.range[0]);
1269
1309
  const inTypeMembers = container !== null && TYPE_MEMBER_CONTAINERS.has(container.type);
1270
- if (hasCommentedOutCode(texts, precedingProse, !inTypeMembers) && !hasIllustrationLeadInAbove(comments, i)) {
1271
- context.report({ node: comment, messageId: "commentedOutCode" });
1310
+ if (hasCommentedOutCode(texts, precedingProse, !inTypeMembers) && !hasIllustrationLeadInAbove(comments, i) && !runDocumentsHttpContract(comments, i)) {
1311
+ if (!reportedCodeRuns.has(runStart)) {
1312
+ context.report({ node: comment, messageId: "commentedOutCode" });
1313
+ reportedCodeRuns.add(runStart);
1314
+ }
1272
1315
  continue;
1273
1316
  }
1274
1317
  if (comment.type === "Line" && texts.length === 1) {
@@ -3335,6 +3378,8 @@ var TYPED_TAG_RE = /@(arg|argument|param|return|returns|yield|yields)\b/i;
3335
3378
  var VALUE_TAG_RE = /@(example|deprecated|see|remarks|throws|internal|public|alpha|beta|since|template|fileoverview)\b/i;
3336
3379
  var BOUNDARY_RE = /(?<=[.!?])["'`)\]]*\s+(?=[A-Z0-9`])/;
3337
3380
  var BULLET_RE = /^\s*(?:[-*+] |\d+[.)] )/;
3381
+ var HEADING_RE = /^[A-Za-z][A-Za-z ]+:$/;
3382
+ var TECHNICAL_ANCHOR_RE = /https?:\/\/|`[^`\n]+`|:[a-z][a-z0-9_-]*:|(["'])[^"'\n]+\1|\d|\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b|\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b|(?:^|\s)(?:[\w.-]+\/)+[\w.-]+|\b[\w.-]+\.(?:py|pyi|js|jsx|ts|tsx|json|ya?ml|toml|csv|parquet|md)\b|->|=>|==|!=|<=|>=|\|/mu;
3338
3383
  function body(comment) {
3339
3384
  return comment.value.replace(/^\*/, "").split("\n").map((line) => line.replace(/^\s*\*?\s?/, "")).join("\n").trim();
3340
3385
  }
@@ -3354,6 +3399,17 @@ function sentenceUnits(text) {
3354
3399
  if (prose.length > 0) units += prose.join(" ").split(BOUNDARY_RE).length;
3355
3400
  return units;
3356
3401
  }
3402
+ function hasDocumentationStructure(text) {
3403
+ const paragraphs = text.split(/\n\s*\n/u).filter((part) => part.trim().length > 0);
3404
+ if (paragraphs.length >= 2) return true;
3405
+ return text.split("\n").some((raw) => {
3406
+ const line = raw.trim().replace(/^\*\s?/u, "").trim();
3407
+ return line.length > 0 && (BULLET_RE.test(line) || HEADING_RE.test(line) || /(?:->|=>|\|)/u.test(line));
3408
+ });
3409
+ }
3410
+ function hasTechnicalAnchor(text) {
3411
+ return TECHNICAL_ANCHOR_RE.test(text);
3412
+ }
3357
3413
  function proseGroups(filename, sourceCode, includeValueTags = false) {
3358
3414
  if (isGeneratedFile(filename, sourceCode.text) || isStoryFile(filename) || isScriptFile(filename) || isTestFile(filename)) return [];
3359
3415
  const groups = [];
@@ -3420,10 +3476,8 @@ function typedFunction(node) {
3420
3476
  }
3421
3477
 
3422
3478
  // src/rules/no-long-comment.ts
3423
- var LIST_ITEM_RE = /^\s*(?:[-*+] |\d+[.)] )/m;
3424
- var DURABLE_RATIONALE_RE = /\b(?:adapter|algorithm|atomic|auth(?:entication|orization)?|backpressure|billing|cache|callback|cluster|commit|compatib|concurr|contract|credential|cross-node|csrf|definite-assignment|error|exception|external|failure|fallback|handshake|interop|invalid|invariant|legacy|migration|navigation|parse[rsd]?|payload|privacy|protocol|race|reject|reload|renderer|resume|retry|runtime|schema|security|selector|sentinel|serializ|socket|stage|third-party|token|transaction|upstream|wire|workaround)\b|\b(?:must|required|prevent|avoid)\b/iu;
3425
- var VENDORED_BASENAME_RE = /(?:^|\/)(?:jquery(?:-ui)?(?:[.-]\d[^/]*)?|lodash(?:\.min)?|bootstrap(?:\.min)?|react(?:\.min)?)\.js$/iu;
3426
3479
  var EXCESSIVE_SENTENCE_COUNT = 8;
3480
+ var VERSIONED_DEPENDENCY_TREE_RE = /(?:^|\/)lib\/[^/]*-?v?\d+\.\d+(?:\.\d+)?[^/]*\//iu;
3427
3481
  function documentsTypeOrMember(sourceCode, comment) {
3428
3482
  const token = sourceCode.getTokenAfter(comment, { includeComments: false });
3429
3483
  if (token === null || token.loc.start.line !== comment.loc.end.line + 1) return false;
@@ -3447,11 +3501,11 @@ var no_long_comment_default = createRule({
3447
3501
  defaultOptions: [],
3448
3502
  create(context) {
3449
3503
  const normalizedFilename = context.filename.replaceAll("\\", "/");
3450
- if (VENDORED_BASENAME_RE.test(normalizedFilename)) return {};
3504
+ if (VERSIONED_DEPENDENCY_TREE_RE.test(normalizedFilename)) return {};
3451
3505
  return {
3452
3506
  Program() {
3453
3507
  for (const group of proseGroups(context.filename, context.sourceCode)) {
3454
- if (group.comment.type !== "Block" || group.hasTypedTags || LIST_ITEM_RE.test(group.text) || DURABLE_RATIONALE_RE.test(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT) continue;
3508
+ if (group.comment.type !== "Block" || !group.comment.value.startsWith("*") || group.hasTypedTags || hasDocumentationStructure(group.text) || hasTechnicalAnchor(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT) continue;
3455
3509
  context.report({ node: group.comment, messageId: "tooLong" });
3456
3510
  }
3457
3511
  }
@@ -9748,31 +9802,6 @@ var prefer_server_actions_default = createRule({
9748
9802
  }
9749
9803
  });
9750
9804
 
9751
- // src/rules/prefer-single-sentence-comment.ts
9752
- var prefer_single_sentence_comment_default = createRule({
9753
- name: "prefer-single-sentence-comment",
9754
- meta: {
9755
- type: "suggestion",
9756
- docs: { description: "Prefer one sentence and self-documenting code over a two-sentence comment." },
9757
- schema: [],
9758
- messages: {
9759
- preferOneSentence: "Two-sentence comment \u2014 prefer one sentence and self-documenting code."
9760
- }
9761
- },
9762
- defaultOptions: [],
9763
- create(context) {
9764
- return {
9765
- Program() {
9766
- for (const group of proseGroups(context.filename, context.sourceCode)) {
9767
- if (!group.hasTypedTags && sentenceUnits(group.text) === 2) {
9768
- context.report({ node: group.comment, messageId: "preferOneSentence" });
9769
- }
9770
- }
9771
- }
9772
- };
9773
- }
9774
- });
9775
-
9776
9805
  // src/rules/prefer-string-literal-union.ts
9777
9806
  var import_utils62 = require("@typescript-eslint/utils");
9778
9807
  var ts2 = __toESM(require("typescript"), 1);
@@ -12260,6 +12289,10 @@ var retiredRules = {
12260
12289
  removedIn: "3.0.0",
12261
12290
  reason: "Delete the retired entry; application-profile consumers can separately adopt `@sarj/prefer-shadcn-primitives`."
12262
12291
  },
12292
+ "prefer-single-sentence-comment": {
12293
+ removedIn: "10.0.0",
12294
+ reason: "Delete the config entry and suppressions; use the narrower no-comment-cruft, no-restated-comment, and no-long-comment rules."
12295
+ },
12263
12296
  "primary-export-file-name": {
12264
12297
  removedIn: "4.0.0",
12265
12298
  reason: "Delete the config entry and suppressions; there is no replacement."
@@ -12334,7 +12367,6 @@ var rules = {
12334
12367
  "prefer-schema-for-api-payload": prefer_schema_for_api_payload_default,
12335
12368
  "prefer-semantic-colors": prefer_semantic_colors_default,
12336
12369
  "prefer-server-actions": prefer_server_actions_default,
12337
- "prefer-single-sentence-comment": prefer_single_sentence_comment_default,
12338
12370
  "prefer-string-literal-union": prefer_string_literal_union_default,
12339
12371
  "prefer-whole-object-assertion": prefer_whole_object_assertion_default,
12340
12372
  "prefer-zod-enum": prefer_zod_enum_default,
@@ -12350,7 +12382,7 @@ var rules = {
12350
12382
  };
12351
12383
  var meta = {
12352
12384
  name: "@sarj/eslint-plugin",
12353
- version: "9.17.0"
12385
+ version: "10.0.0"
12354
12386
  };
12355
12387
  var applicationOnlyRules = [
12356
12388
  "no-restricted-library-load",
@@ -12405,7 +12437,6 @@ var recommendedRules = {
12405
12437
  "@sarj/prefer-schema-for-api-payload": "warn",
12406
12438
  "@sarj/prefer-semantic-colors": ["warn", { requireSemanticTokens: true }],
12407
12439
  "@sarj/prefer-server-actions": "warn",
12408
- "@sarj/prefer-single-sentence-comment": "warn",
12409
12440
  "@sarj/prefer-string-literal-union": "warn",
12410
12441
  "@sarj/prefer-whole-object-assertion": "warn",
12411
12442
  "@sarj/prefer-zod-enum": "warn",
@@ -12471,7 +12502,6 @@ var strictRules = {
12471
12502
  "@sarj/prefer-schema-for-api-payload": "error",
12472
12503
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
12473
12504
  "@sarj/prefer-server-actions": "error",
12474
- "@sarj/prefer-single-sentence-comment": "warn",
12475
12505
  "@sarj/prefer-string-literal-union": "error",
12476
12506
  "@sarj/prefer-whole-object-assertion": "error",
12477
12507
  "@sarj/prefer-zod-enum": "error",
package/dist/index.d.cts CHANGED
@@ -267,9 +267,6 @@ declare const rules: {
267
267
  readonly "prefer-server-actions": _typescript_eslint_utils_ts_eslint.RuleModule<"preferServerAction", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
268
268
  name: string;
269
269
  };
270
- readonly "prefer-single-sentence-comment": _typescript_eslint_utils_ts_eslint.RuleModule<"preferOneSentence", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
271
- name: string;
272
- };
273
270
  readonly "prefer-string-literal-union": _typescript_eslint_utils_ts_eslint.RuleModule<"bareChoiceField" | "comparisonCluster", readonly [{
274
271
  ignoreFields?: readonly string[];
275
272
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -368,7 +365,6 @@ declare const recommendedRules: {
368
365
  readonly requireSemanticTokens: true;
369
366
  }];
370
367
  readonly "@sarj/prefer-server-actions": "warn";
371
- readonly "@sarj/prefer-single-sentence-comment": "warn";
372
368
  readonly "@sarj/prefer-string-literal-union": "warn";
373
369
  readonly "@sarj/prefer-whole-object-assertion": "warn";
374
370
  readonly "@sarj/prefer-zod-enum": "warn";
@@ -436,7 +432,6 @@ declare const strictRules: {
436
432
  readonly requireSemanticTokens: true;
437
433
  }];
438
434
  readonly "@sarj/prefer-server-actions": "error";
439
- readonly "@sarj/prefer-single-sentence-comment": "warn";
440
435
  readonly "@sarj/prefer-string-literal-union": "error";
441
436
  readonly "@sarj/prefer-whole-object-assertion": "error";
442
437
  readonly "@sarj/prefer-zod-enum": "error";
@@ -458,7 +453,7 @@ type FlatPreset = {
458
453
  declare const plugin: {
459
454
  readonly meta: {
460
455
  readonly name: "@sarj/eslint-plugin";
461
- readonly version: "9.17.0";
456
+ readonly version: "10.0.0";
462
457
  };
463
458
  readonly rules: {
464
459
  readonly "duplicate-test-body": _typescript_eslint_utils_ts_eslint.RuleModule<"duplicateTestBody", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -640,9 +635,6 @@ declare const plugin: {
640
635
  readonly "prefer-server-actions": _typescript_eslint_utils_ts_eslint.RuleModule<"preferServerAction", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
641
636
  name: string;
642
637
  };
643
- readonly "prefer-single-sentence-comment": _typescript_eslint_utils_ts_eslint.RuleModule<"preferOneSentence", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
644
- name: string;
645
- };
646
638
  readonly "prefer-string-literal-union": _typescript_eslint_utils_ts_eslint.RuleModule<"bareChoiceField" | "comparisonCluster", readonly [{
647
639
  ignoreFields?: readonly string[];
648
640
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
package/dist/index.d.ts CHANGED
@@ -267,9 +267,6 @@ declare const rules: {
267
267
  readonly "prefer-server-actions": _typescript_eslint_utils_ts_eslint.RuleModule<"preferServerAction", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
268
268
  name: string;
269
269
  };
270
- readonly "prefer-single-sentence-comment": _typescript_eslint_utils_ts_eslint.RuleModule<"preferOneSentence", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
271
- name: string;
272
- };
273
270
  readonly "prefer-string-literal-union": _typescript_eslint_utils_ts_eslint.RuleModule<"bareChoiceField" | "comparisonCluster", readonly [{
274
271
  ignoreFields?: readonly string[];
275
272
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -368,7 +365,6 @@ declare const recommendedRules: {
368
365
  readonly requireSemanticTokens: true;
369
366
  }];
370
367
  readonly "@sarj/prefer-server-actions": "warn";
371
- readonly "@sarj/prefer-single-sentence-comment": "warn";
372
368
  readonly "@sarj/prefer-string-literal-union": "warn";
373
369
  readonly "@sarj/prefer-whole-object-assertion": "warn";
374
370
  readonly "@sarj/prefer-zod-enum": "warn";
@@ -436,7 +432,6 @@ declare const strictRules: {
436
432
  readonly requireSemanticTokens: true;
437
433
  }];
438
434
  readonly "@sarj/prefer-server-actions": "error";
439
- readonly "@sarj/prefer-single-sentence-comment": "warn";
440
435
  readonly "@sarj/prefer-string-literal-union": "error";
441
436
  readonly "@sarj/prefer-whole-object-assertion": "error";
442
437
  readonly "@sarj/prefer-zod-enum": "error";
@@ -458,7 +453,7 @@ type FlatPreset = {
458
453
  declare const plugin: {
459
454
  readonly meta: {
460
455
  readonly name: "@sarj/eslint-plugin";
461
- readonly version: "9.17.0";
456
+ readonly version: "10.0.0";
462
457
  };
463
458
  readonly rules: {
464
459
  readonly "duplicate-test-body": _typescript_eslint_utils_ts_eslint.RuleModule<"duplicateTestBody", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -640,9 +635,6 @@ declare const plugin: {
640
635
  readonly "prefer-server-actions": _typescript_eslint_utils_ts_eslint.RuleModule<"preferServerAction", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
641
636
  name: string;
642
637
  };
643
- readonly "prefer-single-sentence-comment": _typescript_eslint_utils_ts_eslint.RuleModule<"preferOneSentence", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
644
- name: string;
645
- };
646
638
  readonly "prefer-string-literal-union": _typescript_eslint_utils_ts_eslint.RuleModule<"bareChoiceField" | "comparisonCluster", readonly [{
647
639
  ignoreFields?: readonly string[];
648
640
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
package/dist/index.js CHANGED
@@ -886,6 +886,8 @@ var CODE_TAIL_RE = /[;{}()]\s*$|=>\s*$|,\s*$/;
886
886
  var ASSIGN_RE = /^[A-Za-z_$][\w.$[\]]*\s*(?:=(?![=>])|\+=|-=|\*=)\s*\S.*[;)}\]]\s*$/;
887
887
  var CALL_RE = /^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
888
888
  var ASSERTION_CODE_RE = /^(?:await\s+)?(?:expect(?:TypeOf)?|assert(?:\.\w+)?)\s*\(/;
889
+ var HTTP_CONTRACT_RE = /\b(?:GET|HEAD|OPTIONS|PATCH|POST|PUT|DELETE)\s+(?:https?:\/\/|\/|\{[A-Za-z_$])/;
890
+ var PROSE_ASSIGNMENT_RE = /^[A-Za-z_$][\w.$[\]]*\s*=\s*[A-Za-z][A-Za-z '-]{2,}\s+\((?:how|what|when|where|which|who|why)\b[^)]*\)[.!]?$/i;
889
891
  var PSEUDOCODE_RE = /%\w+%|\[opt\]|(?:^|\s)<[A-Za-z]\w*>|…|\.\.\./;
890
892
  function isBanner(text) {
891
893
  const t = text.trim();
@@ -904,6 +906,7 @@ function isRegionMarker(text) {
904
906
  function looksLikeCode(text, allowCall = true) {
905
907
  const t = text.trim();
906
908
  if (!t) return false;
909
+ if (PROSE_ASSIGNMENT_RE.test(t)) return false;
907
910
  if (CODE_KEYWORD_RE.test(t) && CODE_TAIL_RE.test(t)) return true;
908
911
  if (ASSIGN_RE.test(t)) return true;
909
912
  if (ASSERTION_CODE_RE.test(t)) return true;
@@ -1042,6 +1045,21 @@ function runCitesAReference(comments, index) {
1042
1045
  function areAdjacentLineComments(a, b) {
1043
1046
  return a !== void 0 && b !== void 0 && a.type === "Line" && b.type === "Line" && b.loc.start.line === a.loc.end.line + 1;
1044
1047
  }
1048
+ function lineRunStart(comments, index) {
1049
+ let start = index;
1050
+ while (start > 0 && areAdjacentLineComments(comments[start - 1], comments[start])) {
1051
+ start -= 1;
1052
+ }
1053
+ return start;
1054
+ }
1055
+ function runDocumentsHttpContract(comments, index) {
1056
+ const start = lineRunStart(comments, index);
1057
+ for (let i = start; i < comments.length; i++) {
1058
+ if (i > start && !areAdjacentLineComments(comments[i - 1], comments[i])) break;
1059
+ if (HTTP_CONTRACT_RE.test(stripCommentMarker(comments[i]?.value ?? ""))) return true;
1060
+ }
1061
+ return false;
1062
+ }
1045
1063
  function isInsideCommentRun(comments, index) {
1046
1064
  const comment = comments[index];
1047
1065
  return areAdjacentLineComments(comments[index - 1], comment) || areAdjacentLineComments(comment, comments[index + 1]);
@@ -1077,7 +1095,7 @@ var no_comment_cruft_default = createRule({
1077
1095
  schema: [],
1078
1096
  messages: {
1079
1097
  commentedOutCode: "Commented-out code \u2014 delete it; git history remembers.",
1080
- sectionBanner: "Section-banner / region comment \u2014 structure code with functions, not ASCII rules.",
1098
+ sectionBanner: "Section-banner / region comment \u2014 remove the decoration or replace it with a named code boundary.",
1081
1099
  fileHeaderPreamble: "File-header comment preamble \u2014 use a brief doc comment for the why, not a block of `//` lines.",
1082
1100
  commentWall: "Statement comment wall ({{count}} narrated steps) \u2014 delete the walkthrough and name the operations in code; keep only constraints or rationale.",
1083
1101
  redundantNarration: "Comment narrates the code \u2014 delete it or say *why*, not *what*. Code is self-documenting.",
@@ -1100,6 +1118,17 @@ var no_comment_cruft_default = createRule({
1100
1118
  function isJsDoc(comment) {
1101
1119
  return comment.type === "Block" && /^\*/.test(comment.value);
1102
1120
  }
1121
+ function isJsxOnlyComment(comment) {
1122
+ for (let node = sourceCode.getNodeByRangeIndex(
1123
+ comment.range[0]
1124
+ ); node != null; node = node.parent) {
1125
+ if (node.type === AST_NODE_TYPES6.JSXExpressionContainer) {
1126
+ return node.expression.type === AST_NODE_TYPES6.JSXEmptyExpression;
1127
+ }
1128
+ if (node.type === AST_NODE_TYPES6.Program) return false;
1129
+ }
1130
+ return false;
1131
+ }
1103
1132
  function findCommentWalls(comments) {
1104
1133
  const attached = /* @__PURE__ */ new Map();
1105
1134
  for (const comment of comments) {
@@ -1181,6 +1210,8 @@ var no_comment_cruft_default = createRule({
1181
1210
  const enumerated = comments.filter(
1182
1211
  (c) => c.type === "Line" && ENUMERATION_RE.test(stripCommentMarker(c.value))
1183
1212
  );
1213
+ const reportedBannerRuns = /* @__PURE__ */ new Set();
1214
+ const reportedCodeRuns = /* @__PURE__ */ new Set();
1184
1215
  for (let i = 0; i < comments.length; i++) {
1185
1216
  const comment = comments[i];
1186
1217
  if (comment === void 0) continue;
@@ -1200,9 +1231,14 @@ var no_comment_cruft_default = createRule({
1200
1231
  }
1201
1232
  continue;
1202
1233
  }
1203
- if (!isStandalone(comment)) continue;
1204
1234
  if (LICENSE_RE.test(comment.value)) continue;
1205
1235
  const texts = comment.value.split("\n").map(stripCommentMarker).filter((l) => l.length > 0 && !isDirective(l));
1236
+ if (!isStandalone(comment)) {
1237
+ if (isJsxOnlyComment(comment) && texts.some(isBanner)) {
1238
+ context.report({ node: comment, messageId: "sectionBanner" });
1239
+ }
1240
+ continue;
1241
+ }
1206
1242
  const firstText = texts[0];
1207
1243
  if (firstText !== void 0 && /^(?:todo|fixme)\b/i.test(firstText)) {
1208
1244
  if (!runCitesAReference(comments, i)) {
@@ -1217,16 +1253,23 @@ var no_comment_cruft_default = createRule({
1217
1253
  context.report({ node: comment, messageId: "placeholderImplementation" });
1218
1254
  continue;
1219
1255
  }
1256
+ const runStart = lineRunStart(comments, i);
1220
1257
  if (texts.some(isBanner)) {
1221
- context.report({ node: comment, messageId: "sectionBanner" });
1258
+ if (!reportedBannerRuns.has(runStart)) {
1259
+ context.report({ node: comment, messageId: "sectionBanner" });
1260
+ reportedBannerRuns.add(runStart);
1261
+ }
1222
1262
  continue;
1223
1263
  }
1224
1264
  const prev = comments[i - 1];
1225
1265
  const precedingProse = prev !== void 0 && prev.type === "Line" && prev.loc.end.line === comment.loc.start.line - 1 && isProse(stripCommentMarker(prev.value));
1226
1266
  const container = sourceCode.getNodeByRangeIndex(comment.range[0]);
1227
1267
  const inTypeMembers = container !== null && TYPE_MEMBER_CONTAINERS.has(container.type);
1228
- if (hasCommentedOutCode(texts, precedingProse, !inTypeMembers) && !hasIllustrationLeadInAbove(comments, i)) {
1229
- context.report({ node: comment, messageId: "commentedOutCode" });
1268
+ if (hasCommentedOutCode(texts, precedingProse, !inTypeMembers) && !hasIllustrationLeadInAbove(comments, i) && !runDocumentsHttpContract(comments, i)) {
1269
+ if (!reportedCodeRuns.has(runStart)) {
1270
+ context.report({ node: comment, messageId: "commentedOutCode" });
1271
+ reportedCodeRuns.add(runStart);
1272
+ }
1230
1273
  continue;
1231
1274
  }
1232
1275
  if (comment.type === "Line" && texts.length === 1) {
@@ -3293,6 +3336,8 @@ var TYPED_TAG_RE = /@(arg|argument|param|return|returns|yield|yields)\b/i;
3293
3336
  var VALUE_TAG_RE = /@(example|deprecated|see|remarks|throws|internal|public|alpha|beta|since|template|fileoverview)\b/i;
3294
3337
  var BOUNDARY_RE = /(?<=[.!?])["'`)\]]*\s+(?=[A-Z0-9`])/;
3295
3338
  var BULLET_RE = /^\s*(?:[-*+] |\d+[.)] )/;
3339
+ var HEADING_RE = /^[A-Za-z][A-Za-z ]+:$/;
3340
+ var TECHNICAL_ANCHOR_RE = /https?:\/\/|`[^`\n]+`|:[a-z][a-z0-9_-]*:|(["'])[^"'\n]+\1|\d|\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b|\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b|(?:^|\s)(?:[\w.-]+\/)+[\w.-]+|\b[\w.-]+\.(?:py|pyi|js|jsx|ts|tsx|json|ya?ml|toml|csv|parquet|md)\b|->|=>|==|!=|<=|>=|\|/mu;
3296
3341
  function body(comment) {
3297
3342
  return comment.value.replace(/^\*/, "").split("\n").map((line) => line.replace(/^\s*\*?\s?/, "")).join("\n").trim();
3298
3343
  }
@@ -3312,6 +3357,17 @@ function sentenceUnits(text) {
3312
3357
  if (prose.length > 0) units += prose.join(" ").split(BOUNDARY_RE).length;
3313
3358
  return units;
3314
3359
  }
3360
+ function hasDocumentationStructure(text) {
3361
+ const paragraphs = text.split(/\n\s*\n/u).filter((part) => part.trim().length > 0);
3362
+ if (paragraphs.length >= 2) return true;
3363
+ return text.split("\n").some((raw) => {
3364
+ const line = raw.trim().replace(/^\*\s?/u, "").trim();
3365
+ return line.length > 0 && (BULLET_RE.test(line) || HEADING_RE.test(line) || /(?:->|=>|\|)/u.test(line));
3366
+ });
3367
+ }
3368
+ function hasTechnicalAnchor(text) {
3369
+ return TECHNICAL_ANCHOR_RE.test(text);
3370
+ }
3315
3371
  function proseGroups(filename, sourceCode, includeValueTags = false) {
3316
3372
  if (isGeneratedFile(filename, sourceCode.text) || isStoryFile(filename) || isScriptFile(filename) || isTestFile(filename)) return [];
3317
3373
  const groups = [];
@@ -3378,10 +3434,8 @@ function typedFunction(node) {
3378
3434
  }
3379
3435
 
3380
3436
  // src/rules/no-long-comment.ts
3381
- var LIST_ITEM_RE = /^\s*(?:[-*+] |\d+[.)] )/m;
3382
- var DURABLE_RATIONALE_RE = /\b(?:adapter|algorithm|atomic|auth(?:entication|orization)?|backpressure|billing|cache|callback|cluster|commit|compatib|concurr|contract|credential|cross-node|csrf|definite-assignment|error|exception|external|failure|fallback|handshake|interop|invalid|invariant|legacy|migration|navigation|parse[rsd]?|payload|privacy|protocol|race|reject|reload|renderer|resume|retry|runtime|schema|security|selector|sentinel|serializ|socket|stage|third-party|token|transaction|upstream|wire|workaround)\b|\b(?:must|required|prevent|avoid)\b/iu;
3383
- var VENDORED_BASENAME_RE = /(?:^|\/)(?:jquery(?:-ui)?(?:[.-]\d[^/]*)?|lodash(?:\.min)?|bootstrap(?:\.min)?|react(?:\.min)?)\.js$/iu;
3384
3437
  var EXCESSIVE_SENTENCE_COUNT = 8;
3438
+ var VERSIONED_DEPENDENCY_TREE_RE = /(?:^|\/)lib\/[^/]*-?v?\d+\.\d+(?:\.\d+)?[^/]*\//iu;
3385
3439
  function documentsTypeOrMember(sourceCode, comment) {
3386
3440
  const token = sourceCode.getTokenAfter(comment, { includeComments: false });
3387
3441
  if (token === null || token.loc.start.line !== comment.loc.end.line + 1) return false;
@@ -3405,11 +3459,11 @@ var no_long_comment_default = createRule({
3405
3459
  defaultOptions: [],
3406
3460
  create(context) {
3407
3461
  const normalizedFilename = context.filename.replaceAll("\\", "/");
3408
- if (VENDORED_BASENAME_RE.test(normalizedFilename)) return {};
3462
+ if (VERSIONED_DEPENDENCY_TREE_RE.test(normalizedFilename)) return {};
3409
3463
  return {
3410
3464
  Program() {
3411
3465
  for (const group of proseGroups(context.filename, context.sourceCode)) {
3412
- if (group.comment.type !== "Block" || group.hasTypedTags || LIST_ITEM_RE.test(group.text) || DURABLE_RATIONALE_RE.test(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT) continue;
3466
+ if (group.comment.type !== "Block" || !group.comment.value.startsWith("*") || group.hasTypedTags || hasDocumentationStructure(group.text) || hasTechnicalAnchor(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT) continue;
3413
3467
  context.report({ node: group.comment, messageId: "tooLong" });
3414
3468
  }
3415
3469
  }
@@ -9709,31 +9763,6 @@ var prefer_server_actions_default = createRule({
9709
9763
  }
9710
9764
  });
9711
9765
 
9712
- // src/rules/prefer-single-sentence-comment.ts
9713
- var prefer_single_sentence_comment_default = createRule({
9714
- name: "prefer-single-sentence-comment",
9715
- meta: {
9716
- type: "suggestion",
9717
- docs: { description: "Prefer one sentence and self-documenting code over a two-sentence comment." },
9718
- schema: [],
9719
- messages: {
9720
- preferOneSentence: "Two-sentence comment \u2014 prefer one sentence and self-documenting code."
9721
- }
9722
- },
9723
- defaultOptions: [],
9724
- create(context) {
9725
- return {
9726
- Program() {
9727
- for (const group of proseGroups(context.filename, context.sourceCode)) {
9728
- if (!group.hasTypedTags && sentenceUnits(group.text) === 2) {
9729
- context.report({ node: group.comment, messageId: "preferOneSentence" });
9730
- }
9731
- }
9732
- }
9733
- };
9734
- }
9735
- });
9736
-
9737
9766
  // src/rules/prefer-string-literal-union.ts
9738
9767
  import {
9739
9768
  ESLintUtils as ESLintUtils3,
@@ -12224,6 +12253,10 @@ var retiredRules = {
12224
12253
  removedIn: "3.0.0",
12225
12254
  reason: "Delete the retired entry; application-profile consumers can separately adopt `@sarj/prefer-shadcn-primitives`."
12226
12255
  },
12256
+ "prefer-single-sentence-comment": {
12257
+ removedIn: "10.0.0",
12258
+ reason: "Delete the config entry and suppressions; use the narrower no-comment-cruft, no-restated-comment, and no-long-comment rules."
12259
+ },
12227
12260
  "primary-export-file-name": {
12228
12261
  removedIn: "4.0.0",
12229
12262
  reason: "Delete the config entry and suppressions; there is no replacement."
@@ -12298,7 +12331,6 @@ var rules = {
12298
12331
  "prefer-schema-for-api-payload": prefer_schema_for_api_payload_default,
12299
12332
  "prefer-semantic-colors": prefer_semantic_colors_default,
12300
12333
  "prefer-server-actions": prefer_server_actions_default,
12301
- "prefer-single-sentence-comment": prefer_single_sentence_comment_default,
12302
12334
  "prefer-string-literal-union": prefer_string_literal_union_default,
12303
12335
  "prefer-whole-object-assertion": prefer_whole_object_assertion_default,
12304
12336
  "prefer-zod-enum": prefer_zod_enum_default,
@@ -12314,7 +12346,7 @@ var rules = {
12314
12346
  };
12315
12347
  var meta = {
12316
12348
  name: "@sarj/eslint-plugin",
12317
- version: "9.17.0"
12349
+ version: "10.0.0"
12318
12350
  };
12319
12351
  var applicationOnlyRules = [
12320
12352
  "no-restricted-library-load",
@@ -12369,7 +12401,6 @@ var recommendedRules = {
12369
12401
  "@sarj/prefer-schema-for-api-payload": "warn",
12370
12402
  "@sarj/prefer-semantic-colors": ["warn", { requireSemanticTokens: true }],
12371
12403
  "@sarj/prefer-server-actions": "warn",
12372
- "@sarj/prefer-single-sentence-comment": "warn",
12373
12404
  "@sarj/prefer-string-literal-union": "warn",
12374
12405
  "@sarj/prefer-whole-object-assertion": "warn",
12375
12406
  "@sarj/prefer-zod-enum": "warn",
@@ -12435,7 +12466,6 @@ var strictRules = {
12435
12466
  "@sarj/prefer-schema-for-api-payload": "error",
12436
12467
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
12437
12468
  "@sarj/prefer-server-actions": "error",
12438
- "@sarj/prefer-single-sentence-comment": "warn",
12439
12469
  "@sarj/prefer-string-literal-union": "error",
12440
12470
  "@sarj/prefer-whole-object-assertion": "error",
12441
12471
  "@sarj/prefer-zod-enum": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "9.17.0",
3
+ "version": "10.0.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",