also looking at this
fix(emit): keep a */ in a default value from closing its JSDoc
#3118
3 files
+38
-2
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | |
| 5 | 5 | import type { Declaration } from '../types' |
| 6 | 6 | import { formatComments } from './comments' |
| 7 | import { consumeCleanDefault, enableCleanDefaultCollection, extractSatisfiesType, inferNarrowType, isGenericType } from './type-inference' | |
| 7 | import { consumeCleanDefault, enableCleanDefaultCollection, escapeCommentEnd, extractSatisfiesType, inferNarrowType, isGenericType } from './type-inference' | |
| 8 | 8 | |
| 9 | 9 | /** |
| 10 | 10 | * Format a @defaultValue tag as a standalone JSDoc block |
| @@ -183,6 +183,7 @@ export function processVariableDeclaration(decl: Declaration, keepComments: bool | ||
| 183 | 183 | } |
| 184 | 184 | |
| 185 | 185 | result += `: ${typeAnnotation};` |
| 186 | defaultTag = escapeCommentEnd(defaultTag) | |
| 186 | 187 | |
| 187 | 188 | // Skip generated @defaultValue if user already has one |
| 188 | 189 | if (defaultTag && comments && comments.includes('@defaultValue')) { |
| @@ -21,6 +21,15 @@ function withoutComments(type: string): string { | ||
| 21 | 21 | return stripBlockComments(type).replace(/\n[ \t]*(?=\n)/g, '') |
| 22 | 22 | } |
| 23 | 23 | |
| 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 | */ | |
| 29 | export function escapeCommentEnd(text: string): string { | |
| 30 | return text.includes('*/') ? text.replace(/\*\//g, '*\\/') : text | |
| 31 | } | |
| 32 | ||
| 24 | 33 | /** Strip block/JSDoc comments from a property key to keep @defaultValue clean */ |
| 25 | 34 | function stripBlockComments(s: string): string { |
| 26 | 35 | // Fast path: no `/*` present — return trimmed input directly (zero scan). |
| @@ -1295,7 +1304,7 @@ export function inferObjectType(value: string, isConst: boolean, _depth: number | ||
| 1295 | 1304 | } |
| 1296 | 1305 | |
| 1297 | 1306 | 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}`) | |
| 1299 | 1308 | } |
| 1300 | 1309 | else { |
| 1301 | 1310 | propTypes.push(`${commentPrefix}${emitKey}: ${valueType}`) |
| @@ -274,3 +274,29 @@ export const value: number = 1 | ||
| 274 | 274 | expect(output).not.toContain('internal') |
| 275 | 275 | }) |
| 276 | 276 | }) |
| 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 | */ | |
| 288 | describe('`*/` 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 | }) | |