From b6d520a7a52f4edbf8bf46e098ae81b25f6c558d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 13 Mar 2019 12:23:18 -0700 Subject: [PATCH] Handle recursive symlinks when matching file names Fixes #28842 --- src/compiler/sys.ts | 2 +- src/compiler/utilities.ts | 8 ++++-- src/compiler/watchUtilities.ts | 10 ++++++-- src/harness/fakes.ts | 2 +- src/harness/virtualFileSystemWithWatch.ts | 2 +- src/testRunner/unittests/config/matchFiles.ts | 25 +++++++++++++++++++ 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c648f7972bcc7..b1923a73a27e1 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1148,7 +1148,7 @@ namespace ts { } function readDirectory(path: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, includes?: ReadonlyArray, depth?: number): string[] { - return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries); + return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries, realpath); } function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index dd04f5f4c2400..1c239880dc369 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8101,7 +8101,7 @@ namespace ts { } /** @param path directory of the tsconfig.json */ - export function matchFiles(path: string, extensions: ReadonlyArray | undefined, excludes: ReadonlyArray | undefined, includes: ReadonlyArray | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined, getFileSystemEntries: (path: string) => FileSystemEntries): string[] { + export function matchFiles(path: string, extensions: ReadonlyArray | undefined, excludes: ReadonlyArray | undefined, includes: ReadonlyArray | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined, getFileSystemEntries: (path: string) => FileSystemEntries, realpath: (path: string) => string): string[] { path = normalizePath(path); currentDirectory = normalizePath(currentDirectory); @@ -8114,7 +8114,8 @@ namespace ts { // Associate an array of results with each include regex. This keeps results in order of the "include" order. // If there are no "includes", then just put everything in results[0]. const results: string[][] = includeFileRegexes ? includeFileRegexes.map(() => []) : [[]]; - + const visited = createMap(); + const toCanonical = createGetCanonicalFileName(useCaseSensitiveFileNames); for (const basePath of patterns.basePaths) { visitDirectory(basePath, combinePaths(currentDirectory, basePath), depth); } @@ -8122,6 +8123,9 @@ namespace ts { return flatten(results); function visitDirectory(path: string, absolutePath: string, depth: number | undefined) { + const canonicalPath = toCanonical(realpath(absolutePath)); + if (visited.has(canonicalPath)) return; + visited.set(canonicalPath, true); const { files, directories } = getFileSystemEntries(path); for (const current of sort(files, compareStringsCaseSensitive)) { diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 565af054b8feb..7c11dcf2856ff 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -11,6 +11,7 @@ namespace ts { directoryExists?(path: string): boolean; getDirectories?(path: string): string[]; readDirectory?(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + realpath?(path: string): string; createDirectory?(path: string): void; writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; @@ -56,7 +57,8 @@ namespace ts { writeFile: host.writeFile && writeFile, addOrDeleteFileOrDirectory, addOrDeleteFile, - clearCache + clearCache, + realpath: host.realpath && realpath }; function toPath(fileName: string) { @@ -170,7 +172,7 @@ namespace ts { const rootDirPath = toPath(rootDir); const result = tryReadDirectory(rootDir, rootDirPath); if (result) { - return matchFiles(rootDir, extensions, excludes, includes, useCaseSensitiveFileNames, currentDirectory, depth, getFileSystemEntries); + return matchFiles(rootDir, extensions, excludes, includes, useCaseSensitiveFileNames, currentDirectory, depth, getFileSystemEntries, realpath); } return host.readDirectory!(rootDir, extensions, excludes, includes, depth); @@ -183,6 +185,10 @@ namespace ts { } } + function realpath(s: string) { + return host.realpath ? host.realpath(s) : s; + } + function addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path) { const existingResult = getCachedFileSystemEntries(fileOrDirectoryPath); if (existingResult) { diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 31c96ac6e73c7..57e3722f64dc5 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -87,7 +87,7 @@ namespace fakes { } public readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { - return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, path => this.getAccessibleFileSystemEntries(path)); + return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, path => this.getAccessibleFileSystemEntries(path), path => this.realpath(path)); } public getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index e82ee36f8fec6..f49f15cadc2ee 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -826,7 +826,7 @@ interface Array {}` }); } return { directories, files }; - }); + }, path => this.realpath(path)); } watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { diff --git a/src/testRunner/unittests/config/matchFiles.ts b/src/testRunner/unittests/config/matchFiles.ts index a30fa468443aa..ad3c4f7b12378 100644 --- a/src/testRunner/unittests/config/matchFiles.ts +++ b/src/testRunner/unittests/config/matchFiles.ts @@ -1512,5 +1512,30 @@ namespace ts { validateMatches(getExpected(caseSensitiveBasePath), json, caseSensitiveOrderingDiffersWithCaseHost, caseSensitiveBasePath); validateMatches(getExpected(caseInsensitiveBasePath), json, caseInsensitiveOrderingDiffersWithCaseHost, caseInsensitiveBasePath); }); + + it("when recursive symlinked directories are present", () => { + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { + cwd: caseInsensitiveBasePath, files: { + "c:/dev/index.ts": "" + } + }); + fs.mkdirpSync("c:/dev/a/b/c"); + fs.symlinkSync("c:/dev/A", "c:/dev/a/self"); + fs.symlinkSync("c:/dev/a", "c:/dev/a/b/parent"); + fs.symlinkSync("c:/dev/a", "c:/dev/a/b/c/grandparent"); + const host = new fakes.ParseConfigHost(fs); + const json = {}; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/index.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, host, caseInsensitiveBasePath); + }); }); }