UNPKG

18.6 kBJavaScriptView Raw
1// var assert = require('assert');
2var cst = require('cst');
3var Parser = cst.Parser;
4var Token = cst.Token;
5var Program = cst.types.Program;
6var Fragment = cst.Fragment;
7var ScopesApi = cst.api.ScopesApi;
8
9var treeIterator = require('./tree-iterator');
10
11// var Program = cst.types.Program;
12
13/**
14 * Operator list which are represented as keywords in token list.
15 */
16var KEYWORD_OPERATORS = {
17 'instanceof': true,
18 'in': true
19};
20
21/**
22 * File representation for JSCS.
23 *
24 * @name JsFile
25 * @param {Object} params
26 * @param {String} params.filename
27 * @param {String} params.source
28 * @param {Boolean} [params.es3]
29 */
30var JsFile = function(params) {
31 params = params || {};
32 this._parseErrors = [];
33 this._filename = params.filename;
34 this._source = params.source;
35
36 this._es3 = params.es3 || false;
37
38 this._lineBreaks = null;
39 this._lines = this._source.split(/\r\n|\r|\n/);
40
41 var parser = new Parser({
42 strictMode: false,
43 languageExtensions: {
44 gritDirectives: true,
45 appleInstrumentationDirectives: true
46 }
47 });
48
49 try {
50 this._program = parser.parse(this._source);
51 } catch (e) {
52 this._parseErrors.push(e);
53 this._program = new Program([
54 new Token('EOF', '')
55 ]);
56 }
57
58 // Lazy initialization
59 this._scopes = null;
60};
61
62JsFile.prototype = {
63 /**
64 * @returns {cst.types.Program}
65 */
66 getProgram: function() {
67 return this._program;
68 },
69
70 /**
71 * Returns the first line break character encountered in the file.
72 * Assumes LF if the file is only one line.
73 *
74 * @returns {String}
75 */
76 getLineBreakStyle: function() {
77 var lineBreaks = this.getLineBreaks();
78 return lineBreaks.length ? lineBreaks[0] : '\n';
79 },
80
81 /**
82 * Returns all line break characters from the file.
83 *
84 * @returns {String[]}
85 */
86 getLineBreaks: function() {
87 if (this._lineBreaks === null) {
88 this._lineBreaks = this._source.match(/\r\n|\r|\n/g) || [];
89 }
90 return this._lineBreaks;
91 },
92
93 /**
94 * Sets whitespace before specified token.
95 *
96 * @param {Object} token - in front of which we will add/remove/replace the whitespace token
97 * @param {String} whitespace - value of the whitespace token - `\n`, `\s`, `\t`
98 */
99 setWhitespaceBefore: function(token, whitespace) {
100 var prevToken = token.getPreviousToken();
101 var ws = new Token('Whitespace', whitespace);
102 var fragment = new Fragment(ws);
103
104 if (prevToken && prevToken.isWhitespace) {
105 if (whitespace === '') {
106 prevToken.remove();
107 return;
108 }
109
110 prevToken.parentElement.replaceChild(fragment, prevToken);
111 return;
112 }
113
114 this._setTokenBefore(token, fragment);
115 },
116
117 _setTokenBefore: function(token, fragment) {
118 var parent = token;
119 var grandpa = parent.parentElement;
120
121 while (grandpa) {
122 try {
123 grandpa.insertChildBefore(fragment, parent);
124 break;
125 } catch (e) {}
126
127 parent = grandpa;
128 grandpa = parent.parentElement;
129 }
130 },
131
132 /**
133 * Returns whitespace before specified token.
134 *
135 * @param {Object} token
136 * @returns {String}
137 */
138 getWhitespaceBefore: function(token) {
139 if (!token.getPreviousToken) {
140 console.log(token);
141 }
142 var prev = token.getPreviousToken();
143
144 if (prev && prev.isWhitespace) {
145 return prev.getSourceCode();
146 }
147
148 return '';
149 },
150
151 /**
152 * Returns the first token for the node from the AST.
153 *
154 * @param {Object} node
155 * @returns {Object}
156 */
157 getFirstNodeToken: function(node) {
158 return node.getFirstToken();
159 },
160
161 /**
162 * Returns the last token for the node from the AST.
163 *
164 * @param {Object} node
165 * @returns {Object}
166 */
167 getLastNodeToken: function(node) {
168 return node.getLastToken();
169 },
170
171 /**
172 * Returns the first token for the file.
173 *
174 * @param {Option} [options]
175 * @param {Boolean} [options.includeComments=false]
176 * @param {Boolean} [options.includeWhitespace=false]
177 * @returns {Object}
178 */
179 getFirstToken: function(/*options*/) {
180 return this._program.getFirstToken();
181 },
182
183 /**
184 * Returns the last token for the file.
185 *
186 * @param {Option} [options]
187 * @param {Boolean} [options.includeComments=false]
188 * @param {Boolean} [options.includeWhitespace=false]
189 * @returns {Object}
190 */
191 getLastToken: function(/*options*/) {
192 return this._program.getLastToken();
193 },
194
195 /**
196 * Returns the first token before the given.
197 *
198 * @param {Object} token
199 * @param {Object} [options]
200 * @param {Boolean} [options.includeComments=false]
201 * @returns {Object|undefined}
202 */
203 getPrevToken: function(token, options) {
204 if (options && options.includeComments) {
205 return token.getPreviousNonWhitespaceToken();
206 }
207
208 return token.getPreviousCodeToken();
209 },
210
211 /**
212 * Returns the first token after the given.
213 *
214 * @param {Object} token
215 * @param {Object} [options]
216 * @param {Boolean} [options.includeComments=false]
217 * @returns {Object|undefined}
218 */
219 getNextToken: function(token, options) {
220 if (options && options.includeComments) {
221 return token.getNextNonWhitespaceToken();
222 } else {
223 return token.getNextCodeToken();
224 }
225 },
226
227 /**
228 * Returns the first token before the given which matches type (and value).
229 *
230 * @param {Object} token
231 * @param {String} type
232 * @param {String} [value]
233 * @returns {Object|null}
234 */
235 findPrevToken: function(token, type, value) {
236 var prevToken = this.getPrevToken(token);
237 while (prevToken) {
238 if (prevToken.type === type && (value === undefined || prevToken.value === value)) {
239 return prevToken;
240 }
241
242 prevToken = this.getPrevToken(prevToken);
243 }
244 return prevToken;
245 },
246
247 /**
248 * Returns the first token after the given which matches type (and value).
249 *
250 * @param {Object} token
251 * @param {String} type
252 * @param {String} [value]
253 * @returns {Object|null}
254 */
255 findNextToken: function(token, type, value) {
256 var nextToken = token.getNextToken();
257
258 while (nextToken) {
259 if (nextToken.type === type && (value === undefined || nextToken.value === value)) {
260 return nextToken;
261 }
262
263 nextToken = nextToken.getNextToken();
264 }
265 return nextToken;
266 },
267
268 /**
269 * Returns the first token before the given which matches type (and value).
270 *
271 * @param {Object} token
272 * @param {String} value
273 * @returns {Object|null}
274 */
275 findPrevOperatorToken: function(token, value) {
276 return this.findPrevToken(token, value in KEYWORD_OPERATORS ? 'Keyword' : 'Punctuator', value);
277 },
278
279 /**
280 * Returns the first token after the given which matches type (and value).
281 *
282 * @param {Object} token
283 * @param {String} value
284 * @returns {Object|null}
285 */
286 findNextOperatorToken: function(token, value) {
287 return this.findNextToken(token, value in KEYWORD_OPERATORS ? 'Keyword' : 'Punctuator', value);
288 },
289
290 /**
291 * Iterates through the token tree using tree iterator.
292 * Calls passed function for every token.
293 *
294 * @param {Function} cb
295 * @param {Object} [tree]
296 */
297 iterate: function(cb, tree) {
298 return treeIterator.iterate(tree || this._program, cb);
299 },
300
301 /**
302 * Returns nodes by type(s) from earlier built index.
303 *
304 * @param {String|String[]} type
305 * @returns {Object[]}
306 */
307 getNodesByType: function(type) {
308 type = Array.isArray(type) ? type : [type];
309 var result = [];
310
311 for (var i = 0, l = type.length; i < l; i++) {
312 var nodes = this._program.selectNodesByType(type[i]);
313
314 if (nodes) {
315 result = result.concat(nodes);
316 }
317 }
318
319 return result;
320 },
321
322 /**
323 * Iterates nodes by type(s) from earlier built index.
324 * Calls passed function for every matched node.
325 *
326 * @param {String|String[]} type
327 * @param {Function} cb
328 * @param {Object} context
329 */
330 iterateNodesByType: function(type, cb, context) {
331 return this.getNodesByType(type).forEach(cb, context || this);
332 },
333
334 /**
335 * Iterates tokens by type(s) from the token array.
336 * Calls passed function for every matched token.
337 *
338 * @param {String|String[]} type
339 * @param {Function} cb
340 */
341 iterateTokensByType: function(type, cb) {
342 var tokens;
343
344 if (Array.isArray(type)) {
345 tokens = [];
346 for (var i = 0; i < type.length; i++) {
347 var items = this._program.selectTokensByType(type[i]);
348 tokens = tokens.concat(items);
349 }
350 } else {
351 tokens = this._program.selectTokensByType(type);
352 }
353
354 tokens.forEach(cb);
355 },
356
357 /**
358 * Iterates tokens by type and value(s) from the token array.
359 * Calls passed function for every matched token.
360 *
361 * @param {String} type
362 * @param {String|String[]} value
363 * @param {Function} cb
364 */
365 iterateTokensByTypeAndValue: function(type, value, cb) {
366 var values = (typeof value === 'string') ? [value] : value;
367 var valueIndex = {};
368 values.forEach(function(type) {
369 valueIndex[type] = true;
370 });
371
372 this.iterateTokensByType(type, function(token) {
373 if (valueIndex[token.value]) {
374 cb(token);
375 }
376 });
377 },
378
379 getFirstTokenOnLineWith: function(element, options) {
380 options = options || {};
381 var firstToken = element;
382
383 if (element.isComment && !options.includeComments) {
384 firstToken = null;
385 }
386
387 if (element.isWhitespace && !options.includeWhitespace) {
388 firstToken = null;
389 }
390
391 var currentToken = element.getPreviousToken();
392 while (currentToken) {
393 if (currentToken.isWhitespace) {
394 if (currentToken.getNewlineCount() > 0 || !currentToken.getPreviousToken()) {
395 if (options.includeWhitespace) {
396 firstToken = currentToken;
397 }
398 break;
399 }
400 } else if (currentToken.isComment) {
401 if (options.includeComments) {
402 firstToken = currentToken;
403 break;
404 }
405 if (currentToken.getNewlineCount() > 0) {
406 break;
407 }
408 } else {
409 firstToken = currentToken;
410 }
411
412 currentToken = currentToken.getPreviousToken();
413 }
414
415 if (firstToken) {
416 return firstToken;
417 }
418
419 currentToken = element.getNextToken();
420 while (currentToken) {
421 if (currentToken.isWhitespace) {
422 if (currentToken.getNewlineCount() > 0 || !currentToken.getNextToken()) {
423 if (options.includeWhitespace) {
424 firstToken = currentToken;
425 }
426 break;
427 }
428 } else if (currentToken.isComment) {
429 if (options.includeComments) {
430 firstToken = currentToken;
431 break;
432 }
433 if (currentToken.getNewlineCount() > 0) {
434 break;
435 }
436 } else {
437 firstToken = currentToken;
438 }
439
440 currentToken = currentToken.getNextToken();
441 }
442
443 return firstToken;
444 },
445
446 /**
447 * Returns last token for the specified line.
448 * Line numbers start with 1.
449 *
450 * @param {Number} lineNumber
451 * @param {Object} [options]
452 * @param {Boolean} [options.includeComments = false]
453 * @param {Boolean} [options.includeWhitespace = false]
454 * @returns {Object|null}
455 */
456 getLastTokenOnLine: function(lineNumber, options) {
457 options = options || {};
458
459 var loc;
460 var token = this._program.getLastToken();
461 var currentToken;
462
463 while (token) {
464 loc = token.getLoc();
465 currentToken = token;
466 token = token.getPreviousToken();
467
468 if (loc.start.line <= lineNumber && loc.end.line >= lineNumber) {
469
470 // Since whitespace tokens can contain newlines we need to check
471 // if position is in the range, not exact match
472 if (currentToken.isWhitespace && !options.includeWhitespace) {
473 continue;
474 }
475 }
476
477 if (loc.start.line === lineNumber || loc.end.line === lineNumber) {
478 if (currentToken.isComment && !options.includeComments) {
479 continue;
480 }
481
482 return currentToken;
483 }
484 }
485
486 return null;
487 },
488
489 /**
490 * Returns which dialect of JS this file supports.
491 *
492 * @returns {String}
493 */
494 getDialect: function() {
495 if (this._es3) {
496 return 'es3';
497 }
498
499 return 'es6';
500 },
501
502 /**
503 * Returns string representing contents of the file.
504 *
505 * @returns {String}
506 */
507 getSource: function() {
508 return this._source;
509 },
510
511 /**
512 * Returns token program.
513 *
514 * @returns {Object}
515 */
516 getTree: function() {
517 return this._program || {};
518 },
519
520 /**
521 * Returns comment token list.
522 */
523 getComments: function() {
524 var comments = [];
525 var token = this._program.getFirstToken();
526 while (token) {
527 if (token.isComment) {
528 comments[comments.length] = token;
529 }
530 token = token.getNextToken();
531 }
532 return comments;
533 },
534
535 /**
536 * Returns source filename for this object representation.
537 *
538 * @returns {String}
539 */
540 getFilename: function() {
541 return this._filename;
542 },
543
544 /**
545 * Returns array of source lines for the file.
546 *
547 * @returns {String[]}
548 */
549 getLines: function() {
550 return this._lines;
551 },
552
553 /**
554 * Returns analyzed scope.
555 *
556 * @returns {Object}
557 */
558 getScopes: function() {
559 if (!this._scopes) {
560 this._scopes = new ScopesApi(this._program);
561 }
562
563 return this._scopes;
564 },
565
566 /**
567 * Are tokens on the same line.
568 *
569 * @param {Element} tokenBefore
570 * @param {Element} tokenAfter
571 * @return {Boolean}
572 */
573 isOnTheSameLine: function(tokenBefore, tokenAfter) {
574 if (tokenBefore === tokenAfter) {
575 return true;
576 }
577 tokenBefore = tokenBefore instanceof Token ? tokenBefore : tokenBefore.getLastToken();
578 tokenAfter = tokenAfter instanceof Token ? tokenAfter : tokenAfter.getFirstToken();
579 var currentToken = tokenBefore;
580 while (currentToken) {
581 if (currentToken === tokenAfter) {
582 return true;
583 }
584 if (currentToken !== tokenBefore && currentToken.getNewlineCount() > 0) {
585 return false;
586 }
587 currentToken = currentToken.getNextToken();
588 }
589 return false;
590 },
591
592 getDistanceBetween: function(tokenBefore, tokenAfter) {
593 if (tokenBefore === tokenAfter) {
594 return 0;
595 }
596 tokenBefore = tokenBefore instanceof Token ? tokenBefore : tokenBefore.getLastToken();
597 tokenAfter = tokenAfter instanceof Token ? tokenAfter : tokenAfter.getFirstToken();
598 var currentToken = tokenBefore.getNextToken();
599 var distance = 0;
600 while (currentToken) {
601 if (currentToken === tokenAfter) {
602 break;
603 }
604
605 distance += currentToken.getSourceCodeLength();
606 currentToken = currentToken.getNextToken();
607 }
608 return distance;
609 },
610
611 getLineCountBetween: function(tokenBefore, tokenAfter) {
612 if (tokenBefore === tokenAfter) {
613 return 0;
614 }
615 tokenBefore = tokenBefore instanceof Token ? tokenBefore : tokenBefore.getLastToken();
616 tokenAfter = tokenAfter instanceof Token ? tokenAfter : tokenAfter.getFirstToken();
617
618 var currentToken = tokenBefore.getNextToken();
619 var lineCount = 0;
620 while (currentToken) {
621 if (currentToken === tokenAfter) {
622 break;
623 }
624
625 lineCount += currentToken.getNewlineCount();
626 currentToken = currentToken.getNextToken();
627 }
628 return lineCount;
629 },
630
631 /**
632 * Returns array of source lines for the file with comments removed.
633 *
634 * @returns {Array}
635 */
636 getLinesWithCommentsRemoved: function() {
637 var lines = this.getLines().concat();
638
639 this.getComments().concat().reverse().forEach(function(comment) {
640 var loc = comment.getLoc();
641 var startLine = loc.start.line;
642 var startCol = loc.start.column;
643 var endLine = loc.end.line;
644 var endCol = loc.end.column;
645 var i = startLine - 1;
646
647 if (startLine === endLine) {
648 // Remove tralling spaces (see gh-1968)
649 lines[i] = lines[i].replace(/\*\/\s+/, '\*\/');
650 lines[i] = lines[i].substring(0, startCol) + lines[i].substring(endCol);
651 } else {
652 lines[i] = lines[i].substring(0, startCol);
653 for (var x = i + 1; x < endLine - 1; x++) {
654 lines[x] = '';
655 }
656
657 lines[x] = lines[x].substring(endCol);
658 }
659 });
660
661 return lines;
662 },
663
664 /**
665 * Renders JS-file sources using token list.
666 *
667 * @returns {String}
668 */
669 render: function() {
670 return this._program.getSourceCode();
671 },
672
673 /**
674 * Returns list of parse errors.
675 *
676 * @returns {Error[]}
677 */
678 getParseErrors: function() {
679 return this._parseErrors;
680 }
681};
682
683module.exports = JsFile;