1namespace ts.server { 2 export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; 3 /*@internal*/ 4 export const maxFileSize = 4 * 1024 * 1024; 5 6 export const ProjectsUpdatedInBackgroundEvent = "projectsUpdatedInBackground"; 7 export const ProjectLoadingStartEvent = "projectLoadingStart"; 8 export const ProjectLoadingFinishEvent = "projectLoadingFinish"; 9 export const LargeFileReferencedEvent = "largeFileReferenced"; 10 export const ConfigFileDiagEvent = "configFileDiag"; 11 export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; 12 export const ProjectInfoTelemetryEvent = "projectInfo"; 13 export const OpenFileInfoTelemetryEvent = "openFileInfo"; 14 const ensureProjectForOpenFileSchedule = "*ensureProjectForOpenFiles*"; 15 16 export interface ProjectsUpdatedInBackgroundEvent { 17 eventName: typeof ProjectsUpdatedInBackgroundEvent; 18 data: { openFiles: string[]; }; 19 } 20 21 export interface ProjectLoadingStartEvent { 22 eventName: typeof ProjectLoadingStartEvent; 23 data: { project: Project; reason: string; }; 24 } 25 26 export interface ProjectLoadingFinishEvent { 27 eventName: typeof ProjectLoadingFinishEvent; 28 data: { project: Project; }; 29 } 30 31 export interface LargeFileReferencedEvent { 32 eventName: typeof LargeFileReferencedEvent; 33 data: { file: string; fileSize: number; maxFileSize: number; }; 34 } 35 36 export interface ConfigFileDiagEvent { 37 eventName: typeof ConfigFileDiagEvent; 38 data: { triggerFile: string, configFileName: string, diagnostics: readonly Diagnostic[] }; 39 } 40 41 export interface ProjectLanguageServiceStateEvent { 42 eventName: typeof ProjectLanguageServiceStateEvent; 43 data: { project: Project, languageServiceEnabled: boolean }; 44 } 45 46 /** This will be converted to the payload of a protocol.TelemetryEvent in session.defaultEventHandler. */ 47 export interface ProjectInfoTelemetryEvent { 48 readonly eventName: typeof ProjectInfoTelemetryEvent; 49 readonly data: ProjectInfoTelemetryEventData; 50 } 51 52 /* 53 * __GDPR__ 54 * "projectInfo" : { 55 * "${include}": ["${TypeScriptCommonProperties}"], 56 * "projectId": { "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight", "endpoint": "ProjectId" }, 57 * "fileStats": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 58 * "compilerOptions": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 59 * "extends": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 60 * "files": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 61 * "include": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 62 * "exclude": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 63 * "compileOnSave": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 64 * "typeAcquisition": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 65 * "configFileName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 66 * "projectType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 67 * "languageServiceEnabled": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 68 * "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } 69 * } 70 */ 71 export interface ProjectInfoTelemetryEventData { 72 /** Cryptographically secure hash of project file location. */ 73 readonly projectId: string; 74 /** Count of file extensions seen in the project. */ 75 readonly fileStats: FileStats; 76 /** 77 * Any compiler options that might contain paths will be taken out. 78 * Enum compiler options will be converted to strings. 79 */ 80 readonly compilerOptions: CompilerOptions; 81 // "extends", "files", "include", or "exclude" will be undefined if an external config is used. 82 // Otherwise, we will use "true" if the property is present and "false" if it is missing. 83 readonly extends: boolean | undefined; 84 readonly files: boolean | undefined; 85 readonly include: boolean | undefined; 86 readonly exclude: boolean | undefined; 87 readonly compileOnSave: boolean; 88 readonly typeAcquisition: ProjectInfoTypeAcquisitionData; 89 90 readonly configFileName: "tsconfig.json" | "jsconfig.json" | "other"; 91 readonly projectType: "external" | "configured"; 92 readonly languageServiceEnabled: boolean; 93 /** TypeScript version used by the server. */ 94 readonly version: string; 95 } 96 97 /** 98 * Info that we may send about a file that was just opened. 99 * Info about a file will only be sent once per session, even if the file changes in ways that might affect the info. 100 * Currently this is only sent for '.js' files. 101 */ 102 export interface OpenFileInfoTelemetryEvent { 103 readonly eventName: typeof OpenFileInfoTelemetryEvent; 104 readonly data: OpenFileInfoTelemetryEventData; 105 } 106 107 export interface OpenFileInfoTelemetryEventData { 108 readonly info: OpenFileInfo; 109 } 110 111 export interface ProjectInfoTypeAcquisitionData { 112 readonly enable: boolean | undefined; 113 // Actual values of include/exclude entries are scrubbed. 114 readonly include: boolean; 115 readonly exclude: boolean; 116 } 117 118 export interface FileStats { 119 readonly js: number; 120 readonly jsSize?: number; 121 122 readonly jsx: number; 123 readonly jsxSize?: number; 124 125 readonly ts: number; 126 readonly tsSize?: number; 127 128 readonly tsx: number; 129 readonly tsxSize?: number; 130 131 readonly dts: number; 132 readonly dtsSize?: number; 133 134 readonly deferred: number; 135 readonly deferredSize?: number; 136 137 readonly ets: number; 138 readonly etsSize?: number; 139 } 140 141 export interface OpenFileInfo { 142 readonly checkJs: boolean; 143 } 144 145 export type ProjectServiceEvent = 146 LargeFileReferencedEvent 147 | ProjectsUpdatedInBackgroundEvent 148 | ProjectLoadingStartEvent 149 | ProjectLoadingFinishEvent 150 | ConfigFileDiagEvent 151 | ProjectLanguageServiceStateEvent 152 | ProjectInfoTelemetryEvent 153 | OpenFileInfoTelemetryEvent; 154 155 export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; 156 157 /*@internal*/ 158 export type PerformanceEventHandler = (event: PerformanceEvent) => void; 159 160 export interface SafeList { 161 [name: string]: { match: RegExp, exclude?: (string | number)[][], types?: string[] }; 162 } 163 164 function prepareConvertersForEnumLikeCompilerOptions(commandLineOptions: CommandLineOption[]): ESMap<string, ESMap<string, number>> { 165 const map = new Map<string, ESMap<string, number>>(); 166 for (const option of commandLineOptions) { 167 if (typeof option.type === "object") { 168 const optionMap = <ESMap<string, number>>option.type; 169 // verify that map contains only numbers 170 optionMap.forEach(value => { 171 Debug.assert(typeof value === "number"); 172 }); 173 map.set(option.name, optionMap); 174 } 175 } 176 return map; 177 } 178 179 const compilerOptionConverters = prepareConvertersForEnumLikeCompilerOptions(optionDeclarations); 180 const watchOptionsConverters = prepareConvertersForEnumLikeCompilerOptions(optionsForWatch); 181 const indentStyle = new Map(getEntries({ 182 none: IndentStyle.None, 183 block: IndentStyle.Block, 184 smart: IndentStyle.Smart 185 })); 186 187 export interface TypesMapFile { 188 typesMap: SafeList; 189 simpleMap: { [libName: string]: string }; 190 } 191 192 /** 193 * How to understand this block: 194 * * The 'match' property is a regexp that matches a filename. 195 * * If 'match' is successful, then: 196 * * All files from 'exclude' are removed from the project. See below. 197 * * All 'types' are included in ATA 198 * * What the heck is 'exclude' ? 199 * * An array of an array of strings and numbers 200 * * Each array is: 201 * * An array of strings and numbers 202 * * The strings are literals 203 * * The numbers refer to capture group indices from the 'match' regexp 204 * * Remember that '1' is the first group 205 * * These are concatenated together to form a new regexp 206 * * Filenames matching these regexps are excluded from the project 207 * This default value is tested in tsserverProjectSystem.ts; add tests there 208 * if you are changing this so that you can be sure your regexp works! 209 */ 210 const defaultTypeSafeList: SafeList = { 211 "jquery": { 212 // jquery files can have names like "jquery-1.10.2.min.js" (or "jquery.intellisense.js") 213 match: /jquery(-[\d\.]+)?(\.intellisense)?(\.min)?\.js$/i, 214 types: ["jquery"] 215 }, 216 "WinJS": { 217 // e.g. c:/temp/UWApp1/lib/winjs-4.0.1/js/base.js 218 match: /^(.*\/winjs-[.\d]+)\/js\/base\.js$/i, // If the winjs/base.js file is found.. 219 exclude: [["^", 1, "/.*"]], // ..then exclude all files under the winjs folder 220 types: ["winjs"] // And fetch the @types package for WinJS 221 }, 222 "Kendo": { 223 // e.g. /Kendo3/wwwroot/lib/kendo/kendo.all.min.js 224 match: /^(.*\/kendo(-ui)?)\/kendo\.all(\.min)?\.js$/i, 225 exclude: [["^", 1, "/.*"]], 226 types: ["kendo-ui"] 227 }, 228 "Office Nuget": { 229 // e.g. /scripts/Office/1/excel-15.debug.js 230 match: /^(.*\/office\/1)\/excel-\d+\.debug\.js$/i, // Office NuGet package is installed under a "1/office" folder 231 exclude: [["^", 1, "/.*"]], // Exclude that whole folder if the file indicated above is found in it 232 types: ["office"] // @types package to fetch instead 233 }, 234 "References": { 235 match: /^(.*\/_references\.js)$/i, 236 exclude: [["^", 1, "$"]] 237 } 238 }; 239 240 export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings { 241 if (isString(protocolOptions.indentStyle)) { 242 protocolOptions.indentStyle = indentStyle.get(protocolOptions.indentStyle.toLowerCase()); 243 Debug.assert(protocolOptions.indentStyle !== undefined); 244 } 245 return <any>protocolOptions; 246 } 247 248 export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin { 249 compilerOptionConverters.forEach((mappedValues, id) => { 250 const propertyValue = protocolOptions[id]; 251 if (isString(propertyValue)) { 252 protocolOptions[id] = mappedValues.get(propertyValue.toLowerCase()); 253 } 254 }); 255 return <any>protocolOptions; 256 } 257 258 export function convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions, currentDirectory?: string): WatchOptionsAndErrors | undefined { 259 let watchOptions: WatchOptions | undefined; 260 let errors: Diagnostic[] | undefined; 261 optionsForWatch.forEach(option => { 262 const propertyValue = protocolOptions[option.name]; 263 if (propertyValue === undefined) return; 264 const mappedValues = watchOptionsConverters.get(option.name); 265 (watchOptions || (watchOptions = {}))[option.name] = mappedValues ? 266 isString(propertyValue) ? mappedValues.get(propertyValue.toLowerCase()) : propertyValue : 267 convertJsonOption(option, propertyValue, currentDirectory || "", errors || (errors = [])); 268 }); 269 return watchOptions && { watchOptions, errors }; 270 } 271 272 export function convertTypeAcquisition(protocolOptions: protocol.InferredProjectCompilerOptions): TypeAcquisition | undefined { 273 let result: TypeAcquisition | undefined; 274 typeAcquisitionDeclarations.forEach((option) => { 275 const propertyValue = protocolOptions[option.name]; 276 if (propertyValue === undefined) return; 277 (result || (result = {}))[option.name] = propertyValue; 278 }); 279 return result; 280 } 281 282 export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind { 283 return isString(scriptKindName) ? convertScriptKindName(scriptKindName) : scriptKindName; 284 } 285 286 export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) { 287 switch (scriptKindName) { 288 case "JS": 289 return ScriptKind.JS; 290 case "JSX": 291 return ScriptKind.JSX; 292 case "TS": 293 return ScriptKind.TS; 294 case "TSX": 295 return ScriptKind.TSX; 296 case "ETS": 297 return ScriptKind.ETS; 298 default: 299 return ScriptKind.Unknown; 300 } 301 } 302 303 /*@internal*/ 304 export function convertUserPreferences(preferences: protocol.UserPreferences): UserPreferences { 305 const { lazyConfiguredProjectsFromExternalProject, ...userPreferences } = preferences; 306 return userPreferences; 307 } 308 309 export interface HostConfiguration { 310 formatCodeOptions: FormatCodeSettings; 311 preferences: protocol.UserPreferences; 312 hostInfo: string; 313 extraFileExtensions?: FileExtensionInfo[]; 314 watchOptions?: WatchOptions; 315 } 316 317 export interface OpenConfiguredProjectResult { 318 configFileName?: NormalizedPath; 319 configFileErrors?: readonly Diagnostic[]; 320 } 321 322 interface AssignProjectResult extends OpenConfiguredProjectResult { 323 retainProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined; 324 } 325 326 interface FilePropertyReader<T> { 327 getFileName(f: T): string; 328 getScriptKind(f: T, extraFileExtensions?: FileExtensionInfo[]): ScriptKind; 329 hasMixedContent(f: T, extraFileExtensions: FileExtensionInfo[] | undefined): boolean; 330 } 331 332 const fileNamePropertyReader: FilePropertyReader<string> = { 333 getFileName: x => x, 334 getScriptKind: (fileName, extraFileExtensions) => { 335 let result: ScriptKind | undefined; 336 if (extraFileExtensions) { 337 const fileExtension = getAnyExtensionFromPath(fileName); 338 if (fileExtension) { 339 some(extraFileExtensions, info => { 340 if (info.extension === fileExtension) { 341 result = info.scriptKind; 342 return true; 343 } 344 return false; 345 }); 346 } 347 } 348 return result!; // TODO: GH#18217 349 }, 350 hasMixedContent: (fileName, extraFileExtensions) => some(extraFileExtensions, ext => ext.isMixedContent && fileExtensionIs(fileName, ext.extension)), 351 }; 352 353 const externalFilePropertyReader: FilePropertyReader<protocol.ExternalFile> = { 354 getFileName: x => x.fileName, 355 getScriptKind: x => tryConvertScriptKindName(x.scriptKind!), // TODO: GH#18217 356 hasMixedContent: x => !!x.hasMixedContent, 357 }; 358 359 function findProjectByName<T extends Project>(projectName: string, projects: T[]): T | undefined { 360 for (const proj of projects) { 361 if (proj.getProjectName() === projectName) { 362 return proj; 363 } 364 } 365 } 366 367 const enum ConfigFileWatcherStatus { 368 ReloadingFiles = "Reloading configured projects for files", 369 ReloadingInferredRootFiles = "Reloading configured projects for only inferred root files", 370 UpdatedCallback = "Updated the callback", 371 OpenFilesImpactedByConfigFileAdd = "File added to open files impacted by this config file", 372 OpenFilesImpactedByConfigFileRemove = "File removed from open files impacted by this config file", 373 RootOfInferredProjectTrue = "Open file was set as Inferred root", 374 RootOfInferredProjectFalse = "Open file was set as not inferred root", 375 } 376 377 /*@internal*/ 378 interface ConfigFileExistenceInfo { 379 /** 380 * Cached value of existence of config file 381 * It is true if there is configured project open for this file. 382 * It can be either true or false if this is the config file that is being watched by inferred project 383 * to decide when to update the structure so that it knows about updating the project for its files 384 * (config file may include the inferred project files after the change and hence may be wont need to be in inferred project) 385 */ 386 exists: boolean; 387 /** 388 * openFilesImpactedByConfigFiles is a map of open files that would be impacted by this config file 389 * because these are the paths being looked up for their default configured project location 390 * The value in the map is true if the open file is root of the inferred project 391 * It is false when the open file that would still be impacted by existence of 392 * this config file but it is not the root of inferred project 393 */ 394 openFilesImpactedByConfigFile: ESMap<Path, boolean>; 395 /** 396 * The file watcher watching the config file because there is open script info that is root of 397 * inferred project and will be impacted by change in the status of the config file 398 * The watcher is present only when there is no open configured project for the config file 399 */ 400 configFileWatcherForRootOfInferredProject?: FileWatcher; 401 } 402 403 export interface ProjectServiceOptions { 404 host: ServerHost; 405 logger: Logger; 406 cancellationToken: HostCancellationToken; 407 useSingleInferredProject: boolean; 408 useInferredProjectPerProjectRoot: boolean; 409 typingsInstaller: ITypingsInstaller; 410 eventHandler?: ProjectServiceEventHandler; 411 suppressDiagnosticEvents?: boolean; 412 throttleWaitMilliseconds?: number; 413 globalPlugins?: readonly string[]; 414 pluginProbeLocations?: readonly string[]; 415 allowLocalPluginLoads?: boolean; 416 typesMapLocation?: string; 417 /** @deprecated use serverMode instead */ 418 syntaxOnly?: boolean; 419 serverMode?: LanguageServiceMode; 420 } 421 422 interface OriginalFileInfo { fileName: NormalizedPath; path: Path; } 423 interface AncestorConfigFileInfo { 424 /** config file name */ 425 fileName: string; 426 /** path of open file so we can look at correct root */ 427 path: Path; 428 configFileInfo: true; 429 } 430 type OpenScriptInfoOrClosedFileInfo = ScriptInfo | OriginalFileInfo; 431 type OpenScriptInfoOrClosedOrConfigFileInfo = OpenScriptInfoOrClosedFileInfo | AncestorConfigFileInfo; 432 433 function isOpenScriptInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is ScriptInfo { 434 return !!(infoOrFileNameOrConfig as ScriptInfo).containingProjects; 435 } 436 437 function isAncestorConfigFileInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is AncestorConfigFileInfo { 438 return !!(infoOrFileNameOrConfig as AncestorConfigFileInfo).configFileInfo; 439 } 440 441 /*@internal*/ 442 /** Kind of operation to perform to get project reference project */ 443 export enum ProjectReferenceProjectLoadKind { 444 /** Find existing project for project reference */ 445 Find, 446 /** Find existing project or create one for the project reference */ 447 FindCreate, 448 /** Find existing project or create and load it for the project reference */ 449 FindCreateLoad 450 } 451 452 /*@internal*/ 453 export function forEachResolvedProjectReferenceProject<T>( 454 project: ConfiguredProject, 455 fileName: string | undefined, 456 cb: (child: ConfiguredProject) => T | undefined, 457 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.Find | ProjectReferenceProjectLoadKind.FindCreate, 458 ): T | undefined; 459 /*@internal*/ 460 export function forEachResolvedProjectReferenceProject<T>( 461 project: ConfiguredProject, 462 fileName: string | undefined, 463 cb: (child: ConfiguredProject) => T | undefined, 464 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, 465 reason: string 466 ): T | undefined; 467 export function forEachResolvedProjectReferenceProject<T>( 468 project: ConfiguredProject, 469 fileName: string | undefined, 470 cb: (child: ConfiguredProject) => T | undefined, 471 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, 472 reason?: string 473 ): T | undefined { 474 const resolvedRefs = project.getCurrentProgram()?.getResolvedProjectReferences(); 475 if (!resolvedRefs) return undefined; 476 let seenResolvedRefs: ESMap<string, ProjectReferenceProjectLoadKind> | undefined; 477 const possibleDefaultRef = fileName ? project.getResolvedProjectReferenceToRedirect(fileName) : undefined; 478 if (possibleDefaultRef) { 479 // Try to find the name of the file directly through resolved project references 480 const configFileName = toNormalizedPath(possibleDefaultRef.sourceFile.fileName); 481 const child = project.projectService.findConfiguredProjectByProjectName(configFileName); 482 if (child) { 483 const result = cb(child); 484 if (result) return result; 485 } 486 else if (projectReferenceProjectLoadKind !== ProjectReferenceProjectLoadKind.Find) { 487 seenResolvedRefs = new Map(); 488 // Try to see if this project can be loaded 489 const result = forEachResolvedProjectReferenceProjectWorker( 490 resolvedRefs, 491 project.getCompilerOptions(), 492 (ref, loadKind) => possibleDefaultRef === ref ? callback(ref, loadKind) : undefined, 493 projectReferenceProjectLoadKind, 494 project.projectService, 495 seenResolvedRefs 496 ); 497 if (result) return result; 498 // Cleanup seenResolvedRefs 499 seenResolvedRefs.clear(); 500 } 501 } 502 503 return forEachResolvedProjectReferenceProjectWorker( 504 resolvedRefs, 505 project.getCompilerOptions(), 506 (ref, loadKind) => possibleDefaultRef !== ref ? callback(ref, loadKind) : undefined, 507 projectReferenceProjectLoadKind, 508 project.projectService, 509 seenResolvedRefs 510 ); 511 512 function callback(ref: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) { 513 const configFileName = toNormalizedPath(ref.sourceFile.fileName); 514 const child = project.projectService.findConfiguredProjectByProjectName(configFileName) || ( 515 loadKind === ProjectReferenceProjectLoadKind.Find ? 516 undefined : 517 loadKind === ProjectReferenceProjectLoadKind.FindCreate ? 518 project.projectService.createConfiguredProject(configFileName) : 519 loadKind === ProjectReferenceProjectLoadKind.FindCreateLoad ? 520 project.projectService.createAndLoadConfiguredProject(configFileName, reason!) : 521 Debug.assertNever(loadKind) 522 ); 523 524 return child && cb(child); 525 } 526 } 527 528 function forEachResolvedProjectReferenceProjectWorker<T>( 529 resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[], 530 parentOptions: CompilerOptions, 531 cb: (resolvedRef: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) => T | undefined, 532 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, 533 projectService: ProjectService, 534 seenResolvedRefs: ESMap<string, ProjectReferenceProjectLoadKind> | undefined, 535 ): T | undefined { 536 const loadKind = parentOptions.disableReferencedProjectLoad ? ProjectReferenceProjectLoadKind.Find : projectReferenceProjectLoadKind; 537 return forEach(resolvedProjectReferences, ref => { 538 if (!ref) return undefined; 539 540 const configFileName = toNormalizedPath(ref.sourceFile.fileName); 541 const canonicalPath = projectService.toCanonicalFileName(configFileName); 542 const seenValue = seenResolvedRefs?.get(canonicalPath); 543 if (seenValue !== undefined && seenValue >= loadKind) { 544 return undefined; 545 } 546 const result = cb(ref, loadKind); 547 if (result) { 548 return result; 549 } 550 551 (seenResolvedRefs || (seenResolvedRefs = new Map())).set(canonicalPath, loadKind); 552 return ref.references && forEachResolvedProjectReferenceProjectWorker(ref.references, ref.commandLine.options, cb, loadKind, projectService, seenResolvedRefs); 553 }); 554 } 555 556 function forEachPotentialProjectReference<T>( 557 project: ConfiguredProject, 558 cb: (potentialProjectReference: Path) => T | undefined 559 ): T | undefined { 560 return project.potentialProjectReferences && 561 forEachKey(project.potentialProjectReferences, cb); 562 } 563 564 function forEachAnyProjectReferenceKind<T>( 565 project: ConfiguredProject, 566 cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined, 567 cbProjectRef: (projectReference: ProjectReference) => T | undefined, 568 cbPotentialProjectRef: (potentialProjectReference: Path) => T | undefined 569 ): T | undefined { 570 return project.getCurrentProgram() ? 571 project.forEachResolvedProjectReference(cb) : 572 project.isInitialLoadPending() ? 573 forEachPotentialProjectReference(project, cbPotentialProjectRef) : 574 forEach(project.getProjectReferences(), cbProjectRef); 575 } 576 577 function callbackRefProject<T>( 578 project: ConfiguredProject, 579 cb: (refProj: ConfiguredProject) => T | undefined, 580 refPath: Path | undefined 581 ) { 582 const refProject = refPath && project.projectService.configuredProjects.get(refPath); 583 return refProject && cb(refProject); 584 } 585 586 function forEachReferencedProject<T>( 587 project: ConfiguredProject, 588 cb: (refProj: ConfiguredProject) => T | undefined 589 ): T | undefined { 590 return forEachAnyProjectReferenceKind( 591 project, 592 resolvedRef => callbackRefProject(project, cb, resolvedRef.sourceFile.path), 593 projectRef => callbackRefProject(project, cb, project.toPath(resolveProjectReferencePath(projectRef))), 594 potentialProjectRef => callbackRefProject(project, cb, potentialProjectRef) 595 ); 596 } 597 598 interface ScriptInfoInNodeModulesWatcher extends FileWatcher { 599 refCount: number; 600 } 601 602 function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) { 603 return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; 604 } 605 606 function isScriptInfoWatchedFromNodeModules(info: ScriptInfo) { 607 return !info.isScriptOpen() && info.mTime !== undefined; 608 } 609 610 /*@internal*/ 611 /** true if script info is part of project and is not in project because it is referenced from project reference source */ 612 export function projectContainsInfoDirectly(project: Project, info: ScriptInfo) { 613 return project.containsScriptInfo(info) && 614 !project.isSourceOfProjectReferenceRedirect(info.path); 615 } 616 617 /*@internal*/ 618 export function updateProjectIfDirty(project: Project) { 619 project.invalidateResolutionsOfFailedLookupLocations(); 620 return project.dirty && project.updateGraph(); 621 } 622 623 function setProjectOptionsUsed(project: ConfiguredProject | ExternalProject) { 624 if (isConfiguredProject(project)) { 625 project.projectOptions = true; 626 } 627 } 628 629 /*@internal*/ 630 export interface OpenFileArguments { 631 fileName: string; 632 content?: string; 633 scriptKind?: protocol.ScriptKindName | ScriptKind; 634 hasMixedContent?: boolean; 635 projectRootPath?: string; 636 } 637 638 /*@internal*/ 639 export interface ChangeFileArguments { 640 fileName: string; 641 changes: Iterator<TextChange>; 642 } 643 644 export interface WatchOptionsAndErrors { 645 watchOptions: WatchOptions; 646 errors: Diagnostic[] | undefined; 647 } 648 649 export class ProjectService { 650 651 /*@internal*/ 652 readonly typingsCache: TypingsCache; 653 654 /*@internal*/ 655 readonly documentRegistry: DocumentRegistry; 656 657 /** 658 * Container of all known scripts 659 */ 660 /*@internal*/ 661 readonly filenameToScriptInfo = new Map<string, ScriptInfo>(); 662 private readonly scriptInfoInNodeModulesWatchers = new Map<string, ScriptInfoInNodeModulesWatcher>(); 663 /** 664 * Contains all the deleted script info's version information so that 665 * it does not reset when creating script info again 666 * (and could have potentially collided with version where contents mismatch) 667 */ 668 private readonly filenameToScriptInfoVersion = new Map<string, ScriptInfoVersion>(); 669 // Set of all '.js' files ever opened. 670 private readonly allJsFilesForOpenFileTelemetry = new Map<string, true>(); 671 672 /** 673 * Map to the real path of the infos 674 */ 675 /* @internal */ 676 readonly realpathToScriptInfos: MultiMap<Path, ScriptInfo> | undefined; 677 /** 678 * maps external project file name to list of config files that were the part of this project 679 */ 680 private readonly externalProjectToConfiguredProjectMap = new Map<string, NormalizedPath[]>(); 681 682 /** 683 * external projects (configuration and list of root files is not controlled by tsserver) 684 */ 685 readonly externalProjects: ExternalProject[] = []; 686 /** 687 * projects built from openFileRoots 688 */ 689 readonly inferredProjects: InferredProject[] = []; 690 /** 691 * projects specified by a tsconfig.json file 692 */ 693 readonly configuredProjects: Map<ConfiguredProject> = new Map<string, ConfiguredProject>(); 694 /** 695 * Open files: with value being project root path, and key being Path of the file that is open 696 */ 697 readonly openFiles: Map<NormalizedPath | undefined> = new Map<Path, NormalizedPath | undefined>(); 698 /* @internal */ 699 readonly configFileForOpenFiles: ESMap<Path, NormalizedPath | false> = new Map(); 700 /** 701 * Map of open files that are opened without complete path but have projectRoot as current directory 702 */ 703 private readonly openFilesWithNonRootedDiskPath = new Map<string, ScriptInfo>(); 704 705 private compilerOptionsForInferredProjects: CompilerOptions | undefined; 706 private compilerOptionsForInferredProjectsPerProjectRoot = new Map<string, CompilerOptions>(); 707 private watchOptionsForInferredProjects: WatchOptionsAndErrors | undefined; 708 private watchOptionsForInferredProjectsPerProjectRoot = new Map<string, WatchOptionsAndErrors | false>(); 709 private typeAcquisitionForInferredProjects: TypeAcquisition | undefined; 710 private typeAcquisitionForInferredProjectsPerProjectRoot = new Map<string, TypeAcquisition | undefined>(); 711 /** 712 * Project size for configured or external projects 713 */ 714 private readonly projectToSizeMap = new Map<string, number>(); 715 /** 716 * This is a map of config file paths existence that doesnt need query to disk 717 * - The entry can be present because there is inferred project that needs to watch addition of config file to directory 718 * In this case the exists could be true/false based on config file is present or not 719 * - Or it is present if we have configured project open with config file at that location 720 * In this case the exists property is always true 721 */ 722 private readonly configFileExistenceInfoCache = new Map<string, ConfigFileExistenceInfo>(); 723 /*@internal*/ readonly throttledOperations: ThrottledOperations; 724 725 private readonly hostConfiguration: HostConfiguration; 726 private safelist: SafeList = defaultTypeSafeList; 727 private readonly legacySafelist = new Map<string, string>(); 728 729 private pendingProjectUpdates = new Map<string, Project>(); 730 /* @internal */ 731 pendingEnsureProjectForOpenFiles = false; 732 733 readonly currentDirectory: NormalizedPath; 734 readonly toCanonicalFileName: (f: string) => string; 735 736 public readonly host: ServerHost; 737 public readonly logger: Logger; 738 public readonly cancellationToken: HostCancellationToken; 739 public readonly useSingleInferredProject: boolean; 740 public readonly useInferredProjectPerProjectRoot: boolean; 741 public readonly typingsInstaller: ITypingsInstaller; 742 private readonly globalCacheLocationDirectoryPath: Path | undefined; 743 public readonly throttleWaitMilliseconds?: number; 744 private readonly eventHandler?: ProjectServiceEventHandler; 745 private readonly suppressDiagnosticEvents?: boolean; 746 747 public readonly globalPlugins: readonly string[]; 748 public readonly pluginProbeLocations: readonly string[]; 749 public readonly allowLocalPluginLoads: boolean; 750 private currentPluginConfigOverrides: ESMap<string, any> | undefined; 751 752 public readonly typesMapLocation: string | undefined; 753 754 /** @deprecated use serverMode instead */ 755 public readonly syntaxOnly: boolean; 756 public readonly serverMode: LanguageServiceMode; 757 758 /** Tracks projects that we have already sent telemetry for. */ 759 private readonly seenProjects = new Map<string, true>(); 760 761 /*@internal*/ 762 readonly watchFactory: WatchFactory<WatchType, Project>; 763 764 /*@internal*/ 765 private readonly sharedExtendedConfigFileWatchers = new Map<Path, SharedExtendedConfigFileWatcher<NormalizedPath>>(); 766 767 /*@internal*/ 768 readonly packageJsonCache: PackageJsonCache; 769 /*@internal*/ 770 private packageJsonFilesMap: ESMap<Path, FileWatcher> | undefined; 771 772 773 private performanceEventHandler?: PerformanceEventHandler; 774 775 constructor(opts: ProjectServiceOptions) { 776 this.host = opts.host; 777 this.logger = opts.logger; 778 this.cancellationToken = opts.cancellationToken; 779 this.useSingleInferredProject = opts.useSingleInferredProject; 780 this.useInferredProjectPerProjectRoot = opts.useInferredProjectPerProjectRoot; 781 this.typingsInstaller = opts.typingsInstaller || nullTypingsInstaller; 782 this.throttleWaitMilliseconds = opts.throttleWaitMilliseconds; 783 this.eventHandler = opts.eventHandler; 784 this.suppressDiagnosticEvents = opts.suppressDiagnosticEvents; 785 this.globalPlugins = opts.globalPlugins || emptyArray; 786 this.pluginProbeLocations = opts.pluginProbeLocations || emptyArray; 787 this.allowLocalPluginLoads = !!opts.allowLocalPluginLoads; 788 this.typesMapLocation = (opts.typesMapLocation === undefined) ? combinePaths(getDirectoryPath(this.getExecutingFilePath()), "typesMap.json") : opts.typesMapLocation; 789 if (opts.serverMode !== undefined) { 790 this.serverMode = opts.serverMode; 791 this.syntaxOnly = this.serverMode === LanguageServiceMode.Syntactic; 792 } 793 else if (opts.syntaxOnly) { 794 this.serverMode = LanguageServiceMode.Syntactic; 795 this.syntaxOnly = true; 796 } 797 else { 798 this.serverMode = LanguageServiceMode.Semantic; 799 this.syntaxOnly = false; 800 } 801 802 if (this.host.realpath) { 803 this.realpathToScriptInfos = createMultiMap(); 804 } 805 this.currentDirectory = toNormalizedPath(this.host.getCurrentDirectory()); 806 this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); 807 this.globalCacheLocationDirectoryPath = this.typingsInstaller.globalTypingsCacheLocation 808 ? ensureTrailingDirectorySeparator(this.toPath(this.typingsInstaller.globalTypingsCacheLocation)) 809 : undefined; 810 this.throttledOperations = new ThrottledOperations(this.host, this.logger); 811 812 if (this.typesMapLocation) { 813 this.loadTypesMap(); 814 } 815 else { 816 this.logger.info("No types map provided; using the default"); 817 } 818 819 this.typingsInstaller.attach(this); 820 821 this.typingsCache = new TypingsCache(this.typingsInstaller); 822 823 this.hostConfiguration = { 824 formatCodeOptions: getDefaultFormatCodeSettings(this.host.newLine), 825 preferences: emptyOptions, 826 hostInfo: "Unknown host", 827 extraFileExtensions: [], 828 }; 829 830 this.documentRegistry = createDocumentRegistryInternal(this.host.useCaseSensitiveFileNames, this.currentDirectory, this); 831 const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : 832 this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; 833 const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; 834 this.packageJsonCache = createPackageJsonCache(this); 835 this.watchFactory = this.serverMode !== LanguageServiceMode.Semantic ? 836 { 837 watchFile: returnNoopFileWatcher, 838 watchDirectory: returnNoopFileWatcher, 839 } : 840 getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo); 841 } 842 843 toPath(fileName: string) { 844 return toPath(fileName, this.currentDirectory, this.toCanonicalFileName); 845 } 846 847 /*@internal*/ 848 getExecutingFilePath() { 849 return this.getNormalizedAbsolutePath(this.host.getExecutingFilePath()); 850 } 851 852 /*@internal*/ 853 getNormalizedAbsolutePath(fileName: string) { 854 return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory()); 855 } 856 857 /*@internal*/ 858 setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile) { 859 const info = Debug.checkDefined(this.getScriptInfoForPath(path)); 860 info.cacheSourceFile = { key, sourceFile }; 861 } 862 863 /*@internal*/ 864 getDocument(key: DocumentRegistryBucketKey, path: Path): SourceFile | undefined { 865 const info = this.getScriptInfoForPath(path); 866 return info && info.cacheSourceFile && info.cacheSourceFile.key === key ? info.cacheSourceFile.sourceFile : undefined; 867 } 868 869 /* @internal */ 870 ensureInferredProjectsUpToDate_TestOnly() { 871 this.ensureProjectStructuresUptoDate(); 872 } 873 874 /* @internal */ 875 getCompilerOptionsForInferredProjects() { 876 return this.compilerOptionsForInferredProjects; 877 } 878 879 /* @internal */ 880 onUpdateLanguageServiceStateForProject(project: Project, languageServiceEnabled: boolean) { 881 if (!this.eventHandler) { 882 return; 883 } 884 const event: ProjectLanguageServiceStateEvent = { 885 eventName: ProjectLanguageServiceStateEvent, 886 data: { project, languageServiceEnabled } 887 }; 888 this.eventHandler(event); 889 } 890 891 private loadTypesMap() { 892 try { 893 const fileContent = this.host.readFile(this.typesMapLocation!); // TODO: GH#18217 894 if (fileContent === undefined) { 895 this.logger.info(`Provided types map file "${this.typesMapLocation}" doesn't exist`); 896 return; 897 } 898 const raw: TypesMapFile = JSON.parse(fileContent); 899 // Parse the regexps 900 for (const k of Object.keys(raw.typesMap)) { 901 raw.typesMap[k].match = new RegExp(raw.typesMap[k].match as {} as string, "i"); 902 } 903 // raw is now fixed and ready 904 this.safelist = raw.typesMap; 905 for (const key in raw.simpleMap) { 906 if (raw.simpleMap.hasOwnProperty(key)) { 907 this.legacySafelist.set(key, raw.simpleMap[key].toLowerCase()); 908 } 909 } 910 } 911 catch (e) { 912 this.logger.info(`Error loading types map: ${e}`); 913 this.safelist = defaultTypeSafeList; 914 this.legacySafelist.clear(); 915 } 916 } 917 918 updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void; 919 /** @internal */ 920 updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void; // eslint-disable-line @typescript-eslint/unified-signatures 921 updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void { 922 const project = this.findProject(response.projectName); 923 if (!project) { 924 return; 925 } 926 switch (response.kind) { 927 case ActionSet: 928 // Update the typing files and update the project 929 project.updateTypingFiles(this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typeAcquisition, response.unresolvedImports, response.typings)); 930 return; 931 case ActionInvalidate: 932 // Do not clear resolution cache, there was changes detected in typings, so enque typing request and let it get us correct results 933 this.typingsCache.enqueueInstallTypingsForProject(project, project.lastCachedUnresolvedImportsList, /*forceRefresh*/ true); 934 return; 935 } 936 } 937 938 /*@internal*/ 939 delayEnsureProjectForOpenFiles() { 940 if (!this.openFiles.size) return; 941 this.pendingEnsureProjectForOpenFiles = true; 942 this.throttledOperations.schedule(ensureProjectForOpenFileSchedule, /*delay*/ 2500, () => { 943 if (this.pendingProjectUpdates.size !== 0) { 944 this.delayEnsureProjectForOpenFiles(); 945 } 946 else { 947 if (this.pendingEnsureProjectForOpenFiles) { 948 this.ensureProjectForOpenFiles(); 949 950 // Send the event to notify that there were background project updates 951 // send current list of open files 952 this.sendProjectsUpdatedInBackgroundEvent(); 953 } 954 } 955 }); 956 } 957 958 private delayUpdateProjectGraph(project: Project) { 959 project.markAsDirty(); 960 const projectName = project.getProjectName(); 961 this.pendingProjectUpdates.set(projectName, project); 962 this.throttledOperations.schedule(projectName, /*delay*/ 250, () => { 963 if (this.pendingProjectUpdates.delete(projectName)) { 964 updateProjectIfDirty(project); 965 } 966 }); 967 } 968 969 /*@internal*/ 970 hasPendingProjectUpdate(project: Project) { 971 return this.pendingProjectUpdates.has(project.getProjectName()); 972 } 973 974 /* @internal */ 975 sendProjectsUpdatedInBackgroundEvent() { 976 if (!this.eventHandler) { 977 return; 978 } 979 980 const event: ProjectsUpdatedInBackgroundEvent = { 981 eventName: ProjectsUpdatedInBackgroundEvent, 982 data: { 983 openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path)!.fileName) 984 } 985 }; 986 this.eventHandler(event); 987 } 988 989 /* @internal */ 990 sendLargeFileReferencedEvent(file: string, fileSize: number) { 991 if (!this.eventHandler) { 992 return; 993 } 994 995 const event: LargeFileReferencedEvent = { 996 eventName: LargeFileReferencedEvent, 997 data: { file, fileSize, maxFileSize } 998 }; 999 this.eventHandler(event); 1000 } 1001 1002 /* @internal */ 1003 sendProjectLoadingStartEvent(project: ConfiguredProject, reason: string) { 1004 if (!this.eventHandler) { 1005 return; 1006 } 1007 project.sendLoadingProjectFinish = true; 1008 const event: ProjectLoadingStartEvent = { 1009 eventName: ProjectLoadingStartEvent, 1010 data: { project, reason } 1011 }; 1012 this.eventHandler(event); 1013 } 1014 1015 /* @internal */ 1016 sendProjectLoadingFinishEvent(project: ConfiguredProject) { 1017 if (!this.eventHandler || !project.sendLoadingProjectFinish) { 1018 return; 1019 } 1020 1021 project.sendLoadingProjectFinish = false; 1022 const event: ProjectLoadingFinishEvent = { 1023 eventName: ProjectLoadingFinishEvent, 1024 data: { project } 1025 }; 1026 this.eventHandler(event); 1027 } 1028 1029 /* @internal */ 1030 sendPerformanceEvent(kind: PerformanceEvent["kind"], durationMs: number) { 1031 if (this.performanceEventHandler) { 1032 this.performanceEventHandler({ kind, durationMs }); 1033 } 1034 } 1035 1036 /* @internal */ 1037 delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project: Project) { 1038 this.delayUpdateProjectGraph(project); 1039 this.delayEnsureProjectForOpenFiles(); 1040 } 1041 1042 private delayUpdateProjectGraphs(projects: readonly Project[], clearSourceMapperCache: boolean) { 1043 if (projects.length) { 1044 for (const project of projects) { 1045 // Even if program doesnt change, clear the source mapper cache 1046 if (clearSourceMapperCache) project.clearSourceMapperCache(); 1047 this.delayUpdateProjectGraph(project); 1048 } 1049 this.delayEnsureProjectForOpenFiles(); 1050 } 1051 } 1052 1053 setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.InferredProjectCompilerOptions, projectRootPath?: string): void { 1054 Debug.assert(projectRootPath === undefined || this.useInferredProjectPerProjectRoot, "Setting compiler options per project root path is only supported when useInferredProjectPerProjectRoot is enabled"); 1055 1056 const compilerOptions = convertCompilerOptions(projectCompilerOptions); 1057 const watchOptions = convertWatchOptions(projectCompilerOptions, projectRootPath); 1058 const typeAcquisition = convertTypeAcquisition(projectCompilerOptions); 1059 1060 // always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside 1061 // previously we did not expose a way for user to change these settings and this option was enabled by default 1062 compilerOptions.allowNonTsExtensions = true; 1063 const canonicalProjectRootPath = projectRootPath && this.toCanonicalFileName(projectRootPath); 1064 if (canonicalProjectRootPath) { 1065 this.compilerOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, compilerOptions); 1066 this.watchOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, watchOptions || false); 1067 this.typeAcquisitionForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, typeAcquisition); 1068 } 1069 else { 1070 this.compilerOptionsForInferredProjects = compilerOptions; 1071 this.watchOptionsForInferredProjects = watchOptions; 1072 this.typeAcquisitionForInferredProjects = typeAcquisition; 1073 } 1074 1075 for (const project of this.inferredProjects) { 1076 // Only update compiler options in the following cases: 1077 // - Inferred projects without a projectRootPath, if the new options do not apply to 1078 // a workspace root 1079 // - Inferred projects with a projectRootPath, if the new options do not apply to a 1080 // workspace root and there is no more specific set of options for that project's 1081 // root path 1082 // - Inferred projects with a projectRootPath, if the new options apply to that 1083 // project root path. 1084 if (canonicalProjectRootPath ? 1085 project.projectRootPath === canonicalProjectRootPath : 1086 !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) { 1087 project.setCompilerOptions(compilerOptions); 1088 project.setTypeAcquisition(typeAcquisition); 1089 project.setWatchOptions(watchOptions?.watchOptions); 1090 project.setProjectErrors(watchOptions?.errors); 1091 project.compileOnSaveEnabled = compilerOptions.compileOnSave!; 1092 project.markAsDirty(); 1093 this.delayUpdateProjectGraph(project); 1094 } 1095 } 1096 1097 this.delayEnsureProjectForOpenFiles(); 1098 } 1099 1100 findProject(projectName: string): Project | undefined { 1101 if (projectName === undefined) { 1102 return undefined; 1103 } 1104 if (isInferredProjectName(projectName)) { 1105 return findProjectByName(projectName, this.inferredProjects); 1106 } 1107 return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); 1108 } 1109 1110 /* @internal */ 1111 private forEachProject(cb: (project: Project) => void) { 1112 this.externalProjects.forEach(cb); 1113 this.configuredProjects.forEach(cb); 1114 this.inferredProjects.forEach(cb); 1115 } 1116 1117 /* @internal */ 1118 forEachEnabledProject(cb: (project: Project) => void) { 1119 this.forEachProject(project => { 1120 if (!project.isOrphan() && project.languageServiceEnabled) { 1121 cb(project); 1122 } 1123 }); 1124 } 1125 1126 getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined { 1127 return ensureProject ? this.ensureDefaultProjectForFile(fileName) : this.tryGetDefaultProjectForFile(fileName); 1128 } 1129 1130 /* @internal */ 1131 tryGetDefaultProjectForFile(fileName: NormalizedPath): Project | undefined { 1132 const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); 1133 return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : undefined; 1134 } 1135 1136 /* @internal */ 1137 ensureDefaultProjectForFile(fileName: NormalizedPath): Project { 1138 return this.tryGetDefaultProjectForFile(fileName) || this.doEnsureDefaultProjectForFile(fileName); 1139 } 1140 1141 private doEnsureDefaultProjectForFile(fileName: NormalizedPath): Project { 1142 this.ensureProjectStructuresUptoDate(); 1143 const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); 1144 return scriptInfo ? scriptInfo.getDefaultProject() : (this.logErrorForScriptInfoNotFound(fileName), Errors.ThrowNoProject()); 1145 } 1146 1147 getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) { 1148 this.ensureProjectStructuresUptoDate(); 1149 return this.getScriptInfo(uncheckedFileName); 1150 } 1151 1152 /** 1153 * Ensures the project structures are upto date 1154 * This means, 1155 * - we go through all the projects and update them if they are dirty 1156 * - if updates reflect some change in structure or there was pending request to ensure projects for open files 1157 * ensure that each open script info has project 1158 */ 1159 private ensureProjectStructuresUptoDate() { 1160 let hasChanges = this.pendingEnsureProjectForOpenFiles; 1161 this.pendingProjectUpdates.clear(); 1162 const updateGraph = (project: Project) => { 1163 hasChanges = updateProjectIfDirty(project) || hasChanges; 1164 }; 1165 1166 this.externalProjects.forEach(updateGraph); 1167 this.configuredProjects.forEach(updateGraph); 1168 this.inferredProjects.forEach(updateGraph); 1169 if (hasChanges) { 1170 this.ensureProjectForOpenFiles(); 1171 } 1172 } 1173 1174 getFormatCodeOptions(file: NormalizedPath) { 1175 const info = this.getScriptInfoForNormalizedPath(file); 1176 return info && info.getFormatCodeSettings() || this.hostConfiguration.formatCodeOptions; 1177 } 1178 1179 getPreferences(file: NormalizedPath): protocol.UserPreferences { 1180 const info = this.getScriptInfoForNormalizedPath(file); 1181 return { ...this.hostConfiguration.preferences, ...info && info.getPreferences() }; 1182 } 1183 1184 getHostFormatCodeOptions(): FormatCodeSettings { 1185 return this.hostConfiguration.formatCodeOptions; 1186 } 1187 1188 getHostPreferences(): protocol.UserPreferences { 1189 return this.hostConfiguration.preferences; 1190 } 1191 1192 private onSourceFileChanged(info: ScriptInfo, eventKind: FileWatcherEventKind) { 1193 if (info.containingProjects) { 1194 info.containingProjects.forEach(project => project.resolutionCache.removeResolutionsFromProjectReferenceRedirects(info.path)); 1195 } 1196 if (eventKind === FileWatcherEventKind.Deleted) { 1197 // File was deleted 1198 this.handleDeletedFile(info); 1199 } 1200 else if (!info.isScriptOpen()) { 1201 // file has been changed which might affect the set of referenced files in projects that include 1202 // this file and set of inferred projects 1203 info.delayReloadNonMixedContentFile(); 1204 this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ false); 1205 this.handleSourceMapProjects(info); 1206 } 1207 } 1208 1209 private handleSourceMapProjects(info: ScriptInfo) { 1210 // Change in d.ts, update source projects as well 1211 if (info.sourceMapFilePath) { 1212 if (isString(info.sourceMapFilePath)) { 1213 const sourceMapFileInfo = this.getScriptInfoForPath(info.sourceMapFilePath); 1214 this.delayUpdateSourceInfoProjects(sourceMapFileInfo && sourceMapFileInfo.sourceInfos); 1215 } 1216 else { 1217 this.delayUpdateSourceInfoProjects(info.sourceMapFilePath.sourceInfos); 1218 } 1219 } 1220 // Change in mapInfo, update declarationProjects and source projects 1221 this.delayUpdateSourceInfoProjects(info.sourceInfos); 1222 if (info.declarationInfoPath) { 1223 this.delayUpdateProjectsOfScriptInfoPath(info.declarationInfoPath); 1224 } 1225 } 1226 1227 private delayUpdateSourceInfoProjects(sourceInfos: Set<Path> | undefined) { 1228 if (sourceInfos) { 1229 sourceInfos.forEach((_value, path) => this.delayUpdateProjectsOfScriptInfoPath(path)); 1230 } 1231 } 1232 1233 private delayUpdateProjectsOfScriptInfoPath(path: Path) { 1234 const info = this.getScriptInfoForPath(path); 1235 if (info) { 1236 this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ true); 1237 } 1238 } 1239 1240 private handleDeletedFile(info: ScriptInfo) { 1241 this.stopWatchingScriptInfo(info); 1242 1243 if (!info.isScriptOpen()) { 1244 this.deleteScriptInfo(info); 1245 1246 // capture list of projects since detachAllProjects will wipe out original list 1247 const containingProjects = info.containingProjects.slice(); 1248 1249 info.detachAllProjects(); 1250 1251 // update projects to make sure that set of referenced files is correct 1252 this.delayUpdateProjectGraphs(containingProjects, /*clearSourceMapperCache*/ false); 1253 this.handleSourceMapProjects(info); 1254 info.closeSourceMapFileWatcher(); 1255 // need to recalculate source map from declaration file 1256 if (info.declarationInfoPath) { 1257 const declarationInfo = this.getScriptInfoForPath(info.declarationInfoPath); 1258 if (declarationInfo) { 1259 declarationInfo.sourceMapFilePath = undefined; 1260 } 1261 } 1262 } 1263 } 1264 1265 /** 1266 * This is to watch whenever files are added or removed to the wildcard directories 1267 */ 1268 /*@internal*/ 1269 watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, project: ConfiguredProject) { 1270 const watchOptions = this.getWatchOptions(project); 1271 return this.watchFactory.watchDirectory( 1272 directory, 1273 fileOrDirectory => { 1274 const fileOrDirectoryPath = this.toPath(fileOrDirectory); 1275 const fsResult = project.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); 1276 const configFileName = project.getConfigFilePath(); 1277 if (getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) && 1278 (fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectoryPath)) 1279 ) { 1280 this.logger.info(`Project: ${configFileName} Detected new package.json: ${fileOrDirectory}`); 1281 this.onAddPackageJson(fileOrDirectoryPath); 1282 } 1283 1284 if (isIgnoredFileFromWildCardWatching({ 1285 watchedDirPath: directory, 1286 fileOrDirectory, 1287 fileOrDirectoryPath, 1288 configFileName, 1289 extraFileExtensions: this.hostConfiguration.extraFileExtensions, 1290 currentDirectory: this.currentDirectory, 1291 options: project.getCompilationSettings(), 1292 program: project.getCurrentProgram(), 1293 useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames, 1294 writeLog: s => this.logger.info(s) 1295 })) return; 1296 1297 // don't trigger callback on open, existing files 1298 if (project.fileIsOpen(fileOrDirectoryPath)) { 1299 if (project.pendingReload !== ConfigFileProgramReloadLevel.Full) { 1300 const info = Debug.checkDefined(this.getScriptInfoForPath(fileOrDirectoryPath)); 1301 if (info.isAttached(project)) { 1302 project.openFileWatchTriggered.set(fileOrDirectoryPath, true); 1303 } 1304 else { 1305 project.pendingReload = ConfigFileProgramReloadLevel.Partial; 1306 this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project); 1307 } 1308 } 1309 return; 1310 } 1311 1312 // Reload is pending, do the reload 1313 if (project.pendingReload !== ConfigFileProgramReloadLevel.Full) { 1314 project.pendingReload = ConfigFileProgramReloadLevel.Partial; 1315 this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project); 1316 } 1317 }, 1318 flags, 1319 watchOptions, 1320 WatchType.WildcardDirectory, 1321 project 1322 ); 1323 } 1324 1325 /** Gets the config file existence info for the configured project */ 1326 /*@internal*/ 1327 getConfigFileExistenceInfo(project: ConfiguredProject) { 1328 return this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath)!; 1329 } 1330 1331 /*@internal*/ 1332 onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { 1333 const configFileExistenceInfo = this.getConfigFileExistenceInfo(project); 1334 if (eventKind === FileWatcherEventKind.Deleted) { 1335 // Update the cached status 1336 // We arent updating or removing the cached config file presence info as that will be taken care of by 1337 // setConfigFilePresenceByClosedConfigFile when the project is closed (depending on tracking open files) 1338 configFileExistenceInfo.exists = false; 1339 this.removeProject(project); 1340 1341 // Reload the configured projects for the open files in the map as they are affected by this config file 1342 // Since the configured project was deleted, we want to reload projects for all the open files including files 1343 // that are not root of the inferred project 1344 this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles); 1345 this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false); 1346 } 1347 else { 1348 this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles); 1349 // Skip refresh if project is not yet loaded 1350 if (project.isInitialLoadPending()) return; 1351 project.pendingReload = ConfigFileProgramReloadLevel.Full; 1352 project.pendingReloadReason = "Change in config file detected"; 1353 this.delayUpdateProjectGraph(project); 1354 // As we scheduled the update on configured project graph, 1355 // we would need to schedule the project reload for only the root of inferred projects 1356 this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ true); 1357 } 1358 } 1359 1360 /*@internal*/ 1361 updateSharedExtendedConfigFileMap({ canonicalConfigFilePath }: ConfiguredProject, parsedCommandLine: ParsedCommandLine) { 1362 updateSharedExtendedConfigFileWatcher( 1363 canonicalConfigFilePath, 1364 parsedCommandLine, 1365 this.sharedExtendedConfigFileWatchers, 1366 (extendedConfigFileName, extendedConfigFilePath) => this.watchFactory.watchFile( 1367 extendedConfigFileName, 1368 () => { 1369 let ensureProjectsForOpenFiles = false; 1370 this.sharedExtendedConfigFileWatchers.get(extendedConfigFilePath)?.projects.forEach(canonicalPath => { 1371 const project = this.configuredProjects.get(canonicalPath); 1372 // Skip refresh if project is not yet loaded 1373 if (!project || project.isInitialLoadPending()) return; 1374 project.pendingReload = ConfigFileProgramReloadLevel.Full; 1375 project.pendingReloadReason = `Change in extended config file ${extendedConfigFileName} detected`; 1376 this.delayUpdateProjectGraph(project); 1377 ensureProjectsForOpenFiles = true; 1378 }); 1379 if (ensureProjectsForOpenFiles) this.delayEnsureProjectForOpenFiles(); 1380 }, 1381 PollingInterval.High, 1382 this.hostConfiguration.watchOptions, 1383 WatchType.ExtendedConfigFile 1384 ), 1385 fileName => this.toPath(fileName), 1386 ); 1387 } 1388 1389 /*@internal*/ 1390 removeProjectFromSharedExtendedConfigFileMap(project: ConfiguredProject) { 1391 this.sharedExtendedConfigFileWatchers.forEach(watcher => { 1392 watcher.projects.delete(project.canonicalConfigFilePath); 1393 watcher.close(); 1394 }); 1395 } 1396 1397 /** 1398 * This is the callback function for the config file add/remove/change at any location 1399 * that matters to open script info but doesnt have configured project open 1400 * for the config file 1401 */ 1402 private onConfigFileChangeForOpenScriptInfo(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) { 1403 // This callback is called only if we dont have config file project for this config file 1404 const canonicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); 1405 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigPath)!; 1406 configFileExistenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); 1407 this.logConfigFileWatchUpdate(configFileName, canonicalConfigPath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles); 1408 1409 // Because there is no configured project open for the config file, the tracking open files map 1410 // will only have open files that need the re-detection of the project and hence 1411 // reload projects for all the tracking open files in the map 1412 this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false); 1413 } 1414 1415 private removeProject(project: Project) { 1416 this.logger.info("`remove Project::"); 1417 project.print(/*writeProjectFileNames*/ true); 1418 1419 project.close(); 1420 if (Debug.shouldAssert(AssertionLevel.Normal)) { 1421 this.filenameToScriptInfo.forEach(info => Debug.assert( 1422 !info.isAttached(project), 1423 "Found script Info still attached to project", 1424 () => `${project.projectName}: ScriptInfos still attached: ${JSON.stringify( 1425 arrayFrom( 1426 mapDefinedIterator( 1427 this.filenameToScriptInfo.values(), 1428 info => info.isAttached(project) ? 1429 { 1430 fileName: info.fileName, 1431 projects: info.containingProjects.map(p => p.projectName), 1432 hasMixedContent: info.hasMixedContent 1433 } : undefined 1434 ) 1435 ), 1436 /*replacer*/ undefined, 1437 " " 1438 )}`)); 1439 } 1440 // Remove the project from pending project updates 1441 this.pendingProjectUpdates.delete(project.getProjectName()); 1442 1443 switch (project.projectKind) { 1444 case ProjectKind.External: 1445 unorderedRemoveItem(this.externalProjects, <ExternalProject>project); 1446 this.projectToSizeMap.delete(project.getProjectName()); 1447 break; 1448 case ProjectKind.Configured: 1449 this.configuredProjects.delete((<ConfiguredProject>project).canonicalConfigFilePath); 1450 this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); 1451 this.setConfigFileExistenceInfoByClosedConfiguredProject(<ConfiguredProject>project); 1452 break; 1453 case ProjectKind.Inferred: 1454 unorderedRemoveItem(this.inferredProjects, <InferredProject>project); 1455 break; 1456 } 1457 } 1458 1459 /*@internal*/ 1460 assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) { 1461 Debug.assert(info.isOrphan()); 1462 1463 const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) || 1464 this.getOrCreateSingleInferredProjectIfEnabled() || 1465 this.getOrCreateSingleInferredWithoutProjectRoot( 1466 info.isDynamic ? 1467 projectRootPath || this.currentDirectory : 1468 getDirectoryPath( 1469 isRootedDiskPath(info.fileName) ? 1470 info.fileName : 1471 getNormalizedAbsolutePath( 1472 info.fileName, 1473 projectRootPath ? 1474 this.getNormalizedAbsolutePath(projectRootPath) : 1475 this.currentDirectory 1476 ) 1477 ) 1478 ); 1479 1480 project.addRoot(info); 1481 if (info.containingProjects[0] !== project) { 1482 // Ensure this is first project, we could be in this scenario because info could be part of orphan project 1483 info.detachFromProject(project); 1484 info.containingProjects.unshift(project); 1485 } 1486 project.updateGraph(); 1487 1488 if (!this.useSingleInferredProject && !project.projectRootPath) { 1489 // Note that we need to create a copy of the array since the list of project can change 1490 for (const inferredProject of this.inferredProjects) { 1491 if (inferredProject === project || inferredProject.isOrphan()) { 1492 continue; 1493 } 1494 1495 // Remove the inferred project if the root of it is now part of newly created inferred project 1496 // e.g through references 1497 // Which means if any root of inferred project is part of more than 1 project can be removed 1498 // This logic is same as iterating over all open files and calling 1499 // this.removeRootOfInferredProjectIfNowPartOfOtherProject(f); 1500 // Since this is also called from refreshInferredProject and closeOpen file 1501 // to update inferred projects of the open file, this iteration might be faster 1502 // instead of scanning all open files 1503 const roots = inferredProject.getRootScriptInfos(); 1504 Debug.assert(roots.length === 1 || !!inferredProject.projectRootPath); 1505 if (roots.length === 1 && forEach(roots[0].containingProjects, p => p !== roots[0].containingProjects[0] && !p.isOrphan())) { 1506 inferredProject.removeFile(roots[0], /*fileExists*/ true, /*detachFromProject*/ true); 1507 } 1508 } 1509 } 1510 1511 return project; 1512 } 1513 1514 private assignOrphanScriptInfosToInferredProject() { 1515 // collect orphaned files and assign them to inferred project just like we treat open of a file 1516 this.openFiles.forEach((projectRootPath, path) => { 1517 const info = this.getScriptInfoForPath(path as Path)!; 1518 // collect all orphaned script infos from open files 1519 if (info.isOrphan()) { 1520 this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); 1521 } 1522 }); 1523 } 1524 1525 /** 1526 * Remove this file from the set of open, non-configured files. 1527 * @param info The file that has been closed or newly configured 1528 */ 1529 private closeOpenFile(info: ScriptInfo, skipAssignOrphanScriptInfosToInferredProject?: true) { 1530 // Closing file should trigger re-reading the file content from disk. This is 1531 // because the user may chose to discard the buffer content before saving 1532 // to the disk, and the server's version of the file can be out of sync. 1533 const fileExists = info.isDynamic ? false : this.host.fileExists(info.fileName); 1534 info.close(fileExists); 1535 this.stopWatchingConfigFilesForClosedScriptInfo(info); 1536 1537 const canonicalFileName = this.toCanonicalFileName(info.fileName); 1538 if (this.openFilesWithNonRootedDiskPath.get(canonicalFileName) === info) { 1539 this.openFilesWithNonRootedDiskPath.delete(canonicalFileName); 1540 } 1541 1542 // collect all projects that should be removed 1543 let ensureProjectsForOpenFiles = false; 1544 for (const p of info.containingProjects) { 1545 if (isConfiguredProject(p)) { 1546 if (info.hasMixedContent) { 1547 info.registerFileUpdate(); 1548 } 1549 // Do not remove the project so that we can reuse this project 1550 // if it would need to be re-created with next file open 1551 1552 // If project had open file affecting 1553 // Reload the root Files from config if its not already scheduled 1554 if (p.openFileWatchTriggered.has(info.path)) { 1555 p.openFileWatchTriggered.delete(info.path); 1556 if (!p.pendingReload) { 1557 p.pendingReload = ConfigFileProgramReloadLevel.Partial; 1558 p.markFileAsDirty(info.path); 1559 } 1560 } 1561 } 1562 else if (isInferredProject(p) && p.isRoot(info)) { 1563 // If this was the last open root file of inferred project 1564 if (p.isProjectWithSingleRoot()) { 1565 ensureProjectsForOpenFiles = true; 1566 } 1567 1568 p.removeFile(info, fileExists, /*detachFromProject*/ true); 1569 // Do not remove the project even if this was last root of the inferred project 1570 // so that we can reuse this project, if it would need to be re-created with next file open 1571 } 1572 1573 if (!p.languageServiceEnabled) { 1574 // if project language service is disabled then we create a program only for open files. 1575 // this means that project should be marked as dirty to force rebuilding of the program 1576 // on the next request 1577 p.markAsDirty(); 1578 } 1579 } 1580 1581 this.openFiles.delete(info.path); 1582 this.configFileForOpenFiles.delete(info.path); 1583 1584 if (!skipAssignOrphanScriptInfosToInferredProject && ensureProjectsForOpenFiles) { 1585 this.assignOrphanScriptInfosToInferredProject(); 1586 } 1587 1588 // Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project) 1589 // is postponed to next file open so that if file from same project is opened, 1590 // we wont end up creating same script infos 1591 1592 // If the current info is being just closed - add the watcher file to track changes 1593 // But if file was deleted, handle that part 1594 if (fileExists) { 1595 this.watchClosedScriptInfo(info); 1596 } 1597 else { 1598 this.handleDeletedFile(info); 1599 } 1600 1601 return ensureProjectsForOpenFiles; 1602 } 1603 1604 private deleteScriptInfo(info: ScriptInfo) { 1605 this.filenameToScriptInfo.delete(info.path); 1606 this.filenameToScriptInfoVersion.set(info.path, info.getVersion()); 1607 const realpath = info.getRealpathIfDifferent(); 1608 if (realpath) { 1609 this.realpathToScriptInfos!.remove(realpath, info); // TODO: GH#18217 1610 } 1611 } 1612 1613 private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: OpenScriptInfoOrClosedOrConfigFileInfo) { 1614 let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1615 if (configFileExistenceInfo) { 1616 // By default the info would get impacted by presence of config file since its in the detection path 1617 // Only adding the info as a root to inferred project will need the existence to be watched by file watcher 1618 if (isOpenScriptInfo(info) && !configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { 1619 configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false); 1620 this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd); 1621 } 1622 return configFileExistenceInfo.exists; 1623 } 1624 1625 // Theoretically we should be adding watch for the directory here itself. 1626 // In practice there will be very few scenarios where the config file gets added 1627 // somewhere inside the another config file directory. 1628 // And technically we could handle that case in configFile's directory watcher in some cases 1629 // But given that its a rare scenario it seems like too much overhead. (we werent watching those directories earlier either) 1630 1631 // So what we are now watching is: configFile if the configured project corresponding to it is open 1632 // Or the whole chain of config files for the roots of the inferred projects 1633 1634 // Cache the host value of file exists and add the info to map of open files impacted by this config file 1635 const exists = this.host.fileExists(configFileName); 1636 const openFilesImpactedByConfigFile = new Map<Path, boolean>(); 1637 if (isOpenScriptInfo(info)) { 1638 openFilesImpactedByConfigFile.set(info.path, false); 1639 } 1640 configFileExistenceInfo = { exists, openFilesImpactedByConfigFile }; 1641 this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); 1642 this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd); 1643 return exists; 1644 } 1645 1646 private setConfigFileExistenceByNewConfiguredProject(project: ConfiguredProject) { 1647 const configFileExistenceInfo = this.getConfigFileExistenceInfo(project); 1648 if (configFileExistenceInfo) { 1649 // The existence might not be set if the file watcher is not invoked by the time config project is created by external project 1650 configFileExistenceInfo.exists = true; 1651 // close existing watcher 1652 if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { 1653 const configFileName = project.getConfigFilePath(); 1654 configFileExistenceInfo.configFileWatcherForRootOfInferredProject.close(); 1655 configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; 1656 this.logConfigFileWatchUpdate(configFileName, project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); 1657 } 1658 } 1659 else { 1660 // We could be in this scenario if project is the configured project tracked by external project 1661 // Since that route doesnt check if the config file is present or not 1662 this.configFileExistenceInfoCache.set(project.canonicalConfigFilePath, { 1663 exists: true, 1664 openFilesImpactedByConfigFile: new Map<Path, boolean>() 1665 }); 1666 } 1667 } 1668 1669 /** 1670 * Returns true if the configFileExistenceInfo is needed/impacted by open files that are root of inferred project 1671 */ 1672 private configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo: ConfigFileExistenceInfo) { 1673 return forEachEntry(configFileExistenceInfo.openFilesImpactedByConfigFile, (isRootOfInferredProject) => isRootOfInferredProject); 1674 } 1675 1676 private setConfigFileExistenceInfoByClosedConfiguredProject(closedProject: ConfiguredProject) { 1677 const configFileExistenceInfo = this.getConfigFileExistenceInfo(closedProject); 1678 Debug.assert(!!configFileExistenceInfo); 1679 if (configFileExistenceInfo.openFilesImpactedByConfigFile.size) { 1680 const configFileName = closedProject.getConfigFilePath(); 1681 // If there are open files that are impacted by this config file existence 1682 // but none of them are root of inferred project, the config file watcher will be 1683 // created when any of the script infos are added as root of inferred project 1684 if (this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { 1685 Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); 1686 this.createConfigFileWatcherOfConfigFileExistence(configFileName, closedProject.canonicalConfigFilePath, configFileExistenceInfo); 1687 } 1688 } 1689 else { 1690 // There is not a single file open thats tracking the status of this config file. Remove from cache 1691 this.configFileExistenceInfoCache.delete(closedProject.canonicalConfigFilePath); 1692 } 1693 } 1694 1695 private logConfigFileWatchUpdate(configFileName: NormalizedPath, canonicalConfigFilePath: string, configFileExistenceInfo: ConfigFileExistenceInfo, status: ConfigFileWatcherStatus) { 1696 if (!this.logger.hasLevel(LogLevel.verbose)) { 1697 return; 1698 } 1699 const inferredRoots: string[] = []; 1700 const otherFiles: string[] = []; 1701 configFileExistenceInfo.openFilesImpactedByConfigFile.forEach((isRootOfInferredProject, key) => { 1702 const info = this.getScriptInfoForPath(key)!; 1703 (isRootOfInferredProject ? inferredRoots : otherFiles).push(info.fileName); 1704 }); 1705 1706 const watches: WatchType[] = []; 1707 if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { 1708 watches.push( 1709 configFileExistenceInfo.configFileWatcherForRootOfInferredProject === noopFileWatcher ? 1710 WatchType.NoopConfigFileForInferredRoot : 1711 WatchType.ConfigFileForInferredRoot 1712 ); 1713 } 1714 if (this.configuredProjects.has(canonicalConfigFilePath)) { 1715 watches.push(WatchType.ConfigFile); 1716 } 1717 this.logger.info(`ConfigFilePresence:: Current Watches: ${watches}:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`); 1718 } 1719 1720 /** 1721 * Create the watcher for the configFileExistenceInfo 1722 */ 1723 private createConfigFileWatcherOfConfigFileExistence( 1724 configFileName: NormalizedPath, 1725 canonicalConfigFilePath: string, 1726 configFileExistenceInfo: ConfigFileExistenceInfo 1727 ) { 1728 configFileExistenceInfo.configFileWatcherForRootOfInferredProject = 1729 canWatchDirectory(getDirectoryPath(canonicalConfigFilePath) as Path) ? 1730 this.watchFactory.watchFile( 1731 configFileName, 1732 (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind), 1733 PollingInterval.High, 1734 this.hostConfiguration.watchOptions, 1735 WatchType.ConfigFileForInferredRoot 1736 ) : 1737 noopFileWatcher; 1738 this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); 1739 } 1740 1741 /** 1742 * Close the config file watcher in the cached ConfigFileExistenceInfo 1743 * if there arent any open files that are root of inferred project 1744 */ 1745 private closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo: ConfigFileExistenceInfo) { 1746 // Close the config file watcher if there are no more open files that are root of inferred project 1747 if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject && 1748 !this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { 1749 configFileExistenceInfo.configFileWatcherForRootOfInferredProject.close(); 1750 configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; 1751 } 1752 } 1753 1754 /** 1755 * This is called on file close, so that we stop watching the config file for this script info 1756 */ 1757 private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) { 1758 Debug.assert(!info.isScriptOpen()); 1759 this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { 1760 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1761 if (configFileExistenceInfo) { 1762 const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile.get(info.path); 1763 1764 // Delete the info from map, since this file is no more open 1765 configFileExistenceInfo.openFilesImpactedByConfigFile.delete(info.path); 1766 this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileRemove); 1767 1768 // If the script info was not root of inferred project, 1769 // there wont be config file watch open because of this script info 1770 if (infoIsRootOfInferredProject) { 1771 // But if it is a root, it could be the last script info that is root of inferred project 1772 // and hence we would need to close the config file watcher 1773 this.closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo); 1774 } 1775 1776 // If there are no open files that are impacted by configFileExistenceInfo after closing this script info 1777 // there is no configured project present, remove the cached existence info 1778 if (!configFileExistenceInfo.openFilesImpactedByConfigFile.size && 1779 !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { 1780 Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); 1781 this.configFileExistenceInfoCache.delete(canonicalConfigFilePath); 1782 } 1783 } 1784 }); 1785 } 1786 1787 /** 1788 * This is called by inferred project whenever script info is added as a root 1789 */ 1790 /* @internal */ 1791 startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { 1792 Debug.assert(info.isScriptOpen()); 1793 this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { 1794 let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1795 if (!configFileExistenceInfo) { 1796 // Create the cache 1797 configFileExistenceInfo = { 1798 exists: this.host.fileExists(configFileName), 1799 openFilesImpactedByConfigFile: new Map<Path, boolean>() 1800 }; 1801 this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); 1802 } 1803 1804 // Set this file as the root of inferred project 1805 configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, true); 1806 this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectTrue); 1807 1808 // If there is no configured project for this config file, add the file watcher 1809 if (!configFileExistenceInfo.configFileWatcherForRootOfInferredProject && 1810 !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { 1811 this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo); 1812 } 1813 }); 1814 } 1815 1816 /** 1817 * This is called by inferred project whenever root script info is removed from it 1818 */ 1819 /* @internal */ 1820 stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { 1821 this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { 1822 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1823 if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { 1824 Debug.assert(info.isScriptOpen()); 1825 1826 // Info is not root of inferred project any more 1827 configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false); 1828 this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectFalse); 1829 1830 // Close the config file watcher 1831 this.closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo); 1832 } 1833 }); 1834 } 1835 1836 /** 1837 * This function tries to search for a tsconfig.json for the given file. 1838 * This is different from the method the compiler uses because 1839 * the compiler can assume it will always start searching in the 1840 * current directory (the directory in which tsc was invoked). 1841 * The server must start searching from the directory containing 1842 * the newly opened file. 1843 */ 1844 private forEachConfigFileLocation(info: OpenScriptInfoOrClosedOrConfigFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) { 1845 if (this.serverMode !== LanguageServiceMode.Semantic) { 1846 return undefined; 1847 } 1848 1849 Debug.assert(!isOpenScriptInfo(info) || this.openFiles.has(info.path)); 1850 const projectRootPath = this.openFiles.get(info.path); 1851 const scriptInfo = Debug.checkDefined(this.getScriptInfo(info.path)); 1852 if (scriptInfo.isDynamic) return undefined; 1853 1854 let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); 1855 const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames); 1856 1857 // If projectRootPath doesn't contain info.path, then do normal search for config file 1858 const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot(); 1859 // For ancestor of config file always ignore its own directory since its going to result in itself 1860 let searchInDirectory = !isAncestorConfigFileInfo(info); 1861 do { 1862 if (searchInDirectory) { 1863 const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName); 1864 const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); 1865 let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json")); 1866 if (result) return tsconfigFileName; 1867 1868 const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); 1869 result = action(jsconfigFileName, combinePaths(canonicalSearchPath, "jsconfig.json")); 1870 if (result) return jsconfigFileName; 1871 1872 // If we started within node_modules, don't look outside node_modules. 1873 // Otherwise, we might pick up a very large project and pull in the world, 1874 // causing an editor delay. 1875 if (isNodeModulesDirectory(canonicalSearchPath)) { 1876 break; 1877 } 1878 } 1879 1880 const parentPath = asNormalizedPath(getDirectoryPath(searchPath)); 1881 if (parentPath === searchPath) break; 1882 searchPath = parentPath; 1883 searchInDirectory = true; 1884 } while (anySearchPathOk || isSearchPathInProjectRoot()); 1885 1886 return undefined; 1887 } 1888 1889 /*@internal*/ 1890 findDefaultConfiguredProject(info: ScriptInfo) { 1891 if (!info.isScriptOpen()) return undefined; 1892 const configFileName = this.getConfigFileNameForFile(info); 1893 const project = configFileName && 1894 this.findConfiguredProjectByProjectName(configFileName); 1895 1896 return project && projectContainsInfoDirectly(project, info) ? 1897 project : 1898 project?.getDefaultChildProjectFromProjectWithReferences(info); 1899 } 1900 1901 /** 1902 * This function tries to search for a tsconfig.json for the given file. 1903 * This is different from the method the compiler uses because 1904 * the compiler can assume it will always start searching in the 1905 * current directory (the directory in which tsc was invoked). 1906 * The server must start searching from the directory containing 1907 * the newly opened file. 1908 * If script info is passed in, it is asserted to be open script info 1909 * otherwise just file name 1910 */ 1911 private getConfigFileNameForFile(info: OpenScriptInfoOrClosedOrConfigFileInfo) { 1912 if (isOpenScriptInfo(info)) { 1913 Debug.assert(info.isScriptOpen()); 1914 const result = this.configFileForOpenFiles.get(info.path); 1915 if (result !== undefined) return result || undefined; 1916 } 1917 this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); 1918 const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => 1919 this.configFileExists(configFileName, canonicalConfigFilePath, info)); 1920 if (configFileName) { 1921 this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`); 1922 } 1923 else { 1924 this.logger.info(`For info: ${info.fileName} :: No config files found.`); 1925 } 1926 if (isOpenScriptInfo(info)) { 1927 this.configFileForOpenFiles.set(info.path, configFileName || false); 1928 } 1929 return configFileName; 1930 } 1931 1932 private printProjects() { 1933 if (!this.logger.hasLevel(LogLevel.normal)) { 1934 return; 1935 } 1936 1937 this.logger.startGroup(); 1938 1939 this.externalProjects.forEach(printProjectWithoutFileNames); 1940 this.configuredProjects.forEach(printProjectWithoutFileNames); 1941 this.inferredProjects.forEach(printProjectWithoutFileNames); 1942 1943 this.logger.info("Open files: "); 1944 this.openFiles.forEach((projectRootPath, path) => { 1945 const info = this.getScriptInfoForPath(path as Path)!; 1946 this.logger.info(`\tFileName: ${info.fileName} ProjectRootPath: ${projectRootPath}`); 1947 this.logger.info(`\t\tProjects: ${info.containingProjects.map(p => p.getProjectName())}`); 1948 }); 1949 1950 this.logger.endGroup(); 1951 } 1952 1953 /*@internal*/ 1954 findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined { 1955 // make sure that casing of config file name is consistent 1956 const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); 1957 return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath); 1958 } 1959 1960 private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined { 1961 return this.configuredProjects.get(canonicalConfigFilePath); 1962 } 1963 1964 private findExternalProjectByProjectName(projectFileName: string) { 1965 return findProjectByName(projectFileName, this.externalProjects); 1966 } 1967 1968 /** Get a filename if the language service exceeds the maximum allowed program size; otherwise returns undefined. */ 1969 private getFilenameForExceededTotalSizeLimitForNonTsFiles<T>(name: string, options: CompilerOptions | undefined, fileNames: T[], propertyReader: FilePropertyReader<T>): string | undefined { 1970 if (options && options.disableSizeLimit || !this.host.getFileSize) { 1971 return; 1972 } 1973 1974 let availableSpace = maxProgramSizeForNonTsFiles; 1975 this.projectToSizeMap.set(name, 0); 1976 this.projectToSizeMap.forEach(val => (availableSpace -= (val || 0))); 1977 1978 let totalNonTsFileSize = 0; 1979 1980 for (const f of fileNames) { 1981 const fileName = propertyReader.getFileName(f); 1982 if (hasTSFileExtension(fileName)) { 1983 continue; 1984 } 1985 1986 totalNonTsFileSize += this.host.getFileSize(fileName); 1987 1988 if (totalNonTsFileSize > maxProgramSizeForNonTsFiles || totalNonTsFileSize > availableSpace) { 1989 const top5LargestFiles = fileNames.map(f => propertyReader.getFileName(f)) 1990 .filter(name => !hasTSFileExtension(name)) 1991 .map(name => ({ name, size: this.host.getFileSize!(name) })) 1992 .sort((a, b) => b.size - a.size) 1993 .slice(0, 5); 1994 this.logger.info(`Non TS file size exceeded limit (${totalNonTsFileSize}). Largest files: ${top5LargestFiles.map(file => `${file.name}:${file.size}`).join(", ")}`); 1995 // Keep the size as zero since it's disabled 1996 return fileName; 1997 } 1998 } 1999 this.projectToSizeMap.set(name, totalNonTsFileSize); 2000 } 2001 2002 private createExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition, excludedFiles: NormalizedPath[]) { 2003 const compilerOptions = convertCompilerOptions(options); 2004 const watchOptionsAndErrors = convertWatchOptions(options, getDirectoryPath(normalizeSlashes(projectFileName))); 2005 const project = new ExternalProject( 2006 projectFileName, 2007 this, 2008 this.documentRegistry, 2009 compilerOptions, 2010 /*lastFileExceededProgramSize*/ this.getFilenameForExceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader), 2011 options.compileOnSave === undefined ? true : options.compileOnSave, 2012 /*projectFilePath*/ undefined, 2013 this.currentPluginConfigOverrides, 2014 watchOptionsAndErrors?.watchOptions 2015 ); 2016 project.setProjectErrors(watchOptionsAndErrors?.errors); 2017 project.excludedFiles = excludedFiles; 2018 2019 this.addFilesToNonInferredProject(project, files, externalFilePropertyReader, typeAcquisition); 2020 this.externalProjects.push(project); 2021 return project; 2022 } 2023 2024 /*@internal*/ 2025 sendProjectTelemetry(project: ExternalProject | ConfiguredProject): void { 2026 if (this.seenProjects.has(project.projectName)) { 2027 setProjectOptionsUsed(project); 2028 return; 2029 } 2030 this.seenProjects.set(project.projectName, true); 2031 2032 if (!this.eventHandler || !this.host.createSHA256Hash) { 2033 setProjectOptionsUsed(project); 2034 return; 2035 } 2036 2037 const projectOptions = isConfiguredProject(project) ? project.projectOptions as ProjectOptions : undefined; 2038 setProjectOptionsUsed(project); 2039 const data: ProjectInfoTelemetryEventData = { 2040 projectId: this.host.createSHA256Hash(project.projectName), 2041 fileStats: countEachFileTypes(project.getScriptInfos(), /*includeSizes*/ true), 2042 compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilationSettings()), 2043 typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()), 2044 extends: projectOptions && projectOptions.configHasExtendsProperty, 2045 files: projectOptions && projectOptions.configHasFilesProperty, 2046 include: projectOptions && projectOptions.configHasIncludeProperty, 2047 exclude: projectOptions && projectOptions.configHasExcludeProperty, 2048 compileOnSave: project.compileOnSaveEnabled, 2049 configFileName: configFileName(), 2050 projectType: project instanceof ExternalProject ? "external" : "configured", 2051 languageServiceEnabled: project.languageServiceEnabled, 2052 version: ts.version, // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier 2053 }; 2054 this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); 2055 2056 function configFileName(): ProjectInfoTelemetryEventData["configFileName"] { 2057 if (!isConfiguredProject(project)) { 2058 return "other"; 2059 } 2060 2061 return getBaseConfigFileName(project.getConfigFilePath()) || "other"; 2062 } 2063 2064 function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData { 2065 return { 2066 enable, 2067 include: include !== undefined && include.length !== 0, 2068 exclude: exclude !== undefined && exclude.length !== 0, 2069 }; 2070 } 2071 } 2072 2073 private addFilesToNonInferredProject<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, typeAcquisition: TypeAcquisition): void { 2074 this.updateNonInferredProjectFiles(project, files, propertyReader); 2075 project.setTypeAcquisition(typeAcquisition); 2076 } 2077 2078 /* @internal */ 2079 createConfiguredProject(configFileName: NormalizedPath) { 2080 const cachedDirectoryStructureHost = createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!; // TODO: GH#18217 2081 this.logger.info(`Opened configuration file ${configFileName}`); 2082 const project = new ConfiguredProject( 2083 configFileName, 2084 this, 2085 this.documentRegistry, 2086 cachedDirectoryStructureHost); 2087 project.createConfigFileWatcher(); 2088 this.configuredProjects.set(project.canonicalConfigFilePath, project); 2089 this.setConfigFileExistenceByNewConfiguredProject(project); 2090 return project; 2091 } 2092 2093 /* @internal */ 2094 private createConfiguredProjectWithDelayLoad(configFileName: NormalizedPath, reason: string) { 2095 const project = this.createConfiguredProject(configFileName); 2096 project.pendingReload = ConfigFileProgramReloadLevel.Full; 2097 project.pendingReloadReason = reason; 2098 return project; 2099 } 2100 2101 /* @internal */ 2102 createAndLoadConfiguredProject(configFileName: NormalizedPath, reason: string) { 2103 const project = this.createConfiguredProject(configFileName); 2104 this.loadConfiguredProject(project, reason); 2105 return project; 2106 } 2107 2108 /* @internal */ 2109 private createLoadAndUpdateConfiguredProject(configFileName: NormalizedPath, reason: string) { 2110 const project = this.createAndLoadConfiguredProject(configFileName, reason); 2111 project.updateGraph(); 2112 return project; 2113 } 2114 2115 /** 2116 * Read the config file of the project, and update the project root file names. 2117 */ 2118 /* @internal */ 2119 private loadConfiguredProject(project: ConfiguredProject, reason: string) { 2120 this.sendProjectLoadingStartEvent(project, reason); 2121 2122 // Read updated contents from disk 2123 const configFilename = normalizePath(project.getConfigFilePath()); 2124 2125 const configFileContent = tryReadFile(configFilename, fileName => this.host.readFile(fileName)); 2126 const result = parseJsonText(configFilename, isString(configFileContent) ? configFileContent : ""); 2127 const configFileErrors = result.parseDiagnostics as Diagnostic[]; 2128 if (!isString(configFileContent)) configFileErrors.push(configFileContent); 2129 const parsedCommandLine = parseJsonSourceFileConfigFileContent( 2130 result, 2131 project.getCachedDirectoryStructureHost(), 2132 getDirectoryPath(configFilename), 2133 /*existingOptions*/ {}, 2134 configFilename, 2135 /*resolutionStack*/[], 2136 this.hostConfiguration.extraFileExtensions, 2137 /*extendedConfigCache*/ undefined, 2138 ); 2139 2140 if (parsedCommandLine.errors.length) { 2141 configFileErrors.push(...parsedCommandLine.errors); 2142 } 2143 2144 this.logger.info(`Config: ${configFilename} : ${JSON.stringify({ 2145 rootNames: parsedCommandLine.fileNames, 2146 options: parsedCommandLine.options, 2147 projectReferences: parsedCommandLine.projectReferences 2148 }, /*replacer*/ undefined, " ")}`); 2149 2150 Debug.assert(!!parsedCommandLine.fileNames); 2151 const compilerOptions = parsedCommandLine.options; 2152 2153 // Update the project 2154 if (!project.projectOptions) { 2155 project.projectOptions = { 2156 configHasExtendsProperty: parsedCommandLine.raw.extends !== undefined, 2157 configHasFilesProperty: parsedCommandLine.raw.files !== undefined, 2158 configHasIncludeProperty: parsedCommandLine.raw.include !== undefined, 2159 configHasExcludeProperty: parsedCommandLine.raw.exclude !== undefined 2160 }; 2161 } 2162 project.canConfigFileJsonReportNoInputFiles = canJsonReportNoInputFiles(parsedCommandLine.raw); 2163 project.setProjectErrors(configFileErrors); 2164 project.updateReferences(parsedCommandLine.projectReferences); 2165 const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, compilerOptions, parsedCommandLine.fileNames, fileNamePropertyReader); 2166 if (lastFileExceededProgramSize) { 2167 project.disableLanguageService(lastFileExceededProgramSize); 2168 project.stopWatchingWildCards(); 2169 this.removeProjectFromSharedExtendedConfigFileMap(project); 2170 } 2171 else { 2172 project.setCompilerOptions(compilerOptions); 2173 project.setWatchOptions(parsedCommandLine.watchOptions); 2174 project.enableLanguageService(); 2175 project.watchWildcards(new Map(getEntries(parsedCommandLine.wildcardDirectories!))); // TODO: GH#18217 2176 this.updateSharedExtendedConfigFileMap(project, parsedCommandLine); 2177 } 2178 project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides); 2179 const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles()); 2180 this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave, parsedCommandLine.watchOptions); 2181 } 2182 2183 private updateNonInferredProjectFiles<T>(project: ExternalProject | ConfiguredProject | AutoImportProviderProject, files: T[], propertyReader: FilePropertyReader<T>) { 2184 const projectRootFilesMap = project.getRootFilesMap(); 2185 const newRootScriptInfoMap = new Map<string, true>(); 2186 2187 for (const f of files) { 2188 const newRootFile = propertyReader.getFileName(f); 2189 const fileName = toNormalizedPath(newRootFile); 2190 const isDynamic = isDynamicFileName(fileName); 2191 let path: Path; 2192 // Use the project's fileExists so that it can use caching instead of reaching to disk for the query 2193 if (!isDynamic && !project.fileExists(newRootFile)) { 2194 path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); 2195 const existingValue = projectRootFilesMap.get(path); 2196 if (existingValue) { 2197 if (existingValue.info) { 2198 project.removeFile(existingValue.info, /*fileExists*/ false, /*detachFromProject*/ true); 2199 existingValue.info = undefined; 2200 } 2201 existingValue.fileName = fileName; 2202 } 2203 else { 2204 projectRootFilesMap.set(path, { fileName }); 2205 } 2206 } 2207 else { 2208 const scriptKind = propertyReader.getScriptKind(f, this.hostConfiguration.extraFileExtensions); 2209 const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); 2210 const scriptInfo = Debug.checkDefined(this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( 2211 fileName, 2212 project.currentDirectory, 2213 scriptKind, 2214 hasMixedContent, 2215 project.directoryStructureHost 2216 )); 2217 path = scriptInfo.path; 2218 const existingValue = projectRootFilesMap.get(path); 2219 // If this script info is not already a root add it 2220 if (!existingValue || existingValue.info !== scriptInfo) { 2221 project.addRoot(scriptInfo, fileName); 2222 if (scriptInfo.isScriptOpen()) { 2223 // if file is already root in some inferred project 2224 // - remove the file from that project and delete the project if necessary 2225 this.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo); 2226 } 2227 } 2228 else { 2229 // Already root update the fileName 2230 existingValue.fileName = fileName; 2231 } 2232 } 2233 newRootScriptInfoMap.set(path, true); 2234 } 2235 2236 // project's root file map size is always going to be same or larger than new roots map 2237 // as we have already all the new files to the project 2238 if (projectRootFilesMap.size > newRootScriptInfoMap.size) { 2239 projectRootFilesMap.forEach((value, path) => { 2240 if (!newRootScriptInfoMap.has(path)) { 2241 if (value.info) { 2242 project.removeFile(value.info, project.fileExists(path), /*detachFromProject*/ true); 2243 } 2244 else { 2245 projectRootFilesMap.delete(path); 2246 } 2247 } 2248 }); 2249 } 2250 2251 // Just to ensure that even if root files dont change, the changes to the non root file are picked up, 2252 // mark the project as dirty unconditionally 2253 project.markAsDirty(); 2254 } 2255 2256 private updateRootAndOptionsOfNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean | undefined, watchOptions: WatchOptions | undefined) { 2257 project.setCompilerOptions(newOptions); 2258 project.setWatchOptions(watchOptions); 2259 // VS only set the CompileOnSaveEnabled option in the request if the option was changed recently 2260 // therefore if it is undefined, it should not be updated. 2261 if (compileOnSave !== undefined) { 2262 project.compileOnSaveEnabled = compileOnSave; 2263 } 2264 this.addFilesToNonInferredProject(project, newUncheckedFiles, propertyReader, newTypeAcquisition); 2265 } 2266 2267 /** 2268 * Reload the file names from config file specs and update the project graph 2269 */ 2270 /*@internal*/ 2271 reloadFileNamesOfConfiguredProject(project: ConfiguredProject) { 2272 const configFileSpecs = project.getCompilerOptions().configFile!.configFileSpecs!; 2273 const configFileName = project.getConfigFilePath(); 2274 const fileNames = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), project.getCompilationSettings(), project.getCachedDirectoryStructureHost(), this.hostConfiguration.extraFileExtensions); 2275 project.updateErrorOnNoInputFiles(fileNames); 2276 this.updateNonInferredProjectFiles(project, fileNames.concat(project.getExternalFiles()), fileNamePropertyReader); 2277 return project.updateGraph(); 2278 } 2279 2280 /*@internal*/ 2281 setFileNamesOfAutoImportProviderProject(project: AutoImportProviderProject, fileNames: string[]) { 2282 this.updateNonInferredProjectFiles(project, fileNames, fileNamePropertyReader); 2283 } 2284 2285 /** 2286 * Read the config file of the project again by clearing the cache and update the project graph 2287 */ 2288 /* @internal */ 2289 reloadConfiguredProject(project: ConfiguredProject, reason: string, isInitialLoad: boolean, clearSemanticCache: boolean) { 2290 // At this point, there is no reason to not have configFile in the host 2291 const host = project.getCachedDirectoryStructureHost(); 2292 if (clearSemanticCache) this.clearSemanticCache(project); 2293 2294 // Clear the cache since we are reloading the project from disk 2295 host.clearCache(); 2296 const configFileName = project.getConfigFilePath(); 2297 this.logger.info(`${isInitialLoad ? "Loading" : "Reloading"} configured project ${configFileName}`); 2298 2299 // Load project from the disk 2300 this.loadConfiguredProject(project, reason); 2301 project.updateGraph(); 2302 2303 this.sendConfigFileDiagEvent(project, configFileName); 2304 } 2305 2306 /* @internal */ 2307 private clearSemanticCache(project: Project) { 2308 project.resolutionCache.clear(); 2309 project.getLanguageService(/*ensureSynchronized*/ false).cleanupSemanticCache(); 2310 project.markAsDirty(); 2311 } 2312 2313 private sendConfigFileDiagEvent(project: ConfiguredProject, triggerFile: NormalizedPath) { 2314 if (!this.eventHandler || this.suppressDiagnosticEvents) { 2315 return; 2316 } 2317 const diagnostics = project.getLanguageService().getCompilerOptionsDiagnostics(); 2318 diagnostics.push(...project.getAllProjectErrors()); 2319 2320 this.eventHandler(<ConfigFileDiagEvent>{ 2321 eventName: ConfigFileDiagEvent, 2322 data: { configFileName: project.getConfigFilePath(), diagnostics, triggerFile } 2323 }); 2324 } 2325 2326 private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined { 2327 if (!this.useInferredProjectPerProjectRoot || 2328 // Its a dynamic info opened without project root 2329 (info.isDynamic && projectRootPath === undefined)) { 2330 return undefined; 2331 } 2332 2333 if (projectRootPath) { 2334 const canonicalProjectRootPath = this.toCanonicalFileName(projectRootPath); 2335 // if we have an explicit project root path, find (or create) the matching inferred project. 2336 for (const project of this.inferredProjects) { 2337 if (project.projectRootPath === canonicalProjectRootPath) { 2338 return project; 2339 } 2340 } 2341 return this.createInferredProject(projectRootPath, /*isSingleInferredProject*/ false, projectRootPath); 2342 } 2343 2344 // we don't have an explicit root path, so we should try to find an inferred project 2345 // that more closely contains the file. 2346 let bestMatch: InferredProject | undefined; 2347 for (const project of this.inferredProjects) { 2348 // ignore single inferred projects (handled elsewhere) 2349 if (!project.projectRootPath) continue; 2350 // ignore inferred projects that don't contain the root's path 2351 if (!containsPath(project.projectRootPath, info.path, this.host.getCurrentDirectory(), !this.host.useCaseSensitiveFileNames)) continue; 2352 // ignore inferred projects that are higher up in the project root. 2353 // TODO(rbuckton): Should we add the file as a root to these as well? 2354 if (bestMatch && bestMatch.projectRootPath!.length > project.projectRootPath.length) continue; 2355 bestMatch = project; 2356 } 2357 2358 return bestMatch; 2359 } 2360 2361 private getOrCreateSingleInferredProjectIfEnabled(): InferredProject | undefined { 2362 if (!this.useSingleInferredProject) { 2363 return undefined; 2364 } 2365 2366 // If `useInferredProjectPerProjectRoot` is not enabled, then there will only be one 2367 // inferred project for all files. If `useInferredProjectPerProjectRoot` is enabled 2368 // then we want to put all files that are not opened with a `projectRootPath` into 2369 // the same inferred project. 2370 // 2371 // To avoid the cost of searching through the array and to optimize for the case where 2372 // `useInferredProjectPerProjectRoot` is not enabled, we will always put the inferred 2373 // project for non-rooted files at the front of the array. 2374 if (this.inferredProjects.length > 0 && this.inferredProjects[0].projectRootPath === undefined) { 2375 return this.inferredProjects[0]; 2376 } 2377 2378 // Single inferred project does not have a project root and hence no current directory 2379 return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true); 2380 } 2381 2382 private getOrCreateSingleInferredWithoutProjectRoot(currentDirectory: string | undefined): InferredProject { 2383 Debug.assert(!this.useSingleInferredProject); 2384 const expectedCurrentDirectory = this.toCanonicalFileName(this.getNormalizedAbsolutePath(currentDirectory || "")); 2385 // Reuse the project with same current directory but no roots 2386 for (const inferredProject of this.inferredProjects) { 2387 if (!inferredProject.projectRootPath && 2388 inferredProject.isOrphan() && 2389 inferredProject.canonicalCurrentDirectory === expectedCurrentDirectory) { 2390 return inferredProject; 2391 } 2392 } 2393 2394 return this.createInferredProject(currentDirectory); 2395 } 2396 2397 private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject { 2398 const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects!; // TODO: GH#18217 2399 let watchOptionsAndErrors: WatchOptionsAndErrors | false | undefined; 2400 let typeAcquisition: TypeAcquisition | undefined; 2401 if (projectRootPath) { 2402 watchOptionsAndErrors = this.watchOptionsForInferredProjectsPerProjectRoot.get(projectRootPath); 2403 typeAcquisition = this.typeAcquisitionForInferredProjectsPerProjectRoot.get(projectRootPath); 2404 } 2405 if (watchOptionsAndErrors === undefined) { 2406 watchOptionsAndErrors = this.watchOptionsForInferredProjects; 2407 } 2408 if (typeAcquisition === undefined) { 2409 typeAcquisition = this.typeAcquisitionForInferredProjects; 2410 } 2411 watchOptionsAndErrors = watchOptionsAndErrors || undefined; 2412 const project = new InferredProject(this, this.documentRegistry, compilerOptions, watchOptionsAndErrors?.watchOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides, typeAcquisition); 2413 project.setProjectErrors(watchOptionsAndErrors?.errors); 2414 if (isSingleInferredProject) { 2415 this.inferredProjects.unshift(project); 2416 } 2417 else { 2418 this.inferredProjects.push(project); 2419 } 2420 return project; 2421 } 2422 2423 /*@internal*/ 2424 getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, currentDirectory: string, hostToQueryFileExistsOn: DirectoryStructureHost) { 2425 return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( 2426 toNormalizedPath(uncheckedFileName), currentDirectory, /*scriptKind*/ undefined, 2427 /*hasMixedContent*/ undefined, hostToQueryFileExistsOn 2428 ); 2429 } 2430 2431 getScriptInfo(uncheckedFileName: string) { 2432 return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); 2433 } 2434 2435 /* @internal */ 2436 getScriptInfoOrConfig(uncheckedFileName: string): ScriptInfoOrConfig | undefined { 2437 const path = toNormalizedPath(uncheckedFileName); 2438 const info = this.getScriptInfoForNormalizedPath(path); 2439 if (info) return info; 2440 const configProject = this.configuredProjects.get(this.toPath(uncheckedFileName)); 2441 return configProject && configProject.getCompilerOptions().configFile; 2442 } 2443 2444 /* @internal */ 2445 logErrorForScriptInfoNotFound(fileName: string): void { 2446 const names = arrayFrom(this.filenameToScriptInfo.entries()).map(([path, scriptInfo]) => ({ path, fileName: scriptInfo.fileName })); 2447 this.logger.msg(`Could not find file ${JSON.stringify(fileName)}.\nAll files are: ${JSON.stringify(names)}`, Msg.Err); 2448 } 2449 2450 /** 2451 * Returns the projects that contain script info through SymLink 2452 * Note that this does not return projects in info.containingProjects 2453 */ 2454 /*@internal*/ 2455 getSymlinkedProjects(info: ScriptInfo): MultiMap<Path, Project> | undefined { 2456 let projects: MultiMap<Path, Project> | undefined; 2457 if (this.realpathToScriptInfos) { 2458 const realpath = info.getRealpathIfDifferent(); 2459 if (realpath) { 2460 forEach(this.realpathToScriptInfos.get(realpath), combineProjects); 2461 } 2462 forEach(this.realpathToScriptInfos.get(info.path), combineProjects); 2463 } 2464 2465 return projects; 2466 2467 function combineProjects(toAddInfo: ScriptInfo) { 2468 if (toAddInfo !== info) { 2469 for (const project of toAddInfo.containingProjects) { 2470 // Add the projects only if they can use symLink targets and not already in the list 2471 if (project.languageServiceEnabled && 2472 !project.isOrphan() && 2473 !project.getCompilerOptions().preserveSymlinks && 2474 !info.isAttached(project)) { 2475 if (!projects) { 2476 projects = createMultiMap(); 2477 projects.add(toAddInfo.path, project); 2478 } 2479 else if (!forEachEntry(projects, (projs, path) => path === toAddInfo.path ? false : contains(projs, project))) { 2480 projects.add(toAddInfo.path, project); 2481 } 2482 } 2483 } 2484 } 2485 } 2486 } 2487 2488 private watchClosedScriptInfo(info: ScriptInfo) { 2489 Debug.assert(!info.fileWatcher); 2490 // do not watch files with mixed content - server doesn't know how to interpret it 2491 // do not watch files in the global cache location 2492 if (!info.isDynamicOrHasMixedContent() && 2493 (!this.globalCacheLocationDirectoryPath || 2494 !startsWith(info.path, this.globalCacheLocationDirectoryPath))) { 2495 const indexOfNodeModules = info.path.indexOf("/node_modules/"); 2496 if (!this.host.getModifiedTime || indexOfNodeModules === -1) { 2497 info.fileWatcher = this.watchFactory.watchFile( 2498 info.fileName, 2499 (_fileName, eventKind) => this.onSourceFileChanged(info, eventKind), 2500 PollingInterval.Medium, 2501 this.hostConfiguration.watchOptions, 2502 WatchType.ClosedScriptInfo 2503 ); 2504 } 2505 else { 2506 info.mTime = this.getModifiedTime(info); 2507 info.fileWatcher = this.watchClosedScriptInfoInNodeModules(info.path.substr(0, indexOfNodeModules) as Path); 2508 } 2509 } 2510 } 2511 2512 private watchClosedScriptInfoInNodeModules(dir: Path): ScriptInfoInNodeModulesWatcher { 2513 // Watch only directory 2514 const existing = this.scriptInfoInNodeModulesWatchers.get(dir); 2515 if (existing) { 2516 existing.refCount++; 2517 return existing; 2518 } 2519 2520 const watchDir = dir + "/node_modules" as Path; 2521 const watcher = this.watchFactory.watchDirectory( 2522 watchDir, 2523 fileOrDirectory => { 2524 const fileOrDirectoryPath = removeIgnoredPath(this.toPath(fileOrDirectory)); 2525 if (!fileOrDirectoryPath) return; 2526 2527 // Has extension 2528 Debug.assert(result.refCount > 0); 2529 if (watchDir === fileOrDirectoryPath) { 2530 this.refreshScriptInfosInDirectory(watchDir); 2531 } 2532 else { 2533 const info = this.getScriptInfoForPath(fileOrDirectoryPath); 2534 if (info) { 2535 if (isScriptInfoWatchedFromNodeModules(info)) { 2536 this.refreshScriptInfo(info); 2537 } 2538 } 2539 // Folder 2540 else if (!hasExtension(fileOrDirectoryPath)) { 2541 this.refreshScriptInfosInDirectory(fileOrDirectoryPath); 2542 } 2543 } 2544 }, 2545 WatchDirectoryFlags.Recursive, 2546 this.hostConfiguration.watchOptions, 2547 WatchType.NodeModulesForClosedScriptInfo 2548 ); 2549 const result: ScriptInfoInNodeModulesWatcher = { 2550 close: () => { 2551 if (result.refCount === 1) { 2552 watcher.close(); 2553 this.scriptInfoInNodeModulesWatchers.delete(dir); 2554 } 2555 else { 2556 result.refCount--; 2557 } 2558 }, 2559 refCount: 1 2560 }; 2561 this.scriptInfoInNodeModulesWatchers.set(dir, result); 2562 return result; 2563 } 2564 2565 private getModifiedTime(info: ScriptInfo) { 2566 return (this.host.getModifiedTime!(info.path) || missingFileModifiedTime).getTime(); 2567 } 2568 2569 private refreshScriptInfo(info: ScriptInfo) { 2570 const mTime = this.getModifiedTime(info); 2571 if (mTime !== info.mTime) { 2572 const eventKind = getFileWatcherEventKind(info.mTime!, mTime); 2573 info.mTime = mTime; 2574 this.onSourceFileChanged(info, eventKind); 2575 } 2576 } 2577 2578 private refreshScriptInfosInDirectory(dir: Path) { 2579 dir = dir + directorySeparator as Path; 2580 this.filenameToScriptInfo.forEach(info => { 2581 if (isScriptInfoWatchedFromNodeModules(info) && startsWith(info.path, dir)) { 2582 this.refreshScriptInfo(info); 2583 } 2584 }); 2585 } 2586 2587 private stopWatchingScriptInfo(info: ScriptInfo) { 2588 if (info.fileWatcher) { 2589 info.fileWatcher.close(); 2590 info.fileWatcher = undefined; 2591 } 2592 } 2593 2594 private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) { 2595 if (isRootedDiskPath(fileName) || isDynamicFileName(fileName)) { 2596 return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn); 2597 } 2598 2599 // This is non rooted path with different current directory than project service current directory 2600 // Only paths recognized are open relative file paths 2601 const info = this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName)); 2602 if (info) { 2603 return info; 2604 } 2605 2606 // This means triple slash references wont be resolved in dynamic and unsaved files 2607 // which is intentional since we dont know what it means to be relative to non disk files 2608 return undefined; 2609 } 2610 2611 private getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) { 2612 return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); 2613 } 2614 2615 getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) { 2616 return this.getOrCreateScriptInfoWorker(fileName, this.currentDirectory, openedByClient, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); 2617 } 2618 2619 private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) { 2620 Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content"); 2621 const path = normalizedPathToPath(fileName, currentDirectory, this.toCanonicalFileName); 2622 let info = this.getScriptInfoForPath(path); 2623 if (!info) { 2624 const isDynamic = isDynamicFileName(fileName); 2625 Debug.assert(isRootedDiskPath(fileName) || isDynamic || openedByClient, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nScript info with non-dynamic relative file name can only be open script info or in context of host currentDirectory`); 2626 Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nOpen script files with non rooted disk path opened with current directory context cannot have same canonical names`); 2627 Debug.assert(!isDynamic || this.currentDirectory === currentDirectory || this.useInferredProjectPerProjectRoot, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`); 2628 // If the file is not opened by client and the file doesnot exist on the disk, return 2629 if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) { 2630 return; 2631 } 2632 info = new ScriptInfo(this.host, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217 2633 this.filenameToScriptInfo.set(info.path, info); 2634 this.filenameToScriptInfoVersion.delete(info.path); 2635 if (!openedByClient) { 2636 this.watchClosedScriptInfo(info); 2637 } 2638 else if (!isRootedDiskPath(fileName) && (!isDynamic || this.currentDirectory !== currentDirectory)) { 2639 // File that is opened by user but isn't rooted disk path 2640 this.openFilesWithNonRootedDiskPath.set(this.toCanonicalFileName(fileName), info); 2641 } 2642 } 2643 if (openedByClient) { 2644 // Opening closed script info 2645 // either it was created just now, or was part of projects but was closed 2646 this.stopWatchingScriptInfo(info); 2647 info.open(fileContent!); 2648 if (hasMixedContent) { 2649 info.registerFileUpdate(); 2650 } 2651 } 2652 return info; 2653 } 2654 2655 /** 2656 * This gets the script info for the normalized path. If the path is not rooted disk path then the open script info with project root context is preferred 2657 */ 2658 getScriptInfoForNormalizedPath(fileName: NormalizedPath) { 2659 return !isRootedDiskPath(fileName) && this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName)) || 2660 this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName)); 2661 } 2662 2663 getScriptInfoForPath(fileName: Path) { 2664 return this.filenameToScriptInfo.get(fileName); 2665 } 2666 2667 /*@internal*/ 2668 getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { 2669 // Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host 2670 const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host); 2671 if (!declarationInfo) { 2672 if (sourceFileName) { 2673 // Project contains source file and it generates the generated file name 2674 project.addGeneratedFileWatch(generatedFileName, sourceFileName); 2675 } 2676 return undefined; 2677 } 2678 2679 // Try to get from cache 2680 declarationInfo.getSnapshot(); // Ensure synchronized 2681 if (isString(declarationInfo.sourceMapFilePath)) { 2682 // Ensure mapper is synchronized 2683 const sourceMapFileInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath); 2684 if (sourceMapFileInfo) { 2685 sourceMapFileInfo.getSnapshot(); 2686 if (sourceMapFileInfo.documentPositionMapper !== undefined) { 2687 sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos); 2688 return sourceMapFileInfo.documentPositionMapper ? sourceMapFileInfo.documentPositionMapper : undefined; 2689 } 2690 } 2691 declarationInfo.sourceMapFilePath = undefined; 2692 } 2693 else if (declarationInfo.sourceMapFilePath) { 2694 declarationInfo.sourceMapFilePath.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, declarationInfo.sourceMapFilePath.sourceInfos); 2695 return undefined; 2696 } 2697 else if (declarationInfo.sourceMapFilePath !== undefined) { 2698 // Doesnt have sourceMap 2699 return undefined; 2700 } 2701 2702 // Create the mapper 2703 let sourceMapFileInfo: ScriptInfo | undefined; 2704 let mapFileNameFromDeclarationInfo: string | undefined; 2705 2706 let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => { 2707 const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host); 2708 if (!mapInfo) { 2709 mapFileNameFromDeclarationInfo = mapFileNameFromDts; 2710 return undefined; 2711 } 2712 sourceMapFileInfo = mapInfo; 2713 const snap = mapInfo.getSnapshot(); 2714 if (mapInfo.documentPositionMapper !== undefined) return mapInfo.documentPositionMapper; 2715 return snap.getText(0, snap.getLength()); 2716 }; 2717 const projectName = project.projectName; 2718 const documentPositionMapper = getDocumentPositionMapper( 2719 { getCanonicalFileName: this.toCanonicalFileName, log: s => this.logger.info(s), getSourceFileLike: f => this.getSourceFileLike(f, projectName, declarationInfo) }, 2720 declarationInfo.fileName, 2721 declarationInfo.getLineInfo(), 2722 readMapFile 2723 ); 2724 readMapFile = undefined; // Remove ref to project 2725 if (sourceMapFileInfo) { 2726 declarationInfo.sourceMapFilePath = sourceMapFileInfo.path; 2727 sourceMapFileInfo.declarationInfoPath = declarationInfo.path; 2728 sourceMapFileInfo.documentPositionMapper = documentPositionMapper || false; 2729 sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos); 2730 } 2731 else if (mapFileNameFromDeclarationInfo) { 2732 declarationInfo.sourceMapFilePath = { 2733 watcher: this.addMissingSourceMapFile( 2734 project.currentDirectory === this.currentDirectory ? 2735 mapFileNameFromDeclarationInfo : 2736 getNormalizedAbsolutePath(mapFileNameFromDeclarationInfo, project.currentDirectory), 2737 declarationInfo.path 2738 ), 2739 sourceInfos: this.addSourceInfoToSourceMap(sourceFileName, project) 2740 }; 2741 } 2742 else { 2743 declarationInfo.sourceMapFilePath = false; 2744 } 2745 return documentPositionMapper; 2746 } 2747 2748 private addSourceInfoToSourceMap(sourceFileName: string | undefined, project: Project, sourceInfos?: Set<Path>) { 2749 if (sourceFileName) { 2750 // Attach as source 2751 const sourceInfo = this.getOrCreateScriptInfoNotOpenedByClient(sourceFileName, project.currentDirectory, project.directoryStructureHost)!; 2752 (sourceInfos || (sourceInfos = new Set())).add(sourceInfo.path); 2753 } 2754 return sourceInfos; 2755 } 2756 2757 private addMissingSourceMapFile(mapFileName: string, declarationInfoPath: Path) { 2758 const fileWatcher = this.watchFactory.watchFile( 2759 mapFileName, 2760 () => { 2761 const declarationInfo = this.getScriptInfoForPath(declarationInfoPath); 2762 if (declarationInfo && declarationInfo.sourceMapFilePath && !isString(declarationInfo.sourceMapFilePath)) { 2763 // Update declaration and source projects 2764 this.delayUpdateProjectGraphs(declarationInfo.containingProjects, /*clearSourceMapperCache*/ true); 2765 this.delayUpdateSourceInfoProjects(declarationInfo.sourceMapFilePath.sourceInfos); 2766 declarationInfo.closeSourceMapFileWatcher(); 2767 } 2768 }, 2769 PollingInterval.High, 2770 this.hostConfiguration.watchOptions, 2771 WatchType.MissingSourceMapFile, 2772 ); 2773 return fileWatcher; 2774 } 2775 2776 /*@internal*/ 2777 getSourceFileLike(fileName: string, projectNameOrProject: string | Project, declarationInfo?: ScriptInfo) { 2778 const project = (projectNameOrProject as Project).projectName ? projectNameOrProject as Project : this.findProject(projectNameOrProject as string); 2779 if (project) { 2780 const path = project.toPath(fileName); 2781 const sourceFile = project.getSourceFile(path); 2782 if (sourceFile && sourceFile.resolvedPath === path) return sourceFile; 2783 } 2784 2785 // Need to look for other files. 2786 const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host); 2787 if (!info) return undefined; 2788 2789 // Attach as source 2790 if (declarationInfo && isString(declarationInfo.sourceMapFilePath) && info !== declarationInfo) { 2791 const sourceMapInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath); 2792 if (sourceMapInfo) { 2793 (sourceMapInfo.sourceInfos || (sourceMapInfo.sourceInfos = new Set())).add(info.path); 2794 } 2795 } 2796 2797 // Key doesnt matter since its only for text and lines 2798 if (info.cacheSourceFile) return info.cacheSourceFile.sourceFile; 2799 2800 // Create sourceFileLike 2801 if (!info.sourceFileLike) { 2802 info.sourceFileLike = { 2803 get text() { 2804 Debug.fail("shouldnt need text"); 2805 return ""; 2806 }, 2807 getLineAndCharacterOfPosition: pos => { 2808 const lineOffset = info.positionToLineOffset(pos); 2809 return { line: lineOffset.line - 1, character: lineOffset.offset - 1 }; 2810 }, 2811 getPositionOfLineAndCharacter: (line, character, allowEdits) => info.lineOffsetToPosition(line + 1, character + 1, allowEdits) 2812 }; 2813 } 2814 return info.sourceFileLike; 2815 } 2816 2817 /*@internal*/ 2818 setPerformanceEventHandler(performanceEventHandler: PerformanceEventHandler) { 2819 this.performanceEventHandler = performanceEventHandler; 2820 } 2821 2822 setHostConfiguration(args: protocol.ConfigureRequestArguments) { 2823 if (args.file) { 2824 const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); 2825 if (info) { 2826 info.setOptions(convertFormatOptions(args.formatOptions!), args.preferences); 2827 this.logger.info(`Host configuration update for file ${args.file}`); 2828 } 2829 } 2830 else { 2831 if (args.hostInfo !== undefined) { 2832 this.hostConfiguration.hostInfo = args.hostInfo; 2833 this.logger.info(`Host information ${args.hostInfo}`); 2834 } 2835 if (args.formatOptions) { 2836 this.hostConfiguration.formatCodeOptions = { ...this.hostConfiguration.formatCodeOptions, ...convertFormatOptions(args.formatOptions) }; 2837 this.logger.info("Format host information updated"); 2838 } 2839 if (args.preferences) { 2840 const { lazyConfiguredProjectsFromExternalProject, includePackageJsonAutoImports } = this.hostConfiguration.preferences; 2841 this.hostConfiguration.preferences = { ...this.hostConfiguration.preferences, ...args.preferences }; 2842 if (lazyConfiguredProjectsFromExternalProject && !this.hostConfiguration.preferences.lazyConfiguredProjectsFromExternalProject) { 2843 // Load configured projects for external projects that are pending reload 2844 this.configuredProjects.forEach(project => { 2845 if (project.hasExternalProjectRef() && 2846 project.pendingReload === ConfigFileProgramReloadLevel.Full && 2847 !this.pendingProjectUpdates.has(project.getProjectName())) { 2848 project.updateGraph(); 2849 } 2850 }); 2851 } 2852 if (includePackageJsonAutoImports !== args.preferences.includePackageJsonAutoImports) { 2853 this.invalidateProjectAutoImports(/*packageJsonPath*/ undefined); 2854 } 2855 } 2856 if (args.extraFileExtensions) { 2857 this.hostConfiguration.extraFileExtensions = args.extraFileExtensions; 2858 // We need to update the project structures again as it is possible that existing 2859 // project structure could have more or less files depending on extensions permitted 2860 this.reloadProjects(); 2861 this.logger.info("Host file extension mappings updated"); 2862 } 2863 2864 if (args.watchOptions) { 2865 this.hostConfiguration.watchOptions = convertWatchOptions(args.watchOptions)?.watchOptions; 2866 this.logger.info(`Host watch options changed to ${JSON.stringify(this.hostConfiguration.watchOptions)}, it will be take effect for next watches.`); 2867 } 2868 } 2869 } 2870 2871 /*@internal*/ 2872 getWatchOptions(project: Project) { 2873 const projectOptions = project.getWatchOptions(); 2874 return projectOptions && this.hostConfiguration.watchOptions ? 2875 { ...this.hostConfiguration.watchOptions, ...projectOptions } : 2876 projectOptions || this.hostConfiguration.watchOptions; 2877 } 2878 2879 closeLog() { 2880 this.logger.close(); 2881 } 2882 2883 /** 2884 * This function rebuilds the project for every file opened by the client 2885 * This does not reload contents of open files from disk. But we could do that if needed 2886 */ 2887 reloadProjects() { 2888 this.logger.info("reload projects."); 2889 // If we want this to also reload open files from disk, we could do that, 2890 // but then we need to make sure we arent calling this function 2891 // (and would separate out below reloading of projects to be called when immediate reload is needed) 2892 // as there is no need to load contents of the files from the disk 2893 2894 // Reload script infos 2895 this.filenameToScriptInfo.forEach(info => { 2896 if (this.openFiles.has(info.path)) return; // Skip open files 2897 if (!info.fileWatcher) return; // not watched file 2898 // Handle as if file is changed or deleted 2899 this.onSourceFileChanged(info, this.host.fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted); 2900 }); 2901 // Cancel all project updates since we will be updating them now 2902 this.pendingProjectUpdates.forEach((_project, projectName) => { 2903 this.throttledOperations.cancel(projectName); 2904 this.pendingProjectUpdates.delete(projectName); 2905 }); 2906 this.throttledOperations.cancel(ensureProjectForOpenFileSchedule); 2907 this.pendingEnsureProjectForOpenFiles = false; 2908 2909 // Reload Projects 2910 this.reloadConfiguredProjectForFiles(this.openFiles as ESMap<Path, NormalizedPath | undefined>, /*clearSemanticCache*/ true, /*delayReload*/ false, returnTrue, "User requested reload projects"); 2911 this.externalProjects.forEach(project => { 2912 this.clearSemanticCache(project); 2913 project.updateGraph(); 2914 }); 2915 this.inferredProjects.forEach(project => this.clearSemanticCache(project)); 2916 this.ensureProjectForOpenFiles(); 2917 } 2918 2919 private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) { 2920 // Get open files to reload projects for 2921 this.reloadConfiguredProjectForFiles( 2922 configFileExistenceInfo.openFilesImpactedByConfigFile, 2923 /*clearSemanticCache*/ false, 2924 /*delayReload*/ true, 2925 ignoreIfNotRootOfInferredProject ? 2926 isRootOfInferredProject => isRootOfInferredProject : // Reload open files if they are root of inferred project 2927 returnTrue, // Reload all the open files impacted by config file 2928 "Change in config file detected" 2929 ); 2930 this.delayEnsureProjectForOpenFiles(); 2931 } 2932 2933 /** 2934 * This function goes through all the openFiles and tries to file the config file for them. 2935 * If the config file is found and it refers to existing project, it reloads it either immediately 2936 * or schedules it for reload depending on delayReload option 2937 * If the there is no existing project it just opens the configured project for the config file 2938 * reloadForInfo provides a way to filter out files to reload configured project for 2939 */ 2940 private reloadConfiguredProjectForFiles<T>(openFiles: ESMap<Path, T>, clearSemanticCache: boolean, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean, reason: string) { 2941 const updatedProjects = new Map<string, true>(); 2942 const reloadChildProject = (child: ConfiguredProject) => { 2943 if (!updatedProjects.has(child.canonicalConfigFilePath)) { 2944 updatedProjects.set(child.canonicalConfigFilePath, true); 2945 this.reloadConfiguredProject(child, reason, /*isInitialLoad*/ false, clearSemanticCache); 2946 } 2947 }; 2948 // try to reload config file for all open files 2949 openFiles.forEach((openFileValue, path) => { 2950 // Invalidate default config file name for open file 2951 this.configFileForOpenFiles.delete(path); 2952 // Filter out the files that need to be ignored 2953 if (!shouldReloadProjectFor(openFileValue)) { 2954 return; 2955 } 2956 2957 const info = this.getScriptInfoForPath(path)!; // TODO: GH#18217 2958 Debug.assert(info.isScriptOpen()); 2959 // This tries to search for a tsconfig.json for the given file. If we found it, 2960 // we first detect if there is already a configured project created for it: if so, 2961 // we re- read the tsconfig file content and update the project only if we havent already done so 2962 // otherwise we create a new one. 2963 const configFileName = this.getConfigFileNameForFile(info); 2964 if (configFileName) { 2965 const project = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName); 2966 if (!updatedProjects.has(project.canonicalConfigFilePath)) { 2967 updatedProjects.set(project.canonicalConfigFilePath, true); 2968 if (delayReload) { 2969 project.pendingReload = ConfigFileProgramReloadLevel.Full; 2970 project.pendingReloadReason = reason; 2971 if (clearSemanticCache) this.clearSemanticCache(project); 2972 this.delayUpdateProjectGraph(project); 2973 } 2974 else { 2975 // reload from the disk 2976 this.reloadConfiguredProject(project, reason, /*isInitialLoad*/ false, clearSemanticCache); 2977 // If this project does not contain this file directly, reload the project till the reloaded project contains the script info directly 2978 if (!projectContainsInfoDirectly(project, info)) { 2979 const referencedProject = forEachResolvedProjectReferenceProject( 2980 project, 2981 info.path, 2982 child => { 2983 reloadChildProject(child); 2984 return projectContainsInfoDirectly(child, info); 2985 }, 2986 ProjectReferenceProjectLoadKind.FindCreate 2987 ); 2988 if (referencedProject) { 2989 // Reload the project's tree that is already present 2990 forEachResolvedProjectReferenceProject( 2991 project, 2992 /*fileName*/ undefined, 2993 reloadChildProject, 2994 ProjectReferenceProjectLoadKind.Find 2995 ); 2996 } 2997 } 2998 } 2999 } 3000 } 3001 }); 3002 } 3003 3004 /** 3005 * Remove the root of inferred project if script info is part of another project 3006 */ 3007 private removeRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { 3008 // If the script info is root of inferred project, it could only be first containing project 3009 // since info is added as root to the inferred project only when there are no other projects containing it 3010 // So when it is root of the inferred project and after project structure updates its now part 3011 // of multiple project it needs to be removed from that inferred project because: 3012 // - references in inferred project supersede the root part 3013 // - root / reference in non - inferred project beats root in inferred project 3014 3015 // eg. say this is structure /a/b/a.ts /a/b/c.ts where c.ts references a.ts 3016 // When a.ts is opened, since there is no configured project/external project a.ts can be part of 3017 // a.ts is added as root to inferred project. 3018 // Now at time of opening c.ts, c.ts is also not aprt of any existing project, 3019 // so it will be added to inferred project as a root. (for sake of this example assume single inferred project is false) 3020 // So at this poing a.ts is part of first inferred project and second inferred project (of which c.ts is root) 3021 // And hence it needs to be removed from the first inferred project. 3022 Debug.assert(info.containingProjects.length > 0); 3023 const firstProject = info.containingProjects[0]; 3024 3025 if (!firstProject.isOrphan() && 3026 isInferredProject(firstProject) && 3027 firstProject.isRoot(info) && 3028 forEach(info.containingProjects, p => p !== firstProject && !p.isOrphan())) { 3029 firstProject.removeFile(info, /*fileExists*/ true, /*detachFromProject*/ true); 3030 } 3031 } 3032 3033 /** 3034 * This function is to update the project structure for every inferred project. 3035 * It is called on the premise that all the configured projects are 3036 * up to date. 3037 * This will go through open files and assign them to inferred project if open file is not part of any other project 3038 * After that all the inferred project graphs are updated 3039 */ 3040 private ensureProjectForOpenFiles() { 3041 this.logger.info("Before ensureProjectForOpenFiles:"); 3042 this.printProjects(); 3043 3044 this.openFiles.forEach((projectRootPath, path) => { 3045 const info = this.getScriptInfoForPath(path as Path)!; 3046 // collect all orphaned script infos from open files 3047 if (info.isOrphan()) { 3048 this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); 3049 } 3050 else { 3051 // Or remove the root of inferred project if is referenced in more than one projects 3052 this.removeRootOfInferredProjectIfNowPartOfOtherProject(info); 3053 } 3054 }); 3055 this.pendingEnsureProjectForOpenFiles = false; 3056 this.inferredProjects.forEach(updateProjectIfDirty); 3057 3058 this.logger.info("After ensureProjectForOpenFiles:"); 3059 this.printProjects(); 3060 } 3061 3062 /** 3063 * Open file whose contents is managed by the client 3064 * @param filename is absolute pathname 3065 * @param fileContent is a known version of the file content that is more up to date than the one on disk 3066 */ 3067 openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: string): OpenConfiguredProjectResult { 3068 return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath ? toNormalizedPath(projectRootPath) : undefined); 3069 } 3070 3071 /*@internal*/ 3072 getOriginalLocationEnsuringConfiguredProject(project: Project, location: DocumentPosition): DocumentPosition | undefined { 3073 const originalLocation = project.isSourceOfProjectReferenceRedirect(location.fileName) ? 3074 location : 3075 project.getSourceMapper().tryGetSourcePosition(location); 3076 if (!originalLocation) return undefined; 3077 3078 const { fileName } = originalLocation; 3079 if (!this.getScriptInfo(fileName) && !this.host.fileExists(fileName)) return undefined; 3080 3081 const originalFileInfo: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) }; 3082 const configFileName = this.getConfigFileNameForFile(originalFileInfo); 3083 if (!configFileName) return undefined; 3084 3085 let configuredProject: ConfiguredProject | undefined = this.findConfiguredProjectByProjectName(configFileName) || 3086 this.createAndLoadConfiguredProject(configFileName, `Creating project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}`); 3087 updateProjectIfDirty(configuredProject); 3088 3089 const projectContainsOriginalInfo = (project: ConfiguredProject) => { 3090 const info = this.getScriptInfo(fileName); 3091 return info && projectContainsInfoDirectly(project, info); 3092 }; 3093 3094 if (configuredProject.isSolution() || !projectContainsOriginalInfo(configuredProject)) { 3095 // Find the project that is referenced from this solution that contains the script info directly 3096 configuredProject = forEachResolvedProjectReferenceProject( 3097 configuredProject, 3098 fileName, 3099 child => { 3100 updateProjectIfDirty(child); 3101 return projectContainsOriginalInfo(child) ? child : undefined; 3102 }, 3103 ProjectReferenceProjectLoadKind.FindCreateLoad, 3104 `Creating project referenced in solution ${configuredProject.projectName} to find possible configured project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}` 3105 ); 3106 if (!configuredProject) return undefined; 3107 if (configuredProject === project) return originalLocation; 3108 } 3109 3110 // Keep this configured project as referenced from project 3111 addOriginalConfiguredProject(configuredProject); 3112 3113 const originalScriptInfo = this.getScriptInfo(fileName); 3114 if (!originalScriptInfo || !originalScriptInfo.containingProjects.length) return undefined; 3115 3116 // Add configured projects as referenced 3117 originalScriptInfo.containingProjects.forEach(project => { 3118 if (isConfiguredProject(project)) { 3119 addOriginalConfiguredProject(project); 3120 } 3121 }); 3122 return originalLocation; 3123 3124 function addOriginalConfiguredProject(originalProject: ConfiguredProject) { 3125 if (!project.originalConfiguredProjects) { 3126 project.originalConfiguredProjects = new Set(); 3127 } 3128 project.originalConfiguredProjects.add(originalProject.canonicalConfigFilePath); 3129 } 3130 } 3131 3132 /** @internal */ 3133 fileExists(fileName: NormalizedPath): boolean { 3134 return !!this.getScriptInfoForNormalizedPath(fileName) || this.host.fileExists(fileName); 3135 } 3136 3137 private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined { 3138 return find(this.externalProjects, proj => { 3139 // Ensure project structure is up-to-date to check if info is present in external project 3140 updateProjectIfDirty(proj); 3141 return proj.containsScriptInfo(info); 3142 }); 3143 } 3144 3145 private getOrCreateOpenScriptInfo(fileName: NormalizedPath, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, projectRootPath: NormalizedPath | undefined) { 3146 const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent)!; // TODO: GH#18217 3147 this.openFiles.set(info.path, projectRootPath); 3148 return info; 3149 } 3150 3151 private assignProjectToOpenedScriptInfo(info: ScriptInfo): AssignProjectResult { 3152 let configFileName: NormalizedPath | undefined; 3153 let configFileErrors: readonly Diagnostic[] | undefined; 3154 let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info); 3155 let retainProjects: ConfiguredProject[] | ConfiguredProject | undefined; 3156 let projectForConfigFileDiag: ConfiguredProject | undefined; 3157 let defaultConfigProjectIsCreated = false; 3158 if (!project && this.serverMode === LanguageServiceMode.Semantic) { // Checking semantic mode is an optimization 3159 configFileName = this.getConfigFileNameForFile(info); 3160 if (configFileName) { 3161 project = this.findConfiguredProjectByProjectName(configFileName); 3162 if (!project) { 3163 project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${info.fileName} to open`); 3164 defaultConfigProjectIsCreated = true; 3165 } 3166 else { 3167 // Ensure project is ready to check if it contains opened script info 3168 updateProjectIfDirty(project); 3169 } 3170 3171 projectForConfigFileDiag = project.containsScriptInfo(info) ? project : undefined; 3172 retainProjects = project; 3173 3174 // If this configured project doesnt contain script info but 3175 // it is solution with project references, try those project references 3176 if (!projectContainsInfoDirectly(project, info)) { 3177 forEachResolvedProjectReferenceProject( 3178 project, 3179 info.path, 3180 child => { 3181 updateProjectIfDirty(child); 3182 // Retain these projects 3183 if (!isArray(retainProjects)) { 3184 retainProjects = [project as ConfiguredProject, child]; 3185 } 3186 else { 3187 retainProjects.push(child); 3188 } 3189 3190 // If script info belongs to this child project, use this as default config project 3191 if (projectContainsInfoDirectly(child, info)) { 3192 projectForConfigFileDiag = child; 3193 return child; 3194 } 3195 3196 // If this project uses the script info (even through project reference), if default project is not found, use this for configFileDiag 3197 if (!projectForConfigFileDiag && child.containsScriptInfo(info)) { 3198 projectForConfigFileDiag = child; 3199 } 3200 }, 3201 ProjectReferenceProjectLoadKind.FindCreateLoad, 3202 `Creating project referenced in solution ${project.projectName} to find possible configured project for ${info.fileName} to open` 3203 ); 3204 } 3205 3206 // Send the event only if the project got created as part of this open request and info is part of the project 3207 if (projectForConfigFileDiag) { 3208 configFileName = projectForConfigFileDiag.getConfigFilePath(); 3209 if (projectForConfigFileDiag !== project || defaultConfigProjectIsCreated) { 3210 configFileErrors = projectForConfigFileDiag.getAllProjectErrors(); 3211 this.sendConfigFileDiagEvent(projectForConfigFileDiag, info.fileName); 3212 } 3213 } 3214 else { 3215 // Since the file isnt part of configured project, do not send config file info 3216 configFileName = undefined; 3217 } 3218 3219 // Create ancestor configured project 3220 this.createAncestorProjects(info, project); 3221 } 3222 } 3223 3224 // Project we have at this point is going to be updated since its either found through 3225 // - external project search, which updates the project before checking if info is present in it 3226 // - configured project - either created or updated to ensure we know correct status of info 3227 3228 // At this point we need to ensure that containing projects of the info are uptodate 3229 // This will ensure that later question of info.isOrphan() will return correct answer 3230 // and we correctly create inferred project for the info 3231 info.containingProjects.forEach(updateProjectIfDirty); 3232 3233 // At this point if file is part of any any configured or external project, then it would be present in the containing projects 3234 // So if it still doesnt have any containing projects, it needs to be part of inferred project 3235 if (info.isOrphan()) { 3236 // Even though this info did not belong to any of the configured projects, send the config file diag 3237 if (isArray(retainProjects)) { 3238 retainProjects.forEach(project => this.sendConfigFileDiagEvent(project, info.fileName)); 3239 } 3240 else if (retainProjects) { 3241 this.sendConfigFileDiagEvent(retainProjects, info.fileName); 3242 } 3243 Debug.assert(this.openFiles.has(info.path)); 3244 this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path)); 3245 } 3246 Debug.assert(!info.isOrphan()); 3247 return { configFileName, configFileErrors, retainProjects }; 3248 } 3249 3250 private createAncestorProjects(info: ScriptInfo, project: ConfiguredProject) { 3251 // Skip if info is not part of default configured project 3252 if (!info.isAttached(project)) return; 3253 3254 // Create configured project till project root 3255 while (true) { 3256 // Skip if project is not composite 3257 if (!project.isInitialLoadPending() && 3258 ( 3259 !project.getCompilerOptions().composite || 3260 project.getCompilerOptions().disableSolutionSearching 3261 ) 3262 ) return; 3263 3264 // Get config file name 3265 const configFileName = this.getConfigFileNameForFile({ 3266 fileName: project.getConfigFilePath(), 3267 path: info.path, 3268 configFileInfo: true 3269 }); 3270 if (!configFileName) return; 3271 3272 // find or delay load the project 3273 const ancestor = this.findConfiguredProjectByProjectName(configFileName) || 3274 this.createConfiguredProjectWithDelayLoad(configFileName, `Creating project possibly referencing default composite project ${project.getProjectName()} of open file ${info.fileName}`); 3275 if (ancestor.isInitialLoadPending()) { 3276 // Set a potential project reference 3277 ancestor.setPotentialProjectReference(project.canonicalConfigFilePath); 3278 } 3279 project = ancestor; 3280 } 3281 } 3282 3283 /*@internal*/ 3284 loadAncestorProjectTree(forProjects?: ReadonlyCollection<string>) { 3285 forProjects = forProjects || mapDefinedEntries( 3286 this.configuredProjects, 3287 (key, project) => !project.isInitialLoadPending() ? [key, true] : undefined 3288 ); 3289 3290 const seenProjects = new Set<NormalizedPath>(); 3291 // Work on array copy as we could add more projects as part of callback 3292 for (const project of arrayFrom(this.configuredProjects.values())) { 3293 // If this project has potential project reference for any of the project we are loading ancestor tree for 3294 // load this project first 3295 if (forEachPotentialProjectReference(project, potentialRefPath => forProjects!.has(potentialRefPath))) { 3296 updateProjectIfDirty(project); 3297 } 3298 this.ensureProjectChildren(project, forProjects, seenProjects); 3299 } 3300 } 3301 3302 private ensureProjectChildren(project: ConfiguredProject, forProjects: ReadonlyCollection<string>, seenProjects: Set<NormalizedPath>) { 3303 if (!tryAddToSet(seenProjects, project.canonicalConfigFilePath)) return; 3304 3305 // If this project disables child load ignore it 3306 if (project.getCompilerOptions().disableReferencedProjectLoad) return; 3307 3308 const children = project.getCurrentProgram()?.getResolvedProjectReferences(); 3309 if (!children) return; 3310 3311 for (const child of children) { 3312 if (!child) continue; 3313 const referencedProject = forEachResolvedProjectReference(child.references, ref => forProjects.has(ref.sourceFile.path) ? ref : undefined); 3314 if (!referencedProject) continue; 3315 3316 // Load this project, 3317 const configFileName = toNormalizedPath(child.sourceFile.fileName); 3318 const childProject = project.projectService.findConfiguredProjectByProjectName(configFileName) || 3319 project.projectService.createAndLoadConfiguredProject(configFileName, `Creating project referenced by : ${project.projectName} as it references project ${referencedProject.sourceFile.fileName}`); 3320 updateProjectIfDirty(childProject); 3321 3322 // Ensure children for this project 3323 this.ensureProjectChildren(childProject, forProjects, seenProjects); 3324 } 3325 } 3326 3327 private cleanupAfterOpeningFile(toRetainConfigProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) { 3328 // This was postponed from closeOpenFile to after opening next file, 3329 // so that we can reuse the project if we need to right away 3330 this.removeOrphanConfiguredProjects(toRetainConfigProjects); 3331 3332 // Remove orphan inferred projects now that we have reused projects 3333 // We need to create a duplicate because we cant guarantee order after removal 3334 for (const inferredProject of this.inferredProjects.slice()) { 3335 if (inferredProject.isOrphan()) { 3336 this.removeProject(inferredProject); 3337 } 3338 } 3339 3340 // Delete the orphan files here because there might be orphan script infos (which are not part of project) 3341 // when some file/s were closed which resulted in project removal. 3342 // It was then postponed to cleanup these script infos so that they can be reused if 3343 // the file from that old project is reopened because of opening file from here. 3344 this.removeOrphanScriptInfos(); 3345 } 3346 3347 openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult { 3348 const info = this.getOrCreateOpenScriptInfo(fileName, fileContent, scriptKind, hasMixedContent, projectRootPath); 3349 const { retainProjects, ...result } = this.assignProjectToOpenedScriptInfo(info); 3350 this.cleanupAfterOpeningFile(retainProjects); 3351 this.telemetryOnOpenFile(info); 3352 this.printProjects(); 3353 return result; 3354 } 3355 3356 private removeOrphanConfiguredProjects(toRetainConfiguredProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) { 3357 const toRemoveConfiguredProjects = new Map(this.configuredProjects); 3358 const markOriginalProjectsAsUsed = (project: Project) => { 3359 if (!project.isOrphan() && project.originalConfiguredProjects) { 3360 project.originalConfiguredProjects.forEach( 3361 (_value, configuredProjectPath) => { 3362 const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath); 3363 return project && retainConfiguredProject(project); 3364 } 3365 ); 3366 } 3367 }; 3368 if (toRetainConfiguredProjects) { 3369 if (isArray(toRetainConfiguredProjects)) { 3370 toRetainConfiguredProjects.forEach(retainConfiguredProject); 3371 } 3372 else { 3373 retainConfiguredProject(toRetainConfiguredProjects); 3374 } 3375 } 3376 3377 // Do not remove configured projects that are used as original projects of other 3378 this.inferredProjects.forEach(markOriginalProjectsAsUsed); 3379 this.externalProjects.forEach(markOriginalProjectsAsUsed); 3380 this.configuredProjects.forEach(project => { 3381 // If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references 3382 if (project.hasOpenRef()) { 3383 retainConfiguredProject(project); 3384 } 3385 else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) { 3386 // If the configured project for project reference has more than zero references, keep it alive 3387 forEachReferencedProject( 3388 project, 3389 ref => isRetained(ref) && retainConfiguredProject(project) 3390 ); 3391 } 3392 }); 3393 3394 // Remove all the non marked projects 3395 toRemoveConfiguredProjects.forEach(project => this.removeProject(project)); 3396 3397 function isRetained(project: ConfiguredProject) { 3398 return project.hasOpenRef() || !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath); 3399 } 3400 3401 function retainConfiguredProject(project: ConfiguredProject) { 3402 if (toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath)) { 3403 // Keep original projects used 3404 markOriginalProjectsAsUsed(project); 3405 // Keep all the references alive 3406 forEachReferencedProject(project, retainConfiguredProject); 3407 } 3408 } 3409 } 3410 3411 private removeOrphanScriptInfos() { 3412 const toRemoveScriptInfos = new Map(this.filenameToScriptInfo); 3413 this.filenameToScriptInfo.forEach(info => { 3414 // If script info is open or orphan, retain it and its dependencies 3415 if (!info.isScriptOpen() && info.isOrphan() && !info.isContainedByAutoImportProvider()) { 3416 // Otherwise if there is any source info that is alive, this alive too 3417 if (!info.sourceMapFilePath) return; 3418 let sourceInfos: Set<Path> | undefined; 3419 if (isString(info.sourceMapFilePath)) { 3420 const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath); 3421 sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos; 3422 } 3423 else { 3424 sourceInfos = info.sourceMapFilePath.sourceInfos; 3425 } 3426 if (!sourceInfos) return; 3427 if (!forEachKey(sourceInfos, path => { 3428 const info = this.getScriptInfoForPath(path); 3429 return !!info && (info.isScriptOpen() || !info.isOrphan()); 3430 })) { 3431 return; 3432 } 3433 } 3434 3435 // Retain this script info 3436 toRemoveScriptInfos.delete(info.path); 3437 if (info.sourceMapFilePath) { 3438 let sourceInfos: Set<Path> | undefined; 3439 if (isString(info.sourceMapFilePath)) { 3440 // And map file info and source infos 3441 toRemoveScriptInfos.delete(info.sourceMapFilePath); 3442 const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath); 3443 sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos; 3444 } 3445 else { 3446 sourceInfos = info.sourceMapFilePath.sourceInfos; 3447 } 3448 if (sourceInfos) { 3449 sourceInfos.forEach((_value, path) => toRemoveScriptInfos.delete(path)); 3450 } 3451 } 3452 }); 3453 3454 toRemoveScriptInfos.forEach(info => { 3455 // if there are not projects that include this script info - delete it 3456 this.stopWatchingScriptInfo(info); 3457 this.deleteScriptInfo(info); 3458 info.closeSourceMapFileWatcher(); 3459 }); 3460 } 3461 3462 private telemetryOnOpenFile(scriptInfo: ScriptInfo): void { 3463 if (this.serverMode !== LanguageServiceMode.Semantic || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) { 3464 return; 3465 } 3466 3467 const project = scriptInfo.getDefaultProject(); 3468 if (!project.languageServiceEnabled) { 3469 return; 3470 } 3471 3472 const sourceFile = project.getSourceFile(scriptInfo.path); 3473 const checkJs = !!sourceFile && !!sourceFile.checkJsDirective; 3474 this.eventHandler({ eventName: OpenFileInfoTelemetryEvent, data: { info: { checkJs } } }); 3475 } 3476 3477 /** 3478 * Close file whose contents is managed by the client 3479 * @param filename is absolute pathname 3480 */ 3481 closeClientFile(uncheckedFileName: string): void; 3482 /*@internal*/ 3483 closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject: true): boolean; 3484 closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject?: true) { 3485 const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); 3486 const result = info ? this.closeOpenFile(info, skipAssignOrphanScriptInfosToInferredProject) : false; 3487 if (!skipAssignOrphanScriptInfosToInferredProject) { 3488 this.printProjects(); 3489 } 3490 return result; 3491 } 3492 3493 private collectChanges( 3494 lastKnownProjectVersions: protocol.ProjectVersionInfo[], 3495 currentProjects: Project[], 3496 includeProjectReferenceRedirectInfo: boolean | undefined, 3497 result: ProjectFilesWithTSDiagnostics[] 3498 ): void { 3499 for (const proj of currentProjects) { 3500 const knownProject = find(lastKnownProjectVersions, p => p.projectName === proj.getProjectName()); 3501 result.push(proj.getChangesSinceVersion(knownProject && knownProject.version, includeProjectReferenceRedirectInfo)); 3502 } 3503 } 3504 3505 /* @internal */ 3506 synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[], includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics[] { 3507 const files: ProjectFilesWithTSDiagnostics[] = []; 3508 this.collectChanges(knownProjects, this.externalProjects, includeProjectReferenceRedirectInfo, files); 3509 this.collectChanges(knownProjects, arrayFrom(this.configuredProjects.values()), includeProjectReferenceRedirectInfo, files); 3510 this.collectChanges(knownProjects, this.inferredProjects, includeProjectReferenceRedirectInfo, files); 3511 return files; 3512 } 3513 3514 /* @internal */ 3515 applyChangesInOpenFiles(openFiles: Iterator<OpenFileArguments> | undefined, changedFiles?: Iterator<ChangeFileArguments>, closedFiles?: string[]): void { 3516 let openScriptInfos: ScriptInfo[] | undefined; 3517 let assignOrphanScriptInfosToInferredProject = false; 3518 if (openFiles) { 3519 while (true) { 3520 const iterResult = openFiles.next(); 3521 if (iterResult.done) break; 3522 const file = iterResult.value; 3523 // Create script infos so we have the new content for all the open files before we do any updates to projects 3524 const info = this.getOrCreateOpenScriptInfo( 3525 toNormalizedPath(file.fileName), 3526 file.content, 3527 tryConvertScriptKindName(file.scriptKind!), 3528 file.hasMixedContent, 3529 file.projectRootPath ? toNormalizedPath(file.projectRootPath) : undefined 3530 ); 3531 (openScriptInfos || (openScriptInfos = [])).push(info); 3532 } 3533 } 3534 3535 if (changedFiles) { 3536 while (true) { 3537 const iterResult = changedFiles.next(); 3538 if (iterResult.done) break; 3539 const file = iterResult.value; 3540 const scriptInfo = this.getScriptInfo(file.fileName)!; 3541 Debug.assert(!!scriptInfo); 3542 // Make edits to script infos and marks containing project as dirty 3543 this.applyChangesToFile(scriptInfo, file.changes); 3544 } 3545 } 3546 3547 if (closedFiles) { 3548 for (const file of closedFiles) { 3549 // Close files, but dont assign projects to orphan open script infos, that part comes later 3550 assignOrphanScriptInfosToInferredProject = this.closeClientFile(file, /*skipAssignOrphanScriptInfosToInferredProject*/ true) || assignOrphanScriptInfosToInferredProject; 3551 } 3552 } 3553 3554 // All the script infos now exist, so ok to go update projects for open files 3555 let retainProjects: readonly ConfiguredProject[] | undefined; 3556 if (openScriptInfos) { 3557 retainProjects = flatMap(openScriptInfos, info => this.assignProjectToOpenedScriptInfo(info).retainProjects); 3558 } 3559 3560 // While closing files there could be open files that needed assigning new inferred projects, do it now 3561 if (assignOrphanScriptInfosToInferredProject) { 3562 this.assignOrphanScriptInfosToInferredProject(); 3563 } 3564 3565 if (openScriptInfos) { 3566 // Cleanup projects 3567 this.cleanupAfterOpeningFile(retainProjects); 3568 // Telemetry 3569 openScriptInfos.forEach(info => this.telemetryOnOpenFile(info)); 3570 this.printProjects(); 3571 } 3572 else if (length(closedFiles)) { 3573 this.printProjects(); 3574 } 3575 } 3576 3577 /* @internal */ 3578 applyChangesToFile(scriptInfo: ScriptInfo, changes: Iterator<TextChange>) { 3579 while (true) { 3580 const iterResult = changes.next(); 3581 if (iterResult.done) break; 3582 const change = iterResult.value; 3583 scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); 3584 } 3585 } 3586 3587 private closeConfiguredProjectReferencedFromExternalProject(configFile: NormalizedPath) { 3588 const configuredProject = this.findConfiguredProjectByProjectName(configFile); 3589 if (configuredProject) { 3590 configuredProject.deleteExternalProjectReference(); 3591 if (!configuredProject.hasOpenRef()) { 3592 this.removeProject(configuredProject); 3593 return; 3594 } 3595 } 3596 } 3597 3598 closeExternalProject(uncheckedFileName: string): void { 3599 const fileName = toNormalizedPath(uncheckedFileName); 3600 const configFiles = this.externalProjectToConfiguredProjectMap.get(fileName); 3601 if (configFiles) { 3602 for (const configFile of configFiles) { 3603 this.closeConfiguredProjectReferencedFromExternalProject(configFile); 3604 } 3605 this.externalProjectToConfiguredProjectMap.delete(fileName); 3606 } 3607 else { 3608 // close external project 3609 const externalProject = this.findExternalProjectByProjectName(uncheckedFileName); 3610 if (externalProject) { 3611 this.removeProject(externalProject); 3612 } 3613 } 3614 } 3615 3616 openExternalProjects(projects: protocol.ExternalProject[]): void { 3617 // record project list before the update 3618 const projectsToClose = arrayToMap(this.externalProjects, p => p.getProjectName(), _ => true); 3619 forEachKey(this.externalProjectToConfiguredProjectMap, externalProjectName => { 3620 projectsToClose.set(externalProjectName, true); 3621 }); 3622 3623 for (const externalProject of projects) { 3624 this.openExternalProject(externalProject); 3625 // delete project that is present in input list 3626 projectsToClose.delete(externalProject.projectFileName); 3627 } 3628 3629 // close projects that were missing in the input list 3630 forEachKey(projectsToClose, externalProjectName => { 3631 this.closeExternalProject(externalProjectName); 3632 }); 3633 } 3634 3635 /** Makes a filename safe to insert in a RegExp */ 3636 private static readonly filenameEscapeRegexp = /[-\/\\^$*+?.()|[\]{}]/g; 3637 private static escapeFilenameForRegex(filename: string) { 3638 return filename.replace(this.filenameEscapeRegexp, "\\$&"); 3639 } 3640 3641 resetSafeList(): void { 3642 this.safelist = defaultTypeSafeList; 3643 } 3644 3645 applySafeList(proj: protocol.ExternalProject): NormalizedPath[] { 3646 const { rootFiles } = proj; 3647 const typeAcquisition = proj.typeAcquisition!; 3648 Debug.assert(!!typeAcquisition, "proj.typeAcquisition should be set by now"); 3649 3650 if (typeAcquisition.enable === false || typeAcquisition.disableFilenameBasedTypeAcquisition) { 3651 return []; 3652 } 3653 3654 const typeAcqInclude = typeAcquisition.include || (typeAcquisition.include = []); 3655 const excludeRules: string[] = []; 3656 3657 const normalizedNames = rootFiles.map(f => normalizeSlashes(f.fileName)) as NormalizedPath[]; 3658 const excludedFiles: NormalizedPath[] = []; 3659 3660 for (const name of Object.keys(this.safelist)) { 3661 const rule = this.safelist[name]; 3662 for (const root of normalizedNames) { 3663 if (rule.match.test(root)) { 3664 this.logger.info(`Excluding files based on rule ${name} matching file '${root}'`); 3665 3666 // If the file matches, collect its types packages and exclude rules 3667 if (rule.types) { 3668 for (const type of rule.types) { 3669 // Best-effort de-duping here - doesn't need to be unduplicated but 3670 // we don't want the list to become a 400-element array of just 'kendo' 3671 if (typeAcqInclude.indexOf(type) < 0) { 3672 typeAcqInclude.push(type); 3673 } 3674 } 3675 } 3676 3677 if (rule.exclude) { 3678 for (const exclude of rule.exclude) { 3679 const processedRule = root.replace(rule.match, (...groups: string[]) => { 3680 return exclude.map(groupNumberOrString => { 3681 // RegExp group numbers are 1-based, but the first element in groups 3682 // is actually the original string, so it all works out in the end. 3683 if (typeof groupNumberOrString === "number") { 3684 if (!isString(groups[groupNumberOrString])) { 3685 // Specification was wrong - exclude nothing! 3686 this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`); 3687 // * can't appear in a filename; escape it because it's feeding into a RegExp 3688 return "\\*"; 3689 } 3690 return ProjectService.escapeFilenameForRegex(groups[groupNumberOrString]); 3691 } 3692 return groupNumberOrString; 3693 }).join(""); 3694 }); 3695 3696 if (excludeRules.indexOf(processedRule) === -1) { 3697 excludeRules.push(processedRule); 3698 } 3699 } 3700 } 3701 else { 3702 // If not rules listed, add the default rule to exclude the matched file 3703 const escaped = ProjectService.escapeFilenameForRegex(root); 3704 if (excludeRules.indexOf(escaped) < 0) { 3705 excludeRules.push(escaped); 3706 } 3707 } 3708 } 3709 } 3710 } 3711 3712 const excludeRegexes = excludeRules.map(e => new RegExp(e, "i")); 3713 const filesToKeep: protocol.ExternalFile[] = []; 3714 for (let i = 0; i < proj.rootFiles.length; i++) { 3715 if (excludeRegexes.some(re => re.test(normalizedNames[i]))) { 3716 excludedFiles.push(normalizedNames[i]); 3717 } 3718 else { 3719 let exclude = false; 3720 if (typeAcquisition.enable || typeAcquisition.enableAutoDiscovery) { 3721 const baseName = getBaseFileName(toFileNameLowerCase(normalizedNames[i])); 3722 if (fileExtensionIs(baseName, "js")) { 3723 const inferredTypingName = removeFileExtension(baseName); 3724 const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName); 3725 const typeName = this.legacySafelist.get(cleanedTypingName); 3726 if (typeName !== undefined) { 3727 this.logger.info(`Excluded '${normalizedNames[i]}' because it matched ${cleanedTypingName} from the legacy safelist`); 3728 excludedFiles.push(normalizedNames[i]); 3729 // *exclude* it from the project... 3730 exclude = true; 3731 // ... but *include* it in the list of types to acquire 3732 // Same best-effort dedupe as above 3733 if (typeAcqInclude.indexOf(typeName) < 0) { 3734 typeAcqInclude.push(typeName); 3735 } 3736 } 3737 } 3738 } 3739 if (!exclude) { 3740 // Exclude any minified files that get this far 3741 if (/^.+[\.-]min\.js$/.test(normalizedNames[i])) { 3742 excludedFiles.push(normalizedNames[i]); 3743 } 3744 else { 3745 filesToKeep.push(proj.rootFiles[i]); 3746 } 3747 } 3748 } 3749 } 3750 proj.rootFiles = filesToKeep; 3751 return excludedFiles; 3752 } 3753 3754 openExternalProject(proj: protocol.ExternalProject): void { 3755 // typingOptions has been deprecated and is only supported for backward compatibility 3756 // purposes. It should be removed in future releases - use typeAcquisition instead. 3757 if (proj.typingOptions && !proj.typeAcquisition) { 3758 const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions); 3759 proj.typeAcquisition = typeAcquisition; 3760 } 3761 proj.typeAcquisition = proj.typeAcquisition || {}; 3762 proj.typeAcquisition.include = proj.typeAcquisition.include || []; 3763 proj.typeAcquisition.exclude = proj.typeAcquisition.exclude || []; 3764 if (proj.typeAcquisition.enable === undefined) { 3765 proj.typeAcquisition.enable = hasNoTypeScriptSource(proj.rootFiles.map(f => f.fileName)); 3766 } 3767 3768 const excludedFiles = this.applySafeList(proj); 3769 3770 let tsConfigFiles: NormalizedPath[] | undefined; 3771 const rootFiles: protocol.ExternalFile[] = []; 3772 for (const file of proj.rootFiles) { 3773 const normalized = toNormalizedPath(file.fileName); 3774 if (getBaseConfigFileName(normalized)) { 3775 if (this.serverMode === LanguageServiceMode.Semantic && this.host.fileExists(normalized)) { 3776 (tsConfigFiles || (tsConfigFiles = [])).push(normalized); 3777 } 3778 } 3779 else { 3780 rootFiles.push(file); 3781 } 3782 } 3783 3784 // sort config files to simplify comparison later 3785 if (tsConfigFiles) { 3786 tsConfigFiles.sort(); 3787 } 3788 3789 const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); 3790 let exisingConfigFiles: string[] | undefined; 3791 if (externalProject) { 3792 externalProject.excludedFiles = excludedFiles; 3793 if (!tsConfigFiles) { 3794 const compilerOptions = convertCompilerOptions(proj.options); 3795 const watchOptionsAndErrors = convertWatchOptions(proj.options, externalProject.getCurrentDirectory()); 3796 const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(proj.projectFileName, compilerOptions, proj.rootFiles, externalFilePropertyReader); 3797 if (lastFileExceededProgramSize) { 3798 externalProject.disableLanguageService(lastFileExceededProgramSize); 3799 } 3800 else { 3801 externalProject.enableLanguageService(); 3802 } 3803 externalProject.setProjectErrors(watchOptionsAndErrors?.errors); 3804 // external project already exists and not config files were added - update the project and return; 3805 // The graph update here isnt postponed since any file open operation needs all updated external projects 3806 this.updateRootAndOptionsOfNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, watchOptionsAndErrors?.watchOptions); 3807 externalProject.updateGraph(); 3808 return; 3809 } 3810 // some config files were added to external project (that previously were not there) 3811 // close existing project and later we'll open a set of configured projects for these files 3812 this.closeExternalProject(proj.projectFileName); 3813 } 3814 else if (this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)) { 3815 // this project used to include config files 3816 if (!tsConfigFiles) { 3817 // config files were removed from the project - close existing external project which in turn will close configured projects 3818 this.closeExternalProject(proj.projectFileName); 3819 } 3820 else { 3821 // project previously had some config files - compare them with new set of files and close all configured projects that correspond to unused files 3822 const oldConfigFiles = this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)!; 3823 let iNew = 0; 3824 let iOld = 0; 3825 while (iNew < tsConfigFiles.length && iOld < oldConfigFiles.length) { 3826 const newConfig = tsConfigFiles[iNew]; 3827 const oldConfig = oldConfigFiles[iOld]; 3828 if (oldConfig < newConfig) { 3829 this.closeConfiguredProjectReferencedFromExternalProject(oldConfig); 3830 iOld++; 3831 } 3832 else if (oldConfig > newConfig) { 3833 iNew++; 3834 } 3835 else { 3836 // record existing config files so avoid extra add-refs 3837 (exisingConfigFiles || (exisingConfigFiles = [])).push(oldConfig); 3838 iOld++; 3839 iNew++; 3840 } 3841 } 3842 for (let i = iOld; i < oldConfigFiles.length; i++) { 3843 // projects for all remaining old config files should be closed 3844 this.closeConfiguredProjectReferencedFromExternalProject(oldConfigFiles[i]); 3845 } 3846 } 3847 } 3848 if (tsConfigFiles) { 3849 // store the list of tsconfig files that belong to the external project 3850 this.externalProjectToConfiguredProjectMap.set(proj.projectFileName, tsConfigFiles); 3851 for (const tsconfigFile of tsConfigFiles) { 3852 let project = this.findConfiguredProjectByProjectName(tsconfigFile); 3853 if (!project) { 3854 // errors are stored in the project, do not need to update the graph 3855 project = this.getHostPreferences().lazyConfiguredProjectsFromExternalProject ? 3856 this.createConfiguredProjectWithDelayLoad(tsconfigFile, `Creating configured project in external project: ${proj.projectFileName}`) : 3857 this.createLoadAndUpdateConfiguredProject(tsconfigFile, `Creating configured project in external project: ${proj.projectFileName}`); 3858 } 3859 if (project && !contains(exisingConfigFiles, tsconfigFile)) { 3860 // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project 3861 project.addExternalProjectReference(); 3862 } 3863 } 3864 } 3865 else { 3866 // no config files - remove the item from the collection 3867 // Create external project and update its graph, do not delay update since 3868 // any file open operation needs all updated external projects 3869 this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName); 3870 const project = this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles); 3871 project.updateGraph(); 3872 } 3873 } 3874 3875 hasDeferredExtension() { 3876 for (const extension of this.hostConfiguration.extraFileExtensions!) { // TODO: GH#18217 3877 if (extension.scriptKind === ScriptKind.Deferred) { 3878 return true; 3879 } 3880 } 3881 3882 return false; 3883 } 3884 3885 configurePlugin(args: protocol.ConfigurePluginRequestArguments) { 3886 // For any projects that already have the plugin loaded, configure the plugin 3887 this.forEachEnabledProject(project => project.onPluginConfigurationChanged(args.pluginName, args.configuration)); 3888 3889 // Also save the current configuration to pass on to any projects that are yet to be loaded. 3890 // If a plugin is configured twice, only the latest configuration will be remembered. 3891 this.currentPluginConfigOverrides = this.currentPluginConfigOverrides || new Map(); 3892 this.currentPluginConfigOverrides.set(args.pluginName, args.configuration); 3893 } 3894 3895 /*@internal*/ 3896 getPackageJsonsVisibleToFile(fileName: string, rootDir?: string): readonly PackageJsonInfo[] { 3897 const packageJsonCache = this.packageJsonCache; 3898 const rootPath = rootDir && this.toPath(rootDir); 3899 const filePath = this.toPath(fileName); 3900 const result: PackageJsonInfo[] = []; 3901 const processDirectory = (directory: Path): boolean | undefined => { 3902 switch (packageJsonCache.directoryHasPackageJson(directory)) { 3903 // Sync and check same directory again 3904 case Ternary.Maybe: 3905 packageJsonCache.searchDirectoryAndAncestors(directory); 3906 return processDirectory(directory); 3907 // Check package.json 3908 case Ternary.True: 3909 const packageJsonFileName = combinePaths(directory, "package.json"); 3910 this.watchPackageJsonFile(packageJsonFileName as Path); 3911 const info = packageJsonCache.getInDirectory(directory); 3912 if (info) result.push(info); 3913 } 3914 if (rootPath && rootPath === directory) { 3915 return true; 3916 } 3917 }; 3918 3919 forEachAncestorDirectory(getDirectoryPath(filePath), processDirectory); 3920 return result; 3921 } 3922 3923 /*@internal*/ 3924 getNearestAncestorDirectoryWithPackageJson(fileName: string): string | undefined { 3925 return forEachAncestorDirectory(fileName, directory => { 3926 switch (this.packageJsonCache.directoryHasPackageJson(this.toPath(directory))) { 3927 case Ternary.True: return directory; 3928 case Ternary.False: return undefined; 3929 case Ternary.Maybe: 3930 return this.host.fileExists(combinePaths(directory, "package.json")) 3931 ? directory 3932 : undefined; 3933 } 3934 }); 3935 } 3936 3937 /*@internal*/ 3938 private watchPackageJsonFile(path: Path) { 3939 const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = new Map()); 3940 if (!watchers.has(path)) { 3941 this.invalidateProjectAutoImports(path); 3942 watchers.set(path, this.watchFactory.watchFile( 3943 path, 3944 (fileName, eventKind) => { 3945 const path = this.toPath(fileName); 3946 switch (eventKind) { 3947 case FileWatcherEventKind.Created: 3948 return Debug.fail(); 3949 case FileWatcherEventKind.Changed: 3950 this.packageJsonCache.addOrUpdate(path); 3951 this.invalidateProjectAutoImports(path); 3952 break; 3953 case FileWatcherEventKind.Deleted: 3954 this.packageJsonCache.delete(path); 3955 this.invalidateProjectAutoImports(path); 3956 watchers.get(path)!.close(); 3957 watchers.delete(path); 3958 } 3959 }, 3960 PollingInterval.Low, 3961 this.hostConfiguration.watchOptions, 3962 WatchType.PackageJsonFile, 3963 )); 3964 } 3965 } 3966 3967 /*@internal*/ 3968 private onAddPackageJson(path: Path) { 3969 this.packageJsonCache.addOrUpdate(path); 3970 this.watchPackageJsonFile(path); 3971 } 3972 3973 /*@internal*/ 3974 includePackageJsonAutoImports(): PackageJsonAutoImportPreference { 3975 switch (this.hostConfiguration.preferences.includePackageJsonAutoImports) { 3976 case "on": return PackageJsonAutoImportPreference.On; 3977 case "off": return PackageJsonAutoImportPreference.Off; 3978 default: return PackageJsonAutoImportPreference.Auto; 3979 } 3980 } 3981 3982 /*@internal*/ 3983 private invalidateProjectAutoImports(packageJsonPath: Path | undefined) { 3984 if (this.includePackageJsonAutoImports()) { 3985 this.configuredProjects.forEach(invalidate); 3986 this.inferredProjects.forEach(invalidate); 3987 this.externalProjects.forEach(invalidate); 3988 } 3989 function invalidate(project: Project) { 3990 if (!packageJsonPath || project.packageJsonsForAutoImport?.has(packageJsonPath)) { 3991 project.markAutoImportProviderAsDirty(); 3992 } 3993 } 3994 } 3995 } 3996 3997 /* @internal */ 3998 export type ScriptInfoOrConfig = ScriptInfo | TsConfigSourceFile; 3999 /* @internal */ 4000 export function isConfigFile(config: ScriptInfoOrConfig): config is TsConfigSourceFile { 4001 return (config as TsConfigSourceFile).kind !== undefined; 4002 } 4003 4004 function printProjectWithoutFileNames(project: Project) { 4005 project.print(/*writeProjectFileNames*/ false); 4006 } 4007} 4008