also looking at this
fix(scanner): leave line comments out when collapsing multi-line generics
#3119
2 files
+67
-9
| @@ -1389,28 +1389,41 @@ function scanDeclarationsInternal(_source: string, _filename: string, _keepComme | ||
| 1389 | 1389 | return rebased.join('\n') |
| 1390 | 1390 | } |
| 1391 | 1391 | |
| 1392 | /** Extract type parameters <...> (normalized to single line) */ | |
| 1392 | /** Extract type parameters <...>, whitespace outside literals and comments collapsed */ | |
| 1393 | 1393 | function extractGenerics(): string { |
| 1394 | 1394 | if (pos >= len || source.charCodeAt(pos) !== CH_LANGLE) |
| 1395 | 1395 | return '' |
| 1396 | 1396 | const start = pos |
| 1397 | 1397 | 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) | |
| 1400 | 1401 | 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. | |
| 1402 | 1405 | let result = '' |
| 1403 | 1406 | 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) | |
| 1406 | 1410 | if (c === 32 || c === 9 || c === 10 || c === 13) { |
| 1407 | 1411 | if (!prevSpace && result.length > 0) { result += ' '; prevSpace = true } |
| 1412 | pos++ | |
| 1413 | continue | |
| 1408 | 1414 | } |
| 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 | |
| 1412 | 1419 | } |
| 1420 | const from = pos | |
| 1421 | if (!skipNonCode()) | |
| 1422 | pos++ | |
| 1423 | result += source.slice(from, Math.min(pos, end)) | |
| 1424 | prevSpace = false | |
| 1413 | 1425 | } |
| 1426 | pos = end | |
| 1414 | 1427 | // Trim spaces around < and > |
| 1415 | 1428 | if (result.charCodeAt(1) === 32) result = `<${result.slice(2)}` |
| 1416 | 1429 | if (result.charCodeAt(result.length - 2) === 32) result = `${result.slice(0, -2)}>` |
| @@ -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 | */ | |
| 16 | import { describe, expect, it } from 'bun:test' | |
| 17 | import { processCode } from './test-utils' | |
| 18 | ||
| 19 | const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' }) | |
| 20 | ||
| 21 | describe('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 | }) | |