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