• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts {
2    function withChange(text: IScriptSnapshot, start: number, length: number, newText: string): { text: IScriptSnapshot; textChangeRange: TextChangeRange; } {
3        const contents = getSnapshotText(text);
4        const newContents = contents.substr(0, start) + newText + contents.substring(start + length);
5
6        return { text: ScriptSnapshot.fromString(newContents), textChangeRange: createTextChangeRange(createTextSpan(start, length), newText.length) };
7    }
8
9    function withInsert(text: IScriptSnapshot, start: number, newText: string): { text: IScriptSnapshot; textChangeRange: TextChangeRange; } {
10        return withChange(text, start, 0, newText);
11    }
12
13    function withDelete(text: IScriptSnapshot, start: number, length: number): { text: IScriptSnapshot; textChangeRange: TextChangeRange; } {
14        return withChange(text, start, length, "");
15    }
16
17    function createTree(text: IScriptSnapshot, version: string) {
18        return createLanguageServiceSourceFile(/*fileName:*/ "", text, ScriptTarget.Latest, version, /*setNodeParents:*/ true);
19    }
20
21    function assertSameDiagnostics(file1: SourceFile, file2: SourceFile) {
22        const diagnostics1 = file1.parseDiagnostics;
23        const diagnostics2 = file2.parseDiagnostics;
24
25        assert.equal(diagnostics1.length, diagnostics2.length, "diagnostics1.length !== diagnostics2.length");
26        for (let i = 0; i < diagnostics1.length; i++) {
27            const d1 = diagnostics1[i];
28            const d2 = diagnostics2[i];
29
30            assert.equal(d1.file, file1, "d1.file !== file1");
31            assert.equal(d2.file, file2, "d2.file !== file2");
32            assert.equal(d1.start, d2.start, "d1.start !== d2.start");
33            assert.equal(d1.length, d2.length, "d1.length !== d2.length");
34            assert.equal(d1.messageText, d2.messageText, "d1.messageText !== d2.messageText");
35            assert.equal(d1.category, d2.category, "d1.category !== d2.category");
36            assert.equal(d1.code, d2.code, "d1.code !== d2.code");
37        }
38    }
39
40    // NOTE: 'reusedElements' is the expected count of elements reused from the old tree to the new
41    // tree.  It may change as we tweak the parser.  If the count increases then that should always
42    // be a good thing.  If it decreases, that's not great (less reusability), but that may be
43    // unavoidable.  If it does decrease an investigation should be done to make sure that things
44    // are still ok and we're still appropriately reusing most of the tree.
45    function compareTrees(oldText: IScriptSnapshot, newText: IScriptSnapshot, textChangeRange: TextChangeRange, expectedReusedElements: number, oldTree?: SourceFile) {
46        oldTree = oldTree || createTree(oldText, /*version:*/ ".");
47        Utils.assertInvariants(oldTree, /*parent:*/ undefined);
48
49        // Create a tree for the new text, in a non-incremental fashion.
50        const newTree = createTree(newText, oldTree.version + ".");
51        Utils.assertInvariants(newTree, /*parent:*/ undefined);
52
53        // Create a tree for the new text, in an incremental fashion.
54        const incrementalNewTree = updateLanguageServiceSourceFile(oldTree, newText, oldTree.version + ".", textChangeRange);
55        Utils.assertInvariants(incrementalNewTree, /*parent:*/ undefined);
56
57        // We should get the same tree when doign a full or incremental parse.
58        Utils.assertStructuralEquals(newTree, incrementalNewTree);
59
60        // We should also get the exact same set of diagnostics.
61        assertSameDiagnostics(newTree, incrementalNewTree);
62
63        // There should be no reused nodes between two trees that are fully parsed.
64        assert.isTrue(reusedElements(oldTree, newTree) === 0);
65
66        assert.equal(newTree.fileName, incrementalNewTree.fileName, "newTree.fileName !== incrementalNewTree.fileName");
67        assert.equal(newTree.text, incrementalNewTree.text, "newTree.text !== incrementalNewTree.text");
68
69        if (expectedReusedElements !== -1) {
70            const actualReusedCount = reusedElements(oldTree, incrementalNewTree);
71            assert.equal(actualReusedCount, expectedReusedElements, actualReusedCount + " !== " + expectedReusedElements);
72        }
73
74        return { oldTree, newTree, incrementalNewTree };
75    }
76
77    function reusedElements(oldNode: SourceFile, newNode: SourceFile): number {
78        const allOldElements = collectElements(oldNode);
79        const allNewElements = collectElements(newNode);
80
81        return filter(allOldElements, v => contains(allNewElements, v)).length;
82    }
83
84    function collectElements(node: Node) {
85        const result: Node[] = [];
86        visit(node);
87        return result;
88
89        function visit(node: Node) {
90            result.push(node);
91            forEachChild(node, visit);
92        }
93    }
94
95    function deleteCode(source: string, index: number, toDelete: string) {
96        const repeat = toDelete.length;
97        let oldTree = createTree(ScriptSnapshot.fromString(source), /*version:*/ ".");
98        for (let i = 0; i < repeat; i++) {
99            const oldText = ScriptSnapshot.fromString(source);
100            const newTextAndChange = withDelete(oldText, index, 1);
101            const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree;
102
103            source = getSnapshotText(newTextAndChange.text);
104            oldTree = newTree;
105        }
106    }
107
108    function insertCode(source: string, index: number, toInsert: string) {
109        const repeat = toInsert.length;
110        let oldTree = createTree(ScriptSnapshot.fromString(source), /*version:*/ ".");
111        for (let i = 0; i < repeat; i++) {
112            const oldText = ScriptSnapshot.fromString(source);
113            const newTextAndChange = withInsert(oldText, index + i, toInsert.charAt(i));
114            const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree;
115
116            source = getSnapshotText(newTextAndChange.text);
117            oldTree = newTree;
118        }
119    }
120
121    describe("unittests:: Incremental Parser", () => {
122        it("Inserting into method", () => {
123            const source = "class C {\r\n" +
124                "    public foo1() { }\r\n" +
125                "    public foo2() {\r\n" +
126                "        return 1;\r\n" +
127                "    }\r\n" +
128                "    public foo3() { }\r\n" +
129                "}";
130
131            const oldText = ScriptSnapshot.fromString(source);
132            const semicolonIndex = source.indexOf(";");
133            const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1");
134
135            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8);
136        });
137
138        it("Deleting from method", () => {
139            const source = "class C {\r\n" +
140                "    public foo1() { }\r\n" +
141                "    public foo2() {\r\n" +
142                "        return 1 + 1;\r\n" +
143                "    }\r\n" +
144                "    public foo3() { }\r\n" +
145                "}";
146
147            const index = source.indexOf("+ 1");
148            const oldText = ScriptSnapshot.fromString(source);
149            const newTextAndChange = withDelete(oldText, index, "+ 1".length);
150
151            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8);
152        });
153
154        it("Regular expression 1", () => {
155            const source = "class C { public foo1() { /; } public foo2() { return 1;} public foo3() { } }";
156
157            const semicolonIndex = source.indexOf(";}");
158            const oldText = ScriptSnapshot.fromString(source);
159            const newTextAndChange = withInsert(oldText, semicolonIndex, "/");
160
161            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
162        });
163
164        it("Regular expression 2", () => {
165            const source = "class C { public foo1() { ; } public foo2() { return 1/;} public foo3() { } }";
166
167            const semicolonIndex = source.indexOf(";");
168            const oldText = ScriptSnapshot.fromString(source);
169            const newTextAndChange = withInsert(oldText, semicolonIndex, "/");
170
171            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4);
172        });
173
174        it("Comment 1", () => {
175            const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }";
176
177            const semicolonIndex = source.indexOf(";");
178            const oldText = ScriptSnapshot.fromString(source);
179            const newTextAndChange = withInsert(oldText, semicolonIndex, "/");
180
181            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
182        });
183
184        it("Comment 2", () => {
185            const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }";
186
187            const oldText = ScriptSnapshot.fromString(source);
188            const newTextAndChange = withInsert(oldText, 0, "//");
189
190            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
191        });
192
193        it("Comment 3", () => {
194            const source = "//class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }";
195
196            const oldText = ScriptSnapshot.fromString(source);
197            const newTextAndChange = withDelete(oldText, 0, 2);
198
199            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
200        });
201
202        it("Comment 4", () => {
203            const source = "class C { public foo1() { /; } public foo2() { */ return 1; } public foo3() { } }";
204
205            const index = source.indexOf(";");
206            const oldText = ScriptSnapshot.fromString(source);
207            const newTextAndChange = withInsert(oldText, index, "*");
208
209            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4);
210        });
211
212        it("Parameter 1", () => {
213            // Should be able to reuse all the parameters.
214            const source = "class C {\r\n" +
215                "    public foo2(a, b, c, d) {\r\n" +
216                "        return 1;\r\n" +
217                "    }\r\n" +
218                "}";
219
220            const semicolonIndex = source.indexOf(";");
221            const oldText = ScriptSnapshot.fromString(source);
222            const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1");
223
224            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8);
225        });
226
227        it("Type member 1", () => {
228            // Should be able to reuse most of the type members.
229            const source = "interface I { a: number; b: string; (c): d; new (e): f; g(): h }";
230
231            const index = source.indexOf(": string");
232            const oldText = ScriptSnapshot.fromString(source);
233            const newTextAndChange = withInsert(oldText, index, "?");
234
235            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14);
236        });
237
238        it("Enum element 1", () => {
239            // Should be able to reuse most of the enum elements.
240            const source = "enum E { a = 1, b = 1 << 1, c = 3, e = 4, f = 5, g = 7, h = 8, i = 9, j = 10 }";
241
242            const index = source.indexOf("<<");
243            const oldText = ScriptSnapshot.fromString(source);
244            const newTextAndChange = withChange(oldText, index, 2, "+");
245
246            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24);
247        });
248
249        it("Strict mode 1", () => {
250            const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();";
251
252            const oldText = ScriptSnapshot.fromString(source);
253            const newTextAndChange = withInsert(oldText, 0, "'strict';\r\n");
254
255            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9);
256        });
257
258        it("Strict mode 2", () => {
259            const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();";
260
261            const oldText = ScriptSnapshot.fromString(source);
262            const newTextAndChange = withInsert(oldText, 0, "'use strict';\r\n");
263
264            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9);
265        });
266
267        it("Strict mode 3", () => {
268            const source = "'strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();";
269
270            const index = source.indexOf("f");
271            const oldText = ScriptSnapshot.fromString(source);
272            const newTextAndChange = withDelete(oldText, 0, index);
273
274            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9);
275        });
276
277        it("Strict mode 4", () => {
278            const source = "'use strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();";
279
280            const index = source.indexOf("f");
281            const oldText = ScriptSnapshot.fromString(source);
282            const newTextAndChange = withDelete(oldText, 0, index);
283
284            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9);
285        });
286
287        it("Strict mode 5", () => {
288            const source = "'use blahhh';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n";
289
290            const index = source.indexOf("b");
291            const oldText = ScriptSnapshot.fromString(source);
292            const newTextAndChange = withChange(oldText, index, 6, "strict");
293
294            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27);
295        });
296
297        it("Strict mode 6", () => {
298            const source = "'use strict';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n";
299
300            const index = source.indexOf("s");
301            const oldText = ScriptSnapshot.fromString(source);
302            const newTextAndChange = withChange(oldText, index, 6, "blahhh");
303
304            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27);
305        });
306
307        it("Strict mode 7", () => {
308            const source = "'use blahhh';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n";
309
310            const index = source.indexOf("f");
311            const oldText = ScriptSnapshot.fromString(source);
312            const newTextAndChange = withDelete(oldText, 0, index);
313
314            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24);
315        });
316
317        it("Parenthesized expression to arrow function 1", () => {
318            const source = "var v = (a, b, c, d, e)";
319
320            const index = source.indexOf("a");
321            const oldText = ScriptSnapshot.fromString(source);
322            const newTextAndChange = withInsert(oldText, index + 1, ":");
323
324            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
325        });
326
327        it("Parenthesized expression to arrow function 2", () => {
328            const source = "var v = (a, b) = c";
329
330            const index = source.indexOf("= c") + 1;
331            const oldText = ScriptSnapshot.fromString(source);
332            const newTextAndChange = withInsert(oldText, index, ">");
333
334            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
335        });
336
337        it("Arrow function to parenthesized expression 1", () => {
338            const source = "var v = (a:, b, c, d, e)";
339
340            const index = source.indexOf(":");
341            const oldText = ScriptSnapshot.fromString(source);
342            const newTextAndChange = withDelete(oldText, index, 1);
343
344            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
345        });
346
347        it("Arrow function to parenthesized expression 2", () => {
348            const source = "var v = (a, b) => c";
349
350            const index = source.indexOf(">");
351            const oldText = ScriptSnapshot.fromString(source);
352            const newTextAndChange = withDelete(oldText, index, 1);
353
354            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
355        });
356
357        it("Speculative generic lookahead 1", () => {
358            const source = "var v = F<b>e";
359
360            const index = source.indexOf("b");
361            const oldText = ScriptSnapshot.fromString(source);
362            const newTextAndChange = withInsert(oldText, index + 1, ",x");
363
364            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1);
365        });
366
367        it("Speculative generic lookahead 2", () => {
368            const source = "var v = F<a,b>e";
369
370            const index = source.indexOf("b");
371            const oldText = ScriptSnapshot.fromString(source);
372            const newTextAndChange = withInsert(oldText, index + 1, ",x");
373
374            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1);
375        });
376
377        it("Speculative generic lookahead 3", () => {
378            const source = "var v = F<a,b,c>e";
379
380            const index = source.indexOf("b");
381            const oldText = ScriptSnapshot.fromString(source);
382            const newTextAndChange = withInsert(oldText, index + 1, ",x");
383
384            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1);
385        });
386
387        it("Speculative generic lookahead 4", () => {
388            const source = "var v = F<a,b,c,d>e";
389
390            const index = source.indexOf("b");
391            const oldText = ScriptSnapshot.fromString(source);
392            const newTextAndChange = withInsert(oldText, index + 1, ",x");
393
394            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1);
395        });
396
397        it("Assertion to arrow function", () => {
398            const source = "var v = <T>(a);";
399
400            const index = source.indexOf(";");
401            const oldText = ScriptSnapshot.fromString(source);
402            const newTextAndChange = withInsert(oldText, index, " => 1");
403
404            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
405        });
406
407        it("Arrow function to assertion", () => {
408            const source = "var v = <T>(a) => 1;";
409
410            const index = source.indexOf(" =>");
411            const oldText = ScriptSnapshot.fromString(source);
412            const newTextAndChange = withDelete(oldText, index, " => 1".length);
413
414            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
415        });
416
417        it("Contextual shift to shift-equals", () => {
418            const source = "var v = 1 >> = 2";
419
420            const index = source.indexOf(">> =");
421            const oldText = ScriptSnapshot.fromString(source);
422            const newTextAndChange = withDelete(oldText, index + 2, 1);
423
424            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
425        });
426
427        it("Contextual shift-equals to shift", () => {
428            const source = "var v = 1 >>= 2";
429
430            const index = source.indexOf(">>=");
431            const oldText = ScriptSnapshot.fromString(source);
432            const newTextAndChange = withInsert(oldText, index + 2, " ");
433
434            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
435        });
436
437        it("Contextual shift to generic invocation", () => {
438            const source = "var v = T>>(2)";
439
440            const index = source.indexOf("T");
441            const oldText = ScriptSnapshot.fromString(source);
442            const newTextAndChange = withInsert(oldText, index, "Foo<Bar<");
443
444            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
445        });
446
447        it("Test generic invocation to contextual shift", () => {
448            const source = "var v = Foo<Bar<T>>(2)";
449
450            const index = source.indexOf("Foo<Bar<");
451            const oldText = ScriptSnapshot.fromString(source);
452            const newTextAndChange = withDelete(oldText, index, "Foo<Bar<".length);
453
454            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
455        });
456
457        it("Contextual shift to generic type and initializer", () => {
458            const source = "var v = T>>=2;";
459
460            const index = source.indexOf("=");
461            const oldText = ScriptSnapshot.fromString(source);
462            const newTextAndChange = withChange(oldText, index, "= ".length, ": Foo<Bar<");
463
464            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
465        });
466
467        it("Generic type and initializer to contextual shift", () => {
468            const source = "var v : Foo<Bar<T>>=2;";
469
470            const index = source.indexOf(":");
471            const oldText = ScriptSnapshot.fromString(source);
472            const newTextAndChange = withChange(oldText, index, ": Foo<Bar<".length, "= ");
473
474            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
475        });
476
477        it("Arithmetic operator to type argument list", () => {
478            const source = "var v = new Dictionary<A, B>0";
479
480            const index = source.indexOf("0");
481            const oldText = ScriptSnapshot.fromString(source);
482            const newTextAndChange = withChange(oldText, index, 1, "()");
483
484            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
485        });
486
487        it("Type argument list to arithmetic operator", () => {
488            const source = "var v = new Dictionary<A, B>()";
489
490            const index = source.indexOf("()");
491            const oldText = ScriptSnapshot.fromString(source);
492            const newTextAndChange = withDelete(oldText, index, 2);
493
494            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
495        });
496
497        it("Yield context 1", () => {
498            // We're changing from a non-generator to a genarator.  We can't reuse statement nodes.
499            const source = "function foo() {\r\nyield(foo1);\r\n}";
500
501            const oldText = ScriptSnapshot.fromString(source);
502            const index = source.indexOf("foo");
503            const newTextAndChange = withInsert(oldText, index, "*");
504
505            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
506        });
507
508        it("Yield context 2", () => {
509            // We're changing from a generator to a non-genarator.  We can't reuse statement nodes.
510            const source = "function *foo() {\r\nyield(foo1);\r\n}";
511
512            const oldText = ScriptSnapshot.fromString(source);
513            const index = source.indexOf("*");
514            const newTextAndChange = withDelete(oldText, index, "*".length);
515
516            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
517        });
518
519        it("Delete semicolon", () => {
520            const source = "export class Foo {\r\n}\r\n\r\nexport var foo = new Foo();\r\n\r\n    export function test(foo: Foo) {\r\n        return true;\r\n    }\r\n";
521
522            const oldText = ScriptSnapshot.fromString(source);
523            const index = source.lastIndexOf(";");
524            const newTextAndChange = withDelete(oldText, index, 1);
525
526            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14);
527        });
528
529        it("Edit after empty type parameter list", () => {
530            const source = "class Dictionary<> { }\r\nvar y;\r\n";
531
532            const oldText = ScriptSnapshot.fromString(source);
533            const index = source.length;
534            const newTextAndChange = withInsert(oldText, index, "var x;");
535
536            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 2);
537        });
538
539        it("Delete parameter after comment", () => {
540            const source = "function fn(/* comment! */ a: number, c) { }";
541
542            const oldText = ScriptSnapshot.fromString(source);
543            const index = source.indexOf("a:");
544            const newTextAndChange = withDelete(oldText, index, "a: number,".length);
545
546            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
547        });
548
549        it("Modifier added to accessor", () => {
550            const source =
551                "class C {\
552    set Bar(bar:string) {}\
553}\
554var o2 = { set Foo(val:number) { } };";
555
556            const oldText = ScriptSnapshot.fromString(source);
557            const index = source.indexOf("set");
558            const newTextAndChange = withInsert(oldText, index, "public ");
559
560            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14);
561        });
562
563        it("Insert parameter ahead of parameter", () => {
564            const source =
565                "alert(100);\
566\
567class OverloadedMonster {\
568constructor();\
569constructor(name) { }\
570}";
571
572            const oldText = ScriptSnapshot.fromString(source);
573            const index = source.indexOf("100");
574            const newTextAndChange = withInsert(oldText, index, "'1', ");
575
576            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7);
577        });
578
579        it("Insert declare modifier before module", () => {
580            const source =
581                "module mAmbient {\
582module m3 { }\
583}";
584
585            const oldText = ScriptSnapshot.fromString(source);
586            const index = 0;
587            const newTextAndChange = withInsert(oldText, index, "declare ");
588
589            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
590        });
591
592        it("Insert function above arrow function with comment", () => {
593            const source =
594                "\
595() =>\
596   // do something\
5970;";
598
599            const oldText = ScriptSnapshot.fromString(source);
600            const index = 0;
601            const newTextAndChange = withInsert(oldText, index, "function Foo() { }");
602
603            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
604        });
605
606        it("Finish incomplete regular expression", () => {
607            const source = "while (true) /3; return;";
608
609            const oldText = ScriptSnapshot.fromString(source);
610            const index = source.length - 1;
611            const newTextAndChange = withInsert(oldText, index, "/");
612
613            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
614        });
615
616        it("Regular expression to divide operation", () => {
617            const source = "return;\r\nwhile (true) /3/g;";
618
619            const oldText = ScriptSnapshot.fromString(source);
620            const index = source.indexOf("while");
621            const newTextAndChange = withDelete(oldText, index, "while ".length);
622
623            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
624        });
625
626        it("Divide operation to regular expression", () => {
627            const source = "return;\r\n(true) /3/g;";
628
629            const oldText = ScriptSnapshot.fromString(source);
630            const index = source.indexOf("(");
631            const newTextAndChange = withInsert(oldText, index, "while ");
632
633            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
634        });
635
636        it("Unterminated comment after keyword converted to identifier", () => {
637            // 'public' as a keyword should be incrementally unusable (because it has an
638            // unterminated comment).  When we convert it to an identifier, that shouldn't
639            // change anything, and we should still get the same errors.
640            const source = "return; a.public /*";
641
642            const oldText = ScriptSnapshot.fromString(source);
643            const newTextAndChange = withInsert(oldText, 0, "");
644
645            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7);
646        });
647
648        it("Class to interface", () => {
649            const source = "class A { public M1() { } public M2() { } public M3() { } p1 = 0; p2 = 0; p3 = 0 }";
650
651            const oldText = ScriptSnapshot.fromString(source);
652            const newTextAndChange = withChange(oldText, 0, "class".length, "interface");
653
654            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
655        });
656
657        it("Interface to class", () => {
658            const source = "interface A { M1?(); M2?(); M3?(); p1?; p2?; p3? }";
659
660            const oldText = ScriptSnapshot.fromString(source);
661            const newTextAndChange = withChange(oldText, 0, "interface".length, "class");
662
663            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
664        });
665
666        it("Surrounding function declarations with block", () => {
667            const source = "declare function F1() { } export function F2() { } declare export function F3() { }";
668
669            const oldText = ScriptSnapshot.fromString(source);
670            const newTextAndChange = withInsert(oldText, 0, "{");
671
672            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9);
673        });
674
675        it("Removing block around function declarations", () => {
676            const source = "{ declare function F1() { } export function F2() { } declare export function F3() { }";
677
678            const oldText = ScriptSnapshot.fromString(source);
679            const newTextAndChange = withDelete(oldText, 0, "{".length);
680
681            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9);
682        });
683
684        it("Moving methods from class to object literal", () => {
685            const source = "class C { public A() { } public B() { } public C() { } }";
686
687            const oldText = ScriptSnapshot.fromString(source);
688            const newTextAndChange = withChange(oldText, 0, "class C".length, "var v =");
689
690            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
691        });
692
693        it("Moving methods from object literal to class", () => {
694            const source = "var v = { public A() { } public B() { } public C() { } }";
695
696            const oldText = ScriptSnapshot.fromString(source);
697            const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C");
698
699            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4);
700        });
701
702        it("Moving methods from object literal to class in strict mode", () => {
703            const source = "\"use strict\"; var v = { public A() { } public B() { } public C() { } }";
704
705            const oldText = ScriptSnapshot.fromString(source);
706            const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C");
707
708            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4);
709        });
710
711        it("Do not move constructors from class to object-literal.", () => {
712            const source = "class C { public constructor() { } public constructor() { } public constructor() { } }";
713
714            const oldText = ScriptSnapshot.fromString(source);
715            const newTextAndChange = withChange(oldText, 0, "class C".length, "var v =");
716
717            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
718        });
719
720        it("Do not move methods called \"constructor\" from object literal to class", () => {
721            const source = "var v = { public constructor() { } public constructor() { } public constructor() { } }";
722
723            const oldText = ScriptSnapshot.fromString(source);
724            const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C");
725
726            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
727        });
728
729        it("Moving index signatures from class to interface", () => {
730            const source = "class C { public [a: number]: string; public [a: number]: string; public [a: number]: string }";
731
732            const oldText = ScriptSnapshot.fromString(source);
733            const newTextAndChange = withChange(oldText, 0, "class".length, "interface");
734
735            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18);
736        });
737
738        it("Moving index signatures from class to interface in strict mode", () => {
739            const source = "\"use strict\"; class C { public [a: number]: string; public [a: number]: string; public [a: number]: string }";
740
741            const oldText = ScriptSnapshot.fromString(source);
742            const newTextAndChange = withChange(oldText, 14, "class".length, "interface");
743
744            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18);
745        });
746
747        it("Moving index signatures from interface to class", () => {
748            const source = "interface C { public [a: number]: string; public [a: number]: string; public [a: number]: string }";
749
750            const oldText = ScriptSnapshot.fromString(source);
751            const newTextAndChange = withChange(oldText, 0, "interface".length, "class");
752
753            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18);
754        });
755
756
757        it("Moving index signatures from interface to class in strict mode", () => {
758            const source = "\"use strict\"; interface C { public [a: number]: string; public [a: number]: string; public [a: number]: string }";
759
760            const oldText = ScriptSnapshot.fromString(source);
761            const newTextAndChange = withChange(oldText, 14, "interface".length, "class");
762
763            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18);
764        });
765
766        it("Moving accessors from class to object literal", () => {
767            const source = "class C { public get A() { } public get B() { } public get C() { } }";
768
769            const oldText = ScriptSnapshot.fromString(source);
770            const newTextAndChange = withChange(oldText, 0, "class C".length, "var v =");
771
772            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
773        });
774
775        it("Moving accessors from object literal to class", () => {
776            const source = "var v = { public get A() { } public get B() { } public get C() { } }";
777
778            const oldText = ScriptSnapshot.fromString(source);
779            const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C");
780
781            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4);
782        });
783
784
785        it("Moving accessors from object literal to class in strict mode", () => {
786            const source = "\"use strict\"; var v = { public get A() { } public get B() { } public get C() { } }";
787
788            const oldText = ScriptSnapshot.fromString(source);
789            const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C");
790
791            compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4);
792        });
793
794        it("Reuse transformFlags of subtree during bind", () => {
795            const source = `class Greeter { constructor(element: HTMLElement) { } }`;
796            const oldText = ScriptSnapshot.fromString(source);
797            const newTextAndChange = withChange(oldText, 15, 0, "\n");
798            const { oldTree, incrementalNewTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1);
799            bindSourceFile(oldTree, {});
800            bindSourceFile(incrementalNewTree, {});
801            assert.equal(oldTree.transformFlags, incrementalNewTree.transformFlags);
802        });
803
804        // Simulated typing tests.
805
806        it("Type extends clause 1", () => {
807            const source = "interface IFoo<T> { }\r\ninterface Array<T> extends IFoo<T> { }";
808
809            const index = source.indexOf("extends");
810            deleteCode(source, index, "extends IFoo<T>");
811        });
812
813        it("Type after incomplete enum 1", () => {
814            const source = "function foo() {\r\n" +
815                "            function getOccurrencesAtPosition() {\r\n" +
816                "            switch (node) {\r\n" +
817                "                enum \r\n" +
818                "            }\r\n" +
819                "                \r\n" +
820                "                return undefined;\r\n" +
821                "                \r\n" +
822                "                function keywordToReferenceEntry() {\r\n" +
823                "                }\r\n" +
824                "            }\r\n" +
825                "                \r\n" +
826                "            return {\r\n" +
827                "                getEmitOutput: (fileName): Bar => null,\r\n" +
828                "            };\r\n" +
829                "        }";
830
831            const index = source.indexOf("enum ") + "enum ".length;
832            insertCode(source, index, "Fo");
833        });
834
835        for (const tsIgnoreComment of [
836            "// @ts-ignore",
837            "/* @ts-ignore */",
838            "/*\n  @ts-ignore */"
839        ]) {
840            describe(`${tsIgnoreComment} comment directives`, () => {
841                const textWithIgnoreComment = `const x = 10;
842    function foo() {
843        ${tsIgnoreComment}
844        let y: string = x;
845        return y;
846    }
847    function bar() {
848        ${tsIgnoreComment}
849        let z : string = x;
850        return z;
851    }
852    function bar3() {
853        ${tsIgnoreComment}
854        let z : string = x;
855        return z;
856    }
857    foo();
858    bar();
859    bar3();`;
860                verifyScenario("when deleting ts-ignore comment", verifyDelete);
861                verifyScenario("when inserting ts-ignore comment", verifyInsert);
862                verifyScenario("when changing ts-ignore comment to blah", verifyChangeToBlah);
863                verifyScenario("when changing blah comment to ts-ignore", verifyChangeBackToDirective);
864                verifyScenario("when deleting blah comment", verifyDeletingBlah);
865                verifyScenario("when changing text that adds another comment", verifyChangeDirectiveType);
866                verifyScenario("when changing text that keeps the comment but adds more nodes", verifyReuseChange);
867
868                function verifyCommentDirectives(oldText: IScriptSnapshot, newTextAndChange: { text: IScriptSnapshot; textChangeRange: TextChangeRange; }) {
869                    const { incrementalNewTree, newTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1);
870                    assert.deepEqual(incrementalNewTree.commentDirectives, newTree.commentDirectives);
871                }
872
873                function verifyScenario(scenario: string, verifyChange: (atIndex: number, singleIgnore?: true) => void) {
874                    it(`${scenario} - 0`, () => {
875                        verifyChange(0);
876                    });
877                    it(`${scenario} - 1`, () => {
878                        verifyChange(1);
879                    });
880                    it(`${scenario} - 2`, () => {
881                        verifyChange(2);
882                    });
883                    it(`${scenario} - with single ts-ignore`, () => {
884                        verifyChange(0, /*singleIgnore*/ true);
885                    });
886                }
887
888                function getIndexOfTsIgnoreComment(atIndex: number) {
889                    let index = 0;
890                    for (let i = 0; i <= atIndex; i++) {
891                        index = textWithIgnoreComment.indexOf(tsIgnoreComment, index);
892                    }
893                    return index;
894                }
895
896                function textWithIgnoreCommentFrom(text: string, singleIgnore: true | undefined) {
897                    if (!singleIgnore) return text;
898                    const splits = text.split(tsIgnoreComment);
899                    if (splits.length > 2) {
900                        const tail = splits[splits.length - 2] + splits[splits.length - 1];
901                        splits.length = splits.length - 2;
902                        return splits.join(tsIgnoreComment) + tail;
903                    }
904                    else {
905                        return splits.join(tsIgnoreComment);
906                    }
907                }
908
909                function verifyDelete(atIndex: number, singleIgnore?: true) {
910                    const index = getIndexOfTsIgnoreComment(atIndex);
911                    const oldText = ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore));
912                    const newTextAndChange = withDelete(oldText, index, tsIgnoreComment.length);
913                    verifyCommentDirectives(oldText, newTextAndChange);
914                }
915
916                function verifyInsert(atIndex: number, singleIgnore?: true) {
917                    const index = getIndexOfTsIgnoreComment(atIndex);
918                    const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + textWithIgnoreComment.slice(index + tsIgnoreComment.length), singleIgnore);
919                    const oldText = ScriptSnapshot.fromString(source);
920                    const newTextAndChange = withInsert(oldText, index, tsIgnoreComment);
921                    verifyCommentDirectives(oldText, newTextAndChange);
922                }
923
924                function verifyChangeToBlah(atIndex: number, singleIgnore?: true) {
925                    const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("@");
926                    const oldText = ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore));
927                    const newTextAndChange = withChange(oldText, index, 1, "blah ");
928                    verifyCommentDirectives(oldText, newTextAndChange);
929                }
930
931                function verifyChangeBackToDirective(atIndex: number, singleIgnore?: true) {
932                    const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("@");
933                    const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + "blah " + textWithIgnoreComment.slice(index + 1), singleIgnore);
934                    const oldText = ScriptSnapshot.fromString(source);
935                    const newTextAndChange = withChange(oldText, index, "blah ".length, "@");
936                    verifyCommentDirectives(oldText, newTextAndChange);
937                }
938
939                function verifyDeletingBlah(atIndex: number, singleIgnore?: true) {
940                    const tsIgnoreIndex = getIndexOfTsIgnoreComment(atIndex);
941                    const index = tsIgnoreIndex + tsIgnoreComment.indexOf("@");
942                    const source = textWithIgnoreCommentFrom(textWithIgnoreComment.slice(0, index) + "blah " + textWithIgnoreComment.slice(index + 1), singleIgnore);
943                    const oldText = ScriptSnapshot.fromString(source);
944                    const newTextAndChange = withDelete(oldText, tsIgnoreIndex, tsIgnoreComment.length + "blah".length);
945                    verifyCommentDirectives(oldText, newTextAndChange);
946                }
947
948                function verifyChangeDirectiveType(atIndex: number, singleIgnore?: true) {
949                    const index = getIndexOfTsIgnoreComment(atIndex) + tsIgnoreComment.indexOf("ignore");
950                    const oldText = ScriptSnapshot.fromString(textWithIgnoreCommentFrom(textWithIgnoreComment, singleIgnore));
951                    const newTextAndChange = withChange(oldText, index, "ignore".length, "expect-error");
952                    verifyCommentDirectives(oldText, newTextAndChange);
953                }
954
955                function verifyReuseChange(atIndex: number, singleIgnore?: true) {
956                    const source = `const x = 10;
957    function foo1() {
958        const x1 = 10;
959        ${tsIgnoreComment}
960        let y0: string = x;
961        let y1: string = x;
962        return y1;
963    }
964    function foo2() {
965        const x2 = 10;
966        ${tsIgnoreComment}
967        let y0: string = x;
968        let y2: string = x;
969        return y2;
970    }
971    function foo3() {
972        const x3 = 10;
973        ${tsIgnoreComment}
974        let y0: string = x;
975        let y3: string = x;
976        return y3;
977    }
978    foo1();
979    foo2();
980    foo3();`;
981                    const oldText = ScriptSnapshot.fromString(textWithIgnoreCommentFrom(source, singleIgnore));
982                    const start = source.indexOf(`const x${atIndex + 1}`);
983                    const letStr = `let y${atIndex + 1}: string = x;`;
984                    const end = source.indexOf(letStr) + letStr.length;
985                    const oldSubStr = source.slice(start, end);
986                    const newText = oldSubStr.replace(letStr, `let yn : string = x;`);
987                    const newTextAndChange = withChange(oldText, start, end - start, newText);
988                    verifyCommentDirectives(oldText, newTextAndChange);
989                }
990            });
991        }
992    });
993}
994