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