UNPKG

8.82 kBJavaScriptView Raw
1var chalk = require('chalk');
2var TokenAssert = require('./token-assert');
3
4var LINE_SEPARATOR = /\r\n|\r|\n/g;
5
6var EMPTY_POS = {
7 line: 1,
8 column: 0
9};
10
11/**
12 * Set of errors for specified file.
13 *
14 * @name Errors
15 * @param {JsFile} file
16 */
17var Errors = function(file) {
18 this._errorList = [];
19 this._file = file;
20 this._currentRule = '';
21
22 /**
23 * @type {TokenAssert}
24 * @public
25 */
26 this.assert = new TokenAssert(file);
27 this.assert.on('error', this._addError.bind(this));
28};
29
30Errors.prototype = {
31 /**
32 * Adds style error to the list
33 *
34 * @param {String | Error} message
35 * @param {cst.types.Element} element
36 * @param {Number} [offset] relative offset
37 */
38 add: function(message, element, offset) {
39 if (message instanceof Error) {
40 this._addParseError(message);
41 return;
42 }
43
44 this._addError({
45 message: message,
46 element: element,
47 offset: offset
48 });
49 },
50
51 /**
52 * Adds style error to the list
53 *
54 * @param {Object} errorInfo
55 */
56 cast: function(errorInfo) {
57 this._addError(errorInfo);
58 },
59
60 /**
61 * Adds parser error to error list.
62 *
63 * @param {Object} errorInfo
64 * @private
65 */
66 _addParseError: function(errorInfo) {
67 this._errorList.push({
68 filename: this._file.getFilename(),
69 rule: 'parseError',
70 message: errorInfo.message,
71 line: errorInfo.loc ? errorInfo.loc.line : 1,
72 column: errorInfo.loc ? errorInfo.loc.column : 0
73 });
74 },
75
76 /**
77 * Adds error to error list.
78 *
79 * @param {Object} errorInfo
80 * @private
81 */
82 _addError: function(errorInfo) {
83 this._errorList.push({
84 filename: this._file.getFilename(),
85 rule: this._currentRule,
86 message: this._prepareMessage(errorInfo),
87 element: errorInfo.element,
88 offset: errorInfo.offset,
89 additional: errorInfo.additional,
90 fixed: false,
91 fix: errorInfo.fix
92 });
93 },
94
95 /**
96 * Prepare error message.
97 *
98 * @param {Object} errorInfo
99 * @private
100 */
101 _prepareMessage: function(errorInfo) {
102 var rule = errorInfo instanceof Error ? 'parseError' : this._currentRule;
103
104 if (rule) {
105 return rule + ': ' + errorInfo.message;
106 }
107
108 return errorInfo.message;
109 },
110
111 /**
112 * Returns style error list.
113 *
114 * @returns {Object[]}
115 */
116 getErrorList: function() {
117 return this._errorList;
118 },
119
120 /**
121 * Returns filename of file this error list is for.
122 *
123 * @returns {String}
124 */
125 getFilename: function() {
126 return this._file.getFilename();
127 },
128
129 /**
130 * Returns true if no errors are added.
131 *
132 * @returns {Boolean}
133 */
134 isEmpty: function() {
135 return this._errorList.length === 0;
136 },
137
138 /**
139 * Returns amount of errors added by the rules.
140 *
141 * @returns {Number}
142 */
143 getValidationErrorCount: function() {
144 return this._errorList.filter(function(error) {
145 return error.rule !== 'parseError' && error.rule !== 'internalError';
146 });
147 },
148
149 /**
150 * Returns amount of errors added by the rules.
151 *
152 * @returns {Number}
153 */
154 getErrorCount: function() {
155 return this._errorList.length;
156 },
157
158 /**
159 * Strips error list to the specified length.
160 *
161 * @param {Number} length
162 */
163 stripErrorList: function(length) {
164 this._errorList.splice(length);
165 },
166
167 /**
168 * Filters out errors based on the supplied filter function
169 *
170 * @param {Function} filter
171 */
172 filter: function(filter) {
173 this._errorList = this._errorList.filter(filter);
174 },
175
176 /**
177 * @param {TokenIndex} tokenIndex
178 */
179 calculateErrorLocations: function(tokenIndex) {
180 this._errorList.forEach(function(error) {
181 var pos = Errors.getPosition(error, tokenIndex);
182 error.line = pos.line;
183 error.column = pos.column;
184 });
185 },
186
187 /**
188 * Formats error for further output.
189 *
190 * @param {Object} error
191 * @param {Boolean} [colorize = false]
192 * @returns {String}
193 */
194 explainError: function(error, colorize) {
195 var lineNumber = error.line - 1;
196 var lines = this._file.getLines();
197 var result = [
198 renderLine(lineNumber, lines[lineNumber], colorize),
199 renderPointer(error.column, colorize)
200 ];
201 var i = lineNumber - 1;
202 var linesAround = 2;
203 while (i >= 0 && i >= (lineNumber - linesAround)) {
204 result.unshift(renderLine(i, lines[i], colorize));
205 i--;
206 }
207 i = lineNumber + 1;
208 while (i < lines.length && i <= (lineNumber + linesAround)) {
209 result.push(renderLine(i, lines[i], colorize));
210 i++;
211 }
212 result.unshift(formatErrorMessage(error.message, this.getFilename(), colorize));
213 return result.join('\n');
214 },
215
216 /**
217 * Sets the current rule so that errors are aware
218 * of which rule triggered them.
219 *
220 * @param {String} rule
221 */
222 setCurrentRule: function(rule) {
223 this._currentRule = rule;
224 }
225
226};
227
228/**
229 * Formats error message header.
230 *
231 * @param {String} message
232 * @param {String} filename
233 * @param {Boolean} colorize
234 * @returns {String}
235 */
236function formatErrorMessage(message, filename, colorize) {
237 return (colorize ? chalk.bold(message) : message) +
238 ' at ' +
239 (colorize ? chalk.green(filename) : filename) + ' :';
240}
241
242/**
243 * Simple util for prepending spaces to the string until it fits specified size.
244 *
245 * @param {String} s
246 * @param {Number} len
247 * @returns {String}
248 */
249function prependSpaces(s, len) {
250 while (s.length < len) {
251 s = ' ' + s;
252 }
253 return s;
254}
255
256/**
257 * Renders single line of code in style error formatted output.
258 *
259 * @param {Number} n line number
260 * @param {String} line
261 * @param {Boolean} [colorize = false]
262 * @returns {String}
263 */
264function renderLine(n, line, colorize) {
265 // Convert tabs to spaces, so errors in code lines with tabs as indention symbol
266 // could be correctly rendered, plus it will provide less verbose output
267 line = line.replace(/\t/g, ' ');
268
269 // "n + 1" to print lines in human way (counted from 1)
270 var lineNumber = prependSpaces((n + 1).toString(), 5) + ' |';
271 return ' ' + (colorize ? chalk.grey(lineNumber) : lineNumber) + line;
272}
273
274/**
275 * Renders pointer:
276 * ---------------^
277 *
278 * @param {Number} column
279 * @param {Boolean} [colorize = false]
280 * @returns {String}
281 */
282function renderPointer(column, colorize) {
283 var res = (new Array(column + 9)).join('-') + '^';
284 return colorize ? chalk.grey(res) : res;
285}
286
287/**
288 * Get position of the element
289 *
290 * @param {Error} [error]
291 * @param {TokenIndex} [tokenIndex]
292 * @return {Object}
293 */
294Errors.getPosition = function(error, tokenIndex) {
295 var element = error.element;
296 var offset = error.offset;
297 var rule = error.rule;
298
299 if (!element) {
300 return EMPTY_POS;
301 }
302
303 if (offset === undefined) {
304 // TODO: probably should be generalized
305 if (rule === 'validateQuoteMarks') {
306 offset = 0;
307 } else if (element.getSourceCodeLength() === 1) {
308 offset = 0;
309 } else {
310 offset = (element.getNewlineCount() === 0 && Math.ceil(element.getSourceCodeLength() / 2)) || 0;
311 }
312 }
313
314 var pos = tokenIndex ? tokenIndex.getElementLoc(element) : element.getLoc().start;
315 if (!pos) {
316 return EMPTY_POS;
317 }
318
319 if (offset === 0) {
320 return pos;
321 }
322
323 var newlineCount = element.getNewlineCount();
324 if (newlineCount > 0) {
325 var code = element.getSourceCode();
326 LINE_SEPARATOR.lastIndex = 0;
327 var lineOffset = 0;
328 var match;
329 var previousOffset = 0;
330 var firstLineColumnOffset = pos.column;
331 while ((match = LINE_SEPARATOR.exec(code)) !== null) {
332 var currentOffset = match.index;
333 if (offset <= currentOffset) {
334 return {
335 line: pos.line + lineOffset,
336 column: firstLineColumnOffset + offset - previousOffset
337 };
338 }
339 previousOffset = currentOffset + match[0].length;
340 firstLineColumnOffset = 0;
341 lineOffset++;
342 }
343 return {
344 line: pos.line + newlineCount,
345 column: offset - previousOffset
346 };
347 } else {
348 return {
349 line: pos.line,
350 column: pos.column + offset
351 };
352 }
353};
354
355module.exports = Errors;