@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.cjs CHANGED
@@ -533,6 +533,14 @@ function restatesStatementHead(body, statement) {
533
533
 
534
534
  // src/rules/no-comment-cruft.ts
535
535
  var LEADING_PREAMBLE_MIN = 4;
536
+ var WALL_MIN_STATEMENTS = 4;
537
+ var WALL_MIN_COMMENTS = 3;
538
+ var WALL_MIN_COMMENTED_RATIO = 0.6;
539
+ var WALL_MIN_WEAK_RATIO = 0.75;
540
+ var WALL_MAX_WORDS = 18;
541
+ var WALL_MAX_NOVEL_WORDS = 2;
542
+ 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;
543
+ var WALL_STEP_PREFIX_RE = /^(?:\d+[.)]|(?:phase|step)\s+\d+\s*:)\s*/i;
536
544
  var STEP_NARRATION_RE = /^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|after(?:wards| that)?|finally|lastly|now)\s*[,:]\s*\S/i;
537
545
  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;
538
546
  var FOR_NOW_RE = /\bfor now\b/i;
@@ -745,6 +753,63 @@ var STATEMENT_CONTAINERS = /* @__PURE__ */ new Set([
745
753
  import_utils6.AST_NODE_TYPES.TSModuleBlock,
746
754
  import_utils6.AST_NODE_TYPES.TSInterfaceBody
747
755
  ]);
756
+ function directStatements(container) {
757
+ switch (container.type) {
758
+ case import_utils6.AST_NODE_TYPES.Program:
759
+ case import_utils6.AST_NODE_TYPES.BlockStatement:
760
+ case import_utils6.AST_NODE_TYPES.ClassBody:
761
+ case import_utils6.AST_NODE_TYPES.StaticBlock:
762
+ case import_utils6.AST_NODE_TYPES.TSModuleBlock:
763
+ return container.body;
764
+ case import_utils6.AST_NODE_TYPES.SwitchCase:
765
+ return container.consequent;
766
+ case import_utils6.AST_NODE_TYPES.TSInterfaceBody:
767
+ return container.body;
768
+ default:
769
+ return [];
770
+ }
771
+ }
772
+ var WALL_STATEMENTS = /* @__PURE__ */ new Set([
773
+ import_utils6.AST_NODE_TYPES.ExpressionStatement,
774
+ import_utils6.AST_NODE_TYPES.ReturnStatement,
775
+ import_utils6.AST_NODE_TYPES.ThrowStatement,
776
+ import_utils6.AST_NODE_TYPES.VariableDeclaration,
777
+ import_utils6.AST_NODE_TYPES.IfStatement,
778
+ import_utils6.AST_NODE_TYPES.ForStatement,
779
+ import_utils6.AST_NODE_TYPES.ForOfStatement,
780
+ import_utils6.AST_NODE_TYPES.ForInStatement,
781
+ import_utils6.AST_NODE_TYPES.WhileStatement,
782
+ import_utils6.AST_NODE_TYPES.DoWhileStatement,
783
+ import_utils6.AST_NODE_TYPES.SwitchStatement,
784
+ import_utils6.AST_NODE_TYPES.TryStatement
785
+ ]);
786
+ function statementAttachmentBelow(comment, sourceCode) {
787
+ const token = sourceCode.getTokenAfter(comment, { includeComments: false });
788
+ if (token === null || token.loc.start.line !== comment.loc.end.line + 1 || token.loc.start.column !== comment.loc.start.column) {
789
+ return null;
790
+ }
791
+ for (let node = sourceCode.getNodeByRangeIndex(token.range[0]); node?.parent != null; node = node.parent) {
792
+ if (!STATEMENT_CONTAINERS.has(node.parent.type) || !WALL_STATEMENTS.has(node.type)) {
793
+ continue;
794
+ }
795
+ const siblings = directStatements(node.parent);
796
+ const index = siblings.indexOf(node);
797
+ return index < 0 ? null : { container: node.parent, index, statement: sourceCode.getText(node) };
798
+ }
799
+ return null;
800
+ }
801
+ function isWeakWalkthroughComment(body, statement) {
802
+ const normalized = body.replace(WALL_STEP_PREFIX_RE, "");
803
+ if (normalized.length === 0 || normalized.endsWith("?") || normalized.split(/\s+/).length > WALL_MAX_WORDS || isDirective(normalized) || isProtected(normalized) || !WALL_NARRATION_RE.test(normalized)) {
804
+ return false;
805
+ }
806
+ const words = contentTokens(normalized);
807
+ const described = words.slice(1);
808
+ if (described.length === 0) return false;
809
+ const code = codeTokens(statement);
810
+ const matched = described.filter((word) => restates([word], code)).length;
811
+ return matched / described.length >= 0.5 && described.length - matched <= WALL_MAX_NOVEL_WORDS;
812
+ }
748
813
  var TYPE_MEMBER_CONTAINERS = /* @__PURE__ */ new Set([
749
814
  import_utils6.AST_NODE_TYPES.TSInterfaceBody,
750
815
  import_utils6.AST_NODE_TYPES.TSTypeLiteral
@@ -800,6 +865,7 @@ var no_comment_cruft_default = createRule({
800
865
  commentedOutCode: "Commented-out code \u2014 delete it; git history remembers.",
801
866
  sectionBanner: "Section-banner / region comment \u2014 structure code with functions, not ASCII rules.",
802
867
  fileHeaderPreamble: "File-header comment preamble \u2014 use a brief doc comment for the why, not a block of `//` lines.",
868
+ commentWall: "Statement comment wall ({{count}} narrated steps) \u2014 delete the walkthrough and name the operations in code; keep only constraints or rationale.",
803
869
  redundantNarration: "Comment narrates the code \u2014 delete it or say *why*, not *what*. Code is self-documenting.",
804
870
  untrackedTodo: "Untracked TODO/FIXME marker \u2014 add an issue ticket or context link."
805
871
  }
@@ -819,6 +885,48 @@ var no_comment_cruft_default = createRule({
819
885
  function isJsDoc(comment) {
820
886
  return comment.type === "Block" && /^\*/.test(comment.value);
821
887
  }
888
+ function findCommentWalls(comments) {
889
+ const attached = /* @__PURE__ */ new Map();
890
+ for (const comment of comments) {
891
+ if (comment.type !== "Line" || !isStandalone(comment)) continue;
892
+ const attachment = statementAttachmentBelow(comment, sourceCode);
893
+ if (attachment === null) continue;
894
+ const body = stripCommentMarker(comment.value);
895
+ const entries = attached.get(attachment.container) ?? [];
896
+ entries.push({
897
+ comment,
898
+ index: attachment.index,
899
+ weak: isWeakWalkthroughComment(body, attachment.statement)
900
+ });
901
+ attached.set(attachment.container, entries);
902
+ }
903
+ const walls = [];
904
+ for (const entries of attached.values()) {
905
+ const sorted = entries.toSorted((left, right) => left.index - right.index);
906
+ const clusters = [];
907
+ for (const entry of sorted) {
908
+ const cluster = clusters.at(-1);
909
+ if (cluster !== void 0 && entry.index <= (cluster.at(-1)?.index ?? 0) + 2) {
910
+ cluster.push(entry);
911
+ } else {
912
+ clusters.push([entry]);
913
+ }
914
+ }
915
+ for (const cluster of clusters) {
916
+ const firstIndex = cluster[0]?.index;
917
+ const lastIndex = cluster.at(-1)?.index;
918
+ if (firstIndex === void 0 || lastIndex === void 0) continue;
919
+ const span = lastIndex - firstIndex + 1;
920
+ const weak = cluster.filter((entry) => entry.weak).map((entry) => entry.comment);
921
+ 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) {
922
+ continue;
923
+ }
924
+ const leader = weak[0];
925
+ if (leader !== void 0) walls.push({ leader, members: new Set(weak) });
926
+ }
927
+ }
928
+ return walls;
929
+ }
822
930
  function isSectionJsDoc(comment) {
823
931
  const texts = comment.value.split("\n").map(stripCommentMarker).filter((line) => line.length > 0 && !isDirective(line));
824
932
  return texts.length > 0 && texts.every(
@@ -851,6 +959,9 @@ var no_comment_cruft_default = createRule({
851
959
  return {
852
960
  Program() {
853
961
  const comments = sourceCode.getAllComments();
962
+ const walls = findCommentWalls(comments);
963
+ const wallByLeader = new Map(walls.map((wall) => [wall.leader, wall]));
964
+ const wallMembers = new Set(walls.flatMap((wall) => [...wall.members]));
854
965
  const firstCodeLine = sourceCode.ast.tokens[0]?.loc.start.line ?? Number.MAX_SAFE_INTEGER;
855
966
  const enumerated = comments.filter(
856
967
  (c) => c.type === "Line" && ENUMERATION_RE.test(stripCommentMarker(c.value))
@@ -858,6 +969,16 @@ var no_comment_cruft_default = createRule({
858
969
  for (let i = 0; i < comments.length; i++) {
859
970
  const comment = comments[i];
860
971
  if (comment === void 0) continue;
972
+ const wall = wallByLeader.get(comment);
973
+ if (wall !== void 0) {
974
+ context.report({
975
+ node: comment,
976
+ messageId: "commentWall",
977
+ data: { count: String(wall.members.size) }
978
+ });
979
+ continue;
980
+ }
981
+ if (wallMembers.has(comment)) continue;
861
982
  if (isJsDoc(comment)) {
862
983
  if (isStandalone(comment) && isSectionJsDoc(comment)) {
863
984
  context.report({ node: comment, messageId: "sectionBanner" });
@@ -3368,6 +3489,9 @@ var EMPHASIS_RE = /\*\w[^*]*\*|`[^`]+`/;
3368
3489
  var NEGATION_WORD_RE = /\b(?:no|not|never|neither|nor|without|none|non)\b/i;
3369
3490
  var ACTION_STMT_RE = /[\w.$\])]\s*\(|^\s*(?:return|throw|await|yield)\b/;
3370
3491
  var NON_ASCII_LETTER_RE = /[^\p{ASCII}\p{N}\p{P}\p{Z}]/u;
3492
+ 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;
3493
+ var WALL_CLUSTER_MAX_LINE_GAP = 8;
3494
+ var WALL_CLUSTER_MIN_COMMENTS = 3;
3371
3495
  function areAdjacentLineComments2(a, b) {
3372
3496
  return a !== void 0 && b !== void 0 && a.type === "Line" && b.type === "Line" && b.loc.start.line === a.loc.end.line + 1;
3373
3497
  }
@@ -3414,9 +3538,29 @@ var no_restated_comment_default = createRule({
3414
3538
  return {
3415
3539
  Program() {
3416
3540
  const comments = sourceCode.getAllComments();
3541
+ const wallMembers = /* @__PURE__ */ new Set();
3542
+ let cluster = [];
3543
+ for (const candidate of comments) {
3544
+ const body = candidate.value.replace(/^\/*/, "").trim();
3545
+ if (candidate.type !== "Line" || !isStandalone(candidate) || isProtected(body) || !WALL_NARRATION_RE2.test(body)) {
3546
+ continue;
3547
+ }
3548
+ const previous = cluster.at(-1);
3549
+ if (previous !== void 0 && candidate.loc.start.line - previous.loc.start.line > WALL_CLUSTER_MAX_LINE_GAP) {
3550
+ if (cluster.length >= WALL_CLUSTER_MIN_COMMENTS) {
3551
+ for (const member of cluster) wallMembers.add(member);
3552
+ }
3553
+ cluster = [];
3554
+ }
3555
+ cluster.push(candidate);
3556
+ }
3557
+ if (cluster.length >= WALL_CLUSTER_MIN_COMMENTS) {
3558
+ for (const member of cluster) wallMembers.add(member);
3559
+ }
3417
3560
  for (let i = 0; i < comments.length; i++) {
3418
3561
  const comment = comments[i];
3419
3562
  if (comment === void 0 || comment.type !== "Line") continue;
3563
+ if (wallMembers.has(comment)) continue;
3420
3564
  if (!isStandalone(comment)) continue;
3421
3565
  if (areAdjacentLineComments2(comments[i - 1], comment) || areAdjacentLineComments2(comment, comments[i + 1])) {
3422
3566
  continue;
@@ -9547,7 +9691,7 @@ var rules = {
9547
9691
  };
9548
9692
  var meta = {
9549
9693
  name: "@sarj/eslint-plugin",
9550
- version: "9.4.0"
9694
+ version: "9.6.0"
9551
9695
  };
9552
9696
  var recommendedRules = {
9553
9697
  "@sarj/enforce-file-structure": "warn",