From d5f246fd9919072338e0d31331e1fd7276eebd7c Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 18 Apr 2017 15:48:01 -0700 Subject: [PATCH] Reuse module resolutions in unmodified files --- src/compiler/program.ts | 180 +++++++++++++++++++++++++--------------- 1 file changed, 114 insertions(+), 66 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index f2d44adb1f1..8081775b079 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -6,6 +6,23 @@ namespace ts { const emptyArray: any[] = []; const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/; + + const enum StructuralChangesFromOldProgram { + None = 0, + NoOldProgram = 1, + ModuleResolutionOptions = 2, + RootNames = 3, + TypesOptions = 4, + SourceFileRemoved = 5, + HasNoDefaultLib = 6, + TripleSlashReferences = 7, + Imports = 8, + ModuleAugmentations = 9, + TripleSlashTypesReferences = 10, + + CannotReuseResolution = NoOldProgram | ModuleResolutionOptions | RootNames | TypesOptions | SourceFileRemoved + } + export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string { while (true) { const fileName = combinePaths(searchPath, configName); @@ -298,6 +315,7 @@ namespace ts { let diagnosticsProducingTypeChecker: TypeChecker; let noDiagnosticsTypeChecker: TypeChecker; let classifiableNames: Map; + let modifiedFilePaths: Path[] | undefined; const cachedSemanticDiagnosticsForFile: DiagnosticCache = {}; const cachedDeclarationDiagnosticsForFile: DiagnosticCache = {}; @@ -367,7 +385,11 @@ namespace ts { // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap(fileName => fileName.toLowerCase()) : undefined; - if (!tryReuseStructureFromOldProgram()) { + const structuralChanges = tryReuseStructureFromOldProgram(); + if (structuralChanges === StructuralChangesFromOldProgram.None) { + Debug.assert(oldProgram.structureIsReused === true); + } + else { forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false)); // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders @@ -476,18 +498,40 @@ namespace ts { } interface OldProgramState { - program: Program; + program: Program | undefined; file: SourceFile; modifiedFilePaths: Path[]; } - function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile, oldProgramState?: OldProgramState) { - if (!oldProgramState && !file.ambientModuleNames.length) { - // if old program state is not supplied and file does not contain locally defined ambient modules - // then the best we can do is fallback to the default logic + function createOldProgramState( + program: Program | undefined, + file: SourceFile, + modifiedFilePaths: Path[]): OldProgramState { + return { program, file, modifiedFilePaths }; + } + + function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile, oldProgramState: OldProgramState) { + if (structuralChanges & StructuralChangesFromOldProgram.CannotReuseResolution && !file.ambientModuleNames.length) { + // If the old program state does not permit reusing resolutions and `file` does not contain locally defined ambient modules, + // the best we can do is fallback to the default logic. return resolveModuleNamesWorker(moduleNames, containingFile); } + + const oldSourceFile = oldProgramState.program && oldProgramState.program.getSourceFile(containingFile); + if (oldSourceFile === file) { + const oldSourceFileResult: ResolvedModuleFull[] = []; + for (const moduleName of moduleNames) { + const resolvedModule = oldSourceFile.resolvedModules.get(moduleName); + oldSourceFileResult.push(resolvedModule); + } + return oldSourceFileResult; + } + + // if options didn't change but there were changes in module names/triple slash/etc., + // rty to get file with smae name form old program, if equal, then we can just copy resolutions verbatim. + // else the file was edited. Then we just redo the reoslutions for the whole file (could go further, but kiss for now). + // at this point we know that either // - file has local declarations for ambient modules // OR @@ -535,7 +579,7 @@ namespace ts { // since they are known to be unknown unknownModuleNames = moduleNames.slice(0, i); } - // mark prediced resolution in the result list + // Mark predicted resolution in the result list. result[i] = predictedToResolveToAmbientModuleMarker; } else if (unknownModuleNames) { @@ -568,16 +612,13 @@ namespace ts { Debug.assert(j === resolutions.length); return result; - function checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState?: OldProgramState): boolean { - if (!oldProgramState) { - return false; - } + function checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState: OldProgramState): boolean { const resolutionToFile = getResolvedModule(oldProgramState.file, moduleName); if (resolutionToFile) { // module used to be resolved to file - ignore it return false; } - const ambientModule = oldProgram.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(moduleName); + const ambientModule = oldProgramState.program && oldProgramState.program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(moduleName); if (!(ambientModule && ambientModule.declarations)) { return false; } @@ -599,16 +640,16 @@ namespace ts { } } - function tryReuseStructureFromOldProgram(): boolean { + function tryReuseStructureFromOldProgram(): StructuralChangesFromOldProgram { if (!oldProgram) { - return false; + return StructuralChangesFromOldProgram.NoOldProgram; } // check properties that can affect structure of the program or module resolution strategy // if any of these properties has changed - structure cannot be reused const oldOptions = oldProgram.getCompilerOptions(); if (changesAffectModuleResolution(oldOptions, options)) { - return false; + return StructuralChangesFromOldProgram.ModuleResolutionOptions; } Debug.assert(!oldProgram.structureIsReused); @@ -616,17 +657,18 @@ namespace ts { // there is an old program, check if we can reuse its structure const oldRootNames = oldProgram.getRootFileNames(); if (!arrayIsEqualTo(oldRootNames, rootNames)) { - return false; + return StructuralChangesFromOldProgram.RootNames; } if (!arrayIsEqualTo(options.types, oldOptions.types)) { - return false; + return StructuralChangesFromOldProgram.TypesOptions; } // check if program source files has changed in the way that can affect structure of the program const newSourceFiles: SourceFile[] = []; const filePaths: Path[] = []; const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = []; + let structuralChanges = StructuralChangesFromOldProgram.None; for (const oldSourceFile of oldProgram.getSourceFiles()) { let newSourceFile = host.getSourceFileByPath @@ -634,64 +676,70 @@ namespace ts { : host.getSourceFile(oldSourceFile.fileName, options.target); if (!newSourceFile) { - return false; - } - - newSourceFile.path = oldSourceFile.path; - filePaths.push(newSourceFile.path); - - if (oldSourceFile !== newSourceFile) { - if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { - // value of no-default-lib has changed - // this will affect if default library is injected into the list of files - return false; - } - - // check tripleslash references - if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { - // tripleslash references has changed - return false; - } - - // check imports and module augmentations - collectExternalModuleReferences(newSourceFile); - if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { - // imports has changed - return false; - } - if (!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { - // moduleAugmentations has changed - return false; - } - - if (!arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { - // 'types' references has changed - return false; - } - - // tentatively approve the file - modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + structuralChanges |= StructuralChangesFromOldProgram.SourceFileRemoved; } else { - // file has no changes - use it as is - newSourceFile = oldSourceFile; - } + newSourceFile.path = oldSourceFile.path; + filePaths.push(newSourceFile.path); - // if file has passed all checks it should be safe to reuse it - newSourceFiles.push(newSourceFile); + if (oldSourceFile !== newSourceFile) { + if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { + // value of no-default-lib has changed + // this will affect if default library is injected into the list of files + structuralChanges |= StructuralChangesFromOldProgram.HasNoDefaultLib; + } + + // check tripleslash references + if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { + // tripleslash references has changed + structuralChanges |= StructuralChangesFromOldProgram.TripleSlashReferences; + } + + // check imports and module augmentations + collectExternalModuleReferences(newSourceFile); + if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { + // imports has changed + structuralChanges |= StructuralChangesFromOldProgram.Imports; + } + if (!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { + // moduleAugmentations has changed + structuralChanges |= StructuralChangesFromOldProgram.ModuleAugmentations; + } + + if (!arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { + // 'types' references has changed + structuralChanges |= StructuralChangesFromOldProgram.TripleSlashTypesReferences; + } + + // tentatively approve the file + modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + } + else { + // file has no changes - use it as is + newSourceFile = oldSourceFile; + } + + // if file has passed all checks it should be safe to reuse it + newSourceFiles.push(newSourceFile); + } } - const modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path); + if (structuralChanges !== StructuralChangesFromOldProgram.None) { + return structuralChanges; + } + + modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path); // try to verify results of module resolution for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory); if (resolveModuleNamesWorker) { const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral); - const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile, { file: oldSourceFile, program: oldProgram, modifiedFilePaths }); + const oldProgramState = createOldProgramState(oldProgram, oldSourceFile, modifiedFilePaths); + const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile, oldProgramState); // ensure that module resolution results are still correct const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo); if (resolutionsChanged) { - return false; + return StructuralChangesFromOldProgram.Imports | StructuralChangesFromOldProgram.ModuleAugmentations; } } if (resolveTypeReferenceDirectiveNamesWorker) { @@ -700,7 +748,7 @@ namespace ts { // ensure that types resolutions are still correct const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo); if (resolutionsChanged) { - return false; + return StructuralChangesFromOldProgram.TripleSlashTypesReferences; } } // pass the cache of module/types resolutions from the old source file @@ -722,7 +770,7 @@ namespace ts { resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives(); oldProgram.structureIsReused = true; - return true; + return StructuralChangesFromOldProgram.None; } function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { @@ -1519,11 +1567,11 @@ namespace ts { function processImportedModules(file: SourceFile) { collectExternalModuleReferences(file); if (file.imports.length || file.moduleAugmentations.length) { - file.resolvedModules = createMap(); // Because global augmentation doesn't have string literal name, we can check for global augmentation as such. const nonGlobalAugmentation = filter(file.moduleAugmentations, (moduleAugmentation) => moduleAugmentation.kind === SyntaxKind.StringLiteral); const moduleNames = map(concatenate(file.imports, nonGlobalAugmentation), getTextOfLiteral); - const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory), file); + const oldProgramState = createOldProgramState(oldProgram, file, modifiedFilePaths); + const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory), file, oldProgramState); Debug.assert(resolutions.length === moduleNames.length); for (let i = 0; i < moduleNames.length; i++) { const resolution = resolutions[i];