UNPKG

31.3 kBJavaScriptView Raw
1var assert = require('assert');
2
3var escope = require('escope');
4
5var treeIterator = require('./tree-iterator');
6
7/**
8 * Operator list which are represented as keywords in token list.
9 */
10var KEYWORD_OPERATORS = {
11 'instanceof': true,
12 'in': true
13};
14
15/**
16 * File representation for JSCS.
17 *
18 * @name JsFile
19 * @param {Object} params
20 * @param {String} params.filename
21 * @param {String} params.source
22 * @param {Object} params.esprima
23 * @param {Object} [params.esprimaOptions]
24 * @param {Boolean} [params.es3]
25 * @param {Boolean} [params.es6]
26 */
27var JsFile = function(params) {
28 params = params || {};
29 this._parseErrors = [];
30 this._filename = params.filename;
31 this._source = params.source;
32 this._tree = {tokens: [], comments: []};
33
34 this._es3 = params.es3 || false;
35 this._es6 = params.es6 || false;
36
37 this._lineBreaks = null;
38 this._lines = this._source.split(/\r\n|\r|\n/);
39
40 var hasErrors = false;
41 try {
42 this._tree = parseJavaScriptSource(this._source, params.esprima, params.esprimaOptions);
43 } catch (e) {
44 hasErrors = true;
45 this._parseErrors.push(e);
46 }
47
48 // Lazy initialization
49 this._scope = null;
50
51 this._tokens = this._buildTokenList(this._tree.tokens, this._tree.comments);
52 this._addEOFToken(hasErrors);
53 this._tokens = this._addWhitespaceTokens(this._tokens, this._source);
54
55 this._setTokenIndexes();
56
57 var nodeIndexes = this._buildNodeIndex();
58 this._index = nodeIndexes.nodesByType;
59 this._nodesByStartRange = nodeIndexes.nodesByStartRange;
60
61 this._fixEsprimaIdentifiers();
62
63 this._buildDisabledRuleIndex();
64};
65
66JsFile.prototype = {
67 /**
68 * Returns the first line break character encountered in the file.
69 * Assumes LF if the file is only one line.
70 *
71 * @returns {String}
72 */
73 getLineBreakStyle: function() {
74 var lineBreaks = this.getLineBreaks();
75 return lineBreaks.length ? lineBreaks[0] : '\n';
76 },
77
78 /**
79 * Returns all line break characters from the file.
80 *
81 * @returns {String[]}
82 */
83 getLineBreaks: function() {
84 if (this._lineBreaks === null) {
85 this._lineBreaks = this._source.match(/\r\n|\r|\n/g) || [];
86 }
87
88 return this._lineBreaks;
89 },
90
91 /**
92 * Set token indexes
93 *
94 * @private
95 */
96 _setTokenIndexes: function() {
97 var tokenIndexes = this._buildTokenIndex(this._tokens);
98
99 this._tokenRangeStartIndex = tokenIndexes.tokenRangeStartIndex;
100 this._tokenRangeEndIndex = tokenIndexes.tokenRangeEndIndex;
101 this._tokensByLineIndex = tokenIndexes.tokensByLineIndex;
102 },
103
104 /**
105 * Builds an index of disabled rules by starting line for error suppression.
106 *
107 * @private
108 */
109 _buildDisabledRuleIndex: function() {
110 this._disabledRuleIndex = [];
111
112 var comments = this.getComments();
113
114 // Matches a comment enabling or disabling rules.
115 var blockRe = /(jscs\s*:\s*(en|dis)able)(.*)/;
116
117 // Matches a comment disbling a rule for one line.
118 var lineRe = /(jscs\s*:\s*ignore)(.*)/;
119
120 comments.forEach(function(comment) {
121 var enabled;
122 var value = comment.value.trim();
123 var blockParsed = blockRe.exec(value);
124 var lineParsed = lineRe.exec(value);
125 var line = comment.loc.start.line;
126
127 if (blockParsed && blockParsed.index === 0) {
128 enabled = blockParsed[2] === 'en';
129 this._addToDisabledRuleIndex(enabled, blockParsed[3], line);
130
131 } else if (lineParsed && lineParsed.index === 0) {
132 this._disableRulesAt(lineParsed[2], line);
133 }
134
135 }, this);
136 },
137
138 /**
139 * Sets whitespace before specified token.
140 *
141 * @param {Object} token
142 * @param {String} whitespace
143 */
144 setWhitespaceBefore: function(token, whitespace) {
145 var whitespaceToken = this.getPrevToken(token, {includeWhitespace: true});
146 if (whitespaceToken && whitespaceToken.type === 'Whitespace') {
147
148 // Modifying already existing token.
149 if (whitespace === '') {
150 this.removeToken(whitespaceToken);
151 } else {
152 whitespaceToken.value = whitespace;
153 }
154 } else if (whitespace !== '') {
155 var tokenIndex = token._tokenIndex;
156
157 // Adding a token before specified one.
158 this._tokens.splice(tokenIndex, 0, {
159 type: 'Whitespace',
160 value: whitespace,
161 isWhitespace: true
162 });
163
164 // Quickly updating modified token order
165 for (var i = tokenIndex; i < this._tokens.length; i++) {
166 this._tokens[i]._tokenIndex = i;
167 }
168 }
169 },
170
171 /**
172 * Returns whitespace before specified token.
173 *
174 * @param {Object} token
175 * @returns {String}
176 */
177 getWhitespaceBefore: function(token) {
178 var whitespaceToken = this.getPrevToken(token, {includeWhitespace: true});
179 if (whitespaceToken && whitespaceToken.type === 'Whitespace') {
180 return whitespaceToken.value;
181 } else {
182 return '';
183 }
184 },
185
186 /**
187 * Remove some entity (only one) from array with predicate
188 *
189 * @param {Array} entities
190 * @param {*} entity
191 */
192 removeEntity: function(entities, entity) {
193 for (var i = 0; i < entities.length; i++) {
194 if (entities[i] === entity) {
195 entities.splice(i, 1);
196
197 return;
198 }
199 }
200 },
201
202 /**
203 * Remove token from token list.
204 *
205 * @param {Object} token
206 */
207 removeToken: function(token) {
208 this.removeEntity(this._tokens, token);
209
210 this._setTokenIndexes();
211 },
212
213 /**
214 * Disables a rules for a single line, not re-enabling any disabled rules
215 *
216 * @private
217 */
218 _disableRulesAt: function(rules, line) {
219 rules = rules.split(/\s*,\s*/);
220 for (var i = 0; i < rules.length; i++) {
221 if (!this.isEnabledRule(rules[i], line)) {
222 continue;
223 }
224
225 this._addToDisabledRuleIndex(false, rules[i], line);
226 this._addToDisabledRuleIndex(true, rules[i], line + 1);
227 }
228 },
229
230 /**
231 * Returns whether a specific rule is disabled on the given line.
232 *
233 * @param {String} ruleName the rule name being tested
234 * @param {Number} line the line number being tested
235 * @returns {Boolean} true if the rule is enabled
236 */
237 isEnabledRule: function(ruleName, line) {
238 var enabled = true;
239 ruleName = ruleName.trim();
240
241 this._disabledRuleIndex.some(function(region) {
242 // once the comment we're inspecting occurs after the location of the error,
243 // no longer check for whether the state is enabled or disable
244 if (region.line > line) {
245 return true;
246 }
247
248 if (region.rule === ruleName || region.rule === '*') {
249 enabled = region.enabled;
250 }
251 }, this);
252
253 return enabled;
254 },
255
256 /**
257 * Adds rules to the disabled index given a string containing rules (or '' for all).
258 *
259 * @param {Boolean} enabled whether the rule is disabled or enabled on this line
260 * @param {String} rulesStr the string containing specific rules to en/disable
261 * @param {Number} line the line the comment appears on
262 * @private
263 */
264 _addToDisabledRuleIndex: function(enabled, rulesStr, line) {
265 rulesStr = rulesStr || '*';
266
267 rulesStr.split(',').forEach(function(rule) {
268 rule = rule.trim();
269
270 if (!rule) {
271 return;
272 }
273
274 this._disabledRuleIndex.push({
275 rule: rule,
276 enabled: enabled,
277 line: line
278 });
279 }, this);
280 },
281
282 /**
283 * Builds token index by starting pos for futher navigation.
284 *
285 * @param {Object[]} tokens
286 * @returns {{tokenRangeStartIndex: {}, tokenRangeEndIndex: {}}}
287 * @private
288 */
289 _buildTokenIndex: function(tokens) {
290 var tokenRangeStartIndex = {};
291 var tokenRangeEndIndex = {};
292 var tokensByLineIndex = {};
293 for (var i = 0, l = tokens.length; i < l; i++) {
294 var token = tokens[i];
295
296 token._tokenIndex = i;
297
298 if (token.type === 'Whitespace') {
299 continue;
300 }
301
302 // tokens by range
303 tokenRangeStartIndex[token.range[0]] = token;
304 tokenRangeEndIndex[token.range[1]] = token;
305
306 // tokens by line
307 var lineNumber = token.loc.start.line;
308 if (!tokensByLineIndex[lineNumber]) {
309 tokensByLineIndex[lineNumber] = [];
310 }
311
312 tokensByLineIndex[lineNumber].push(token);
313 }
314
315 return {
316 tokenRangeStartIndex: tokenRangeStartIndex,
317 tokenRangeEndIndex: tokenRangeEndIndex,
318 tokensByLineIndex: tokensByLineIndex
319 };
320 },
321
322 /**
323 * Returns token using range start from the index.
324 *
325 * @returns {Object|null}
326 */
327 getTokenByRangeStart: function(start) {
328 return this._tokenRangeStartIndex[start] || null;
329 },
330
331 /**
332 * Returns token using range end from the index.
333 *
334 * @returns {Object|null}
335 */
336 getTokenByRangeEnd: function(end) {
337 return this._tokenRangeEndIndex[end] || null;
338 },
339
340 /**
341 * Returns the first token for the node from the AST.
342 *
343 * @param {Object} node
344 * @returns {Object}
345 */
346 getFirstNodeToken: function(node) {
347 return this.getTokenByRangeStart(node.range[0]);
348 },
349
350 /**
351 * Returns the last token for the node from the AST.
352 *
353 * @param {Object} node
354 * @returns {Object}
355 */
356 getLastNodeToken: function(node) {
357 return this.getTokenByRangeEnd(node.range[1]);
358 },
359
360 /**
361 * Returns the first token for the file.
362 *
363 * @param {Option} [options]
364 * @param {Boolean} [options.includeComments=false]
365 * @param {Boolean} [options.includeWhitespace=false]
366 * @returns {Object}
367 */
368 getFirstToken: function(options) {
369 return this._getTokenFromIndex(0, 1, options);
370 },
371
372 /**
373 * Returns the last token for the file.
374 *
375 * @param {Option} [options]
376 * @param {Boolean} [options.includeComments=false]
377 * @param {Boolean} [options.includeWhitespace=false]
378 * @returns {Object}
379 */
380 getLastToken: function(options) {
381 return this._getTokenFromIndex(this._tokens.length - 1, -1, options);
382 },
383
384 /**
385 * Returns the first token after the given using direction and specified conditions.
386 *
387 * @param {Number} index
388 * @param {Number} direction `1` - forward or `-1` - backwards
389 * @param {Object} [options]
390 * @param {Boolean} [options.includeComments=false]
391 * @param {Boolean} [options.includeWhitespace=false]
392 * @returns {Object|null}
393 */
394 _getTokenFromIndex: function(index, direction, options) {
395 while (true) {
396 var followingToken = this._tokens[index];
397
398 if (!followingToken) {
399 return null;
400 }
401
402 if (
403 (!followingToken.isComment || (options && options.includeComments)) &&
404 (!followingToken.isWhitespace || (options && options.includeWhitespace))
405 ) {
406 return followingToken;
407 }
408
409 index += direction;
410 }
411 },
412
413 /**
414 * Returns the first token before the given.
415 *
416 * @param {Object} token
417 * @param {Object} [options]
418 * @param {Boolean} [options.includeComments=false]
419 * @param {Boolean} [options.includeWhitespace=false]
420 * @returns {Object|null}
421 */
422 getPrevToken: function(token, options) {
423 return this._getTokenFromIndex(token._tokenIndex - 1, -1, options);
424 },
425
426 /**
427 * Returns the first token after the given.
428 *
429 * @param {Object} token
430 * @param {Object} [options]
431 * @param {Boolean} [options.includeComments=false]
432 * @param {Boolean} [options.includeWhitespace=false]
433 * @returns {Object|null}
434 */
435 getNextToken: function(token, options) {
436 return this._getTokenFromIndex(token._tokenIndex + 1, 1, options);
437 },
438
439 /**
440 * Returns the first token before the given which matches type (and value).
441 *
442 * @param {Object} token
443 * @param {String} type
444 * @param {String} [value]
445 * @returns {Object|null}
446 */
447 findPrevToken: function(token, type, value) {
448 var prevToken = this.getPrevToken(token);
449 while (prevToken) {
450 if (prevToken.type === type && (value === undefined || prevToken.value === value)) {
451 return prevToken;
452 }
453
454 prevToken = this.getPrevToken(prevToken);
455 }
456
457 return prevToken;
458 },
459
460 /**
461 * Returns the first token after the given which matches type (and value).
462 *
463 * @param {Object} token
464 * @param {String} type
465 * @param {String} [value]
466 * @returns {Object|null}
467 */
468 findNextToken: function(token, type, value) {
469 var nextToken = this.getNextToken(token);
470 while (nextToken) {
471 if (nextToken.type === type && (value === undefined || nextToken.value === value)) {
472 return nextToken;
473 }
474
475 nextToken = this.getNextToken(nextToken);
476 }
477
478 return nextToken;
479 },
480
481 /**
482 * Returns the first token before the given which matches type (and value).
483 *
484 * @param {Object} token
485 * @param {String} value
486 * @returns {Object|null}
487 */
488 findPrevOperatorToken: function(token, value) {
489 return this.findPrevToken(token, value in KEYWORD_OPERATORS ? 'Keyword' : 'Punctuator', value);
490 },
491
492 /**
493 * Returns the first token after the given which matches type (and value).
494 *
495 * @param {Object} token
496 * @param {String} value
497 * @returns {Object|null}
498 */
499 findNextOperatorToken: function(token, value) {
500 return this.findNextToken(token, value in KEYWORD_OPERATORS ? 'Keyword' : 'Punctuator', value);
501 },
502
503 /**
504 * Iterates through the token tree using tree iterator.
505 * Calls passed function for every token.
506 *
507 * @param {Function} cb
508 * @param {Object} [tree]
509 */
510 iterate: function(cb, tree) {
511 return treeIterator.iterate(tree || this._tree, cb);
512 },
513
514 /**
515 * Returns node by its range position from earlier built index.
516 *
517 * @returns {Object}
518 */
519 getNodeByRange: function(number) {
520 assert(typeof number === 'number', 'requires node range argument');
521
522 var result = {};
523
524 // Look backwards for the first node(s) spanning `number`
525 // (possible with this.iterate, but too slow on large files)
526 var i = number;
527 var nodes;
528 do {
529 // Escape hatch
530 if (i < 0) {
531 return result;
532 }
533
534 nodes = this._nodesByStartRange[i];
535 i--;
536 } while (!nodes || nodes[0].range[1] <= number);
537
538 // Return the deepest such node
539 for (i = nodes.length - 1; i >= 0; i--) {
540 if (nodes[i].range[1] > number) {
541 return nodes[i];
542 }
543 }
544 },
545
546 /**
547 * Returns nodes by range start index from earlier built index.
548 *
549 * @param {Object} token
550 * @returns {Object[]}
551 */
552 getNodesByFirstToken: function(token) {
553 var result = [];
554 if (token && token.range && token.range[0] >= 0) {
555 var nodes = this._nodesByStartRange[token.range[0]];
556 if (nodes) {
557 result = result.concat(nodes);
558 }
559 }
560
561 return result;
562 },
563
564 /**
565 * Returns nodes by type(s) from earlier built index.
566 *
567 * @param {String|String[]} type
568 * @returns {Object[]}
569 */
570 getNodesByType: function(type) {
571 if (typeof type === 'string') {
572 return this._index[type] || [];
573 } else {
574 var result = [];
575 for (var i = 0, l = type.length; i < l; i++) {
576 var nodes = this._index[type[i]];
577 if (nodes) {
578 result = result.concat(nodes);
579 }
580 }
581
582 return result;
583 }
584 },
585
586 /**
587 * Iterates nodes by type(s) from earlier built index.
588 * Calls passed function for every matched node.
589 *
590 * @param {String|String[]} type
591 * @param {Function} cb
592 * @param {Object} context
593 */
594 iterateNodesByType: function(type, cb, context) {
595 return this.getNodesByType(type).forEach(cb, context || this);
596 },
597
598 /**
599 * Iterates tokens by type(s) from the token array.
600 * Calls passed function for every matched token.
601 *
602 * @param {String|String[]} type
603 * @param {Function} cb
604 */
605 iterateTokensByType: function(type, cb) {
606 var types = (typeof type === 'string') ? [type] : type;
607 var typeIndex = {};
608 types.forEach(function(type) {
609 typeIndex[type] = true;
610 });
611
612 this._forEachToken(function(token, index, tokens) {
613 if (typeIndex[token.type]) {
614 cb(token, index, tokens);
615 }
616 });
617 },
618
619 /**
620 * Iterates token by value from the token array.
621 * Calls passed function for every matched token.
622 *
623 * @param {String|String[]} name
624 * @param {Function} cb
625 */
626 iterateTokenByValue: function(name, cb) {
627 var names = (typeof name === 'string') ? [name] : name;
628 var nameIndex = {};
629 names.forEach(function(type) {
630 nameIndex[type] = true;
631 });
632
633 this._forEachToken(function(token, index, tokens) {
634 if (nameIndex.hasOwnProperty(token.value)) {
635 cb(token, index, tokens);
636 }
637 });
638 },
639
640 /**
641 * Executes callback for each token in token list.
642 *
643 * @param {Function} cb
644 * @private
645 */
646 _forEachToken: function(cb) {
647 var index = 0;
648 var tokens = this._tokens;
649 while (index < tokens.length) {
650 var token = tokens[index];
651 cb(token, index, tokens);
652 index = token._tokenIndex;
653 index++;
654 }
655 },
656
657 /**
658 * Iterates tokens by type and value(s) from the token array.
659 * Calls passed function for every matched token.
660 *
661 * @param {String} type
662 * @param {String|String[]} value
663 * @param {Function} cb
664 */
665 iterateTokensByTypeAndValue: function(type, value, cb) {
666 var values = (typeof value === 'string') ? [value] : value;
667 var valueIndex = {};
668 values.forEach(function(type) {
669 valueIndex[type] = true;
670 });
671
672 this._forEachToken(function(token, index, tokens) {
673 if (token.type === type && valueIndex[token.value]) {
674 cb(token, index, tokens);
675 }
676 });
677 },
678
679 /**
680 * Returns first token for the specified line.
681 * Line numbers start with 1.
682 *
683 * @param {Number} lineNumber
684 * @param {Object} [options]
685 * @param {Boolean} [options.includeComments = false]
686 * @returns {Object|null}
687 */
688 getFirstTokenOnLine: function(lineNumber, options) {
689 var tokensByLine = this._tokensByLineIndex[lineNumber];
690
691 if (!tokensByLine) {
692 return null;
693 }
694
695 if (options && options.includeComments) {
696 return tokensByLine[0];
697 }
698
699 for (var i = 0; i < tokensByLine.length; i++) {
700 var token = tokensByLine[i];
701 if (!token.isComment) {
702 return token;
703 }
704 }
705
706 return null;
707 },
708
709 /**
710 * Returns last token for the specified line.
711 * Line numbers start with 1.
712 *
713 * @param {Number} lineNumber
714 * @param {Object} [options]
715 * @param {Boolean} [options.includeComments = false]
716 * @returns {Object|null}
717 */
718 getLastTokenOnLine: function(lineNumber, options) {
719 var tokensByLine = this._tokensByLineIndex[lineNumber];
720
721 if (!tokensByLine) {
722 return null;
723 }
724
725 if (options && options.includeComments) {
726 return tokensByLine[tokensByLine.length - 1];
727 }
728
729 for (var i = tokensByLine.length - 1; i >= 0; i--) {
730 var token = tokensByLine[i];
731 if (!token.isComment) {
732 return token;
733 }
734 }
735
736 return null;
737 },
738
739 /**
740 * Returns which dialect of JS this file supports.
741 *
742 * @returns {String}
743 */
744 getDialect: function() {
745 if (this._es6) {
746 return 'es6';
747 }
748
749 if (this._es3) {
750 return 'es3';
751 }
752
753 return 'es5';
754 },
755
756 /**
757 * Returns string representing contents of the file.
758 *
759 * @returns {String}
760 */
761 getSource: function() {
762 return this._source;
763 },
764
765 /**
766 * Returns token tree, built using esprima.
767 *
768 * @returns {Object}
769 */
770 getTree: function() {
771 return this._tree;
772 },
773
774 /**
775 * Returns token list, built using esprima.
776 *
777 * @returns {Object[]}
778 */
779 getTokens: function() {
780 return this._tokens;
781 },
782
783 /**
784 * Set token list.
785 *
786 * @param {Array} tokens
787 */
788 setTokens: function(tokens) {
789 this._tokens = tokens;
790 },
791
792 /**
793 * Returns comment token list, built using esprima.
794 */
795 getComments: function() {
796 return this._tree.comments;
797 },
798
799 /**
800 * Returns source filename for this object representation.
801 *
802 * @returns {String}
803 */
804 getFilename: function() {
805 return this._filename;
806 },
807
808 /**
809 * Returns array of source lines for the file.
810 *
811 * @returns {String[]}
812 */
813 getLines: function() {
814 return this._lines;
815 },
816
817 /**
818 * Returns analyzed scope.
819 *
820 * @returns {Object}
821 */
822 getScope: function() {
823 if (!this._scope) {
824 this._scope = escope.analyze(this._tree, {
825 ecmaVersion: 6,
826 ignoreEval: true,
827 sourceType: 'module'
828 });
829 }
830
831 return this._scope;
832 },
833
834 /**
835 * Returns array of source lines for the file with comments removed.
836 *
837 * @returns {Array}
838 */
839 getLinesWithCommentsRemoved: function() {
840 var lines = this.getLines().concat();
841
842 this.getComments().concat().reverse().forEach(function(comment) {
843 var startLine = comment.loc.start.line;
844 var startCol = comment.loc.start.column;
845 var endLine = comment.loc.end.line;
846 var endCol = comment.loc.end.column;
847 var i = startLine - 1;
848
849 if (startLine === endLine) {
850 // Remove tralling spaces (see gh-1968)
851 lines[i] = lines[i].replace(/\*\/\s+/, '\*\/');
852 lines[i] = lines[i].substring(0, startCol) + lines[i].substring(endCol);
853 } else {
854 lines[i] = lines[i].substring(0, startCol);
855 for (var x = i + 1; x < endLine - 1; x++) {
856 lines[x] = '';
857 }
858
859 lines[x] = lines[x].substring(endCol);
860 }
861 });
862
863 return lines;
864 },
865
866 /**
867 * Renders JS-file sources using token list.
868 *
869 * @returns {String}
870 */
871 render: function() {
872 var result = '';
873
874 // For-loop for maximal speed.
875 for (var i = 0; i < this._tokens.length; i++) {
876 var token = this._tokens[i];
877
878 switch (token.type) {
879 // Line-comment: // ...
880 case 'Line':
881 result += '//' + token.value;
882 break;
883
884 // Block-comment: /* ... */
885 case 'Block':
886 result += '/*' + token.value + '*/';
887 break;
888
889 default:
890 result += token.value;
891 }
892 }
893
894 return result;
895 },
896
897 /**
898 * Returns list of parse errors.
899 *
900 * @returns {Error[]}
901 */
902 getParseErrors: function() {
903 return this._parseErrors;
904 },
905
906 /**
907 * Builds token list using both code tokens and comment-tokens.
908 *
909 * @returns {Object[]}
910 * @private
911 */
912 _buildTokenList: function(codeTokens, commentTokens) {
913 var result = [];
914 var codeQueue = codeTokens.concat();
915 var commentQueue = commentTokens.concat();
916 while (codeQueue.length > 0 || commentQueue.length > 0) {
917 if (codeQueue.length > 0 && (!commentQueue.length || commentQueue[0].range[0] > codeQueue[0].range[0])) {
918 result.push(codeQueue.shift());
919 } else {
920 var commentToken = commentQueue.shift();
921 commentToken.isComment = true;
922 result.push(commentToken);
923 }
924 }
925
926 return result;
927 },
928
929 /**
930 * Adds JSCS-specific EOF (end of file) token.
931 *
932 * @private
933 */
934 _addEOFToken: function(hasErrors) {
935 var loc = hasErrors ?
936 {line: 0, column: 0} :
937 {
938 line: this._lines.length,
939 column: this._lines[this._lines.length - 1].length
940 };
941 this._tokens.push({
942 type: 'EOF',
943 value: '',
944 range: hasErrors ? [0, 0] : [this._source.length, this._source.length + 1],
945 loc: {start: loc, end: loc}
946 });
947 },
948
949 /**
950 * Applies whitespace information to the token list.
951 *
952 * @param {Object[]} tokens
953 * @param {String} source
954 * @private
955 */
956 _addWhitespaceTokens: function(tokens, source) {
957 var prevPos = 0;
958 var result = [];
959
960 // For-loop for maximal speed.
961 for (var i = 0; i < tokens.length; i++) {
962 var token = tokens[i];
963 var rangeStart = token.range[0];
964 if (rangeStart !== prevPos) {
965 var whitespace = source.substring(prevPos, rangeStart);
966 result.push({
967 type: 'Whitespace',
968 value: whitespace,
969 isWhitespace: true
970 });
971 }
972
973 result.push(token);
974
975 prevPos = token.range[1];
976 }
977
978 return result;
979 },
980
981 /**
982 * Builds node indexes using
983 * i. node type as the key
984 * ii. node start range as the key
985 *
986 * @returns {{nodesByType: {}, nodesByStartRange: {}}}
987 * @private
988 */
989 _buildNodeIndex: function() {
990 var nodesByType = {};
991 var nodesByStartRange = {};
992 this.iterate(function(node, parentNode, parentCollection) {
993 var type = node.type;
994
995 node.parentNode = parentNode;
996 node.parentCollection = parentCollection;
997 (nodesByType[type] || (nodesByType[type] = [])).push(node);
998
999 // this part builds a node index that uses node start ranges as the key
1000 var startRange = node.range[0];
1001 (nodesByStartRange[startRange] || (nodesByStartRange[startRange] = [])).push(node);
1002 });
1003
1004 return {
1005 nodesByType: nodesByType,
1006 nodesByStartRange: nodesByStartRange
1007 };
1008 },
1009
1010 /**
1011 * Temporary fix (I hope, two years and counting :-) for esprima/babylon tokenizer
1012 * (https://github.com/jquery/esprima/issues/317)
1013 * Fixes #83, #180
1014 *
1015 * @private
1016 */
1017 _fixEsprimaIdentifiers: function() {
1018 var _this = this;
1019
1020 this.iterateNodesByType(['Property', 'MethodDefinition', 'MemberExpression'], function(node) {
1021 switch (node.type) {
1022 case 'Property':
1023 convertKeywordToIdentifierIfRequired(node.key);
1024 break;
1025 case 'MethodDefinition':
1026 convertKeywordToIdentifierIfRequired(node.key);
1027 break;
1028 case 'MemberExpression':
1029 convertKeywordToIdentifierIfRequired(node.property);
1030 break;
1031 }
1032 });
1033
1034 function convertKeywordToIdentifierIfRequired(node) {
1035 var token = _this.getTokenByRangeStart(node.range[0]);
1036
1037 if (token.type === 'Keyword') {
1038 token.type = 'Identifier';
1039 }
1040 }
1041 }
1042};
1043
1044/**
1045 * Parses a JS-file.
1046 *
1047 * @param {String} source
1048 * @param {Object} esprima
1049 * @param {Object} [esprimaOptions]
1050 * @returns {Object}
1051 */
1052function parseJavaScriptSource(source, esprima, esprimaOptions) {
1053 var finalEsprimaOptions = {
1054 tolerant: true
1055 };
1056
1057 if (esprimaOptions) {
1058 for (var key in esprimaOptions) {
1059 finalEsprimaOptions[key] = esprimaOptions[key];
1060 }
1061 }
1062
1063 // Set required options
1064 finalEsprimaOptions.loc = true;
1065 finalEsprimaOptions.range = true;
1066 finalEsprimaOptions.comment = true;
1067 finalEsprimaOptions.tokens = true;
1068 finalEsprimaOptions.sourceType = 'module';
1069
1070 var hashbang = source.indexOf('#!') === 0;
1071 var tree;
1072
1073 // Convert bin annotation to a comment
1074 if (hashbang) {
1075 source = '//' + source.substr(2);
1076 }
1077
1078 var instrumentationData = {};
1079 var hasInstrumentationData = false;
1080
1081 // Process special case code like iOS instrumentation imports: `#import 'abc.js';`
1082 source = source.replace(/^#!?[^\n]+\n/gm, function(str, pos) {
1083 hasInstrumentationData = true;
1084 instrumentationData[pos] = str.substring(0, str.length - 1);
1085 return '//' + str.slice(2);
1086 });
1087
1088 var gritData = {};
1089 var hasGritData = false;
1090
1091 // Process grit tags like `<if ...>` and `<include ...>`
1092 source = source.replace(/^\s*<\/?\s*(if|include)(?!\w)[^]*?>/gim, function(str, p1, pos) {
1093 hasGritData = true;
1094 gritData[pos] = str.substring(0, str.length - 1);
1095
1096 // Cut 4 characters to save correct line/column info for surrounding code
1097 return '/*' + str.slice(4) + '*/';
1098 });
1099
1100 tree = esprima.parse(source, finalEsprimaOptions);
1101
1102 // Change the bin annotation comment
1103 if (hashbang) {
1104 tree.comments[0].type = 'Hashbang';
1105 tree.comments[0].value = '#!' + tree.comments[0].value;
1106 }
1107
1108 if (hasInstrumentationData) {
1109 tree.comments.forEach(function(token) {
1110 var rangeStart = token.range[0];
1111 if (instrumentationData.hasOwnProperty(rangeStart)) {
1112 token.type = 'InstrumentationDirective';
1113 token.value = instrumentationData[rangeStart];
1114 }
1115 });
1116 }
1117
1118 if (hasGritData) {
1119 tree.comments.forEach(function(token) {
1120 var rangeStart = token.range[0];
1121 if (gritData.hasOwnProperty(rangeStart)) {
1122 token.type = 'GritTag';
1123 token.value = gritData[rangeStart];
1124 }
1125 });
1126 }
1127
1128 return tree;
1129}
1130
1131module.exports = JsFile;