1namespace ts.projectSystem { 2 const packageJson: File = { 3 path: "/package.json", 4 content: `{ "dependencies": { "mobx": "*" } }` 5 }; 6 const aTs: File = { 7 path: "/a.ts", 8 content: "export const foo = 0;", 9 }; 10 const bTs: File = { 11 path: "/b.ts", 12 content: "foo", 13 }; 14 const tsconfig: File = { 15 path: "/tsconfig.json", 16 content: "{}", 17 }; 18 const ambientDeclaration: File = { 19 path: "/ambient.d.ts", 20 content: "declare module 'ambient' {}" 21 }; 22 const mobxPackageJson: File = { 23 path: "/node_modules/mobx/package.json", 24 content: `{ "name": "mobx", "version": "1.0.0" }` 25 }; 26 const mobxDts: File = { 27 path: "/node_modules/mobx/index.d.ts", 28 content: "export declare function observable(): unknown;" 29 }; 30 const exportEqualsMappedType: File = { 31 path: "/lib/foo/constants.d.ts", 32 content: ` 33 type Signals = "SIGINT" | "SIGABRT"; 34 declare const exp: {} & { [K in Signals]: K }; 35 export = exp;`, 36 }; 37 38 describe("unittests:: tsserver:: exportMapCache", () => { 39 it("caches auto-imports in the same file", () => { 40 const { exportMapCache } = setup(); 41 assert.ok(exportMapCache.isUsableByFile(bTs.path as Path)); 42 assert.ok(!exportMapCache.isEmpty()); 43 }); 44 45 it("invalidates the cache when new files are added", () => { 46 const { host, exportMapCache } = setup(); 47 host.writeFile("/src/a2.ts", aTs.content); 48 host.runQueuedTimeoutCallbacks(); 49 assert.ok(!exportMapCache.isUsableByFile(bTs.path as Path)); 50 assert.ok(exportMapCache.isEmpty()); 51 }); 52 53 it("invalidates the cache when files are deleted", () => { 54 const { host, projectService, exportMapCache } = setup(); 55 projectService.closeClientFile(aTs.path); 56 host.deleteFile(aTs.path); 57 host.runQueuedTimeoutCallbacks(); 58 assert.ok(!exportMapCache.isUsableByFile(bTs.path as Path)); 59 assert.ok(exportMapCache.isEmpty()); 60 }); 61 62 it("does not invalidate the cache when package.json is changed inconsequentially", () => { 63 const { host, exportMapCache, project } = setup(); 64 host.writeFile("/package.json", `{ "name": "blah", "dependencies": { "mobx": "*" } }`); 65 host.runQueuedTimeoutCallbacks(); 66 project.getPackageJsonAutoImportProvider(); 67 assert.ok(exportMapCache.isUsableByFile(bTs.path as Path)); 68 assert.ok(!exportMapCache.isEmpty()); 69 }); 70 71 it("invalidates the cache when package.json change results in AutoImportProvider change", () => { 72 const { host, exportMapCache, project } = setup(); 73 host.writeFile("/package.json", `{}`); 74 host.runQueuedTimeoutCallbacks(); 75 project.getPackageJsonAutoImportProvider(); 76 assert.ok(!exportMapCache.isUsableByFile(bTs.path as Path)); 77 assert.ok(exportMapCache.isEmpty()); 78 }); 79 80 it("does not store transient symbols through program updates", () => { 81 const { exportMapCache, project, session } = setup(); 82 // SIGINT, exported from /lib/foo/constants.d.ts, is a mapped type property, which will be a transient symbol. 83 // Transient symbols contain types, which retain the checkers they came from, so are not safe to cache. 84 // We clear symbols from the cache during updateGraph, leaving only the information about how to re-get them 85 // (see getters on `CachedSymbolExportInfo`). We can roughly test that this is working by ensuring that 86 // accessing a transient symbol with two different checkers results in different symbol identities, since 87 // transient symbols are recreated with every new checker. 88 const programBefore = project.getCurrentProgram()!; 89 let sigintPropBefore: readonly SymbolExportInfo[] | undefined; 90 exportMapCache.search(bTs.path as Path, /*preferCapitalized*/ false, returnTrue, (info, symbolName) => { 91 if (symbolName === "SIGINT") sigintPropBefore = info; 92 }); 93 assert.ok(sigintPropBefore); 94 assert.ok(sigintPropBefore![0].symbol.flags & SymbolFlags.Transient); 95 const symbolIdBefore = getSymbolId(sigintPropBefore![0].symbol); 96 97 // Update program without clearing cache 98 session.executeCommandSeq<protocol.UpdateOpenRequest>({ 99 command: protocol.CommandTypes.UpdateOpen, 100 arguments: { 101 changedFiles: [{ 102 fileName: bTs.path, 103 textChanges: [{ 104 newText: " ", 105 start: { line: 1, offset: 1 }, 106 end: { line: 1, offset: 1 }, 107 }] 108 }] 109 } 110 }); 111 project.getLanguageService(/*ensureSynchronized*/ true); 112 assert.notEqual(programBefore, project.getCurrentProgram()!); 113 114 // Get same info from cache again 115 let sigintPropAfter: readonly SymbolExportInfo[] | undefined; 116 exportMapCache.search(bTs.path as Path, /*preferCapitalized*/ false, returnTrue, (info, symbolName) => { 117 if (symbolName === "SIGINT") sigintPropAfter = info; 118 }); 119 assert.ok(sigintPropAfter); 120 assert.notEqual(symbolIdBefore, getSymbolId(sigintPropAfter![0].symbol)); 121 }); 122 }); 123 124 function setup() { 125 const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts, exportEqualsMappedType]); 126 const session = createSession(host); 127 openFilesForSession([aTs, bTs], session); 128 const projectService = session.getProjectService(); 129 const project = configuredProjectAt(projectService, 0); 130 triggerCompletions(); 131 const checker = project.getLanguageService().getProgram()!.getTypeChecker(); 132 return { host, project, projectService, session, exportMapCache: project.getCachedExportInfoMap(), checker, triggerCompletions }; 133 134 function triggerCompletions() { 135 const requestLocation: protocol.FileLocationRequestArgs = { 136 file: bTs.path, 137 line: 1, 138 offset: 3, 139 }; 140 executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, { 141 ...requestLocation, 142 includeExternalModuleExports: true, 143 prefix: "foo", 144 }); 145 } 146 } 147} 148