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 }