diff --git a/src/server/builder.ts b/src/server/builder.ts index 285ab9ece60..001bd708b2e 100644 --- a/src/server/builder.ts +++ b/src/server/builder.ts @@ -3,48 +3,45 @@ /// namespace ts.server { - export function shouldEmitFile(scriptInfo: ScriptInfo) { - return !scriptInfo.hasMixedContent; - } - export interface Builder { /** * This is the callback when file infos in the builder are updated */ - onProjectUpdateGraph(): void; - getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - /** - * @returns {boolean} whether the emit was conducted or not - */ - emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean; + onProgramUpdateGraph(program: Program): void; + getFilesAffectedBy(program: Program, path: Path): string[]; + emitFile(program: Program, path: Path): EmitOutput; clear(): void; } interface EmitHandler { - addScriptInfo(scriptInfo: ScriptInfo): void; + addScriptInfo(program: Program, sourceFile: SourceFile): void; removeScriptInfo(path: Path): void; - updateScriptInfo(scriptInfo: ScriptInfo): void; + updateScriptInfo(program: Program, sourceFile: SourceFile): void; /** * Gets the files affected by the script info which has updated shape from the known one */ - getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[]; + getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[]; } - export function createBuilder(project: Project): Builder { + export function createBuilder( + getCanonicalFileName: (fileName: string) => string, + getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean) => EmitOutput, + computeHash: (data: string) => string, + shouldEmitFile: (sourceFile: SourceFile) => boolean + ): Builder { let isModuleEmit: boolean | undefined; - let projectVersionForDependencyGraph: string; // Last checked shape signature for the file info let fileInfos: Map; let emitHandler: EmitHandler; return { - onProjectUpdateGraph, + onProgramUpdateGraph, getFilesAffectedBy, emitFile, clear }; - function createProjectGraph() { - const currentIsModuleEmit = project.getCompilerOptions().module !== ModuleKind.None; + function createProgramGraph(program: Program) { + const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None; if (isModuleEmit !== currentIsModuleEmit) { isModuleEmit = currentIsModuleEmit; emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); @@ -52,77 +49,55 @@ namespace ts.server { } fileInfos = mutateExistingMap( - fileInfos, arrayToMap(project.getScriptInfos(), info => info.path), - (_path, info) => { - emitHandler.addScriptInfo(info); + fileInfos, arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), + (_path, sourceFile) => { + emitHandler.addScriptInfo(program, sourceFile); return ""; }, (path: Path, _value) => emitHandler.removeScriptInfo(path), /*isSameValue*/ undefined, /*OnDeleteExistingMismatchValue*/ undefined, - (_prevValue, scriptInfo) => emitHandler.updateScriptInfo(scriptInfo) + (_prevValue, sourceFile) => emitHandler.updateScriptInfo(program, sourceFile) ); - projectVersionForDependencyGraph = project.getProjectVersion(); } - function ensureFileInfos() { + function ensureProgramGraph(program: Program) { if (!emitHandler) { - createProjectGraph(); + createProgramGraph(program); } - Debug.assert(projectVersionForDependencyGraph === project.getProjectVersion()); } - function onProjectUpdateGraph() { + function onProgramUpdateGraph(program: Program) { if (emitHandler) { - createProjectGraph(); + createProgramGraph(program); } } - function getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - ensureFileInfos(); + function getFilesAffectedBy(program: Program, path: Path): string[] { + ensureProgramGraph(program); - const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; - const path = scriptInfo.path; - if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(scriptInfo)) { + const sourceFile = program.getSourceFile(path); + const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; + if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(program, sourceFile)) { return singleFileResult; } - return emitHandler.getFilesAffectedByUpdatedShape(scriptInfo, singleFileResult); + return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult); } - /** - * @returns {boolean} whether the emit was conducted or not - */ - function emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { - ensureFileInfos(); - if (!fileInfos || !fileInfos.has(scriptInfo.path)) { - return false; + function emitFile(program: Program, path: Path): EmitOutput { + ensureProgramGraph(program); + if (!fileInfos || !fileInfos.has(path)) { + return { outputFiles: [], emitSkipped: true }; } - const { emitSkipped, outputFiles } = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ false); - if (!emitSkipped) { - const projectRootPath = project.getProjectRootPath(); - for (const outputFile of outputFiles) { - const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); - writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); - } - } - return !emitSkipped; + return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false); } function clear() { isModuleEmit = undefined; emitHandler = undefined; fileInfos = undefined; - projectVersionForDependencyGraph = undefined; - } - - function getSourceFile(path: Path) { - return project.getSourceFile(path); - } - - function getScriptInfo(path: Path) { - return project.projectService.getScriptInfoForPath(path); } function isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile: SourceFile) { @@ -147,12 +122,8 @@ namespace ts.server { /** * @return {boolean} indicates if the shape signature has changed since last update. */ - function updateShapeSignature(scriptInfo: ScriptInfo) { - const path = scriptInfo.path; - const sourceFile = getSourceFile(path); - if (!sourceFile) { - return true; - } + function updateShapeSignature(program: Program, sourceFile: SourceFile) { + const path = sourceFile.path; const prevSignature = fileInfos.get(path); let latestSignature = prevSignature; @@ -161,7 +132,7 @@ namespace ts.server { fileInfos.set(path, latestSignature); } else { - const emitOutput = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ true); + const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = computeHash(emitOutput.outputFiles[0].text); fileInfos.set(path, latestSignature); @@ -171,8 +142,67 @@ namespace ts.server { return !prevSignature || latestSignature !== prevSignature; } - function computeHash(text: string) { - return project.projectService.host.createHash(text); + /** + * Gets the referenced files for a file from the program + * @param program + * @param path + */ + function getReferencedFiles(program: Program, sourceFile: SourceFile): Map { + const referencedFiles = createMap(); + // We need to use a set here since the code can contain the same import twice, + // but that will only be one dependency. + // To avoid invernal conversion, the key of the referencedFiles map must be of type Path + if (sourceFile.imports && sourceFile.imports.length > 0) { + const checker: TypeChecker = program.getTypeChecker(); + for (const importName of sourceFile.imports) { + const symbol = checker.getSymbolAtLocation(importName); + if (symbol && symbol.declarations && symbol.declarations[0]) { + const declarationSourceFile = symbol.declarations[0].getSourceFile(); + if (declarationSourceFile) { + referencedFiles.set(declarationSourceFile.path, true); + } + } + } + } + + const sourceFileDirectory = getDirectoryPath(sourceFile.path); + // Handle triple slash references + if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { + for (const referencedFile of sourceFile.referencedFiles) { + const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); + referencedFiles.set(referencedPath, true); + } + } + + // Handle type reference directives + if (sourceFile.resolvedTypeReferenceDirectiveNames) { + sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { + if (!resolvedTypeReferenceDirective) { + return; + } + + const fileName = resolvedTypeReferenceDirective.resolvedFileName; + const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); + referencedFiles.set(typeFilePath, true); + }); + } + + return referencedFiles; + } + + /** + * Gets all the emittable files from the program + */ + function getAllEmittableFiles(program: Program) { + const defaultLibraryFileName = getDefaultLibFileName(program.getCompilerOptions()); + const sourceFiles = program.getSourceFiles(); + const result: string[] = []; + for (const sourceFile of sourceFiles) { + if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && shouldEmitFile(sourceFile)) { + result.push(sourceFile.fileName); + } + } + return result; } function noop() { } @@ -185,14 +215,14 @@ namespace ts.server { getFilesAffectedByUpdatedShape }; - function getFilesAffectedByUpdatedShape(_scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { - const options = project.getCompilerOptions(); + function getFilesAffectedByUpdatedShape(program: Program, _sourceFile: SourceFile, singleFileResult: string[]): string[] { + const options = program.getCompilerOptions(); // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, // so returning the file itself is good enough. if (options && (options.out || options.outFile)) { return singleFileResult; } - return project.getAllEmittableFiles(); + return getAllEmittableFiles(program); } } @@ -207,19 +237,22 @@ namespace ts.server { getFilesAffectedByUpdatedShape }; - function setReferences(path: Path, latestVersion: string, existingMap: Map) { - existingMap = mutateExistingMapWithNewSet(existingMap, project.getReferencedFiles(path), + function setReferences(program: Program, sourceFile: SourceFile, existingMap: Map) { + const path = sourceFile.path; + existingMap = mutateExistingMapWithNewSet( + existingMap, + getReferencedFiles(program, sourceFile), // Creating new Reference: Also add referenced by key => { referencedBy.add(key, path); return true; }, // Remove existing reference (key, _existingValue) => { referencedBy.remove(key, path); } ); references.set(path, existingMap); - scriptVersionForReferences.set(path, latestVersion); + scriptVersionForReferences.set(path, sourceFile.version); } - function addScriptInfo(info: ScriptInfo) { - setReferences(info.path, info.getLatestVersion(), undefined); + function addScriptInfo(program: Program, sourceFile: SourceFile) { + setReferences(program, sourceFile, undefined); } function removeScriptInfo(path: Path) { @@ -227,12 +260,11 @@ namespace ts.server { scriptVersionForReferences.delete(path); } - function updateScriptInfo(scriptInfo: ScriptInfo) { - const path = scriptInfo.path; + function updateScriptInfo(program: Program, sourceFile: SourceFile) { + const path = sourceFile.path; const lastUpdatedVersion = scriptVersionForReferences.get(path); - const latestVersion = scriptInfo.getLatestVersion(); - if (lastUpdatedVersion !== latestVersion) { - setReferences(path, latestVersion, references.get(path)); + if (lastUpdatedVersion !== sourceFile.version) { + setReferences(program, sourceFile, references.get(path)); } } @@ -240,14 +272,12 @@ namespace ts.server { return referencedBy.get(path) || []; } - function getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { - const path = scriptInfo.path; - const sourceFile = getSourceFile(path); + function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { if (!isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile)) { - return project.getAllEmittableFiles(); + return getAllEmittableFiles(program); } - const options = project.getCompilerOptions(); + const options = program.getCompilerOptions(); if (options && (options.isolatedModules || options.out || options.outFile)) { return singleFileResult; } @@ -256,22 +286,23 @@ namespace ts.server { // Because if so, its own referencedBy files need to be saved as well to make the // emitting result consistent with files on disk. - const fileNamesMap = createMap(); - const setFileName = (path: Path, scriptInfo: ScriptInfo) => { - fileNamesMap.set(path, scriptInfo && shouldEmitFile(scriptInfo) ? scriptInfo.fileName : undefined); + const fileNamesMap = createMap(); + const setFileName = (path: Path, sourceFile: SourceFile) => { + fileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); }; // Start with the paths this file was referenced by - setFileName(path, scriptInfo); + const path = sourceFile.path; + setFileName(path, sourceFile); const queue = getReferencedByPaths(path).slice(); while (queue.length > 0) { const currentPath = queue.pop(); if (!fileNamesMap.has(currentPath)) { - const currentScriptInfo = getScriptInfo(currentPath); - if (currentScriptInfo && updateShapeSignature(currentScriptInfo)) { + const currentSourceFile = program.getSourceFileByPath(currentPath); + if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { queue.push(...getReferencedByPaths(currentPath)); } - setFileName(currentPath, currentScriptInfo); + setFileName(currentPath, currentSourceFile); } } diff --git a/src/server/project.ts b/src/server/project.ts index d7c062178c4..851a26a5d4a 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -219,7 +219,6 @@ namespace ts.server { this.disableLanguageService(); } - this.builder = createBuilder(this); this.markAsDirty(); } @@ -247,12 +246,41 @@ namespace ts.server { return this.languageService; } + private ensureBuilder() { + if (!this.builder) { + this.builder = createBuilder( + this.projectService.toCanonicalFileName, + (_program, sourceFile, emitOnlyDts) => this.getFileEmitOutput(sourceFile, emitOnlyDts), + data => this.projectService.host.createHash(data), + sourceFile => !this.projectService.getScriptInfoForPath(sourceFile.path).hasMixedContent + ); + } + } + getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { if (!this.languageServiceEnabled) { return []; } this.updateGraph(); - return this.builder.getFilesAffectedBy(scriptInfo); + this.ensureBuilder(); + return this.builder.getFilesAffectedBy(this.program, scriptInfo.path); + } + + /** + * Returns true if emit was conducted + */ + emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { + this.ensureBuilder(); + const { emitSkipped, outputFiles } = this.builder.emitFile(this.program, scriptInfo.path); + if (!emitSkipped) { + const projectRootPath = this.getProjectRootPath(); + for (const outputFile of outputFiles) { + const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); + writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); + } + } + + return !emitSkipped; } getProjectVersion() { @@ -390,11 +418,11 @@ namespace ts.server { }); } - getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) { + private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean) { if (!this.languageServiceEnabled) { return undefined; } - return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles); + return this.getLanguageService().getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles); } getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) { @@ -453,21 +481,6 @@ namespace ts.server { return false; } - getAllEmittableFiles() { - if (!this.languageServiceEnabled) { - return []; - } - const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions); - const infos = this.getScriptInfos(); - const result: string[] = []; - for (const info of infos) { - if (getBaseFileName(info.fileName) !== defaultLibraryFileName && shouldEmitFile(info)) { - result.push(info.fileName); - } - } - return result; - } - containsScriptInfo(info: ScriptInfo): boolean { return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined); } @@ -594,11 +607,13 @@ namespace ts.server { // update builder only if language service is enabled // otherwise tell it to drop its internal state - if (this.languageServiceEnabled && this.compileOnSaveEnabled) { - this.builder.onProjectUpdateGraph(); - } - else { - this.builder.clear(); + if (this.builder) { + if (this.languageServiceEnabled && this.compileOnSaveEnabled) { + this.builder.onProgramUpdateGraph(this.program); + } + else { + this.builder.clear(); + } } if (hasChanges) { diff --git a/src/server/session.ts b/src/server/session.ts index 1e851d09615..31cb8c1e15d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1223,7 +1223,7 @@ namespace ts.server { return false; } const scriptInfo = project.getScriptInfo(file); - return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); + return project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); } private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems {