For duplicate source files of the same package, make one redirect to the other (#16274)

* For duplicate source files of the same package, make one redirect to the other

* Add reuseProgramStructure tests

* Copy `sourceFileToPackageId` and `isSourceFileTargetOfRedirect` only if we completely reuse old structure

* Use fallthrough instead of early exit from loop

* Use a set to efficiently detect duplicate package names

* Move map setting outside of createRedirectSourceFile

* Correctly handle seenPackageNames set

* sourceFileToPackageId -> sourceFileToPackageName

* Renames

* Respond to PR comments

* Fix bug where `oldSourceFile !== newSourceFile` because oldSourceFile was a redirect

* Clean up redirectInfo

* Respond to PR comments
This commit is contained in:
Andy
2017-08-09 14:39:06 -07:00
committed by GitHub
parent 17a6f7b56a
commit 37b20ee670
16 changed files with 696 additions and 106 deletions

View File

@@ -472,6 +472,15 @@ namespace ts {
resolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile) => loadWithLocalCache(checkAllDefined(typeReferenceDirectiveNames), containingFile, loader);
}
// Map from a stringified PackageId to the source file with that id.
// Only one source file may have a given packageId. Others become redirects (see createRedirectSourceFile).
// `packageIdToSourceFile` is only used while building the program, while `sourceFileToPackageName` and `isSourceFileTargetOfRedirect` are kept around.
const packageIdToSourceFile = createMap<SourceFile>();
// Maps from a SourceFile's `.path` to the name of the package it was imported with.
let sourceFileToPackageName = createMap<string>();
// See `sourceFileIsRedirectedTo`.
let redirectTargetsSet = createMap<true>();
const filesByName = createMap<SourceFile | undefined>();
// stores 'filename -> file association' ignoring case
// used to track cases when two file names differ only in casing
@@ -548,6 +557,8 @@ namespace ts {
isSourceFileFromExternalLibrary,
dropDiagnosticsProducingTypeChecker,
getSourceFileFromReference,
sourceFileToPackageName,
redirectTargetsSet,
};
verifyCompilerOptions();
@@ -773,8 +784,12 @@ namespace ts {
const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = [];
oldProgram.structureIsReused = StructureIsReused.Completely;
for (const oldSourceFile of oldProgram.getSourceFiles()) {
const newSourceFile = host.getSourceFileByPath
const oldSourceFiles = oldProgram.getSourceFiles();
const enum SeenPackageName { Exists, Modified }
const seenPackageNames = createMap<SeenPackageName>();
for (const oldSourceFile of oldSourceFiles) {
let newSourceFile = host.getSourceFileByPath
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target)
: host.getSourceFile(oldSourceFile.fileName, options.target);
@@ -782,10 +797,46 @@ namespace ts {
return oldProgram.structureIsReused = StructureIsReused.Not;
}
Debug.assert(!newSourceFile.redirectInfo, "Host should not return a redirect source file from `getSourceFile`");
let fileChanged: boolean;
if (oldSourceFile.redirectInfo) {
// We got `newSourceFile` by path, so it is actually for the unredirected file.
// This lets us know if the unredirected file has changed. If it has we should break the redirect.
if (newSourceFile !== oldSourceFile.redirectInfo.unredirected) {
// Underlying file has changed. Might not redirect anymore. Must rebuild program.
return oldProgram.structureIsReused = StructureIsReused.Not;
}
fileChanged = false;
newSourceFile = oldSourceFile; // Use the redirect.
}
else if (oldProgram.redirectTargetsSet.has(oldSourceFile.path)) {
// If a redirected-to source file changes, the redirect may be broken.
if (newSourceFile !== oldSourceFile) {
return oldProgram.structureIsReused = StructureIsReused.Not;
}
fileChanged = false;
}
else {
fileChanged = newSourceFile !== oldSourceFile;
}
newSourceFile.path = oldSourceFile.path;
filePaths.push(newSourceFile.path);
if (oldSourceFile !== newSourceFile) {
const packageName = oldProgram.sourceFileToPackageName.get(oldSourceFile.path);
if (packageName !== undefined) {
// If there are 2 different source files for the same package name and at least one of them changes,
// they might become redirects. So we must rebuild the program.
const prevKind = seenPackageNames.get(packageName);
const newKind = fileChanged ? SeenPackageName.Modified : SeenPackageName.Exists;
if ((prevKind !== undefined && newKind === SeenPackageName.Modified) || prevKind === SeenPackageName.Modified) {
return oldProgram.structureIsReused = StructureIsReused.Not;
}
seenPackageNames.set(packageName, newKind);
}
if (fileChanged) {
// The `newSourceFile` object was created for the new program.
if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) {
@@ -897,6 +948,9 @@ namespace ts {
}
resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives();
sourceFileToPackageName = oldProgram.sourceFileToPackageName;
redirectTargetsSet = oldProgram.redirectTargetsSet;
return oldProgram.structureIsReused = StructureIsReused.Completely;
}
@@ -1537,7 +1591,7 @@ namespace ts {
/** This has side effects through `findSourceFile`. */
function processSourceFile(fileName: string, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): void {
getSourceFileFromReferenceWorker(fileName,
fileName => findSourceFile(fileName, toPath(fileName), isDefaultLib, refFile, refPos, refEnd),
fileName => findSourceFile(fileName, toPath(fileName), isDefaultLib, refFile, refPos, refEnd, /*packageId*/ undefined),
(diagnostic, ...args) => {
fileProcessingDiagnostics.add(refFile !== undefined && refEnd !== undefined && refPos !== undefined
? createFileDiagnostic(refFile, refPos, refEnd - refPos, diagnostic, ...args)
@@ -1556,8 +1610,26 @@ namespace ts {
}
}
function createRedirectSourceFile(redirectTarget: SourceFile, unredirected: SourceFile, fileName: string, path: Path): SourceFile {
const redirect: SourceFile = Object.create(redirectTarget);
redirect.fileName = fileName;
redirect.path = path;
redirect.redirectInfo = { redirectTarget, unredirected };
Object.defineProperties(redirect, {
id: {
get(this: SourceFile) { return this.redirectInfo.redirectTarget.id; },
set(this: SourceFile, value: SourceFile["id"]) { this.redirectInfo.redirectTarget.id = value; },
},
symbol: {
get(this: SourceFile) { return this.redirectInfo.redirectTarget.symbol; },
set(this: SourceFile, value: SourceFile["symbol"]) { this.redirectInfo.redirectTarget.symbol = value; },
},
});
return redirect;
}
// Get source file from normalized fileName
function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile {
function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, refFile: SourceFile, refPos: number, refEnd: number, packageId: PackageId | undefined): SourceFile | undefined {
if (filesByName.has(path)) {
const file = filesByName.get(path);
// try to check if we've already seen this file but with a different casing in path
@@ -1600,6 +1672,26 @@ namespace ts {
}
});
if (packageId) {
const packageIdKey = `${packageId.name}@${packageId.version}`;
const fileFromPackageId = packageIdToSourceFile.get(packageIdKey);
if (fileFromPackageId) {
// Some other SourceFile already exists with this package name and version.
// Instead of creating a duplicate, just redirect to the existing one.
const dupFile = createRedirectSourceFile(fileFromPackageId, file, fileName, path);
redirectTargetsSet.set(fileFromPackageId.path, true);
filesByName.set(path, dupFile);
sourceFileToPackageName.set(path, packageId.name);
files.push(dupFile);
return dupFile;
}
else if (file) {
// This is the first source file to have this packageId.
packageIdToSourceFile.set(packageIdKey, file);
sourceFileToPackageName.set(path, packageId.name);
}
}
filesByName.set(path, file);
if (file) {
sourceFilesFoundSearchingNodeModules.set(path, currentNodeModulesDepth > 0);
@@ -1762,7 +1854,7 @@ namespace ts {
else if (shouldAddFile) {
const path = toPath(resolvedFileName);
const pos = skipTrivia(file.text, file.imports[i].pos);
findSourceFile(resolvedFileName, path, /*isDefaultLib*/ false, file, pos, file.imports[i].end);
findSourceFile(resolvedFileName, path, /*isDefaultLib*/ false, file, pos, file.imports[i].end, resolution.packageId);
}
if (isFromNodeModulesSearch) {