From 0107079fea9ada8919d708eff2fcb47f7b4a18a0 Mon Sep 17 00:00:00 2001 From: Arno V Date: Sun, 4 Jan 2026 18:02:36 -0500 Subject: [PATCH] fix(comments): Improve handling of linter and tool directive comments Expanded detection and preservation of linter/tool directive comments (ESLint, Biome, Prettier, TypeScript, StyleLint, coverage tools) to prevent them from being wrapped or merged. Added comprehensive tests to ensure these directives are left untouched and not merged with adjacent comments. Updated documentation to reflect the expanded set of preserved directives. --- packages/comments/README.md | 9 +- packages/comments/src/__tests__/lib.test.ts | 157 ++++++++++++++++++++ packages/comments/src/lib.ts | 20 ++- 3 files changed, 181 insertions(+), 5 deletions(-) diff --git a/packages/comments/README.md b/packages/comments/README.md index 0e4aa837..ed54101c 100644 --- a/packages/comments/README.md +++ b/packages/comments/README.md @@ -117,7 +117,14 @@ interface FileResult { - Adds a terminal period to sentences lacking final punctuation (unless ending with a colon, tag line, list marker, or detected continuation). - Normalizes `NOTE:` capitalization and splits multiple NOTE sentences safely. -- Skips wrapping for directive / tool lines (`@tag`, `eslint`, `ts-ignore`) and URLs. +- Skips wrapping/merging for linter and tool directive comments, including: + - ESLint (`eslint-disable`, `eslint-enable`, `eslint-disable-next-line`, etc.) + - Biome (`biome-ignore`) + - TypeScript (`@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `@ts-check`) + - Prettier (`prettier-ignore`) + - StyleLint (`stylelint-disable`, `stylelint-enable`) + - Coverage tools (`v8 ignore`, `c8 ignore`, `istanbul ignore`) + - URLs (`https://...`) - Preserves fenced code blocks and visually indented code blocks in JSDoc. - Structured explanation groups (regex / pattern walkthroughs) remain line‑stable to avoid unintended semantic changes. - Protects against pathological inputs with maximum indentation and comment body size limits. diff --git a/packages/comments/src/__tests__/lib.test.ts b/packages/comments/src/__tests__/lib.test.ts index 77fd0689..e08b8195 100644 --- a/packages/comments/src/__tests__/lib.test.ts +++ b/packages/comments/src/__tests__/lib.test.ts @@ -585,4 +585,161 @@ describe("parseAndTransformComments", () => { expect(out).toMatch(/NOTE: important note here/); expect(out).toMatch(/Final concluding text/); }); + + it("preserves biome-ignore comments without modification", () => { + const src = [ + "// biome-ignore lint/suspicious/noExplicitAny: required for dynamic typing", + "const x: any = {};", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }).transformed; + expect(out).toBe(src); + }); + + it("does not merge biome-ignore with following comment lines", () => { + const src = [ + "// biome-ignore lint/suspicious/noExplicitAny: dynamic", + "// This is a separate comment", + "// that should not be merged with biome-ignore", + "const x = 1;", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }).transformed; + // The biome directive should remain as a line comment. + expect(out).toContain( + "// biome-ignore lint/suspicious/noExplicitAny: dynamic", + ); + // The following comments should not include the biome directive content. + expect(out).not.toMatch(/\* biome-ignore/); + }); + + it("preserves eslint-disable comments", () => { + const src = [ + "// eslint-disable-next-line no-console", + "console.log('test');", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: false, + }).transformed; + expect(out).toBe(src); + }); + + it("preserves prettier-ignore comments", () => { + const src = [ + "// prettier-ignore", + "const matrix = [", + " 1, 0, 0,", + " 0, 1, 0,", + "];", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }).transformed; + expect(out).toContain("// prettier-ignore"); + }); + + it("preserves @ts-expect-error comments", () => { + const src = [ + "// @ts-expect-error - intentionally testing error case", + "invalidFunction();", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: false, + }).transformed; + expect(out).toBe(src); + }); + + it("preserves @ts-ignore comments", () => { + const src = "// @ts-ignore\nconst x = badCode;"; + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: false, + }).transformed; + expect(out).toBe(src); + }); + + it("preserves stylelint-disable comments", () => { + const src = "// stylelint-disable declaration-no-important"; + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: false, + }).transformed; + expect(out).toBe(src); + }); + + it("preserves v8 ignore and c8 ignore coverage directives", () => { + const src = [ + "// v8 ignore next", + "// c8 ignore start", + "function untestable() {}", + "// c8 ignore stop", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }).transformed; + expect(out).toContain("// v8 ignore next"); + expect(out).toContain("// c8 ignore start"); + expect(out).toContain("// c8 ignore stop"); + }); + + it("preserves istanbul ignore comments", () => { + const src = "// istanbul ignore next"; + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: false, + }).transformed; + expect(out).toBe(src); + }); + + it("stops merging comment group before linter directive", () => { + const src = [ + "// This is an explanatory comment", + "// that spans multiple lines", + "// biome-ignore lint/style/useConst: needs reassignment", + "let value = compute();", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }).transformed; + expect(out).toContain("/**"); + expect(out).toContain("// biome-ignore"); + expect(out).not.toMatch(/\* biome-ignore/); + }); + + it("handles multiple different linter directives in sequence", () => { + const src = [ + "// eslint-disable-next-line @typescript-eslint/no-explicit-any", + "// biome-ignore lint/suspicious/noExplicitAny: legacy code", + "// @ts-expect-error - type mismatch expected", + "const x: any = getValue();", + ].join("\n"); + const out = parseAndTransformComments(src, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }).transformed; + expect(out).toContain("// eslint-disable-next-line"); + expect(out).toContain("// biome-ignore"); + expect(out).toContain("// @ts-expect-error"); + expect(out).not.toContain("/**"); + }); }); diff --git a/packages/comments/src/lib.ts b/packages/comments/src/lib.ts index 70854392..0a8cd532 100644 --- a/packages/comments/src/lib.ts +++ b/packages/comments/src/lib.ts @@ -191,6 +191,18 @@ function isVisuallyIndentedCode(line: string): boolean { return /^\s{2,}\S/.test(line); } +/** + * Detects linter/tool directive comments that should be left untouched. + * These include ESLint, Biome, Prettier, TypeScript, StyleLint, and coverage + * tool directives. + */ +function isLinterDirective(text: string): boolean { + const trimmed = text.trim(); + return /^(@?(?:ts-ignore|ts-expect-error|ts-nocheck|ts-check)|eslint|biome-ignore|prettier-ignore|stylelint-(?:disable|enable)|c8 ignore|v8 ignore|istanbul ignore|tsc:|type:|@ts-)/i.test( + trimmed, + ); +} + function wrapWords(text: string, width: number): string[] { const words = text.split(/\s+/).filter(Boolean); const lines: string[] = []; @@ -456,7 +468,7 @@ function wrapLineComments( break; } const body = gm[2]; - if (/^(@|eslint|ts-ignore)/.test(body) || /https?:\/\//.test(body)) { + if (isLinterDirective(body) || /https?:\/\//.test(body)) { break; // stop group before directives/URLs; process current line normally } group.push({ raw: lines[j], indent: gm[1], body }); @@ -466,7 +478,7 @@ function wrapLineComments( // Single line: existing logic (add period if needed). const indent = m[1]; const body = m[3]; - if (/^(@|eslint|ts-ignore)/.test(body) || /https?:\/\//.test(body)) { + if (isLinterDirective(body) || /https?:\/\//.test(body)) { out.push(line); i++; continue; @@ -532,7 +544,7 @@ function mergeLineCommentGroups(content: string): { break; } const body = lm[3]; - if (/^(@|eslint|ts-ignore)/.test(body) || /https?:\/\//.test(body)) { + if (isLinterDirective(body) || /https?:\/\//.test(body)) { break; } collected.push(body.trim()); @@ -582,7 +594,7 @@ function mergeLineCommentGroups(content: string): { if ( /license|copyright/i.test(txt) || /https?:\/\//.test(txt) || - /eslint|ts-ignore/.test(txt) + isLinterDirective(txt) ) { break; // do not merge directive / license groups }