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