OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library analyzer_cli.src.error_formatter; | 5 library analyzer_cli.src.error_formatter; |
6 | 6 |
7 import 'package:analyzer/error/error.dart'; | 7 import 'package:analyzer/error/error.dart'; |
8 import 'package:analyzer/src/generated/engine.dart'; | 8 import 'package:analyzer/src/generated/engine.dart'; |
9 import 'package:analyzer/src/generated/source.dart'; | 9 import 'package:analyzer/src/generated/source.dart'; |
10 import 'package:analyzer_cli/src/ansi.dart'; | 10 import 'package:analyzer_cli/src/ansi.dart'; |
11 import 'package:analyzer_cli/src/options.dart'; | 11 import 'package:analyzer_cli/src/options.dart'; |
12 import 'package:path/path.dart' as path; | 12 import 'package:path/path.dart' as path; |
13 | 13 |
| 14 final Map<String, int> _severityCompare = { |
| 15 'error': 5, |
| 16 'warning': 4, |
| 17 'info': 3, |
| 18 'lint': 2, |
| 19 'hint': 1, |
| 20 }; |
| 21 |
| 22 String _pluralize(String word, int count) => count == 1 ? word : word + "s"; |
| 23 |
| 24 /// Given an absolute path, return a relative path if the file is contained in |
| 25 /// the current directory; return the original path otherwise. |
| 26 String _relative(String file) { |
| 27 return file.startsWith(path.current) ? path.relative(file) : file; |
| 28 } |
| 29 |
14 /// Returns the given error's severity. | 30 /// Returns the given error's severity. |
15 ErrorSeverity _severityIdentity(AnalysisError error) => | 31 ErrorSeverity _severityIdentity(AnalysisError error) => |
16 error.errorCode.errorSeverity; | 32 error.errorCode.errorSeverity; |
17 | 33 |
18 String _pluralize(String word, int count) => count == 1 ? word : word + "s"; | |
19 | |
20 /// Returns desired severity for the given [error] (or `null` if it's to be | 34 /// Returns desired severity for the given [error] (or `null` if it's to be |
21 /// suppressed). | 35 /// suppressed). |
22 typedef ErrorSeverity SeverityProcessor(AnalysisError error); | 36 typedef ErrorSeverity SeverityProcessor(AnalysisError error); |
23 | 37 |
24 /// Analysis statistics counter. | 38 /// Analysis statistics counter. |
25 class AnalysisStats { | 39 class AnalysisStats { |
26 /// The total number of diagnostics sent to [formatErrors]. | 40 /// The total number of diagnostics sent to [formatErrors]. |
27 int unfilteredCount = 0; | 41 int unfilteredCount = 0; |
28 | 42 |
29 int errorCount = 0; | 43 int errorCount = 0; |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
81 hasContent = true; | 95 hasContent = true; |
82 } | 96 } |
83 if (hasContent) { | 97 if (hasContent) { |
84 out.writeln(" found."); | 98 out.writeln(" found."); |
85 } else { | 99 } else { |
86 out.writeln("No issues found!"); | 100 out.writeln("No issues found!"); |
87 } | 101 } |
88 } | 102 } |
89 } | 103 } |
90 | 104 |
| 105 /// An [AnalysisError] with line and column information. |
| 106 class CLIError implements Comparable<CLIError> { |
| 107 final String severity; |
| 108 final String sourcePath; |
| 109 final int offset; |
| 110 final int line; |
| 111 final int column; |
| 112 final String message; |
| 113 final String errorCode; |
| 114 final String correction; |
| 115 |
| 116 CLIError({ |
| 117 this.severity, |
| 118 this.sourcePath, |
| 119 this.offset, |
| 120 this.line, |
| 121 this.column, |
| 122 this.message, |
| 123 this.errorCode, |
| 124 this.correction, |
| 125 }); |
| 126 |
| 127 @override |
| 128 int get hashCode => |
| 129 severity.hashCode ^ sourcePath.hashCode ^ errorCode.hashCode ^ offset; |
| 130 bool get isError => severity == 'error'; |
| 131 bool get isHint => severity == 'hint'; |
| 132 bool get isLint => severity == 'lint'; |
| 133 |
| 134 bool get isWarning => severity == 'warning'; |
| 135 |
| 136 @override |
| 137 bool operator ==(other) { |
| 138 if (other is! CLIError) return false; |
| 139 |
| 140 return severity == other.severity && |
| 141 sourcePath == other.sourcePath && |
| 142 errorCode == other.errorCode && |
| 143 offset == other.offset; |
| 144 } |
| 145 |
| 146 @override |
| 147 int compareTo(CLIError other) { |
| 148 // severity |
| 149 int compare = |
| 150 _severityCompare[other.severity] - _severityCompare[this.severity]; |
| 151 if (compare != 0) return compare; |
| 152 |
| 153 // path |
| 154 compare = Comparable.compare( |
| 155 this.sourcePath.toLowerCase(), other.sourcePath.toLowerCase()); |
| 156 if (compare != 0) return compare; |
| 157 |
| 158 // offset |
| 159 return this.offset - other.offset; |
| 160 } |
| 161 } |
| 162 |
91 /// Helper for formatting [AnalysisError]s. | 163 /// Helper for formatting [AnalysisError]s. |
92 /// | 164 /// |
93 /// The two format options are a user consumable format and a machine consumable | 165 /// The two format options are a user consumable format and a machine consumable |
94 /// format. | 166 /// format. |
95 abstract class ErrorFormatter { | 167 abstract class ErrorFormatter { |
96 final StringSink out; | 168 final StringSink out; |
97 final CommandLineOptions options; | 169 final CommandLineOptions options; |
98 final AnalysisStats stats; | 170 final AnalysisStats stats; |
99 SeverityProcessor _severityProcessor; | 171 SeverityProcessor _severityProcessor; |
100 | 172 |
101 ErrorFormatter(this.out, this.options, this.stats, | 173 ErrorFormatter(this.out, this.options, this.stats, |
102 {SeverityProcessor severityProcessor}) { | 174 {SeverityProcessor severityProcessor}) { |
103 _severityProcessor = | 175 _severityProcessor = |
104 severityProcessor == null ? _severityIdentity : severityProcessor; | 176 severityProcessor == null ? _severityIdentity : severityProcessor; |
105 } | 177 } |
106 | 178 |
107 /// Compute the severity for this [error] or `null` if this error should be | 179 /// Call to write any batched up errors from [formatErrors]. |
108 /// filtered. | 180 void flush(); |
109 ErrorSeverity _computeSeverity(AnalysisError error) => | 181 |
110 _severityProcessor(error); | 182 void formatError( |
| 183 Map<AnalysisError, LineInfo> errorToLine, AnalysisError error); |
111 | 184 |
112 void formatErrors(List<AnalysisErrorInfo> errorInfos) { | 185 void formatErrors(List<AnalysisErrorInfo> errorInfos) { |
113 stats.unfilteredCount += errorInfos.length; | 186 stats.unfilteredCount += errorInfos.length; |
114 | 187 |
115 List<AnalysisError> errors = new List<AnalysisError>(); | 188 List<AnalysisError> errors = new List<AnalysisError>(); |
116 Map<AnalysisError, LineInfo> errorToLine = | 189 Map<AnalysisError, LineInfo> errorToLine = |
117 new Map<AnalysisError, LineInfo>(); | 190 new Map<AnalysisError, LineInfo>(); |
118 for (AnalysisErrorInfo errorInfo in errorInfos) { | 191 for (AnalysisErrorInfo errorInfo in errorInfos) { |
119 for (AnalysisError error in errorInfo.errors) { | 192 for (AnalysisError error in errorInfo.errors) { |
120 if (_computeSeverity(error) != null) { | 193 if (_computeSeverity(error) != null) { |
121 errors.add(error); | 194 errors.add(error); |
122 errorToLine[error] = errorInfo.lineInfo; | 195 errorToLine[error] = errorInfo.lineInfo; |
123 } | 196 } |
124 } | 197 } |
125 } | 198 } |
126 | 199 |
127 for (AnalysisError error in errors) { | 200 for (AnalysisError error in errors) { |
128 formatError(errorToLine, error); | 201 formatError(errorToLine, error); |
129 } | 202 } |
130 } | 203 } |
131 | 204 |
132 void formatError( | 205 /// Compute the severity for this [error] or `null` if this error should be |
133 Map<AnalysisError, LineInfo> errorToLine, AnalysisError error); | 206 /// filtered. |
134 | 207 ErrorSeverity _computeSeverity(AnalysisError error) => |
135 /// Call to write any batched up errors from [formatErrors]. | 208 _severityProcessor(error); |
136 void flush(); | |
137 } | |
138 | |
139 class MachineErrorFormatter extends ErrorFormatter { | |
140 static final int _pipeCodeUnit = '|'.codeUnitAt(0); | |
141 static final int _slashCodeUnit = '\\'.codeUnitAt(0); | |
142 static final int _newline = '\n'.codeUnitAt(0); | |
143 static final int _return = '\r'.codeUnitAt(0); | |
144 | |
145 MachineErrorFormatter( | |
146 StringSink out, CommandLineOptions options, AnalysisStats stats, | |
147 {SeverityProcessor severityProcessor}) | |
148 : super(out, options, stats, severityProcessor: severityProcessor); | |
149 | |
150 void formatError( | |
151 Map<AnalysisError, LineInfo> errorToLine, AnalysisError error) { | |
152 Source source = error.source; | |
153 LineInfo_Location location = errorToLine[error].getLocation(error.offset); | |
154 int length = error.length; | |
155 | |
156 ErrorSeverity severity = _severityProcessor(error); | |
157 | |
158 if (severity == ErrorSeverity.ERROR) { | |
159 stats.errorCount++; | |
160 } else if (severity == ErrorSeverity.WARNING) { | |
161 stats.warnCount++; | |
162 } else if (error.errorCode.type == ErrorType.HINT) { | |
163 stats.hintCount++; | |
164 } else if (error.errorCode.type == ErrorType.LINT) { | |
165 stats.lintCount++; | |
166 } | |
167 | |
168 out.write(severity); | |
169 out.write('|'); | |
170 out.write(error.errorCode.type); | |
171 out.write('|'); | |
172 out.write(error.errorCode.name); | |
173 out.write('|'); | |
174 out.write(_escapeForMachineMode(source.fullName)); | |
175 out.write('|'); | |
176 out.write(location.lineNumber); | |
177 out.write('|'); | |
178 out.write(location.columnNumber); | |
179 out.write('|'); | |
180 out.write(length); | |
181 out.write('|'); | |
182 out.write(_escapeForMachineMode(error.message)); | |
183 out.writeln(); | |
184 } | |
185 | |
186 static String _escapeForMachineMode(String input) { | |
187 StringBuffer result = new StringBuffer(); | |
188 for (int c in input.codeUnits) { | |
189 if (c == _newline) { | |
190 result.write(r'\n'); | |
191 } else if (c == _return) { | |
192 result.write(r'\r'); | |
193 } else { | |
194 if (c == _slashCodeUnit || c == _pipeCodeUnit) { | |
195 result.write('\\'); | |
196 } | |
197 result.writeCharCode(c); | |
198 } | |
199 } | |
200 return result.toString(); | |
201 } | |
202 | |
203 void flush() {} | |
204 } | 209 } |
205 | 210 |
206 class HumanErrorFormatter extends ErrorFormatter { | 211 class HumanErrorFormatter extends ErrorFormatter { |
207 AnsiLogger ansi; | 212 AnsiLogger ansi; |
208 | 213 |
209 // This is a Set in order to de-dup CLI errors. | 214 // This is a Set in order to de-dup CLI errors. |
210 Set<CLIError> batchedErrors = new Set(); | 215 Set<CLIError> batchedErrors = new Set(); |
211 | 216 |
212 HumanErrorFormatter( | 217 HumanErrorFormatter( |
213 StringSink out, CommandLineOptions options, AnalysisStats stats, | 218 StringSink out, CommandLineOptions options, AnalysisStats stats, |
214 {SeverityProcessor severityProcessor}) | 219 {SeverityProcessor severityProcessor}) |
215 : super(out, options, stats, severityProcessor: severityProcessor) { | 220 : super(out, options, stats, severityProcessor: severityProcessor) { |
216 ansi = new AnsiLogger(this.options.color); | 221 ansi = new AnsiLogger(this.options.color); |
217 } | 222 } |
218 | 223 |
| 224 void flush() { |
| 225 // sort |
| 226 List<CLIError> sortedErrors = batchedErrors.toList()..sort(); |
| 227 |
| 228 // print |
| 229 for (CLIError error in sortedErrors) { |
| 230 if (error.isError) { |
| 231 stats.errorCount++; |
| 232 } else if (error.isWarning) { |
| 233 stats.warnCount++; |
| 234 } else if (error.isLint) { |
| 235 stats.lintCount++; |
| 236 } else if (error.isHint) { |
| 237 stats.hintCount++; |
| 238 } |
| 239 |
| 240 // warning • 'foo' is not a bar at lib/foo.dart:1:2 • foo_warning |
| 241 String issueColor = (error.isError == ErrorSeverity.ERROR || |
| 242 error.isWarning == ErrorSeverity.WARNING) |
| 243 ? ansi.red |
| 244 : ''; |
| 245 out.write(' $issueColor${error.severity}${ansi.none} ' |
| 246 '${ansi.bullet} ${ansi.bold}${error.message}${ansi.none} '); |
| 247 out.write('at ${error.sourcePath}'); |
| 248 out.write(':${error.line}:${error.column} '); |
| 249 out.write('${ansi.bullet} ${error.errorCode}'); |
| 250 out.writeln(); |
| 251 |
| 252 // If verbose, also print any associated correction. |
| 253 if (options.verbose && error.correction != null) { |
| 254 out.writeln( |
| 255 '${' '.padLeft(error.severity.length + 2)}${error.correction}'); |
| 256 } |
| 257 } |
| 258 |
| 259 // clear out batched errors |
| 260 batchedErrors.clear(); |
| 261 } |
| 262 |
219 void formatError( | 263 void formatError( |
220 Map<AnalysisError, LineInfo> errorToLine, AnalysisError error) { | 264 Map<AnalysisError, LineInfo> errorToLine, AnalysisError error) { |
221 Source source = error.source; | 265 Source source = error.source; |
222 LineInfo_Location location = errorToLine[error].getLocation(error.offset); | 266 LineInfo_Location location = errorToLine[error].getLocation(error.offset); |
223 | 267 |
224 ErrorSeverity severity = _severityProcessor(error); | 268 ErrorSeverity severity = _severityProcessor(error); |
225 | 269 |
226 // Get display name; translate INFOs into LINTS and HINTS. | 270 // Get display name; translate INFOs into LINTS and HINTS. |
227 String errorType = severity.displayName; | 271 String errorType = severity.displayName; |
228 if (severity == ErrorSeverity.INFO) { | 272 if (severity == ErrorSeverity.INFO) { |
(...skipping 26 matching lines...) Expand all Loading... |
255 severity: errorType, | 299 severity: errorType, |
256 sourcePath: sourcePath, | 300 sourcePath: sourcePath, |
257 offset: error.offset, | 301 offset: error.offset, |
258 line: location.lineNumber, | 302 line: location.lineNumber, |
259 column: location.columnNumber, | 303 column: location.columnNumber, |
260 message: message, | 304 message: message, |
261 errorCode: error.errorCode.name.toLowerCase(), | 305 errorCode: error.errorCode.name.toLowerCase(), |
262 correction: error.correction, | 306 correction: error.correction, |
263 )); | 307 )); |
264 } | 308 } |
| 309 } |
265 | 310 |
266 void flush() { | 311 class MachineErrorFormatter extends ErrorFormatter { |
267 // sort | 312 static final int _pipeCodeUnit = '|'.codeUnitAt(0); |
268 List<CLIError> sortedErrors = batchedErrors.toList()..sort(); | 313 static final int _slashCodeUnit = '\\'.codeUnitAt(0); |
| 314 static final int _newline = '\n'.codeUnitAt(0); |
| 315 static final int _return = '\r'.codeUnitAt(0); |
269 | 316 |
270 // print | 317 MachineErrorFormatter( |
271 for (CLIError error in sortedErrors) { | 318 StringSink out, CommandLineOptions options, AnalysisStats stats, |
272 if (error.isError) { | 319 {SeverityProcessor severityProcessor}) |
273 stats.errorCount++; | 320 : super(out, options, stats, severityProcessor: severityProcessor); |
274 } else if (error.isWarning) { | |
275 stats.warnCount++; | |
276 } else if (error.isLint) { | |
277 stats.lintCount++; | |
278 } else if (error.isHint) { | |
279 stats.hintCount++; | |
280 } | |
281 | 321 |
282 // warning • 'foo' is not a bar at lib/foo.dart:1:2 • foo_warning | 322 void flush() {} |
283 String issueColor = (error.isError == ErrorSeverity.ERROR || | |
284 error.isWarning == ErrorSeverity.WARNING) | |
285 ? ansi.red | |
286 : ''; | |
287 out.write(' $issueColor${error.severity}${ansi.none} ' | |
288 '${ansi.bullet} ${ansi.bold}${error.message}${ansi.none} '); | |
289 out.write('at ${error.sourcePath}'); | |
290 out.write(':${error.line}:${error.column} '); | |
291 out.write('${ansi.bullet} ${error.errorCode}'); | |
292 out.writeln(); | |
293 | 323 |
294 // If verbose, also print any associated correction. | 324 void formatError( |
295 if (options.verbose && error.correction != null) { | 325 Map<AnalysisError, LineInfo> errorToLine, AnalysisError error) { |
296 out.writeln( | 326 Source source = error.source; |
297 '${' '.padLeft(error.severity.length + 2)}${error.correction}'); | 327 LineInfo_Location location = errorToLine[error].getLocation(error.offset); |
| 328 int length = error.length; |
| 329 |
| 330 ErrorSeverity severity = _severityProcessor(error); |
| 331 |
| 332 if (severity == ErrorSeverity.ERROR) { |
| 333 stats.errorCount++; |
| 334 } else if (severity == ErrorSeverity.WARNING) { |
| 335 stats.warnCount++; |
| 336 } else if (error.errorCode.type == ErrorType.HINT) { |
| 337 stats.hintCount++; |
| 338 } else if (error.errorCode.type == ErrorType.LINT) { |
| 339 stats.lintCount++; |
| 340 } |
| 341 |
| 342 out.write(severity); |
| 343 out.write('|'); |
| 344 out.write(error.errorCode.type); |
| 345 out.write('|'); |
| 346 out.write(error.errorCode.name); |
| 347 out.write('|'); |
| 348 out.write(_escapeForMachineMode(source.fullName)); |
| 349 out.write('|'); |
| 350 out.write(location.lineNumber); |
| 351 out.write('|'); |
| 352 out.write(location.columnNumber); |
| 353 out.write('|'); |
| 354 out.write(length); |
| 355 out.write('|'); |
| 356 out.write(_escapeForMachineMode(error.message)); |
| 357 out.writeln(); |
| 358 } |
| 359 |
| 360 static String _escapeForMachineMode(String input) { |
| 361 StringBuffer result = new StringBuffer(); |
| 362 for (int c in input.codeUnits) { |
| 363 if (c == _newline) { |
| 364 result.write(r'\n'); |
| 365 } else if (c == _return) { |
| 366 result.write(r'\r'); |
| 367 } else { |
| 368 if (c == _slashCodeUnit || c == _pipeCodeUnit) { |
| 369 result.write('\\'); |
| 370 } |
| 371 result.writeCharCode(c); |
298 } | 372 } |
299 } | 373 } |
300 | 374 return result.toString(); |
301 // clear out batched errors | |
302 batchedErrors.clear(); | |
303 } | 375 } |
304 } | 376 } |
305 | |
306 final Map<String, int> _severityCompare = { | |
307 'error': 5, | |
308 'warning': 4, | |
309 'info': 3, | |
310 'lint': 2, | |
311 'hint': 1, | |
312 }; | |
313 | |
314 /// An [AnalysisError] with line and column information. | |
315 class CLIError implements Comparable<CLIError> { | |
316 final String severity; | |
317 final String sourcePath; | |
318 final int offset; | |
319 final int line; | |
320 final int column; | |
321 final String message; | |
322 final String errorCode; | |
323 final String correction; | |
324 | |
325 CLIError({ | |
326 this.severity, | |
327 this.sourcePath, | |
328 this.offset, | |
329 this.line, | |
330 this.column, | |
331 this.message, | |
332 this.errorCode, | |
333 this.correction, | |
334 }); | |
335 | |
336 bool get isError => severity == 'error'; | |
337 bool get isWarning => severity == 'warning'; | |
338 bool get isLint => severity == 'lint'; | |
339 bool get isHint => severity == 'hint'; | |
340 | |
341 @override | |
342 int get hashCode => | |
343 severity.hashCode ^ sourcePath.hashCode ^ errorCode.hashCode ^ offset; | |
344 | |
345 @override | |
346 bool operator ==(other) { | |
347 if (other is! CLIError) return false; | |
348 | |
349 return severity == other.severity && | |
350 sourcePath == other.sourcePath && | |
351 errorCode == other.errorCode && | |
352 offset == other.offset; | |
353 } | |
354 | |
355 @override | |
356 int compareTo(CLIError other) { | |
357 // severity | |
358 int compare = | |
359 _severityCompare[other.severity] - _severityCompare[this.severity]; | |
360 if (compare != 0) return compare; | |
361 | |
362 // path | |
363 compare = Comparable.compare( | |
364 this.sourcePath.toLowerCase(), other.sourcePath.toLowerCase()); | |
365 if (compare != 0) return compare; | |
366 | |
367 // offset | |
368 return this.offset - other.offset; | |
369 } | |
370 } | |
371 | |
372 /// Given an absolute path, return a relative path if the file is contained in | |
373 /// the current directory; return the original path otherwise. | |
374 String _relative(String file) { | |
375 return file.startsWith(path.current) ? path.relative(file) : file; | |
376 } | |
OLD | NEW |