ReviewOS

also looking at this

stacks/dtsx

fix(emit): keep a */ in a default value from closing its JSDoc

#3118
Open mkantautas wants to merge fix/defaultvalue-comment-close into main
3 files +38 -2
packages/dtsx/src/processor/declarations.tsmodified+2-1
Changes to packages/dtsx/src/processor/declarations.ts
@@ -4,7 +4,7 @@
44
55import type { Declaration } from '../types'
66import { formatComments } from './comments'
7import { consumeCleanDefault, enableCleanDefaultCollection, extractSatisfiesType, inferNarrowType, isGenericType } from './type-inference'
7import { consumeCleanDefault, enableCleanDefaultCollection, escapeCommentEnd, extractSatisfiesType, inferNarrowType, isGenericType } from './type-inference'
88
99/**
1010 * Format a @defaultValue tag as a standalone JSDoc block
@@ -183,6 +183,7 @@ export function processVariableDeclaration(decl: Declaration, keepComments: bool
183183 }
184184
185185 result += `: ${typeAnnotation};`
186 defaultTag = escapeCommentEnd(defaultTag)
186187
187188 // Skip generated @defaultValue if user already has one
188189 if (defaultTag && comments && comments.includes('@defaultValue')) {
packages/dtsx/src/processor/type-inference.tsmodified+10-1
Changes to packages/dtsx/src/processor/type-inference.ts
@@ -21,6 +21,15 @@ function withoutComments(type: string): string {
2121 return stripBlockComments(type).replace(/\n[ \t]*(?=\n)/g, '')
2222}
2323
24/**
25 * A default value as written into a JSDoc comment, with a backslash between
26 * `*` and `/` wherever they meet. Left as is, a glob such as `'src/**\/*.ts'`
27 * ends the comment early and the rest of the line becomes code.
28 */
29export function escapeCommentEnd(text: string): string {
30 return text.includes('*/') ? text.replace(/\*\//g, '*\\/') : text
31}
32
2433/** Strip block/JSDoc comments from a property key to keep @defaultValue clean */
2534function stripBlockComments(s: string): string {
2635 // Fast path: no `/*` present — return trimmed input directly (zero scan).
@@ -1295,7 +1304,7 @@ export function inferObjectType(value: string, isConst: boolean, _depth: number
12951304 }
12961305
12971306 if (!isConst && isBaseType(valueType) && isPrimitiveLiteral(rawVal)) {
1298 propTypes.push(`${commentPrefix}/** @defaultValue ${rawVal} */\n ${emitKey}: ${valueType}`)
1307 propTypes.push(`${commentPrefix}/** @defaultValue ${escapeCommentEnd(rawVal)} */\n ${emitKey}: ${valueType}`)
12991308 }
13001309 else {
13011310 propTypes.push(`${commentPrefix}${emitKey}: ${valueType}`)
packages/dtsx/test/declaration-emission.test.tsmodified+26-0
Changes to packages/dtsx/test/declaration-emission.test.ts
@@ -274,3 +274,29 @@ export const value: number = 1
274274 expect(output).not.toContain('internal')
275275 })
276276})
277
278/**
279 * A default value holding `*\/` closed the generated JSDoc early:
280 *
281 * export const entry = ['src/**\/*.ts']
282 *
283 * emitted `/** @defaultValue `['src/**\/*.ts']` *\/`, which ends at the `**\/`
284 * and leaves `*.ts']` ... as code, so the whole `.d.ts` failed to parse. Globs
285 * like this are the usual defaults in config files; knip's plugins hit it in
286 * 52 files.
287 */
288describe('`*/` inside a generated @defaultValue', () => {
289 it.each([
290 ['const array', 'export const entry = [\'src/**/*.ts\']\nexport const after = 1\n', '`[\'src/**\\/*.ts\']`'],
291 ['let string', 'export let close = \'*/\'\nexport const after = 1\n', '@defaultValue \'*\\/\''],
292 ['property of a let object', 'export let config = { entry: \'**/*.ts\' }\nexport const after = 1\n', '/** @defaultValue \'**\\/*.ts\' */'],
293 ['merged into an existing JSDoc', '/** Entry globs */\nexport const entry = [\'**/*.ts\']\nexport const after = 1\n', '* @defaultValue `[\'**\\/*.ts\']`'],
294 ['generic annotation', 'export const paths: Record<string, string> = { glob: \'**/*.ts\' }\nexport const after = 1\n', '`{ glob: \'**\\/*.ts\' }`'],
295 ['multi-line default', 'export const config = {\n entry: [\'src/index.ts\', \'src/cli.ts\', \'src/plugins/index.ts\'],\n project: [\'src/**/*.ts\', \'scripts/**/*.ts\'],\n}\nexport const after = 1\n', ' * project: [\'src/**\\/*.ts\', \'scripts/**\\/*.ts\']'],
296 ])('keeps the comment closed: %s', (_label, code, expected) => {
297 const output = processCode(code)
298 expectValidTypeScript(output)
299 expect(output).toContain(expected)
300 expect(output).toContain('export declare const after: 1;')
301 })
302})