diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 2dd3e465268..04af5c4d972 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -393,7 +393,7 @@ namespace ts { allDiagnostics?: Diagnostic[]; } - export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, + export function isProgramUptoDate(program: Program | undefined, rootFileNames: string[], newOptions: CompilerOptions, getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution): boolean { // If we haven't create a program yet, then it is not up-to-date if (!program) { @@ -406,14 +406,12 @@ namespace ts { } // If any file is not up-to-date, then the whole program is not up-to-date - for (const file of program.getSourceFiles()) { - if (!sourceFileUpToDate(program.getSourceFile(file.fileName))) { + if (program.getSourceFiles().some(sourceFileNotUptoDate)) { return false; - } } // If any of the missing file paths are now created - if (program.getMissingFilePaths().some(missingFilePath => fileExists(missingFilePath))) { + if (program.getMissingFilePaths().some(fileExists)) { return false; } @@ -431,15 +429,18 @@ namespace ts { return true; - function sourceFileUpToDate(sourceFile: SourceFile): boolean { - return sourceFile && - sourceFile.version === getSourceVersion(sourceFile.path) && - !hasInvalidatedResolution(sourceFile.path); + function sourceFileNotUptoDate(sourceFile: SourceFile): boolean { + return sourceFile.version !== getSourceVersion(sourceFile.path) || + hasInvalidatedResolution(sourceFile.path); } } + /** + * Determined if source file needs to be re-created even if its text hasnt changed + */ function shouldProgramCreateNewSourceFiles(program: Program, newOptions: CompilerOptions) { // If any of these options change, we cant reuse old source file even if version match + // The change in options like these could result in change in syntax tree change const oldOptions = program && program.getCompilerOptions(); return oldOptions && (oldOptions.target !== newOptions.target || @@ -478,19 +479,19 @@ namespace ts { ); } - export interface WildcardDirectoryWatchers { + export interface WildcardDirectoryWatcher { watcher: FileWatcher; flags: WatchDirectoryFlags; } /** - * Updates the existing wild card directory watcyhes with the new set of wild card directories from the config file after new program is created + * Updates the existing wild card directory watches with the new set of wild card directories from the config file after new program is created */ export function updateWatchingWildcardDirectories( - existingWatchedForWildcards: Map, + existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher, - closeDirectoryWatcher: (directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatchers, flagsChanged: boolean) => void + closeDirectoryWatcher: (directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flagsChanged: boolean) => void ) { mutateMap( existingWatchedForWildcards, @@ -506,7 +507,7 @@ namespace ts { } ); - function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatchers { + function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher { // Create new watch and recursive info return { watcher: watchDirectory(directory, flags), @@ -514,7 +515,7 @@ namespace ts { }; } - function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatchers, flags: WatchDirectoryFlags) { + function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags) { // Watcher needs to be updated if the recursive flags dont match if (wildcardDirectoryWatcher.flags === flags) { return; @@ -622,7 +623,7 @@ namespace ts { let redirectTargetsSet = createMap(); const filesByName = createMap(); - let missingFilePaths: Path[]; + let missingFilePaths: ReadonlyArray; // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; @@ -666,6 +667,8 @@ namespace ts { missingFilePaths = arrayFrom(filesByName.keys(), p => p).filter(p => !filesByName.get(p)); } + Debug.assert(!!missingFilePaths); + // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 0379289cc8e..dc2e61cd578 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -1,7 +1,9 @@ /// /// +/*@internal*/ namespace ts { + /** This is the cache of module/typedirectives resolution that can be retained across program */ export interface ResolutionCache { setModuleResolutionHost(host: ModuleResolutionHost): void; @@ -19,10 +21,15 @@ namespace ts { clear(): void; } - type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; - type ResolverWithGlobalCache = (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost) => ResolvedModuleWithFailedLookupLocations | undefined; + interface NameResolutionWithFailedLookupLocations { + readonly failedLookupLocations: string[]; + isInvalidated?: boolean; + } + + interface ResolverWithGlobalCache { + (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations | undefined; + } - /*@internal*/ export function resolveWithGlobalCache(primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string | undefined, projectName: string): ResolvedModuleWithFailedLookupLocations | undefined { if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) { // otherwise try to load typings from @types @@ -36,7 +43,11 @@ namespace ts { } } - /*@internal*/ + interface FailedLookupLocationsWatcher { + fileWatcher: FileWatcher; + refCount: number; + } + export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, @@ -51,7 +62,6 @@ namespace ts { const resolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); - type FailedLookupLocationsWatcher = { fileWatcher: FileWatcher; refCount: number }; const failedLookupLocationsWatches = createMap(); return { @@ -71,11 +81,10 @@ namespace ts { } function clear() { - failedLookupLocationsWatches.forEach((failedLookupLocationWatcher, failedLookupLocationPath: Path) => { + clearMap(failedLookupLocationsWatches, (failedLookupLocationPath, failedLookupLocationWatcher) => { log(`Watcher: FailedLookupLocations: Status: ForceClose: LocationPath: ${failedLookupLocationPath}, refCount: ${failedLookupLocationWatcher.refCount}`); failedLookupLocationWatcher.fileWatcher.close(); }); - failedLookupLocationsWatches.clear(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e4ea722df3d..9fe93f55cb3 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2411,7 +2411,7 @@ namespace ts { * program source file but could not be located. */ /* @internal */ - getMissingFilePaths(): Path[]; + getMissingFilePaths(): ReadonlyArray; /** * Emits the JavaScript and declaration files. If targetSourceFile is not specified, then @@ -3561,6 +3561,7 @@ namespace ts { charset?: string; checkJs?: boolean; /* @internal */ configFilePath?: string; + /** configFile is set as non enumerable property so as to avoid checking of json source files */ /* @internal */ readonly configFile?: JsonSourceFile; declaration?: boolean; declarationDir?: string; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 6e26a287b83..ebb6c785588 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -403,7 +403,10 @@ namespace ts { ((node).name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(node)); } - /* @internal */ + export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration { + return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral; + } + export function isNonGlobalAmbientModule(node: Node): node is ModuleDeclaration & { name: StringLiteral } { return isModuleDeclaration(node) && isStringLiteral(node.name); } @@ -1403,10 +1406,6 @@ namespace ts { return SpecialPropertyAssignmentKind.None; } - export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration { - return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral; - } - export function getExternalModuleName(node: Node): Expression { if (node.kind === SyntaxKind.ImportDeclaration) { return (node).moduleSpecifier; diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index e79049b983c..6423b981896 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -241,12 +241,13 @@ namespace ts { let needsReload: boolean; // true if the config file changed and needs to reload it from the disk let missingFilesMap: Map; // Map of file watchers for the missing files let configFileWatcher: FileWatcher; // watcher for the config file - let watchedWildcardDirectories: Map; // map of watchers for the wild card directories in the config file + let watchedWildcardDirectories: Map; // map of watchers for the wild card directories in the config file let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // Cache that stores the source file and version info let missingFilePathsRequestedForRelease: Path[]; // These paths are held temparirly so that we can remove the entry from source file cache if the file is not tracked by missing files let hasInvalidatedResolution: HasInvalidatedResolution; // Passed along to see if source file has invalidated resolutions + let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -291,9 +292,10 @@ namespace ts { // Create the compiler host const compilerHost = createWatchedCompilerHost(compilerOptions); resolutionCache.setModuleResolutionHost(compilerHost); - if (changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { + if (hasChangedCompilerOptions && changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { resolutionCache.clear(); } + hasChangedCompilerOptions = false; beforeCompile(compilerOptions); // Compile the program @@ -504,6 +506,7 @@ namespace ts { const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); rootFileNames = configParseResult.fileNames; compilerOptions = configParseResult.options; + hasChangedCompilerOptions = true; configFileSpecs = configParseResult.configFileSpecs; configFileWildCardDirectories = configParseResult.wildcardDirectories; @@ -603,7 +606,7 @@ namespace ts { (flags & WatchDirectoryFlags.Recursive) !== 0); } - function stopWatchingWildCardDirectory(_directory: string, { watcher }: WildcardDirectoryWatchers, _recursiveChanged: boolean) { + function stopWatchingWildCardDirectory(_directory: string, { watcher }: WildcardDirectoryWatcher, _recursiveChanged: boolean) { watcher.close(); } diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts index 668b684a250..20323b37380 100644 --- a/src/harness/unittests/programMissingFiles.ts +++ b/src/harness/unittests/programMissingFiles.ts @@ -1,6 +1,18 @@ /// namespace ts { + function verifyMissingFilePaths(missingPaths: ReadonlyArray, expected: ReadonlyArray) { + assert.isDefined(missingPaths); + const map = arrayToMap(expected, k => k, _v => true); + for (const missing of missingPaths) { + const value = map.get(missing); + assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`); + map.set(missing, false); + } + const notFound = mapDefinedIter(map.keys(), k => map.get(k) === true ? k : undefined); + assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); + } + describe("Program.getMissingFilePaths", () => { const options: CompilerOptions = { @@ -40,34 +52,31 @@ namespace ts { it("handles no missing root files", () => { const program = createProgram([emptyFileRelativePath], options, testCompilerHost); const missing = program.getMissingFilePaths(); - assert.isDefined(missing); - assert.deepEqual(missing, []); + verifyMissingFilePaths(missing, []); }); it("handles missing root file", () => { const program = createProgram(["./nonexistent.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths(); - assert.isDefined(missing); - assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path }); it("handles multiple missing root files", () => { const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths().sort(); - assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); }); it("handles a mix of present and missing root files", () => { const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths().sort(); - assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); }); it("handles repeatedly specified root files", () => { const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths(); - assert.isDefined(missing); - assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); }); it("normalizes file paths", () => { @@ -81,9 +90,8 @@ namespace ts { it("handles missing triple slash references", () => { const program = createProgram([referenceFileRelativePath], options, testCompilerHost); - const missing = program.getMissingFilePaths().sort(); - assert.isDefined(missing); - assert.deepEqual(missing, [ + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, [ // From absolute reference "d:/imaginary/nonexistent1.ts", @@ -100,4 +108,4 @@ namespace ts { ]); }); }); -} \ No newline at end of file +} diff --git a/src/server/project.ts b/src/server/project.ts index 552778e86c0..d50621c50bb 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1020,11 +1020,6 @@ namespace ts.server { } } - interface WildcardDirectoryWatcher { - watcher: FileWatcher; - flags: WatchDirectoryFlags; - } - /** * If a file is opened, the server will look for a tsconfig (or jsconfig) * and if successfull create a ConfiguredProject for it.