espree.js 12 KB

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