• 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 compilationSettingsOrHost 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. A minimal
27         * resolution cache is needed to fully define a source file's shape when
28         * the compilation settings include `module: node16`+, so providing a cache host
29         * object should be preferred. A common host is a language service `ConfiguredProject`.
30         * @param scriptSnapshot Text of the file. Only used if the file was not found
31         * in the registry and a new one was created.
32         * @param version Current version of the file. Only used if the file was not found
33         * in the registry and a new one was created.
34         */
35        acquireDocument(
36            fileName: string,
37            compilationSettingsOrHost: CompilerOptions | MinimalResolutionCacheHost,
38            scriptSnapshot: IScriptSnapshot,
39            version: string,
40            scriptKind?: ScriptKind,
41            sourceFileOptions?: CreateSourceFileOptions | ScriptTarget,
42        ): SourceFile;
43
44        acquireDocumentWithKey(
45            fileName: string,
46            path: Path,
47            compilationSettingsOrHost: CompilerOptions | MinimalResolutionCacheHost,
48            key: DocumentRegistryBucketKey,
49            scriptSnapshot: IScriptSnapshot,
50            version: string,
51            scriptKind?: ScriptKind,
52            sourceFileOptions?: CreateSourceFileOptions | ScriptTarget,
53        ): SourceFile;
54
55        /**
56         * Request an updated version of an already existing SourceFile with a given fileName
57         * and compilationSettings. The update will in-turn call updateLanguageServiceSourceFile
58         * to get an updated SourceFile.
59         *
60         * @param fileName The name of the file requested
61         * @param compilationSettingsOrHost Some compilation settings like target affects the
62         * shape of a the resulting SourceFile. This allows the DocumentRegistry to store
63         * multiple copies of the same file for different compilation settings. A minimal
64         * resolution cache is needed to fully define a source file's shape when
65         * the compilation settings include `module: node16`+, so providing a cache host
66         * object should be preferred. A common host is a language service `ConfiguredProject`.
67         * @param scriptSnapshot Text of the file.
68         * @param version Current version of the file.
69         */
70        updateDocument(
71            fileName: string,
72            compilationSettingsOrHost: CompilerOptions | MinimalResolutionCacheHost,
73            scriptSnapshot: IScriptSnapshot,
74            version: string,
75            scriptKind?: ScriptKind,
76            sourceFileOptions?: CreateSourceFileOptions | ScriptTarget,
77        ): SourceFile;
78
79        updateDocumentWithKey(
80            fileName: string,
81            path: Path,
82            compilationSettingsOrHost: CompilerOptions | MinimalResolutionCacheHost,
83            key: DocumentRegistryBucketKey,
84            scriptSnapshot: IScriptSnapshot,
85            version: string,
86            scriptKind?: ScriptKind,
87            sourceFileOptions?: CreateSourceFileOptions | ScriptTarget,
88        ): SourceFile;
89
90        getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey;
91        /**
92         * Informs the DocumentRegistry that a file is not needed any longer.
93         *
94         * Note: It is not allowed to call release on a SourceFile that was not acquired from
95         * this registry originally.
96         *
97         * @param fileName The name of the file to be released
98         * @param compilationSettings The compilation settings used to acquire the file
99         * @param scriptKind The script kind of the file to be released
100         */
101        /**@deprecated pass scriptKind and impliedNodeFormat for correctness */
102        releaseDocument(fileName: string, compilationSettings: CompilerOptions, scriptKind?: ScriptKind): void;
103        /**
104         * Informs the DocumentRegistry that a file is not needed any longer.
105         *
106         * Note: It is not allowed to call release on a SourceFile that was not acquired from
107         * this registry originally.
108         *
109         * @param fileName The name of the file to be released
110         * @param compilationSettings The compilation settings used to acquire the file
111         * @param scriptKind The script kind of the file to be released
112         * @param impliedNodeFormat The implied source file format of the file to be released
113         */
114        releaseDocument(fileName: string, compilationSettings: CompilerOptions, scriptKind: ScriptKind, impliedNodeFormat: SourceFile["impliedNodeFormat"]): void; // eslint-disable-line @typescript-eslint/unified-signatures
115        /**
116         * @deprecated pass scriptKind for and impliedNodeFormat correctness */
117        releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey, scriptKind?: ScriptKind): void;
118        releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey, scriptKind: ScriptKind, impliedNodeFormat: SourceFile["impliedNodeFormat"]): void; // eslint-disable-line @typescript-eslint/unified-signatures
119
120        /*@internal*/
121        getLanguageServiceRefCounts(path: Path, scriptKind: ScriptKind): [string, number | undefined][];
122
123        reportStats(): string;
124    }
125
126    /*@internal*/
127    export interface ExternalDocumentCache {
128        setDocument(key: DocumentRegistryBucketKeyWithMode, path: Path, sourceFile: SourceFile): void;
129        getDocument(key: DocumentRegistryBucketKeyWithMode, path: Path): SourceFile | undefined;
130    }
131
132    export type DocumentRegistryBucketKey = string & { __bucketKey: any };
133
134    interface DocumentRegistryEntry {
135        sourceFile: SourceFile;
136
137        // The number of language services that this source file is referenced in.   When no more
138        // language services are referencing the file, then the file can be removed from the
139        // registry.
140        languageServiceRefCount: number;
141    }
142
143    type BucketEntry = DocumentRegistryEntry | ESMap<ScriptKind, DocumentRegistryEntry>;
144    function isDocumentRegistryEntry(entry: BucketEntry): entry is DocumentRegistryEntry {
145        return !!(entry as DocumentRegistryEntry).sourceFile;
146    }
147
148    export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry {
149        return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory);
150    }
151
152    /*@internal*/
153    export type DocumentRegistryBucketKeyWithMode = string & { __documentRegistryBucketKeyWithMode: any; };
154    /*@internal*/
155    export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boolean, currentDirectory = "", externalCache?: ExternalDocumentCache): DocumentRegistry {
156        // Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have
157        // for those settings.
158        const buckets = new Map<DocumentRegistryBucketKeyWithMode, ESMap<Path, BucketEntry>>();
159        const getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames);
160
161        function reportStats() {
162            const bucketInfoArray = arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => {
163                const entries = buckets.get(name)!;
164                const sourceFiles: { name: string; scriptKind: ScriptKind, refCount: number; }[] = [];
165                entries.forEach((entry, name) => {
166                    if (isDocumentRegistryEntry(entry)) {
167                        sourceFiles.push({
168                            name,
169                            scriptKind: entry.sourceFile.scriptKind,
170                            refCount: entry.languageServiceRefCount
171                        });
172                    }
173                    else {
174                        entry.forEach((value, scriptKind) => sourceFiles.push({ name, scriptKind, refCount: value.languageServiceRefCount }));
175                    }
176                });
177                sourceFiles.sort((x, y) => y.refCount - x.refCount);
178                return {
179                    bucket: name,
180                    sourceFiles
181                };
182            });
183            return JSON.stringify(bucketInfoArray, undefined, 2);
184        }
185
186        function getCompilationSettings(settingsOrHost: CompilerOptions | MinimalResolutionCacheHost) {
187            if (typeof settingsOrHost.getCompilationSettings === "function") {
188                return (settingsOrHost as MinimalResolutionCacheHost).getCompilationSettings();
189            }
190            return settingsOrHost as CompilerOptions;
191        }
192
193        function acquireDocument(fileName: string, compilationSettings: CompilerOptions | MinimalResolutionCacheHost, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind, languageVersionOrOptions?: CreateSourceFileOptions | ScriptTarget): SourceFile {
194            const path = toPath(fileName, currentDirectory, getCanonicalFileName);
195            const key = getKeyForCompilationSettings(getCompilationSettings(compilationSettings));
196            return acquireDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind, languageVersionOrOptions);
197        }
198
199        function acquireDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions | MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind, languageVersionOrOptions?: CreateSourceFileOptions | ScriptTarget): SourceFile {
200            return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ true, scriptKind, languageVersionOrOptions);
201        }
202
203        function updateDocument(fileName: string, compilationSettings: CompilerOptions | MinimalResolutionCacheHost, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind, languageVersionOrOptions?: CreateSourceFileOptions | ScriptTarget): SourceFile {
204            const path = toPath(fileName, currentDirectory, getCanonicalFileName);
205            const key = getKeyForCompilationSettings(getCompilationSettings(compilationSettings));
206            return updateDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind, languageVersionOrOptions);
207        }
208
209        function updateDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions | MinimalResolutionCacheHost, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind, languageVersionOrOptions?: CreateSourceFileOptions | ScriptTarget): SourceFile {
210            return acquireOrUpdateDocument(fileName, path, getCompilationSettings(compilationSettings), key, scriptSnapshot, version, /*acquiring*/ false, scriptKind, languageVersionOrOptions);
211        }
212
213        function getDocumentRegistryEntry(bucketEntry: BucketEntry, scriptKind: ScriptKind | undefined) {
214            const entry = isDocumentRegistryEntry(bucketEntry) ? bucketEntry : bucketEntry.get(Debug.checkDefined(scriptKind, "If there are more than one scriptKind's for same document the scriptKind should be provided"));
215            Debug.assert(scriptKind === undefined || !entry || entry.sourceFile.scriptKind === scriptKind, `Script kind should match provided ScriptKind:${scriptKind} and sourceFile.scriptKind: ${entry?.sourceFile.scriptKind}, !entry: ${!entry}`);
216            return entry;
217        }
218
219        function acquireOrUpdateDocument(
220            fileName: string,
221            path: Path,
222            compilationSettingsOrHost: CompilerOptions | MinimalResolutionCacheHost,
223            key: DocumentRegistryBucketKey,
224            scriptSnapshot: IScriptSnapshot,
225            version: string,
226            acquiring: boolean,
227            scriptKind: ScriptKind | undefined,
228            languageVersionOrOptions: CreateSourceFileOptions | ScriptTarget | undefined,
229        ): SourceFile {
230            scriptKind = ensureScriptKind(fileName, scriptKind);
231            const compilationSettings = getCompilationSettings(compilationSettingsOrHost);
232            const host: MinimalResolutionCacheHost | undefined = compilationSettingsOrHost === compilationSettings ? undefined : compilationSettingsOrHost as MinimalResolutionCacheHost;
233            const scriptTarget = scriptKind === ScriptKind.JSON ? ScriptTarget.JSON : getEmitScriptTarget(compilationSettings);
234            const sourceFileOptions: CreateSourceFileOptions = typeof languageVersionOrOptions === "object" ?
235                languageVersionOrOptions :
236                {
237                    languageVersion: scriptTarget,
238                    impliedNodeFormat: host && getImpliedNodeFormatForFile(path, host.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(), host, compilationSettings),
239                    setExternalModuleIndicator: getSetExternalModuleIndicator(compilationSettings)
240                };
241            sourceFileOptions.languageVersion = scriptTarget;
242            const oldBucketCount = buckets.size;
243            const keyWithMode = getDocumentRegistryBucketKeyWithMode(key, sourceFileOptions.impliedNodeFormat);
244            const bucket = getOrUpdate(buckets, keyWithMode, () => new Map());
245            if (tracing) {
246                if (buckets.size > oldBucketCount) {
247                    // It is interesting, but not definitively problematic if a build requires multiple document registry buckets -
248                    // perhaps they are for two projects that don't have any overlap.
249                    // Bonus: these events can help us interpret the more interesting event below.
250                    tracing.instant(tracing.Phase.Session, "createdDocumentRegistryBucket", { configFilePath: compilationSettings.configFilePath, key: keyWithMode });
251                }
252
253                // It is fairly suspicious to have one path in two buckets - you'd expect dependencies to have similar configurations.
254                // If this occurs unexpectedly, the fix is likely to synchronize the project settings.
255                // Skip .d.ts files to reduce noise (should also cover most of node_modules).
256                const otherBucketKey = !isDeclarationFileName(path) &&
257                    forEachEntry(buckets, (bucket, bucketKey) => bucketKey !== keyWithMode && bucket.has(path) && bucketKey);
258                if (otherBucketKey) {
259                    tracing.instant(tracing.Phase.Session, "documentRegistryBucketOverlap", { path, key1: otherBucketKey, key2: keyWithMode });
260                }
261            }
262
263            const bucketEntry = bucket.get(path);
264            let entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind);
265            if (!entry && externalCache) {
266                const sourceFile = externalCache.getDocument(keyWithMode, path);
267                if (sourceFile) {
268                    Debug.assert(acquiring);
269                    entry = {
270                        sourceFile,
271                        languageServiceRefCount: 0
272                    };
273                    setBucketEntry();
274                }
275            }
276
277            if (!entry) {
278                // Have never seen this file with these settings.  Create a new source file for it.
279                const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, sourceFileOptions, version, /*setNodeParents*/ false, scriptKind, compilationSettings);
280                if (externalCache) {
281                    externalCache.setDocument(keyWithMode, path, sourceFile);
282                }
283                entry = {
284                    sourceFile,
285                    languageServiceRefCount: 1,
286                };
287                setBucketEntry();
288            }
289            else {
290                // We have an entry for this file.  However, it may be for a different version of
291                // the script snapshot.  If so, update it appropriately.  Otherwise, we can just
292                // return it as is.
293                if (entry.sourceFile.version !== version) {
294                    entry.sourceFile = updateLanguageServiceSourceFile(entry.sourceFile, scriptSnapshot, version,
295                        scriptSnapshot.getChangeRange(entry.sourceFile.scriptSnapshot!), /*aggressiveChecks*/ undefined, compilationSettings); // TODO: GH#18217
296                    if (externalCache) {
297                        externalCache.setDocument(keyWithMode, path, entry.sourceFile);
298                    }
299                }
300
301                // If we're acquiring, then this is the first time this LS is asking for this document.
302                // Increase our ref count so we know there's another LS using the document.  If we're
303                // not acquiring, then that means the LS is 'updating' the file instead, and that means
304                // it has already acquired the document previously.  As such, we do not need to increase
305                // the ref count.
306                if (acquiring) {
307                    entry.languageServiceRefCount++;
308                }
309            }
310            Debug.assert(entry.languageServiceRefCount !== 0);
311
312            return entry.sourceFile;
313
314            function setBucketEntry() {
315                if (!bucketEntry) {
316                    bucket.set(path, entry!);
317                }
318                else if (isDocumentRegistryEntry(bucketEntry)) {
319                    const scriptKindMap = new Map<ScriptKind, DocumentRegistryEntry>();
320                    scriptKindMap.set(bucketEntry.sourceFile.scriptKind, bucketEntry);
321                    scriptKindMap.set(scriptKind!, entry!);
322                    bucket.set(path, scriptKindMap);
323                }
324                else {
325                    bucketEntry.set(scriptKind!, entry!);
326                }
327            }
328        }
329
330        function releaseDocument(fileName: string, compilationSettings: CompilerOptions, scriptKind?: ScriptKind, impliedNodeFormat?: SourceFile["impliedNodeFormat"]): void {
331            const path = toPath(fileName, currentDirectory, getCanonicalFileName);
332            const key = getKeyForCompilationSettings(compilationSettings);
333            return releaseDocumentWithKey(path, key, scriptKind, impliedNodeFormat);
334        }
335
336        function releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey, scriptKind?: ScriptKind, impliedNodeFormat?: SourceFile["impliedNodeFormat"]): void {
337            const bucket = Debug.checkDefined(buckets.get(getDocumentRegistryBucketKeyWithMode(key, impliedNodeFormat)));
338            const bucketEntry = bucket.get(path)!;
339            const entry = getDocumentRegistryEntry(bucketEntry, scriptKind)!;
340            entry.languageServiceRefCount--;
341
342            Debug.assert(entry.languageServiceRefCount >= 0);
343            if (entry.languageServiceRefCount === 0) {
344                if (isDocumentRegistryEntry(bucketEntry)) {
345                    bucket.delete(path);
346                }
347                else {
348                    bucketEntry.delete(scriptKind!);
349                    if (bucketEntry.size === 1) {
350                        bucket.set(path, firstDefinedIterator(bucketEntry.values(), identity)!);
351                    }
352                }
353            }
354        }
355
356        function getLanguageServiceRefCounts(path: Path, scriptKind: ScriptKind) {
357            return arrayFrom(buckets.entries(), ([key, bucket]): [string, number | undefined] => {
358                const bucketEntry = bucket.get(path);
359                const entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind);
360                return [key, entry && entry.languageServiceRefCount];
361            });
362        }
363
364        return {
365            acquireDocument,
366            acquireDocumentWithKey,
367            updateDocument,
368            updateDocumentWithKey,
369            releaseDocument,
370            releaseDocumentWithKey,
371            getLanguageServiceRefCounts,
372            reportStats,
373            getKeyForCompilationSettings
374        };
375    }
376
377    function compilerOptionValueToString(value: unknown): string {
378        if (value === null || typeof value !== "object") { // eslint-disable-line no-null/no-null
379            return "" + value;
380        }
381        if (isArray(value)) {
382            return `[${map(value, e => compilerOptionValueToString(e))?.join(",")}]`;
383        }
384        let str = "{";
385        for (const key in value) {
386            if (hasProperty(value, key)) {
387                str += `${key}: ${compilerOptionValueToString((value as any)[key])}`;
388            }
389        }
390        return str + "}";
391    }
392
393    function getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey {
394        return sourceFileAffectingCompilerOptions.map(option => compilerOptionValueToString(getCompilerOptionValue(settings, option))).join("|") + (settings.pathsBasePath ? `|${settings.pathsBasePath}` : undefined) as DocumentRegistryBucketKey;
395    }
396
397    function getDocumentRegistryBucketKeyWithMode(key: DocumentRegistryBucketKey, mode: ModuleKind.ESNext | ModuleKind.CommonJS | undefined) {
398        return (mode ? `${key}|${mode}` : key) as DocumentRegistryBucketKeyWithMode;
399    }
400}
401