espree.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. /* eslint-disable no-param-reassign*/
  2. import TokenTranslator from "./token-translator.js";
  3. import { normalizeOptions } from "./options.js";
  4. const STATE = Symbol("espree's internal state");
  5. const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode");
  6. /**
  7. * Converts an Acorn comment to a Esprima comment.
  8. * @param {boolean} block True if it's a block comment, false if not.
  9. * @param {string} text The text of the comment.
  10. * @param {int} start The index at which the comment starts.
  11. * @param {int} end The index at which the comment ends.
  12. * @param {Location} startLoc The location at which the comment starts.
  13. * @param {Location} endLoc The location at which the comment ends.
  14. * @returns {Object} The comment object.
  15. * @private
  16. */
  17. function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc) {
  18. const comment = {
  19. type: block ? "Block" : "Line",
  20. value: text
  21. };
  22. if (typeof start === "number") {
  23. comment.start = start;
  24. comment.end = end;
  25. comment.range = [start, end];
  26. }
  27. if (typeof startLoc === "object") {
  28. comment.loc = {
  29. start: startLoc,
  30. end: endLoc
  31. };
  32. }
  33. return comment;
  34. }
  35. export default () => Parser => {
  36. const tokTypes = Object.assign({}, Parser.acorn.tokTypes);
  37. if (Parser.acornJsx) {
  38. Object.assign(tokTypes, Parser.acornJsx.tokTypes);
  39. }
  40. return class Espree extends Parser {
  41. constructor(opts, code) {
  42. if (typeof opts !== "object" || opts === null) {
  43. opts = {};
  44. }
  45. if (typeof code !== "string" && !(code instanceof String)) {
  46. code = String(code);
  47. }
  48. // save original source type in case of commonjs
  49. const originalSourceType = opts.sourceType;
  50. const options = normalizeOptions(opts);
  51. const ecmaFeatures = options.ecmaFeatures || {};
  52. const tokenTranslator =
  53. options.tokens === true
  54. ? new TokenTranslator(tokTypes, code)
  55. : null;
  56. // Initialize acorn parser.
  57. super({
  58. // do not use spread, because we don't want to pass any unknown options to acorn
  59. ecmaVersion: options.ecmaVersion,
  60. sourceType: options.sourceType,
  61. ranges: options.ranges,
  62. locations: options.locations,
  63. allowReserved: options.allowReserved,
  64. // Truthy value is true for backward compatibility.
  65. allowReturnOutsideFunction: options.allowReturnOutsideFunction,
  66. // Collect tokens
  67. onToken: token => {
  68. if (tokenTranslator) {
  69. // Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state.
  70. tokenTranslator.onToken(token, this[STATE]);
  71. }
  72. if (token.type !== tokTypes.eof) {
  73. this[STATE].lastToken = token;
  74. }
  75. },
  76. // Collect comments
  77. onComment: (block, text, start, end, startLoc, endLoc) => {
  78. if (this[STATE].comments) {
  79. const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc);
  80. this[STATE].comments.push(comment);
  81. }
  82. }
  83. }, code);
  84. /*
  85. * Data that is unique to Espree and is not represented internally in
  86. * Acorn. We put all of this data into a symbol property as a way to
  87. * avoid potential naming conflicts with future versions of Acorn.
  88. */
  89. this[STATE] = {
  90. originalSourceType: originalSourceType || options.sourceType,
  91. tokens: tokenTranslator ? [] : null,
  92. comments: options.comment === true ? [] : null,
  93. impliedStrict: ecmaFeatures.impliedStrict === true && this.options.ecmaVersion >= 5,
  94. ecmaVersion: this.options.ecmaVersion,
  95. jsxAttrValueToken: false,
  96. lastToken: null,
  97. templateElements: []
  98. };
  99. }
  100. tokenize() {
  101. do {
  102. this.next();
  103. } while (this.type !== tokTypes.eof);
  104. // Consume the final eof token
  105. this.next();
  106. const extra = this[STATE];
  107. const tokens = extra.tokens;
  108. if (extra.comments) {
  109. tokens.comments = extra.comments;
  110. }
  111. return tokens;
  112. }
  113. finishNode(...args) {
  114. const result = super.finishNode(...args);
  115. return this[ESPRIMA_FINISH_NODE](result);
  116. }
  117. finishNodeAt(...args) {
  118. const result = super.finishNodeAt(...args);
  119. return this[ESPRIMA_FINISH_NODE](result);
  120. }
  121. parse() {
  122. const extra = this[STATE];
  123. const program = super.parse();
  124. program.sourceType = extra.originalSourceType;
  125. if (extra.comments) {
  126. program.comments = extra.comments;
  127. }
  128. if (extra.tokens) {
  129. program.tokens = extra.tokens;
  130. }
  131. /*
  132. * Adjust opening and closing position of program to match Esprima.
  133. * Acorn always starts programs at range 0 whereas Esprima starts at the
  134. * first AST node's start (the only real difference is when there's leading
  135. * whitespace or leading comments). Acorn also counts trailing whitespace
  136. * as part of the program whereas Esprima only counts up to the last token.
  137. */
  138. if (program.body.length) {
  139. const [firstNode] = program.body;
  140. if (program.range) {
  141. program.range[0] = firstNode.range[0];
  142. }
  143. if (program.loc) {
  144. program.loc.start = firstNode.loc.start;
  145. }
  146. program.start = firstNode.start;
  147. }
  148. if (extra.lastToken) {
  149. if (program.range) {
  150. program.range[1] = extra.lastToken.range[1];
  151. }
  152. if (program.loc) {
  153. program.loc.end = extra.lastToken.loc.end;
  154. }
  155. program.end = extra.lastToken.end;
  156. }
  157. /*
  158. * https://github.com/eslint/espree/issues/349
  159. * Ensure that template elements have correct range information.
  160. * This is one location where Acorn produces a different value
  161. * for its start and end properties vs. the values present in the
  162. * range property. In order to avoid confusion, we set the start
  163. * and end properties to the values that are present in range.
  164. * This is done here, instead of in finishNode(), because Acorn
  165. * uses the values of start and end internally while parsing, making
  166. * it dangerous to change those values while parsing is ongoing.
  167. * By waiting until the end of parsing, we can safely change these
  168. * values without affect any other part of the process.
  169. */
  170. this[STATE].templateElements.forEach(templateElement => {
  171. const startOffset = -1;
  172. const endOffset = templateElement.tail ? 1 : 2;
  173. templateElement.start += startOffset;
  174. templateElement.end += endOffset;
  175. if (templateElement.range) {
  176. templateElement.range[0] += startOffset;
  177. templateElement.range[1] += endOffset;
  178. }
  179. if (templateElement.loc) {
  180. templateElement.loc.start.column += startOffset;
  181. templateElement.loc.end.column += endOffset;
  182. }
  183. });
  184. return program;
  185. }
  186. parseTopLevel(node) {
  187. if (this[STATE].impliedStrict) {
  188. this.strict = true;
  189. }
  190. return super.parseTopLevel(node);
  191. }
  192. /**
  193. * Overwrites the default raise method to throw Esprima-style errors.
  194. * @param {int} pos The position of the error.
  195. * @param {string} message The error message.
  196. * @throws {SyntaxError} A syntax error.
  197. * @returns {void}
  198. */
  199. raise(pos, message) {
  200. const loc = Parser.acorn.getLineInfo(this.input, pos);
  201. const err = new SyntaxError(message);
  202. err.index = pos;
  203. err.lineNumber = loc.line;
  204. err.column = loc.column + 1; // acorn uses 0-based columns
  205. throw err;
  206. }
  207. /**
  208. * Overwrites the default raise method to throw Esprima-style errors.
  209. * @param {int} pos The position of the error.
  210. * @param {string} message The error message.
  211. * @throws {SyntaxError} A syntax error.
  212. * @returns {void}
  213. */
  214. raiseRecoverable(pos, message) {
  215. this.raise(pos, message);
  216. }
  217. /**
  218. * Overwrites the default unexpected method to throw Esprima-style errors.
  219. * @param {int} pos The position of the error.
  220. * @throws {SyntaxError} A syntax error.
  221. * @returns {void}
  222. */
  223. unexpected(pos) {
  224. let message = "Unexpected token";
  225. if (pos !== null && pos !== void 0) {
  226. this.pos = pos;
  227. if (this.options.locations) {
  228. while (this.pos < this.lineStart) {
  229. this.lineStart = this.input.lastIndexOf("\n", this.lineStart - 2) + 1;
  230. --this.curLine;
  231. }
  232. }
  233. this.nextToken();
  234. }
  235. if (this.end > this.start) {
  236. message += ` ${this.input.slice(this.start, this.end)}`;
  237. }
  238. this.raise(this.start, message);
  239. }
  240. /*
  241. * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX
  242. * uses regular tt.string without any distinction between this and regular JS
  243. * strings. As such, we intercept an attempt to read a JSX string and set a flag
  244. * on extra so that when tokens are converted, the next token will be switched
  245. * to JSXText via onToken.
  246. */
  247. jsx_readString(quote) { // eslint-disable-line camelcase
  248. const result = super.jsx_readString(quote);
  249. if (this.type === tokTypes.string) {
  250. this[STATE].jsxAttrValueToken = true;
  251. }
  252. return result;
  253. }
  254. /**
  255. * Performs last-minute Esprima-specific compatibility checks and fixes.
  256. * @param {ASTNode} result The node to check.
  257. * @returns {ASTNode} The finished node.
  258. */
  259. [ESPRIMA_FINISH_NODE](result) {
  260. // Acorn doesn't count the opening and closing backticks as part of templates
  261. // so we have to adjust ranges/locations appropriately.
  262. if (result.type === "TemplateElement") {
  263. // save template element references to fix start/end later
  264. this[STATE].templateElements.push(result);
  265. }
  266. if (result.type.includes("Function") && !result.generator) {
  267. result.generator = false;
  268. }
  269. return result;
  270. }
  271. };
  272. };