UNPKG

13.3 kBJavaScriptView Raw
1var utils = require('util');
2var EventEmitter = require('events').EventEmitter;
3var Token = require('cst').Token;
4
5/**
6 * Token assertions class.
7 *
8 * @name {TokenAssert}
9 * @param {JsFile} file
10 */
11function TokenAssert(file) {
12 EventEmitter.call(this);
13
14 this._file = file;
15}
16
17utils.inherits(TokenAssert, EventEmitter);
18
19/**
20 * Requires to have whitespace between specified tokens. Ignores newlines.
21 *
22 * @param {Object} options
23 * @param {Object} options.token
24 * @param {Object} options.nextToken
25 * @param {String} [options.message]
26 * @param {Number} [options.spaces] Amount of spaces between tokens.
27 * @return {Boolean} whether an error was found
28 */
29TokenAssert.prototype.whitespaceBetween = function(options) {
30 options.atLeast = 1;
31 return this.spacesBetween(options);
32};
33
34/**
35 * Requires to have no whitespace between specified tokens.
36 *
37 * @param {Object} options
38 * @param {Object} options.token
39 * @param {Object} options.nextToken
40 * @param {String} [options.message]
41 * @param {Boolean} [options.disallowNewLine=false]
42 * @return {Boolean} whether an error was found
43 */
44TokenAssert.prototype.noWhitespaceBetween = function(options) {
45 options.exactly = 0;
46 return this.spacesBetween(options);
47};
48
49/**
50 * Requires to have the whitespace between specified tokens with the provided options.
51 *
52 * @param {Object} options
53 * @param {Object} options.token
54 * @param {Object} options.nextToken
55 * @param {String} [options.message]
56 * @param {Object} [options.atLeast] At least how many spaces the tokens are apart
57 * @param {Object} [options.atMost] At most how many spaces the tokens are apart
58 * @param {Object} [options.exactly] Exactly how many spaces the tokens are apart
59 * @param {Boolean} [options.disallowNewLine=false]
60 * @return {Boolean} whether an error was found
61 */
62TokenAssert.prototype.spacesBetween = function(options) {
63 var token = options.token;
64 var nextToken = options.nextToken;
65 var atLeast = options.atLeast;
66 var atMost = options.atMost;
67 var exactly = options.exactly;
68
69 if (!token || !nextToken) {
70 return false;
71 }
72
73 this._validateOptions(options);
74
75 if (!options.disallowNewLine && !this._file.isOnTheSameLine(token, nextToken)) {
76 return false;
77 }
78
79 // Only attempt to remove or add lines if there are no comments between the two nodes
80 // as this prevents accidentally moving a valid token onto a line comment ed line
81 var fixed = !options.token.getNextNonWhitespaceToken().isComment;
82
83 var emitError = function(countPrefix, spaceCount) {
84 var fix = function() {
85 this._file.setWhitespaceBefore(nextToken, new Array(spaceCount + 1).join(' '));
86 }.bind(this);
87
88 var msgPostfix = token.value + ' and ' + nextToken.value;
89
90 if (!options.message) {
91 if (exactly === 0) {
92 // support noWhitespaceBetween
93 options.message = 'Unexpected whitespace between ' + msgPostfix;
94 } else if (exactly !== undefined) {
95 // support whitespaceBetween (spaces option)
96 options.message = spaceCount + ' spaces required between ' + msgPostfix;
97 } else if (atLeast === 1 && atMost === undefined) {
98 // support whitespaceBetween (no spaces option)
99 options.message = 'Missing space between ' + msgPostfix;
100 } else {
101 options.message = countPrefix + ' ' + spaceCount + ' spaces required between ' + msgPostfix;
102 }
103 }
104
105 this.emit('error', {
106 message: options.message,
107 element: token,
108 offset: token.getSourceCodeLength(),
109 fix: fixed ? fix : undefined
110 });
111 }.bind(this);
112
113 var spacesBetween = this._file.getDistanceBetween(token, nextToken);
114
115 if (atLeast !== undefined && spacesBetween < atLeast) {
116 emitError('at least', atLeast);
117 return true;
118 }
119
120 if (atMost !== undefined && spacesBetween > atMost) {
121 emitError('at most', atMost);
122 return true;
123 }
124
125 if (exactly !== undefined && spacesBetween !== exactly) {
126 emitError('exactly', exactly);
127 return true;
128 }
129
130 return false;
131};
132
133/**
134 * Requires the specified line to have the expected indentation.
135 *
136 * @param {Object} options
137 * @param {Number} options.actual
138 * @param {Number} options.expected
139 * @param {String} options.indentChar
140 * @param {String} options.token
141 * @return {Boolean} whether an error was found
142 */
143TokenAssert.prototype.indentation = function(options) {
144 var token = options.token;
145 var lineNumber = options.lineNumber;
146 var actual = options.actual;
147 var expected = options.expected;
148 var indentChar = options.indentChar;
149
150 if (actual === expected) {
151 return false;
152 }
153
154 this.emit('error', {
155 message: 'Expected indentation of ' + expected + ' characters',
156 line: lineNumber,
157 column: expected,
158 fix: function() {
159 var newWhitespace = (new Array(expected + 1)).join(indentChar);
160
161 this._updateWhitespaceByLine(token, function(lines) {
162 lines[lines.length - 1] = newWhitespace;
163 return lines;
164 });
165
166 if (token.isComment) {
167 this._updateCommentWhitespace(token, indentChar, actual, expected);
168 }
169 }.bind(this)
170 });
171
172 return true;
173};
174
175/**
176 * Updates the whitespace of a line by passing split lines to a callback function
177 * for editing.
178 *
179 * @param {Object} token
180 * @param {Function} callback
181 */
182TokenAssert.prototype._updateWhitespaceByLine = function(token, callback) {
183 var lineBreak = this._file.getLineBreakStyle();
184 var lines = this._file.getWhitespaceBefore(token).split(/\r\n|\r|\n/);
185
186 lines = callback(lines);
187 this._file.setWhitespaceBefore(token, lines.join(lineBreak));
188};
189
190/**
191 * Updates the whitespace of a line by passing split lines to a callback function
192 * for editing.
193 *
194 * @param {Object} token
195 * @param {Function} indentChar
196 * @param {Number} actual
197 * @param {Number} expected
198 */
199TokenAssert.prototype._updateCommentWhitespace = function(token, indentChar, actual, expected) {
200 var difference = expected - actual;
201 var tokenLines = token.value.split(/\r\n|\r|\n/);
202 var i = 1;
203 if (difference >= 0) {
204 var lineWhitespace = (new Array(difference + 1)).join(indentChar);
205 for (; i < tokenLines.length; i++) {
206 tokenLines[i] = tokenLines[i] === '' ? '' : lineWhitespace + tokenLines[i];
207 }
208 } else {
209 for (; i < tokenLines.length; i++) {
210 tokenLines[i] = tokenLines[i].substring(-difference);
211 }
212 }
213
214 var newComment = new Token('CommentBlock', tokenLines.join(this._file.getLineBreakStyle()));
215 token.parentElement.replaceChild(newComment, token);
216};
217
218/**
219 * Requires tokens to be on the same line.
220 *
221 * @param {Object} options
222 * @param {Object} options.token
223 * @param {Object} options.nextToken
224 * @param {Boolean} [options.stickToPreviousToken]
225 * @param {String} [options.message]
226 * @return {Boolean} whether an error was found
227 */
228TokenAssert.prototype.sameLine = function(options) {
229 options.exactly = 0;
230
231 return this.linesBetween(options);
232};
233
234/**
235 * Requires tokens to be on different lines.
236 *
237 * @param {Object} options
238 * @param {Object} options.token
239 * @param {Object} options.nextToken
240 * @param {Object} [options.message]
241 * @return {Boolean} whether an error was found
242 */
243TokenAssert.prototype.differentLine = function(options) {
244 options.atLeast = 1;
245
246 return this.linesBetween(options);
247};
248
249/**
250 * Requires tokens to have a certain amount of lines between them.
251 * Set at least one of atLeast or atMost OR set exactly.
252 *
253 * @param {Object} options
254 * @param {Object} options.token
255 * @param {Object} options.nextToken
256 * @param {Object} [options.message]
257 * @param {Object} [options.atLeast] At least how many lines the tokens are apart
258 * @param {Object} [options.atMost] At most how many lines the tokens are apart
259 * @param {Object} [options.exactly] Exactly how many lines the tokens are apart
260 * @param {Boolean} [options.stickToPreviousToken] When auto-fixing stick the
261 * nextToken onto the previous token.
262 * @return {Boolean} whether an error was found
263 */
264TokenAssert.prototype.linesBetween = function(options) {
265 var token = options.token;
266 var nextToken = options.nextToken;
267 var atLeast = options.atLeast;
268 var atMost = options.atMost;
269 var exactly = options.exactly;
270
271 if (!token || !nextToken) {
272 return false;
273 }
274
275 this._validateOptions(options);
276
277 // Only attempt to remove or add lines if there are no comments between the two nodes
278 // as this prevents accidentally moving a valid token onto a line comment ed line
279 var fixed = !options.token.getNextNonWhitespaceToken().isComment;
280
281 var linesBetween = this._file.getLineCountBetween(token, nextToken);
282
283 var emitError = function(countPrefix, lineCount) {
284 var msgPrefix = token.value + ' and ' + nextToken.value;
285
286 var fix = function() {
287 this._augmentLineCount(options, lineCount);
288 }.bind(this);
289
290 if (!options.message) {
291 if (exactly === 0) {
292 // support sameLine
293 options.message = msgPrefix + ' should be on the same line';
294 } else if (atLeast === 1 && atMost === undefined) {
295 // support differentLine
296 options.message = msgPrefix + ' should be on different lines';
297 } else {
298 // support linesBetween
299 options.message = msgPrefix + ' should have ' + countPrefix + ' ' + lineCount + ' line(s) between them';
300 }
301 }
302
303 this.emit('error', {
304 message: options.message,
305 element: token,
306 offset: token.getSourceCodeLength(),
307 fix: fixed ? fix : undefined
308 });
309 }.bind(this);
310
311 if (atLeast !== undefined && linesBetween < atLeast) {
312 emitError('at least', atLeast);
313 return true;
314 }
315
316 if (atMost !== undefined && linesBetween > atMost) {
317 emitError('at most', atMost);
318 return true;
319 }
320
321 if (exactly !== undefined && linesBetween !== exactly) {
322 emitError('exactly', exactly);
323 return true;
324 }
325
326 return false;
327};
328
329/**
330 * Throws errors if atLeast, atMost, and exactly options don't mix together properly or
331 * if the tokens provided are equivalent.
332 *
333 * @param {Object} options
334 * @param {Object} options.token
335 * @param {Object} options.nextToken
336 * @param {Object} [options.atLeast] At least how many spaces the tokens are apart
337 * @param {Object} [options.atMost] At most how many spaces the tokens are apart
338 * @param {Object} [options.exactly] Exactly how many spaces the tokens are apart
339 * @throws {Error} If the options are non-sensical
340 * @private
341 */
342TokenAssert.prototype._validateOptions = function(options) {
343 var token = options.token;
344 var nextToken = options.nextToken;
345 var atLeast = options.atLeast;
346 var atMost = options.atMost;
347 var exactly = options.exactly;
348
349 if (token === nextToken) {
350 throw new Error('You cannot specify the same token as both token and nextToken');
351 }
352
353 if (atLeast === undefined &&
354 atMost === undefined &&
355 exactly === undefined) {
356 throw new Error('You must specify at least one option');
357 }
358
359 if (exactly !== undefined && (atLeast !== undefined || atMost !== undefined)) {
360 throw new Error('You cannot specify atLeast or atMost with exactly');
361 }
362
363 if (atLeast !== undefined && atMost !== undefined && atMost < atLeast) {
364 throw new Error('atLeast and atMost are in conflict');
365 }
366};
367
368/**
369 * Augments token whitespace to contain the correct number of newlines while preserving indentation
370 *
371 * @param {Object} options
372 * @param {Object} options.nextToken
373 * @param {Boolean} [options.stickToPreviousToken]
374 * @param {Number} lineCount
375 * @private
376 */
377TokenAssert.prototype._augmentLineCount = function(options, lineCount) {
378 var token = options.nextToken;
379 if (lineCount === 0) {
380 if (options.stickToPreviousToken) {
381 var nextToken = this._file.getNextToken(token, {
382 includeComments: true
383 });
384 this._file.setWhitespaceBefore(nextToken, this._file.getWhitespaceBefore(token));
385 }
386
387 this._file.setWhitespaceBefore(token, ' ');
388 return;
389 }
390
391 this._updateWhitespaceByLine(token, function(lines) {
392 var currentLineCount = lines.length;
393 var lastLine = lines[lines.length - 1];
394
395 if (currentLineCount <= lineCount) {
396 // add additional lines that maintain the same indentation as the former last line
397 for (; currentLineCount <= lineCount; currentLineCount++) {
398 lines[lines.length - 1] = '';
399 lines.push(lastLine);
400 }
401 } else {
402 // remove lines and then ensure that the new last line maintains the previous indentation
403 lines = lines.slice(0, lineCount + 1);
404 lines[lines.length - 1] = lastLine;
405 }
406
407 return lines;
408 });
409};
410
411module.exports = TokenAssert;