Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/comments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
157 changes: 157 additions & 0 deletions packages/comments/src/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("/**");
});
});
20 changes: 16 additions & 4 deletions packages/comments/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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 });
Expand All @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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
}
Expand Down