• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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