From d967dd31b1e66128328fba865963831f6c495298 Mon Sep 17 00:00:00 2001 From: Arno V Date: Sat, 30 May 2026 11:21:29 -0400 Subject: [PATCH] fix(comments): prevent scoped package names from corrupting wrapped prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isTagLine() classified any line starting with `@` as a JSDoc tag. When reflowed prose placed a scoped npm package name (e.g. `@auth0/auth0-react`, `@versini/ui-main`) at the start of a wrapped line, the engine treated it as a paragraph boundary, flushed the in-progress sentence early, and injected a spurious period mid-clause — silently corrupting comments in a format-only diff. Narrow the tag match with a negative lookahead so a scoped specifier (`@scope/...`) is no longer detected as a tag, while genuine `@param`, `@returns`, etc. still are. Adds regression tests covering both cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/comments/src/__tests__/lib.test.ts | 31 +++++++++++++++++++++ packages/comments/src/lib.ts | 9 +++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/comments/src/__tests__/lib.test.ts b/packages/comments/src/__tests__/lib.test.ts index e08b8195..1932e5b8 100644 --- a/packages/comments/src/__tests__/lib.test.ts +++ b/packages/comments/src/__tests__/lib.test.ts @@ -95,6 +95,37 @@ describe("parseAndTransformComments", () => { expect(/@returns something/.test(res)).toBe(true); }); + it("does not treat a scoped package name as a JSDoc tag", () => { + const input = + [ + "/**", + " * We match on the error code. If a future", + " * @auth0/auth0-react release reshapes the description, the check still fires.", + " */", + ].join("\n") + "\n"; + const { transformed } = parseAndTransformComments(input, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }); + // No period injected mid-sentence. + expect(transformed).not.toMatch(/future\./); + // Sentence joined, scoped package kept inline as prose. + expect(transformed).toMatch(/future @auth0\/auth0-react/); + }); + + it("still treats real JSDoc tags as tags after the scoped-package fix", () => { + const input = + "/**\n * Does a thing.\n * @param x in\n * @returns out\n */\n"; + const { transformed } = parseAndTransformComments(input, { + width: 80, + wrapLineComments: true, + mergeLineComments: true, + }); + expect(transformed).toMatch(/^ \* @param x in$/m); + expect(transformed).toMatch(/^ \* @returns out$/m); + }); + it("handles heading-like lines with colon and visually indented code & numeric lists", () => { const input = [ "/**", diff --git a/packages/comments/src/lib.ts b/packages/comments/src/lib.ts index 0a8cd532..c245b480 100644 --- a/packages/comments/src/lib.ts +++ b/packages/comments/src/lib.ts @@ -165,7 +165,14 @@ function isListLike(line: string): boolean { } function isTagLine(line: string): boolean { - return /^@/.test(line.trim()); + /** + * A JSDoc/TSDoc tag is `@name` (optionally hyphenated) and is never + * immediately followed by `/`. The negative lookahead excludes scoped npm + * package specifiers like `@auth0/auth0-react` or `@versini/ui-main` that can + * legitimately open a wrapped prose line; treating those as tags severs the + * sentence and injects a spurious period. + */ + return /^@[A-Za-z][\w-]*(?![\w/-])/.test(line.trim()); } function isHeadingLike(line: string): boolean {