espree.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. "use strict";
  2. /* eslint-disable no-param-reassign*/
  3. const acorn = require("acorn");
  4. const jsx = require("acorn-jsx");
  5. const TokenTranslator = require("./token-translator");
  6. const DEFAULT_ECMA_VERSION = 5;
  7. const STATE = Symbol("espree's internal state");
  8. const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode");
  9. const tokTypes = Object.assign({}, acorn.tokTypes, jsx.tokTypes);
  10. /**
  11. * Normalize ECMAScript version from the initial config
  12. * @param {number} ecmaVersion ECMAScript version from the initial config
  13. * @returns {number} normalized ECMAScript version
  14. */
  15. function normalizeEcmaVersion(ecmaVersion) {
  16. if (typeof ecmaVersion === "number") {
  17. let version = ecmaVersion;
  18. // Calculate ECMAScript edition number from official year version starting with
  19. // ES2015, which corresponds with ES6 (or a difference of 2009).
  20. if (version >= 2015) {
  21. version -= 2009;
  22. }
  23. switch (version) {
  24. case 3:
  25. case 5:
  26. case 6:
  27. case 7:
  28. case 8:
  29. case 9:
  30. case 10:
  31. return version;
  32. default:
  33. throw new Error("Invalid ecmaVersion.");
  34. }
  35. } else {
  36. return DEFAULT_ECMA_VERSION;
  37. }
  38. }
  39. /**
  40. * Converts an Acorn comment to a Esprima comment.
  41. * @param {boolean} block True if it's a block comment, false if not.
  42. * @param {string} text The text of the comment.
  43. * @param {int} start The index at which the comment starts.
  44. * @param {int} end The index at which the comment ends.
  45. * @param {Location} startLoc The location at which the comment starts.
  46. * @param {Location} endLoc The location at which the comment ends.
  47. * @returns {Object} The comment object.
  48. * @private
  49. */
  50. function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc) {
  51. const comment = {
  52. type: block ? "Block" : "Line",
  53. value: text
  54. };
  55. if (typeof start === "number") {
  56. comment.start = start;
  57. comment.end = end;
  58. comment.range = [start, end];
  59. }
  60. if (typeof startLoc === "object") {
  61. comment.loc = {
  62. start: startLoc,
  63. end: endLoc
  64. };
  65. }
  66. return comment;
  67. }
  68. module.exports = () => Parser => class Espree extends Parser {
  69. constructor(options, code) {
  70. if (typeof options !== "object" || options === null) {
  71. options = {};
  72. }
  73. if (typeof code !== "string" && !(code instanceof String)) {
  74. code = String(code);
  75. }
  76. const ecmaFeatures = options.ecmaFeatures || {};
  77. const ecmaVersion = normalizeEcmaVersion(options.ecmaVersion);
  78. const isModule = options.sourceType === "module";
  79. const tokenTranslator =
  80. options.tokens === true
  81. ? new TokenTranslator(tokTypes, code)
  82. : null;
  83. // Initialize acorn parser.
  84. super({
  85. ecmaVersion: isModule ? Math.max(6, ecmaVersion) : ecmaVersion,
  86. sourceType: isModule ? "module" : "script",
  87. ranges: options.range === true,
  88. locations: options.loc === true,
  89. // Truthy value is true for backward compatibility.
  90. allowReturnOutsideFunction: Boolean(ecmaFeatures.globalReturn),
  91. // Collect tokens
  92. onToken: token => {
  93. if (tokenTranslator) {
  94. // Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state.
  95. tokenTranslator.onToken(token, this[STATE]);
  96. }
  97. if (token.type !== tokTypes.eof) {
  98. this[STATE].lastToken = token;
  99. }
  100. },
  101. // Collect comments
  102. onComment: (block, text, start, end, startLoc, endLoc) => {
  103. if (this[STATE].comments) {
  104. const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc);
  105. this[STATE].comments.push(comment);
  106. }
  107. }
  108. }, code);
  109. // Initialize internal state.
  110. this[STATE] = {
  111. tokens: tokenTranslator ? [] : null,
  112. comments: options.comment === true ? [] : null,
  113. impliedStrict: ecmaFeatures.impliedStrict === true && this.options.ecmaVersion >= 5,
  114. ecmaVersion: this.options.ecmaVersion,
  115. jsxAttrValueToken: false,
  116. lastToken: null
  117. };
  118. }
  119. tokenize() {
  120. do {
  121. this.next();
  122. } while (this.type !== tokTypes.eof);
  123. const extra = this[STATE];
  124. const tokens = extra.tokens;
  125. if (extra.comments) {
  126. tokens.comments = extra.comments;
  127. }
  128. return tokens;
  129. }
  130. finishNode(...args) {
  131. const result = super.finishNode(...args);
  132. return this[ESPRIMA_FINISH_NODE](result);
  133. }
  134. finishNodeAt(...args) {
  135. const result = super.finishNodeAt(...args);
  136. return this[ESPRIMA_FINISH_NODE](result);
  137. }
  138. parse() {
  139. const extra = this[STATE];
  140. const program = super.parse();
  141. program.sourceType = this.options.sourceType;
  142. if (extra.comments) {
  143. program.comments = extra.comments;
  144. }
  145. if (extra.tokens) {
  146. program.tokens = extra.tokens;
  147. }
  148. /*
  149. * Adjust opening and closing position of program to match Esprima.
  150. * Acorn always starts programs at range 0 whereas Esprima starts at the
  151. * first AST node's start (the only real difference is when there's leading
  152. * whitespace or leading comments). Acorn also counts trailing whitespace
  153. * as part of the program whereas Esprima only counts up to the last token.
  154. */
  155. if (program.range) {
  156. program.range[0] = program.body.length ? program.body[0].range[0] : program.range[0];
  157. program.range[1] = extra.lastToken ? extra.lastToken.range[1] : program.range[1];
  158. }
  159. if (program.loc) {
  160. program.loc.start = program.body.length ? program.body[0].loc.start : program.loc.start;
  161. program.loc.end = extra.lastToken ? extra.lastToken.loc.end : program.loc.end;
  162. }
  163. return program;
  164. }
  165. parseTopLevel(node) {
  166. if (this[STATE].impliedStrict) {
  167. this.strict = true;
  168. }
  169. return super.parseTopLevel(node);
  170. }
  171. /**
  172. * Overwrites the default raise method to throw Esprima-style errors.
  173. * @param {int} pos The position of the error.
  174. * @param {string} message The error message.
  175. * @throws {SyntaxError} A syntax error.
  176. * @returns {void}
  177. */
  178. raise(pos, message) {
  179. const loc = acorn.getLineInfo(this.input, pos);
  180. const err = new SyntaxError(message);
  181. err.index = pos;
  182. err.lineNumber = loc.line;
  183. err.column = loc.column + 1; // acorn uses 0-based columns
  184. throw err;
  185. }
  186. /**
  187. * Overwrites the default raise method to throw Esprima-style errors.
  188. * @param {int} pos The position of the error.
  189. * @param {string} message The error message.
  190. * @throws {SyntaxError} A syntax error.
  191. * @returns {void}
  192. */
  193. raiseRecoverable(pos, message) {
  194. this.raise(pos, message);
  195. }
  196. /**
  197. * Overwrites the default unexpected method to throw Esprima-style errors.
  198. * @param {int} pos The position of the error.
  199. * @throws {SyntaxError} A syntax error.
  200. * @returns {void}
  201. */
  202. unexpected(pos) {
  203. let message = "Unexpected token";
  204. if (pos !== null && pos !== void 0) {
  205. this.pos = pos;
  206. if (this.options.locations) {
  207. while (this.pos < this.lineStart) {
  208. this.lineStart = this.input.lastIndexOf("\n", this.lineStart - 2) + 1;
  209. --this.curLine;
  210. }
  211. }
  212. this.nextToken();
  213. }
  214. if (this.end > this.start) {
  215. message += ` ${this.input.slice(this.start, this.end)}`;
  216. }
  217. this.raise(this.start, message);
  218. }
  219. /*
  220. * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX
  221. * uses regular tt.string without any distinction between this and regular JS
  222. * strings. As such, we intercept an attempt to read a JSX string and set a flag
  223. * on extra so that when tokens are converted, the next token will be switched
  224. * to JSXText via onToken.
  225. */
  226. jsx_readString(quote) { // eslint-disable-line camelcase
  227. const result = super.jsx_readString(quote);
  228. if (this.type === tokTypes.string) {
  229. this[STATE].jsxAttrValueToken = true;
  230. }
  231. return result;
  232. }
  233. /**
  234. * Performs last-minute Esprima-specific compatibility checks and fixes.
  235. * @param {ASTNode} result The node to check.
  236. * @returns {ASTNode} The finished node.
  237. */
  238. [ESPRIMA_FINISH_NODE](result) {
  239. // Acorn doesn't count the opening and closing backticks as part of templates
  240. // so we have to adjust ranges/locations appropriately.
  241. if (result.type === "TemplateElement") {
  242. // additional adjustment needed if ${ is the last token
  243. const terminalDollarBraceL = this.input.slice(result.end, result.end + 2) === "${";
  244. if (result.range) {
  245. result.range[0]--;
  246. result.range[1] += (terminalDollarBraceL ? 2 : 1);
  247. }
  248. if (result.loc) {
  249. result.loc.start.column--;
  250. result.loc.end.column += (terminalDollarBraceL ? 2 : 1);
  251. }
  252. }
  253. if (result.type.indexOf("Function") > -1 && !result.generator) {
  254. result.generator = false;
  255. }
  256. return result;
  257. }
  258. };