UNPKG

espree

Version:

An Esprima-compatible JavaScript parser built on Acorn

494 lines (433 loc) 13.3 kB
/* eslint no-param-reassign: 0 -- stylistic choice */ import TokenTranslator from "./token-translator.js"; import { normalizeOptions } from "./options.js"; /** * @import { * CommentType, * EspreeParserCtor, * EsprimaNode, * AcornJsxParserCtorEnhanced, * TokTypes * } from "./types.js"; * @import { * Options, * EspreeToken as EsprimaToken, * EspreeTokens as EsprimaTokens, * EspreeComment as EsprimaComment * } from "../espree.js"; * @import { NormalizedEcmaVersion } from "./options.js"; * @import * as acorn from "acorn"; */ /** * @typedef {{ * originalSourceType: "script" | "module" | "commonjs" | undefined * tokens: EsprimaToken[] | null, * comments: EsprimaComment[] | null, * impliedStrict: boolean, * ecmaVersion: NormalizedEcmaVersion, * jsxAttrValueToken: boolean, * lastToken: acorn.Token | null, * templateElements: acorn.TemplateElement[] * }} State */ /** * @typedef {{ * sourceType?: "script"|"module"|"commonjs"; * comments?: EsprimaComment[]; * tokens?: EsprimaToken[]; * body: acorn.Node[]; * } & acorn.Program} EsprimaProgramNode */ // ---------------------------------------------------------------------------- // Types exported from file // ---------------------------------------------------------------------------- /** * @typedef {{ * index?: number; * lineNumber?: number; * column?: number; * } & SyntaxError} EnhancedSyntaxError */ // We add `jsxAttrValueToken` ourselves. /** * @typedef {{ * jsxAttrValueToken?: acorn.TokenType; * } & TokTypes} EnhancedTokTypes */ const STATE = Symbol("espree's internal state"); const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode"); /** * Converts an Acorn comment to a Esprima comment. * @param {boolean} block True if it's a block comment, false if not. * @param {string} text The text of the comment. * @param {number} start The index at which the comment starts. * @param {number} end The index at which the comment ends. * @param {acorn.Position | undefined} startLoc The location at which the comment starts. * @param {acorn.Position | undefined} endLoc The location at which the comment ends. * @param {string} code The source code being parsed. * @returns {EsprimaComment} The comment object. * @private */ function convertAcornCommentToEsprimaComment( block, text, start, end, startLoc, endLoc, code, ) { /** @type {CommentType} */ let type; if (block) { type = "Block"; } else if (code.slice(start, start + 2) === "#!") { type = "Hashbang"; } else { type = "Line"; } /** * @type {{ * type: CommentType, * value: string, * start?: number, * end?: number, * range?: [number, number], * loc?: { * start: acorn.Position | undefined, * end: acorn.Position | undefined * } * }} */ const comment = { type, value: text, }; if (typeof start === "number") { comment.start = start; comment.end = end; comment.range = [start, end]; } if (typeof startLoc === "object") { comment.loc = { start: startLoc, end: endLoc, }; } return comment; } // eslint-disable-next-line arrow-body-style -- For TS export default () => { /** * Returns the Espree parser. * @param {AcornJsxParserCtorEnhanced} Parser The Acorn parser. The `acorn` property is missing from acorn's * TypeScript but is present statically on the class. * @returns {EspreeParserCtor} The Espree Parser constructor. */ return Parser => { const tokTypes = /** @type {EnhancedTokTypes} */ ( Object.assign({}, Parser.acorn.tokTypes) ); if (Parser.acornJsx) { Object.assign(tokTypes, Parser.acornJsx.tokTypes); } return class Espree extends Parser { /** * @param {Options | null | undefined} opts The parser options * @param {string | object} code The code which will be converted to a string. */ constructor(opts, code) { if (typeof opts !== "object" || opts === null) { opts = {}; } if (typeof code !== "string" && !(code instanceof String)) { code = String(code); } // save original source type in case of commonjs const originalSourceType = opts.sourceType; const options = normalizeOptions(opts); const ecmaFeatures = options.ecmaFeatures || {}; const tokenTranslator = options.tokens === true ? new TokenTranslator( tokTypes, // @ts-expect-error Appears to be a TS bug since the type is indeed string|String code, ) : null; /** * Data that is unique to Espree and is not represented internally * in Acorn. * * For ES2023 hashbangs, Espree will call `onComment()` during the * constructor, so we must define state before having access to * `this`. * @type {State} */ const state = { originalSourceType: originalSourceType || options.sourceType, tokens: tokenTranslator ? [] : null, comments: options.comment === true ? [] : null, impliedStrict: ecmaFeatures.impliedStrict === true && options.ecmaVersion >= 5, ecmaVersion: options.ecmaVersion, jsxAttrValueToken: false, lastToken: null, templateElements: [], }; // Initialize acorn parser. super( { // do not use spread, because we don't want to pass any unknown options to acorn ecmaVersion: options.ecmaVersion, sourceType: options.sourceType, ranges: options.ranges, locations: options.locations, allowReserved: options.allowReserved, // Truthy value is true for backward compatibility. allowReturnOutsideFunction: options.allowReturnOutsideFunction, // Collect tokens onToken(token) { if (tokenTranslator) { // Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state. tokenTranslator.onToken( token, /** * @type {Omit<State, "tokens"> & { * tokens: EsprimaToken[] * }} */ (state), ); } if (token.type !== tokTypes.eof) { state.lastToken = token; } }, // Collect comments onComment(block, text, start, end, startLoc, endLoc) { if (state.comments) { const comment = convertAcornCommentToEsprimaComment( block, text, start, end, startLoc, endLoc, // @ts-expect-error Appears to be a TS bug // since the type is indeed string|String code, ); state.comments.push(comment); } }, }, // @ts-expect-error Appears to be a TS bug // since the type is indeed string|String code, ); /* * We put all of this data into a symbol property as a way to avoid * potential naming conflicts with future versions of Acorn. */ this[STATE] = state; } /** * Returns Espree tokens. * @returns {EsprimaTokens} The Esprima-compatible tokens */ tokenize() { do { this.next(); } while (this.type !== tokTypes.eof); // Consume the final eof token this.next(); const extra = this[STATE]; const tokens = /** @type {EsprimaTokens} */ (extra.tokens); if (extra.comments) { tokens.comments = extra.comments; } return tokens; } /** * Calls parent. * @param {acorn.Node} node The node * @param {string} type The type * @returns {acorn.Node} The altered Node */ finishNode(node, type) { const result = super.finishNode(node, type); return this[ESPRIMA_FINISH_NODE](result); } /** * Calls parent. * @param {acorn.Node} node The node * @param {string} type The type * @param {number} pos The position * @param {acorn.Position} loc The location * @returns {acorn.Node} The altered Node */ finishNodeAt(node, type, pos, loc) { const result = super.finishNodeAt(node, type, pos, loc); return this[ESPRIMA_FINISH_NODE](result); } /** * Parses. * @returns {EsprimaProgramNode} The program Node */ parse() { const extra = this[STATE]; const prog = super.parse(); const program = /** @type {EsprimaProgramNode} */ (prog); // @ts-expect-error TS bug? We've already converted to `EsprimaProgramNode` program.sourceType = extra.originalSourceType; if (extra.comments) { program.comments = extra.comments; } if (extra.tokens) { program.tokens = extra.tokens; } /* * https://github.com/eslint/espree/issues/349 * Ensure that template elements have correct range information. * This is one location where Acorn produces a different value * for its start and end properties vs. the values present in the * range property. In order to avoid confusion, we set the start * and end properties to the values that are present in range. * This is done here, instead of in finishNode(), because Acorn * uses the values of start and end internally while parsing, making * it dangerous to change those values while parsing is ongoing. * By waiting until the end of parsing, we can safely change these * values without affect any other part of the process. */ this[STATE].templateElements.forEach(templateElement => { const startOffset = -1; const endOffset = templateElement.tail ? 1 : 2; templateElement.start += startOffset; templateElement.end += endOffset; if (templateElement.range) { templateElement.range[0] += startOffset; templateElement.range[1] += endOffset; } if (templateElement.loc) { templateElement.loc.start.column += startOffset; templateElement.loc.end.column += endOffset; } }); return program; } /** * Parses top level. * @param {acorn.Node} node AST Node * @returns {acorn.Node} The changed node */ parseTopLevel(node) { if (this[STATE].impliedStrict) { this.strict = true; } return super.parseTopLevel(node); } /** * Overwrites the default raise method to throw Esprima-style errors. * @param {number} pos The position of the error. * @param {string} message The error message. * @throws {EnhancedSyntaxError} A syntax error. * @returns {void} */ raise(pos, message) { const loc = Parser.acorn.getLineInfo(this.input, pos); const err = /** @type {EnhancedSyntaxError} */ ( new SyntaxError(message) ); err.index = pos; err.lineNumber = loc.line; err.column = loc.column + 1; // acorn uses 0-based columns throw err; } /** * Overwrites the default raise method to throw Esprima-style errors. * @param {number} pos The position of the error. * @param {string} message The error message. * @throws {SyntaxError} A syntax error. * @returns {void} */ raiseRecoverable(pos, message) { this.raise(pos, message); } /** * Overwrites the default unexpected method to throw Esprima-style errors. * @param {number} pos The position of the error. * @throws {SyntaxError} A syntax error. * @returns {void} */ unexpected(pos) { let message = "Unexpected token"; if (pos !== null && pos !== void 0) { this.pos = pos; if (this.options.locations) { while (this.pos < this.lineStart) { this.lineStart = this.input.lastIndexOf( "\n", this.lineStart - 2, ) + 1; --this.curLine; } } this.nextToken(); } if (this.end > this.start) { message += ` ${this.input.slice(this.start, this.end)}`; } this.raise(this.start, message); } /** * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX * uses regular tt.string without any distinction between this and regular JS * strings. As such, we intercept an attempt to read a JSX string and set a flag * on extra so that when tokens are converted, the next token will be switched * to JSXText via onToken. * @param {number} quote A character code * @returns {void} */ // eslint-disable-next-line camelcase -- required by API jsx_readString(quote) { const result = super.jsx_readString(quote); if (this.type === tokTypes.string) { this[STATE].jsxAttrValueToken = true; } return result; } /** * Performs last-minute Esprima-specific compatibility checks and fixes. * @param {acorn.Node} result The node to check. * @returns {EsprimaNode} The finished node. */ [ESPRIMA_FINISH_NODE](result) { // Acorn doesn't count the opening and closing backticks as part of templates // so we have to adjust ranges/locations appropriately. if (result.type === "TemplateElement") { // save template element references to fix start/end later this[STATE].templateElements.push( /** @type {acorn.TemplateElement} */ (result), ); } if ( result.type.includes("Function") && !("generator" in result) ) { /** * @type {acorn.FunctionDeclaration|acorn.FunctionExpression| * acorn.ArrowFunctionExpression} */ (result).generator = false; } return result; } }; }; };