1namespace ts { 2 describe("unittests:: TransformAPI", () => { 3 function replaceUndefinedWithVoid0(context: TransformationContext) { 4 const previousOnSubstituteNode = context.onSubstituteNode; 5 context.enableSubstitution(SyntaxKind.Identifier); 6 context.onSubstituteNode = (hint, node) => { 7 node = previousOnSubstituteNode(hint, node); 8 if (hint === EmitHint.Expression && isIdentifier(node) && node.escapedText === "undefined") { 9 node = factory.createPartiallyEmittedExpression( 10 addSyntheticTrailingComment( 11 setTextRange( 12 factory.createVoidZero(), 13 node), 14 SyntaxKind.MultiLineCommentTrivia, "undefined")); 15 } 16 return node; 17 }; 18 return (file: SourceFile) => file; 19 } 20 function replaceNumberWith2(context: TransformationContext) { 21 function visitor(node: Node): Node { 22 if (isNumericLiteral(node)) { 23 return factory.createNumericLiteral("2"); 24 } 25 return visitEachChild(node, visitor, context); 26 } 27 return (file: SourceFile) => visitNode(file, visitor); 28 } 29 30 function replaceIdentifiersNamedOldNameWithNewName(context: TransformationContext) { 31 const previousOnSubstituteNode = context.onSubstituteNode; 32 context.enableSubstitution(SyntaxKind.Identifier); 33 context.onSubstituteNode = (hint, node) => { 34 node = previousOnSubstituteNode(hint, node); 35 if (isIdentifier(node) && node.escapedText === "oldName") { 36 node = setTextRange(factory.createIdentifier("newName"), node); 37 } 38 return node; 39 }; 40 return (file: SourceFile) => file; 41 } 42 43 function replaceIdentifiersNamedOldNameWithNewName2(context: TransformationContext) { 44 const visitor: Visitor = (node) => { 45 if (isIdentifier(node) && node.text === "oldName") { 46 return factory.createIdentifier("newName"); 47 } 48 return visitEachChild(node, visitor, context); 49 }; 50 return (node: SourceFile) => visitNode(node, visitor); 51 } 52 53 function createTaggedTemplateLiteral(): Transformer<SourceFile> { 54 return sourceFile => factory.updateSourceFile(sourceFile, [ 55 factory.createExpressionStatement( 56 factory.createTaggedTemplateExpression( 57 factory.createIdentifier("$tpl"), 58 /*typeArguments*/ undefined, 59 factory.createNoSubstitutionTemplateLiteral("foo", "foo"))) 60 ]); 61 } 62 63 function transformSourceFile(sourceText: string, transformers: TransformerFactory<SourceFile>[]) { 64 const transformed = transform(createSourceFile("source.ts", sourceText, ScriptTarget.ES2015), transformers); 65 const printer = createPrinter({ newLine: NewLineKind.CarriageReturnLineFeed }, { 66 onEmitNode: transformed.emitNodeWithNotification, 67 substituteNode: transformed.substituteNode 68 }); 69 const result = printer.printBundle(factory.createBundle(transformed.transformed)); 70 transformed.dispose(); 71 return result; 72 } 73 74 function testBaseline(testName: string, test: () => string) { 75 it(testName, () => { 76 Harness.Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, test()); 77 }); 78 } 79 80 function testBaselineAndEvaluate(testName: string, test: () => string, onEvaluate: (exports: any) => void) { 81 describe(testName, () => { 82 let sourceText!: string; 83 before(() => { 84 sourceText = test(); 85 }); 86 after(() => { 87 sourceText = undefined!; 88 }); 89 it("compare baselines", () => { 90 Harness.Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, sourceText); 91 }); 92 it("evaluate", () => { 93 onEvaluate(evaluator.evaluateJavaScript(sourceText)); 94 }); 95 }); 96 } 97 98 testBaseline("substitution", () => { 99 return transformSourceFile(`var a = undefined;`, [replaceUndefinedWithVoid0]); 100 }); 101 102 testBaseline("types", () => { 103 return transformSourceFile(`let a: () => void`, [ 104 context => file => visitNode(file, function visitor(node: Node): VisitResult<Node> { 105 return visitEachChild(node, visitor, context); 106 }) 107 ]); 108 }); 109 110 testBaseline("transformDefiniteAssignmentAssertions", () => { 111 return transformSourceFile(`let a!: () => void`, [ 112 context => file => visitNode(file, function visitor(node: Node): VisitResult<Node> { 113 if (node.kind === SyntaxKind.VoidKeyword) { 114 return factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword); 115 } 116 return visitEachChild(node, visitor, context); 117 }) 118 ]); 119 }); 120 121 testBaseline("fromTranspileModule", () => { 122 return transpileModule(`var oldName = undefined;`, { 123 transformers: { 124 before: [replaceUndefinedWithVoid0], 125 after: [replaceIdentifiersNamedOldNameWithNewName] 126 }, 127 compilerOptions: { 128 newLine: NewLineKind.CarriageReturnLineFeed 129 } 130 }).outputText; 131 }); 132 133 testBaseline("transformTaggedTemplateLiteral", () => { 134 return transpileModule("", { 135 transformers: { 136 before: [createTaggedTemplateLiteral], 137 }, 138 compilerOptions: { 139 target: ScriptTarget.ES5, 140 newLine: NewLineKind.CarriageReturnLineFeed 141 } 142 }).outputText; 143 }); 144 145 testBaseline("issue27854", () => { 146 return transpileModule(`oldName<{ a: string; }>\` ... \`;`, { 147 transformers: { 148 before: [replaceIdentifiersNamedOldNameWithNewName2] 149 }, 150 compilerOptions: { 151 newLine: NewLineKind.CarriageReturnLineFeed, 152 target: ScriptTarget.Latest 153 } 154 }).outputText; 155 }); 156 157 testBaseline("issue44068", () => { 158 return transformSourceFile(` 159 const FirstVar = null; 160 const SecondVar = null; 161 `, [ 162 context => file => { 163 const firstVarName = (file.statements[0] as VariableStatement) 164 .declarationList.declarations[0].name as Identifier; 165 const secondVarName = (file.statements[0] as VariableStatement) 166 .declarationList.declarations[0].name as Identifier; 167 168 return context.factory.updateSourceFile(file, file.statements.concat([ 169 context.factory.createExpressionStatement( 170 context.factory.createArrayLiteralExpression([firstVarName, secondVarName]) 171 ), 172 ])); 173 } 174 ]); 175 }); 176 177 testBaseline("rewrittenNamespace", () => { 178 return transpileModule(`namespace Reflect { const x = 1; }`, { 179 transformers: { 180 before: [forceNamespaceRewrite], 181 }, 182 compilerOptions: { 183 newLine: NewLineKind.CarriageReturnLineFeed, 184 } 185 }).outputText; 186 }); 187 188 testBaseline("rewrittenNamespaceFollowingClass", () => { 189 return transpileModule(` 190 class C { foo = 10; static bar = 20 } 191 namespace C { export let x = 10; } 192 `, { 193 transformers: { 194 before: [forceNamespaceRewrite], 195 }, 196 compilerOptions: { 197 target: ScriptTarget.ESNext, 198 newLine: NewLineKind.CarriageReturnLineFeed, 199 useDefineForClassFields: false, 200 } 201 }).outputText; 202 }); 203 204 testBaseline("transformTypesInExportDefault", () => { 205 return transpileModule(` 206 export default (foo: string) => { return 1; } 207 `, { 208 transformers: { 209 before: [replaceNumberWith2], 210 }, 211 compilerOptions: { 212 target: ScriptTarget.ESNext, 213 newLine: NewLineKind.CarriageReturnLineFeed, 214 } 215 }).outputText; 216 }); 217 218 testBaseline("synthesizedClassAndNamespaceCombination", () => { 219 return transpileModule("", { 220 transformers: { 221 before: [replaceWithClassAndNamespace], 222 }, 223 compilerOptions: { 224 target: ScriptTarget.ESNext, 225 newLine: NewLineKind.CarriageReturnLineFeed, 226 } 227 }).outputText; 228 229 function replaceWithClassAndNamespace() { 230 return (sourceFile: SourceFile) => { 231 // TODO(rbuckton): Does this need to be parented? 232 const result = factory.updateSourceFile( 233 sourceFile, 234 factory.createNodeArray([ 235 factory.createClassDeclaration(/*modifiers*/ undefined, "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, /*members*/ undefined!), // TODO: GH#18217 236 factory.createModuleDeclaration(/*modifiers*/ undefined, factory.createIdentifier("Foo"), factory.createModuleBlock([factory.createEmptyStatement()])) 237 ]) 238 ); 239 return result; 240 }; 241 } 242 }); 243 244 function forceNamespaceRewrite(context: TransformationContext) { 245 return (sourceFile: SourceFile): SourceFile => { 246 return visitNode(sourceFile); 247 248 function visitNode<T extends Node>(node: T): T { 249 if (node.kind === SyntaxKind.ModuleBlock) { 250 const block = node as T & ModuleBlock; 251 const statements = factory.createNodeArray([...block.statements]); 252 return factory.updateModuleBlock(block, statements) as typeof block; 253 } 254 return visitEachChild(node, visitNode, context); 255 } 256 }; 257 } 258 259 testBaseline("transformAwayExportStar", () => { 260 return transpileModule("export * from './helper';", { 261 transformers: { 262 before: [expandExportStar], 263 }, 264 compilerOptions: { 265 target: ScriptTarget.ESNext, 266 newLine: NewLineKind.CarriageReturnLineFeed, 267 } 268 }).outputText; 269 270 function expandExportStar(context: TransformationContext) { 271 return (sourceFile: SourceFile): SourceFile => { 272 return visitNode(sourceFile); 273 274 function visitNode<T extends Node>(node: T): T { 275 if (node.kind === SyntaxKind.ExportDeclaration) { 276 const ed = node as Node as ExportDeclaration; 277 const exports = [{ name: "x" }]; 278 const exportSpecifiers = exports.map(e => factory.createExportSpecifier(/*isTypeOnly*/ false, e.name, e.name)); 279 const exportClause = factory.createNamedExports(exportSpecifiers); 280 const newEd = factory.updateExportDeclaration(ed, ed.modifiers, ed.isTypeOnly, exportClause, ed.moduleSpecifier, ed.assertClause); 281 282 return newEd as Node as T; 283 } 284 return visitEachChild(node, visitNode, context); 285 } 286 }; 287 } 288 }); 289 290 // https://github.com/Microsoft/TypeScript/issues/19618 291 testBaseline("transformAddImportStar", () => { 292 return transpileModule("", { 293 transformers: { 294 before: [transformAddImportStar], 295 }, 296 compilerOptions: { 297 target: ScriptTarget.ES5, 298 module: ModuleKind.System, 299 newLine: NewLineKind.CarriageReturnLineFeed, 300 } 301 }).outputText; 302 303 function transformAddImportStar(_context: TransformationContext) { 304 return (sourceFile: SourceFile): SourceFile => { 305 return visitNode(sourceFile); 306 }; 307 function visitNode(sf: SourceFile) { 308 // produce `import * as i0 from './comp'; 309 const importStar = factory.createImportDeclaration( 310 /*modifiers*/ undefined, 311 /*importClause*/ factory.createImportClause( 312 /*isTypeOnly*/ false, 313 /*name*/ undefined, 314 factory.createNamespaceImport(factory.createIdentifier("i0")) 315 ), 316 /*moduleSpecifier*/ factory.createStringLiteral("./comp1"), 317 /*assertClause*/ undefined); 318 return factory.updateSourceFile(sf, [importStar]); 319 } 320 } 321 }); 322 323 // https://github.com/Microsoft/TypeScript/issues/17384 324 testBaseline("transformAddDecoratedNode", () => { 325 return transpileModule("", { 326 transformers: { 327 before: [transformAddDecoratedNode], 328 }, 329 compilerOptions: { 330 target: ScriptTarget.ES5, 331 newLine: NewLineKind.CarriageReturnLineFeed, 332 } 333 }).outputText; 334 335 function transformAddDecoratedNode(_context: TransformationContext) { 336 return (sourceFile: SourceFile): SourceFile => { 337 return visitNode(sourceFile); 338 }; 339 function visitNode(sf: SourceFile) { 340 // produce `class Foo { @Bar baz() {} }`; 341 const classDecl = factory.createClassDeclaration(/*modifiers*/ undefined, "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ 342 factory.createMethodDeclaration([factory.createDecorator(factory.createIdentifier("Bar"))], /**/ undefined, "baz", /**/ undefined, /**/ undefined, [], /**/ undefined, factory.createBlock([])) 343 ]); 344 return factory.updateSourceFile(sf, [classDecl]); 345 } 346 } 347 }); 348 349 testBaseline("transformDeclarationFile", () => { 350 return baselineDeclarationTransform(`var oldName = undefined;`, { 351 transformers: { 352 afterDeclarations: [replaceIdentifiersNamedOldNameWithNewName] 353 }, 354 compilerOptions: { 355 newLine: NewLineKind.CarriageReturnLineFeed, 356 declaration: true 357 } 358 }); 359 }); 360 361 // https://github.com/microsoft/TypeScript/issues/33295 362 testBaseline("transformParameterProperty", () => { 363 return transpileModule("", { 364 transformers: { 365 before: [transformAddParameterProperty], 366 }, 367 compilerOptions: { 368 target: ScriptTarget.ES5, 369 newLine: NewLineKind.CarriageReturnLineFeed, 370 } 371 }).outputText; 372 373 function transformAddParameterProperty(_context: TransformationContext) { 374 return (sourceFile: SourceFile): SourceFile => { 375 return visitNode(sourceFile); 376 }; 377 function visitNode(sf: SourceFile) { 378 // produce `class Foo { constructor(@Dec private x) {} }`; 379 // The decorator is required to trigger ts.ts transformations. 380 const classDecl = factory.createClassDeclaration([], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ 381 factory.createConstructorDeclaration(/*modifiers*/ undefined, [ 382 factory.createParameterDeclaration([factory.createDecorator(factory.createIdentifier("Dec")), factory.createModifier(SyntaxKind.PrivateKeyword)], /*dotDotDotToken*/ undefined, "x")], factory.createBlock([])) 383 ]); 384 return factory.updateSourceFile(sf, [classDecl]); 385 } 386 } 387 }); 388 389 function baselineDeclarationTransform(text: string, opts: TranspileOptions) { 390 const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true, { documents: [new documents.TextDocument("/.src/index.ts", text)] }); 391 const host = new fakes.CompilerHost(fs, opts.compilerOptions); 392 const program = createProgram(["/.src/index.ts"], opts.compilerOptions!, host); 393 program.emit(program.getSourceFile("/.src/index.ts"), (p, s, bom) => host.writeFile(p, s, bom), /*cancellationToken*/ undefined, /*onlyDts*/ true, opts.transformers); 394 return fs.readFileSync("/.src/index.d.ts").toString(); 395 } 396 397 function addSyntheticComment(nodeFilter: (node: Node) => boolean) { 398 return (context: TransformationContext) => { 399 return (sourceFile: SourceFile): SourceFile => { 400 return visitNode(sourceFile, rootTransform, isSourceFile); 401 }; 402 function rootTransform<T extends Node>(node: T): VisitResult<T> { 403 if (nodeFilter(node)) { 404 setEmitFlags(node, EmitFlags.NoLeadingComments); 405 setSyntheticLeadingComments(node, [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); 406 } 407 return visitEachChild(node, rootTransform, context); 408 } 409 }; 410 } 411 412 // https://github.com/Microsoft/TypeScript/issues/24096 413 testBaseline("transformAddCommentToArrowReturnValue", () => { 414 return transpileModule(`const foo = () => 415 void 0 416`, { 417 transformers: { 418 before: [addSyntheticComment(isVoidExpression)], 419 }, 420 compilerOptions: { 421 target: ScriptTarget.ES5, 422 newLine: NewLineKind.CarriageReturnLineFeed, 423 } 424 }).outputText; 425 }); 426 427 // https://github.com/Microsoft/TypeScript/issues/17594 428 testBaseline("transformAddCommentToExportedVar", () => { 429 return transpileModule(`export const exportedDirectly = 1; 430const exportedSeparately = 2; 431export {exportedSeparately}; 432`, { 433 transformers: { 434 before: [addSyntheticComment(isVariableStatement)], 435 }, 436 compilerOptions: { 437 target: ScriptTarget.ES5, 438 newLine: NewLineKind.CarriageReturnLineFeed, 439 } 440 }).outputText; 441 }); 442 443 // https://github.com/Microsoft/TypeScript/issues/17594 444 testBaseline("transformAddCommentToImport", () => { 445 return transpileModule(` 446// Previous comment on import. 447import {Value} from 'somewhere'; 448import * as X from 'somewhere'; 449// Previous comment on export. 450export { /* specifier comment */ X, Y} from 'somewhere'; 451export * from 'somewhere'; 452export {Value}; 453`, { 454 transformers: { 455 before: [addSyntheticComment(n => isImportDeclaration(n) || isExportDeclaration(n) || isImportSpecifier(n) || isExportSpecifier(n))], 456 }, 457 compilerOptions: { 458 target: ScriptTarget.ES5, 459 newLine: NewLineKind.CarriageReturnLineFeed, 460 } 461 }).outputText; 462 }); 463 464 // https://github.com/Microsoft/TypeScript/issues/17594 465 testBaseline("transformAddCommentToProperties", () => { 466 return transpileModule(` 467// class comment. 468class Clazz { 469 // original comment 1. 470 static staticProp: number = 1; 471 // original comment 2. 472 instanceProp: number = 2; 473 // original comment 3. 474 constructor(readonly field = 1) {} 475} 476`, { 477 transformers: { 478 before: [addSyntheticComment(n => isPropertyDeclaration(n) || isParameterPropertyDeclaration(n, n.parent) || isClassDeclaration(n) || isConstructorDeclaration(n))], 479 }, 480 compilerOptions: { 481 target: ScriptTarget.ES2015, 482 newLine: NewLineKind.CarriageReturnLineFeed, 483 } 484 }).outputText; 485 }); 486 487 testBaseline("transformAddCommentToNamespace", () => { 488 return transpileModule(` 489// namespace comment. 490namespace Foo { 491 export const x = 1; 492} 493// another comment. 494namespace Foo { 495 export const y = 1; 496} 497`, { 498 transformers: { 499 before: [addSyntheticComment(n => isModuleDeclaration(n))], 500 }, 501 compilerOptions: { 502 target: ScriptTarget.ES2015, 503 newLine: NewLineKind.CarriageReturnLineFeed, 504 } 505 }).outputText; 506 }); 507 508 testBaseline("transformUpdateModuleMember", () => { 509 return transpileModule(` 510module MyModule { 511 const myVariable = 1; 512 function foo(param: string) {} 513} 514`, { 515 transformers: { 516 before: [renameVariable], 517 }, 518 compilerOptions: { 519 target: ScriptTarget.ES2015, 520 newLine: NewLineKind.CarriageReturnLineFeed, 521 } 522 }).outputText; 523 524 function renameVariable(context: TransformationContext) { 525 return (sourceFile: SourceFile): SourceFile => { 526 return visitNode(sourceFile, rootTransform, isSourceFile); 527 }; 528 function rootTransform<T extends Node>(node: T): Node { 529 if (isVariableDeclaration(node)) { 530 return factory.updateVariableDeclaration(node, factory.createIdentifier("newName"), /*exclamationToken*/ undefined, /*type*/ undefined, node.initializer); 531 } 532 return visitEachChild(node, rootTransform, context); 533 } 534 } 535 }); 536 537 // https://github.com/Microsoft/TypeScript/issues/24709 538 testBaseline("issue24709", () => { 539 const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true); 540 const transformed = transform(createSourceFile("source.ts", "class X { echo(x: string) { return x; } }", ScriptTarget.ES3), [transformSourceFile]); 541 const transformedSourceFile = transformed.transformed[0]; 542 transformed.dispose(); 543 const host = new fakes.CompilerHost(fs); 544 host.getSourceFile = () => transformedSourceFile; 545 const program = createProgram(["source.ts"], { 546 target: ScriptTarget.ES3, 547 module: ModuleKind.None, 548 noLib: true 549 }, host); 550 program.emit(transformedSourceFile, (_p, s, b) => host.writeFile("source.js", s, b)); 551 return host.readFile("source.js")!.toString(); 552 553 function transformSourceFile(context: TransformationContext) { 554 const visitor: Visitor = (node) => { 555 if (isMethodDeclaration(node)) { 556 return factory.updateMethodDeclaration( 557 node, 558 node.modifiers, 559 node.asteriskToken, 560 factory.createIdentifier("foobar"), 561 node.questionToken, 562 node.typeParameters, 563 node.parameters, 564 node.type, 565 node.body, 566 ); 567 } 568 return visitEachChild(node, visitor, context); 569 }; 570 return (node: SourceFile) => visitNode(node, visitor); 571 } 572 573 }); 574 575 testBaselineAndEvaluate("templateSpans", () => { 576 return transpileModule("const x = String.raw`\n\nhello`; exports.stringLength = x.trim().length;", { 577 compilerOptions: { 578 target: ScriptTarget.ESNext, 579 newLine: NewLineKind.CarriageReturnLineFeed, 580 }, 581 transformers: { 582 before: [transformSourceFile] 583 } 584 }).outputText; 585 586 function transformSourceFile(context: TransformationContext): Transformer<SourceFile> { 587 function visitor(node: Node): VisitResult<Node> { 588 if (isNoSubstitutionTemplateLiteral(node)) { 589 return factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText); 590 } 591 else { 592 return visitEachChild(node, visitor, context); 593 } 594 } 595 return sourceFile => visitNode(sourceFile, visitor, isSourceFile); 596 } 597 }, exports => { 598 assert.equal(exports.stringLength, 5); 599 }); 600 601 function addStaticFieldWithComment(context: TransformationContext) { 602 return (sourceFile: SourceFile): SourceFile => { 603 return visitNode(sourceFile, rootTransform, isSourceFile); 604 }; 605 function rootTransform<T extends Node>(node: T): Node { 606 if (isClassLike(node)) { 607 const newMembers = [factory.createPropertyDeclaration([factory.createModifier(SyntaxKind.StaticKeyword)], "newField", /* questionOrExclamationToken */ undefined, /* type */ undefined, factory.createStringLiteral("x"))]; 608 setSyntheticLeadingComments(newMembers[0], [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); 609 return isClassDeclaration(node) ? 610 factory.updateClassDeclaration( 611 node, 612 node.modifiers, 613 node.name, 614 node.typeParameters, 615 node.heritageClauses, 616 newMembers) : 617 isClassExpression(node) ? 618 factory.updateClassExpression( 619 node, 620 node.modifiers, 621 node.name, 622 node.typeParameters, 623 node.heritageClauses, 624 newMembers) : 625 factory.updateStructDeclaration( 626 node, 627 node.modifiers, 628 node.name, 629 node.typeParameters, 630 node.heritageClauses, 631 node.members); 632 } 633 return visitEachChild(node, rootTransform, context); 634 } 635 } 636 637 testBaseline("transformSyntheticCommentOnStaticFieldInClassDeclaration", () => { 638 return transpileModule(` 639declare const Decorator: any; 640@Decorator 641class MyClass { 642} 643`, { 644 transformers: { 645 before: [addStaticFieldWithComment], 646 }, 647 compilerOptions: { 648 target: ScriptTarget.ES2015, 649 newLine: NewLineKind.CarriageReturnLineFeed, 650 } 651 }).outputText; 652 }); 653 654 testBaseline("transformSyntheticCommentOnStaticFieldInClassExpression", () => { 655 return transpileModule(` 656const MyClass = class { 657}; 658`, { 659 transformers: { 660 before: [addStaticFieldWithComment], 661 }, 662 compilerOptions: { 663 target: ScriptTarget.ES2015, 664 newLine: NewLineKind.CarriageReturnLineFeed, 665 } 666 }).outputText; 667 }); 668 669 }); 670} 671 672