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