@sarj/eslint-plugin 9.4.0 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -99,7 +99,7 @@ declare const rules: {
99
99
  "no-client-side-data-fetching": _typescript_eslint_utils_ts_eslint.RuleModule<"noClientFetch", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
100
100
  name: string;
101
101
  };
102
- "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
102
+ "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "commentWall" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
103
103
  name: string;
104
104
  };
105
105
  "no-conditional-in-test": _typescript_eslint_utils_ts_eslint.RuleModule<"noConditionalInTest", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -397,7 +397,7 @@ type FlatPreset = {
397
397
  declare const plugin: {
398
398
  meta: {
399
399
  readonly name: "@sarj/eslint-plugin";
400
- readonly version: "9.4.0";
400
+ readonly version: "9.6.0";
401
401
  };
402
402
  rules: {
403
403
  "enforce-file-structure": _typescript_eslint_utils_ts_eslint.RuleModule<"importsFirst" | "useServerDirective", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -409,7 +409,7 @@ declare const plugin: {
409
409
  "no-client-side-data-fetching": _typescript_eslint_utils_ts_eslint.RuleModule<"noClientFetch", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
410
410
  name: string;
411
411
  };
412
- "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
412
+ "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "commentWall" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
413
413
  name: string;
414
414
  };
415
415
  "no-conditional-in-test": _typescript_eslint_utils_ts_eslint.RuleModule<"noConditionalInTest", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
package/dist/index.d.ts CHANGED
@@ -99,7 +99,7 @@ declare const rules: {
99
99
  "no-client-side-data-fetching": _typescript_eslint_utils_ts_eslint.RuleModule<"noClientFetch", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
100
100
  name: string;
101
101
  };
102
- "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
102
+ "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "commentWall" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
103
103
  name: string;
104
104
  };
105
105
  "no-conditional-in-test": _typescript_eslint_utils_ts_eslint.RuleModule<"noConditionalInTest", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -397,7 +397,7 @@ type FlatPreset = {
397
397
  declare const plugin: {
398
398
  meta: {
399
399
  readonly name: "@sarj/eslint-plugin";
400
- readonly version: "9.4.0";
400
+ readonly version: "9.6.0";
401
401
  };
402
402
  rules: {
403
403
  "enforce-file-structure": _typescript_eslint_utils_ts_eslint.RuleModule<"importsFirst" | "useServerDirective", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -409,7 +409,7 @@ declare const plugin: {
409
409
  "no-client-side-data-fetching": _typescript_eslint_utils_ts_eslint.RuleModule<"noClientFetch", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
410
410
  name: string;
411
411
  };
412
- "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
412
+ "no-comment-cruft": _typescript_eslint_utils_ts_eslint.RuleModule<"commentedOutCode" | "sectionBanner" | "fileHeaderPreamble" | "commentWall" | "redundantNarration" | "untrackedTodo", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
413
413
  name: string;
414
414
  };
415
415
  "no-conditional-in-test": _typescript_eslint_utils_ts_eslint.RuleModule<"noConditionalInTest", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
package/dist/index.js CHANGED
@@ -492,6 +492,14 @@ function restatesStatementHead(body, statement) {
492
492
 
493
493
  // src/rules/no-comment-cruft.ts
494
494
  var LEADING_PREAMBLE_MIN = 4;
495
+ var WALL_MIN_STATEMENTS = 4;
496
+ var WALL_MIN_COMMENTS = 3;
497
+ var WALL_MIN_COMMENTED_RATIO = 0.6;
498
+ var WALL_MIN_WEAK_RATIO = 0.75;
499
+ var WALL_MAX_WORDS = 18;
500
+ var WALL_MAX_NOVEL_WORDS = 2;
501
+ var WALL_NARRATION_RE = /^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|now|finally|lastly|add|append|assign|await|build|calculate|call|check|clear|close|compute|convert|copy|count|create|declare|define|delete|extract|fetch|filter|find|format|generate|get|handle|initialize|insert|iterate|join|load|log|loop|map|merge|open|parse|print|process|push|read|remove|render|reset|return|save|send|set|setup|sort|split|start|stop|store|update|validate|wrap|write)(?:s|es|d|ed|ing)?\b/i;
502
+ var WALL_STEP_PREFIX_RE = /^(?:\d+[.)]|(?:phase|step)\s+\d+\s*:)\s*/i;
495
503
  var STEP_NARRATION_RE = /^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|after(?:wards| that)?|finally|lastly|now)\s*[,:]\s*\S/i;
496
504
  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;
497
505
  var FOR_NOW_RE = /\bfor now\b/i;
@@ -704,6 +712,63 @@ var STATEMENT_CONTAINERS = /* @__PURE__ */ new Set([
704
712
  AST_NODE_TYPES5.TSModuleBlock,
705
713
  AST_NODE_TYPES5.TSInterfaceBody
706
714
  ]);
715
+ function directStatements(container) {
716
+ switch (container.type) {
717
+ case AST_NODE_TYPES5.Program:
718
+ case AST_NODE_TYPES5.BlockStatement:
719
+ case AST_NODE_TYPES5.ClassBody:
720
+ case AST_NODE_TYPES5.StaticBlock:
721
+ case AST_NODE_TYPES5.TSModuleBlock:
722
+ return container.body;
723
+ case AST_NODE_TYPES5.SwitchCase:
724
+ return container.consequent;
725
+ case AST_NODE_TYPES5.TSInterfaceBody:
726
+ return container.body;
727
+ default:
728
+ return [];
729
+ }
730
+ }
731
+ var WALL_STATEMENTS = /* @__PURE__ */ new Set([
732
+ AST_NODE_TYPES5.ExpressionStatement,
733
+ AST_NODE_TYPES5.ReturnStatement,
734
+ AST_NODE_TYPES5.ThrowStatement,
735
+ AST_NODE_TYPES5.VariableDeclaration,
736
+ AST_NODE_TYPES5.IfStatement,
737
+ AST_NODE_TYPES5.ForStatement,
738
+ AST_NODE_TYPES5.ForOfStatement,
739
+ AST_NODE_TYPES5.ForInStatement,
740
+ AST_NODE_TYPES5.WhileStatement,
741
+ AST_NODE_TYPES5.DoWhileStatement,
742
+ AST_NODE_TYPES5.SwitchStatement,
743
+ AST_NODE_TYPES5.TryStatement
744
+ ]);
745
+ function statementAttachmentBelow(comment, sourceCode) {
746
+ const token = sourceCode.getTokenAfter(comment, { includeComments: false });
747
+ if (token === null || token.loc.start.line !== comment.loc.end.line + 1 || token.loc.start.column !== comment.loc.start.column) {
748
+ return null;
749
+ }
750
+ for (let node = sourceCode.getNodeByRangeIndex(token.range[0]); node?.parent != null; node = node.parent) {
751
+ if (!STATEMENT_CONTAINERS.has(node.parent.type) || !WALL_STATEMENTS.has(node.type)) {
752
+ continue;
753
+ }
754
+ const siblings = directStatements(node.parent);
755
+ const index = siblings.indexOf(node);
756
+ return index < 0 ? null : { container: node.parent, index, statement: sourceCode.getText(node) };
757
+ }
758
+ return null;
759
+ }
760
+ function isWeakWalkthroughComment(body, statement) {
761
+ const normalized = body.replace(WALL_STEP_PREFIX_RE, "");
762
+ if (normalized.length === 0 || normalized.endsWith("?") || normalized.split(/\s+/).length > WALL_MAX_WORDS || isDirective(normalized) || isProtected(normalized) || !WALL_NARRATION_RE.test(normalized)) {
763
+ return false;
764
+ }
765
+ const words = contentTokens(normalized);
766
+ const described = words.slice(1);
767
+ if (described.length === 0) return false;
768
+ const code = codeTokens(statement);
769
+ const matched = described.filter((word) => restates([word], code)).length;
770
+ return matched / described.length >= 0.5 && described.length - matched <= WALL_MAX_NOVEL_WORDS;
771
+ }
707
772
  var TYPE_MEMBER_CONTAINERS = /* @__PURE__ */ new Set([
708
773
  AST_NODE_TYPES5.TSInterfaceBody,
709
774
  AST_NODE_TYPES5.TSTypeLiteral
@@ -759,6 +824,7 @@ var no_comment_cruft_default = createRule({
759
824
  commentedOutCode: "Commented-out code \u2014 delete it; git history remembers.",
760
825
  sectionBanner: "Section-banner / region comment \u2014 structure code with functions, not ASCII rules.",
761
826
  fileHeaderPreamble: "File-header comment preamble \u2014 use a brief doc comment for the why, not a block of `//` lines.",
827
+ commentWall: "Statement comment wall ({{count}} narrated steps) \u2014 delete the walkthrough and name the operations in code; keep only constraints or rationale.",
762
828
  redundantNarration: "Comment narrates the code \u2014 delete it or say *why*, not *what*. Code is self-documenting.",
763
829
  untrackedTodo: "Untracked TODO/FIXME marker \u2014 add an issue ticket or context link."
764
830
  }
@@ -778,6 +844,48 @@ var no_comment_cruft_default = createRule({
778
844
  function isJsDoc(comment) {
779
845
  return comment.type === "Block" && /^\*/.test(comment.value);
780
846
  }
847
+ function findCommentWalls(comments) {
848
+ const attached = /* @__PURE__ */ new Map();
849
+ for (const comment of comments) {
850
+ if (comment.type !== "Line" || !isStandalone(comment)) continue;
851
+ const attachment = statementAttachmentBelow(comment, sourceCode);
852
+ if (attachment === null) continue;
853
+ const body = stripCommentMarker(comment.value);
854
+ const entries = attached.get(attachment.container) ?? [];
855
+ entries.push({
856
+ comment,
857
+ index: attachment.index,
858
+ weak: isWeakWalkthroughComment(body, attachment.statement)
859
+ });
860
+ attached.set(attachment.container, entries);
861
+ }
862
+ const walls = [];
863
+ for (const entries of attached.values()) {
864
+ const sorted = entries.toSorted((left, right) => left.index - right.index);
865
+ const clusters = [];
866
+ for (const entry of sorted) {
867
+ const cluster = clusters.at(-1);
868
+ if (cluster !== void 0 && entry.index <= (cluster.at(-1)?.index ?? 0) + 2) {
869
+ cluster.push(entry);
870
+ } else {
871
+ clusters.push([entry]);
872
+ }
873
+ }
874
+ for (const cluster of clusters) {
875
+ const firstIndex = cluster[0]?.index;
876
+ const lastIndex = cluster.at(-1)?.index;
877
+ if (firstIndex === void 0 || lastIndex === void 0) continue;
878
+ const span = lastIndex - firstIndex + 1;
879
+ const weak = cluster.filter((entry) => entry.weak).map((entry) => entry.comment);
880
+ if (span < WALL_MIN_STATEMENTS || weak.length < WALL_MIN_COMMENTS || cluster.length / span < WALL_MIN_COMMENTED_RATIO || weak.length / cluster.length < WALL_MIN_WEAK_RATIO) {
881
+ continue;
882
+ }
883
+ const leader = weak[0];
884
+ if (leader !== void 0) walls.push({ leader, members: new Set(weak) });
885
+ }
886
+ }
887
+ return walls;
888
+ }
781
889
  function isSectionJsDoc(comment) {
782
890
  const texts = comment.value.split("\n").map(stripCommentMarker).filter((line) => line.length > 0 && !isDirective(line));
783
891
  return texts.length > 0 && texts.every(
@@ -810,6 +918,9 @@ var no_comment_cruft_default = createRule({
810
918
  return {
811
919
  Program() {
812
920
  const comments = sourceCode.getAllComments();
921
+ const walls = findCommentWalls(comments);
922
+ const wallByLeader = new Map(walls.map((wall) => [wall.leader, wall]));
923
+ const wallMembers = new Set(walls.flatMap((wall) => [...wall.members]));
813
924
  const firstCodeLine = sourceCode.ast.tokens[0]?.loc.start.line ?? Number.MAX_SAFE_INTEGER;
814
925
  const enumerated = comments.filter(
815
926
  (c) => c.type === "Line" && ENUMERATION_RE.test(stripCommentMarker(c.value))
@@ -817,6 +928,16 @@ var no_comment_cruft_default = createRule({
817
928
  for (let i = 0; i < comments.length; i++) {
818
929
  const comment = comments[i];
819
930
  if (comment === void 0) continue;
931
+ const wall = wallByLeader.get(comment);
932
+ if (wall !== void 0) {
933
+ context.report({
934
+ node: comment,
935
+ messageId: "commentWall",
936
+ data: { count: String(wall.members.size) }
937
+ });
938
+ continue;
939
+ }
940
+ if (wallMembers.has(comment)) continue;
820
941
  if (isJsDoc(comment)) {
821
942
  if (isStandalone(comment) && isSectionJsDoc(comment)) {
822
943
  context.report({ node: comment, messageId: "sectionBanner" });
@@ -3327,6 +3448,9 @@ var EMPHASIS_RE = /\*\w[^*]*\*|`[^`]+`/;
3327
3448
  var NEGATION_WORD_RE = /\b(?:no|not|never|neither|nor|without|none|non)\b/i;
3328
3449
  var ACTION_STMT_RE = /[\w.$\])]\s*\(|^\s*(?:return|throw|await|yield)\b/;
3329
3450
  var NON_ASCII_LETTER_RE = /[^\p{ASCII}\p{N}\p{P}\p{Z}]/u;
3451
+ var WALL_NARRATION_RE2 = /^(?:(?:\d+[.)]|(?:phase|step)\s+\d+\s*:?)\s*)?(?:add|build|call|check|compute|copy|count|create|fetch|filter|find|get|handle|load|map|merge|parse|process|read|remove|return|save|send|set|sort|store|update|validate|write)(?:s|es|d|ed|ing)?\b/i;
3452
+ var WALL_CLUSTER_MAX_LINE_GAP = 8;
3453
+ var WALL_CLUSTER_MIN_COMMENTS = 3;
3330
3454
  function areAdjacentLineComments2(a, b) {
3331
3455
  return a !== void 0 && b !== void 0 && a.type === "Line" && b.type === "Line" && b.loc.start.line === a.loc.end.line + 1;
3332
3456
  }
@@ -3373,9 +3497,29 @@ var no_restated_comment_default = createRule({
3373
3497
  return {
3374
3498
  Program() {
3375
3499
  const comments = sourceCode.getAllComments();
3500
+ const wallMembers = /* @__PURE__ */ new Set();
3501
+ let cluster = [];
3502
+ for (const candidate of comments) {
3503
+ const body = candidate.value.replace(/^\/*/, "").trim();
3504
+ if (candidate.type !== "Line" || !isStandalone(candidate) || isProtected(body) || !WALL_NARRATION_RE2.test(body)) {
3505
+ continue;
3506
+ }
3507
+ const previous = cluster.at(-1);
3508
+ if (previous !== void 0 && candidate.loc.start.line - previous.loc.start.line > WALL_CLUSTER_MAX_LINE_GAP) {
3509
+ if (cluster.length >= WALL_CLUSTER_MIN_COMMENTS) {
3510
+ for (const member of cluster) wallMembers.add(member);
3511
+ }
3512
+ cluster = [];
3513
+ }
3514
+ cluster.push(candidate);
3515
+ }
3516
+ if (cluster.length >= WALL_CLUSTER_MIN_COMMENTS) {
3517
+ for (const member of cluster) wallMembers.add(member);
3518
+ }
3376
3519
  for (let i = 0; i < comments.length; i++) {
3377
3520
  const comment = comments[i];
3378
3521
  if (comment === void 0 || comment.type !== "Line") continue;
3522
+ if (wallMembers.has(comment)) continue;
3379
3523
  if (!isStandalone(comment)) continue;
3380
3524
  if (areAdjacentLineComments2(comments[i - 1], comment) || areAdjacentLineComments2(comment, comments[i + 1])) {
3381
3525
  continue;
@@ -9512,7 +9656,7 @@ var rules = {
9512
9656
  };
9513
9657
  var meta = {
9514
9658
  name: "@sarj/eslint-plugin",
9515
- version: "9.4.0"
9659
+ version: "9.6.0"
9516
9660
  };
9517
9661
  var recommendedRules = {
9518
9662
  "@sarj/enforce-file-structure": "warn",