Skip to content

Commit 7e07a2b

Browse files
authored
Allow rich response for compile on save (#37462)
Fixes #30739
1 parent c513a4a commit 7e07a2b

File tree

11 files changed

+177
-26
lines changed

11 files changed

+177
-26
lines changed

src/compiler/builderState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ namespace ts {
33
export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean,
44
cancellationToken?: CancellationToken, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitOutput {
55
const outputFiles: OutputFile[] = [];
6-
const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit);
7-
return { outputFiles, emitSkipped: emitResult.emitSkipped, exportedModulesFromDeclarationEmit: emitResult.exportedModulesFromDeclarationEmit };
6+
const { emitSkipped, diagnostics, exportedModulesFromDeclarationEmit } = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit);
7+
return { outputFiles, emitSkipped, diagnostics, exportedModulesFromDeclarationEmit };
88

99
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) {
1010
outputFiles.push({ name: fileName, writeByteOrderMark, text });

src/compiler/builderStatePublic.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace ts {
22
export interface EmitOutput {
33
outputFiles: OutputFile[];
44
emitSkipped: boolean;
5+
/* @internal */ diagnostics: readonly Diagnostic[];
56
/* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit;
67
}
78

@@ -10,4 +11,4 @@ namespace ts {
1011
writeByteOrderMark: boolean;
1112
text: string;
1213
}
13-
}
14+
}

src/harness/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ namespace ts.server {
358358
getEmitOutput(file: string): EmitOutput {
359359
const request = this.processRequest<protocol.EmitOutputRequest>(protocol.CommandTypes.EmitOutput, { file });
360360
const response = this.processResponse<protocol.EmitOutputResponse>(request);
361-
return response.body;
361+
return response.body as EmitOutput;
362362
}
363363

364364
getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] {

src/server/project.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ namespace ts.server {
118118
return (watch as GeneratedFileWatcher).generatedFilePath !== undefined;
119119
}
120120

121+
/*@internal*/
122+
export interface EmitResult {
123+
emitSkipped: boolean;
124+
diagnostics: readonly Diagnostic[];
125+
}
126+
121127
export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
122128
private rootFiles: ScriptInfo[] = [];
123129
private rootFilesMap = createMap<ProjectRootFile>();
@@ -587,19 +593,19 @@ namespace ts.server {
587593
/**
588594
* Returns true if emit was conducted
589595
*/
590-
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
596+
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): EmitResult {
591597
if (!this.languageServiceEnabled || !this.shouldEmitFile(scriptInfo)) {
592-
return false;
598+
return { emitSkipped: true, diagnostics: emptyArray };
593599
}
594-
const { emitSkipped, outputFiles } = this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(scriptInfo.fileName);
600+
const { emitSkipped, diagnostics, outputFiles } = this.getLanguageService().getEmitOutput(scriptInfo.fileName);
595601
if (!emitSkipped) {
596602
for (const outputFile of outputFiles) {
597603
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, this.currentDirectory);
598604
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
599605
}
600606
}
601607

602-
return !emitSkipped;
608+
return { emitSkipped, diagnostics };
603609
}
604610

605611
enableLanguageService() {

src/server/protocol.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -857,10 +857,25 @@ namespace ts.server.protocol {
857857
}
858858

859859
/** @internal */
860-
export interface EmitOutputRequest extends FileRequest {}
860+
export interface EmitOutputRequest extends FileRequest {
861+
command: CommandTypes.EmitOutput;
862+
arguments: EmitOutputRequestArgs;
863+
}
864+
/** @internal */
865+
export interface EmitOutputRequestArgs extends FileRequestArgs {
866+
includeLinePosition?: boolean;
867+
/** if true - return response as object with emitSkipped and diagnostics */
868+
richResponse?: boolean;
869+
}
861870
/** @internal */
862871
export interface EmitOutputResponse extends Response {
863-
readonly body: EmitOutput;
872+
readonly body: EmitOutput | ts.EmitOutput;
873+
}
874+
/** @internal */
875+
export interface EmitOutput {
876+
outputFiles: OutputFile[];
877+
emitSkipped: boolean;
878+
diagnostics: Diagnostic[] | DiagnosticWithLinePosition[];
864879
}
865880

866881
/**
@@ -1808,6 +1823,18 @@ namespace ts.server.protocol {
18081823
* if true - then file should be recompiled even if it does not have any changes.
18091824
*/
18101825
forced?: boolean;
1826+
includeLinePosition?: boolean;
1827+
/** if true - return response as object with emitSkipped and diagnostics */
1828+
richResponse?: boolean;
1829+
}
1830+
1831+
export interface CompileOnSaveEmitFileResponse extends Response {
1832+
body: boolean | EmitResult;
1833+
}
1834+
1835+
export interface EmitResult {
1836+
emitSkipped: boolean;
1837+
diagnostics: Diagnostic[] | DiagnosticWithLinePosition[];
18111838
}
18121839

18131840
/**

src/server/session.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ namespace ts.server {
8585
return { line: lineAndCharacter.line + 1, offset: lineAndCharacter.character + 1 };
8686
}
8787

88-
function formatConfigFileDiag(diag: Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName;
89-
function formatConfigFileDiag(diag: Diagnostic, includeFileName: false): protocol.Diagnostic;
90-
function formatConfigFileDiag(diag: Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName {
88+
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName;
89+
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: false): protocol.Diagnostic;
90+
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName {
9191
const start = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start!)))!; // TODO: GH#18217
9292
const end = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start! + diag.length!)))!; // TODO: GH#18217
9393
const text = flattenDiagnosticMessageText(diag.messageText, "\n");
@@ -699,7 +699,7 @@ namespace ts.server {
699699
break;
700700
case ConfigFileDiagEvent:
701701
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
702-
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
702+
const bakedDiags = map(diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true));
703703
this.event<protocol.ConfigFileDiagnosticEventBody>({
704704
triggerFile,
705705
configFile,
@@ -998,7 +998,7 @@ namespace ts.server {
998998
this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnosticsForConfigFile) :
999999
map(
10001000
diagnosticsForConfigFile,
1001-
diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ false)
1001+
diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ false)
10021002
);
10031003
}
10041004

@@ -1009,8 +1009,10 @@ namespace ts.server {
10091009
length: d.length!, // TODO: GH#18217
10101010
category: diagnosticCategoryName(d),
10111011
code: d.code,
1012+
source: d.source,
10121013
startLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start!)))!, // TODO: GH#18217
10131014
endLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start! + d.length!)))!, // TODO: GH#18217
1015+
reportsUnnecessary: d.reportsUnnecessary,
10141016
relatedInformation: map(d.relatedInformation, formatRelatedInformation)
10151017
}));
10161018
}
@@ -1108,11 +1110,20 @@ namespace ts.server {
11081110
};
11091111
}
11101112

1111-
private getEmitOutput(args: protocol.FileRequestArgs): EmitOutput {
1113+
private getEmitOutput(args: protocol.EmitOutputRequestArgs): EmitOutput | protocol.EmitOutput {
11121114
const { file, project } = this.getFileAndProject(args);
1113-
return project.shouldEmitFile(project.getScriptInfo(file)) ?
1114-
project.getLanguageService().getEmitOutput(file) :
1115-
{ emitSkipped: true, outputFiles: [] };
1115+
if (!project.shouldEmitFile(project.getScriptInfo(file))) {
1116+
return { emitSkipped: true, outputFiles: [], diagnostics: [] };
1117+
}
1118+
const result = project.getLanguageService().getEmitOutput(file);
1119+
return args.richResponse ?
1120+
{
1121+
...result,
1122+
diagnostics: args.includeLinePosition ?
1123+
this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(result.diagnostics) :
1124+
result.diagnostics.map(d => formatDiagnosticToProtocol(d, /*includeFileName*/ true))
1125+
} :
1126+
result;
11161127
}
11171128

11181129
private mapDefinitionInfo(definitions: readonly DefinitionInfo[], project: Project): readonly protocol.FileSpanWithContext[] {
@@ -1708,16 +1719,24 @@ namespace ts.server {
17081719
);
17091720
}
17101721

1711-
private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs) {
1722+
private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs): boolean | protocol.EmitResult | EmitResult {
17121723
const { file, project } = this.getFileAndProject(args);
17131724
if (!project) {
17141725
Errors.ThrowNoProject();
17151726
}
17161727
if (!project.languageServiceEnabled) {
1717-
return false;
1728+
return args.richResponse ? { emitSkipped: true, diagnostics: [] } : false;
17181729
}
17191730
const scriptInfo = project.getScriptInfo(file)!;
1720-
return project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
1731+
const { emitSkipped, diagnostics } = project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
1732+
return args.richResponse ?
1733+
{
1734+
emitSkipped,
1735+
diagnostics: args.includeLinePosition ?
1736+
this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnostics) :
1737+
diagnostics.map(d => formatDiagnosticToProtocol(d, /*includeFileName*/ true))
1738+
} :
1739+
!emitSkipped;
17211740
}
17221741

17231742
private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems | undefined {

src/services/shims.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,10 @@ namespace ts {
10511051
public getEmitOutput(fileName: string): string {
10521052
return this.forwardJSONCall(
10531053
`getEmitOutput('${fileName}')`,
1054-
() => this.languageService.getEmitOutput(fileName)
1054+
() => {
1055+
const { diagnostics, ...rest } = this.languageService.getEmitOutput(fileName);
1056+
return { ...rest, diagnostics: this.realizeDiagnostics(diagnostics) };
1057+
}
10551058
);
10561059
}
10571060

src/testRunner/unittests/services/languageService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export function Component(x: Config): any;`
5858
),
5959
{
6060
emitSkipped: true,
61+
diagnostics: emptyArray,
6162
outputFiles: emptyArray,
6263
exportedModulesFromDeclarationEmit: undefined
6364
}
@@ -71,6 +72,7 @@ export function Component(x: Config): any;`
7172
),
7273
{
7374
emitSkipped: false,
75+
diagnostics: emptyArray,
7476
outputFiles: [{
7577
name: "foo.d.ts",
7678
text: "export {};\r\n",

src/testRunner/unittests/tsserver/compileOnSave.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,87 @@ namespace ts.projectSystem {
799799
assert.isTrue(stringContains(content, str), `Expected "${content}" to have "${str}"`);
800800
}
801801
});
802+
803+
describe("compile on save emit with and without richResponse", () => {
804+
it("without rich Response", () => {
805+
verify(/*richRepsonse*/ undefined);
806+
});
807+
it("with rich Response set to false", () => {
808+
verify(/*richRepsonse*/ false);
809+
});
810+
it("with rich Repsonse", () => {
811+
verify(/*richRepsonse*/ true);
812+
});
813+
814+
function verify(richResponse: boolean | undefined) {
815+
const config: File = {
816+
path: `${tscWatch.projectRoot}/tsconfig.json`,
817+
content: JSON.stringify({
818+
compileOnSave: true,
819+
compilerOptions: {
820+
outDir: "test",
821+
noEmitOnError: true,
822+
declaration: true,
823+
},
824+
exclude: ["node_modules"]
825+
})
826+
};
827+
const file1: File = {
828+
path: `${tscWatch.projectRoot}/file1.ts`,
829+
content: "const x = 1;"
830+
};
831+
const file2: File = {
832+
path: `${tscWatch.projectRoot}/file2.ts`,
833+
content: "const y = 2;"
834+
};
835+
const host = createServerHost([file1, file2, config, libFile]);
836+
const session = createSession(host);
837+
openFilesForSession([file1], session);
838+
839+
const affectedFileResponse = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
840+
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
841+
arguments: { file: file1.path }
842+
}).response as protocol.CompileOnSaveAffectedFileListSingleProject[];
843+
assert.deepEqual(affectedFileResponse, [
844+
{ fileNames: [file1.path, file2.path], projectFileName: config.path, projectUsesOutFile: false }
845+
]);
846+
const file1SaveResponse = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({
847+
command: protocol.CommandTypes.CompileOnSaveEmitFile,
848+
arguments: { file: file1.path, richResponse }
849+
}).response;
850+
if (richResponse) {
851+
assert.deepEqual(file1SaveResponse, { emitSkipped: false, diagnostics: emptyArray });
852+
}
853+
else {
854+
assert.isTrue(file1SaveResponse);
855+
}
856+
assert.strictEqual(host.readFile(`${tscWatch.projectRoot}/test/file1.d.ts`), "declare const x = 1;\n");
857+
const file2SaveResponse = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({
858+
command: protocol.CommandTypes.CompileOnSaveEmitFile,
859+
arguments: { file: file2.path, richResponse }
860+
}).response;
861+
if (richResponse) {
862+
assert.deepEqual(file2SaveResponse, {
863+
emitSkipped: true,
864+
diagnostics: [{
865+
start: undefined,
866+
end: undefined,
867+
fileName: undefined,
868+
text: formatStringFromArgs(Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.message, [`${tscWatch.projectRoot}/test/file1.d.ts`]),
869+
code: Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.code,
870+
category: diagnosticCategoryName(Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file),
871+
reportsUnnecessary: undefined,
872+
relatedInformation: undefined,
873+
source: undefined
874+
}]
875+
});
876+
}
877+
else {
878+
assert.isFalse(file2SaveResponse);
879+
}
880+
assert.isFalse(host.fileExists(`${tscWatch.projectRoot}/test/file2.d.ts`));
881+
}
882+
});
802883
});
803884

804885
describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => {

src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ ${appendJs}`
252252
text: content,
253253
writeByteOrderMark: false
254254
})),
255-
emitSkipped: false
255+
emitSkipped: false,
256+
diagnostics: emptyArray
256257
};
257258
}
258259

@@ -270,7 +271,8 @@ ${appendJs}`
270271
function noEmitOutput(): EmitOutput {
271272
return {
272273
emitSkipped: true,
273-
outputFiles: []
274+
outputFiles: [],
275+
diagnostics: emptyArray
274276
};
275277
}
276278

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7495,6 +7495,16 @@ declare namespace ts.server.protocol {
74957495
* if true - then file should be recompiled even if it does not have any changes.
74967496
*/
74977497
forced?: boolean;
7498+
includeLinePosition?: boolean;
7499+
/** if true - return response as object with emitSkipped and diagnostics */
7500+
richResponse?: boolean;
7501+
}
7502+
interface CompileOnSaveEmitFileResponse extends Response {
7503+
body: boolean | EmitResult;
7504+
}
7505+
interface EmitResult {
7506+
emitSkipped: boolean;
7507+
diagnostics: Diagnostic[] | DiagnosticWithLinePosition[];
74987508
}
74997509
/**
75007510
* Quickinfo request; value of command field is
@@ -8909,7 +8919,7 @@ declare namespace ts.server {
89098919
/**
89108920
* Returns true if emit was conducted
89118921
*/
8912-
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
8922+
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): EmitResult;
89138923
enableLanguageService(): void;
89148924
disableLanguageService(lastFileExceededProgramSize?: string): void;
89158925
getProjectName(): string;

0 commit comments

Comments
 (0)