diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 36521a14ea..7451eb678b 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -425,7 +425,7 @@ namespace ts { const options = program.getCompilerOptions(); forEach(program.getSourceFiles(), f => program.isSourceFileDefaultLibrary(f) && - !skipTypeChecking(f, options) && + !skipTypeChecking(f, options, program) && removeSemanticDiagnosticsOf(state, f.path) ); } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2a3f2390cf..59b75691a0 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -572,7 +572,7 @@ namespace ts { return node && getTypeArgumentConstraint(node); }, getSuggestionDiagnostics: (file, ct) => { - if (skipTypeChecking(file, compilerOptions)) { + if (skipTypeChecking(file, compilerOptions, host)) { return emptyArray; } @@ -31048,7 +31048,7 @@ namespace ts { function checkSourceFileWorker(node: SourceFile) { const links = getNodeLinks(node); if (!(links.flags & NodeCheckFlags.TypeChecked)) { - if (skipTypeChecking(node, compilerOptions)) { + if (skipTypeChecking(node, compilerOptions, host)) { return; } diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 009c04fbb8..fd0d428d54 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -772,6 +772,12 @@ namespace ts { category: Diagnostics.Advanced_Options, description: Diagnostics.Disable_size_limitations_on_JavaScript_projects }, + { + name: "disableSourceOfProjectReferenceRedirect", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Disable_use_of_source_files_instead_of_declaration_files_from_referenced_projects + }, { name: "noImplicitUseStrict", type: "boolean", diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3b219b37de..7ae7588bb1 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4036,6 +4036,10 @@ "category": "Message", "code": 6220 }, + "Disable use of source files instead of declaration files from referenced projects.": { + "category": "Message", + "code": 6221 + }, "Projects to reference": { "category": "Message", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 23a3e660c8..e7c51ba84c 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -817,6 +817,8 @@ namespace ts { let resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined; let projectReferenceRedirects: Map | undefined; let mapFromFileToProjectReferenceRedirects: Map | undefined; + let mapFromToProjectReferenceRedirectSource: Map | undefined; + const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect && host.useSourceOfProjectReferenceRedirect(); const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); // We set `structuralIsReused` to `undefined` because `tryReuseStructureFromOldProgram` calls `tryReuseStructureFromOldProgram` which checks @@ -831,17 +833,32 @@ namespace ts { if (!resolvedProjectReferences) { resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); } + if (host.setResolvedProjectReferenceCallbacks) { + host.setResolvedProjectReferenceCallbacks({ + getSourceOfProjectReferenceRedirect, + forEachResolvedProjectReference + }); + } if (rootNames.length) { for (const parsedRef of resolvedProjectReferences) { if (!parsedRef) continue; const out = parsedRef.commandLine.options.outFile || parsedRef.commandLine.options.out; - if (out) { - processSourceFile(changeExtension(out, ".d.ts"), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + if (useSourceOfProjectReferenceRedirect) { + if (out || getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { + for (const fileName of parsedRef.commandLine.fileNames) { + processSourceFile(fileName, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + } } - else if (getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { - for (const fileName of parsedRef.commandLine.fileNames) { - if (!fileExtensionIs(fileName, Extension.Dts) && hasTSFileExtension(fileName)) { - processSourceFile(getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames()), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + else { + if (out) { + processSourceFile(changeExtension(out, ".d.ts"), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + else if (getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { + for (const fileName of parsedRef.commandLine.fileNames) { + if (!fileExtensionIs(fileName, Extension.Dts) && hasTSFileExtension(fileName)) { + processSourceFile(getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames()), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } } } } @@ -955,6 +972,7 @@ namespace ts { getResolvedProjectReferenceToRedirect, getResolvedProjectReferenceByPath, forEachResolvedProjectReference, + isSourceOfProjectReferenceRedirect, emitBuildInfo }; @@ -987,9 +1005,15 @@ namespace ts { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } + function isValidSourceFileForEmit(file: SourceFile) { + // source file is allowed to be emitted and its not source of project reference redirect + return sourceFileMayBeEmitted(file, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect) && + !isSourceOfProjectReferenceRedirect(file.fileName); + } + function getCommonSourceDirectory() { if (commonSourceDirectory === undefined) { - const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect)); + const emittedFiles = filter(files, file => isValidSourceFileForEmit(file)); if (options.rootDir && checkSourceFilesBelongToPath(emittedFiles, options.rootDir)) { // If a rootDir is specified use it as the commonSourceDirectory commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); @@ -1220,6 +1244,12 @@ namespace ts { } if (projectReferences) { resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + if (host.setResolvedProjectReferenceCallbacks) { + host.setResolvedProjectReferenceCallbacks({ + getSourceOfProjectReferenceRedirect, + forEachResolvedProjectReference + }); + } } // check if program source files has changed in the way that can affect structure of the program @@ -1403,6 +1433,13 @@ namespace ts { for (const newSourceFile of newSourceFiles) { const filePath = newSourceFile.path; addFileToFilesByName(newSourceFile, filePath, newSourceFile.resolvedPath); + if (useSourceOfProjectReferenceRedirect) { + const redirectProject = getProjectReferenceRedirectProject(newSourceFile.fileName); + if (redirectProject && !(redirectProject.commandLine.options.outFile || redirectProject.commandLine.options.out)) { + const redirect = getProjectReferenceOutputName(redirectProject, newSourceFile.fileName); + addFileToFilesByName(newSourceFile, toPath(redirect), /*redirectedPath*/ undefined); + } + } // Set the file as found during node modules search if it was found that way in old progra, if (oldProgram.isSourceFileFromExternalLibrary(oldProgram.getSourceFileByPath(newSourceFile.resolvedPath)!)) { sourceFilesFoundSearchingNodeModules.set(filePath, true); @@ -1682,7 +1719,7 @@ namespace ts { function getSemanticDiagnosticsForFileNoCache(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] | undefined { return runWithCancellationToken(() => { - if (skipTypeChecking(sourceFile, options)) { + if (skipTypeChecking(sourceFile, options, program)) { return emptyArray; } @@ -2234,6 +2271,16 @@ namespace ts { // Get source file from normalized fileName function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, refFile: RefFile | undefined, packageId: PackageId | undefined): SourceFile | undefined { + if (useSourceOfProjectReferenceRedirect) { + const source = getSourceOfProjectReferenceRedirect(fileName); + if (source) { + const file = isString(source) ? + findSourceFile(source, toPath(source), isDefaultLib, ignoreNoDefaultLib, refFile, packageId) : + undefined; + if (file) addFileToFilesByName(file, path, /*redirectedPath*/ undefined); + return file; + } + } const originalFileName = fileName; if (filesByName.has(path)) { const file = filesByName.get(path); @@ -2282,7 +2329,7 @@ namespace ts { } let redirectedPath: Path | undefined; - if (refFile) { + if (refFile && !useSourceOfProjectReferenceRedirect) { const redirectProject = getProjectReferenceRedirectProject(fileName); if (redirectProject) { if (redirectProject.commandLine.options.outFile || redirectProject.commandLine.options.out) { @@ -2451,6 +2498,36 @@ namespace ts { }); } + function getSourceOfProjectReferenceRedirect(file: string) { + if (!isDeclarationFileName(file)) return undefined; + if (mapFromToProjectReferenceRedirectSource === undefined) { + mapFromToProjectReferenceRedirectSource = createMap(); + forEachResolvedProjectReference(resolvedRef => { + if (resolvedRef) { + const out = resolvedRef.commandLine.options.outFile || resolvedRef.commandLine.options.out; + if (out) { + // Dont know which source file it means so return true? + const outputDts = changeExtension(out, Extension.Dts); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); + } + else { + forEach(resolvedRef.commandLine.fileNames, fileName => { + if (!fileExtensionIs(fileName, Extension.Dts) && hasTSFileExtension(fileName)) { + const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, host.useCaseSensitiveFileNames()); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); + } + }); + } + } + }); + } + return mapFromToProjectReferenceRedirectSource.get(toPath(file)); + } + + function isSourceOfProjectReferenceRedirect(fileName: string) { + return useSourceOfProjectReferenceRedirect && !!getResolvedProjectReferenceToRedirect(fileName); + } + function forEachProjectReference( projectReferences: readonly ProjectReference[] | undefined, resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, @@ -2858,8 +2935,7 @@ namespace ts { const rootPaths = arrayToSet(rootNames, toPath); for (const file of files) { // Ignore file that is not emitted - if (!sourceFileMayBeEmitted(file, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect)) continue; - if (!rootPaths.has(file.path)) { + if (isValidSourceFileForEmit(file) && !rootPaths.has(file.path)) { addProgramDiagnosticAtRefPath( file, rootPaths, diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 2d6286a379..82e566184b 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -522,6 +522,33 @@ namespace ts { } } + function recursiveCreateDirectory(directoryPath: string, sys: System) { + const basePath = getDirectoryPath(directoryPath); + const shouldCreateParent = basePath !== "" && directoryPath !== basePath && !sys.directoryExists(basePath); + if (shouldCreateParent) { + recursiveCreateDirectory(basePath, sys); + } + if (shouldCreateParent || !sys.directoryExists(directoryPath)) { + sys.createDirectory(directoryPath); + } + } + + /** + * patch writefile to create folder before writing the file + */ + /*@internal*/ + export function patchWriteFileEnsuringDirectory(sys: System) { + // patch writefile to create folder before writing the file + const originalWriteFile = sys.writeFile; + sys.writeFile = (path, data, writeBom) => { + const directoryPath = getDirectoryPath(normalizeSlashes(path)); + if (directoryPath && !sys.directoryExists(directoryPath)) { + recursiveCreateDirectory(directoryPath, sys); + } + originalWriteFile.call(sys, path, data, writeBom); + }; + } + /*@internal*/ export type BufferEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"; @@ -1365,17 +1392,6 @@ namespace ts { }; } - function recursiveCreateDirectory(directoryPath: string, sys: System) { - const basePath = getDirectoryPath(directoryPath); - const shouldCreateParent = basePath !== "" && directoryPath !== basePath && !sys.directoryExists(basePath); - if (shouldCreateParent) { - recursiveCreateDirectory(basePath, sys); - } - if (shouldCreateParent || !sys.directoryExists(directoryPath)) { - sys.createDirectory(directoryPath); - } - } - let sys: System | undefined; if (typeof ChakraHost !== "undefined") { sys = getChakraSystem(); @@ -1387,14 +1403,7 @@ namespace ts { } if (sys) { // patch writefile to create folder before writing the file - const originalWriteFile = sys.writeFile; - sys.writeFile = (path, data, writeBom) => { - const directoryPath = getDirectoryPath(normalizeSlashes(path)); - if (directoryPath && !sys!.directoryExists(directoryPath)) { - recursiveCreateDirectory(directoryPath, sys!); - } - originalWriteFile.call(sys, path, data, writeBom); - }; + patchWriteFileEnsuringDirectory(sys); } return sys!; })(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b5baf7cf27..def1074ee1 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3065,6 +3065,7 @@ namespace ts { /*@internal*/ getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined; /*@internal*/ forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined; /*@internal*/ getResolvedProjectReferenceByPath(projectReferencePath: Path): ResolvedProjectReference | undefined; + /*@internal*/ isSourceOfProjectReferenceRedirect(fileName: string): boolean; /*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined; /*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult; } @@ -3165,6 +3166,7 @@ namespace ts { getSourceFile(fileName: string): SourceFile | undefined; getResolvedTypeReferenceDirectives(): ReadonlyMap; getProjectReferenceRedirect(fileName: string): string | undefined; + isSourceOfProjectReferenceRedirect(fileName: string): boolean; readonly redirectTargetsMap: RedirectTargetsMap; } @@ -4775,6 +4777,7 @@ namespace ts { /* @internal */ diagnostics?: boolean; /* @internal */ extendedDiagnostics?: boolean; disableSizeLimit?: boolean; + disableSourceOfProjectReferenceRedirect?: boolean; downlevelIteration?: boolean; emitBOM?: boolean; emitDecoratorMetadata?: boolean; @@ -5303,11 +5306,23 @@ namespace ts { /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; + /* @internal */ setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void; + /* @internal */ useSourceOfProjectReferenceRedirect?(): boolean; // TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesn't use compilerHost as base /*@internal*/createDirectory?(directory: string): void; } + /** true if --out otherwise source file name */ + /*@internal*/ + export type SourceOfProjectReferenceRedirect = string | true; + + /*@internal*/ + export interface ResolvedProjectReferenceCallbacks { + getSourceOfProjectReferenceRedirect(fileName: string): SourceOfProjectReferenceRedirect | undefined; + forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined; + } + /* @internal */ export const enum TransformFlags { None = 0, diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 1645b9339a..6b48cb8062 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8713,11 +8713,16 @@ namespace ts { return { pos: typeParameters.pos - 1, end: typeParameters.end + 1 }; } - export function skipTypeChecking(sourceFile: SourceFile, options: CompilerOptions) { + export interface HostWithIsSourceOfProjectReferenceRedirect { + isSourceOfProjectReferenceRedirect(fileName: string): boolean; + } + export function skipTypeChecking(sourceFile: SourceFile, options: CompilerOptions, host: HostWithIsSourceOfProjectReferenceRedirect) { // If skipLibCheck is enabled, skip reporting errors if file is a declaration file. // If skipDefaultLibCheck is enabled, skip reporting errors if file contains a // '/// ' directive. - return options.skipLibCheck && sourceFile.isDeclarationFile || options.skipDefaultLibCheck && sourceFile.hasNoDefaultLib; + return (options.skipLibCheck && sourceFile.isDeclarationFile || + options.skipDefaultLibCheck && sourceFile.hasNoDefaultLib) || + host.isSourceOfProjectReferenceRedirect(sourceFile.fileName); } export function isJsonEqual(a: unknown, b: unknown): boolean { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 1a2fd5f4cc..0417ad55c9 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -44,7 +44,10 @@ interface Array { length: number; [n: number]: T; }` } export function createServerHost(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost { - return new TestServerHost(/*withSafelist*/ true, fileOrFolderList, params); + const host = new TestServerHost(/*withSafelist*/ true, fileOrFolderList, params); + // Just like sys, patch the host to use writeFile + patchWriteFileEnsuringDirectory(host); + return host; } export interface File { @@ -174,8 +177,8 @@ interface Array { length: number; [n: number]: T; }` } } - export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { - checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); + export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[], additionalInfo?: string) { + checkMapKeys(`watchedFiles:: ${additionalInfo || ""}::`, host.watchedFiles, expectedFiles); } export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyMap): void; @@ -1016,6 +1019,19 @@ interface Array { length: number; [n: number]: T; }` } } + export type TestServerHostTrackingWrittenFiles = TestServerHost & { writtenFiles: Map; }; + + export function changeToHostTrackingWrittenFiles(inputHost: TestServerHost) { + const host = inputHost as TestServerHostTrackingWrittenFiles; + const originalWriteFile = host.writeFile; + host.writtenFiles = createMap(); + host.writeFile = (fileName, content) => { + originalWriteFile.call(host, fileName, content); + const path = host.toFullPath(fileName); + host.writtenFiles.set(path, true); + }; + return host; + } export const tsbuildProjectsLocation = "/user/username/projects"; export function getTsBuildProjectFilePath(project: string, file: string) { return `${tsbuildProjectsLocation}/${project}/${file}`; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 37b4ddc47a..759eec5d54 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1777,6 +1777,12 @@ namespace ts.server { configFileErrors.push(...parsedCommandLine.errors); } + this.logger.info(`Config: ${configFilename} : ${JSON.stringify({ + rootNames: parsedCommandLine.fileNames, + options: parsedCommandLine.options, + projectReferences: parsedCommandLine.projectReferences + }, /*replacer*/ undefined, " ")}`); + Debug.assert(!!parsedCommandLine.fileNames); const compilerOptions = parsedCommandLine.options; @@ -1818,7 +1824,7 @@ namespace ts.server { let scriptInfo: ScriptInfo | NormalizedPath; let path: Path; // Use the project's fileExists so that it can use caching instead of reaching to disk for the query - if (!isDynamic && !project.fileExists(newRootFile)) { + if (!isDynamic && !project.fileExistsWithCache(newRootFile)) { path = normalizedPathToPath(normalizedPath, this.currentDirectory, this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path)!; if (isScriptInfo(existingValue)) { @@ -1851,7 +1857,7 @@ namespace ts.server { projectRootFilesMap.forEach((value, path) => { if (!newRootScriptInfoMap.has(path)) { if (isScriptInfo(value)) { - project.removeFile(value, project.fileExists(path), /*detachFromProject*/ true); + project.removeFile(value, project.fileExistsWithCache(path), /*detachFromProject*/ true); } else { projectRootFilesMap.delete(path); @@ -2584,7 +2590,9 @@ namespace ts.server { /*@internal*/ getOriginalLocationEnsuringConfiguredProject(project: Project, location: DocumentPosition): DocumentPosition | undefined { - const originalLocation = project.getSourceMapper().tryGetSourcePosition(location); + const originalLocation = project.isSourceOfProjectReferenceRedirect(location.fileName) ? + location : + project.getSourceMapper().tryGetSourcePosition(location); if (!originalLocation) return undefined; const { fileName } = originalLocation; @@ -2595,7 +2603,8 @@ namespace ts.server { if (!configFileName) return undefined; const configuredProject = this.findConfiguredProjectByProjectName(configFileName) || - this.createAndLoadConfiguredProject(configFileName, `Creating project for original file: ${originalFileInfo.fileName} for location: ${location.fileName}`); + this.createAndLoadConfiguredProject(configFileName, `Creating project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}`); + if (configuredProject === project) return originalLocation; updateProjectIfDirty(configuredProject); // Keep this configured project as referenced from project addOriginalConfiguredProject(configuredProject); diff --git a/src/server/project.ts b/src/server/project.ts index 4b38a099b2..418532bdc4 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -196,6 +196,11 @@ namespace ts.server { /*@internal*/ originalConfiguredProjects: Map | undefined; + /*@internal*/ + getResolvedProjectReferenceToRedirect(_fileName: string): ResolvedProjectReference | undefined { + return undefined; + } + private readonly cancellationToken: ThrottledCancellationToken; public isNonTsProject() { @@ -391,6 +396,11 @@ namespace ts.server { } fileExists(file: string): boolean { + return this.fileExistsWithCache(file); + } + + /* @internal */ + fileExistsWithCache(file: string): boolean { // As an optimization, don't hit the disks for files we already know don't exist // (because we're watching for their creation). const path = this.toPath(file); @@ -527,8 +537,11 @@ namespace ts.server { return this.projectService.getSourceFileLike(fileName, this); } - private shouldEmitFile(scriptInfo: ScriptInfo) { - return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent(); + /*@internal*/ + shouldEmitFile(scriptInfo: ScriptInfo | undefined) { + return scriptInfo && + !scriptInfo.isDynamicOrHasMixedContent() && + !this.program!.isSourceOfProjectReferenceRedirect(scriptInfo.path); } getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { @@ -538,7 +551,7 @@ namespace ts.server { updateProjectIfDirty(this); this.builderState = BuilderState.create(this.program!, this.projectService.toCanonicalFileName, this.builderState); return mapDefined(BuilderState.getFilesAffectedBy(this.builderState, this.program!, scriptInfo.path, this.cancellationToken, data => this.projectService.host.createHash!(data)), // TODO: GH#18217 - sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)!) ? sourceFile.fileName : undefined); + sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } /** @@ -1223,6 +1236,11 @@ namespace ts.server { this.rootFilesMap.delete(info.path); } + /*@internal*/ + isSourceOfProjectReferenceRedirect(fileName: string) { + return !!this.program && this.program.isSourceOfProjectReferenceRedirect(fileName); + } + protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; @@ -1475,6 +1493,8 @@ namespace ts.server { configFileWatcher: FileWatcher | undefined; private directoriesWatchedForWildcards: Map | undefined; readonly canonicalConfigFilePath: NormalizedPath; + private projectReferenceCallbacks: ResolvedProjectReferenceCallbacks | undefined; + private mapOfDeclarationDirectories: Map | undefined; /* @internal */ pendingReload: ConfigFileProgramReloadLevel | undefined; @@ -1520,6 +1540,63 @@ namespace ts.server { this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); } + /* @internal */ + setResolvedProjectReferenceCallbacks(projectReferenceCallbacks: ResolvedProjectReferenceCallbacks) { + this.projectReferenceCallbacks = projectReferenceCallbacks; + } + + /* @internal */ + useSourceOfProjectReferenceRedirect = () => !!this.languageServiceEnabled && + !this.getCompilerOptions().disableSourceOfProjectReferenceRedirect; + + /** + * This implementation of fileExists checks if the file being requested is + * .d.ts file for the referenced Project. + * If it is it returns true irrespective of whether that file exists on host + */ + fileExists(file: string): boolean { + // Project references go to source file instead of .d.ts file + if (this.useSourceOfProjectReferenceRedirect() && this.projectReferenceCallbacks) { + const source = this.projectReferenceCallbacks.getSourceOfProjectReferenceRedirect(file); + if (source) return isString(source) ? super.fileExists(source) : true; + } + return super.fileExists(file); + } + + /** + * This implementation of directoryExists checks if the directory being requested is + * directory of .d.ts file for the referenced Project. + * If it is it returns true irrespective of whether that directory exists on host + */ + directoryExists(path: string): boolean { + if (super.directoryExists(path)) return true; + if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false; + + if (!this.mapOfDeclarationDirectories) { + this.mapOfDeclarationDirectories = createMap(); + this.projectReferenceCallbacks.forEachResolvedProjectReference(ref => { + if (!ref) return; + const out = ref.commandLine.options.outFile || ref.commandLine.options.outDir; + if (out) { + this.mapOfDeclarationDirectories!.set(getDirectoryPath(this.toPath(out)), true); + } + else { + // Set declaration's in different locations only, if they are next to source the directory present doesnt change + const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir; + if (declarationDir) { + this.mapOfDeclarationDirectories!.set(this.toPath(declarationDir), true); + } + } + }); + } + const dirPath = this.toPath(path); + const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`; + return !!forEachKey( + this.mapOfDeclarationDirectories, + declDirPath => dirPath === declDirPath || startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) + ); + } + /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise. @@ -1528,6 +1605,8 @@ namespace ts.server { this.isInitialLoadPending = returnFalse; const reloadLevel = this.pendingReload; this.pendingReload = ConfigFileProgramReloadLevel.None; + this.projectReferenceCallbacks = undefined; + this.mapOfDeclarationDirectories = undefined; let result: boolean; switch (reloadLevel) { case ConfigFileProgramReloadLevel.Partial: @@ -1570,6 +1649,12 @@ namespace ts.server { return program && program.forEachResolvedProjectReference(cb); } + /*@internal*/ + getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined { + const program = this.getCurrentProgram(); + return program && program.getResolvedProjectReferenceToRedirect(fileName); + } + /*@internal*/ enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; @@ -1652,6 +1737,8 @@ namespace ts.server { this.stopWatchingWildCards(); this.projectErrors = undefined; this.configFileSpecs = undefined; + this.projectReferenceCallbacks = undefined; + this.mapOfDeclarationDirectories = undefined; super.close(); } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index e441ca54d3..cbd006dd9d 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -495,15 +495,17 @@ namespace ts.server { // the default project; if no configured projects, the first external project should // be the default project; otherwise the first inferred project should be the default. let firstExternalProject; + let firstConfiguredProject; for (const project of this.containingProjects) { if (project.projectKind === ProjectKind.Configured) { - return project; + if (!project.isSourceOfProjectReferenceRedirect(this.fileName)) return project; + if (!firstConfiguredProject) firstConfiguredProject = project; } else if (project.projectKind === ProjectKind.External && !firstExternalProject) { firstExternalProject = project; } } - return firstExternalProject || this.containingProjects[0]; + return firstConfiguredProject || firstExternalProject || this.containingProjects[0]; } } diff --git a/src/server/session.ts b/src/server/session.ts index f2996b98dd..6fe392c722 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -448,7 +448,9 @@ namespace ts.server { function getDefinitionInProject(definition: DocumentPosition | undefined, definingProject: Project, project: Project): DocumentPosition | undefined { if (!definition || project.containsFile(toNormalizedPath(definition.fileName))) return definition; - const mappedDefinition = definingProject.getLanguageService().getSourceMapper().tryGetGeneratedPosition(definition); + const mappedDefinition = definingProject.isSourceOfProjectReferenceRedirect(definition.fileName) ? + definition : + definingProject.getLanguageService().getSourceMapper().tryGetGeneratedPosition(definition); return mappedDefinition && project.containsFile(toNormalizedPath(mappedDefinition.fileName)) ? mappedDefinition : undefined; } @@ -477,7 +479,7 @@ namespace ts.server { for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: originalLocation as TLocation }, toDo!, seenProjects); }); } - return originalLocation; + return originalLocation === location ? undefined : originalLocation; }); return toDo; } @@ -1037,7 +1039,9 @@ namespace ts.server { private getEmitOutput(args: protocol.FileRequestArgs): EmitOutput { const { file, project } = this.getFileAndProject(args); - return project.getLanguageService().getEmitOutput(file); + return project.shouldEmitFile(project.getScriptInfo(file)) ? + project.getLanguageService().getEmitOutput(file) : + { emitSkipped: true, outputFiles: [] }; } private mapDefinitionInfo(definitions: readonly DefinitionInfo[], project: Project): readonly protocol.FileSpanWithContext[] { @@ -1672,10 +1676,10 @@ namespace ts.server { } } - private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] { + private createCheckList(fileNames: string[]): PendingErrorCheck[] { return mapDefined(fileNames, uncheckedFileName => { const fileName = toNormalizedPath(uncheckedFileName); - const project = defaultProject || this.projectService.tryGetDefaultProjectForFile(fileName); + const project = this.projectService.tryGetDefaultProjectForFile(fileName); return project && { fileName, project }; }); } diff --git a/src/services/services.ts b/src/services/services.ts index 644629ce2a..fb5c35d8f5 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1149,10 +1149,10 @@ namespace ts { useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, getCurrentDirectory: () => currentDirectory, getProgram, - fileExists: host.fileExists && (f => host.fileExists!(f)), - readFile: host.readFile && ((f, encoding) => host.readFile!(f, encoding)), - getDocumentPositionMapper: host.getDocumentPositionMapper && ((generatedFileName, sourceFileName) => host.getDocumentPositionMapper!(generatedFileName, sourceFileName)), - getSourceFileLike: host.getSourceFileLike && (f => host.getSourceFileLike!(f)), + fileExists: maybeBind(host, host.fileExists), + readFile: maybeBind(host, host.readFile), + getDocumentPositionMapper: maybeBind(host, host.getDocumentPositionMapper), + getSourceFileLike: maybeBind(host, host.getSourceFileLike), log }); @@ -1250,6 +1250,12 @@ namespace ts { if (host.resolveTypeReferenceDirectives) { compilerHost.resolveTypeReferenceDirectives = (...args) => host.resolveTypeReferenceDirectives!(...args); } + if (host.setResolvedProjectReferenceCallbacks) { + compilerHost.setResolvedProjectReferenceCallbacks = callbacks => host.setResolvedProjectReferenceCallbacks!(callbacks); + } + if (host.useSourceOfProjectReferenceRedirect) { + compilerHost.useSourceOfProjectReferenceRedirect = () => host.useSourceOfProjectReferenceRedirect!(); + } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); const options: CreateProgramOptions = { diff --git a/src/services/sourcemaps.ts b/src/services/sourcemaps.ts index d07c21a9f4..6ab656b423 100644 --- a/src/services/sourcemaps.ts +++ b/src/services/sourcemaps.ts @@ -70,6 +70,11 @@ namespace ts { if (!sourceFile) return undefined; const program = host.getProgram()!; + // If this is source file of project reference source (instead of redirect) there is no generated position + if (program.isSourceOfProjectReferenceRedirect(sourceFile.fileName)) { + return undefined; + } + const options = program.getCompilerOptions(); const outPath = options.outFile || options.out; diff --git a/src/services/types.ts b/src/services/types.ts index c44370febe..d77007f045 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -234,6 +234,10 @@ namespace ts { getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; /* @internal */ getSourceFileLike?(fileName: string): SourceFileLike | undefined; + /* @internal */ + setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void; + /* @internal */ + useSourceOfProjectReferenceRedirect?(): boolean; } /* @internal */ diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index db9e96e1ad..6d0c83fa95 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -145,6 +145,8 @@ "unittests/tsserver/occurences.ts", "unittests/tsserver/openFile.ts", "unittests/tsserver/projectErrors.ts", + "unittests/tsserver/projectReferenceCompileOnSave.ts", + "unittests/tsserver/projectReferenceErrors.ts", "unittests/tsserver/projectReferences.ts", "unittests/tsserver/projects.ts", "unittests/tsserver/refactors.ts", diff --git a/src/testRunner/unittests/tsbuild/watchMode.ts b/src/testRunner/unittests/tsbuild/watchMode.ts index bc5cd79f7e..b2f2eac158 100644 --- a/src/testRunner/unittests/tsbuild/watchMode.ts +++ b/src/testRunner/unittests/tsbuild/watchMode.ts @@ -2,18 +2,12 @@ namespace ts.tscWatch { import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation; import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath; import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile; - type TsBuildWatchSystem = WatchedSystem & { writtenFiles: Map; }; + type TsBuildWatchSystem = TestFSWithWatch.TestServerHostTrackingWrittenFiles; function createTsBuildWatchSystem(fileOrFolderList: readonly TestFSWithWatch.FileOrFolderOrSymLink[], params?: TestFSWithWatch.TestServerHostCreationParameters) { - const host = createWatchedSystem(fileOrFolderList, params) as TsBuildWatchSystem; - const originalWriteFile = host.writeFile; - host.writtenFiles = createMap(); - host.writeFile = (fileName, content) => { - originalWriteFile.call(host, fileName, content); - const path = host.toFullPath(fileName); - host.writtenFiles.set(path, true); - }; - return host; + return TestFSWithWatch.changeToHostTrackingWrittenFiles( + createWatchedSystem(fileOrFolderList, params) + ); } export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], defaultOptions?: BuildOptions) { @@ -710,8 +704,8 @@ let x: string = 10;`); const coreIndexDts = projectFileName(SubProject.core, "index.d.ts"); const coreAnotherModuleDts = projectFileName(SubProject.core, "anotherModule.d.ts"); const logicIndexDts = projectFileName(SubProject.logic, "index.d.ts"); - const expectedWatchedFiles = () => [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase())); const expectedWatchedDirectoriesRecursive = projectSystem.getTypeRootsFromLocation(projectPath(SubProject.tests)); + const expectedProjectFiles = () => [libFile, ...tests, ...logic.slice(1), ...core.slice(1, core.length - 1)].map(f => f.path); const expectedProgramFiles = () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]; function createSolutionAndWatchMode() { @@ -723,12 +717,19 @@ let x: string = 10;`); } function verifyWatches(host: TsBuildWatchSystem, withTsserver?: boolean) { - verifyWatchesOfProject(host, withTsserver ? expectedWatchedFiles().filter(f => f !== tests[1].path.toLowerCase()) : expectedWatchedFiles(), expectedWatchedDirectoriesRecursive); + verifyWatchesOfProject( + host, + withTsserver ? + [...core.slice(0, core.length - 1), ...logic, tests[0], libFile].map(f => f.path.toLowerCase()) : + [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase())), + expectedWatchedDirectoriesRecursive + ); } function verifyScenario( edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, - expectedFilesAfterEdit: () => readonly string[] + expectedProgramFilesAfterEdit: () => readonly string[], + expectedProjectFilesAfterEdit: () => readonly string[] ) { it("with tsc-watch", () => { const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); @@ -737,7 +738,7 @@ let x: string = 10;`); host.checkTimeoutQueueLengthAndRun(1); checkOutputErrorsIncremental(host, emptyArray); - checkProgramActualFiles(watch(), expectedFilesAfterEdit()); + checkProgramActualFiles(watch(), expectedProgramFilesAfterEdit()); }); @@ -747,7 +748,7 @@ let x: string = 10;`); edit(host, solutionBuilder); host.checkTimeoutQueueLengthAndRun(2); - checkProjectActualFiles(service, tests[0].path, [tests[0].path, ...expectedFilesAfterEdit()]); + checkProjectActualFiles(service, tests[0].path, expectedProjectFilesAfterEdit()); }); } @@ -777,7 +778,7 @@ function foo() { // not ideal, but currently because of d.ts but no new file is written // There will be timeout queued even though file contents are same - }, expectedProgramFiles); + }, expectedProgramFiles, expectedProjectFiles); }); describe("non local edit in ts file, rebuilds in watch compilation", () => { @@ -787,7 +788,7 @@ export function gfoo() { }`); solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); solutionBuilder.buildNextInvalidatedProject(); - }, expectedProgramFiles); + }, expectedProgramFiles, expectedProjectFiles); }); describe("change in project reference config file builds correctly", () => { @@ -798,7 +799,7 @@ export function gfoo() { })); solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath, ConfigFileProgramReloadLevel.Full); solutionBuilder.buildNextInvalidatedProject(); - }, () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")]); + }, () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")], expectedProjectFiles); }); }); @@ -888,7 +889,9 @@ export function gfoo() { const aDts = dtsFile(multiFolder ? "a/index" : "a"), bDts = dtsFile(multiFolder ? "b/index" : "b"); const expectedFiles = [jsFile(multiFolder ? "a/index" : "a"), aDts, jsFile(multiFolder ? "b/index" : "b"), bDts, jsFile(multiFolder ? "c/index" : "c")]; const expectedProgramFiles = [cTs.path, libFile.path, aDts, refs.path, bDts]; + const expectedProjectFiles = [cTs.path, libFile.path, aTs.path, refs.path, bTs.path]; const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); + const expectedProjectWatchedFiles = expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); const expectedWatchedDirectories = multiFolder ? [ getProjectPath(project).toLowerCase() // watches for directories created for resolution of b ] : emptyArray; @@ -926,22 +929,29 @@ export function gfoo() { } function verifyProject(host: TsBuildWatchSystem, service: projectSystem.TestProjectService, orphanInfos?: readonly string[]) { - verifyServerState(host, service, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos); + verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }); } - function verifyServerState( - host: TsBuildWatchSystem, - service: projectSystem.TestProjectService, - expectedProgramFiles: readonly string[], - expectedWatchedFiles: readonly string[], - expectedWatchedDirectoriesRecursive: readonly string[], - orphanInfos?: readonly string[]) { - checkProjectActualFiles(service, cTsconfig.path, expectedProgramFiles.concat(cTsconfig.path)); - const watchedFiles = expectedWatchedFiles.filter(f => f !== cTs.path.toLowerCase()); - if (orphanInfos) { + interface VerifyServerState { + host: TsBuildWatchSystem; + service: projectSystem.TestProjectService; + expectedProjectFiles: readonly string[]; + expectedProjectWatchedFiles: readonly string[]; + expectedWatchedDirectoriesRecursive: readonly string[]; + orphanInfos?: readonly string[]; + } + function verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }: VerifyServerState) { + checkProjectActualFiles(service, cTsconfig.path, expectedProjectFiles.concat(cTsconfig.path)); + const watchedFiles = expectedProjectWatchedFiles.filter(f => f !== cTs.path.toLowerCase()); + const actualOrphan = arrayFrom(mapDefinedIterator( + service.filenameToScriptInfo.values(), + v => v.containingProjects.length === 0 ? v.fileName : undefined + )); + assert.equal(actualOrphan.length, orphanInfos ? orphanInfos.length : 0, `Orphans found: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); + if (orphanInfos && orphanInfos.length) { for (const orphan of orphanInfos) { const info = service.getScriptInfoForPath(orphan as Path); - assert.isDefined(info); + assert.isDefined(info, `${orphan} expected to be present. Actual: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); assert.equal(info!.containingProjects.length, 0); watchedFiles.push(orphan); } @@ -949,16 +959,20 @@ export function gfoo() { verifyWatchesOfProject(host, watchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); } - function verifyScenario( - edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, - expectedEditErrors: readonly string[], - expectedProgramFiles: readonly string[], - expectedWatchedFiles: readonly string[], - expectedWatchedDirectoriesRecursive: readonly string[], - dependencies: readonly [string, readonly string[]][], - revert?: (host: TsBuildWatchSystem) => void, - orphanInfosAfterEdit?: readonly string[], - orphanInfosAfterRevert?: readonly string[]) { + interface VerifyScenario { + edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void; + expectedEditErrors: readonly string[]; + expectedProgramFiles: readonly string[]; + expectedProjectFiles: readonly string[]; + expectedWatchedFiles: readonly string[]; + expectedProjectWatchedFiles: readonly string[]; + expectedWatchedDirectoriesRecursive: readonly string[]; + dependencies: readonly [string, readonly string[]][]; + revert?: (host: TsBuildWatchSystem) => void; + orphanInfosAfterEdit?: readonly string[]; + orphanInfosAfterRevert?: readonly string[]; + } + function verifyScenario({ edit, expectedEditErrors, expectedProgramFiles, expectedProjectFiles, expectedWatchedFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, revert, orphanInfosAfterEdit, orphanInfosAfterRevert }: VerifyScenario) { it("with tsc-watch", () => { const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); @@ -985,7 +999,7 @@ export function gfoo() { edit(host, solutionBuilder); host.checkTimeoutQueueLengthAndRun(2); - verifyServerState(host, service, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfosAfterEdit); + verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos: orphanInfosAfterEdit }); if (revert) { revert(host); @@ -1010,20 +1024,21 @@ export function gfoo() { }); describe("non local edit updates the program and watch correctly", () => { - verifyScenario( - (host, solutionBuilder) => { + verifyScenario({ + edit: (host, solutionBuilder) => { // edit - host.writeFile(bTs.path, `${bTs.content} -export function gfoo() { -}`); - solutionBuilder.invalidateProject(bTsconfig.path.toLowerCase() as ResolvedConfigFilePath); + host.writeFile(bTs.path, `${bTs.content}\nexport function gfoo() {\n}`); + solutionBuilder.invalidateProject((bTsconfig.path.toLowerCase() as ResolvedConfigFilePath)); solutionBuilder.buildNextInvalidatedProject(); }, - emptyArray, + expectedEditErrors: emptyArray, expectedProgramFiles, + expectedProjectFiles, expectedWatchedFiles, + expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, - defaultDependencies); + dependencies: defaultDependencies + }); }); describe("edit on config file", () => { @@ -1032,30 +1047,32 @@ export function gfoo() { path: getFilePathInProject(project, "nrefs/a.d.ts"), content: refs.content }; - verifyScenario( - host => { + verifyScenario({ + edit: host => { const cTsConfigJson = JSON.parse(cTsconfig.content); host.ensureFileOrFolder(nrefs); cTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; host.writeFile(cTsconfig.path, JSON.stringify(cTsConfigJson)); }, - emptyArray, - expectedProgramFiles.map(nrefReplacer), - expectedWatchedFiles.map(nrefReplacer), - expectedWatchedDirectoriesRecursive.map(nrefReplacer), - [ + expectedEditErrors: emptyArray, + expectedProgramFiles: expectedProgramFiles.map(nrefReplacer), + expectedProjectFiles: expectedProjectFiles.map(nrefReplacer), + expectedWatchedFiles: expectedWatchedFiles.map(nrefReplacer), + expectedProjectWatchedFiles: expectedProjectWatchedFiles.map(nrefReplacer), + expectedWatchedDirectoriesRecursive: expectedWatchedDirectoriesRecursive.map(nrefReplacer), + dependencies: [ [aDts, [aDts]], [bDts, [bDts, aDts]], [nrefs.path, [nrefs.path]], [cTs.path, [cTs.path, nrefs.path, bDts]] ], // revert the update - host => host.writeFile(cTsconfig.path, cTsconfig.content), + revert: host => host.writeFile(cTsconfig.path, cTsconfig.content), // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open - [refs.path.toLowerCase()], + orphanInfosAfterEdit: [refs.path.toLowerCase()], // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open - [nrefs.path.toLowerCase()] - ); + orphanInfosAfterRevert: [nrefs.path.toLowerCase()] + }); }); describe("edit in referenced config file", () => { @@ -1064,82 +1081,84 @@ export function gfoo() { content: "export declare class A {}" }; const expectedProgramFiles = [cTs.path, bDts, nrefs.path, refs.path, libFile.path]; + const expectedProjectFiles = [cTs.path, bTs.path, nrefs.path, refs.path, libFile.path]; const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario - verifyScenario( - host => { + verifyScenario({ + edit: host => { const bTsConfigJson = JSON.parse(bTsconfig.content); host.ensureFileOrFolder(nrefs); bTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; host.writeFile(bTsconfig.path, JSON.stringify(bTsConfigJson)); }, - emptyArray, + expectedEditErrors: emptyArray, expectedProgramFiles, - expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), - (multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive).concat(getFilePathInProject(project, "nrefs").toLowerCase()), - [ + expectedProjectFiles, + expectedWatchedFiles: expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), + expectedProjectWatchedFiles: expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), + expectedWatchedDirectoriesRecursive: (multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive).concat(getFilePathInProject(project, "nrefs").toLowerCase()), + dependencies: [ [nrefs.path, [nrefs.path]], [bDts, [bDts, nrefs.path]], [refs.path, [refs.path]], [cTs.path, [cTs.path, refs.path, bDts]], ], // revert the update - host => host.writeFile(bTsconfig.path, bTsconfig.content), + revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open - [aDts.toLowerCase()], + orphanInfosAfterEdit: [aTs.path.toLowerCase()], // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open - [nrefs.path.toLowerCase()] - ); + orphanInfosAfterRevert: [nrefs.path.toLowerCase()] + }); }); describe("deleting referenced config file", () => { const expectedProgramFiles = [cTs.path, bTs.path, refs.path, libFile.path]; + const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path).map(s => s.toLowerCase()); const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario // Resolutions should change now // Should map to b.ts instead with options from our own config - verifyScenario( - host => host.deleteFile(bTsconfig.path), - [ + verifyScenario({ + edit: host => host.deleteFile(bTsconfig.path), + expectedEditErrors: [ `${multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"}(9,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "b" : "tsconfig.b.json"}' not found.\n` ], expectedProgramFiles, - expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path).map(s => s.toLowerCase()), - multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive, - [ + expectedProjectFiles: expectedProgramFiles, + expectedWatchedFiles, + expectedProjectWatchedFiles: expectedWatchedFiles, + expectedWatchedDirectoriesRecursive: multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive, + dependencies: [ [bTs.path, [bTs.path, refs.path]], [refs.path, [refs.path]], [cTs.path, [cTs.path, refs.path, bTs.path]], ], // revert the update - host => host.writeFile(bTsconfig.path, bTsconfig.content), + revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open - [bDts.toLowerCase(), aDts.toLowerCase(), aTsconfig.path.toLowerCase()], - // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open - [bTs.path.toLowerCase()] - ); + orphanInfosAfterEdit: [aTs.path.toLowerCase(), aTsconfig.path.toLowerCase()], + }); }); describe("deleting transitively referenced config file", () => { - verifyScenario( - host => host.deleteFile(aTsconfig.path), - [ + verifyScenario({ + edit: host => host.deleteFile(aTsconfig.path), + expectedEditErrors: [ `${multiFolder ? "b/tsconfig.json" : "tsconfig.b.json"}(10,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "a" : "tsconfig.a.json"}' not found.\n` ], - expectedProgramFiles.map(s => s.replace(aDts, aTs.path)), - expectedWatchedFiles.map(s => s.replace(aDts.toLowerCase(), aTs.path.toLocaleLowerCase())), + expectedProgramFiles: expectedProgramFiles.map(s => s.replace(aDts, aTs.path)), + expectedProjectFiles, + expectedWatchedFiles: expectedWatchedFiles.map(s => s.replace(aDts.toLowerCase(), aTs.path.toLocaleLowerCase())), + expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, - [ + dependencies: [ [aTs.path, [aTs.path]], [bDts, [bDts, aTs.path]], [refs.path, [refs.path]], [cTs.path, [cTs.path, refs.path, bDts]], ], // revert the update - host => host.writeFile(aTsconfig.path, aTsconfig.content), - // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open - [aDts.toLowerCase()], - // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open - [aTs.path.toLowerCase()] - ); + revert: host => host.writeFile(aTsconfig.path, aTsconfig.content), + }); }); } diff --git a/src/testRunner/unittests/tsserver/declarationFileMaps.ts b/src/testRunner/unittests/tsserver/declarationFileMaps.ts index d1c64fe46c..a6f217385d 100644 --- a/src/testRunner/unittests/tsserver/declarationFileMaps.ts +++ b/src/testRunner/unittests/tsserver/declarationFileMaps.ts @@ -179,7 +179,7 @@ namespace ts.projectSystem { } function verifyUserTsConfigProject(session: TestSession) { - checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aDts.path, userTsconfig.path]); + checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aTs.path, userTsconfig.path]); } it("goToDefinition", () => { @@ -450,6 +450,13 @@ namespace ts.projectSystem { name: "function f(): void", }, references: [ + makeReferenceEntry({ + file: aTs, + text: "f", + options: { index: 1 }, + contextText: "function f() {}", + isDefinition: true + }), { fileName: bTs.path, isDefinition: false, @@ -457,13 +464,6 @@ namespace ts.projectSystem { isWriteAccess: false, textSpan: { start: 0, length: 1 }, }, - makeReferenceEntry({ - file: aTs, - text: "f", - options: { index: 1 }, - contextText: "function f() {}", - isDefinition: true - }) ], } ]); diff --git a/src/testRunner/unittests/tsserver/events/projectLoading.ts b/src/testRunner/unittests/tsserver/events/projectLoading.ts index cb22f41ff2..5f4bf1a52c 100644 --- a/src/testRunner/unittests/tsserver/events/projectLoading.ts +++ b/src/testRunner/unittests/tsserver/events/projectLoading.ts @@ -73,44 +73,64 @@ namespace ts.projectSystem { verifyEvent(project, `Change in config file detected`); }); - it("when opening original location project", () => { - const aDTs: File = { - path: `${projectRoot}/a/a.d.ts`, - content: `export declare class A { + describe("when opening original location project", () => { + it("with project references", () => { + verify(); + }); + + it("when disableSourceOfProjectReferenceRedirect is true", () => { + verify(/*disableSourceOfProjectReferenceRedirect*/ true); + }); + + function verify(disableSourceOfProjectReferenceRedirect?: true) { + const aDTs: File = { + path: `${projectRoot}/a/a.d.ts`, + content: `export declare class A { } //# sourceMappingURL=a.d.ts.map ` - }; - const aDTsMap: File = { - path: `${projectRoot}/a/a.d.ts.map`, - content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}` - }; - const bTs: File = { - path: bTsPath, - content: `import {A} from "../a/a"; new A();` - }; - const configB: File = { - path: configBPath, - content: JSON.stringify({ - references: [{ path: "../a" }] - }) - }; + }; + const aDTsMap: File = { + path: `${projectRoot}/a/a.d.ts.map`, + content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}` + }; + const bTs: File = { + path: bTsPath, + content: `import {A} from "../a/a"; new A();` + }; + const configB: File = { + path: configBPath, + content: JSON.stringify({ + ...(disableSourceOfProjectReferenceRedirect && { + compilerOptions: { + disableSourceOfProjectReferenceRedirect + } + }), + references: [{ path: "../a" }] + }) + }; - const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); - verifyEventWithOpenTs(bTs, configB.path, 1); + const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); + verifyEventWithOpenTs(bTs, configB.path, 1); - session.executeCommandSeq({ - command: protocol.CommandTypes.References, - arguments: { - file: bTs.path, - ...protocolLocationFromSubstring(bTs.content, "A()") - } - }); + session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: { + file: bTs.path, + ...protocolLocationFromSubstring(bTs.content, "A()") + } + }); - checkNumberOfProjects(service, { configuredProjects: 2 }); - const project = service.configuredProjects.get(configA.path)!; - assert.isDefined(project); - verifyEvent(project, `Creating project for original file: ${aTs.path} for location: ${aDTs.path}`); + checkNumberOfProjects(service, { configuredProjects: 2 }); + const project = service.configuredProjects.get(configA.path)!; + assert.isDefined(project); + verifyEvent( + project, + disableSourceOfProjectReferenceRedirect ? + `Creating project for original file: ${aTs.path} for location: ${aDTs.path}` : + `Creating project for original file: ${aTs.path}` + ); + } }); describe("with external projects and config files ", () => { diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index 1fb3593590..a8c79d0b60 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -491,8 +491,8 @@ namespace ts.projectSystem { checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); } - export function checkScriptInfos(projectService: server.ProjectService, expectedFiles: readonly string[]) { - checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); + export function checkScriptInfos(projectService: server.ProjectService, expectedFiles: readonly string[], additionInfo?: string) { + checkArray(`ScriptInfos files: ${additionInfo || ""}`, arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); } export function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { @@ -501,7 +501,7 @@ namespace ts.projectSystem { return protocolToLocation(str)(start); } - function protocolToLocation(text: string): (pos: number) => protocol.Location { + export function protocolToLocation(text: string): (pos: number) => protocol.Location { const lineStarts = computeLineStarts(text); return pos => { const x = computeLineAndCharacterOfPosition(lineStarts, pos); diff --git a/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts b/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts new file mode 100644 index 0000000000..9602ef6360 --- /dev/null +++ b/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts @@ -0,0 +1,410 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: with project references and compile on save", () => { + const projectLocation = "/user/username/projects/myproject"; + const dependecyLocation = `${projectLocation}/dependency`; + const usageLocation = `${projectLocation}/usage`; + const dependencyTs: File = { + path: `${dependecyLocation}/fns.ts`, + content: `export function fn1() { } +export function fn2() { } +` + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, declarationDir: "../decls" }, + compileOnSave: true + }) + }; + const usageTs: File = { + path: `${usageLocation}/usage.ts`, + content: `import { + fn1, + fn2, +} from '../decls/fns' +fn1(); +fn2(); +` + }; + const usageConfig: File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compileOnSave: true, + references: [{ path: "../dependency" }] + }) + }; + + interface VerifySingleScenarioWorker extends VerifySingleScenario { + withProject: boolean; + } + function verifySingleScenarioWorker({ + withProject, scenario, openFiles, requestArgs, change, expectedResult + }: VerifySingleScenarioWorker) { + it(scenario, () => { + const host = TestFSWithWatch.changeToHostTrackingWrittenFiles( + createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, libFile]) + ); + const session = createSession(host); + openFilesForSession(openFiles(), session); + const reqArgs = requestArgs(); + const { + expectedAffected, + expectedEmit: { expectedEmitSuccess, expectedFiles }, + expectedEmitOutput + } = expectedResult(withProject); + + if (change) { + session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const { file, insertString } = change(); + if (session.getProjectService().openFiles.has(file.path)) { + const toLocation = protocolToLocation(file.content); + const location = toLocation(file.content.length); + session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: file.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString + } + }); + } + else { + host.writeFile(file.path, `${file.content}${insertString}`); + } + host.writtenFiles.clear(); + } + + const args = withProject ? reqArgs : { file: reqArgs.file }; + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: args + }).response as protocol.CompileOnSaveAffectedFileListSingleProject[]; + assert.deepEqual(actualAffectedFiles, expectedAffected, "Affected files"); + + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: args + }).response; + assert.deepEqual(actualEmit, expectedEmitSuccess, "Emit files"); + assert.equal(host.writtenFiles.size, expectedFiles.length); + for (const file of expectedFiles) { + assert.equal(host.readFile(file.path), file.content, `Expected to write ${file.path}`); + assert.isTrue(host.writtenFiles.has(file.path), `${file.path} is newly written`); + } + + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ + command: protocol.CommandTypes.EmitOutput, + arguments: args + }).response as EmitOutput; + assert.deepEqual(actualEmitOutput, expectedEmitOutput, "Emit output"); + }); + } + + interface VerifySingleScenario { + scenario: string; + openFiles: () => readonly File[]; + requestArgs: () => protocol.FileRequestArgs; + skipWithoutProject?: boolean; + change?: () => SingleScenarioChange; + expectedResult: GetSingleScenarioResult; + } + function verifySingleScenario(scenario: VerifySingleScenario) { + if (!scenario.skipWithoutProject) { + describe("without specifying project file", () => { + verifySingleScenarioWorker({ + withProject: false, + ...scenario + }); + }); + } + describe("with specifying project file", () => { + verifySingleScenarioWorker({ + withProject: true, + ...scenario + }); + }); + } + + interface SingleScenarioExpectedEmit { + expectedEmitSuccess: boolean; + expectedFiles: readonly File[]; + } + interface SingleScenarioResult { + expectedAffected: protocol.CompileOnSaveAffectedFileListSingleProject[]; + expectedEmit: SingleScenarioExpectedEmit; + expectedEmitOutput: EmitOutput; + } + type GetSingleScenarioResult = (withProject: boolean) => SingleScenarioResult; + interface SingleScenarioChange { + file: File; + insertString: string; + } + interface ScenarioDetails { + scenarioName: string; + requestArgs: () => protocol.FileRequestArgs; + skipWithoutProject?: boolean; + initial: GetSingleScenarioResult; + localChangeToDependency: GetSingleScenarioResult; + localChangeToUsage: GetSingleScenarioResult; + changeToDependency: GetSingleScenarioResult; + changeToUsage: GetSingleScenarioResult; + } + interface VerifyScenario { + openFiles: () => readonly File[]; + scenarios: readonly ScenarioDetails[]; + } + + const localChange = "function fn3() { }"; + const change = `export ${localChange}`; + const changeJs = `function fn3() { } +exports.fn3 = fn3;`; + const changeDts = "export declare function fn3(): void;"; + function verifyScenario({ openFiles, scenarios }: VerifyScenario) { + for (const { + scenarioName, requestArgs, skipWithoutProject, initial, + localChangeToDependency, localChangeToUsage, + changeToDependency, changeToUsage + } of scenarios) { + describe(scenarioName, () => { + verifySingleScenario({ + scenario: "with initial file open", + openFiles, + requestArgs, + skipWithoutProject, + expectedResult: initial + }); + + verifySingleScenario({ + scenario: "with local change to dependency", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: dependencyTs, insertString: localChange }), + expectedResult: localChangeToDependency + }); + + verifySingleScenario({ + scenario: "with local change to usage", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: usageTs, insertString: localChange }), + expectedResult: localChangeToUsage + }); + + verifySingleScenario({ + scenario: "with change to dependency", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: dependencyTs, insertString: change }), + expectedResult: changeToDependency + }); + + verifySingleScenario({ + scenario: "with change to usage", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: usageTs, insertString: change }), + expectedResult: changeToUsage + }); + }); + } + } + + function expectedAffectedFiles(config: File, fileNames: File[]): protocol.CompileOnSaveAffectedFileListSingleProject { + return { + projectFileName: config.path, + fileNames: fileNames.map(f => f.path), + projectUsesOutFile: false + }; + } + + function expectedUsageEmit(appendJsText?: string): SingleScenarioExpectedEmit { + const appendJs = appendJsText ? `${appendJsText} +` : ""; + return { + expectedEmitSuccess: true, + expectedFiles: [{ + path: `${usageLocation}/usage.js`, + content: `"use strict"; +exports.__esModule = true; +var fns_1 = require("../decls/fns"); +fns_1.fn1(); +fns_1.fn2(); +${appendJs}` + }] + }; + } + + function expectedEmitOutput({ expectedFiles }: SingleScenarioExpectedEmit): EmitOutput { + return { + outputFiles: expectedFiles.map(({ path, content }) => ({ + name: path, + text: content, + writeByteOrderMark: false + })), + emitSkipped: false + }; + } + + function expectedUsageEmitOutput(appendJsText?: string): EmitOutput { + return expectedEmitOutput(expectedUsageEmit(appendJsText)); + } + + function noEmit(): SingleScenarioExpectedEmit { + return { + expectedEmitSuccess: false, + expectedFiles: emptyArray + }; + } + + function noEmitOutput(): EmitOutput { + return { + emitSkipped: true, + outputFiles: [] + }; + } + + function expectedDependencyEmit(appendJsText?: string, appendDtsText?: string): SingleScenarioExpectedEmit { + const appendJs = appendJsText ? `${appendJsText} +` : ""; + const appendDts = appendDtsText ? `${appendDtsText} +` : ""; + return { + expectedEmitSuccess: true, + expectedFiles: [ + { + path: `${dependecyLocation}/fns.js`, + content: `"use strict"; +exports.__esModule = true; +function fn1() { } +exports.fn1 = fn1; +function fn2() { } +exports.fn2 = fn2; +${appendJs}` + }, + { + path: `${projectLocation}/decls/fns.d.ts`, + content: `export declare function fn1(): void; +export declare function fn2(): void; +${appendDts}` + } + ] + }; + } + + function expectedDependencyEmitOutput(appendJsText?: string, appendDtsText?: string): EmitOutput { + return expectedEmitOutput(expectedDependencyEmit(appendJsText, appendDtsText)); + } + + function scenarioDetailsOfUsage(isDependencyOpen?: boolean): ScenarioDetails[] { + return [ + { + scenarioName: "Of usageTs", + requestArgs: () => ({ file: usageTs.path, projectFileName: usageConfig.path }), + initial: () => initialUsageTs(), + // no change to usage so same as initial only usage file + localChangeToDependency: () => initialUsageTs(), + localChangeToUsage: () => initialUsageTs(localChange), + changeToDependency: () => initialUsageTs(), + changeToUsage: () => initialUsageTs(changeJs) + }, + { + scenarioName: "Of dependencyTs in usage project", + requestArgs: () => ({ file: dependencyTs.path, projectFileName: usageConfig.path }), + skipWithoutProject: !!isDependencyOpen, + initial: () => initialDependencyTs(), + localChangeToDependency: () => initialDependencyTs(/*noUsageFiles*/ true), + localChangeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true), + changeToDependency: () => initialDependencyTs(), + changeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true) + } + ]; + + function initialUsageTs(jsText?: string) { + return { + expectedAffected: [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], + expectedEmit: expectedUsageEmit(jsText), + expectedEmitOutput: expectedUsageEmitOutput(jsText) + }; + } + + function initialDependencyTs(noUsageFiles?: true) { + return { + expectedAffected: [ + expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]) + ], + expectedEmit: noEmit(), + expectedEmitOutput: noEmitOutput() + }; + } + } + + function scenarioDetailsOfDependencyWhenOpen(): ScenarioDetails { + return { + scenarioName: "Of dependencyTs", + requestArgs: () => ({ file: dependencyTs.path, projectFileName: dependencyConfig.path }), + initial, + localChangeToDependency: withProject => ({ + expectedAffected: withProject ? + [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ] : + [ + expectedAffectedFiles(usageConfig, []), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], + expectedEmit: expectedDependencyEmit(localChange), + expectedEmitOutput: expectedDependencyEmitOutput(localChange) + }), + localChangeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true), + changeToDependency: withProject => initial(withProject, /*noUsageFiles*/ undefined, changeJs, changeDts), + changeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true) + }; + + function initial(withProject: boolean, noUsageFiles?: true, appendJs?: string, appendDts?: string): SingleScenarioResult { + return { + expectedAffected: withProject ? + [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ] : + [ + expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], + expectedEmit: expectedDependencyEmit(appendJs, appendDts), + expectedEmitOutput: expectedDependencyEmitOutput(appendJs, appendDts) + }; + } + } + + describe("when dependency project is not open", () => { + verifyScenario({ + openFiles: () => [usageTs], + scenarios: scenarioDetailsOfUsage() + }); + }); + + describe("when the depedency file is open", () => { + verifyScenario({ + openFiles: () => [usageTs, dependencyTs], + scenarios: [ + ...scenarioDetailsOfUsage(/*isDependencyOpen*/ true), + scenarioDetailsOfDependencyWhenOpen(), + ] + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts new file mode 100644 index 0000000000..a3ec484861 --- /dev/null +++ b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts @@ -0,0 +1,430 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: with project references and error reporting", () => { + const projectLocation = "/user/username/projects/myproject"; + const dependecyLocation = `${projectLocation}/dependency`; + const usageLocation = `${projectLocation}/usage`; + + interface CheckErrorsInFile { + session: TestSession; + host: TestServerHost; + expected: GetErrDiagnostics; + expectedSequenceId?: number; + } + function checkErrorsInFile({ session, host, expected: { file, syntax, semantic, suggestion }, expectedSequenceId }: CheckErrorsInFile) { + host.checkTimeoutQueueLengthAndRun(1); + checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: syntax }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: semantic }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "suggestionDiag", { file: file.path, diagnostics: suggestion }); + if (expectedSequenceId !== undefined) { + checkCompleteEvent(session, 2, expectedSequenceId); + } + session.clearMessages(); + } + + interface CheckAllErrors { + session: TestSession; + host: TestServerHost; + expected: readonly GetErrDiagnostics[]; + expectedSequenceId: number; + } + function checkAllErrors({ session, host, expected, expectedSequenceId }: CheckAllErrors) { + for (let i = 0; i < expected.length; i++) { + checkErrorsInFile({ + session, + host, + expected: expected[i], + expectedSequenceId: i === expected.length - 1 ? expectedSequenceId : undefined + }); + } + } + + function verifyErrorsUsingGeterr({ allFiles, openFiles, expectedGetErr }: VerifyScenario) { + it("verifies the errors in open file", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, }); + openFilesForSession(openFiles(), session); + + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + const expected = expectedGetErr(); + session.executeCommandSeq({ + command: protocol.CommandTypes.Geterr, + arguments: { + delay: 0, + files: expected.map(f => f.file.path) + } + }); + + checkAllErrors({ session, host, expected, expectedSequenceId }); + }); + } + + function verifyErrorsUsingGeterrForProject({ allFiles, openFiles, expectedGetErrForProject }: VerifyScenario) { + it("verifies the errors in projects", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, }); + openFilesForSession(openFiles(), session); + + session.clearMessages(); + for (const expected of expectedGetErrForProject()) { + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: protocol.CommandTypes.GeterrForProject, + arguments: { + delay: 0, + file: expected.project + } + }); + + checkAllErrors({ session, host, expected: expected.errors, expectedSequenceId }); + } + }); + } + + function verifyErrorsUsingSyncMethods({ allFiles, openFiles, expectedSyncDiagnostics }: VerifyScenario) { + it("verifies the errors using sync commands", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host); + openFilesForSession(openFiles(), session); + for (const { file, project, syntax, semantic, suggestion } of expectedSyncDiagnostics()) { + const actualSyntax = session.executeCommandSeq({ + command: protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: { + file: file.path, + projectFileName: project + } + }).response as protocol.Diagnostic[]; + assert.deepEqual(actualSyntax, syntax, `Syntax diagnostics for file: ${file.path}, project: ${project}`); + const actualSemantic = session.executeCommandSeq({ + command: protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: { + file: file.path, + projectFileName: project + } + }).response as protocol.Diagnostic[]; + assert.deepEqual(actualSemantic, semantic, `Semantic diagnostics for file: ${file.path}, project: ${project}`); + const actualSuggestion = session.executeCommandSeq({ + command: protocol.CommandTypes.SuggestionDiagnosticsSync, + arguments: { + file: file.path, + projectFileName: project + } + }).response as protocol.Diagnostic[]; + assert.deepEqual(actualSuggestion, suggestion, `Suggestion diagnostics for file: ${file.path}, project: ${project}`); + } + }); + } + + function verifyConfigFileErrors({ allFiles, openFiles, expectedConfigFileDiagEvents }: VerifyScenario) { + it("verify config file errors", () => { + const host = createServerHost([...allFiles(), libFile]); + const { session, events } = createSessionWithEventTracking(host, server.ConfigFileDiagEvent); + + for (const file of openFiles()) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { file: file.path } + }); + } + + assert.deepEqual(events, expectedConfigFileDiagEvents().map(data => ({ + eventName: server.ConfigFileDiagEvent, + data + }))); + }); + } + + interface GetErrDiagnostics { + file: File; + syntax: protocol.Diagnostic[]; + semantic: protocol.Diagnostic[]; + suggestion: protocol.Diagnostic[]; + } + interface GetErrForProjectDiagnostics { + project: string; + errors: readonly GetErrDiagnostics[]; + } + interface SyncDiagnostics extends GetErrDiagnostics { + project?: string; + } + interface VerifyScenario { + allFiles: () => readonly File[]; + openFiles: () => readonly File[]; + expectedGetErr: () => readonly GetErrDiagnostics[]; + expectedGetErrForProject: () => readonly GetErrForProjectDiagnostics[]; + expectedSyncDiagnostics: () => readonly SyncDiagnostics[]; + expectedConfigFileDiagEvents: () => readonly server.ConfigFileDiagEvent["data"][]; + } + function verifyScenario(scenario: VerifyScenario) { + verifyErrorsUsingGeterr(scenario); + verifyErrorsUsingGeterrForProject(scenario); + verifyErrorsUsingSyncMethods(scenario); + verifyConfigFileErrors(scenario); + } + + function emptyDiagnostics(file: File): GetErrDiagnostics { + return { + file, + syntax: emptyArray, + semantic: emptyArray, + suggestion: emptyArray + }; + } + + function syncDiagnostics(diagnostics: GetErrDiagnostics, project: string): SyncDiagnostics { + return { project, ...diagnostics }; + } + + interface VerifyUsageAndDependency { + allFiles: readonly [File, File, File, File]; // dependencyTs, dependencyConfig, usageTs, usageConfig + usageDiagnostics(): GetErrDiagnostics; + dependencyDiagnostics(): GetErrDiagnostics; + + } + function verifyUsageAndDependency({ allFiles, usageDiagnostics, dependencyDiagnostics }: VerifyUsageAndDependency) { + const [dependencyTs, dependencyConfig, usageTs, usageConfig] = allFiles; + function usageProjectDiagnostics(): GetErrForProjectDiagnostics { + return { + project: usageTs.path, + errors: [ + usageDiagnostics(), + emptyDiagnostics(dependencyTs) + ] + }; + } + + function dependencyProjectDiagnostics(): GetErrForProjectDiagnostics { + return { + project: dependencyTs.path, + errors: [ + dependencyDiagnostics() + ] + }; + } + + function usageConfigDiag(): server.ConfigFileDiagEvent["data"] { + return { + triggerFile: usageTs.path, + configFileName: usageConfig.path, + diagnostics: emptyArray + }; + } + + function dependencyConfigDiag(): server.ConfigFileDiagEvent["data"] { + return { + triggerFile: dependencyTs.path, + configFileName: dependencyConfig.path, + diagnostics: emptyArray + }; + } + + describe("when dependency project is not open", () => { + verifyScenario({ + allFiles: () => allFiles, + openFiles: () => [usageTs], + expectedGetErr: () => [ + usageDiagnostics() + ], + expectedGetErrForProject: () => [ + usageProjectDiagnostics(), + { + project: dependencyTs.path, + errors: [ + emptyDiagnostics(dependencyTs), + usageDiagnostics() + ] + } + ], + expectedSyncDiagnostics: () => [ + // Without project + usageDiagnostics(), + emptyDiagnostics(dependencyTs), + // With project + syncDiagnostics(usageDiagnostics(), usageConfig.path), + syncDiagnostics(emptyDiagnostics(dependencyTs), usageConfig.path), + ], + expectedConfigFileDiagEvents: () => [ + usageConfigDiag() + ], + }); + }); + + describe("when the depedency file is open", () => { + verifyScenario({ + allFiles: () => allFiles, + openFiles: () => [usageTs, dependencyTs], + expectedGetErr: () => [ + usageDiagnostics(), + dependencyDiagnostics(), + ], + expectedGetErrForProject: () => [ + usageProjectDiagnostics(), + dependencyProjectDiagnostics() + ], + expectedSyncDiagnostics: () => [ + // Without project + usageDiagnostics(), + dependencyDiagnostics(), + // With project + syncDiagnostics(usageDiagnostics(), usageConfig.path), + syncDiagnostics(emptyDiagnostics(dependencyTs), usageConfig.path), + syncDiagnostics(dependencyDiagnostics(), dependencyConfig.path), + ], + expectedConfigFileDiagEvents: () => [ + usageConfigDiag(), + dependencyConfigDiag() + ], + }); + }); + } + + describe("with module scenario", () => { + const dependencyTs: File = { + path: `${dependecyLocation}/fns.ts`, + content: `export function fn1() { } +export function fn2() { } +// Introduce error for fnErr import in main +// export function fnErr() { } +// Error in dependency ts file +export let x: string = 10;` + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, declarationDir: "../decls" } }) + }; + const usageTs: File = { + path: `${usageLocation}/usage.ts`, + content: `import { + fn1, + fn2, + fnErr +} from '../decls/fns' +fn1(); +fn2(); +fnErr(); +` + }; + const usageConfig: File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true }, + references: [{ path: "../dependency" }] + }) + }; + function usageDiagnostics(): GetErrDiagnostics { + return { + file: usageTs, + syntax: emptyArray, + semantic: [ + createDiagnostic( + { line: 4, offset: 5 }, + { line: 4, offset: 10 }, + Diagnostics.Module_0_has_no_exported_member_1, + [`"../dependency/fns"`, "fnErr"], + "error", + ) + ], + suggestion: emptyArray + }; + } + + function dependencyDiagnostics(): GetErrDiagnostics { + return { + file: dependencyTs, + syntax: emptyArray, + semantic: [ + createDiagnostic( + { line: 6, offset: 12 }, + { line: 6, offset: 13 }, + Diagnostics.Type_0_is_not_assignable_to_type_1, + ["10", "string"], + "error", + ) + ], + suggestion: emptyArray + }; + } + + verifyUsageAndDependency({ + allFiles: [dependencyTs, dependencyConfig, usageTs, usageConfig], + usageDiagnostics, + dependencyDiagnostics + }); + }); + + describe("with non module --out", () => { + const dependencyTs: File = { + path: `${dependecyLocation}/fns.ts`, + content: `function fn1() { } +function fn2() { } +// Introduce error for fnErr import in main +// function fnErr() { } +// Error in dependency ts file +let x: string = 10;` + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, outFile: "../dependency.js" } }) + }; + const usageTs: File = { + path: `${usageLocation}/usage.ts`, + content: `fn1(); +fn2(); +fnErr(); +` + }; + const usageConfig: File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, outFile: "../usage.js" }, + references: [{ path: "../dependency" }] + }) + }; + function usageDiagnostics(): GetErrDiagnostics { + return { + file: usageTs, + syntax: emptyArray, + semantic: [ + createDiagnostic( + { line: 3, offset: 1 }, + { line: 3, offset: 6 }, + Diagnostics.Cannot_find_name_0, + ["fnErr"], + "error", + ) + ], + suggestion: emptyArray + }; + } + + function dependencyDiagnostics(): GetErrDiagnostics { + return { + file: dependencyTs, + syntax: emptyArray, + semantic: [ + createDiagnostic( + { line: 6, offset: 5 }, + { line: 6, offset: 6 }, + Diagnostics.Type_0_is_not_assignable_to_type_1, + ["10", "string"], + "error", + ) + ], + suggestion: emptyArray + }; + } + + verifyUsageAndDependency({ + allFiles: [dependencyTs, dependencyConfig, usageTs, usageConfig], + usageDiagnostics, + dependencyDiagnostics + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index 7db9d755b2..2e886e2169 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -85,8 +85,8 @@ namespace ts.projectSystem { }); const { file: _, ...renameTextOfMyConstInLib } = locationOfMyConstInLib; assert.deepEqual(response.locs, [ - { file: myConstFile, locs: [{ start: myConstStart, end: myConstEnd }] }, - { file: locationOfMyConstInLib.file, locs: [renameTextOfMyConstInLib] } + { file: locationOfMyConstInLib.file, locs: [renameTextOfMyConstInLib] }, + { file: myConstFile, locs: [{ start: myConstStart, end: myConstEnd }] } ]); }); }); @@ -105,6 +105,7 @@ export function fn4() { } export function fn5() { } ` }; + const dependencyTsPath = dependencyTs.path.toLowerCase(); const dependencyConfig: File = { path: `${dependecyLocation}/tsconfig.json`, content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true, declarationDir: "../decls" } }) @@ -150,59 +151,17 @@ fn5(); const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig]; - function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[]) { - checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos)); - checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase())); + function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], additionalInfo: string) { + checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos), additionalInfo); + checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()), additionalInfo); } - function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[]) { - verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path)); + function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], reqName: string) { + verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path), reqName); } function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) { - verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path]); - } - - // Returns request and expected Response, expected response when no map file - interface SessionAction { - reqName: string; - request: Partial; - expectedResponse: Response; - expectedResponseNoMap?: Response; - expectedResponseNoDts?: Response; - } - function gotoDefintinionFromMainTs(fn: number): SessionAction { - const textSpan = usageSpan(fn); - const definition: protocol.FileSpan = { file: dependencyTs.path, ...declarationSpan(fn) }; - const declareSpaceLength = "declare ".length; - return { - reqName: "goToDef", - request: { - command: protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // To dependency - definitions: [definition], - textSpan - }, - expectedResponseNoMap: { - // To the dts - definitions: [{ - file: dtsPath, - start: { line: fn, offset: definition.start.offset + declareSpaceLength }, - end: { line: fn, offset: definition.end.offset + declareSpaceLength }, - contextStart: { line: fn, offset: 1 }, - contextEnd: { line: fn, offset: 37 } - }], - textSpan - }, - expectedResponseNoDts: { - // To import declaration - definitions: [{ file: mainTs.path, ...importSpan(fn) }], - textSpan - } - }; + verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path], "Random"); } function declarationSpan(fn: number): protocol.TextSpanWithContext { @@ -225,7 +184,91 @@ fn5(); return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } }; } - function renameFromDependencyTs(fn: number): SessionAction { + function goToDefFromMainTs(fn: number): Action { + const textSpan = usageSpan(fn); + const definition: protocol.FileSpan = { file: dependencyTs.path, ...declarationSpan(fn) }; + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To dependency + definitions: [definition], + textSpan + } + }; + } + + function goToDefFromMainTsWithNoMap(fn: number): Action { + const textSpan = usageSpan(fn); + const definition = declarationSpan(fn); + const declareSpaceLength = "declare ".length; + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To the dts + definitions: [{ + file: dtsPath, + start: { line: fn, offset: definition.start.offset + declareSpaceLength }, + end: { line: fn, offset: definition.end.offset + declareSpaceLength }, + contextStart: { line: fn, offset: 1 }, + contextEnd: { line: fn, offset: 37 } + }], + textSpan + } + }; + } + + function goToDefFromMainTsWithNoDts(fn: number): Action { + const textSpan = usageSpan(fn); + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To import declaration + definitions: [{ file: mainTs.path, ...importSpan(fn) }], + textSpan + } + }; + } + + function goToDefFromMainTsWithDependencyChange(fn: number): Action { + const textSpan = usageSpan(fn); + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // Definition on fn + 1 line + definitions: [{ file: dependencyTs.path, ...declarationSpan(fn + 1) }], + textSpan + } + }; + } + + function goToDefFromMainTsProjectInfoVerifier(withRefs: boolean): ProjectInfoVerifier { + return { + openFile: mainTs, + openFileLastLine: 14, + configFile: mainConfig, + expectedProjectActualFiles: withRefs ? + [mainTs.path, libFile.path, mainConfig.path, dependencyTs.path] : + [mainTs.path, libFile.path, mainConfig.path, dtsPath] + }; + } + + function renameFromDependencyTs(fn: number): Action { const defSpan = declarationSpan(fn); const { contextStart: _, contextEnd: _1, ...triggerSpan } = defSpan; return { @@ -251,7 +294,32 @@ fn5(); }; } - function renameFromDependencyTsWithBothProjectsOpen(fn: number): SessionAction { + function renameFromDependencyTsWithDependencyChange(fn: number): Action { + const { expectedResponse: { info, locs }, ...rest } = renameFromDependencyTs(fn + 1); + + return { + ...rest, + expectedResponse: { + info: { + ...info as protocol.RenameInfoSuccess, + displayName: `fn${fn}`, + fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, + }, + locs + } + }; + } + + function renameFromDependencyTsProjectInfoVerifier(): ProjectInfoVerifier { + return { + openFile: dependencyTs, + openFileLastLine: 6, + configFile: dependencyConfig, + expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path] + }; + } + + function renameFromDependencyTsWithBothProjectsOpen(fn: number): Action { const { reqName, request, expectedResponse } = renameFromDependencyTs(fn); const { info, locs } = expectedResponse; return { @@ -269,442 +337,846 @@ fn5(); ] } ] - }, - // Only dependency result - expectedResponseNoMap: expectedResponse, - expectedResponseNoDts: expectedResponse + } }; } - // Returns request and expected Response - type SessionActionGetter = (fn: number) => SessionAction; - // Open File, expectedProjectActualFiles, actionGetter, openFileLastLine - interface DocumentPositionMapperVerifier { - openFile: File; - expectedProjectActualFiles: readonly string[]; - actionGetter: SessionActionGetter; - openFileLastLine: number; + function renameFromDependencyTsWithBothProjectsOpenWithDependencyChange(fn: number): Action { + const { reqName, request, expectedResponse, } = renameFromDependencyTsWithDependencyChange(fn); + const { info, locs } = expectedResponse; + return { + reqName, + request, + expectedResponse: { + info, + locs: [ + locs[0], + { + file: mainTs.path, + locs: [ + importSpan(fn), + usageSpan(fn) + ] + } + ] + } + }; } - function verifyDocumentPositionMapperUpdates( - mainScenario: string, - verifier: readonly DocumentPositionMapperVerifier[], - closedInfos: readonly string[], - withRefs: boolean) { - const openFiles = verifier.map(v => v.openFile); - const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles); - const openFileLastLines = verifier.map(v => v.openFileLastLine); + function removePath(array: readonly string[], ...delPaths: string[]) { + return array.filter(a => { + const aLower = a.toLowerCase(); + return delPaths.every(dPath => dPath !== aLower); + }); + } - const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`); - const openInfos = openFiles.map(f => f.path); - // When usage and dependency are used, dependency config is part of closedInfo so ignore - const otherWatchedFiles = withRefs && verifier.length > 1 ? [configFiles[0]] : configFiles; - function openTsFile(onHostCreate?: (host: TestServerHost) => void) { - const host = createHost(files, [mainConfig.path]); - if (!withRefs) { - // Erase project reference - host.writeFile(mainConfig.path, JSON.stringify({ - compilerOptions: { composite: true, declarationMap: true } - })); - } - if (onHostCreate) { - onHostCreate(host); - } - const session = createSession(host); - openFilesForSession([...openFiles, randomFile], session); - return { host, session }; + interface Action { + reqName: string; + request: Partial; + expectedResponse: Response; + } + interface ActionInfo { + action: (fn: number) => Action; + closedInfos: readonly string[]; + otherWatchedFiles: readonly string[]; + expectsDts: boolean; + expectsMap: boolean; + freshMapInfo?: boolean; + freshDocumentMapper?: boolean; + skipDtsMapCheck?: boolean; + } + type ActionKey = keyof ActionInfoVerifier; + type ActionInfoGetterFn = () => ActionInfo; + type ActionInfoSpreader = [ + ActionKey, // Key to get initial value and pass this value to spread function + (actionInfo: ActionInfo) => Partial> + ]; + type ActionInfoGetter = ActionInfoGetterFn | ActionKey | ActionInfoSpreader; + interface ProjectInfoVerifier { + openFile: File; + openFileLastLine: number; + configFile: File; + expectedProjectActualFiles: readonly string[]; + } + interface ActionInfoVerifier { + main: ActionInfoGetter; + change: ActionInfoGetter; + dtsChange: ActionInfoGetter; + mapChange: ActionInfoGetter; + noMap: ActionInfoGetter; + mapFileCreated: ActionInfoGetter; + mapFileDeleted: ActionInfoGetter; + noDts: ActionInfoGetter; + dtsFileCreated: ActionInfoGetter; + dtsFileDeleted: ActionInfoGetter; + dependencyChange: ActionInfoGetter; + noBuild: ActionInfoGetter; + } + interface DocumentPositionMapperVerifier extends ProjectInfoVerifier, ActionInfoVerifier { + } + + interface VerifierAndWithRefs { + withRefs: boolean; + disableSourceOfProjectReferenceRedirect?: true; + verifier: (withRefs: boolean) => readonly DocumentPositionMapperVerifier[]; + } + + function openFiles(verifiers: readonly DocumentPositionMapperVerifier[]) { + return verifiers.map(v => v.openFile); + } + interface OpenTsFile extends VerifierAndWithRefs { + onHostCreate?: (host: TestServerHost) => void; + } + function openTsFile({ withRefs, disableSourceOfProjectReferenceRedirect, verifier, onHostCreate }: OpenTsFile) { + const host = createHost(files, [mainConfig.path]); + if (!withRefs) { + // Erase project reference + host.writeFile(mainConfig.path, JSON.stringify({ + compilerOptions: { composite: true, declarationMap: true } + })); } - - function checkProject(session: TestSession, noDts?: true) { - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length }); - configFiles.forEach((configFile, index) => { - checkProjectActualFiles( - service.configuredProjects.get(configFile)!, - noDts ? - expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) : - expectedProjectActualFiles[index] - ); - }); + else if (disableSourceOfProjectReferenceRedirect) { + // Erase project reference + host.writeFile(mainConfig.path, JSON.stringify({ + compilerOptions: { + composite: true, + declarationMap: true, + disableSourceOfProjectReferenceRedirect: !!disableSourceOfProjectReferenceRedirect + }, + references: [{ path: "../dependency" }] + })); } - - function verifyInfos(session: TestSession, host: TestServerHost) { - verifyInfosWithRandom(session, host, openInfos, closedInfos, otherWatchedFiles); + if (onHostCreate) { + onHostCreate(host); } + const session = createSession(host); + const verifiers = verifier(withRefs && !disableSourceOfProjectReferenceRedirect); + openFilesForSession([...openFiles(verifiers), randomFile], session); + return { host, session, verifiers }; + } - function verifyInfosWhenNoMapFile(session: TestSession, host: TestServerHost, dependencyTsOK?: true) { - const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); - verifyInfosWithRandom( - session, - host, - openInfos, - closedInfos.filter(f => f !== dtsMapClosedInfo && (dependencyTsOK || f !== dependencyTs.path)), - dtsMapClosedInfo ? otherWatchedFiles.concat(dtsMapClosedInfo) : otherWatchedFiles + function checkProject(session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[], noDts?: true) { + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 + verifiers.length }); + verifiers.forEach(({ configFile, expectedProjectActualFiles }) => { + checkProjectActualFiles( + service.configuredProjects.get(configFile.path.toLowerCase())!, + noDts ? + expectedProjectActualFiles.filter(f => f.toLowerCase() !== dtsPath) : + expectedProjectActualFiles ); - } + }); + } - function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, watchDts: boolean, dependencyTsAndMapOk?: true) { - const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); - const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined); - verifyInfosWithRandom( - session, - host, - openInfos, - closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)), - dtsClosedInfo && watchDts ? - otherWatchedFiles.concat(dtsClosedInfo) : - otherWatchedFiles - ); + function firstAction(session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[]) { + for (const { action } of getActionInfo(verifiers, "main")) { + const { request } = action(1); + session.executeCommandSeq(request); } + } - function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) { - assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap); - if (notEqual) { - assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); + function verifyAction(session: TestSession, { reqName, request, expectedResponse }: Action) { + const { response } = session.executeCommandSeq(request); + assert.deepEqual(response, expectedResponse, `Failed Request: ${reqName}`); + } + + function verifyScriptInfoPresence(session: TestSession, path: string, expectedToBePresent: boolean, reqName: string) { + const info = session.getProjectService().filenameToScriptInfo.get(path); + if (expectedToBePresent) { + assert.isDefined(info, `${reqName}:: ${path} expected to be present`); + } + else { + assert.isUndefined(info, `${reqName}:: ${path} expected to be not present`); + } + return info; + } + + interface VerifyDocumentPositionMapper { + session: TestSession; + dependencyMap: server.ScriptInfo | undefined; + documentPositionMapper: server.ScriptInfo["documentPositionMapper"]; + equal: boolean; + debugInfo: string; + } + function verifyDocumentPositionMapper({ session, dependencyMap, documentPositionMapper, equal, debugInfo }: VerifyDocumentPositionMapper) { + assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap, debugInfo); + if (dependencyMap) { + if (equal) { + assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper, debugInfo); } else { - assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); + assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper, debugInfo); } } + } - function action(verifier: DocumentPositionMapperVerifier, fn: number, session: TestSession) { - const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = verifier.actionGetter(fn); - const { response } = session.executeCommandSeq(request); - return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts, verifier }; + function getActionInfoOfVerfier(verifier: DocumentPositionMapperVerifier, actionKey: ActionKey): ActionInfo { + const actionInfoGetter = verifier[actionKey]; + if (isString(actionInfoGetter)) { + return getActionInfoOfVerfier(verifier, actionInfoGetter); } - function firstAction(session: TestSession) { - verifier.forEach(v => action(v, 1, session)); + if (isArray(actionInfoGetter)) { + const initialValue = getActionInfoOfVerfier(verifier, actionInfoGetter[0]); + return { + ...initialValue, + ...actionInfoGetter[1](initialValue) + }; } - function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) { - // action - let isFirst = true; - for (const v of verifier) { - for (let fn = 1; fn <= 5; fn++) { - const result = action(v, fn, session); - const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath); - if (dtsAbsent) { - assert.isUndefined(dtsInfo); - } - else { - assert.isDefined(dtsInfo); - } - verifyAction(result, dtsInfo, isFirst); - isFirst = false; - } - } - } + return actionInfoGetter(); + } - function verifyAllFnAction( - session: TestSession, - host: TestServerHost, - firstDocumentPositionMapperNotEquals?: true, - dependencyMap?: server.ScriptInfo, - documentPositionMapper?: server.ScriptInfo["documentPositionMapper"] - ) { - // action - verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse }, dtsInfo, isFirst) => { - assert.deepEqual(response, expectedResponse, `Failed on ${reqName}`); - verifyInfos(session, host); - assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath); - if (isFirst) { - if (dependencyMap) { - verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals); - documentPositionMapper = dependencyMap.documentPositionMapper; - } - else { - dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; - documentPositionMapper = dependencyMap.documentPositionMapper; - } - } - else { - verifyDocumentPositionMapper(session, dependencyMap!, documentPositionMapper); - } - }); - return { dependencyMap: dependencyMap!, documentPositionMapper }; - } + function getActionInfo(verifiers: readonly DocumentPositionMapperVerifier[], actionKey: ActionKey): ActionInfo[] { + return verifiers.map(v => getActionInfoOfVerfier(v, actionKey)); + } - function verifyAllFnActionWithNoMap( - session: TestSession, - host: TestServerHost, - dependencyTsOK?: true - ) { - let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"]; - // action - verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoMap }, dtsInfo, isFirst) => { - assert.deepEqual(response, expectedResponseNoMap || expectedResponse, `Failed on ${reqName}`); - verifyInfosWhenNoMapFile(session, host, dependencyTsOK); - assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath)); - if (isFirst) { - assert.isNotString(dtsInfo!.sourceMapFilePath); - assert.isNotFalse(dtsInfo!.sourceMapFilePath); - assert.isDefined(dtsInfo!.sourceMapFilePath); - sourceMapFilePath = dtsInfo!.sourceMapFilePath; - } - else { - assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath); - } - }); - return sourceMapFilePath; - } - - function verifyAllFnActionWithNoDts( - session: TestSession, - host: TestServerHost, - dependencyTsAndMapOk?: true - ) { - // action - verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts, verifier }) => { - assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`); - verifyInfosWhenNoDtsFile( + interface VerifyAllFnAction { + session: TestSession; + host: TestServerHost; + verifiers: readonly DocumentPositionMapperVerifier[]; + actionKey: ActionKey; + sourceMapPath?: server.ScriptInfo["sourceMapFilePath"]; + dependencyMap?: server.ScriptInfo | undefined; + documentPositionMapper?: server.ScriptInfo["documentPositionMapper"]; + } + interface VerifyAllFnActionResult { + actionInfos: readonly ActionInfo[]; + actionKey: ActionKey; + dependencyMap: server.ScriptInfo | undefined; + documentPositionMapper: server.ScriptInfo["documentPositionMapper"] | undefined; + } + function verifyAllFnAction({ + session, + host, + verifiers, + actionKey, + dependencyMap, + documentPositionMapper, + }: VerifyAllFnAction): VerifyAllFnActionResult { + const actionInfos = getActionInfo(verifiers, actionKey); + let sourceMapPath: server.ScriptInfo["sourceMapFilePath"] | undefined; + // action + let first = true; + for (const { + action, + closedInfos, + otherWatchedFiles, + expectsDts, + expectsMap, + freshMapInfo, + freshDocumentMapper, + skipDtsMapCheck + } of actionInfos) { + for (let fn = 1; fn <= 5; fn++) { + const fnAction = action(fn); + verifyAction(session, fnAction); + const debugInfo = `${actionKey}:: ${fnAction.reqName}:: ${fn}`; + const dtsInfo = verifyScriptInfoPresence(session, dtsPath, expectsDts, debugInfo); + const dtsMapInfo = verifyScriptInfoPresence(session, dtsMapPath, expectsMap, debugInfo); + verifyInfosWithRandom( session, host, - // Even when project actual file contains dts, its not watched because the dts is in another folder and module resolution just fails - // instead of succeeding to source file and then mapping using project reference (When using usage location) - // But watched if sourcemapper is in source project since we need to keep track of dts to update the source mapper for any potential usages - verifier.expectedProjectActualFiles.every(f => f.toLowerCase() !== dtsPath), - dependencyTsAndMapOk, + openFiles(verifiers).map(f => f.path), + closedInfos, + otherWatchedFiles, + debugInfo ); - }, /*dtsAbsent*/ true); + + if (dtsInfo) { + if (first || (fn === 1 && freshMapInfo)) { + if (!skipDtsMapCheck) { + if (dtsMapInfo) { + assert.equal(dtsInfo.sourceMapFilePath, dtsMapPath, debugInfo); + } + else { + assert.isNotString(dtsInfo.sourceMapFilePath, debugInfo); + assert.isNotFalse(dtsInfo.sourceMapFilePath, debugInfo); + assert.isDefined(dtsInfo.sourceMapFilePath, debugInfo); + } + } + } + else { + assert.equal(dtsInfo.sourceMapFilePath, sourceMapPath, debugInfo); + } + } + + if (!first && (fn !== 1 || !freshMapInfo)) { + verifyDocumentPositionMapper({ + session, + dependencyMap, + documentPositionMapper, + equal: fn !== 1 || !freshDocumentMapper, + debugInfo + }); + } + sourceMapPath = dtsInfo && dtsInfo.sourceMapFilePath; + dependencyMap = dtsMapInfo; + documentPositionMapper = dependencyMap && dependencyMap.documentPositionMapper; + first = false; + } } - function verifyScenarioWithChangesWorker( - change: (host: TestServerHost, session: TestSession) => void, - afterActionDocumentPositionMapperNotEquals: true | undefined, - timeoutBeforeAction: boolean - ) { - const { host, session } = openTsFile(); + return { actionInfos, actionKey, dependencyMap, documentPositionMapper }; + } + + function verifyScriptInfoCollection( + session: TestSession, + host: TestServerHost, + verifiers: readonly DocumentPositionMapperVerifier[], + { dependencyMap, documentPositionMapper, actionInfos, actionKey }: VerifyAllFnActionResult + ) { + // Collecting at this point retains dependency.d.ts and map + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + + const { closedInfos, otherWatchedFiles } = last(actionInfos); + const debugInfo = `${actionKey} Collection`; + verifyInfosWithRandom( + session, + host, + openFiles(verifiers).map(f => f.path), + closedInfos, + otherWatchedFiles, + debugInfo + ); + verifyDocumentPositionMapper({ + session, + dependencyMap, + documentPositionMapper, + equal: true, + debugInfo + }); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles(verifiers), randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + function verifyScenarioAndScriptInfoCollection( + session: TestSession, + host: TestServerHost, + verifiers: readonly DocumentPositionMapperVerifier[], + actionKey: ActionKey, + noDts?: true + ) { + // Main scenario action + const result = verifyAllFnAction({ session, host, verifiers, actionKey }); + checkProject(session, verifiers, noDts); + verifyScriptInfoCollection(session, host, verifiers, result); + } + + function verifyScenarioWithChangesWorker( + { + scenarioName, + verifier, + withRefs, + change, + afterChangeActionKey + }: VerifyScenarioWithChanges, + timeoutBeforeAction: boolean, + ) { + it(scenarioName, () => { + const { host, session, verifiers } = openTsFile({ verifier, withRefs }); // Create DocumentPositionMapper - firstAction(session); - const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; - const documentPositionMapper = dependencyMap.documentPositionMapper; + firstAction(session, verifiers); + const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath); + const documentPositionMapper = dependencyMap && dependencyMap.documentPositionMapper; // change - change(host, session); + change(host, session, verifiers); if (timeoutBeforeAction) { host.runQueuedTimeoutCallbacks(); - checkProject(session); - verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); + checkProject(session, verifiers); + verifyDocumentPositionMapper({ + session, + dependencyMap, + documentPositionMapper, + equal: true, + debugInfo: "After change timeout" + }); } // action - verifyAllFnAction(session, host, afterActionDocumentPositionMapperNotEquals, dependencyMap, documentPositionMapper); - } - - function verifyScenarioWithChanges( - scenarioName: string, - change: (host: TestServerHost, session: TestSession) => void, - afterActionDocumentPositionMapperNotEquals?: true - ) { - describe(scenarioName, () => { - it("when timeout occurs before request", () => { - verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ true); - }); - - it("when timeout does not occur before request", () => { - verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ false); - }); - }); - } - - function verifyMainScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost) { - // Main scenario action - const { dependencyMap, documentPositionMapper } = verifyAllFnAction(session, host); - checkProject(session); - verifyInfos(session, host); - - // Collecting at this point retains dependency.d.ts and map - closeFilesForSession([randomFile], session); - openFilesForSession([randomFile], session); - verifyInfos(session, host); - verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); - - // Closing open file, removes dependencies too - closeFilesForSession([...openFiles, randomFile], session); - openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } - - function verifyMainScenarioAndScriptInfoCollectionWithNoMap(session: TestSession, host: TestServerHost, dependencyTsOKInScenario?: true) { - // Main scenario action - verifyAllFnActionWithNoMap(session, host, dependencyTsOKInScenario); - - // Collecting at this point retains dependency.d.ts and map watcher - closeFilesForSession([randomFile], session); - openFilesForSession([randomFile], session); - verifyInfosWhenNoMapFile(session, host); - - // Closing open file, removes dependencies too - closeFilesForSession([...openFiles, randomFile], session); - openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } - - function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) { - // Main scenario action - verifyAllFnActionWithNoDts(session, host, dependencyTsAndMapOk); - - // Collecting at this point retains dependency.d.ts and map watcher - closeFilesForSession([randomFile], session); - openFilesForSession([randomFile], session); - verifyInfosWhenNoDtsFile( + verifyAllFnAction({ session, host, - !!forEach(verifier, v => v.expectedProjectActualFiles.every(f => f.toLowerCase() !== dtsPath)) - ); + verifiers, + actionKey: afterChangeActionKey, + dependencyMap, + documentPositionMapper + }); + }); + } - // Closing open file, removes dependencies too - closeFilesForSession([...openFiles, randomFile], session); - openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } + interface VerifyScenarioWithChanges extends VerifierAndWithRefs { + scenarioName: string; + change: (host: TestServerHost, session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[]) => void; + afterChangeActionKey: ActionKey; + } + function verifyScenarioWithChanges(verify: VerifyScenarioWithChanges) { + describe("when timeout occurs before request", () => { + verifyScenarioWithChangesWorker(verify, /*timeoutBeforeAction*/ true); + }); - function verifyScenarioWhenFileNotPresent( - scenarioName: string, - fileLocation: string, - verifyScenarioAndScriptInfoCollection: (session: TestSession, host: TestServerHost, dependencyTsOk?: true) => void, - noDts?: true - ) { - describe(scenarioName, () => { - it(mainScenario, () => { - const { host, session } = openTsFile(host => host.deleteFile(fileLocation)); - checkProject(session, noDts); + describe("when timeout does not occur before request", () => { + verifyScenarioWithChangesWorker(verify, /*timeoutBeforeAction*/ false); + }); + } - verifyScenarioAndScriptInfoCollection(session, host); + interface VerifyScenarioWhenFileNotPresent extends VerifierAndWithRefs { + scenarioName: string; + fileLocation: string; + fileNotPresentKey: ActionKey; + fileCreatedKey: ActionKey; + fileDeletedKey: ActionKey; + noDts?: true; + } + function verifyScenarioWhenFileNotPresent({ + scenarioName, + verifier, + withRefs, + fileLocation, + fileNotPresentKey, + fileCreatedKey, + fileDeletedKey, + noDts + }: VerifyScenarioWhenFileNotPresent) { + describe(scenarioName, () => { + it("when file is not present", () => { + const { host, session, verifiers } = openTsFile({ + verifier, + withRefs, + onHostCreate: host => host.deleteFile(fileLocation) }); + checkProject(session, verifiers, noDts); - it("when file is created", () => { - let fileContents: string | undefined; - const { host, session } = openTsFile(host => { + verifyScenarioAndScriptInfoCollection(session, host, verifiers, fileNotPresentKey, noDts); + }); + + it("when file is created after actions on projects", () => { + let fileContents: string | undefined; + const { host, session, verifiers } = openTsFile({ + verifier, + withRefs, + onHostCreate: host => { fileContents = host.readFile(fileLocation); host.deleteFile(fileLocation); - }); - firstAction(session); - - host.writeFile(fileLocation, fileContents!); - verifyMainScenarioAndScriptInfoCollection(session, host); + } }); + firstAction(session, verifiers); - it("when file is deleted", () => { - const { host, session } = openTsFile(); - firstAction(session); - - // The dependency file is deleted when orphan files are collected - host.deleteFile(fileLocation); - verifyScenarioAndScriptInfoCollection(session, host, /*dependencyTsOk*/ true); - }); + host.writeFile(fileLocation, fileContents!); + verifyScenarioAndScriptInfoCollection(session, host, verifiers, fileCreatedKey); }); - } + it("when file is deleted after actions on the projects", () => { + const { host, session, verifiers } = openTsFile({ verifier, withRefs }); + firstAction(session, verifiers); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(fileLocation); + // Verify with deleted action key + verifyAllFnAction({ session, host, verifiers, actionKey: fileDeletedKey }); + checkProject(session, verifiers, noDts); + + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection( + session, + host, + verifiers, + { + actionInfos: getActionInfo(verifiers, fileNotPresentKey), + actionKey: fileNotPresentKey, + dependencyMap: undefined, + documentPositionMapper: undefined + } + ); + }); + }); + } + + function verifyScenarioWorker({ mainScenario, verifier }: VerifyScenario, withRefs: boolean, disableSourceOfProjectReferenceRedirect?: true) { it(mainScenario, () => { - const { host, session } = openTsFile(); - checkProject(session); - - verifyMainScenarioAndScriptInfoCollection(session, host); + const { host, session, verifiers } = openTsFile({ withRefs, disableSourceOfProjectReferenceRedirect, verifier }); + checkProject(session, verifiers); + verifyScenarioAndScriptInfoCollection(session, host, verifiers, "main"); }); // Edit - verifyScenarioWithChanges( - "when usage file changes, document position mapper doesnt change", - (_host, session) => openFiles.forEach( - (openFile, index) => session.executeCommandSeq({ + verifyScenarioWithChanges({ + scenarioName: "when usage file changes, document position mapper doesnt change", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + change: (_host, session, verifiers) => verifiers.forEach( + verifier => session.executeCommandSeq({ command: protocol.CommandTypes.Change, - arguments: { file: openFile.path, line: openFileLastLines[index], offset: 1, endLine: openFileLastLines[index], endOffset: 1, insertString: "const x = 10;" } + arguments: { + file: verifier.openFile.path, + line: verifier.openFileLastLine, + offset: 1, + endLine: verifier.openFileLastLine, + endOffset: 1, + insertString: "const x = 10;" + } }) - ) - ); + ), + afterChangeActionKey: "change" + }); // Edit dts to add new fn - verifyScenarioWithChanges( - "when dependency .d.ts changes, document position mapper doesnt change", - host => host.writeFile( + verifyScenarioWithChanges({ + scenarioName: "when dependency .d.ts changes, document position mapper doesnt change", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + change: host => host.writeFile( dtsLocation, host.readFile(dtsLocation)!.replace( "//# sourceMappingURL=FnS.d.ts.map", `export declare function fn6(): void; //# sourceMappingURL=FnS.d.ts.map` ) - ) - ); + ), + afterChangeActionKey: "dtsChange" + }); // Edit map file to represent added new line - verifyScenarioWithChanges( - "when dependency file's map changes", - host => host.writeFile( + verifyScenarioWithChanges({ + scenarioName: "when dependency file's map changes", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + change: host => host.writeFile( dtsMapLocation, `{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["../dependency/FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}` ), - /*afterActionDocumentPositionMapperNotEquals*/ true - ); + afterChangeActionKey: "mapChange" + }); - verifyScenarioWhenFileNotPresent( - "when map file is not present", - dtsMapLocation, - verifyMainScenarioAndScriptInfoCollectionWithNoMap - ); + verifyScenarioWhenFileNotPresent({ + scenarioName: "with depedency files map file", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + fileLocation: dtsMapLocation, + fileNotPresentKey: "noMap", + fileCreatedKey: "mapFileCreated", + fileDeletedKey: "mapFileDeleted" + }); - verifyScenarioWhenFileNotPresent( - "when .d.ts file is not present", - dtsLocation, - verifyMainScenarioAndScriptInfoCollectionWithNoDts, - /*noDts*/ true - ); + verifyScenarioWhenFileNotPresent({ + scenarioName: "with depedency .d.ts file", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + fileLocation: dtsLocation, + fileNotPresentKey: "noDts", + fileCreatedKey: "dtsFileCreated", + fileDeletedKey: "dtsFileDeleted", + noDts: true + }); + + if (withRefs && !disableSourceOfProjectReferenceRedirect) { + verifyScenarioWithChanges({ + scenarioName: "when defining project source changes", + verifier, + withRefs, + change: (host, session, verifiers) => { + // Make change, without rebuild of solution + if (contains(openFiles(verifiers), dependencyTs)) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } +`} + }); + } + else { + host.writeFile(dependencyTs.path, `function fooBar() { } +${dependencyTs.content}`); + } + }, + afterChangeActionKey: "dependencyChange" + }); + + it("when projects are not built", () => { + const host = createServerHost(files); + const session = createSession(host); + const verifiers = verifier(withRefs); + openFilesForSession([...openFiles(verifiers), randomFile], session); + verifyScenarioAndScriptInfoCollection(session, host, verifiers, "noBuild"); + }); + } } - function verifyScenarios(withRefs: boolean) { - describe(withRefs ? "when main tsconfig has project reference" : "when main tsconfig doesnt have project reference", () => { - const usageVerifier: DocumentPositionMapperVerifier = { - openFile: mainTs, - expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath], - actionGetter: gotoDefintinionFromMainTs, - openFileLastLine: 14 - }; - describe("from project that uses dependency", () => { - const closedInfos = withRefs ? - [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation] : - [dependencyTs.path, libFile.path, dtsPath, dtsMapLocation]; - verifyDocumentPositionMapperUpdates( - "can go to definition correctly", - [usageVerifier], - closedInfos, - withRefs - ); - }); - - const definingVerifier: DocumentPositionMapperVerifier = { - openFile: dependencyTs, - expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path], - actionGetter: renameFromDependencyTs, - openFileLastLine: 6, - }; - describe("from defining project", () => { - const closedInfos = [libFile.path, dtsLocation, dtsMapLocation]; - verifyDocumentPositionMapperUpdates( - "rename locations from dependency", - [definingVerifier], - closedInfos, - withRefs - ); - }); - - describe("when opening depedency and usage project", () => { - const closedInfos = withRefs ? - [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] : - [libFile.path, dtsPath, dtsMapLocation]; - verifyDocumentPositionMapperUpdates( - "goto Definition in usage and rename locations from defining project", - [usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }], - closedInfos, - withRefs - ); - }); + interface VerifyScenario { + mainScenario: string; + verifier: (withRefs: boolean) => readonly DocumentPositionMapperVerifier[]; + } + function verifyScenario(scenario: VerifyScenario) { + describe("when main tsconfig doesnt have project reference", () => { + verifyScenarioWorker(scenario, /*withRefs*/ false); + }); + describe("when main tsconfig has project reference", () => { + verifyScenarioWorker(scenario, /*withRefs*/ true); + }); + describe("when main tsconfig has but has disableSourceOfProjectReferenceRedirect", () => { + verifyScenarioWorker(scenario, /*withRefs*/ true); }); } - verifyScenarios(/*withRefs*/ false); - verifyScenarios(/*withRefs*/ true); + describe("from project that uses dependency", () => { + verifyScenario({ + mainScenario: "can go to definition correctly", + verifier: withRefs => [ + { + ...goToDefFromMainTsProjectInfoVerifier(withRefs), + main: () => ({ + action: goToDefFromMainTs, + closedInfos: withRefs ? + [dependencyTs.path, dependencyConfig.path, libFile.path] : + [dependencyTs.path, libFile.path, dtsPath, dtsMapLocation], + otherWatchedFiles: [mainConfig.path], + expectsDts: !withRefs, // Dts script info present only if no project reference + expectsMap: !withRefs // Map script info present only if no project reference + }), + change: "main", + dtsChange: "main", + mapChange: ["main", () => ({ + freshDocumentMapper: true + })], + noMap: withRefs ? + "main" : + ["main", main => ({ + action: goToDefFromMainTsWithNoMap, + // Because map is deleted, dts and dependency are released + closedInfos: removePath(main.closedInfos, dtsMapPath, dependencyTsPath), + // Watches deleted file + otherWatchedFiles: main.otherWatchedFiles.concat(dtsMapLocation), + expectsMap: false + })], + mapFileCreated: "main", + mapFileDeleted: withRefs ? + "main" : + ["noMap", noMap => ({ + // The script info for depedency is collected only after file open + closedInfos: noMap.closedInfos.concat(dependencyTs.path) + })], + noDts: withRefs ? + "main" : + ["main", main => ({ + action: goToDefFromMainTsWithNoDts, + // No dts, no map, no dependency + closedInfos: removePath(main.closedInfos, dtsPath, dtsMapPath, dependencyTsPath), + expectsDts: false, + expectsMap: false + })], + dtsFileCreated: "main", + dtsFileDeleted: withRefs ? + "main" : + ["noDts", noDts => ({ + // The script info for map is collected only after file open + closedInfos: noDts.closedInfos.concat(dependencyTs.path, dtsMapLocation), + expectsMap: true + })], + dependencyChange: ["main", () => ({ + action: goToDefFromMainTsWithDependencyChange, + })], + noBuild: "noDts" + } + ] + }); + }); + + describe("from defining project", () => { + verifyScenario({ + mainScenario: "rename locations from dependency", + verifier: () => [ + { + ...renameFromDependencyTsProjectInfoVerifier(), + main: () => ({ + action: renameFromDependencyTs, + closedInfos: [libFile.path, dtsLocation, dtsMapLocation], + otherWatchedFiles: [dependencyConfig.path], + expectsDts: true, + expectsMap: true + }), + change: "main", + dtsChange: "main", + mapChange: ["main", () => ({ + freshDocumentMapper: true + })], + noMap: ["main", main => ({ + // No map + closedInfos: removePath(main.closedInfos, dtsMapPath), + // watch map + otherWatchedFiles: [...main.otherWatchedFiles, dtsMapLocation], + expectsMap: false + })], + mapFileCreated: "main", + mapFileDeleted: "noMap", + noDts: ["main", main => ({ + // no dts or map since dts itself doesnt exist + closedInfos: removePath(main.closedInfos, dtsMapPath, dtsPath), + // watch deleted file + otherWatchedFiles: [...main.otherWatchedFiles, dtsLocation], + expectsDts: false, + expectsMap: false + })], + dtsFileCreated: "main", + dtsFileDeleted: ["noDts", noDts => ({ + // Map is collected after file open + closedInfos: noDts.closedInfos.concat(dtsMapLocation), + expectsMap: true + })], + dependencyChange: ["main", () => ({ + action: renameFromDependencyTsWithDependencyChange + })], + noBuild: "noDts" + } + ] + }); + }); + + describe("when opening depedency and usage project", () => { + verifyScenario({ + mainScenario: "goto Definition in usage and rename locations from defining project", + verifier: withRefs => [ + { + ...goToDefFromMainTsProjectInfoVerifier(withRefs), + main: () => ({ + action: goToDefFromMainTs, + // DependencyTs is open, so omit it from closed infos + closedInfos: withRefs ? + [dependencyConfig.path, libFile.path] : + [libFile.path, dtsPath, dtsMapLocation], + otherWatchedFiles: withRefs ? + [mainConfig.path] : // Its in closed info + [mainConfig.path, dependencyConfig.path], + expectsDts: !withRefs, // Dts script info present only if no project reference + expectsMap: !withRefs // Map script info present only if no project reference + }), + change: withRefs ? + ["main", main => ({ + // Because before this rename is done the closed info remains same as rename's main operation + closedInfos: main.closedInfos.concat(dtsLocation, dtsMapLocation), + expectsDts: true, + expectsMap: true + })] : + "main", + dtsChange: "change", + mapChange: "change", + noMap: withRefs ? + "main" : + ["main", main => ({ + action: goToDefFromMainTsWithNoMap, + closedInfos: removePath(main.closedInfos, dtsMapPath), + otherWatchedFiles: main.otherWatchedFiles.concat(dtsMapLocation), + expectsMap: false + })], + mapFileCreated: withRefs ? + ["main", main => ({ + // Because before this rename is done the closed info remains same as rename's main + closedInfos: main.closedInfos.concat(dtsLocation), + expectsDts: true, + // This operation doesnt need map so the map info path in dts is not refreshed + skipDtsMapCheck: withRefs + })] : + "main", + mapFileDeleted: withRefs ? + ["noMap", noMap => ({ + // Because before this rename is done the closed info remains same as rename's noMap operation + closedInfos: noMap.closedInfos.concat(dtsLocation), + expectsDts: true, + // This operation doesnt need map so the map info path in dts is not refreshed + skipDtsMapCheck: true + })] : + "noMap", + noDts: withRefs ? + "main" : + ["main", main => ({ + action: goToDefFromMainTsWithNoDts, + closedInfos: removePath(main.closedInfos, dtsMapPath, dtsPath), + expectsDts: false, + expectsMap: false + })], + dtsFileCreated: withRefs ? + ["main", main => ({ + // Since the project for dependency is not updated, the watcher from rename for dts still there + otherWatchedFiles: main.otherWatchedFiles.concat(dtsLocation) + })] : + "main", + dtsFileDeleted: ["noDts", noDts => ({ + // Map collection after file open + closedInfos: noDts.closedInfos.concat(dtsMapLocation), + expectsMap: true + })], + dependencyChange: ["change", () => ({ + action: goToDefFromMainTsWithDependencyChange, + })], + noBuild: "noDts" + }, + { + ...renameFromDependencyTsProjectInfoVerifier(), + main: () => ({ + action: renameFromDependencyTsWithBothProjectsOpen, + // DependencyTs is open, so omit it from closed infos + closedInfos: withRefs ? + [dependencyConfig.path, libFile.path, dtsLocation, dtsMapLocation] : + [libFile.path, dtsPath, dtsMapLocation], + otherWatchedFiles: withRefs ? + [mainConfig.path] : // Its in closed info + [mainConfig.path, dependencyConfig.path], + expectsDts: true, + expectsMap: true, + freshMapInfo: withRefs + }), + change: ["main", () => ({ + freshMapInfo: false + })], + dtsChange: "change", + mapChange: ["main", () => ({ + freshMapInfo: false, + freshDocumentMapper: withRefs + })], + noMap: ["main", main => ({ + action: withRefs ? + renameFromDependencyTsWithBothProjectsOpen : + renameFromDependencyTs, + closedInfos: removePath(main.closedInfos, dtsMapPath), + otherWatchedFiles: main.otherWatchedFiles.concat(dtsMapLocation), + expectsMap: false, + freshDocumentMapper: withRefs + })], + mapFileCreated: "main", + mapFileDeleted: "noMap", + noDts: ["change", change => ({ + action: withRefs ? + renameFromDependencyTsWithBothProjectsOpen : + renameFromDependencyTs, + closedInfos: removePath(change.closedInfos, dtsPath, dtsMapPath), + otherWatchedFiles: change.otherWatchedFiles.concat(dtsLocation), + expectsDts: false, + expectsMap: false + })], + dtsFileCreated: "main", + dtsFileDeleted: ["noDts", noDts => ({ + // Map collection after file open + closedInfos: noDts.closedInfos.concat(dtsMapLocation) , + expectsMap: true + })], + dependencyChange: ["change", () => ({ + action: renameFromDependencyTsWithBothProjectsOpenWithDependencyChange + })], + noBuild: "noDts" + } + ] + }); + }); }); it("reusing d.ts files from composite and non composite projects", () => { @@ -774,7 +1246,7 @@ fn5(); service.openClientFile(cTs.path); service.checkNumberOfProjects({ configuredProjects: 2 }); const projectC = service.configuredProjects.get(configC.path)!; - checkProjectActualFiles(projectC, [cTs.path, bDts.path, libFile.path, configC.path]); + checkProjectActualFiles(projectC, [cTs.path, bTs.path, libFile.path, configC.path]); // Now new project for project A tries to reuse b but there is no filesByName mapping for b's source location host.writeFile(a2Ts.path, `${a2Ts.content}export const y = 30;`); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index fb98e740ba..f7a65687f3 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2567,6 +2567,7 @@ declare namespace ts { emitDeclarationOnly?: boolean; declarationDir?: string; disableSizeLimit?: boolean; + disableSourceOfProjectReferenceRedirect?: boolean; downlevelIteration?: boolean; emitBOM?: boolean; emitDecoratorMetadata?: boolean; @@ -8529,7 +8530,6 @@ declare namespace ts.server { getGlobalProjectErrors(): readonly Diagnostic[]; getAllProjectErrors(): readonly Diagnostic[]; getLanguageService(ensureSynchronized?: boolean): LanguageService; - private shouldEmitFile; getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[]; /** * Returns true if emit was conducted @@ -8610,11 +8610,25 @@ declare namespace ts.server { private typeAcquisition; private directoriesWatchedForWildcards; readonly canonicalConfigFilePath: NormalizedPath; + private projectReferenceCallbacks; + private mapOfDeclarationDirectories; /** Ref count to the project when opened from external project */ private externalProjectRefCount; private projectErrors; private projectReferences; protected isInitialLoadPending: () => boolean; + /** + * This implementation of fileExists checks if the file being requested is + * .d.ts file for the referenced Project. + * If it is it returns true irrespective of whether that file exists on host + */ + fileExists(file: string): boolean; + /** + * This implementation of directoryExists checks if the directory being requested is + * directory of .d.ts file for the referenced Project. + * If it is it returns true irrespective of whether that directory exists on host + */ + directoryExists(path: string): boolean; /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise. diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index b0d81933fd..23fd01a86c 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2567,6 +2567,7 @@ declare namespace ts { emitDeclarationOnly?: boolean; declarationDir?: string; disableSizeLimit?: boolean; + disableSourceOfProjectReferenceRedirect?: boolean; downlevelIteration?: boolean; emitBOM?: boolean; emitDecoratorMetadata?: boolean; diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/disableSourceOfProjectReferenceRedirect/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/disableSourceOfProjectReferenceRedirect/tsconfig.json new file mode 100644 index 0000000000..c8b95e0909 --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/disableSourceOfProjectReferenceRedirect/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "disableSourceOfProjectReferenceRedirect": true + } +}