UNPKG

espree

Version:

An Esprima-compatible JavaScript parser built on Acorn

307 lines (272 loc) 8.39 kB
/** * @fileoverview Translates tokens between Acorn format and Esprima format. * @author Nicholas C. Zakas */ /** * @import * as acorn from "acorn"; * @import { EnhancedTokTypes } from "./espree.js" * @import { NormalizedEcmaVersion } from "./options.js"; * @import { EspreeToken as EsprimaToken } from "../espree.js"; */ /** * Based on the `acorn.Token` class, but without a fixed `type` (since we need * it to be a string). Avoiding `type` lets us make one extending interface * more strict and another more lax. * * We could make `value` more strict to `string` even though the original is * `any`. * * `start` and `end` are required in `acorn.Token` * * `loc` and `range` are from `acorn.Token` * * Adds `regex`. */ /** * @typedef {{ * jsxAttrValueToken: boolean; * ecmaVersion: NormalizedEcmaVersion; * }} ExtraNoTokens * @typedef {{ * tokens: EsprimaToken[] * } & ExtraNoTokens} Extra */ //------------------------------------------------------------------------------ // Private //------------------------------------------------------------------------------ // Esprima Token Types const Token = { Boolean: "Boolean", EOF: "<end>", Identifier: "Identifier", PrivateIdentifier: "PrivateIdentifier", Keyword: "Keyword", Null: "Null", Numeric: "Numeric", Punctuator: "Punctuator", String: "String", RegularExpression: "RegularExpression", Template: "Template", JSXIdentifier: "JSXIdentifier", JSXText: "JSXText", }; /** * Converts part of a template into an Esprima token. * @param {acorn.Token[]} tokens The Acorn tokens representing the template. * @param {string} code The source code. * @returns {EsprimaToken} The Esprima equivalent of the template token. * @private */ function convertTemplatePart(tokens, code) { const firstToken = tokens[0], lastTemplateToken = /** @type {acorn.Token & { loc: acorn.SourceLocation, range: [number, number] }} */ ( tokens.at(-1) ); /** @type {EsprimaToken} */ const token = { type: Token.Template, value: code.slice(firstToken.start, lastTemplateToken.end), }; if (firstToken.loc) { token.loc = { start: firstToken.loc.start, end: lastTemplateToken.loc.end, }; } if (firstToken.range) { token.start = firstToken.range[0]; token.end = lastTemplateToken.range[1]; token.range = [token.start, token.end]; } return token; } /* eslint-disable jsdoc/check-types -- The API allows either */ /** * Contains logic to translate Acorn tokens into Esprima tokens. */ class TokenTranslator { /** * Contains logic to translate Acorn tokens into Esprima tokens. * @param {EnhancedTokTypes} acornTokTypes The Acorn token types. * @param {string|String} code The source code Acorn is parsing. This is necessary * to correct the "value" property of some tokens. */ constructor(acornTokTypes, code) { /* eslint-enable jsdoc/check-types -- The API allows either */ // token types this._acornTokTypes = acornTokTypes; // token buffer for templates /** @type {acorn.Token[]} */ this._tokens = []; // track the last curly brace this._curlyBrace = null; // the source code this._code = code; } /** * Translates a single Esprima token to a single Acorn token. This may be * inaccurate due to how templates are handled differently in Esprima and * Acorn, but should be accurate for all other tokens. * @param {acorn.Token} token The Acorn token to translate. * @param {ExtraNoTokens} extra Espree extra object. * @returns {EsprimaToken} The Esprima version of the token. */ translate(token, extra) { const type = token.type, tt = this._acornTokTypes, // We use an unknown type because `acorn.Token` is a class whose // `type` property we cannot override to our desired `string`; // this also allows us to define a stricter `EsprimaToken` with // a string-only `type` property unknownTokenType = /** @type {unknown} */ (token), newToken = /** @type {EsprimaToken} */ (unknownTokenType); if (type === tt.name) { newToken.type = Token.Identifier; // TODO: See if this is an Acorn bug if ("value" in token && token.value === "static") { newToken.type = Token.Keyword; } if ( extra.ecmaVersion > 5 && "value" in token && (token.value === "yield" || token.value === "let") ) { newToken.type = Token.Keyword; } } else if (type === tt.privateId) { newToken.type = Token.PrivateIdentifier; } else if ( type === tt.semi || type === tt.comma || type === tt.parenL || type === tt.parenR || type === tt.braceL || type === tt.braceR || type === tt.dot || type === tt.bracketL || type === tt.colon || type === tt.question || type === tt.bracketR || type === tt.ellipsis || type === tt.arrow || type === tt.jsxTagStart || type === tt.incDec || type === tt.starstar || type === tt.jsxTagEnd || type === tt.prefix || type === tt.questionDot || ("binop" in type && type.binop && !type.keyword) || ("isAssign" in type && type.isAssign) ) { newToken.type = Token.Punctuator; newToken.value = this._code.slice(token.start, token.end); } else if (type === tt.jsxName) { newToken.type = Token.JSXIdentifier; } else if (type.label === "jsxText" || type === tt.jsxAttrValueToken) { newToken.type = Token.JSXText; } else if (type.keyword) { if (type.keyword === "true" || type.keyword === "false") { newToken.type = Token.Boolean; } else if (type.keyword === "null") { newToken.type = Token.Null; } else { newToken.type = Token.Keyword; } } else if (type === tt.num) { newToken.type = Token.Numeric; newToken.value = this._code.slice(token.start, token.end); } else if (type === tt.string) { if (extra.jsxAttrValueToken) { extra.jsxAttrValueToken = false; newToken.type = Token.JSXText; } else { newToken.type = Token.String; } newToken.value = this._code.slice(token.start, token.end); } else if (type === tt.regexp) { newToken.type = Token.RegularExpression; const value = /** @type {{flags: string, pattern: string}} */ ( "value" in token && token.value ); newToken.regex = { flags: value.flags, pattern: value.pattern, }; newToken.value = `/${value.pattern}/${value.flags}`; } return newToken; } /** * Function to call during Acorn's onToken handler. * @param {acorn.Token} token The Acorn token. * @param {Extra} extra The Espree extra object. * @returns {void} */ onToken(token, extra) { const tt = this._acornTokTypes, tokens = extra.tokens, templateTokens = this._tokens; /** * Flushes the buffered template tokens and resets the template * tracking. * @returns {void} * @private */ const translateTemplateTokens = () => { tokens.push(convertTemplatePart(this._tokens, this._code)); this._tokens = []; }; if (token.type === tt.eof) { // might be one last curlyBrace if (this._curlyBrace) { tokens.push(this.translate(this._curlyBrace, extra)); } return; } if (token.type === tt.backQuote) { // if there's already a curly, it's not part of the template if (this._curlyBrace) { tokens.push(this.translate(this._curlyBrace, extra)); this._curlyBrace = null; } templateTokens.push(token); // it's the end if (templateTokens.length > 1) { translateTemplateTokens(); } return; } if (token.type === tt.dollarBraceL) { templateTokens.push(token); translateTemplateTokens(); return; } if (token.type === tt.braceR) { // if there's already a curly, it's not part of the template if (this._curlyBrace) { tokens.push(this.translate(this._curlyBrace, extra)); } // store new curly for later this._curlyBrace = token; return; } if (token.type === tt.template || token.type === tt.invalidTemplate) { if (this._curlyBrace) { templateTokens.push(this._curlyBrace); this._curlyBrace = null; } templateTokens.push(token); return; } if (this._curlyBrace) { tokens.push(this.translate(this._curlyBrace, extra)); this._curlyBrace = null; } tokens.push(this.translate(token, extra)); } } //------------------------------------------------------------------------------ // Public //------------------------------------------------------------------------------ export default TokenTranslator;