From cbac1ddfc73ca3b9d8741c1b51b74663a0f24695 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 9 Jan 2025 17:35:20 -0800 Subject: [PATCH] Skip `verifyCompilerOptions` when possible on program updates (#60754) Co-authored-by: Daniel Rosenwasser --- src/compiler/_namespaces/ts.ts | 1 + src/compiler/program.ts | 483 ++++------------------------- src/compiler/programDiagnostics.ts | 426 +++++++++++++++++++++++++ src/compiler/types.ts | 2 + src/compiler/utilities.ts | 100 +++++- 5 files changed, 581 insertions(+), 431 deletions(-) create mode 100644 src/compiler/programDiagnostics.ts diff --git a/src/compiler/_namespaces/ts.ts b/src/compiler/_namespaces/ts.ts index 94fb16857d38e..bd9684e481982 100644 --- a/src/compiler/_namespaces/ts.ts +++ b/src/compiler/_namespaces/ts.ts @@ -61,6 +61,7 @@ export * from "../transformer.js"; export * from "../emitter.js"; export * from "../watchUtilities.js"; export * from "../program.js"; +export * from "../programDiagnostics.js"; export * from "../builderStatePublic.js"; export * from "../builderState.js"; export * from "../builder.js"; diff --git a/src/compiler/program.ts b/src/compiler/program.ts index fbb0145507ce0..96fad44852c62 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -33,17 +33,16 @@ import { createCommentDirectivesMap, createCompilerDiagnostic, createCompilerDiagnosticFromMessageChain, - createDiagnosticCollection, createDiagnosticForNodeFromMessageChain, createDiagnosticForNodeInSourceFile, createDiagnosticForRange, createFileDiagnostic, - createFileDiagnosticFromMessageChain, createGetCanonicalFileName, createModeAwareCache, createModeAwareCacheKey, createModuleResolutionCache, createMultiMap, + createProgramDiagnostics, CreateProgramOptions, createSourceFile, CreateSourceFileOptions, @@ -75,7 +74,6 @@ import { equateStringsCaseInsensitive, equateStringsCaseSensitive, exclusivelyPrefixedNodeCoreModules, - explainIfFileIsRedirectAndImpliedFormat, ExportAssignment, ExportDeclaration, Extension, @@ -86,10 +84,7 @@ import { fileExtensionIsOneOf, FileIncludeKind, FileIncludeReason, - fileIncludeReasonToDiagnostics, - FilePreprocessingDiagnostics, FilePreprocessingDiagnosticsKind, - FilePreprocessingLibReferenceDiagnostic, FileReference, filter, find, @@ -104,6 +99,8 @@ import { forEachEmittedFile, forEachEntry, forEachKey, + forEachOptionsSyntaxByName, + forEachProjectReference, forEachPropertyAssignment, forEachResolvedProjectReference as ts_forEachResolvedProjectReference, forEachTsConfigPropArray, @@ -126,11 +123,9 @@ import { getIsolatedModules, getJSXImplicitImportBase, getJSXRuntimeImport, + getLibFileNameFromLibReference, getLineAndCharacterOfPosition, getLineStarts, - getMatchedFileSpec, - getMatchedIncludeSpec, - getNameOfScriptTarget, getNewLineCharacter, getNormalizedAbsolutePath, getNormalizedAbsolutePathWithoutRoot, @@ -139,14 +134,12 @@ import { getPackageScopeForPath, getPathFromPathComponents, getPositionOfLineAndCharacter, - getPropertyArrayElementValue, getResolvedModuleFromResolution, getResolvedTypeReferenceDirectiveFromResolution, getResolveJsonModule, getRootLength, getSetExternalModuleIndicator, getSourceFileOfNode, - getSpellingSuggestion, getStrictOptionValue, getSupportedExtensions, getSupportedExtensionsWithJsonIfResolveJsonModule, @@ -155,7 +148,6 @@ import { getTransformers, getTsBuildInfoEmitOutputFilePath, getTsConfigObjectLiteralExpression, - getTsConfigPropArrayElementValue, getTypesPackageName, HasChangedAutomaticTypeDirectiveNames, hasChangesInResolutions, @@ -215,7 +207,6 @@ import { JsonSourceFile, JsxEmit, length, - libMap, LibResolution, libs, mapDefined, @@ -246,6 +237,7 @@ import { noTransformers, ObjectLiteralExpression, OperationCanceledException, + optionDeclarations, optionsHaveChanges, PackageId, packageIdToPackageName, @@ -317,7 +309,6 @@ import { trace, tracing, tryCast, - TsConfigSourceFile, TypeChecker, typeDirectiveIsEqualTo, TypeReferenceDirectiveResolutionCache, @@ -1142,60 +1133,6 @@ export function loadWithModeAwareCache( - resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, - cb: (resolvedProjectReference: ResolvedProjectReference, parent: ResolvedProjectReference | undefined) => T | undefined, -): T | undefined { - return forEachProjectReference( - /*projectReferences*/ undefined, - resolvedProjectReferences, - (resolvedRef, parent) => resolvedRef && cb(resolvedRef, parent), - ); -} - -function forEachProjectReference( - projectReferences: readonly ProjectReference[] | undefined, - resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, - cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, parent: ResolvedProjectReference | undefined, index: number) => T | undefined, - cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined, -): T | undefined { - let seenResolvedRefs: Set | undefined; - return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined); - - function worker( - projectReferences: readonly ProjectReference[] | undefined, - resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, - parent: ResolvedProjectReference | undefined, - ): T | undefined { - // Visit project references first - if (cbRef) { - const result = cbRef(projectReferences, parent); - if (result) return result; - } - let skipChildren: Set | undefined; - return forEach( - resolvedProjectReferences, - (resolvedRef, index) => { - if (resolvedRef && seenResolvedRefs?.has(resolvedRef.sourceFile.path)) { - (skipChildren ??= new Set()).add(resolvedRef); - // ignore recursives - return undefined; - } - const result = cbResolvedRef(resolvedRef, parent, index); - if (result || !resolvedRef) return result; - (seenResolvedRefs ||= new Set()).add(resolvedRef.sourceFile.path); - }, - ) || forEach( - resolvedProjectReferences, - resolvedRef => - resolvedRef && !skipChildren?.has(resolvedRef) ? - worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef) : - undefined, - ); - } -} - /** @internal */ export const inferredTypesContainingFile = "__inferred type names__.ts"; @@ -1220,15 +1157,6 @@ export function getLibraryNameFromLibFileName(libFileName: string): string { return "@typescript/lib-" + path; } -function getLibNameFromLibReference(libReference: FileReference) { - return toFileNameLowerCase(libReference.fileName); -} - -function getLibFileNameFromLibReference(libReference: FileReference) { - const libName = getLibNameFromLibReference(libReference); - return libMap.get(libName); -} - /** @internal */ export function isReferencedFile(reason: FileIncludeReason | undefined): reason is ReferencedFile { switch (reason?.kind) { @@ -1536,16 +1464,6 @@ const plainJSErrors = new Set([ Diagnostics.This_condition_will_always_return_0_since_JavaScript_compares_objects_by_reference_not_value.code, ]); -interface LazyProgramDiagnosticExplainingFile { - file: SourceFile; - diagnostic: DiagnosticMessage; - args: DiagnosticArguments; -} -interface FileReasonToChainCache { - fileIncludeReasonDetails: DiagnosticMessageChain | undefined; - redirectInfo: DiagnosticMessageChain[] | undefined; - details?: DiagnosticMessageChain[]; -} /** * Determine if source file needs to be re-created even if its text hasn't changed */ @@ -1611,17 +1529,13 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg let processingOtherFiles: SourceFile[] | undefined; let files: SourceFile[]; let symlinks: SymlinkCache | undefined; - let commonSourceDirectory: string; let typeChecker: TypeChecker; let classifiableNames: Set<__String>; - let fileReasons = createMultiMap(); let filesWithReferencesProcessed: Set | undefined; - let fileReasonsToChain: Map | undefined; - let reasonToRelatedInfo: Map | undefined; let cachedBindAndCheckDiagnosticsForFile: Map | undefined; let cachedDeclarationDiagnosticsForFile: Map | undefined; + const programDiagnostics = createProgramDiagnostics(getCompilerOptionsObjectLiteralSyntax); - let fileProcessingDiagnostics: FilePreprocessingDiagnostics[] | undefined; let automaticTypeDirectiveNames: string[] | undefined; let automaticTypeDirectiveResolutions: ModeAwareCache; @@ -1661,13 +1575,8 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg let skipDefaultLib = options.noLib; const getDefaultLibraryFileName = memoize(() => host.getDefaultLibFileName(options)); const defaultLibraryPath = host.getDefaultLibLocation ? host.getDefaultLibLocation() : getDirectoryPath(getDefaultLibraryFileName()); - /** - * Diagnostics for the program - * Only add diagnostics directly if it always would be done irrespective of program structure reuse. - * Otherwise fileProcessingDiagnostics is correct locations so that the diagnostics can be reported in all structure use scenarios - */ - const programDiagnostics = createDiagnosticCollection(); - let lazyProgramDiagnosticExplainingFile: LazyProgramDiagnosticExplainingFile[] | undefined = []; + + let skipVerifyCompilerOptions = false; const currentDirectory = host.getCurrentDirectory(); const supportedExtensions = getSupportedExtensions(options); const supportedExtensionsWithJsonIfResolveJsonModule = getSupportedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); @@ -1990,7 +1899,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg getTypeCount: () => getTypeChecker().getTypeCount(), getInstantiationCount: () => getTypeChecker().getInstantiationCount(), getRelationCacheSizes: () => getTypeChecker().getRelationCacheSizes(), - getFileProcessingDiagnostics: () => fileProcessingDiagnostics, + getFileProcessingDiagnostics: () => programDiagnostics.getFileProcessingDiagnostics(), getAutomaticTypeDirectiveNames: () => automaticTypeDirectiveNames!, getAutomaticTypeDirectiveResolutions: () => automaticTypeDirectiveResolutions, isSourceFileFromExternalLibrary, @@ -2006,6 +1915,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg resolvedModules, resolvedTypeReferenceDirectiveNames, resolvedLibReferences, + getProgramDiagnosticsContainer: () => programDiagnostics, getResolvedModule, getResolvedModuleFromModuleSpecifier, getResolvedTypeReferenceDirective, @@ -2038,7 +1948,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg realpath: host.realpath?.bind(host), useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), getCanonicalFileName, - getFileIncludeReasons: () => fileReasons, + getFileIncludeReasons: () => programDiagnostics.getFileReasons(), structureIsReused, writeFile, getGlobalTypingsCacheLocation: maybeBind(host, host.getGlobalTypingsCacheLocation), @@ -2046,63 +1956,15 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg onProgramCreateComplete(); - verifyCompilerOptions(); + if (!skipVerifyCompilerOptions) { + verifyCompilerOptions(); + } performance.mark("afterProgram"); performance.measure("Program", "beforeProgram", "afterProgram"); tracing?.pop(); return program; - function updateAndGetProgramDiagnostics() { - if (lazyProgramDiagnosticExplainingFile) { - // Add file processingDiagnostics - fileProcessingDiagnostics?.forEach(diagnostic => { - switch (diagnostic.kind) { - case FilePreprocessingDiagnosticsKind.FilePreprocessingFileExplainingDiagnostic: - return programDiagnostics.add( - createDiagnosticExplainingFile( - diagnostic.file && getSourceFileByPath(diagnostic.file), - diagnostic.fileProcessingReason, - diagnostic.diagnostic, - diagnostic.args || emptyArray, - ), - ); - case FilePreprocessingDiagnosticsKind.FilePreprocessingLibReferenceDiagnostic: - return programDiagnostics.add(filePreprocessingLibreferenceDiagnostic(diagnostic)); - case FilePreprocessingDiagnosticsKind.ResolutionDiagnostics: - return diagnostic.diagnostics.forEach(d => programDiagnostics.add(d)); - default: - Debug.assertNever(diagnostic); - } - }); - lazyProgramDiagnosticExplainingFile.forEach(({ file, diagnostic, args }) => - programDiagnostics.add( - createDiagnosticExplainingFile(file, /*fileProcessingReason*/ undefined, diagnostic, args), - ) - ); - lazyProgramDiagnosticExplainingFile = undefined; - fileReasonsToChain = undefined; - reasonToRelatedInfo = undefined; - } - return programDiagnostics; - } - - function filePreprocessingLibreferenceDiagnostic({ reason }: FilePreprocessingLibReferenceDiagnostic) { - const { file, pos, end } = getReferencedFileLocation(program, reason) as ReferenceFileLocation; - const libReference = file.libReferenceDirectives[reason.index]; - const libName = getLibNameFromLibReference(libReference); - const unqualifiedLibName = removeSuffix(removePrefix(libName, "lib."), ".d.ts"); - const suggestion = getSpellingSuggestion(unqualifiedLibName, libs, identity); - return createFileDiagnostic( - file, - Debug.checkDefined(pos), - Debug.checkDefined(end) - pos, - suggestion ? Diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1 : Diagnostics.Cannot_find_lib_definition_for_0, - libName, - suggestion!, - ); - } - function getResolvedModule(file: SourceFile, moduleName: string, mode: ResolutionMode) { return resolvedModules?.get(file.path)?.get(moduleName, mode); } @@ -2170,7 +2032,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg function addResolutionDiagnostics(resolution: ResolvedModuleWithFailedLookupLocations | ResolvedTypeReferenceDirectiveWithFailedLookupLocations) { if (!resolution.resolutionDiagnostics?.length) return; - (fileProcessingDiagnostics ??= []).push({ + programDiagnostics.addFileProcessingDiagnostic({ kind: FilePreprocessingDiagnosticsKind.ResolutionDiagnostics, diagnostics: resolution.resolutionDiagnostics, }); @@ -2289,16 +2151,19 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg } function getCommonSourceDirectory() { - if (commonSourceDirectory === undefined) { - const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program)); - commonSourceDirectory = ts_getCommonSourceDirectory( - options, - () => mapDefined(emittedFiles, file => file.isDeclarationFile ? undefined : file.fileName), - currentDirectory, - getCanonicalFileName, - commonSourceDirectory => checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory), - ); + let commonSourceDirectory = programDiagnostics.getCommonSourceDirectory(); + if (commonSourceDirectory !== undefined) { + return commonSourceDirectory; } + const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program)); + commonSourceDirectory = ts_getCommonSourceDirectory( + options, + () => mapDefined(emittedFiles, file => file.isDeclarationFile ? undefined : file.fileName), + currentDirectory, + getCanonicalFileName, + commonSourceDirectory => checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory), + ); + programDiagnostics.setCommonSourceDirectory(commonSourceDirectory); return commonSourceDirectory; } @@ -2722,9 +2587,12 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg filesByName.set(path, filesByName.get(oldFile.path)); }); + const isConfigIdentical = oldOptions.configFile && oldOptions.configFile === options.configFile + || !oldOptions.configFile && !options.configFile && !optionsHaveChanges(oldOptions, options, optionDeclarations); + programDiagnostics.reuseStateFromOldProgram(oldProgram.getProgramDiagnosticsContainer(), isConfigIdentical); + skipVerifyCompilerOptions = isConfigIdentical; + files = newSourceFiles; - fileReasons = oldProgram.getFileIncludeReasons(); - fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); automaticTypeDirectiveNames = oldProgram.getAutomaticTypeDirectiveNames(); automaticTypeDirectiveResolutions = oldProgram.getAutomaticTypeDirectiveResolutions(); @@ -2985,7 +2853,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg return emptyArray; } - const programDiagnosticsInFile = updateAndGetProgramDiagnostics().getDiagnostics(sourceFile.fileName); + const programDiagnosticsInFile = programDiagnostics.getCombinedDiagnostics(program).getDiagnostics(sourceFile.fileName); if (!sourceFile.commentDirectives?.length) { return programDiagnosticsInFile; } @@ -3435,16 +3303,16 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg function getOptionsDiagnostics(): SortedReadonlyArray { return sortAndDeduplicateDiagnostics(concatenate( - updateAndGetProgramDiagnostics().getGlobalDiagnostics(), + programDiagnostics.getCombinedDiagnostics(program).getGlobalDiagnostics(), getOptionsDiagnosticsOfConfigFile(), )); } function getOptionsDiagnosticsOfConfigFile() { if (!options.configFile) return emptyArray; - let diagnostics = updateAndGetProgramDiagnostics().getDiagnostics(options.configFile.fileName); + let diagnostics = programDiagnostics.getCombinedDiagnostics(program).getDiagnostics(options.configFile.fileName); forEachResolvedProjectReference(resolvedRef => { - diagnostics = concatenate(diagnostics, updateAndGetProgramDiagnostics().getDiagnostics(resolvedRef.sourceFile.fileName)); + diagnostics = concatenate(diagnostics, programDiagnostics.getCombinedDiagnostics(program).getDiagnostics(resolvedRef.sourceFile.fileName)); }); return diagnostics; } @@ -3666,7 +3534,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg } function reportFileNamesDifferOnlyInCasingError(fileName: string, existingFile: SourceFile, reason: FileIncludeReason): void { - const hasExistingReasonToReportErrorOn = !isReferencedFile(reason) && some(fileReasons.get(existingFile.path), isReferencedFile); + const hasExistingReasonToReportErrorOn = !isReferencedFile(reason) && some(programDiagnostics.getFileReasons().get(existingFile.path), isReferencedFile); if (hasExistingReasonToReportErrorOn) { addFilePreprocessingFileExplainingDiagnostic(existingFile, reason, Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, [existingFile.fileName, fileName]); } @@ -3881,7 +3749,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg function addFileIncludeReason(file: SourceFile | undefined, reason: FileIncludeReason, checkExisting: boolean) { if (file && (!checkExisting || !isReferencedFile(reason) || !filesWithReferencesProcessed?.has(reason.file))) { - fileReasons.add(file.path, reason); + programDiagnostics.getFileReasons().add(file.path, reason); return true; } return false; @@ -4115,7 +3983,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg processRootFile(pathForLibFile(libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ true, { kind: FileIncludeKind.LibReferenceDirective, file: file.path, index }); } else { - (fileProcessingDiagnostics ||= []).push({ + programDiagnostics.addFileProcessingDiagnostic({ kind: FilePreprocessingDiagnosticsKind.FilePreprocessingLibReferenceDiagnostic, reason: { kind: FileIncludeKind.LibReferenceDirective, file: file.path, index }, }); @@ -4202,10 +4070,11 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg if (!sourceFile.isDeclarationFile) { const absoluteSourceFilePath = host.getCanonicalFileName(getNormalizedAbsolutePath(sourceFile.fileName, currentDirectory)); if (absoluteSourceFilePath.indexOf(absoluteRootDirectoryPath) !== 0) { - addLazyProgramDiagnosticExplainingFile( + programDiagnostics.addLazyConfigDiagnostic( sourceFile, Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, - [sourceFile.fileName, rootDirectory], + sourceFile.fileName, + rootDirectory, ); allFilesBelongToPath = false; } @@ -4308,7 +4177,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg const outputFile = options.outFile; if (!options.tsBuildInfoFile && options.incremental && !outputFile && !options.configFilePath) { - programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_incremental_can_only_be_specified_using_tsconfig_emitting_to_single_file_or_when_option_tsBuildInfoFile_is_specified)); + programDiagnostics.addConfigDiagnostic(createCompilerDiagnostic(Diagnostics.Option_incremental_can_only_be_specified_using_tsconfig_emitting_to_single_file_or_when_option_tsBuildInfoFile_is_specified)); } verifyDeprecatedCompilerOptions(); @@ -4320,10 +4189,11 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg for (const file of files) { // Ignore file that is not emitted if (sourceFileMayBeEmitted(file, program) && !rootPaths.has(file.path)) { - addLazyProgramDiagnosticExplainingFile( + programDiagnostics.addLazyConfigDiagnostic( file, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, - [file.fileName, options.configFilePath || ""], + file.fileName, + options.configFilePath || "", ); } } @@ -4410,7 +4280,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg else if (firstNonAmbientExternalModuleSourceFile && languageVersion < ScriptTarget.ES2015 && options.module === ModuleKind.None) { // We cannot use createDiagnosticFromNode because nodes do not have parents yet const span = getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, typeof firstNonAmbientExternalModuleSourceFile.externalModuleIndicator === "boolean" ? firstNonAmbientExternalModuleSourceFile : firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); - programDiagnostics.add(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_use_imports_exports_or_module_augmentations_when_module_is_none)); + programDiagnostics.addConfigDiagnostic(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_use_imports_exports_or_module_augmentations_when_module_is_none)); } // Cannot specify module gen that isn't amd or system with --out @@ -4420,7 +4290,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg } else if (options.module === undefined && firstNonAmbientExternalModuleSourceFile) { const span = getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, typeof firstNonAmbientExternalModuleSourceFile.externalModuleIndicator === "boolean" ? firstNonAmbientExternalModuleSourceFile : firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); - programDiagnostics.add(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_compile_modules_using_option_0_unless_the_module_flag_is_amd_or_system, "outFile")); + programDiagnostics.addConfigDiagnostic(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_compile_modules_using_option_0_unless_the_module_flag_is_amd_or_system, "outFile")); } } @@ -4699,123 +4569,8 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg }); } - function createDiagnosticExplainingFile(file: SourceFile | undefined, fileProcessingReason: FileIncludeReason | undefined, diagnostic: DiagnosticMessage, args: DiagnosticArguments): Diagnostic { - let seenReasons: Set | undefined; - const reasons = file && fileReasons.get(file.path); - let fileIncludeReasons: DiagnosticMessageChain[] | undefined; - let relatedInfo: DiagnosticWithLocation[] | undefined; - let locationReason = isReferencedFile(fileProcessingReason) ? fileProcessingReason : undefined; - let fileIncludeReasonDetails: DiagnosticMessageChain | undefined; - let redirectInfo: DiagnosticMessageChain[] | undefined; - let cachedChain = file && fileReasonsToChain?.get(file.path); - let chain: DiagnosticMessageChain | undefined; - if (cachedChain) { - if (cachedChain.fileIncludeReasonDetails) { - seenReasons = new Set(reasons); - reasons?.forEach(populateRelatedInfo); - } - else { - reasons?.forEach(processReason); - } - redirectInfo = cachedChain.redirectInfo; - } - else { - reasons?.forEach(processReason); - redirectInfo = file && explainIfFileIsRedirectAndImpliedFormat(file, getCompilerOptionsForFile(file)); - } - - if (fileProcessingReason) processReason(fileProcessingReason); - const processedExtraReason = seenReasons?.size !== reasons?.length; - - // If we have location and there is only one reason file is in which is the location, dont add details for file include - if (locationReason && seenReasons?.size === 1) seenReasons = undefined; - - if (seenReasons && cachedChain) { - if (cachedChain.details && !processedExtraReason) { - chain = chainDiagnosticMessages(cachedChain.details, diagnostic, ...args || emptyArray); - } - else if (cachedChain.fileIncludeReasonDetails) { - if (!processedExtraReason) { - if (!cachedFileIncludeDetailsHasProcessedExtraReason()) { - fileIncludeReasonDetails = cachedChain.fileIncludeReasonDetails; - } - else { - fileIncludeReasons = cachedChain.fileIncludeReasonDetails.next!.slice(0, reasons!.length); - } - } - else { - if (!cachedFileIncludeDetailsHasProcessedExtraReason()) { - fileIncludeReasons = [...cachedChain.fileIncludeReasonDetails.next!, fileIncludeReasons![0]]; - } - else { - fileIncludeReasons = append(cachedChain.fileIncludeReasonDetails.next!.slice(0, reasons!.length), fileIncludeReasons![0]); - } - } - } - } - - if (!chain) { - if (!fileIncludeReasonDetails) fileIncludeReasonDetails = seenReasons && chainDiagnosticMessages(fileIncludeReasons, Diagnostics.The_file_is_in_the_program_because_Colon); - chain = chainDiagnosticMessages( - redirectInfo ? - fileIncludeReasonDetails ? [fileIncludeReasonDetails, ...redirectInfo] : redirectInfo : - fileIncludeReasonDetails, - diagnostic, - ...args || emptyArray, - ); - } - - // This is chain's next contains: - // - File is in program because: - // - Files reasons listed - // - extra reason if its not already processed - this happens in case sensitive file system where files differ in casing and we are giving reasons for two files so reason is not in file's reason - // fyi above whole secton is ommited if we have single reason and we are reporting at that reason's location - // - redirect and additional information about file - // So cache result if we havent ommited file include reasons - if (file) { - if (cachedChain) { - // Cache new fileIncludeDetails if we have update - // Or if we had cached with more details than the reasons - if (!cachedChain.fileIncludeReasonDetails || (!processedExtraReason && fileIncludeReasonDetails)) { - cachedChain.fileIncludeReasonDetails = fileIncludeReasonDetails; - } - } - else { - (fileReasonsToChain ??= new Map()).set(file.path, cachedChain = { fileIncludeReasonDetails, redirectInfo }); - } - // If we didnt compute extra file include reason , cache the details to use directly - if (!cachedChain.details && !processedExtraReason) cachedChain.details = chain.next; - } - - const location = locationReason && getReferencedFileLocation(program, locationReason); - return location && isReferenceFileLocation(location) ? - createFileDiagnosticFromMessageChain(location.file, location.pos, location.end - location.pos, chain, relatedInfo) : - createCompilerDiagnosticFromMessageChain(chain, relatedInfo); - - function processReason(reason: FileIncludeReason) { - if (seenReasons?.has(reason)) return; - (seenReasons ??= new Set()).add(reason); - (fileIncludeReasons ??= []).push(fileIncludeReasonToDiagnostics(program, reason)); - populateRelatedInfo(reason); - } - - function populateRelatedInfo(reason: FileIncludeReason) { - if (!locationReason && isReferencedFile(reason)) { - // Report error at first reference file or file currently in processing and dont report in related information - locationReason = reason; - } - else if (locationReason !== reason) { - relatedInfo = append(relatedInfo, getFileIncludeReasonToRelatedInformation(reason)); - } - } - - function cachedFileIncludeDetailsHasProcessedExtraReason() { - return cachedChain!.fileIncludeReasonDetails!.next?.length !== reasons?.length; - } - } - function addFilePreprocessingFileExplainingDiagnostic(file: SourceFile | undefined, fileProcessingReason: FileIncludeReason, diagnostic: DiagnosticMessage, args: DiagnosticArguments) { - (fileProcessingDiagnostics ||= []).push({ + programDiagnostics.addFileProcessingDiagnostic({ kind: FilePreprocessingDiagnosticsKind.FilePreprocessingFileExplainingDiagnostic, file: file && file.path, fileProcessingReason, @@ -4824,111 +4579,6 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg }); } - function addLazyProgramDiagnosticExplainingFile(file: SourceFile, diagnostic: DiagnosticMessage, args: DiagnosticArguments) { - lazyProgramDiagnosticExplainingFile!.push({ file, diagnostic, args }); - } - - function getFileIncludeReasonToRelatedInformation(reason: FileIncludeReason) { - let relatedInfo = reasonToRelatedInfo?.get(reason); - if (relatedInfo === undefined) (reasonToRelatedInfo ??= new Map()).set(reason, relatedInfo = fileIncludeReasonToRelatedInformation(reason) ?? false); - return relatedInfo || undefined; - } - - function fileIncludeReasonToRelatedInformation(reason: FileIncludeReason): DiagnosticWithLocation | undefined { - if (isReferencedFile(reason)) { - const referenceLocation = getReferencedFileLocation(program, reason); - let message: DiagnosticMessage; - switch (reason.kind) { - case FileIncludeKind.Import: - message = Diagnostics.File_is_included_via_import_here; - break; - case FileIncludeKind.ReferenceFile: - message = Diagnostics.File_is_included_via_reference_here; - break; - case FileIncludeKind.TypeReferenceDirective: - message = Diagnostics.File_is_included_via_type_library_reference_here; - break; - case FileIncludeKind.LibReferenceDirective: - message = Diagnostics.File_is_included_via_library_reference_here; - break; - default: - Debug.assertNever(reason); - } - return isReferenceFileLocation(referenceLocation) ? createFileDiagnostic( - referenceLocation.file, - referenceLocation.pos, - referenceLocation.end - referenceLocation.pos, - message, - ) : undefined; - } - - if (!options.configFile) return undefined; - let configFileNode: Node | undefined; - let message: DiagnosticMessage; - switch (reason.kind) { - case FileIncludeKind.RootFile: - if (!options.configFile.configFileSpecs) return undefined; - const fileName = getNormalizedAbsolutePath(rootNames[reason.index], currentDirectory); - const matchedByFiles = getMatchedFileSpec(program, fileName); - if (matchedByFiles) { - configFileNode = getTsConfigPropArrayElementValue(options.configFile, "files", matchedByFiles); - message = Diagnostics.File_is_matched_by_files_list_specified_here; - break; - } - const matchedByInclude = getMatchedIncludeSpec(program, fileName); - // Could be additional files specified as roots - if (!matchedByInclude || !isString(matchedByInclude)) return undefined; - configFileNode = getTsConfigPropArrayElementValue(options.configFile, "include", matchedByInclude); - message = Diagnostics.File_is_matched_by_include_pattern_specified_here; - break; - case FileIncludeKind.SourceFromProjectReference: - case FileIncludeKind.OutputFromProjectReference: - const referencedResolvedRef = Debug.checkDefined(resolvedProjectReferences?.[reason.index]); - const referenceInfo = forEachProjectReference( - projectReferences, - resolvedProjectReferences, - (resolvedRef, parent, index) => - resolvedRef === referencedResolvedRef ? - { sourceFile: parent?.sourceFile || options.configFile!, index } : - undefined, - ); - if (!referenceInfo) return undefined; - const { sourceFile, index } = referenceInfo; - const referencesSyntax = forEachTsConfigPropArray(sourceFile as TsConfigSourceFile, "references", property => isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); - return referencesSyntax && referencesSyntax.elements.length > index ? - createDiagnosticForNodeInSourceFile( - sourceFile, - referencesSyntax.elements[index], - reason.kind === FileIncludeKind.OutputFromProjectReference ? - Diagnostics.File_is_output_from_referenced_project_specified_here : - Diagnostics.File_is_source_from_referenced_project_specified_here, - ) : - undefined; - case FileIncludeKind.AutomaticTypeDirectiveFile: - if (!options.types) return undefined; - configFileNode = getOptionsSyntaxByArrayElementValue("types", reason.typeReference); - message = Diagnostics.File_is_entry_point_of_type_library_specified_here; - break; - case FileIncludeKind.LibFile: - if (reason.index !== undefined) { - configFileNode = getOptionsSyntaxByArrayElementValue("lib", options.lib![reason.index]); - message = Diagnostics.File_is_library_specified_here; - break; - } - const target = getNameOfScriptTarget(getEmitScriptTarget(options)); - configFileNode = target ? getOptionsSyntaxByValue("target", target) : undefined; - message = Diagnostics.File_is_default_library_for_target_specified_here; - break; - default: - Debug.assertNever(reason); - } - return configFileNode && createDiagnosticForNodeInSourceFile( - options.configFile, - configFileNode, - message, - ); - } - function verifyProjectReferences() { const buildInfoPath = !options.suppressOutputPathCheck ? getTsBuildInfoEmitOutputFilePath(options) : undefined; forEachProjectReference( @@ -4966,7 +4616,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg forEachPropertyAssignment(pathProp.initializer, key, keyProps => { const initializer = keyProps.initializer; if (isArrayLiteralExpression(initializer) && initializer.elements.length > valueIndex) { - programDiagnostics.add(createDiagnosticForNodeInSourceFile(options.configFile!, initializer.elements[valueIndex], message, ...args)); + programDiagnostics.addConfigDiagnostic(createDiagnosticForNodeInSourceFile(options.configFile!, initializer.elements[valueIndex], message, ...args)); needCompilerDiagnostic = false; } }); @@ -4999,21 +4649,8 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg } } - function forEachOptionsSyntaxByName(name: string, callback: (prop: PropertyAssignment) => T | undefined): T | undefined { - return forEachPropertyAssignment(getCompilerOptionsObjectLiteralSyntax(), name, callback); - } - function forEachOptionPathsSyntax(callback: (prop: PropertyAssignment) => T | undefined) { - return forEachOptionsSyntaxByName("paths", callback); - } - - function getOptionsSyntaxByValue(name: string, value: string) { - return forEachOptionsSyntaxByName(name, property => isStringLiteral(property.initializer) && property.initializer.text === value ? property.initializer : undefined); - } - - function getOptionsSyntaxByArrayElementValue(name: string, value: string) { - const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); - return compilerOptionsObjectLiteralSyntax && getPropertyArrayElementValue(compilerOptionsObjectLiteralSyntax, name, value); + return forEachOptionsSyntaxByName(getCompilerOptionsObjectLiteralSyntax(), "paths", callback); } function createDiagnosticForOptionName(message: DiagnosticMessage, option1: string, option2?: string, option3?: string) { @@ -5028,10 +4665,10 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg function createDiagnosticForReference(sourceFile: JsonSourceFile | undefined, index: number, message: DiagnosticMessage, ...args: DiagnosticArguments) { const referencesSyntax = forEachTsConfigPropArray(sourceFile || options.configFile, "references", property => isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); if (referencesSyntax && referencesSyntax.elements.length > index) { - programDiagnostics.add(createDiagnosticForNodeInSourceFile(sourceFile || options.configFile!, referencesSyntax.elements[index], message, ...args)); + programDiagnostics.addConfigDiagnostic(createDiagnosticForNodeInSourceFile(sourceFile || options.configFile!, referencesSyntax.elements[index], message, ...args)); } else { - programDiagnostics.add(createCompilerDiagnostic(message, ...args)); + programDiagnostics.addConfigDiagnostic(createCompilerDiagnostic(message, ...args)); } } @@ -5055,18 +4692,18 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg if (compilerOptionsProperty) { // eslint-disable-next-line local/no-in-operator if ("messageText" in message) { - programDiagnostics.add(createDiagnosticForNodeFromMessageChain(options.configFile!, compilerOptionsProperty.name, message)); + programDiagnostics.addConfigDiagnostic(createDiagnosticForNodeFromMessageChain(options.configFile!, compilerOptionsProperty.name, message)); } else { - programDiagnostics.add(createDiagnosticForNodeInSourceFile(options.configFile!, compilerOptionsProperty.name, message, ...args)); + programDiagnostics.addConfigDiagnostic(createDiagnosticForNodeInSourceFile(options.configFile!, compilerOptionsProperty.name, message, ...args)); } } // eslint-disable-next-line local/no-in-operator else if ("messageText" in message) { - programDiagnostics.add(createCompilerDiagnosticFromMessageChain(message)); + programDiagnostics.addConfigDiagnostic(createCompilerDiagnosticFromMessageChain(message)); } else { - programDiagnostics.add(createCompilerDiagnostic(message, ...args)); + programDiagnostics.addConfigDiagnostic(createCompilerDiagnostic(message, ...args)); } } @@ -5097,10 +4734,10 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg forEachPropertyAssignment(objectLiteral, key1, prop => { // eslint-disable-next-line local/no-in-operator if ("messageText" in message) { - programDiagnostics.add(createDiagnosticForNodeFromMessageChain(options.configFile!, onKey ? prop.name : prop.initializer, message)); + programDiagnostics.addConfigDiagnostic(createDiagnosticForNodeFromMessageChain(options.configFile!, onKey ? prop.name : prop.initializer, message)); } else { - programDiagnostics.add(createDiagnosticForNodeInSourceFile(options.configFile!, onKey ? prop.name : prop.initializer, message, ...args)); + programDiagnostics.addConfigDiagnostic(createDiagnosticForNodeInSourceFile(options.configFile!, onKey ? prop.name : prop.initializer, message, ...args)); } needsCompilerDiagnostic = true; }, key2); @@ -5109,7 +4746,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg function blockEmittingOfFile(emitFileName: string, diag: Diagnostic) { hasEmitBlockingDiagnostics.set(toPath(emitFileName), true); - programDiagnostics.add(diag); + programDiagnostics.addConfigDiagnostic(diag); } function isEmittedFile(file: string): boolean { diff --git a/src/compiler/programDiagnostics.ts b/src/compiler/programDiagnostics.ts new file mode 100644 index 0000000000000..ee4d1161eaa19 --- /dev/null +++ b/src/compiler/programDiagnostics.ts @@ -0,0 +1,426 @@ +import { + append, + chainDiagnosticMessages, + createCompilerDiagnosticFromMessageChain, + createDiagnosticCollection, + createDiagnosticForNodeInSourceFile, + createFileDiagnostic, + createFileDiagnosticFromMessageChain, + createMultiMap, + Debug, + Diagnostic, + DiagnosticArguments, + DiagnosticCollection, + DiagnosticMessage, + DiagnosticMessageChain, + Diagnostics, + DiagnosticWithLocation, + emptyArray, + explainIfFileIsRedirectAndImpliedFormat, + FileIncludeKind, + FileIncludeReason, + fileIncludeReasonToDiagnostics, + FilePreprocessingDiagnostics, + FilePreprocessingDiagnosticsKind, + FilePreprocessingLibReferenceDiagnostic, + forEachProjectReference, + forEachTsConfigPropArray, + getEmitScriptTarget, + getLibNameFromLibReference, + getMatchedFileSpec, + getMatchedIncludeSpec, + getNameOfScriptTarget, + getNormalizedAbsolutePath, + getOptionsSyntaxByArrayElementValue, + getOptionsSyntaxByValue, + getReferencedFileLocation, + getSpellingSuggestion, + getTsConfigPropArrayElementValue, + identity, + isArrayLiteralExpression, + isReferencedFile, + isReferenceFileLocation, + isString, + libs, + MultiMap, + Node, + ObjectLiteralExpression, + Path, + Program, + ReferenceFileLocation, + removePrefix, + removeSuffix, + SourceFile, + TsConfigSourceFile, +} from "./_namespaces/ts.js"; + +interface FileReasonToChainCache { + fileIncludeReasonDetails: DiagnosticMessageChain | undefined; + redirectInfo: DiagnosticMessageChain[] | undefined; + details?: DiagnosticMessageChain[]; +} + +/** @internal */ +export interface LazyConfigDiagnostic { + file: SourceFile; + diagnostic: DiagnosticMessage; + args: DiagnosticArguments; +} + +/** @internal */ +export interface ProgramDiagnostics { + addConfigDiagnostic(diag: Diagnostic): void; + addLazyConfigDiagnostic(file: SourceFile, message: DiagnosticMessage, ...args: DiagnosticArguments): void; + addFileProcessingDiagnostic(diag: FilePreprocessingDiagnostics): void; + setCommonSourceDirectory(directory: string): void; + + reuseStateFromOldProgram(oldProgramDiagnostics: ProgramDiagnostics, isConfigIdentical: boolean): void; + + getFileProcessingDiagnostics(): FilePreprocessingDiagnostics[] | undefined; + getFileReasons(): MultiMap; + getConfigDiagnostics(): DiagnosticCollection | undefined; + getLazyConfigDiagnostics(): LazyConfigDiagnostic[] | undefined; + getCommonSourceDirectory(): string | undefined; + getCombinedDiagnostics(program: Program): DiagnosticCollection; +} + +/** @internal */ +export function createProgramDiagnostics(getCompilerOptionsObjectLiteralSyntax: () => ObjectLiteralExpression | undefined): ProgramDiagnostics { + // Only valid for a single program. The individual components can be reused under certain + // circumstances (described below), but during `getCombinedDiagnostics`, position information + // is applied to the diagnostics, so they cannot be shared between programs. + let computedDiagnostics: DiagnosticCollection | undefined; + + // Valid between programs if `StructureIsReused.Completely`. + let fileReasons = createMultiMap(); + let fileProcessingDiagnostics: FilePreprocessingDiagnostics[] | undefined; + + // Valid between programs if `StructureIsReused.Completely` and config file is identical. + let commonSourceDirectory: string | undefined; + let configDiagnostics: DiagnosticCollection | undefined; + let lazyConfigDiagnostics: LazyConfigDiagnostic[] | undefined; + + // Local state, only used during getDiagnostics + let fileReasonsToChain: Map | undefined; + let reasonToRelatedInfo: Map | undefined; + + return { + addConfigDiagnostic(diag) { + Debug.assert(computedDiagnostics === undefined, "Cannot modify program diagnostic state after requesting combined diagnostics"); + (configDiagnostics ??= createDiagnosticCollection()).add(diag); + }, + addLazyConfigDiagnostic(file, message, ...args) { + Debug.assert(computedDiagnostics === undefined, "Cannot modify program diagnostic state after requesting combined diagnostics"); + (lazyConfigDiagnostics ??= []).push({ file, diagnostic: message, args }); + }, + addFileProcessingDiagnostic(diag) { + Debug.assert(computedDiagnostics === undefined, "Cannot modify program diagnostic state after requesting combined diagnostics"); + (fileProcessingDiagnostics ??= []).push(diag); + }, + setCommonSourceDirectory(directory) { + commonSourceDirectory = directory; + }, + + reuseStateFromOldProgram(oldProgramDiagnostics, isConfigIdentical) { + fileReasons = oldProgramDiagnostics.getFileReasons(); + fileProcessingDiagnostics = oldProgramDiagnostics.getFileProcessingDiagnostics(); + if (isConfigIdentical) { + commonSourceDirectory = oldProgramDiagnostics.getCommonSourceDirectory(); + configDiagnostics = oldProgramDiagnostics.getConfigDiagnostics(); + lazyConfigDiagnostics = oldProgramDiagnostics.getLazyConfigDiagnostics(); + } + }, + + getFileProcessingDiagnostics() { + return fileProcessingDiagnostics; + }, + getFileReasons() { + return fileReasons; + }, + getCommonSourceDirectory() { + return commonSourceDirectory; + }, + getConfigDiagnostics() { + return configDiagnostics; + }, + getLazyConfigDiagnostics() { + return lazyConfigDiagnostics; + }, + getCombinedDiagnostics(program: Program) { + if (computedDiagnostics) { + return computedDiagnostics; + } + + computedDiagnostics = createDiagnosticCollection(); + + configDiagnostics?.getDiagnostics().forEach(d => computedDiagnostics!.add(d)); + + fileProcessingDiagnostics?.forEach(diagnostic => { + switch (diagnostic.kind) { + case FilePreprocessingDiagnosticsKind.FilePreprocessingFileExplainingDiagnostic: + return computedDiagnostics!.add( + createDiagnosticExplainingFile( + program, + diagnostic.file && program.getSourceFileByPath(diagnostic.file), + diagnostic.fileProcessingReason, + diagnostic.diagnostic, + diagnostic.args || emptyArray, + ), + ); + case FilePreprocessingDiagnosticsKind.FilePreprocessingLibReferenceDiagnostic: + return computedDiagnostics!.add(filePreprocessingLibreferenceDiagnostic(program, diagnostic)); + case FilePreprocessingDiagnosticsKind.ResolutionDiagnostics: + return diagnostic.diagnostics.forEach(d => computedDiagnostics!.add(d)); + default: + Debug.assertNever(diagnostic); + } + }); + lazyConfigDiagnostics?.forEach(({ file, diagnostic, args }) => + computedDiagnostics!.add( + createDiagnosticExplainingFile(program, file, /*fileProcessingReason*/ undefined, diagnostic, args), + ) + ); + fileReasonsToChain = undefined; + reasonToRelatedInfo = undefined; + return computedDiagnostics; + }, + }; + + function filePreprocessingLibreferenceDiagnostic(program: Program, { reason }: FilePreprocessingLibReferenceDiagnostic) { + const { file, pos, end } = getReferencedFileLocation(program, reason) as ReferenceFileLocation; + const libReference = file.libReferenceDirectives[reason.index]; + const libName = getLibNameFromLibReference(libReference); + const unqualifiedLibName = removeSuffix(removePrefix(libName, "lib."), ".d.ts"); + const suggestion = getSpellingSuggestion(unqualifiedLibName, libs, identity); + return createFileDiagnostic( + file, + Debug.checkDefined(pos), + Debug.checkDefined(end) - pos, + suggestion ? Diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1 : Diagnostics.Cannot_find_lib_definition_for_0, + libName, + suggestion!, + ); + } + + function createDiagnosticExplainingFile(program: Program, file: SourceFile | undefined, fileProcessingReason: FileIncludeReason | undefined, diagnostic: DiagnosticMessage, args: DiagnosticArguments): Diagnostic { + let seenReasons: Set | undefined; + let fileIncludeReasons: DiagnosticMessageChain[] | undefined; + let relatedInfo: DiagnosticWithLocation[] | undefined; + let fileIncludeReasonDetails: DiagnosticMessageChain | undefined; + let redirectInfo: DiagnosticMessageChain[] | undefined; + let chain: DiagnosticMessageChain | undefined; + + const reasons = file && fileReasons.get(file.path); + let locationReason = isReferencedFile(fileProcessingReason) ? fileProcessingReason : undefined; + let cachedChain = file && fileReasonsToChain?.get(file.path); + if (cachedChain) { + if (cachedChain.fileIncludeReasonDetails) { + seenReasons = new Set(reasons); + reasons?.forEach(populateRelatedInfo); + } + else { + reasons?.forEach(processReason); + } + redirectInfo = cachedChain.redirectInfo; + } + else { + reasons?.forEach(processReason); + redirectInfo = file && explainIfFileIsRedirectAndImpliedFormat(file, program.getCompilerOptionsForFile(file)); + } + + if (fileProcessingReason) processReason(fileProcessingReason); + const processedExtraReason = seenReasons?.size !== reasons?.length; + + // If we have location and there is only one reason file is in which is the location, dont add details for file include + if (locationReason && seenReasons?.size === 1) seenReasons = undefined; + + if (seenReasons && cachedChain) { + if (cachedChain.details && !processedExtraReason) { + chain = chainDiagnosticMessages(cachedChain.details, diagnostic, ...args ?? emptyArray); + } + else if (cachedChain.fileIncludeReasonDetails) { + if (!processedExtraReason) { + if (!cachedFileIncludeDetailsHasProcessedExtraReason()) { + fileIncludeReasonDetails = cachedChain.fileIncludeReasonDetails; + } + else { + fileIncludeReasons = cachedChain.fileIncludeReasonDetails.next!.slice(0, reasons!.length); + } + } + else { + if (!cachedFileIncludeDetailsHasProcessedExtraReason()) { + fileIncludeReasons = [...cachedChain.fileIncludeReasonDetails.next!, fileIncludeReasons![0]]; + } + else { + fileIncludeReasons = append(cachedChain.fileIncludeReasonDetails.next!.slice(0, reasons!.length), fileIncludeReasons![0]); + } + } + } + } + + if (!chain) { + if (!fileIncludeReasonDetails) fileIncludeReasonDetails = seenReasons && chainDiagnosticMessages(fileIncludeReasons, Diagnostics.The_file_is_in_the_program_because_Colon); + chain = chainDiagnosticMessages( + redirectInfo ? + fileIncludeReasonDetails ? [fileIncludeReasonDetails, ...redirectInfo] : redirectInfo : + fileIncludeReasonDetails, + diagnostic, + ...args || emptyArray, + ); + } + + // This is chain's next contains: + // - File is in program because: + // - Files reasons listed + // - extra reason if its not already processed - this happens in case sensitive file system where files differ in casing and we are giving reasons for two files so reason is not in file's reason + // fyi above whole secton is ommited if we have single reason and we are reporting at that reason's location + // - redirect and additional information about file + // So cache result if we havent ommited file include reasons + if (file) { + if (cachedChain) { + // Cache new fileIncludeDetails if we have update + // Or if we had cached with more details than the reasons + if (!cachedChain.fileIncludeReasonDetails || (!processedExtraReason && fileIncludeReasonDetails)) { + cachedChain.fileIncludeReasonDetails = fileIncludeReasonDetails; + } + } + else { + (fileReasonsToChain ??= new Map()).set(file.path, cachedChain = { fileIncludeReasonDetails, redirectInfo }); + } + // If we didnt compute extra file include reason , cache the details to use directly + if (!cachedChain.details && !processedExtraReason) cachedChain.details = chain.next; + } + + const location = locationReason && getReferencedFileLocation(program, locationReason); + return location && isReferenceFileLocation(location) ? + createFileDiagnosticFromMessageChain(location.file, location.pos, location.end - location.pos, chain, relatedInfo) : + createCompilerDiagnosticFromMessageChain(chain, relatedInfo); + + function processReason(reason: FileIncludeReason) { + if (seenReasons?.has(reason)) return; + (seenReasons ??= new Set()).add(reason); + (fileIncludeReasons ??= []).push(fileIncludeReasonToDiagnostics(program, reason)); + populateRelatedInfo(reason); + } + + function populateRelatedInfo(reason: FileIncludeReason) { + if (!locationReason && isReferencedFile(reason)) { + // Report error at first reference file or file currently in processing and dont report in related information + locationReason = reason; + } + else if (locationReason !== reason) { + relatedInfo = append(relatedInfo, getFileIncludeReasonToRelatedInformation(program, reason)); + } + } + + function cachedFileIncludeDetailsHasProcessedExtraReason() { + return cachedChain!.fileIncludeReasonDetails!.next?.length !== reasons?.length; + } + } + + function getFileIncludeReasonToRelatedInformation(program: Program, reason: FileIncludeReason) { + let relatedInfo = reasonToRelatedInfo?.get(reason); + if (relatedInfo === undefined) (reasonToRelatedInfo ??= new Map()).set(reason, relatedInfo = fileIncludeReasonToRelatedInformation(program, reason) ?? false); + return relatedInfo || undefined; + } + + function fileIncludeReasonToRelatedInformation(program: Program, reason: FileIncludeReason): DiagnosticWithLocation | undefined { + if (isReferencedFile(reason)) { + const referenceLocation = getReferencedFileLocation(program, reason); + let message: DiagnosticMessage; + switch (reason.kind) { + case FileIncludeKind.Import: + message = Diagnostics.File_is_included_via_import_here; + break; + case FileIncludeKind.ReferenceFile: + message = Diagnostics.File_is_included_via_reference_here; + break; + case FileIncludeKind.TypeReferenceDirective: + message = Diagnostics.File_is_included_via_type_library_reference_here; + break; + case FileIncludeKind.LibReferenceDirective: + message = Diagnostics.File_is_included_via_library_reference_here; + break; + default: + Debug.assertNever(reason); + } + return isReferenceFileLocation(referenceLocation) ? createFileDiagnostic( + referenceLocation.file, + referenceLocation.pos, + referenceLocation.end - referenceLocation.pos, + message, + ) : undefined; + } + + const currentDirectory = program.getCurrentDirectory(); + const rootNames = program.getRootFileNames(); + const options = program.getCompilerOptions(); + if (!options.configFile) return undefined; + let configFileNode: Node | undefined; + let message: DiagnosticMessage; + switch (reason.kind) { + case FileIncludeKind.RootFile: + if (!options.configFile.configFileSpecs) return undefined; + const fileName = getNormalizedAbsolutePath(rootNames[reason.index], currentDirectory); + const matchedByFiles = getMatchedFileSpec(program, fileName); + if (matchedByFiles) { + configFileNode = getTsConfigPropArrayElementValue(options.configFile, "files", matchedByFiles); + message = Diagnostics.File_is_matched_by_files_list_specified_here; + break; + } + const matchedByInclude = getMatchedIncludeSpec(program, fileName); + // Could be additional files specified as roots + if (!matchedByInclude || !isString(matchedByInclude)) return undefined; + configFileNode = getTsConfigPropArrayElementValue(options.configFile, "include", matchedByInclude); + message = Diagnostics.File_is_matched_by_include_pattern_specified_here; + break; + case FileIncludeKind.SourceFromProjectReference: + case FileIncludeKind.OutputFromProjectReference: + const resolvedProjectReferences = program.getResolvedProjectReferences(); + const projectReferences = program.getProjectReferences(); + const referencedResolvedRef = Debug.checkDefined(resolvedProjectReferences?.[reason.index]); + const referenceInfo = forEachProjectReference( + projectReferences, + resolvedProjectReferences, + (resolvedRef, parent, index) => + resolvedRef === referencedResolvedRef ? + { sourceFile: parent?.sourceFile || options.configFile!, index } : + undefined, + ); + if (!referenceInfo) return undefined; + const { sourceFile, index } = referenceInfo; + const referencesSyntax = forEachTsConfigPropArray(sourceFile as TsConfigSourceFile, "references", property => isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); + return referencesSyntax && referencesSyntax.elements.length > index ? + createDiagnosticForNodeInSourceFile( + sourceFile, + referencesSyntax.elements[index], + reason.kind === FileIncludeKind.OutputFromProjectReference ? + Diagnostics.File_is_output_from_referenced_project_specified_here : + Diagnostics.File_is_source_from_referenced_project_specified_here, + ) : + undefined; + case FileIncludeKind.AutomaticTypeDirectiveFile: + if (!options.types) return undefined; + configFileNode = getOptionsSyntaxByArrayElementValue(getCompilerOptionsObjectLiteralSyntax(), "types", reason.typeReference); + message = Diagnostics.File_is_entry_point_of_type_library_specified_here; + break; + case FileIncludeKind.LibFile: + if (reason.index !== undefined) { + configFileNode = getOptionsSyntaxByArrayElementValue(getCompilerOptionsObjectLiteralSyntax(), "lib", options.lib![reason.index]); + message = Diagnostics.File_is_library_specified_here; + break; + } + const target = getNameOfScriptTarget(getEmitScriptTarget(options)); + configFileNode = target ? getOptionsSyntaxByValue(getCompilerOptionsObjectLiteralSyntax(), "target", target) : undefined; + message = Diagnostics.File_is_default_library_for_target_specified_here; + break; + default: + Debug.assertNever(reason); + } + return configFileNode && createDiagnosticForNodeInSourceFile( + options.configFile, + configFileNode, + message, + ); + } +} diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 49938f0fc8249..4a92c3187045f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -13,6 +13,7 @@ import { PackageJsonInfo, PackageJsonInfoCache, Pattern, + ProgramDiagnostics, SymlinkCache, ThisContainer, } from "./_namespaces/ts.js"; @@ -4892,6 +4893,7 @@ export interface Program extends ScriptReferenceHost { * @internal */ resolvedLibReferences: Map | undefined; + /** @internal */ getProgramDiagnosticsContainer: () => ProgramDiagnostics; /** @internal */ getCurrentPackagesMap(): Map | undefined; /** * Is the file emitted file diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 1ff3a2acada46..60db22498601f 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -138,6 +138,7 @@ import { FileExtensionInfo, fileExtensionIs, fileExtensionIsOneOf, + FileReference, FileWatcher, filter, find, @@ -408,6 +409,7 @@ import { lastOrUndefined, LateVisibilityPaintedStatement, length, + libMap, LiteralImportTypeNode, LiteralLikeElementAccessExpression, LiteralLikeNode, @@ -497,6 +499,7 @@ import { ResolutionMode, ResolvedModuleFull, ResolvedModuleWithFailedLookupLocations, + ResolvedProjectReference, ResolvedTypeReferenceDirective, ResolvedTypeReferenceDirectiveWithFailedLookupLocations, resolvePath, @@ -549,6 +552,7 @@ import { TextRange, TextSpan, ThisTypePredicate, + toFileNameLowerCase, Token, TokenFlags, tokenToString, @@ -3016,14 +3020,6 @@ export function forEachPropertyAssignment(objectLiteral: ObjectLiteralExpress }); } -/** @internal */ -export function getPropertyArrayElementValue(objectLiteral: ObjectLiteralExpression, propKey: string, elementValue: string): StringLiteral | undefined { - return forEachPropertyAssignment(objectLiteral, propKey, property => - isArrayLiteralExpression(property.initializer) ? - find(property.initializer.elements, (element): element is StringLiteral => isStringLiteral(element) && element.text === elementValue) : - undefined); -} - /** @internal */ export function getTsConfigObjectLiteralExpression(tsConfigSourceFile: TsConfigSourceFile | undefined): ObjectLiteralExpression | undefined { if (tsConfigSourceFile && tsConfigSourceFile.statements.length) { @@ -12113,3 +12109,91 @@ export function isNewScopeNode(node: Node): node is IntroducesNewScopeNode { || isJSDocSignature(node) || isMappedTypeNode(node); } + +/** @internal */ +export function getLibNameFromLibReference(libReference: FileReference): string { + return toFileNameLowerCase(libReference.fileName); +} + +/** @internal */ +export function getLibFileNameFromLibReference(libReference: FileReference): string | undefined { + const libName = getLibNameFromLibReference(libReference); + return libMap.get(libName); +} + +/** @internal */ +export function forEachResolvedProjectReference( + resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, + cb: (resolvedProjectReference: ResolvedProjectReference, parent: ResolvedProjectReference | undefined) => T | undefined, +): T | undefined { + return forEachProjectReference( + /*projectReferences*/ undefined, + resolvedProjectReferences, + (resolvedRef, parent) => resolvedRef && cb(resolvedRef, parent), + ); +} + +/** @internal */ +export function forEachProjectReference( + projectReferences: readonly ProjectReference[] | undefined, + resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, + cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, parent: ResolvedProjectReference | undefined, index: number) => T | undefined, + cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined, +): T | undefined { + let seenResolvedRefs: Set | undefined; + return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined); + + function worker( + projectReferences: readonly ProjectReference[] | undefined, + resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, + parent: ResolvedProjectReference | undefined, + ): T | undefined { + // Visit project references first + if (cbRef) { + const result = cbRef(projectReferences, parent); + if (result) return result; + } + let skipChildren: Set | undefined; + return forEach( + resolvedProjectReferences, + (resolvedRef, index) => { + if (resolvedRef && seenResolvedRefs?.has(resolvedRef.sourceFile.path)) { + (skipChildren ??= new Set()).add(resolvedRef); + // ignore recursives + return undefined; + } + const result = cbResolvedRef(resolvedRef, parent, index); + if (result || !resolvedRef) return result; + (seenResolvedRefs ||= new Set()).add(resolvedRef.sourceFile.path); + }, + ) || forEach( + resolvedProjectReferences, + resolvedRef => + resolvedRef && !skipChildren?.has(resolvedRef) ? + worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef) : + undefined, + ); + } +} + +/** @internal */ +export function getOptionsSyntaxByArrayElementValue(optionsObject: ObjectLiteralExpression | undefined, name: string, value: string): StringLiteral | undefined { + return optionsObject && getPropertyArrayElementValue(optionsObject, name, value); +} + +function getPropertyArrayElementValue(objectLiteral: ObjectLiteralExpression, propKey: string, elementValue: string): StringLiteral | undefined { + return forEachPropertyAssignment(objectLiteral, propKey, property => + isArrayLiteralExpression(property.initializer) ? + find(property.initializer.elements, (element): element is StringLiteral => isStringLiteral(element) && element.text === elementValue) : + undefined); +} + +/** @internal */ +export function getOptionsSyntaxByValue(optionsObject: ObjectLiteralExpression | undefined, name: string, value: string): StringLiteral | undefined { + return forEachOptionsSyntaxByName(optionsObject, name, property => isStringLiteral(property.initializer) && property.initializer.text === value ? property.initializer : undefined); +} + +/** @internal */ +export function forEachOptionsSyntaxByName(optionsObject: ObjectLiteralExpression | undefined, name: string, callback: (prop: PropertyAssignment) => T | undefined): T | undefined { + return forEachPropertyAssignment(optionsObject, name, callback); +}