ReviewOS

also looking at this

stacks/dtsx

fix(scanner): leave line comments out when collapsing multi-line generics

#3119
Open mkantautas wants to merge fix/generics-line-comment into main
2 files +67 -9
packages/dtsx/src/extractor/scanner.tsmodified+22-9
Changes to packages/dtsx/src/extractor/scanner.ts
@@ -1389,28 +1389,41 @@ function scanDeclarationsInternal(_source: string, _filename: string, _keepComme
13891389 return rebased.join('\n')
13901390 }
13911391
1392 /** Extract type parameters <...> (normalized to single line) */
1392 /** Extract type parameters <...>, whitespace outside literals and comments collapsed */
13931393 function extractGenerics(): string {
13941394 if (pos >= len || source.charCodeAt(pos) !== CH_LANGLE)
13951395 return ''
13961396 const start = pos
13971397 findMatchingClose(CH_LANGLE, CH_RANGLE)
1398 const raw = source.slice(start, pos)
1399 // Normalize multi-line generics to single line (avoid regex)
1398 const end = pos
1399 const raw = source.slice(start, end)
1400 // Normalize multi-line generics (avoid regex)
14001401 if (raw.includes('\n')) {
1401 // Collapse whitespace/newlines to single spaces
1402 // Collapse whitespace/newlines to single spaces. A `//` comment is left
1403 // out: on one line it would comment out everything after it, the closing
1404 // `>` included. Strings and block comments are copied as they are.
14021405 let result = ''
14031406 let prevSpace = false
1404 for (let i = 0; i < raw.length; i++) {
1405 const c = raw.charCodeAt(i)
1407 pos = start
1408 while (pos < end) {
1409 const c = source.charCodeAt(pos)
14061410 if (c === 32 || c === 9 || c === 10 || c === 13) {
14071411 if (!prevSpace && result.length > 0) { result += ' '; prevSpace = true }
1412 pos++
1413 continue
14081414 }
1409 else {
1410 result += raw[i]
1411 prevSpace = false
1415 if (c === CH_SLASH && source.charCodeAt(pos + 1) === CH_SLASH) {
1416 const nl = source.indexOf('\n', pos + 2)
1417 pos = nl === -1 || nl > end ? end : nl
1418 continue
14121419 }
1420 const from = pos
1421 if (!skipNonCode())
1422 pos++
1423 result += source.slice(from, Math.min(pos, end))
1424 prevSpace = false
14131425 }
1426 pos = end
14141427 // Trim spaces around < and >
14151428 if (result.charCodeAt(1) === 32) result = `<${result.slice(2)}`
14161429 if (result.charCodeAt(result.length - 2) === 32) result = `${result.slice(0, -2)}>`
packages/dtsx/test/generic-parameter-comments.test.tsadded+45-0
Changes to packages/dtsx/test/generic-parameter-comments.test.ts
@@ -0,0 +1,45 @@
1/**
2 * A `//` comment inside a multi-line type parameter list commented out the rest
3 * of the declaration. Multi-line `<...>` is collapsed onto one line, so
4 *
5 * export function defineComponent<
6 * // props
7 * TypeProps,
8 * ...
9 * >(options: ...): ...
10 *
11 * came out as `defineComponent<// props TypeProps, ...` and everything after
12 * the comment, closing `>` included, was gone. Vue's `defineComponent`,
13 * vue-router's `RouteRecordInfo` and Pinia's store types are all written this
14 * way; their `.d.ts` did not parse.
15 */
16import { describe, expect, it } from 'bun:test'
17import { processCode } from './test-utils'
18
19const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' })
20
21describe('comments in a multi-line type parameter list', () => {
22 it.each([
23 ['function', 'export function f<\n // first\n A,\n B = string, // second\n>(a: A, b: B): void {}\n', 'f<A, B = string,>(a: A, b: B): void;'],
24 ['type alias', 'export type T<\n // note\n A = 1,\n> = A\n', 'type T<A = 1,> = A'],
25 ['interface', 'export interface I<\n // note\n A extends string = string,\n> { a: A }\n', 'interface I<A extends string = string,>'],
26 ['class', 'export class C<\n // note\n T,\n> { v!: T }\n', 'class C<T,>'],
27 ])('leaves the comment out: %s', (_label, code, expected) => {
28 const output = processCode(code)
29 expect(() => transpiler.transformSync(output)).not.toThrow()
30 expect(output).toContain(expected)
31 expect(output).not.toContain('//')
32 })
33
34 it('copies strings and block comments as they are', () => {
35 const output = processCode('export type S<\n /* see http://example.com */ A extends \'//\' | \'a b\' = \'//\',\n> = A\n')
36 expect(() => transpiler.transformSync(output)).not.toThrow()
37 expect(output).toContain('/* see http://example.com */ A extends \'//\' | \'a b\' = \'//\'')
38 })
39
40 it('keeps a multi-line template literal type as written', () => {
41 const output = processCode('export type F<\n // note\n T = `a\nb`,\n> = T\n')
42 expect(() => transpiler.transformSync(output)).not.toThrow()
43 expect(output).toContain('T = `a\nb`')
44 })
45})