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("rewrittenNamespace", () => { 158 return transpileModule(`namespace Reflect { const x = 1; }`, { 159 transformers: { 160 before: [forceNamespaceRewrite], 161 }, 162 compilerOptions: { 163 newLine: NewLineKind.CarriageReturnLineFeed, 164 } 165 }).outputText; 166 }); 167 168 testBaseline("rewrittenNamespaceFollowingClass", () => { 169 return transpileModule(` 170 class C { foo = 10; static bar = 20 } 171 namespace C { export let x = 10; } 172 `, { 173 transformers: { 174 before: [forceNamespaceRewrite], 175 }, 176 compilerOptions: { 177 target: ScriptTarget.ESNext, 178 newLine: NewLineKind.CarriageReturnLineFeed, 179 } 180 }).outputText; 181 }); 182 183 testBaseline("transformTypesInExportDefault", () => { 184 return transpileModule(` 185 export default (foo: string) => { return 1; } 186 `, { 187 transformers: { 188 before: [replaceNumberWith2], 189 }, 190 compilerOptions: { 191 target: ScriptTarget.ESNext, 192 newLine: NewLineKind.CarriageReturnLineFeed, 193 } 194 }).outputText; 195 }); 196 197 testBaseline("synthesizedClassAndNamespaceCombination", () => { 198 return transpileModule("", { 199 transformers: { 200 before: [replaceWithClassAndNamespace], 201 }, 202 compilerOptions: { 203 target: ScriptTarget.ESNext, 204 newLine: NewLineKind.CarriageReturnLineFeed, 205 } 206 }).outputText; 207 208 function replaceWithClassAndNamespace() { 209 return (sourceFile: SourceFile) => { 210 // TODO(rbuckton): Does this need to be parented? 211 const result = factory.updateSourceFile( 212 sourceFile, 213 factory.createNodeArray([ 214 factory.createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, /*members*/ undefined!), // TODO: GH#18217 215 factory.createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, factory.createIdentifier("Foo"), factory.createModuleBlock([factory.createEmptyStatement()])) 216 ]) 217 ); 218 return result; 219 }; 220 } 221 }); 222 223 function forceNamespaceRewrite(context: TransformationContext) { 224 return (sourceFile: SourceFile): SourceFile => { 225 return visitNode(sourceFile); 226 227 function visitNode<T extends Node>(node: T): T { 228 if (node.kind === SyntaxKind.ModuleBlock) { 229 const block = node as T & ModuleBlock; 230 const statements = factory.createNodeArray([...block.statements]); 231 return factory.updateModuleBlock(block, statements) as typeof block; 232 } 233 return visitEachChild(node, visitNode, context); 234 } 235 }; 236 } 237 238 testBaseline("transformAwayExportStar", () => { 239 return transpileModule("export * from './helper';", { 240 transformers: { 241 before: [expandExportStar], 242 }, 243 compilerOptions: { 244 target: ScriptTarget.ESNext, 245 newLine: NewLineKind.CarriageReturnLineFeed, 246 } 247 }).outputText; 248 249 function expandExportStar(context: TransformationContext) { 250 return (sourceFile: SourceFile): SourceFile => { 251 return visitNode(sourceFile); 252 253 function visitNode<T extends Node>(node: T): T { 254 if (node.kind === SyntaxKind.ExportDeclaration) { 255 const ed = node as Node as ExportDeclaration; 256 const exports = [{ name: "x" }]; 257 const exportSpecifiers = exports.map(e => factory.createExportSpecifier(e.name, e.name)); 258 const exportClause = factory.createNamedExports(exportSpecifiers); 259 const newEd = factory.updateExportDeclaration(ed, ed.decorators, ed.modifiers, ed.isTypeOnly, exportClause, ed.moduleSpecifier); 260 261 return newEd as Node as T; 262 } 263 return visitEachChild(node, visitNode, context); 264 } 265 }; 266 } 267 }); 268 269 // https://github.com/Microsoft/TypeScript/issues/19618 270 testBaseline("transformAddImportStar", () => { 271 return transpileModule("", { 272 transformers: { 273 before: [transformAddImportStar], 274 }, 275 compilerOptions: { 276 target: ScriptTarget.ES5, 277 module: ModuleKind.System, 278 newLine: NewLineKind.CarriageReturnLineFeed, 279 } 280 }).outputText; 281 282 function transformAddImportStar(_context: TransformationContext) { 283 return (sourceFile: SourceFile): SourceFile => { 284 return visitNode(sourceFile); 285 }; 286 function visitNode(sf: SourceFile) { 287 // produce `import * as i0 from './comp'; 288 const importStar = factory.createImportDeclaration( 289 /*decorators*/ undefined, 290 /*modifiers*/ undefined, 291 /*importClause*/ factory.createImportClause( 292 /*isTypeOnly*/ false, 293 /*name*/ undefined, 294 factory.createNamespaceImport(factory.createIdentifier("i0")) 295 ), 296 /*moduleSpecifier*/ factory.createStringLiteral("./comp1")); 297 return factory.updateSourceFile(sf, [importStar]); 298 } 299 } 300 }); 301 302 // https://github.com/Microsoft/TypeScript/issues/17384 303 testBaseline("transformAddDecoratedNode", () => { 304 return transpileModule("", { 305 transformers: { 306 before: [transformAddDecoratedNode], 307 }, 308 compilerOptions: { 309 target: ScriptTarget.ES5, 310 newLine: NewLineKind.CarriageReturnLineFeed, 311 } 312 }).outputText; 313 314 function transformAddDecoratedNode(_context: TransformationContext) { 315 return (sourceFile: SourceFile): SourceFile => { 316 return visitNode(sourceFile); 317 }; 318 function visitNode(sf: SourceFile) { 319 // produce `class Foo { @Bar baz() {} }`; 320 const classDecl = factory.createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ 321 factory.createMethodDeclaration([factory.createDecorator(factory.createIdentifier("Bar"))], [], /**/ undefined, "baz", /**/ undefined, /**/ undefined, [], /**/ undefined, factory.createBlock([])) 322 ]); 323 return factory.updateSourceFile(sf, [classDecl]); 324 } 325 } 326 }); 327 328 testBaseline("transformDeclarationFile", () => { 329 return baselineDeclarationTransform(`var oldName = undefined;`, { 330 transformers: { 331 afterDeclarations: [replaceIdentifiersNamedOldNameWithNewName] 332 }, 333 compilerOptions: { 334 newLine: NewLineKind.CarriageReturnLineFeed, 335 declaration: true 336 } 337 }); 338 }); 339 340 // https://github.com/microsoft/TypeScript/issues/33295 341 testBaseline("transformParameterProperty", () => { 342 return transpileModule("", { 343 transformers: { 344 before: [transformAddParameterProperty], 345 }, 346 compilerOptions: { 347 target: ScriptTarget.ES5, 348 newLine: NewLineKind.CarriageReturnLineFeed, 349 } 350 }).outputText; 351 352 function transformAddParameterProperty(_context: TransformationContext) { 353 return (sourceFile: SourceFile): SourceFile => { 354 return visitNode(sourceFile); 355 }; 356 function visitNode(sf: SourceFile) { 357 // produce `class Foo { constructor(@Dec private x) {} }`; 358 // The decorator is required to trigger ts.ts transformations. 359 const classDecl = factory.createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ 360 factory.createConstructorDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, [ 361 factory.createParameterDeclaration(/*decorators*/ [factory.createDecorator(factory.createIdentifier("Dec"))], /*modifiers*/ [factory.createModifier(SyntaxKind.PrivateKeyword)], /*dotDotDotToken*/ undefined, "x")], factory.createBlock([])) 362 ]); 363 return factory.updateSourceFile(sf, [classDecl]); 364 } 365 } 366 }); 367 368 function baselineDeclarationTransform(text: string, opts: TranspileOptions) { 369 const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true, { documents: [new documents.TextDocument("/.src/index.ts", text)] }); 370 const host = new fakes.CompilerHost(fs, opts.compilerOptions); 371 const program = createProgram(["/.src/index.ts"], opts.compilerOptions!, host); 372 program.emit(program.getSourceFile("/.src/index.ts"), (p, s, bom) => host.writeFile(p, s, bom), /*cancellationToken*/ undefined, /*onlyDts*/ true, opts.transformers); 373 return fs.readFileSync("/.src/index.d.ts").toString(); 374 } 375 376 function addSyntheticComment(nodeFilter: (node: Node) => boolean) { 377 return (context: TransformationContext) => { 378 return (sourceFile: SourceFile): SourceFile => { 379 return visitNode(sourceFile, rootTransform, isSourceFile); 380 }; 381 function rootTransform<T extends Node>(node: T): VisitResult<T> { 382 if (nodeFilter(node)) { 383 setEmitFlags(node, EmitFlags.NoLeadingComments); 384 setSyntheticLeadingComments(node, [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); 385 } 386 return visitEachChild(node, rootTransform, context); 387 } 388 }; 389 } 390 391 // https://github.com/Microsoft/TypeScript/issues/24096 392 testBaseline("transformAddCommentToArrowReturnValue", () => { 393 return transpileModule(`const foo = () => 394 void 0 395`, { 396 transformers: { 397 before: [addSyntheticComment(isVoidExpression)], 398 }, 399 compilerOptions: { 400 target: ScriptTarget.ES5, 401 newLine: NewLineKind.CarriageReturnLineFeed, 402 } 403 }).outputText; 404 }); 405 406 // https://github.com/Microsoft/TypeScript/issues/17594 407 testBaseline("transformAddCommentToExportedVar", () => { 408 return transpileModule(`export const exportedDirectly = 1; 409const exportedSeparately = 2; 410export {exportedSeparately}; 411`, { 412 transformers: { 413 before: [addSyntheticComment(isVariableStatement)], 414 }, 415 compilerOptions: { 416 target: ScriptTarget.ES5, 417 newLine: NewLineKind.CarriageReturnLineFeed, 418 } 419 }).outputText; 420 }); 421 422 // https://github.com/Microsoft/TypeScript/issues/17594 423 testBaseline("transformAddCommentToImport", () => { 424 return transpileModule(` 425// Previous comment on import. 426import {Value} from 'somewhere'; 427import * as X from 'somewhere'; 428// Previous comment on export. 429export { /* specifier comment */ X, Y} from 'somewhere'; 430export * from 'somewhere'; 431export {Value}; 432`, { 433 transformers: { 434 before: [addSyntheticComment(n => isImportDeclaration(n) || isExportDeclaration(n) || isImportSpecifier(n) || isExportSpecifier(n))], 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("transformAddCommentToProperties", () => { 445 return transpileModule(` 446// class comment. 447class Clazz { 448 // original comment 1. 449 static staticProp: number = 1; 450 // original comment 2. 451 instanceProp: number = 2; 452 // original comment 3. 453 constructor(readonly field = 1) {} 454} 455`, { 456 transformers: { 457 before: [addSyntheticComment(n => isPropertyDeclaration(n) || isParameterPropertyDeclaration(n, n.parent) || isClassDeclaration(n) || isConstructorDeclaration(n))], 458 }, 459 compilerOptions: { 460 target: ScriptTarget.ES2015, 461 newLine: NewLineKind.CarriageReturnLineFeed, 462 } 463 }).outputText; 464 }); 465 466 testBaseline("transformAddCommentToNamespace", () => { 467 return transpileModule(` 468// namespace comment. 469namespace Foo { 470 export const x = 1; 471} 472// another comment. 473namespace Foo { 474 export const y = 1; 475} 476`, { 477 transformers: { 478 before: [addSyntheticComment(n => isModuleDeclaration(n))], 479 }, 480 compilerOptions: { 481 target: ScriptTarget.ES2015, 482 newLine: NewLineKind.CarriageReturnLineFeed, 483 } 484 }).outputText; 485 }); 486 487 testBaseline("transformUpdateModuleMember", () => { 488 return transpileModule(` 489module MyModule { 490 const myVariable = 1; 491 function foo(param: string) {} 492} 493`, { 494 transformers: { 495 before: [renameVariable], 496 }, 497 compilerOptions: { 498 target: ScriptTarget.ES2015, 499 newLine: NewLineKind.CarriageReturnLineFeed, 500 } 501 }).outputText; 502 503 function renameVariable(context: TransformationContext) { 504 return (sourceFile: SourceFile): SourceFile => { 505 return visitNode(sourceFile, rootTransform, isSourceFile); 506 }; 507 function rootTransform<T extends Node>(node: T): Node { 508 if (isVariableDeclaration(node)) { 509 return factory.updateVariableDeclaration(node, factory.createIdentifier("newName"), /*exclamationToken*/ undefined, /*type*/ undefined, node.initializer); 510 } 511 return visitEachChild(node, rootTransform, context); 512 } 513 } 514 }); 515 516 // https://github.com/Microsoft/TypeScript/issues/24709 517 testBaseline("issue24709", () => { 518 const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true); 519 const transformed = transform(createSourceFile("source.ts", "class X { echo(x: string) { return x; } }", ScriptTarget.ES3), [transformSourceFile]); 520 const transformedSourceFile = transformed.transformed[0]; 521 transformed.dispose(); 522 const host = new fakes.CompilerHost(fs); 523 host.getSourceFile = () => transformedSourceFile; 524 const program = createProgram(["source.ts"], { 525 target: ScriptTarget.ES3, 526 module: ModuleKind.None, 527 noLib: true 528 }, host); 529 program.emit(transformedSourceFile, (_p, s, b) => host.writeFile("source.js", s, b)); 530 return host.readFile("source.js")!.toString(); 531 532 function transformSourceFile(context: TransformationContext) { 533 const visitor: Visitor = (node) => { 534 if (isMethodDeclaration(node)) { 535 return factory.updateMethodDeclaration( 536 node, 537 node.decorators, 538 node.modifiers, 539 node.asteriskToken, 540 factory.createIdentifier("foobar"), 541 node.questionToken, 542 node.typeParameters, 543 node.parameters, 544 node.type, 545 node.body, 546 ); 547 } 548 return visitEachChild(node, visitor, context); 549 }; 550 return (node: SourceFile) => visitNode(node, visitor); 551 } 552 553 }); 554 555 testBaselineAndEvaluate("templateSpans", () => { 556 return transpileModule("const x = String.raw`\n\nhello`; exports.stringLength = x.trim().length;", { 557 compilerOptions: { 558 target: ScriptTarget.ESNext, 559 newLine: NewLineKind.CarriageReturnLineFeed, 560 }, 561 transformers: { 562 before: [transformSourceFile] 563 } 564 }).outputText; 565 566 function transformSourceFile(context: TransformationContext): Transformer<SourceFile> { 567 function visitor(node: Node): VisitResult<Node> { 568 if (isNoSubstitutionTemplateLiteral(node)) { 569 return factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText); 570 } 571 else { 572 return visitEachChild(node, visitor, context); 573 } 574 } 575 return sourceFile => visitNode(sourceFile, visitor, isSourceFile); 576 } 577 }, exports => { 578 assert.equal(exports.stringLength, 5); 579 }); 580 }); 581} 582 583