diff --git a/src/compiler/core.ts b/src/compiler/core.ts index d5c8396d06d6b..8ea54c216175e 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2208,20 +2208,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.Dts, Extension.Ts, Extension.Tsx]; export const supportedJavascriptExtensions: ReadonlyArray = [Extension.Js, Extension.Jsx]; - const allSupportedExtensions = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions]; + const allSupportedExtensions: ReadonlyArray = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions]; export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: ReadonlyArray): ReadonlyArray { 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) { @@ -2588,6 +2582,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(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e)); } diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index be867db974363..2b3974e5287f9 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -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 { + 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, 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, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined { + function loadModuleFromFile(extensions: Extensions, candidate: string, failedLookupLocations: Push, 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, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined { + function tryAddingExtensions(candidate: string, extensions: Extensions, failedLookupLocations: Push, 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, 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, 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, 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, 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 { - 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)); } } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 64e8eb0c80392..305058285f999 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -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(); + // Maps from a SourceFile's `.path` to the name of the package it was imported with. + let sourceFileToPackageName = createMap(); + // See `sourceFileIsRedirectedTo`. + let redirectTargetsSet = createMap(); + const filesByName = createMap(); // 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(); + + 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) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 27dcbda309ffd..76c0b640f4570 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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; + /** Set of all source files that some other source file redirects to. */ + /* @internal */ redirectTargetsSet: Map; } /* @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 { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 76d242e8b08d8..dd8b332d2d4a7 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -116,7 +116,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; } /* @internal */ diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index c6ec0f257b606..5052363c1baa7 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -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); diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index af5a998df99b2..b67468206fbfd 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -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; diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index de5f5a756d866..5062f4c4b39ee 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -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 = oldProgram.getSourceFile(t.name); + let oldFile = 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, 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 { /// `; 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", () => { diff --git a/tests/baselines/reference/duplicatePackage.errors.txt b/tests/baselines/reference/duplicatePackage.errors.txt new file mode 100644 index 0000000000000..917ed711c640d --- /dev/null +++ b/tests/baselines/reference/duplicatePackage.errors.txt @@ -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" } + \ No newline at end of file diff --git a/tests/baselines/reference/duplicatePackage.js b/tests/baselines/reference/duplicatePackage.js new file mode 100644 index 0000000000000..ada5c900b9371 --- /dev/null +++ b/tests/baselines/reference/duplicatePackage.js @@ -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. diff --git a/tests/baselines/reference/duplicatePackage_withErrors.errors.txt b/tests/baselines/reference/duplicatePackage_withErrors.errors.txt new file mode 100644 index 0000000000000..ad24637e98331 --- /dev/null +++ b/tests/baselines/reference/duplicatePackage_withErrors.errors.txt @@ -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" } + \ No newline at end of file diff --git a/tests/baselines/reference/duplicatePackage_withErrors.js b/tests/baselines/reference/duplicatePackage_withErrors.js new file mode 100644 index 0000000000000..a7fdafd522f5e --- /dev/null +++ b/tests/baselines/reference/duplicatePackage_withErrors.js @@ -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; diff --git a/tests/cases/compiler/duplicatePackage.ts b/tests/cases/compiler/duplicatePackage.ts new file mode 100644 index 0000000000000..31df2d2450883 --- /dev/null +++ b/tests/cases/compiler/duplicatePackage.ts @@ -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. diff --git a/tests/cases/compiler/duplicatePackage_withErrors.ts b/tests/cases/compiler/duplicatePackage_withErrors.ts new file mode 100644 index 0000000000000..266c7239971d7 --- /dev/null +++ b/tests/cases/compiler/duplicatePackage_withErrors.ts @@ -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"; diff --git a/tests/cases/fourslash/duplicatePackageServices.ts b/tests/cases/fourslash/duplicatePackageServices.ts new file mode 100644 index 0000000000000..360611ad1403b --- /dev/null +++ b/tests/cases/fourslash/duplicatePackageServices.ts @@ -0,0 +1,46 @@ +/// +// @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]); diff --git a/tests/cases/fourslash/duplicatePackageServices_fileChanges.ts b/tests/cases/fourslash/duplicatePackageServices_fileChanges.ts new file mode 100644 index 0000000000000..203277d7ad467 --- /dev/null +++ b/tests/cases/fourslash/duplicatePackageServices_fileChanges.ts @@ -0,0 +1,57 @@ +/// +// @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); +}