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