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