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