Skip to content

Commit 38b990b

Browse files
Merge pull request #5140 from Microsoft/iFeelPrettyErr
Stylize tsc's error messages with color and context
2 parents 8d60ed3 + ce24bcb commit 38b990b

File tree

6 files changed

+133
-27
lines changed

6 files changed

+133
-27
lines changed

src/compiler/commandLineParser.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference path="sys.ts"/>
22
/// <reference path="types.ts"/>
33
/// <reference path="core.ts"/>
4+
/// <reference path="diagnosticInformationMap.generated.ts"/>
45
/// <reference path="scanner.ts"/>
56

67
namespace ts {
@@ -150,6 +151,12 @@ namespace ts {
150151
type: "boolean",
151152
description: Diagnostics.Do_not_erase_const_enum_declarations_in_generated_code
152153
},
154+
{
155+
name: "pretty",
156+
paramType: Diagnostics.KIND,
157+
description: Diagnostics.Stylize_errors_and_messages_using_colors_if_available_experimental,
158+
type: "boolean"
159+
},
153160
{
154161
name: "project",
155162
shortName: "p",

src/compiler/diagnosticMessages.json

+16-10
Original file line numberDiff line numberDiff line change
@@ -2258,14 +2258,6 @@
22582258
"code": 6063
22592259
},
22602260

2261-
"Specify JSX code generation: 'preserve' or 'react'": {
2262-
"category": "Message",
2263-
"code": 6080
2264-
},
2265-
"Argument for '--jsx' must be 'preserve' or 'react'.": {
2266-
"category": "Message",
2267-
"code": 6081
2268-
},
22692261
"Enables experimental support for ES7 decorators.": {
22702262
"category": "Message",
22712263
"code": 6065
@@ -2293,8 +2285,8 @@
22932285
"Suppress excess property checks for object literals.": {
22942286
"category": "Message",
22952287
"code": 6072
2296-
},
2297-
"Disallow inconsistently-cased references to the same file.": {
2288+
},
2289+
"Stylize errors and messages using colors if available. (experimental)": {
22982290
"category": "Message",
22992291
"code": 6073
23002292
},
@@ -2314,6 +2306,20 @@
23142306
"category": "Message",
23152307
"code": 6077
23162308
},
2309+
"Disallow inconsistently-cased references to the same file.": {
2310+
"category": "Message",
2311+
"code": 6078
2312+
},
2313+
2314+
"Specify JSX code generation: 'preserve' or 'react'": {
2315+
"category": "Message",
2316+
"code": 6080
2317+
},
2318+
"Argument for '--jsx' must be 'preserve' or 'react'.": {
2319+
"category": "Message",
2320+
"code": 6081
2321+
},
2322+
23172323
"Variable '{0}' implicitly has an '{1}' type.": {
23182324
"category": "Error",
23192325
"code": 7005

src/compiler/sys.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ namespace ts {
198198
const _fs = require("fs");
199199
const _path = require("path");
200200
const _os = require("os");
201+
const _tty = require("tty");
201202

202203
// average async stat takes about 30 microseconds
203204
// set chunk size to do 30 files in < 1 millisecond
@@ -375,15 +376,7 @@ namespace ts {
375376
newLine: _os.EOL,
376377
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
377378
write(s: string): void {
378-
const buffer = new Buffer(s, "utf8");
379-
let offset = 0;
380-
let toWrite: number = buffer.length;
381-
let written = 0;
382-
// 1 is a standard descriptor for stdout
383-
while ((written = _fs.writeSync(1, buffer, offset, toWrite)) < toWrite) {
384-
offset += written;
385-
toWrite -= written;
386-
}
379+
process.stdout.write(s);
387380
},
388381
readFile,
389382
writeFile,

src/compiler/tsc.ts

+99-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ namespace ts {
66
fileWatcher?: FileWatcher;
77
}
88

9+
let reportDiagnostic = reportDiagnosticSimply;
10+
11+
function reportDiagnostics(diagnostics: Diagnostic[], host: CompilerHost): void {
12+
for (let diagnostic of diagnostics) {
13+
reportDiagnostic(diagnostic, host);
14+
}
15+
}
16+
917
/**
1018
* Checks to see if the locale is in the appropriate format,
1119
* and if it is, attempts to set the appropriate language.
@@ -81,16 +89,16 @@ namespace ts {
8189
return <string>diagnostic.messageText;
8290
}
8391

84-
function reportDiagnostic(diagnostic: Diagnostic, host: CompilerHost) {
92+
function reportDiagnosticSimply(diagnostic: Diagnostic, host: CompilerHost): void {
8593
let output = "";
8694

8795
if (diagnostic.file) {
88-
let loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
96+
const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
8997
const relativeFileName = host
9098
? convertToRelativePath(diagnostic.file.fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName))
9199
: diagnostic.file.fileName;
92100

93-
output += `${ relativeFileName }(${ loc.line + 1 },${ loc.character + 1 }): `;
101+
output += `${ diagnostic.file.fileName }(${ line + 1 },${ character + 1 }): `;
94102
}
95103

96104
let category = DiagnosticCategory[diagnostic.category].toLowerCase();
@@ -99,10 +107,91 @@ namespace ts {
99107
sys.write(output);
100108
}
101109

102-
function reportDiagnostics(diagnostics: Diagnostic[], host: CompilerHost) {
103-
for (let i = 0; i < diagnostics.length; i++) {
104-
reportDiagnostic(diagnostics[i], host);
110+
111+
const redForegroundEscapeSequence = "\u001b[91m";
112+
const yellowForegroundEscapeSequence = "\u001b[93m";
113+
const blueForegroundEscapeSequence = "\u001b[93m";
114+
const gutterStyleSequence = "\u001b[100;30m";
115+
const gutterSeparator = " ";
116+
const resetEscapeSequence = "\u001b[0m";
117+
const elipsis = "...";
118+
const categoryFormatMap: Map<string> = {
119+
[DiagnosticCategory.Warning]: yellowForegroundEscapeSequence,
120+
[DiagnosticCategory.Error]: redForegroundEscapeSequence,
121+
[DiagnosticCategory.Message]: blueForegroundEscapeSequence,
122+
};
123+
124+
function formatAndReset(text: string, formatStyle: string) {
125+
return formatStyle + text + resetEscapeSequence;
126+
}
127+
128+
function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: CompilerHost): void {
129+
let output = "";
130+
131+
if (diagnostic.file) {
132+
let { start, length, file } = diagnostic;
133+
let { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start);
134+
let { line: lastLine, character: lastLineChar } = getLineAndCharacterOfPosition(file, start + length);
135+
const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line;
136+
137+
let hasMoreThanFiveLines = (lastLine - firstLine) >= 4;
138+
let gutterWidth = (lastLine + 1 + "").length;
139+
if (hasMoreThanFiveLines) {
140+
gutterWidth = Math.max(elipsis.length, gutterWidth);
141+
}
142+
143+
output += sys.newLine;
144+
for (let i = firstLine; i <= lastLine; i++) {
145+
// If the error spans over 5 lines, we'll only show the first 2 and last 2 lines,
146+
// so we'll skip ahead to the second-to-last line.
147+
if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) {
148+
output += formatAndReset(padLeft(elipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + sys.newLine;
149+
i = lastLine - 1;
150+
}
151+
152+
let lineStart = getPositionOfLineAndCharacter(file, i, 0);
153+
let lineEnd = i < lastLineInFile ? getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length;
154+
let lineContent = file.text.slice(lineStart, lineEnd);
155+
lineContent = lineContent.replace(/\s+$/g, ""); // trim from end
156+
lineContent = lineContent.replace("\t", " "); // convert tabs to single spaces
157+
158+
// Output the gutter and the actual contents of the line.
159+
output += formatAndReset(padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator;
160+
output += lineContent + sys.newLine;
161+
162+
// Output the gutter and the error span for the line using tildes.
163+
output += formatAndReset(padLeft("", gutterWidth), gutterStyleSequence) + gutterSeparator;
164+
output += redForegroundEscapeSequence;
165+
if (i === firstLine) {
166+
// If we're on the last line, then limit it to the last character of the last line.
167+
// Otherwise, we'll just squiggle the rest of the line, giving 'slice' no end position.
168+
const lastCharForLine = i === lastLine ? lastLineChar : undefined;
169+
170+
output += lineContent.slice(0, firstLineChar).replace(/\S/g, " ");
171+
output += lineContent.slice(firstLineChar, lastCharForLine).replace(/./g, "~");
172+
}
173+
else if (i === lastLine) {
174+
output += lineContent.slice(0, lastLineChar).replace(/./g, "~");
175+
}
176+
else {
177+
// Squiggle the entire line.
178+
output += lineContent.replace(/./g, "~");
179+
}
180+
output += resetEscapeSequence;
181+
182+
output += sys.newLine;
183+
}
184+
185+
output += sys.newLine;
186+
output += `${ file.fileName }(${ firstLine + 1 },${ firstLineChar + 1 }): `;
105187
}
188+
189+
const categoryColor = categoryFormatMap[diagnostic.category];
190+
const category = DiagnosticCategory[diagnostic.category].toLowerCase();
191+
output += `${ formatAndReset(category, categoryColor) } TS${ diagnostic.code }: ${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }`;
192+
output += sys.newLine + sys.newLine;
193+
194+
sys.write(output);
106195
}
107196

108197
function reportWatchDiagnostic(diagnostic: Diagnostic) {
@@ -288,6 +377,10 @@ namespace ts {
288377
compilerHost.fileExists = cachedFileExists;
289378
}
290379

380+
if (compilerOptions.pretty) {
381+
reportDiagnostic = reportDiagnosticWithColorAndContext;
382+
}
383+
291384
// reset the cache of existing files
292385
cachedExistingFiles = {};
293386

src/compiler/tsconfig.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"core.ts",
1212
"sys.ts",
1313
"types.ts",
14+
"diagnosticInformationMap.generated.ts",
1415
"scanner.ts",
1516
"parser.ts",
1617
"utilities.ts",
@@ -19,7 +20,6 @@
1920
"emitter.ts",
2021
"program.ts",
2122
"commandLineParser.ts",
22-
"tsc.ts",
23-
"diagnosticInformationMap.generated.ts"
23+
"tsc.ts"
2424
]
2525
}

src/compiler/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -2087,6 +2087,7 @@ namespace ts {
20872087
outFile?: string;
20882088
outDir?: string;
20892089
preserveConstEnums?: boolean;
2090+
/* @internal */ pretty?: DiagnosticStyle;
20902091
project?: string;
20912092
removeComments?: boolean;
20922093
rootDir?: string;
@@ -2156,6 +2157,12 @@ namespace ts {
21562157
JSX,
21572158
}
21582159

2160+
/* @internal */
2161+
export const enum DiagnosticStyle {
2162+
Simple,
2163+
Pretty,
2164+
}
2165+
21592166
export interface ParsedCommandLine {
21602167
options: CompilerOptions;
21612168
fileNames: string[];

0 commit comments

Comments
 (0)