mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-15 03:23:08 -06:00
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:
parent
17a6f7b56a
commit
37b20ee670
@ -2210,20 +2210,14 @@ namespace ts {
|
||||
/** Must have ".d.ts" first because if ".ts" goes first, that will be detected as the extension instead of ".d.ts". */
|
||||
export const supportedTypescriptExtensionsForExtractExtension: ReadonlyArray<Extension> = [Extension.Dts, Extension.Ts, Extension.Tsx];
|
||||
export const supportedJavascriptExtensions: ReadonlyArray<Extension> = [Extension.Js, Extension.Jsx];
|
||||
const allSupportedExtensions = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions];
|
||||
const allSupportedExtensions: ReadonlyArray<Extension> = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions];
|
||||
|
||||
export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: ReadonlyArray<JsFileExtensionInfo>): ReadonlyArray<string> {
|
||||
const needAllExtensions = options && options.allowJs;
|
||||
if (!extraFileExtensions || extraFileExtensions.length === 0 || !needAllExtensions) {
|
||||
return needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions;
|
||||
}
|
||||
const extensions: string[] = allSupportedExtensions.slice(0);
|
||||
for (const extInfo of extraFileExtensions) {
|
||||
if (extensions.indexOf(extInfo.extension) === -1) {
|
||||
extensions.push(extInfo.extension);
|
||||
}
|
||||
}
|
||||
return extensions;
|
||||
return deduplicate([...allSupportedExtensions, ...extraFileExtensions.map(e => e.extension)]);
|
||||
}
|
||||
|
||||
export function hasJavaScriptFileExtension(fileName: string) {
|
||||
@ -2590,6 +2584,11 @@ namespace ts {
|
||||
}
|
||||
Debug.fail(`File ${path} has unknown extension.`);
|
||||
}
|
||||
|
||||
export function isAnySupportedFileExtension(path: string): boolean {
|
||||
return tryGetExtensionFromPath(path) !== undefined;
|
||||
}
|
||||
|
||||
export function tryGetExtensionFromPath(path: string): Extension | undefined {
|
||||
return find<Extension>(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e));
|
||||
}
|
||||
|
||||
@ -13,13 +13,32 @@ namespace ts {
|
||||
return compilerOptions.traceResolution && host.trace !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of trying to resolve a module.
|
||||
* At least one of `ts` and `js` should be defined, or the whole thing should be `undefined`.
|
||||
*/
|
||||
/** Array that is only intended to be pushed to, never read. */
|
||||
/* @internal */
|
||||
export interface Push<T> {
|
||||
push(value: T): void;
|
||||
}
|
||||
|
||||
function withPackageId(packageId: PackageId | undefined, r: PathAndExtension | undefined): Resolved {
|
||||
return r && { path: r.path, extension: r.ext, packageId };
|
||||
}
|
||||
|
||||
function noPackageId(r: PathAndExtension | undefined): Resolved {
|
||||
return withPackageId(/*packageId*/ undefined, r);
|
||||
}
|
||||
|
||||
/** Result of trying to resolve a module. */
|
||||
interface Resolved {
|
||||
path: string;
|
||||
extension: Extension;
|
||||
packageId: PackageId | undefined;
|
||||
}
|
||||
|
||||
/** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */
|
||||
interface PathAndExtension {
|
||||
path: string;
|
||||
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
|
||||
ext: Extension;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +62,7 @@ namespace ts {
|
||||
|
||||
function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean, failedLookupLocations: string[]): ResolvedModuleWithFailedLookupLocations {
|
||||
return {
|
||||
resolvedModule: resolved && { resolvedFileName: resolved.path, extension: resolved.extension, isExternalLibraryImport },
|
||||
resolvedModule: resolved && { resolvedFileName: resolved.path, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId },
|
||||
failedLookupLocations
|
||||
};
|
||||
}
|
||||
@ -54,9 +73,16 @@ namespace ts {
|
||||
traceEnabled: boolean;
|
||||
}
|
||||
|
||||
interface PackageJson {
|
||||
name?: string;
|
||||
version?: string;
|
||||
typings?: string;
|
||||
types?: string;
|
||||
main?: string;
|
||||
}
|
||||
|
||||
/** Reads from "main" or "types"/"typings" depending on `extensions`. */
|
||||
function tryReadPackageJsonFields(readTypes: boolean, packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string | undefined {
|
||||
const jsonContent = readJson(packageJsonPath, state.host);
|
||||
function tryReadPackageJsonFields(readTypes: boolean, jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState): string | undefined {
|
||||
return readTypes ? tryReadFromField("typings") || tryReadFromField("types") : tryReadFromField("main");
|
||||
|
||||
function tryReadFromField(fieldName: "typings" | "types" | "main"): string | undefined {
|
||||
@ -83,7 +109,7 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(path: string, host: ModuleResolutionHost): { typings?: string, types?: string, main?: string } {
|
||||
function readJson(path: string, host: ModuleResolutionHost): PackageJson {
|
||||
try {
|
||||
const jsonText = host.readFile(path);
|
||||
return jsonText ? JSON.parse(jsonText) : {};
|
||||
@ -646,7 +672,7 @@ namespace ts {
|
||||
if (extension !== undefined) {
|
||||
const path = tryFile(candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state);
|
||||
if (path !== undefined) {
|
||||
return { path, extension };
|
||||
return { path, extension, packageId: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
@ -709,7 +735,7 @@ namespace ts {
|
||||
}
|
||||
const resolved = loadModuleFromNodeModules(extensions, moduleName, containingDirectory, failedLookupLocations, state, cache);
|
||||
// For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files.
|
||||
return resolved && { value: resolved.value && { resolved: { path: realpath(resolved.value.path, host, traceEnabled), extension: resolved.value.extension }, isExternalLibraryImport: true } };
|
||||
return resolved && { value: resolved.value && { resolved: { ...resolved.value, path: realpath(resolved.value.path, host, traceEnabled) }, isExternalLibraryImport: true } };
|
||||
}
|
||||
else {
|
||||
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
|
||||
@ -747,7 +773,7 @@ namespace ts {
|
||||
}
|
||||
const resolvedFromFile = loadModuleFromFile(extensions, candidate, failedLookupLocations, onlyRecordFailures, state);
|
||||
if (resolvedFromFile) {
|
||||
return resolvedFromFile;
|
||||
return noPackageId(resolvedFromFile);
|
||||
}
|
||||
}
|
||||
if (!onlyRecordFailures) {
|
||||
@ -768,11 +794,15 @@ namespace ts {
|
||||
return !host.directoryExists || host.directoryExists(directoryName);
|
||||
}
|
||||
|
||||
function loadModuleFromFileNoPackageId(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved {
|
||||
return noPackageId(loadModuleFromFile(extensions, candidate, failedLookupLocations, onlyRecordFailures, state));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary
|
||||
* in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations.
|
||||
*/
|
||||
function loadModuleFromFile(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
|
||||
function loadModuleFromFile(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
|
||||
// First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts"
|
||||
const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, failedLookupLocations, onlyRecordFailures, state);
|
||||
if (resolvedByAddingExtension) {
|
||||
@ -792,7 +822,7 @@ namespace ts {
|
||||
}
|
||||
|
||||
/** Try to return an existing file that adds one of the `extensions` to `candidate`. */
|
||||
function tryAddingExtensions(candidate: string, extensions: Extensions, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
|
||||
function tryAddingExtensions(candidate: string, extensions: Extensions, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
|
||||
if (!onlyRecordFailures) {
|
||||
// check if containing folder exists - if it doesn't then just record failures for all supported extensions without disk probing
|
||||
const directory = getDirectoryPath(candidate);
|
||||
@ -810,9 +840,9 @@ namespace ts {
|
||||
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
|
||||
}
|
||||
|
||||
function tryExtension(extension: Extension): Resolved | undefined {
|
||||
const path = tryFile(candidate + extension, failedLookupLocations, onlyRecordFailures, state);
|
||||
return path && { path, extension };
|
||||
function tryExtension(ext: Extension): PathAndExtension | undefined {
|
||||
const path = tryFile(candidate + ext, failedLookupLocations, onlyRecordFailures, state);
|
||||
return path && { path, ext };
|
||||
}
|
||||
}
|
||||
|
||||
@ -838,12 +868,23 @@ namespace ts {
|
||||
function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true): Resolved | undefined {
|
||||
const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host);
|
||||
|
||||
let packageId: PackageId | undefined;
|
||||
|
||||
if (considerPackageJson) {
|
||||
const packageJsonPath = pathToPackageJson(candidate);
|
||||
if (directoryExists && state.host.fileExists(packageJsonPath)) {
|
||||
const fromPackageJson = loadModuleFromPackageJson(packageJsonPath, extensions, candidate, failedLookupLocations, state);
|
||||
if (state.traceEnabled) {
|
||||
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
|
||||
}
|
||||
const jsonContent = readJson(packageJsonPath, state.host);
|
||||
|
||||
if (typeof jsonContent.name === "string" && typeof jsonContent.version === "string") {
|
||||
packageId = { name: jsonContent.name, version: jsonContent.version };
|
||||
}
|
||||
|
||||
const fromPackageJson = loadModuleFromPackageJson(jsonContent, extensions, candidate, failedLookupLocations, state);
|
||||
if (fromPackageJson) {
|
||||
return fromPackageJson;
|
||||
return withPackageId(packageId, fromPackageJson);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@ -855,15 +896,11 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
return loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocations, !directoryExists, state);
|
||||
return withPackageId(packageId, loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocations, !directoryExists, state));
|
||||
}
|
||||
|
||||
function loadModuleFromPackageJson(packageJsonPath: string, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
|
||||
if (state.traceEnabled) {
|
||||
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
|
||||
}
|
||||
|
||||
const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, packageJsonPath, candidate, state);
|
||||
function loadModuleFromPackageJson(jsonContent: PackageJson, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): PathAndExtension | undefined {
|
||||
const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, jsonContent, candidate, state);
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
@ -883,13 +920,18 @@ namespace ts {
|
||||
// Even if extensions is DtsOnly, we can still look up a .ts file as a result of package.json "types"
|
||||
const nextExtensions = extensions === Extensions.DtsOnly ? Extensions.TypeScript : extensions;
|
||||
// Don't do package.json lookup recursively, because Node.js' package lookup doesn't.
|
||||
return nodeLoadModuleByRelativeName(nextExtensions, file, failedLookupLocations, onlyRecordFailures, state, /*considerPackageJson*/ false);
|
||||
const result = nodeLoadModuleByRelativeName(nextExtensions, file, failedLookupLocations, onlyRecordFailures, state, /*considerPackageJson*/ false);
|
||||
if (result) {
|
||||
// It won't have a `packageId` set, because we disabled `considerPackageJson`.
|
||||
Debug.assert(result.packageId === undefined);
|
||||
return { path: result.path, ext: result.extension };
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */
|
||||
function resolvedIfExtensionMatches(extensions: Extensions, path: string): Resolved | undefined {
|
||||
const extension = tryGetExtensionFromPath(path);
|
||||
return extension !== undefined && extensionIsOk(extensions, extension) ? { path, extension } : undefined;
|
||||
function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined {
|
||||
const ext = tryGetExtensionFromPath(path);
|
||||
return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined;
|
||||
}
|
||||
|
||||
/** True if `extension` is one of the supported `extensions`. */
|
||||
@ -911,7 +953,7 @@ namespace ts {
|
||||
function loadModuleFromNodeModulesFolder(extensions: Extensions, moduleName: string, nodeModulesFolder: string, nodeModulesFolderExists: boolean, failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
|
||||
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
|
||||
|
||||
return loadModuleFromFile(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state) ||
|
||||
return loadModuleFromFileNoPackageId(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state) ||
|
||||
loadNodeModuleFromDirectory(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
|
||||
}
|
||||
|
||||
@ -996,7 +1038,7 @@ namespace ts {
|
||||
if (traceEnabled) {
|
||||
trace(host, Diagnostics.Resolution_for_module_0_was_found_in_cache, moduleName);
|
||||
}
|
||||
return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension } };
|
||||
return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1010,7 +1052,7 @@ namespace ts {
|
||||
return createResolvedModuleWithFailedLookupLocations(resolved && resolved.value, /*isExternalLibraryImport*/ false, failedLookupLocations);
|
||||
|
||||
function tryResolve(extensions: Extensions): SearchResult<Resolved> {
|
||||
const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFile, failedLookupLocations, state);
|
||||
const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFileNoPackageId, failedLookupLocations, state);
|
||||
if (resolvedUsingSettings) {
|
||||
return { value: resolvedUsingSettings };
|
||||
}
|
||||
@ -1024,7 +1066,7 @@ namespace ts {
|
||||
return resolutionFromCache;
|
||||
}
|
||||
const searchName = normalizePath(combinePaths(directory, moduleName));
|
||||
return toSearchResult(loadModuleFromFile(extensions, searchName, failedLookupLocations, /*onlyRecordFailures*/ false, state));
|
||||
return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, failedLookupLocations, /*onlyRecordFailures*/ false, state));
|
||||
});
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
@ -1036,7 +1078,7 @@ namespace ts {
|
||||
}
|
||||
else {
|
||||
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
|
||||
return toSearchResult(loadModuleFromFile(extensions, candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state));
|
||||
return toSearchResult(loadModuleFromFileNoPackageId(extensions, candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -2255,6 +2255,17 @@ namespace ts {
|
||||
}
|
||||
|
||||
|
||||
/* @internal */
|
||||
export interface RedirectInfo {
|
||||
/** Source file this redirects to. */
|
||||
readonly redirectTarget: SourceFile;
|
||||
/**
|
||||
* Source file for the duplicate package. This will not be used by the Program,
|
||||
* but we need to keep this around so we can watch for changes in underlying.
|
||||
*/
|
||||
readonly unredirected: SourceFile;
|
||||
}
|
||||
|
||||
// Source files are declarations when they are external modules.
|
||||
export interface SourceFile extends Declaration {
|
||||
kind: SyntaxKind.SourceFile;
|
||||
@ -2265,6 +2276,13 @@ namespace ts {
|
||||
/* @internal */ path: Path;
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* If two source files are for the same version of the same package, one will redirect to the other.
|
||||
* (See `createRedirectSourceFile` in program.ts.)
|
||||
* The redirect will have this set. The other will not have anything set, but see Program#sourceFileIsRedirectedTo.
|
||||
*/
|
||||
/* @internal */ redirectInfo?: RedirectInfo | undefined;
|
||||
|
||||
amdDependencies: AmdDependency[];
|
||||
moduleName: string;
|
||||
referencedFiles: FileReference[];
|
||||
@ -2435,6 +2453,11 @@ namespace ts {
|
||||
/* @internal */ structureIsReused?: StructureIsReused;
|
||||
|
||||
/* @internal */ getSourceFileFromReference(referencingFile: SourceFile, ref: FileReference): SourceFile | undefined;
|
||||
|
||||
/** Given a source file, get the name of the package it was imported from. */
|
||||
/* @internal */ sourceFileToPackageName: Map<string>;
|
||||
/** Set of all source files that some other source file redirects to. */
|
||||
/* @internal */ redirectTargetsSet: Map<true>;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
@ -3925,6 +3948,7 @@ namespace ts {
|
||||
/**
|
||||
* ResolvedModule with an explicitly provided `extension` property.
|
||||
* Prefer this over `ResolvedModule`.
|
||||
* If changing this, remember to change `moduleResolutionIsEqualTo`.
|
||||
*/
|
||||
export interface ResolvedModuleFull extends ResolvedModule {
|
||||
/**
|
||||
@ -3932,6 +3956,22 @@ namespace ts {
|
||||
* This is optional for backwards-compatibility, but will be added if not provided.
|
||||
*/
|
||||
extension: Extension;
|
||||
packageId?: PackageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique identifier with a package name and version.
|
||||
* If changing this, remember to change `packageIdIsEqual`.
|
||||
*/
|
||||
export interface PackageId {
|
||||
/**
|
||||
* Name of the package.
|
||||
* Should not include `@types`.
|
||||
* If accessing a non-index file, this should include its name e.g. "foo/bar".
|
||||
*/
|
||||
name: string;
|
||||
/** Version of the package, e.g. "1.2.3" */
|
||||
version: string;
|
||||
}
|
||||
|
||||
export const enum Extension {
|
||||
|
||||
@ -101,7 +101,12 @@ namespace ts {
|
||||
export function moduleResolutionIsEqualTo(oldResolution: ResolvedModuleFull, newResolution: ResolvedModuleFull): boolean {
|
||||
return oldResolution.isExternalLibraryImport === newResolution.isExternalLibraryImport &&
|
||||
oldResolution.extension === newResolution.extension &&
|
||||
oldResolution.resolvedFileName === newResolution.resolvedFileName;
|
||||
oldResolution.resolvedFileName === newResolution.resolvedFileName &&
|
||||
packageIdIsEqual(oldResolution.packageId, newResolution.packageId);
|
||||
}
|
||||
|
||||
function packageIdIsEqual(a: PackageId | undefined, b: PackageId | undefined): boolean {
|
||||
return a === b || a && b && a.name === b.name && a.version === b.version;
|
||||
}
|
||||
|
||||
export function typeDirectiveIsEqualTo(oldResolution: ResolvedTypeReferenceDirective, newResolution: ResolvedTypeReferenceDirective): boolean {
|
||||
|
||||
@ -451,7 +451,7 @@ namespace FourSlash {
|
||||
this.languageServiceAdapterHost.openFile(fileToOpen.fileName, content, scriptKindName);
|
||||
}
|
||||
|
||||
public verifyErrorExistsBetweenMarkers(startMarkerName: string, endMarkerName: string, negative: boolean) {
|
||||
public verifyErrorExistsBetweenMarkers(startMarkerName: string, endMarkerName: string, shouldExist: boolean) {
|
||||
const startMarker = this.getMarkerByName(startMarkerName);
|
||||
const endMarker = this.getMarkerByName(endMarkerName);
|
||||
const predicate = (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) =>
|
||||
@ -459,9 +459,9 @@ namespace FourSlash {
|
||||
|
||||
const exists = this.anyErrorInRange(predicate, startMarker, endMarker);
|
||||
|
||||
if (exists !== negative) {
|
||||
this.printErrorLog(negative, this.getAllDiagnostics());
|
||||
throw new Error(`Failure between markers: '${startMarkerName}', '${endMarkerName}'`);
|
||||
if (exists !== shouldExist) {
|
||||
this.printErrorLog(shouldExist, this.getAllDiagnostics());
|
||||
throw new Error(`${shouldExist ? "Expected" : "Did not expect"} failure between markers: '${startMarkerName}', '${endMarkerName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,10 +483,11 @@ namespace FourSlash {
|
||||
}
|
||||
|
||||
private getAllDiagnostics(): ts.Diagnostic[] {
|
||||
return ts.flatMap(this.languageServiceAdapterHost.getFilenames(), fileName => this.getDiagnostics(fileName));
|
||||
return ts.flatMap(this.languageServiceAdapterHost.getFilenames(), fileName =>
|
||||
ts.isAnySupportedFileExtension(fileName) ? this.getDiagnostics(fileName) : []);
|
||||
}
|
||||
|
||||
public verifyErrorExistsAfterMarker(markerName: string, negative: boolean, after: boolean) {
|
||||
public verifyErrorExistsAfterMarker(markerName: string, shouldExist: boolean, after: boolean) {
|
||||
const marker: Marker = this.getMarkerByName(markerName);
|
||||
let predicate: (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) => boolean;
|
||||
|
||||
@ -502,30 +503,15 @@ namespace FourSlash {
|
||||
const exists = this.anyErrorInRange(predicate, marker);
|
||||
const diagnostics = this.getAllDiagnostics();
|
||||
|
||||
if (exists !== negative) {
|
||||
this.printErrorLog(negative, diagnostics);
|
||||
throw new Error("Failure at marker: " + markerName);
|
||||
if (exists !== shouldExist) {
|
||||
this.printErrorLog(shouldExist, diagnostics);
|
||||
throw new Error(`${shouldExist ? "Expected" : "Did not expect"} failure at marker '${markerName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
private anyErrorInRange(predicate: (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) => boolean, startMarker: Marker, endMarker?: Marker) {
|
||||
|
||||
const errors = this.getDiagnostics(startMarker.fileName);
|
||||
let exists = false;
|
||||
|
||||
const startPos = startMarker.position;
|
||||
let endPos: number = undefined;
|
||||
if (endMarker !== undefined) {
|
||||
endPos = endMarker.position;
|
||||
}
|
||||
|
||||
errors.forEach(function (error: ts.Diagnostic) {
|
||||
if (predicate(error.start, error.start + error.length, startPos, endPos)) {
|
||||
exists = true;
|
||||
}
|
||||
});
|
||||
|
||||
return exists;
|
||||
private anyErrorInRange(predicate: (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) => boolean, startMarker: Marker, endMarker?: Marker): boolean {
|
||||
return this.getDiagnostics(startMarker.fileName).some(({ start, length }) =>
|
||||
predicate(start, start + length, startMarker.position, endMarker === undefined ? undefined : endMarker.position));
|
||||
}
|
||||
|
||||
private printErrorLog(expectErrors: boolean, errors: ts.Diagnostic[]) {
|
||||
@ -550,6 +536,7 @@ namespace FourSlash {
|
||||
|
||||
public verifyNoErrors() {
|
||||
ts.forEachKey(this.inputFiles, fileName => {
|
||||
if (!ts.isAnySupportedFileExtension(fileName)) return;
|
||||
const errors = this.getDiagnostics(fileName);
|
||||
if (errors.length) {
|
||||
this.printErrorLog(/*expectErrors*/ false, errors);
|
||||
|
||||
@ -193,7 +193,9 @@ namespace Harness.LanguageService {
|
||||
}
|
||||
getCurrentDirectory(): string { return virtualFileSystemRoot; }
|
||||
getDefaultLibFileName(): string { return Harness.Compiler.defaultLibFileName; }
|
||||
getScriptFileNames(): string[] { return this.getFilenames(); }
|
||||
getScriptFileNames(): string[] {
|
||||
return this.getFilenames().filter(ts.isAnySupportedFileExtension);
|
||||
}
|
||||
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
|
||||
const script = this.getScriptInfo(fileName);
|
||||
return script ? new ScriptSnapshot(script) : undefined;
|
||||
|
||||
@ -109,7 +109,10 @@ namespace ts {
|
||||
function createTestCompilerHost(texts: NamedSourceText[], target: ScriptTarget, oldProgram?: ProgramWithSourceTexts): TestCompilerHost {
|
||||
const files = arrayToMap(texts, t => t.name, t => {
|
||||
if (oldProgram) {
|
||||
const oldFile = <SourceFileWithText>oldProgram.getSourceFile(t.name);
|
||||
let oldFile = <SourceFileWithText>oldProgram.getSourceFile(t.name);
|
||||
if (oldFile && oldFile.redirectInfo) {
|
||||
oldFile = oldFile.redirectInfo.unredirected;
|
||||
}
|
||||
if (oldFile && oldFile.sourceText.getVersion() === t.text.getVersion()) {
|
||||
return oldFile;
|
||||
}
|
||||
@ -171,11 +174,16 @@ namespace ts {
|
||||
return program;
|
||||
}
|
||||
|
||||
function updateProgramText(files: ReadonlyArray<NamedSourceText>, fileName: string, newProgramText: string) {
|
||||
const file = find(files, f => f.name === fileName)!;
|
||||
file.text = file.text.updateProgram(newProgramText);
|
||||
}
|
||||
|
||||
function checkResolvedTypeDirective(expected: ResolvedTypeReferenceDirective, actual: ResolvedTypeReferenceDirective): boolean {
|
||||
if (!expected === !actual) {
|
||||
if (expected) {
|
||||
assert.isTrue(expected.resolvedFileName === actual.resolvedFileName, `'resolvedFileName': expected '${expected.resolvedFileName}' to be equal to '${actual.resolvedFileName}'`);
|
||||
assert.isTrue(expected.primary === actual.primary, `'primary': expected '${expected.primary}' to be equal to '${actual.primary}'`);
|
||||
assert.equal(expected.resolvedFileName, actual.resolvedFileName, `'resolvedFileName': expected '${expected.resolvedFileName}' to be equal to '${actual.resolvedFileName}'`);
|
||||
assert.equal(expected.primary, actual.primary, `'primary': expected '${expected.primary}' to be equal to '${actual.primary}'`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -238,7 +246,7 @@ namespace ts {
|
||||
const program_2 = updateProgram(program_1, ["a.ts"], { target }, files => {
|
||||
files[0].text = files[0].text.updateProgram("var x = 100");
|
||||
});
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Completely);
|
||||
const program1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("a.ts"));
|
||||
const program2Diagnostics = program_2.getSemanticDiagnostics(program_1.getSourceFile("a.ts"));
|
||||
assert.equal(program1Diagnostics.length, program2Diagnostics.length);
|
||||
@ -249,7 +257,7 @@ namespace ts {
|
||||
const program_2 = updateProgram(program_1, ["a.ts"], { target }, files => {
|
||||
files[0].text = files[0].text.updateProgram("var x = 100");
|
||||
});
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Completely);
|
||||
const program1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("a.ts"));
|
||||
const program2Diagnostics = program_2.getSemanticDiagnostics(program_1.getSourceFile("a.ts"));
|
||||
assert.equal(program1Diagnostics.length, program2Diagnostics.length);
|
||||
@ -263,19 +271,19 @@ namespace ts {
|
||||
`;
|
||||
files[0].text = files[0].text.updateReferences(newReferences);
|
||||
});
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.SafeModules);
|
||||
});
|
||||
|
||||
it("fails if change affects type references", () => {
|
||||
const program_1 = newProgram(files, ["a.ts"], { types: ["a"] });
|
||||
updateProgram(program_1, ["a.ts"], { types: ["b"] }, noop);
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Not);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Not);
|
||||
});
|
||||
|
||||
it("succeeds if change doesn't affect type references", () => {
|
||||
const program_1 = newProgram(files, ["a.ts"], { types: ["a"] });
|
||||
updateProgram(program_1, ["a.ts"], { types: ["a"] }, noop);
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Completely);
|
||||
});
|
||||
|
||||
it("fails if change affects imports", () => {
|
||||
@ -283,7 +291,7 @@ namespace ts {
|
||||
updateProgram(program_1, ["a.ts"], { target }, files => {
|
||||
files[2].text = files[2].text.updateImportsAndExports("import x from 'b'");
|
||||
});
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.SafeModules);
|
||||
});
|
||||
|
||||
it("fails if change affects type directives", () => {
|
||||
@ -295,25 +303,25 @@ namespace ts {
|
||||
/// <reference types="typerefs1" />`;
|
||||
files[0].text = files[0].text.updateReferences(newReferences);
|
||||
});
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.SafeModules);
|
||||
});
|
||||
|
||||
it("fails if module kind changes", () => {
|
||||
const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS });
|
||||
updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.AMD }, noop);
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Not);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Not);
|
||||
});
|
||||
|
||||
it("fails if rootdir changes", () => {
|
||||
const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/b" });
|
||||
updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/c" }, noop);
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Not);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Not);
|
||||
});
|
||||
|
||||
it("fails if config path changes", () => {
|
||||
const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" });
|
||||
updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, noop);
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Not);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Not);
|
||||
});
|
||||
|
||||
it("succeeds if missing files remain missing", () => {
|
||||
@ -357,7 +365,7 @@ namespace ts {
|
||||
const program_2 = updateProgram(program_1, ["a.ts"], options, files => {
|
||||
files[0].text = files[0].text.updateProgram("var x = 2");
|
||||
});
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Completely);
|
||||
|
||||
// content of resolution cache should not change
|
||||
checkResolvedModulesCache(program_1, "a.ts", createMapFromTemplate({ "b": createResolvedModule("b.ts") }));
|
||||
@ -367,7 +375,7 @@ namespace ts {
|
||||
const program_3 = updateProgram(program_2, ["a.ts"], options, files => {
|
||||
files[0].text = files[0].text.updateImportsAndExports("");
|
||||
});
|
||||
assert.isTrue(program_2.structureIsReused === StructureIsReused.SafeModules);
|
||||
assert.equal(program_2.structureIsReused, StructureIsReused.SafeModules);
|
||||
checkResolvedModulesCache(program_3, "a.ts", /*expectedContent*/ undefined);
|
||||
|
||||
const program_4 = updateProgram(program_3, ["a.ts"], options, files => {
|
||||
@ -376,7 +384,7 @@ namespace ts {
|
||||
`;
|
||||
files[0].text = files[0].text.updateImportsAndExports(newImports);
|
||||
});
|
||||
assert.isTrue(program_3.structureIsReused === StructureIsReused.SafeModules);
|
||||
assert.equal(program_3.structureIsReused, StructureIsReused.SafeModules);
|
||||
checkResolvedModulesCache(program_4, "a.ts", createMapFromTemplate({ "b": createResolvedModule("b.ts"), "c": undefined }));
|
||||
});
|
||||
|
||||
@ -394,7 +402,7 @@ namespace ts {
|
||||
const program_2 = updateProgram(program_1, ["/a.ts"], options, files => {
|
||||
files[0].text = files[0].text.updateProgram("var x = 2");
|
||||
});
|
||||
assert.isTrue(program_1.structureIsReused === StructureIsReused.Completely);
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Completely);
|
||||
|
||||
// content of resolution cache should not change
|
||||
checkResolvedTypeDirectivesCache(program_1, "/a.ts", createMapFromTemplate({ "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }));
|
||||
@ -405,7 +413,7 @@ namespace ts {
|
||||
files[0].text = files[0].text.updateReferences("");
|
||||
});
|
||||
|
||||
assert.isTrue(program_2.structureIsReused === StructureIsReused.SafeModules);
|
||||
assert.equal(program_2.structureIsReused, StructureIsReused.SafeModules);
|
||||
checkResolvedTypeDirectivesCache(program_3, "/a.ts", /*expectedContent*/ undefined);
|
||||
|
||||
updateProgram(program_3, ["/a.ts"], options, files => {
|
||||
@ -414,7 +422,7 @@ namespace ts {
|
||||
`;
|
||||
files[0].text = files[0].text.updateReferences(newReferences);
|
||||
});
|
||||
assert.isTrue(program_3.structureIsReused === StructureIsReused.SafeModules);
|
||||
assert.equal(program_3.structureIsReused, StructureIsReused.SafeModules);
|
||||
checkResolvedTypeDirectivesCache(program_1, "/a.ts", createMapFromTemplate({ "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }));
|
||||
});
|
||||
|
||||
@ -454,7 +462,7 @@ namespace ts {
|
||||
"initialProgram: execute module resolution normally.");
|
||||
|
||||
const initialProgramDiagnostics = initialProgram.getSemanticDiagnostics(initialProgram.getSourceFile("file1.ts"));
|
||||
assert(initialProgramDiagnostics.length === 1, `initialProgram: import should fail.`);
|
||||
assert.lengthOf(initialProgramDiagnostics, 1, `initialProgram: import should fail.`);
|
||||
}
|
||||
|
||||
const afterNpmInstallProgram = updateProgram(initialProgram, rootFiles.map(f => f.name), options, f => {
|
||||
@ -478,7 +486,7 @@ namespace ts {
|
||||
"afterNpmInstallProgram: execute module resolution normally.");
|
||||
|
||||
const afterNpmInstallProgramDiagnostics = afterNpmInstallProgram.getSemanticDiagnostics(afterNpmInstallProgram.getSourceFile("file1.ts"));
|
||||
assert(afterNpmInstallProgramDiagnostics.length === 0, `afterNpmInstallProgram: program is well-formed with import.`);
|
||||
assert.lengthOf(afterNpmInstallProgramDiagnostics, 0, `afterNpmInstallProgram: program is well-formed with import.`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -617,10 +625,10 @@ namespace ts {
|
||||
"File 'f1.ts' exist - use it as a name resolution result.",
|
||||
"======== Module name './f1' was successfully resolved to 'f1.ts'. ========"
|
||||
],
|
||||
"program_1: execute module reoslution normally.");
|
||||
"program_1: execute module resolution normally.");
|
||||
|
||||
const program_1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("f2.ts"));
|
||||
assert(program_1Diagnostics.length === expectedErrors, `initial program should be well-formed`);
|
||||
assert.lengthOf(program_1Diagnostics, expectedErrors, `initial program should be well-formed`);
|
||||
}
|
||||
const indexOfF1 = 6;
|
||||
const program_2 = updateProgram(program_1, program_1.getRootFileNames(), options, f => {
|
||||
@ -630,7 +638,7 @@ namespace ts {
|
||||
|
||||
{
|
||||
const program_2Diagnostics = program_2.getSemanticDiagnostics(program_2.getSourceFile("f2.ts"));
|
||||
assert(program_2Diagnostics.length === expectedErrors, `removing no-default-lib shouldn't affect any types used.`);
|
||||
assert.lengthOf(program_2Diagnostics, expectedErrors, `removing no-default-lib shouldn't affect any types used.`);
|
||||
|
||||
assert.deepEqual(program_2.host.getTrace(), [
|
||||
"======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========",
|
||||
@ -659,7 +667,7 @@ namespace ts {
|
||||
|
||||
{
|
||||
const program_3Diagnostics = program_3.getSemanticDiagnostics(program_3.getSourceFile("f2.ts"));
|
||||
assert(program_3Diagnostics.length === expectedErrors, `typerefs2 was unused, so diagnostics should be unaffected.`);
|
||||
assert.lengthOf(program_3Diagnostics, expectedErrors, `typerefs2 was unused, so diagnostics should be unaffected.`);
|
||||
|
||||
assert.deepEqual(program_3.host.getTrace(), [
|
||||
"======== Resolving module './b1' from 'f1.ts'. ========",
|
||||
@ -684,7 +692,7 @@ namespace ts {
|
||||
|
||||
{
|
||||
const program_4Diagnostics = program_4.getSemanticDiagnostics(program_4.getSourceFile("f2.ts"));
|
||||
assert(program_4Diagnostics.length === expectedErrors, `a1.ts was unused, so diagnostics should be unaffected.`);
|
||||
assert.lengthOf(program_4Diagnostics, expectedErrors, `a1.ts was unused, so diagnostics should be unaffected.`);
|
||||
|
||||
assert.deepEqual(program_4.host.getTrace(), [
|
||||
"======== Resolving module './b1' from 'f1.ts'. ========",
|
||||
@ -708,7 +716,7 @@ namespace ts {
|
||||
|
||||
{
|
||||
const program_5Diagnostics = program_5.getSemanticDiagnostics(program_5.getSourceFile("f2.ts"));
|
||||
assert(program_5Diagnostics.length === ++expectedErrors, `import of BB in f1 fails. BB is of type any. Add one error`);
|
||||
assert.lengthOf(program_5Diagnostics, ++expectedErrors, `import of BB in f1 fails. BB is of type any. Add one error`);
|
||||
|
||||
assert.deepEqual(program_5.host.getTrace(), [
|
||||
"======== Resolving module './b1' from 'f1.ts'. ========",
|
||||
@ -725,7 +733,7 @@ namespace ts {
|
||||
|
||||
{
|
||||
const program_6Diagnostics = program_6.getSemanticDiagnostics(program_6.getSourceFile("f2.ts"));
|
||||
assert(program_6Diagnostics.length === expectedErrors, `import of BB in f1 fails.`);
|
||||
assert.lengthOf(program_6Diagnostics, expectedErrors, `import of BB in f1 fails.`);
|
||||
|
||||
assert.deepEqual(program_6.host.getTrace(), [
|
||||
"======== Resolving module './b1' from 'f1.ts'. ========",
|
||||
@ -749,7 +757,7 @@ namespace ts {
|
||||
|
||||
{
|
||||
const program_7Diagnostics = program_7.getSemanticDiagnostics(program_7.getSourceFile("f2.ts"));
|
||||
assert(program_7Diagnostics.length === expectedErrors, `removing import is noop with respect to program, so no change in diagnostics.`);
|
||||
assert.lengthOf(program_7Diagnostics, expectedErrors, `removing import is noop with respect to program, so no change in diagnostics.`);
|
||||
|
||||
assert.deepEqual(program_7.host.getTrace(), [
|
||||
"======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========",
|
||||
@ -762,6 +770,98 @@ namespace ts {
|
||||
], "program_7 should reuse module resolutions in f2 since it is unchanged");
|
||||
}
|
||||
});
|
||||
|
||||
describe("redirects", () => {
|
||||
const axIndex = "/node_modules/a/node_modules/x/index.d.ts";
|
||||
const axPackage = "/node_modules/a/node_modules/x/package.json";
|
||||
const bxIndex = "/node_modules/b/node_modules/x/index.d.ts";
|
||||
const bxPackage = "/node_modules/b/node_modules/x/package.json";
|
||||
const root = "/a.ts";
|
||||
const compilerOptions = { target, moduleResolution: ModuleResolutionKind.NodeJs };
|
||||
|
||||
function createRedirectProgram(options?: { bText: string, bVersion: string }): ProgramWithSourceTexts {
|
||||
const files: NamedSourceText[] = [
|
||||
{
|
||||
name: "/node_modules/a/index.d.ts",
|
||||
text: SourceText.New("", 'import X from "x";', "export function a(x: X): void;"),
|
||||
},
|
||||
{
|
||||
name: axIndex,
|
||||
text: SourceText.New("", "", "export default class X { private x: number; }"),
|
||||
},
|
||||
{
|
||||
name: axPackage,
|
||||
text: SourceText.New("", "", JSON.stringify({ name: "x", version: "1.2.3" })),
|
||||
},
|
||||
{
|
||||
name: "/node_modules/b/index.d.ts",
|
||||
text: SourceText.New("", 'import X from "x";', "export const b: X;"),
|
||||
},
|
||||
{
|
||||
name: bxIndex,
|
||||
text: SourceText.New("", "", options ? options.bText : "export default class X { private x: number; }"),
|
||||
},
|
||||
{
|
||||
name: bxPackage,
|
||||
text: SourceText.New("", "", JSON.stringify({ name: "x", version: options ? options.bVersion : "1.2.3" })),
|
||||
},
|
||||
{
|
||||
name: root,
|
||||
text: SourceText.New("", 'import { a } from "a"; import { b } from "b";', "a(b)"),
|
||||
},
|
||||
];
|
||||
|
||||
return newProgram(files, [root], compilerOptions);
|
||||
}
|
||||
|
||||
function updateRedirectProgram(program: ProgramWithSourceTexts, updater: (files: NamedSourceText[]) => void): ProgramWithSourceTexts {
|
||||
return updateProgram(program, [root], compilerOptions, updater);
|
||||
}
|
||||
|
||||
it("No changes -> redirect not broken", () => {
|
||||
const program_1 = createRedirectProgram();
|
||||
|
||||
const program_2 = updateRedirectProgram(program_1, files => {
|
||||
updateProgramText(files, root, "const x = 1;");
|
||||
});
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Completely);
|
||||
assert.deepEqual(program_2.getSemanticDiagnostics(), emptyArray);
|
||||
});
|
||||
|
||||
it("Target changes -> redirect broken", () => {
|
||||
const program_1 = createRedirectProgram();
|
||||
assert.deepEqual(program_1.getSemanticDiagnostics(), emptyArray);
|
||||
|
||||
const program_2 = updateRedirectProgram(program_1, files => {
|
||||
updateProgramText(files, axIndex, "export default class X { private x: number; private y: number; }");
|
||||
updateProgramText(files, axPackage, JSON.stringify('{ name: "x", version: "1.2.4" }'));
|
||||
});
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Not);
|
||||
assert.lengthOf(program_2.getSemanticDiagnostics(), 1);
|
||||
});
|
||||
|
||||
it("Underlying changes -> redirect broken", () => {
|
||||
const program_1 = createRedirectProgram();
|
||||
|
||||
const program_2 = updateRedirectProgram(program_1, files => {
|
||||
updateProgramText(files, bxIndex, "export default class X { private x: number; private y: number; }");
|
||||
updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.4" }));
|
||||
});
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Not);
|
||||
assert.lengthOf(program_2.getSemanticDiagnostics(), 1);
|
||||
});
|
||||
|
||||
it("Previously duplicate packages -> program structure not reused", () => {
|
||||
const program_1 = createRedirectProgram({ bVersion: "1.2.4", bText: "export = class X { private x: number; }" });
|
||||
|
||||
const program_2 = updateRedirectProgram(program_1, files => {
|
||||
updateProgramText(files, bxIndex, "export default class X { private x: number; }");
|
||||
updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.3" }));
|
||||
});
|
||||
assert.equal(program_1.structureIsReused, StructureIsReused.Not);
|
||||
assert.deepEqual(program_2.getSemanticDiagnostics(), []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("host is optional", () => {
|
||||
|
||||
48
tests/baselines/reference/duplicatePackage.errors.txt
Normal file
48
tests/baselines/reference/duplicatePackage.errors.txt
Normal file
@ -0,0 +1,48 @@
|
||||
/src/a.ts(5,3): error TS2345: Argument of type 'X' is not assignable to parameter of type 'X'.
|
||||
Types have separate declarations of a private property 'x'.
|
||||
|
||||
|
||||
==== /src/a.ts (1 errors) ====
|
||||
import { a } from "a";
|
||||
import { b } from "b";
|
||||
import { c } from "c";
|
||||
a(b); // Works
|
||||
a(c); // Error, these are from different versions of the library.
|
||||
~
|
||||
!!! error TS2345: Argument of type 'X' is not assignable to parameter of type 'X'.
|
||||
!!! error TS2345: Types have separate declarations of a private property 'x'.
|
||||
|
||||
==== /node_modules/a/index.d.ts (0 errors) ====
|
||||
import X from "x";
|
||||
export function a(x: X): void;
|
||||
|
||||
==== /node_modules/a/node_modules/x/index.d.ts (0 errors) ====
|
||||
export default class X {
|
||||
private x: number;
|
||||
}
|
||||
|
||||
==== /node_modules/a/node_modules/x/package.json (0 errors) ====
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
==== /node_modules/b/index.d.ts (0 errors) ====
|
||||
import X from "x";
|
||||
export const b: X;
|
||||
|
||||
==== /node_modules/b/node_modules/x/index.d.ts (0 errors) ====
|
||||
content not parsed
|
||||
|
||||
==== /node_modules/b/node_modules/x/package.json (0 errors) ====
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
==== /node_modules/c/index.d.ts (0 errors) ====
|
||||
import X from "x";
|
||||
export const c: X;
|
||||
|
||||
==== /node_modules/c/node_modules/x/index.d.ts (0 errors) ====
|
||||
export default class X {
|
||||
private x: number;
|
||||
}
|
||||
|
||||
==== /node_modules/c/node_modules/x/package.json (0 errors) ====
|
||||
{ "name": "x", "version": "1.2.4" }
|
||||
|
||||
52
tests/baselines/reference/duplicatePackage.js
Normal file
52
tests/baselines/reference/duplicatePackage.js
Normal file
@ -0,0 +1,52 @@
|
||||
//// [tests/cases/compiler/duplicatePackage.ts] ////
|
||||
|
||||
//// [index.d.ts]
|
||||
import X from "x";
|
||||
export function a(x: X): void;
|
||||
|
||||
//// [index.d.ts]
|
||||
export default class X {
|
||||
private x: number;
|
||||
}
|
||||
|
||||
//// [package.json]
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
//// [index.d.ts]
|
||||
import X from "x";
|
||||
export const b: X;
|
||||
|
||||
//// [index.d.ts]
|
||||
content not parsed
|
||||
|
||||
//// [package.json]
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
//// [index.d.ts]
|
||||
import X from "x";
|
||||
export const c: X;
|
||||
|
||||
//// [index.d.ts]
|
||||
export default class X {
|
||||
private x: number;
|
||||
}
|
||||
|
||||
//// [package.json]
|
||||
{ "name": "x", "version": "1.2.4" }
|
||||
|
||||
//// [a.ts]
|
||||
import { a } from "a";
|
||||
import { b } from "b";
|
||||
import { c } from "c";
|
||||
a(b); // Works
|
||||
a(c); // Error, these are from different versions of the library.
|
||||
|
||||
|
||||
//// [a.js]
|
||||
"use strict";
|
||||
exports.__esModule = true;
|
||||
var a_1 = require("a");
|
||||
var b_1 = require("b");
|
||||
var c_1 = require("c");
|
||||
a_1.a(b_1.b); // Works
|
||||
a_1.a(c_1.c); // Error, these are from different versions of the library.
|
||||
@ -0,0 +1,27 @@
|
||||
/node_modules/a/node_modules/x/index.d.ts(1,18): error TS1254: A 'const' initializer in an ambient context must be a string or numeric literal.
|
||||
|
||||
|
||||
==== /src/a.ts (0 errors) ====
|
||||
import { x as xa } from "a";
|
||||
import { x as xb } from "b";
|
||||
|
||||
==== /node_modules/a/index.d.ts (0 errors) ====
|
||||
export { x } from "x";
|
||||
|
||||
==== /node_modules/a/node_modules/x/index.d.ts (1 errors) ====
|
||||
export const x = 1 + 1;
|
||||
~~~~~
|
||||
!!! error TS1254: A 'const' initializer in an ambient context must be a string or numeric literal.
|
||||
|
||||
==== /node_modules/a/node_modules/x/package.json (0 errors) ====
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
==== /node_modules/b/index.d.ts (0 errors) ====
|
||||
export { x } from "x";
|
||||
|
||||
==== /node_modules/b/node_modules/x/index.d.ts (0 errors) ====
|
||||
content not parsed
|
||||
|
||||
==== /node_modules/b/node_modules/x/package.json (0 errors) ====
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
28
tests/baselines/reference/duplicatePackage_withErrors.js
Normal file
28
tests/baselines/reference/duplicatePackage_withErrors.js
Normal file
@ -0,0 +1,28 @@
|
||||
//// [tests/cases/compiler/duplicatePackage_withErrors.ts] ////
|
||||
|
||||
//// [index.d.ts]
|
||||
export { x } from "x";
|
||||
|
||||
//// [index.d.ts]
|
||||
export const x = 1 + 1;
|
||||
|
||||
//// [package.json]
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
//// [index.d.ts]
|
||||
export { x } from "x";
|
||||
|
||||
//// [index.d.ts]
|
||||
content not parsed
|
||||
|
||||
//// [package.json]
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
//// [a.ts]
|
||||
import { x as xa } from "a";
|
||||
import { x as xb } from "b";
|
||||
|
||||
|
||||
//// [a.js]
|
||||
"use strict";
|
||||
exports.__esModule = true;
|
||||
42
tests/cases/compiler/duplicatePackage.ts
Normal file
42
tests/cases/compiler/duplicatePackage.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// @noImplicitReferences: true
|
||||
|
||||
// @Filename: /node_modules/a/index.d.ts
|
||||
import X from "x";
|
||||
export function a(x: X): void;
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/index.d.ts
|
||||
export default class X {
|
||||
private x: number;
|
||||
}
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/package.json
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
// @Filename: /node_modules/b/index.d.ts
|
||||
import X from "x";
|
||||
export const b: X;
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/index.d.ts
|
||||
content not parsed
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/package.json
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
// @Filename: /node_modules/c/index.d.ts
|
||||
import X from "x";
|
||||
export const c: X;
|
||||
|
||||
// @Filename: /node_modules/c/node_modules/x/index.d.ts
|
||||
export default class X {
|
||||
private x: number;
|
||||
}
|
||||
|
||||
// @Filename: /node_modules/c/node_modules/x/package.json
|
||||
{ "name": "x", "version": "1.2.4" }
|
||||
|
||||
// @Filename: /src/a.ts
|
||||
import { a } from "a";
|
||||
import { b } from "b";
|
||||
import { c } from "c";
|
||||
a(b); // Works
|
||||
a(c); // Error, these are from different versions of the library.
|
||||
23
tests/cases/compiler/duplicatePackage_withErrors.ts
Normal file
23
tests/cases/compiler/duplicatePackage_withErrors.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// @noImplicitReferences: true
|
||||
|
||||
// @Filename: /node_modules/a/index.d.ts
|
||||
export { x } from "x";
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/index.d.ts
|
||||
export const x = 1 + 1;
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/package.json
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
// @Filename: /node_modules/b/index.d.ts
|
||||
export { x } from "x";
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/index.d.ts
|
||||
content not parsed
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/package.json
|
||||
{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
// @Filename: /src/a.ts
|
||||
import { x as xa } from "a";
|
||||
import { x as xb } from "b";
|
||||
46
tests/cases/fourslash/duplicatePackageServices.ts
Normal file
46
tests/cases/fourslash/duplicatePackageServices.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/// <reference path='fourslash.ts'/>
|
||||
// @noImplicitReferences: true
|
||||
|
||||
// @Filename: /node_modules/a/index.d.ts
|
||||
////import /*useAX*/[|{| "isWriteAccess": true, "isDefinition": true |}X|] from "x";
|
||||
////export function a(x: [|X|]): void;
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/index.d.ts
|
||||
////export default class /*defAX*/[|{| "isWriteAccess": true, "isDefinition": true |}X|] {
|
||||
//// private x: number;
|
||||
////}
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/package.json
|
||||
////{ "name": "x", "version": "1.2.3" }
|
||||
|
||||
// @Filename: /node_modules/b/index.d.ts
|
||||
////import /*useBX*/[|{| "isWriteAccess": true, "isDefinition": true |}X|] from "x";
|
||||
////export const b: [|X|];
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/index.d.ts
|
||||
////export default class /*defBX*/[|{| "isWriteAccess": true, "isDefinition": true |}X|] {
|
||||
//// private x: number;
|
||||
////}
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/package.json
|
||||
////{ "name": "x", "version": "1.2./*bVersionPatch*/3" }
|
||||
|
||||
// @Filename: /src/a.ts
|
||||
////import { a } from "a";
|
||||
////import { b } from "b";
|
||||
////a(/*error*/b);
|
||||
|
||||
goTo.file("/src/a.ts");
|
||||
verify.numberOfErrorsInCurrentFile(0);
|
||||
verify.goToDefinition("useAX", "defAX");
|
||||
verify.goToDefinition("useBX", "defAX");
|
||||
|
||||
const [r0, r1, r2, r3, r4, r5] = test.ranges();
|
||||
const aImport = { definition: "import X", ranges: [r0, r1] };
|
||||
const def = { definition: "class X", ranges: [r2] };
|
||||
const bImport = { definition: "import X", ranges: [r3, r4] };
|
||||
verify.referenceGroups([r0, r1], [aImport, def, bImport]);
|
||||
verify.referenceGroups([r2], [def, aImport, bImport]);
|
||||
verify.referenceGroups([r3, r4], [bImport, def, aImport]);
|
||||
|
||||
verify.referenceGroups(r5, [def, aImport, bImport]);
|
||||
@ -0,0 +1,57 @@
|
||||
/// <reference path='fourslash.ts'/>
|
||||
// @noImplicitReferences: true
|
||||
|
||||
// @Filename: /node_modules/a/index.d.ts
|
||||
////import X from "x";
|
||||
////export function a(x: X): void;
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/index.d.ts
|
||||
////export default class /*defAX*/X {
|
||||
//// private x: number;
|
||||
////}
|
||||
|
||||
// @Filename: /node_modules/a/node_modules/x/package.json
|
||||
////{ "name": "x", "version": "1.2./*aVersionPatch*/3" }
|
||||
|
||||
// @Filename: /node_modules/b/index.d.ts
|
||||
////import X from "x";
|
||||
////export const b: X;
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/index.d.ts
|
||||
////export default class /*defBX*/X {
|
||||
//// private x: number;
|
||||
////}
|
||||
|
||||
// @Filename: /node_modules/b/node_modules/x/package.json
|
||||
////{ "name": "x", "version": "1.2./*bVersionPatch*/3" }
|
||||
|
||||
// @Filename: /src/a.ts
|
||||
////import { a } from "a";
|
||||
////import { b } from "b";
|
||||
////a(/*error*/b);
|
||||
|
||||
goTo.file("/src/a.ts");
|
||||
verify.numberOfErrorsInCurrentFile(0);
|
||||
|
||||
testChangeAndChangeBack("aVersionPatch", "defAX");
|
||||
testChangeAndChangeBack("bVersionPatch", "defBX");
|
||||
|
||||
function testChangeAndChangeBack(versionPatch: string, def: string) {
|
||||
goTo.marker(versionPatch);
|
||||
edit.insert("4");
|
||||
goTo.marker(def);
|
||||
edit.insert(" ");
|
||||
|
||||
// No longer have identical packageId, so we get errors.
|
||||
verify.errorExistsAfterMarker("error");
|
||||
|
||||
// Undo the change.
|
||||
goTo.marker(versionPatch);
|
||||
edit.deleteAtCaret();
|
||||
goTo.marker(def);
|
||||
edit.deleteAtCaret();
|
||||
|
||||
// Back to being identical.
|
||||
goTo.file("/src/a.ts");
|
||||
verify.numberOfErrorsInCurrentFile(0);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user