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