• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts {
2    /**
3     * The document registry represents a store of SourceFile objects that can be shared between
4     * multiple LanguageService instances. A LanguageService instance holds on the SourceFile (AST)
5     * of files in the context.
6     * SourceFile objects account for most of the memory usage by the language service. Sharing
7     * the same DocumentRegistry instance between different instances of LanguageService allow
8     * for more efficient memory utilization since all projects will share at least the library
9     * file (lib.d.ts).
10     *
11     * A more advanced use of the document registry is to serialize sourceFile objects to disk
12     * and re-hydrate them when needed.
13     *
14     * To create a default DocumentRegistry, use createDocumentRegistry to create one, and pass it
15     * to all subsequent createLanguageService calls.
16     */
17    export interface DocumentRegistry {
18        /**
19         * Request a stored SourceFile with a given fileName and compilationSettings.
20         * The first call to acquire will call createLanguageServiceSourceFile to generate
21         * the SourceFile if was not found in the registry.
22         *
23         * @param fileName The name of the file requested
24         * @param compilationSettings Some compilation settings like target affects the
25         * shape of a the resulting SourceFile. This allows the DocumentRegistry to store
26         * multiple copies of the same file for different compilation settings.
27         * @param scriptSnapshot Text of the file. Only used if the file was not found
28         * in the registry and a new one was created.
29         * @param version Current version of the file. Only used if the file was not found
30         * in the registry and a new one was created.
31         */
32        acquireDocument(
33            fileName: string,
34            compilationSettings: CompilerOptions,
35            scriptSnapshot: IScriptSnapshot,
36            version: string,
37            scriptKind?: ScriptKind): SourceFile;
38
39        acquireDocumentWithKey(
40            fileName: string,
41            path: Path,
42            compilationSettings: CompilerOptions,
43            key: DocumentRegistryBucketKey,
44            scriptSnapshot: IScriptSnapshot,
45            version: string,
46            scriptKind?: ScriptKind): SourceFile;
47
48        /**
49         * Request an updated version of an already existing SourceFile with a given fileName
50         * and compilationSettings. The update will in-turn call updateLanguageServiceSourceFile
51         * to get an updated SourceFile.
52         *
53         * @param fileName The name of the file requested
54         * @param compilationSettings Some compilation settings like target affects the
55         * shape of a the resulting SourceFile. This allows the DocumentRegistry to store
56         * multiple copies of the same file for different compilation settings.
57         * @param scriptSnapshot Text of the file.
58         * @param version Current version of the file.
59         */
60        updateDocument(
61            fileName: string,
62            compilationSettings: CompilerOptions,
63            scriptSnapshot: IScriptSnapshot,
64            version: string,
65            scriptKind?: ScriptKind): SourceFile;
66
67        updateDocumentWithKey(
68            fileName: string,
69            path: Path,
70            compilationSettings: CompilerOptions,
71            key: DocumentRegistryBucketKey,
72            scriptSnapshot: IScriptSnapshot,
73            version: string,
74            scriptKind?: ScriptKind): SourceFile;
75
76        getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey;
77        /**
78         * Informs the DocumentRegistry that a file is not needed any longer.
79         *
80         * Note: It is not allowed to call release on a SourceFile that was not acquired from
81         * this registry originally.
82         *
83         * @param fileName The name of the file to be released
84         * @param compilationSettings The compilation settings used to acquire the file
85         */
86        releaseDocument(fileName: string, compilationSettings: CompilerOptions): void;
87
88        releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void;
89
90        /*@internal*/
91        getLanguageServiceRefCounts(path: Path): [string, number | undefined][];
92
93        reportStats(): string;
94    }
95
96    /*@internal*/
97    export interface ExternalDocumentCache {
98        setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile): void;
99        getDocument(key: DocumentRegistryBucketKey, path: Path): SourceFile | undefined;
100    }
101
102    export type DocumentRegistryBucketKey = string & { __bucketKey: any };
103
104    interface DocumentRegistryEntry {
105        sourceFile: SourceFile;
106
107        // The number of language services that this source file is referenced in.   When no more
108        // language services are referencing the file, then the file can be removed from the
109        // registry.
110        languageServiceRefCount: number;
111    }
112
113    export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry {
114        return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory);
115    }
116
117    /*@internal*/
118    export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boolean, currentDirectory = "", externalCache?: ExternalDocumentCache): DocumentRegistry {
119        // Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have
120        // for those settings.
121        const buckets = new Map<string, ESMap<Path, DocumentRegistryEntry>>();
122        const getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames);
123
124        function reportStats() {
125            const bucketInfoArray = arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => {
126                const entries = buckets.get(name)!;
127                const sourceFiles: { name: string; refCount: number; }[] = [];
128                entries.forEach((entry, name) => {
129                    sourceFiles.push({
130                        name,
131                        refCount: entry.languageServiceRefCount
132                    });
133                });
134                sourceFiles.sort((x, y) => y.refCount - x.refCount);
135                return {
136                    bucket: name,
137                    sourceFiles
138                };
139            });
140            return JSON.stringify(bucketInfoArray, undefined, 2);
141        }
142
143        function acquireDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile {
144            const path = toPath(fileName, currentDirectory, getCanonicalFileName);
145            const key = getKeyForCompilationSettings(compilationSettings);
146            return acquireDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind);
147        }
148
149        function acquireDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile {
150            return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ true, scriptKind);
151        }
152
153        function updateDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile {
154            const path = toPath(fileName, currentDirectory, getCanonicalFileName);
155            const key = getKeyForCompilationSettings(compilationSettings);
156            return updateDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind);
157        }
158
159        function updateDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile {
160            return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ false, scriptKind);
161        }
162
163        function acquireOrUpdateDocument(
164            fileName: string,
165            path: Path,
166            compilationSettings: CompilerOptions,
167            key: DocumentRegistryBucketKey,
168            scriptSnapshot: IScriptSnapshot,
169            version: string,
170            acquiring: boolean,
171            scriptKind?: ScriptKind): SourceFile {
172
173            const bucket = getOrUpdate(buckets, key, () => new Map<Path, DocumentRegistryEntry>());
174            let entry = bucket.get(path);
175            const scriptTarget = scriptKind === ScriptKind.JSON ? ScriptTarget.JSON : compilationSettings.target || ScriptTarget.ES5;
176            if (!entry && externalCache) {
177                const sourceFile = externalCache.getDocument(key, path);
178                if (sourceFile) {
179                    Debug.assert(acquiring);
180                    entry = {
181                        sourceFile,
182                        languageServiceRefCount: 0
183                    };
184                    bucket.set(path, entry);
185                }
186            }
187
188            if (!entry) {
189                // Have never seen this file with these settings.  Create a new source file for it.
190                const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, scriptTarget, version, /*setNodeParents*/ false, scriptKind, compilationSettings);
191                if (externalCache) {
192                    externalCache.setDocument(key, path, sourceFile);
193                }
194                entry = {
195                    sourceFile,
196                    languageServiceRefCount: 1,
197                };
198                bucket.set(path, entry);
199            }
200            else {
201                // We have an entry for this file.  However, it may be for a different version of
202                // the script snapshot.  If so, update it appropriately.  Otherwise, we can just
203                // return it as is.
204                if (entry.sourceFile.version !== version) {
205                    entry.sourceFile = updateLanguageServiceSourceFile(entry.sourceFile, scriptSnapshot, version,
206                        scriptSnapshot.getChangeRange(entry.sourceFile.scriptSnapshot!), /*aggressiveChecks*/ undefined, compilationSettings); // TODO: GH#18217
207                    if (externalCache) {
208                        externalCache.setDocument(key, path, entry.sourceFile);
209                    }
210                }
211
212                // If we're acquiring, then this is the first time this LS is asking for this document.
213                // Increase our ref count so we know there's another LS using the document.  If we're
214                // not acquiring, then that means the LS is 'updating' the file instead, and that means
215                // it has already acquired the document previously.  As such, we do not need to increase
216                // the ref count.
217                if (acquiring) {
218                    entry.languageServiceRefCount++;
219                }
220            }
221            Debug.assert(entry.languageServiceRefCount !== 0);
222
223            return entry.sourceFile;
224        }
225
226        function releaseDocument(fileName: string, compilationSettings: CompilerOptions): void {
227            const path = toPath(fileName, currentDirectory, getCanonicalFileName);
228            const key = getKeyForCompilationSettings(compilationSettings);
229            return releaseDocumentWithKey(path, key);
230        }
231
232        function releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void {
233            const bucket = Debug.checkDefined(buckets.get(key));
234            const entry = bucket.get(path)!;
235            entry.languageServiceRefCount--;
236
237            Debug.assert(entry.languageServiceRefCount >= 0);
238            if (entry.languageServiceRefCount === 0) {
239                bucket.delete(path);
240            }
241        }
242
243        function getLanguageServiceRefCounts(path: Path) {
244            return arrayFrom(buckets.entries(), ([key, bucket]): [string, number | undefined] => {
245                const entry = bucket.get(path);
246                return [key, entry && entry.languageServiceRefCount];
247            });
248        }
249
250        return {
251            acquireDocument,
252            acquireDocumentWithKey,
253            updateDocument,
254            updateDocumentWithKey,
255            releaseDocument,
256            releaseDocumentWithKey,
257            getLanguageServiceRefCounts,
258            reportStats,
259            getKeyForCompilationSettings
260        };
261    }
262
263    function getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey {
264        return sourceFileAffectingCompilerOptions.map(option => getCompilerOptionValue(settings, option)).join("|") as DocumentRegistryBucketKey;
265    }
266}
267