Allow rich response for compile on save (#37462)

Fixes #30739
This commit is contained in:
Sheetal Nandi
2020-03-19 14:03:21 -07:00
committed by GitHub
parent c513a4adea
commit 7e07a2b5d1
11 changed files with 177 additions and 26 deletions

View File

@@ -3,8 +3,8 @@ namespace ts {
export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean,
cancellationToken?: CancellationToken, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitOutput {
const outputFiles: OutputFile[] = [];
const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit);
return { outputFiles, emitSkipped: emitResult.emitSkipped, exportedModulesFromDeclarationEmit: emitResult.exportedModulesFromDeclarationEmit };
const { emitSkipped, diagnostics, exportedModulesFromDeclarationEmit } = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit);
return { outputFiles, emitSkipped, diagnostics, exportedModulesFromDeclarationEmit };
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) {
outputFiles.push({ name: fileName, writeByteOrderMark, text });

View File

@@ -2,6 +2,7 @@ namespace ts {
export interface EmitOutput {
outputFiles: OutputFile[];
emitSkipped: boolean;
/* @internal */ diagnostics: readonly Diagnostic[];
/* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit;
}
@@ -10,4 +11,4 @@ namespace ts {
writeByteOrderMark: boolean;
text: string;
}
}
}

View File

@@ -358,7 +358,7 @@ namespace ts.server {
getEmitOutput(file: string): EmitOutput {
const request = this.processRequest<protocol.EmitOutputRequest>(protocol.CommandTypes.EmitOutput, { file });
const response = this.processResponse<protocol.EmitOutputResponse>(request);
return response.body;
return response.body as EmitOutput;
}
getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] {

View File

@@ -118,6 +118,12 @@ namespace ts.server {
return (watch as GeneratedFileWatcher).generatedFilePath !== undefined;
}
/*@internal*/
export interface EmitResult {
emitSkipped: boolean;
diagnostics: readonly Diagnostic[];
}
export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap = createMap<ProjectRootFile>();
@@ -587,11 +593,11 @@ namespace ts.server {
/**
* Returns true if emit was conducted
*/
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): EmitResult {
if (!this.languageServiceEnabled || !this.shouldEmitFile(scriptInfo)) {
return false;
return { emitSkipped: true, diagnostics: emptyArray };
}
const { emitSkipped, outputFiles } = this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(scriptInfo.fileName);
const { emitSkipped, diagnostics, outputFiles } = this.getLanguageService().getEmitOutput(scriptInfo.fileName);
if (!emitSkipped) {
for (const outputFile of outputFiles) {
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, this.currentDirectory);
@@ -599,7 +605,7 @@ namespace ts.server {
}
}
return !emitSkipped;
return { emitSkipped, diagnostics };
}
enableLanguageService() {

View File

@@ -857,10 +857,25 @@ namespace ts.server.protocol {
}
/** @internal */
export interface EmitOutputRequest extends FileRequest {}
export interface EmitOutputRequest extends FileRequest {
command: CommandTypes.EmitOutput;
arguments: EmitOutputRequestArgs;
}
/** @internal */
export interface EmitOutputRequestArgs extends FileRequestArgs {
includeLinePosition?: boolean;
/** if true - return response as object with emitSkipped and diagnostics */
richResponse?: boolean;
}
/** @internal */
export interface EmitOutputResponse extends Response {
readonly body: EmitOutput;
readonly body: EmitOutput | ts.EmitOutput;
}
/** @internal */
export interface EmitOutput {
outputFiles: OutputFile[];
emitSkipped: boolean;
diagnostics: Diagnostic[] | DiagnosticWithLinePosition[];
}
/**
@@ -1808,6 +1823,18 @@ namespace ts.server.protocol {
* if true - then file should be recompiled even if it does not have any changes.
*/
forced?: boolean;
includeLinePosition?: boolean;
/** if true - return response as object with emitSkipped and diagnostics */
richResponse?: boolean;
}
export interface CompileOnSaveEmitFileResponse extends Response {
body: boolean | EmitResult;
}
export interface EmitResult {
emitSkipped: boolean;
diagnostics: Diagnostic[] | DiagnosticWithLinePosition[];
}
/**

View File

@@ -85,9 +85,9 @@ namespace ts.server {
return { line: lineAndCharacter.line + 1, offset: lineAndCharacter.character + 1 };
}
function formatConfigFileDiag(diag: Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName;
function formatConfigFileDiag(diag: Diagnostic, includeFileName: false): protocol.Diagnostic;
function formatConfigFileDiag(diag: Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName {
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName;
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: false): protocol.Diagnostic;
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName {
const start = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start!)))!; // TODO: GH#18217
const end = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start! + diag.length!)))!; // TODO: GH#18217
const text = flattenDiagnosticMessageText(diag.messageText, "\n");
@@ -699,7 +699,7 @@ namespace ts.server {
break;
case ConfigFileDiagEvent:
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
const bakedDiags = map(diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true));
this.event<protocol.ConfigFileDiagnosticEventBody>({
triggerFile,
configFile,
@@ -998,7 +998,7 @@ namespace ts.server {
this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnosticsForConfigFile) :
map(
diagnosticsForConfigFile,
diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ false)
diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ false)
);
}
@@ -1009,8 +1009,10 @@ namespace ts.server {
length: d.length!, // TODO: GH#18217
category: diagnosticCategoryName(d),
code: d.code,
source: d.source,
startLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start!)))!, // TODO: GH#18217
endLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start! + d.length!)))!, // TODO: GH#18217
reportsUnnecessary: d.reportsUnnecessary,
relatedInformation: map(d.relatedInformation, formatRelatedInformation)
}));
}
@@ -1108,11 +1110,20 @@ namespace ts.server {
};
}
private getEmitOutput(args: protocol.FileRequestArgs): EmitOutput {
private getEmitOutput(args: protocol.EmitOutputRequestArgs): EmitOutput | protocol.EmitOutput {
const { file, project } = this.getFileAndProject(args);
return project.shouldEmitFile(project.getScriptInfo(file)) ?
project.getLanguageService().getEmitOutput(file) :
{ emitSkipped: true, outputFiles: [] };
if (!project.shouldEmitFile(project.getScriptInfo(file))) {
return { emitSkipped: true, outputFiles: [], diagnostics: [] };
}
const result = project.getLanguageService().getEmitOutput(file);
return args.richResponse ?
{
...result,
diagnostics: args.includeLinePosition ?
this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(result.diagnostics) :
result.diagnostics.map(d => formatDiagnosticToProtocol(d, /*includeFileName*/ true))
} :
result;
}
private mapDefinitionInfo(definitions: readonly DefinitionInfo[], project: Project): readonly protocol.FileSpanWithContext[] {
@@ -1708,16 +1719,24 @@ namespace ts.server {
);
}
private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs) {
private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs): boolean | protocol.EmitResult | EmitResult {
const { file, project } = this.getFileAndProject(args);
if (!project) {
Errors.ThrowNoProject();
}
if (!project.languageServiceEnabled) {
return false;
return args.richResponse ? { emitSkipped: true, diagnostics: [] } : false;
}
const scriptInfo = project.getScriptInfo(file)!;
return project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
const { emitSkipped, diagnostics } = project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
return args.richResponse ?
{
emitSkipped,
diagnostics: args.includeLinePosition ?
this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnostics) :
diagnostics.map(d => formatDiagnosticToProtocol(d, /*includeFileName*/ true))
} :
!emitSkipped;
}
private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems | undefined {

View File

@@ -1051,7 +1051,10 @@ namespace ts {
public getEmitOutput(fileName: string): string {
return this.forwardJSONCall(
`getEmitOutput('${fileName}')`,
() => this.languageService.getEmitOutput(fileName)
() => {
const { diagnostics, ...rest } = this.languageService.getEmitOutput(fileName);
return { ...rest, diagnostics: this.realizeDiagnostics(diagnostics) };
}
);
}

View File

@@ -58,6 +58,7 @@ export function Component(x: Config): any;`
),
{
emitSkipped: true,
diagnostics: emptyArray,
outputFiles: emptyArray,
exportedModulesFromDeclarationEmit: undefined
}
@@ -71,6 +72,7 @@ export function Component(x: Config): any;`
),
{
emitSkipped: false,
diagnostics: emptyArray,
outputFiles: [{
name: "foo.d.ts",
text: "export {};\r\n",

View File

@@ -799,6 +799,87 @@ namespace ts.projectSystem {
assert.isTrue(stringContains(content, str), `Expected "${content}" to have "${str}"`);
}
});
describe("compile on save emit with and without richResponse", () => {
it("without rich Response", () => {
verify(/*richRepsonse*/ undefined);
});
it("with rich Response set to false", () => {
verify(/*richRepsonse*/ false);
});
it("with rich Repsonse", () => {
verify(/*richRepsonse*/ true);
});
function verify(richResponse: boolean | undefined) {
const config: File = {
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: JSON.stringify({
compileOnSave: true,
compilerOptions: {
outDir: "test",
noEmitOnError: true,
declaration: true,
},
exclude: ["node_modules"]
})
};
const file1: File = {
path: `${tscWatch.projectRoot}/file1.ts`,
content: "const x = 1;"
};
const file2: File = {
path: `${tscWatch.projectRoot}/file2.ts`,
content: "const y = 2;"
};
const host = createServerHost([file1, file2, config, libFile]);
const session = createSession(host);
openFilesForSession([file1], session);
const affectedFileResponse = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
arguments: { file: file1.path }
}).response as protocol.CompileOnSaveAffectedFileListSingleProject[];
assert.deepEqual(affectedFileResponse, [
{ fileNames: [file1.path, file2.path], projectFileName: config.path, projectUsesOutFile: false }
]);
const file1SaveResponse = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({
command: protocol.CommandTypes.CompileOnSaveEmitFile,
arguments: { file: file1.path, richResponse }
}).response;
if (richResponse) {
assert.deepEqual(file1SaveResponse, { emitSkipped: false, diagnostics: emptyArray });
}
else {
assert.isTrue(file1SaveResponse);
}
assert.strictEqual(host.readFile(`${tscWatch.projectRoot}/test/file1.d.ts`), "declare const x = 1;\n");
const file2SaveResponse = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({
command: protocol.CommandTypes.CompileOnSaveEmitFile,
arguments: { file: file2.path, richResponse }
}).response;
if (richResponse) {
assert.deepEqual(file2SaveResponse, {
emitSkipped: true,
diagnostics: [{
start: undefined,
end: undefined,
fileName: undefined,
text: formatStringFromArgs(Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.message, [`${tscWatch.projectRoot}/test/file1.d.ts`]),
code: Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.code,
category: diagnosticCategoryName(Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file),
reportsUnnecessary: undefined,
relatedInformation: undefined,
source: undefined
}]
});
}
else {
assert.isFalse(file2SaveResponse);
}
assert.isFalse(host.fileExists(`${tscWatch.projectRoot}/test/file2.d.ts`));
}
});
});
describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => {

View File

@@ -252,7 +252,8 @@ ${appendJs}`
text: content,
writeByteOrderMark: false
})),
emitSkipped: false
emitSkipped: false,
diagnostics: emptyArray
};
}
@@ -270,7 +271,8 @@ ${appendJs}`
function noEmitOutput(): EmitOutput {
return {
emitSkipped: true,
outputFiles: []
outputFiles: [],
diagnostics: emptyArray
};
}