Skip to content

Commit 61fb222

Browse files
authored
Use symlinks when looking for module names for declaration emit (#24874)
* fix symlink tag, support arbitrary (ie, directory) links via @link Introduce indirect symlink lookup to specifier deriver Use fileset, move exec vfs path resolution :shakes fist: Apply files symlink relative to dirname Use directory function * Accept really bad baseline updates
1 parent bcd6919 commit 61fb222

22 files changed

+1504
-1171
lines changed

src/compiler/checker.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4091,7 +4091,14 @@ namespace ts {
40914091
}
40924092
else {
40934093
const contextFile = getSourceFileOfNode(getOriginalNode(context!.enclosingDeclaration))!;
4094-
return `"${file.moduleName || moduleSpecifiers.getModuleSpecifier(compilerOptions, contextFile, contextFile.path, file.path, context!.tracker.moduleResolverHost!)}"`;
4094+
return `"${file.moduleName || moduleSpecifiers.getModuleSpecifiers(
4095+
symbol,
4096+
compilerOptions,
4097+
contextFile,
4098+
context!.tracker.moduleResolverHost!,
4099+
context!.tracker.moduleResolverHost!.getSourceFiles!(),
4100+
{ importModuleSpecifierPreference: "non-relative" }
4101+
)[0]}"`;
40954102
}
40964103
}
40974104
const declaration = symbol.declarations[0];

src/compiler/moduleSpecifiers.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@ namespace ts.moduleSpecifiers {
1515
// For each symlink/original for a module, returns a list of ways to import that file.
1616
export function getModuleSpecifiers(
1717
moduleSymbol: Symbol,
18-
program: Program,
18+
compilerOptions: CompilerOptions,
1919
importingSourceFile: SourceFile,
2020
host: ModuleSpecifierResolutionHost,
21+
files: ReadonlyArray<SourceFile>,
2122
preferences: ModuleSpecifierPreferences,
2223
): ReadonlyArray<ReadonlyArray<string>> {
2324
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol);
2425
if (ambient) return [[ambient]];
2526

26-
const compilerOptions = program.getCompilerOptions();
27-
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.fileName, host);
28-
const modulePaths = getAllModulePaths(program, getSourceFileOfNode(moduleSymbol.valueDeclaration));
27+
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.path, host);
28+
if (!files) {
29+
return Debug.fail("Files list must be present to resolve symlinks in specifier resolution");
30+
}
31+
const modulePaths = getAllModulePaths(files, getSourceFileOfNode(moduleSymbol.valueDeclaration), info.getCanonicalFileName, host);
2932

3033
const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions));
3134
return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName =>
@@ -130,15 +133,57 @@ namespace ts.moduleSpecifiers {
130133
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? fileExtensionIs(text, Extension.Js) : undefined) || false;
131134
}
132135

136+
function discoverProbableSymlinks(files: ReadonlyArray<SourceFile>) {
137+
const symlinks = mapDefined(files, sf =>
138+
sf.resolvedModules && firstDefinedIterator(sf.resolvedModules.values(), res =>
139+
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] : undefined));
140+
const result = createMap<string>();
141+
if (symlinks) {
142+
for (const [resolvedPath, originalPath] of symlinks) {
143+
const resolvedParts = getPathComponents(resolvedPath);
144+
const originalParts = getPathComponents(originalPath);
145+
while (resolvedParts[resolvedParts.length - 1] === originalParts[originalParts.length - 1]) {
146+
resolvedParts.pop();
147+
originalParts.pop();
148+
}
149+
result.set(getPathFromPathComponents(originalParts), getPathFromPathComponents(resolvedParts));
150+
}
151+
}
152+
return result;
153+
}
154+
155+
function getAllModulePathsUsingIndirectSymlinks(files: ReadonlyArray<SourceFile>, target: string, getCanonicalFileName: (file: string) => string, host: ModuleSpecifierResolutionHost) {
156+
const links = discoverProbableSymlinks(files);
157+
const paths = arrayFrom(links.keys());
158+
let options: string[] | undefined;
159+
for (const path of paths) {
160+
const resolved = links.get(path)!;
161+
if (startsWith(target, resolved + "/")) {
162+
const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName);
163+
const option = resolvePath(path, relative);
164+
if (!host.fileExists || host.fileExists(option)) {
165+
if (!options) options = [];
166+
options.push(option);
167+
}
168+
}
169+
}
170+
const resolvedtarget = host.getCurrentDirectory ? resolvePath(host.getCurrentDirectory(), target) : target;
171+
if (options) {
172+
options.push(resolvedtarget); // Since these are speculative, we also include the original resolved name as a possibility
173+
return options;
174+
}
175+
return [resolvedtarget];
176+
}
177+
133178
/**
134179
* Looks for a existing imports that use symlinks to this module.
135180
* Only if no symlink is available, the real path will be used.
136181
*/
137-
function getAllModulePaths(program: Program, { fileName }: SourceFile): ReadonlyArray<string> {
138-
const symlinks = mapDefined(program.getSourceFiles(), sf =>
182+
function getAllModulePaths(files: ReadonlyArray<SourceFile>, { fileName }: SourceFile, getCanonicalFileName: (file: string) => string, host: ModuleSpecifierResolutionHost): ReadonlyArray<string> {
183+
const symlinks = mapDefined(files, sf =>
139184
sf.resolvedModules && firstDefinedIterator(sf.resolvedModules.values(), res =>
140185
res && res.resolvedFileName === fileName ? res.originalPath : undefined));
141-
return symlinks.length === 0 ? [fileName] : symlinks;
186+
return symlinks.length === 0 ? getAllModulePathsUsingIndirectSymlinks(files, fileName, getCanonicalFileName, host) : symlinks;
142187
}
143188

144189
function getRelativePathNParents(relativePath: string): number {

src/harness/harness.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,7 @@ namespace Harness {
11081108
{ name: "noImplicitReferences", type: "boolean" },
11091109
{ name: "currentDirectory", type: "string" },
11101110
{ name: "symlink", type: "string" },
1111+
{ name: "link", type: "string" },
11111112
// Emitted js baseline will print full paths for every output file
11121113
{ name: "fullEmitPaths", type: "boolean" }
11131114
];
@@ -1179,7 +1180,9 @@ namespace Harness {
11791180
harnessSettings: TestCaseParser.CompilerSettings | undefined,
11801181
compilerOptions: ts.CompilerOptions | undefined,
11811182
// Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file
1182-
currentDirectory: string | undefined): compiler.CompilationResult {
1183+
currentDirectory: string | undefined,
1184+
symlinks?: vfs.FileSet
1185+
): compiler.CompilationResult {
11831186
const options: ts.CompilerOptions & HarnessOptions = compilerOptions ? ts.cloneCompilerOptions(compilerOptions) : { noResolve: false };
11841187
options.target = options.target || ts.ScriptTarget.ES3;
11851188
options.newLine = options.newLine || ts.NewLineKind.CarriageReturnLineFeed;
@@ -1216,6 +1219,9 @@ namespace Harness {
12161219

12171220
const docs = inputFiles.concat(otherFiles).map(documents.TextDocument.fromTestFile);
12181221
const fs = vfs.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory });
1222+
if (symlinks) {
1223+
fs.apply(symlinks);
1224+
}
12191225
const host = new fakes.CompilerHost(fs, options);
12201226
return compiler.compileFiles(host, programFileNames, options);
12211227
}
@@ -1836,6 +1842,7 @@ namespace Harness {
18361842

18371843
// Regex for parsing options in the format "@Alpha: Value of any sort"
18381844
const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines
1845+
const linkRegex = /^[\/]{2}\s*@link\s*:\s*([^\r\n]*)\s*->\s*([^\r\n]*)/gm; // multiple matches on multiple lines
18391846

18401847
export function extractCompilerSettings(content: string): CompilerSettings {
18411848
const opts: CompilerSettings = {};
@@ -1855,6 +1862,7 @@ namespace Harness {
18551862
testUnitData: TestUnitData[];
18561863
tsConfig: ts.ParsedCommandLine | undefined;
18571864
tsConfigFileUnitData: TestUnitData | undefined;
1865+
symlinks?: vfs.FileSet;
18581866
}
18591867

18601868
/** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */
@@ -1869,10 +1877,16 @@ namespace Harness {
18691877
let currentFileOptions: any = {};
18701878
let currentFileName: any;
18711879
let refs: string[] = [];
1880+
let symlinks: vfs.FileSet | undefined;
18721881

18731882
for (const line of lines) {
1874-
const testMetaData = optionRegex.exec(line);
1875-
if (testMetaData) {
1883+
let testMetaData: RegExpExecArray | null;
1884+
const linkMetaData = linkRegex.exec(line);
1885+
if (linkMetaData) {
1886+
if (!symlinks) symlinks = {};
1887+
symlinks[linkMetaData[2].trim()] = new vfs.Symlink(linkMetaData[1].trim());
1888+
}
1889+
else if (testMetaData = optionRegex.exec(line)) {
18761890
// Comment line, check for global/file @options and record them
18771891
optionRegex.lastIndex = 0;
18781892
const metaDataName = testMetaData[1].toLowerCase();
@@ -1961,7 +1975,7 @@ namespace Harness {
19611975
break;
19621976
}
19631977
}
1964-
return { settings, testUnitData, tsConfig, tsConfigFileUnitData };
1978+
return { settings, testUnitData, tsConfig, tsConfigFileUnitData, symlinks };
19651979
}
19661980
}
19671981

src/harness/vfs.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ namespace vfs {
911911
if (this.stringComparer(vpath.dirname(path), path) === 0) {
912912
throw new TypeError("Roots cannot be symbolic links.");
913913
}
914-
this.symlinkSync(entry.symlink, path);
914+
this.symlinkSync(vpath.resolve(dirname, entry.symlink), path);
915915
this._applyFileExtendedOptions(path, entry);
916916
}
917917
else if (entry instanceof Link) {
@@ -1078,8 +1078,7 @@ namespace vfs {
10781078
if (symlink) {
10791079
for (const link of symlink.split(",").map(link => link.trim())) {
10801080
fs.mkdirpSync(vpath.dirname(link));
1081-
fs.symlinkSync(document.file, link);
1082-
fs.filemeta(link).set("document", document);
1081+
fs.symlinkSync(vpath.resolve(fs.cwd(), document.file), link);
10831082
}
10841083
}
10851084
}

src/parser/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5255,6 +5255,7 @@ namespace ts {
52555255
useCaseSensitiveFileNames?(): boolean;
52565256
fileExists?(path: string): boolean;
52575257
readFile?(path: string): string | undefined;
5258+
getSourceFiles?(): ReadonlyArray<SourceFile>; // Used for cached resolutions to find symlinks without traversing the fs (again)
52585259
}
52595260

52605261
/** @deprecated See comment on SymbolWriter */

src/services/codefixes/importFixes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ namespace ts.codefix {
247247
preferences: UserPreferences,
248248
): ReadonlyArray<NewImportInfo> {
249249
const choicesForEachExportingModule = flatMap<SymbolExportInfo, NewImportInfo[]>(moduleSymbols, ({ moduleSymbol, importKind }) => {
250-
const modulePathsGroups = moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program, sourceFile, host, preferences);
250+
const modulePathsGroups = moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getCompilerOptions(), sourceFile, host, program.getSourceFiles(), preferences);
251251
return modulePathsGroups.map(group => group.map(moduleSpecifier => ({ moduleSpecifier, importKind })));
252252
});
253253
// Sort to keep the shortest paths first, but keep [relativePath, importRelativeToBaseUrl] groups together

src/testRunner/compilerRunner.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,9 @@ class CompilerTest {
179179
this.otherFiles,
180180
this.harnessSettings,
181181
/*options*/ tsConfigOptions,
182-
/*currentDirectory*/ this.harnessSettings.currentDirectory);
182+
/*currentDirectory*/ this.harnessSettings.currentDirectory,
183+
testCaseContent.symlinks
184+
);
183185

184186
this.options = this.result.options;
185187
}

0 commit comments

Comments
 (0)