diff --git a/packages/espree/espree.cts b/packages/espree/espree.cts new file mode 100644 index 00000000..e785df5d --- /dev/null +++ b/packages/espree/espree.cts @@ -0,0 +1,2 @@ +import * as espree from "./espree.js"; +export = espree; diff --git a/packages/espree/espree.js b/packages/espree/espree.js index 15e0ce52..2bb1a7e2 100644 --- a/packages/espree/espree.js +++ b/packages/espree/espree.js @@ -55,6 +55,59 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +/** + * @import { EnhancedSyntaxError } from "./lib/espree.js"; + * @import { EspreeParserCtor, EspreeParserJsxCtor } from "./lib/types.js"; + */ + +// ---------------------------------------------------------------------------- +// Types exported from file +// ---------------------------------------------------------------------------- +/** + * @typedef {3|5|6|7|8|9|10|11|12|13|14|15|16|17|2015|2016|2017|2018|2019|2020|2021|2022|2023|2024|2025|2026|'latest'} ecmaVersion + */ + +/** + * @typedef {import('./lib/token-translator.js').EsprimaToken} EspreeToken + */ + +/** + * @typedef {import('./lib/espree.js').EsprimaComment} EspreeComment + */ + +/** + * @typedef {{ + * comments?: EspreeComment[] + * } & EspreeToken[]} EspreeTokens + */ + +/** + * `allowReserved` is as in `acorn.Options` + * + * `ecmaVersion` currently as in `acorn.Options` though optional + * + * `sourceType` as in `acorn.Options` but also allows `commonjs` + * + * `ecmaFeatures`, `range`, `loc`, `tokens` are not in `acorn.Options` + * + * `comment` is not in `acorn.Options` and doesn't err without it, but is used + */ +/** + * @typedef {{ + * allowReserved?: boolean, + * ecmaVersion?: ecmaVersion, + * sourceType?: "script"|"module"|"commonjs", + * ecmaFeatures?: { + * jsx?: boolean, + * globalReturn?: boolean, + * impliedStrict?: boolean + * }, + * range?: boolean, + * loc?: boolean, + * tokens?: boolean, + * comment?: boolean, + * }} ParserOptions + */ import * as acorn from "acorn"; import jsx from "acorn-jsx"; @@ -66,23 +119,66 @@ import { getLatestEcmaVersion, getSupportedEcmaVersions } from "./lib/options.js // To initialize lazily. const parsers = { + + /** @type {EspreeParserCtor|null} */ _regular: null, + + /** @type {EspreeParserJsxCtor|null} */ _jsx: null, + /** + * Returns regular Parser + * @returns {EspreeParserCtor} Regular Acorn parser + */ get regular() { if (this._regular === null) { - this._regular = acorn.Parser.extend(espree()); + const espreeParserFactory = /** @type {unknown} */ (espree()); + + this._regular = /** @type {EspreeParserCtor} */ ( + // Without conversion, types are incompatible, as + // acorn's has a protected constructor + /** @type {unknown} */ + (acorn.Parser.extend( + /** + * @type {( + * BaseParser: typeof acorn.Parser + * ) => typeof acorn.Parser} + */ (espreeParserFactory) + )) + ); } return this._regular; }, + /** + * Returns JSX Parser + * @returns {EspreeParserJsxCtor} JSX Acorn parser + */ get jsx() { if (this._jsx === null) { - this._jsx = acorn.Parser.extend(jsx(), espree()); + const espreeParserFactory = /** @type {unknown} */ (espree()); + const jsxFactory = jsx(); + + this._jsx = /** @type {EspreeParserJsxCtor} */ ( + // Without conversion, types are incompatible, as + // acorn's has a protected constructor + /** @type {unknown} */ + (acorn.Parser.extend( + jsxFactory, + + /** @type {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} */ + (espreeParserFactory) + )) + ); } return this._jsx; }, + /** + * Gets the parser object based on the supplied options. + * @param {ParserOptions} options The parser options. + * @returns {EspreeParserJsxCtor|EspreeParserCtor} Regular or JSX Acorn parser + */ get(options) { const useJsx = Boolean( options && @@ -101,9 +197,9 @@ const parsers = { /** * Tokenizes the given code. * @param {string} code The code to tokenize. - * @param {Object} options Options defining how to tokenize. - * @returns {Token[]} An array of tokens. - * @throws {SyntaxError} If the input code is invalid. + * @param {ParserOptions} options Options defining how to tokenize. + * @returns {EspreeTokens} An array of tokens. + * @throws {EnhancedSyntaxError} If the input code is invalid. * @private */ export function tokenize(code, options) { @@ -114,7 +210,7 @@ export function tokenize(code, options) { options = Object.assign({}, options, { tokens: true }); // eslint-disable-line no-param-reassign -- stylistic choice } - return new Parser(options, code).tokenize(); + return /** @type {EspreeTokens} */ (new Parser(options, code).tokenize()); } //------------------------------------------------------------------------------ @@ -124,9 +220,9 @@ export function tokenize(code, options) { /** * Parses the given code. * @param {string} code The code to tokenize. - * @param {Object} options Options defining how to tokenize. - * @returns {ASTNode} The "Program" AST node. - * @throws {SyntaxError} If the input code is invalid. + * @param {ParserOptions} options Options defining how to tokenize. + * @returns {acorn.Node} The "Program" AST node. + * @throws {EnhancedSyntaxError} If the input code is invalid. */ export function parse(code, options) { const Parser = parsers.get(options); @@ -150,6 +246,8 @@ export const VisitorKeys = (function() { /* istanbul ignore next */ export const Syntax = (function() { let key, + + /** @type {Record} */ types = {}; if (typeof Object.create === "function") { diff --git a/packages/espree/lib/espree.js b/packages/espree/lib/espree.js index bfe62fb8..eedd7b0a 100644 --- a/packages/espree/lib/espree.js +++ b/packages/espree/lib/espree.js @@ -3,6 +3,77 @@ import TokenTranslator from "./token-translator.js"; import { normalizeOptions } from "./options.js"; +/** + * @import { + * Integer, + * CommentType, + * EspreeParserCtor, + * EsprimaNode, + * AcornJsxParserCtorEnhanced + * } from "./types.js"; + * @import { EsprimaToken } from "./token-translator.js"; + * @import { ParserOptions } from "../espree.js"; + * @import { normalizedEcmaVersion } from "./options.js"; + * @import * as acorn from "acorn"; + * @import { + * TokTypes + * } from "acorn-jsx"; + */ + +/** + * @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?: import('./token-translator.js').EsprimaToken[]; + * body: acorn.Node[]; + * } & acorn.Program} EsprimaProgramNode + */ + +/** + * @typedef {{ + * type: "Block" | "Hashbang" | "Line", + * value: string, + * range?: [number, number], + * start?: number, + * end?: number, + * loc?: { + * start: acorn.Position | undefined, + * end: acorn.Position | undefined + * } + * }} EsprimaComment + */ + +// ---------------------------------------------------------------------------- +// 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"); @@ -12,15 +83,17 @@ 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 {int} start The index at which the comment starts. - * @param {int} end The index at which the comment ends. - * @param {Location} startLoc The location at which the comment starts. - * @param {Location} endLoc The location at which the comment ends. - * @param {string} code The source code being parsed. - * @returns {Object} The comment object. + * @param {Integer} start The index at which the comment starts. + * @param {Integer} 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 | 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) { @@ -31,6 +104,19 @@ function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, 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 @@ -52,269 +138,360 @@ function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, return comment; } -export default () => Parser => { - const tokTypes = Object.assign({}, Parser.acorn.tokTypes); - - if (Parser.acornJsx) { - Object.assign(tokTypes, Parser.acornJsx.tokTypes); - } +// 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 { - constructor(opts, code) { - if (typeof opts !== "object" || opts === null) { - opts = {}; - } - if (typeof code !== "string" && !(code instanceof String)) { - code = String(code); - } + return class Espree extends Parser { - // 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, 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`. + /** + * @param {ParserOptions | null} opts The parser options + * @param {string | object} code The code which will be converted to a string. */ - 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, state); - } - if (token.type !== tokTypes.eof) { - state.lastToken = token; + 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 & { + * 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); + } } - }, - // Collect comments - onComment(block, text, start, end, startLoc, endLoc) { - if (state.comments) { - const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc, code); + // @ts-expect-error Appears to be a TS bug + // since the type is indeed string|String + }, code); - state.comments.push(comment); - } - } - }, 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; + } - /* - * We put all of this data into a symbol property as a way to avoid - * potential naming conflicts with future versions of Acorn. + /** + * Returns Espree tokens. + * @returns {EsprimaToken[]} The Esprima-compatible tokens */ - this[STATE] = state; - } + tokenize() { + do { + this.next(); + } while (this.type !== tokTypes.eof); - tokenize() { - do { + // Consume the final eof token this.next(); - } while (this.type !== tokTypes.eof); - // Consume the final eof token - this.next(); + const extra = this[STATE]; + const tokens = /** @type {EsprimaToken[]} */ (extra.tokens); - const extra = this[STATE]; - const tokens = extra.tokens; + // Uncovered in tests, and problematic for types to + // add property to an array + // if (extra.comments) { + // tokens.comments = extra.comments; + // } - if (extra.comments) { - tokens.comments = extra.comments; + return tokens; } - 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); - finishNode(...args) { - const result = super.finishNode(...args); + return this[ESPRIMA_FINISH_NODE](result); + } - 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); - finishNodeAt(...args) { - const result = super.finishNodeAt(...args); + return this[ESPRIMA_FINISH_NODE](result); + } - return this[ESPRIMA_FINISH_NODE](result); - } + /** + * Parses. + * @returns {EsprimaProgramNode} The program Node + */ + parse() { + const extra = this[STATE]; + const prog = super.parse(); - parse() { - const extra = this[STATE]; - const program = super.parse(); + const program = /** @type {EsprimaProgramNode} */ (prog); - program.sourceType = extra.originalSourceType; + // @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; - } + 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; + /* + * 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; + } - templateElement.start += startOffset; - templateElement.end += endOffset; + if (templateElement.loc) { + templateElement.loc.start.column += startOffset; + templateElement.loc.end.column += endOffset; + } + }); - if (templateElement.range) { - templateElement.range[0] += startOffset; - templateElement.range[1] += endOffset; - } + return program; + } - if (templateElement.loc) { - templateElement.loc.start.column += startOffset; - templateElement.loc.end.column += endOffset; + /** + * 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); + } - return program; - } + /** + * Overwrites the default raise method to throw Esprima-style errors. + * @param {Integer} 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; + } - parseTopLevel(node) { - if (this[STATE].impliedStrict) { - this.strict = true; + /** + * Overwrites the default raise method to throw Esprima-style errors. + * @param {Integer} 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); } - return super.parseTopLevel(node); - } - /** - * Overwrites the default raise method to throw Esprima-style errors. - * @param {int} pos The position of the error. - * @param {string} message The error message. - * @throws {SyntaxError} A syntax error. - * @returns {void} - */ - raise(pos, message) { - const loc = Parser.acorn.getLineInfo(this.input, pos); - const err = 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 unexpected method to throw Esprima-style errors. + * @param {Integer} pos The position of the error. + * @throws {SyntaxError} A syntax error. + * @returns {void} + */ + unexpected(pos) { + let message = "Unexpected token"; - /** - * Overwrites the default raise method to throw Esprima-style errors. - * @param {int} 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); - } + if (pos !== null && pos !== void 0) { + this.pos = pos; - /** - * Overwrites the default unexpected method to throw Esprima-style errors. - * @param {int} 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; + if (this.options.locations) { + while (this.pos < this.lineStart) { + this.lineStart = this.input.lastIndexOf("\n", this.lineStart - 2) + 1; + --this.curLine; + } } + + this.nextToken(); } - this.nextToken(); - } + if (this.end > this.start) { + message += ` ${this.input.slice(this.start, this.end)}`; + } - if (this.end > this.start) { - message += ` ${this.input.slice(this.start, this.end)}`; + this.raise(this.start, message); } - 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} + */ + jsx_readString(quote) { // eslint-disable-line camelcase -- required by API + const result = super.jsx_readString(quote); - /* - * 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. - */ - jsx_readString(quote) { // eslint-disable-line camelcase -- required by API - const result = super.jsx_readString(quote); - - if (this.type === tokTypes.string) { - this[STATE].jsxAttrValueToken = true; + if (this.type === tokTypes.string) { + this[STATE].jsxAttrValueToken = true; + } + return result; } - return result; - } - /** - * Performs last-minute Esprima-specific compatibility checks and fixes. - * @param {ASTNode} result The node to check. - * @returns {ASTNode} The finished node. - */ - [ESPRIMA_FINISH_NODE](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") { + // 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(result); - } + // save template element references to fix start/end later + this[STATE].templateElements.push( - if (result.type.includes("Function") && !result.generator) { - result.generator = false; - } + /** @type {acorn.TemplateElement} */ + (result) + ); + } - return result; - } + if (result.type.includes("Function") && (!("generator" in result))) { + + /** + * @type {acorn.FunctionDeclaration|acorn.FunctionExpression| + * acorn.ArrowFunctionExpression} + */ + (result).generator = false; + } + + return result; + } + }; }; }; diff --git a/packages/espree/lib/options.js b/packages/espree/lib/options.js index 85bb9029..debfba0c 100644 --- a/packages/espree/lib/options.js +++ b/packages/espree/lib/options.js @@ -3,11 +3,15 @@ * @author Kai Cataldo */ +/** + * @import { ecmaVersion, ParserOptions } from "../espree.js"; + */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ -const SUPPORTED_VERSIONS = [ +const SUPPORTED_VERSIONS = /** @type {const} */ ([ 3, 5, 6, // 2015 @@ -22,19 +26,23 @@ const SUPPORTED_VERSIONS = [ 15, // 2024 16, // 2025 17 // 2026 -]; +]); + +/** + * @typedef {typeof SUPPORTED_VERSIONS[number]} normalizedEcmaVersion + */ /** * Get the latest ECMAScript version supported by Espree. - * @returns {number} The latest ECMAScript version. + * @returns {normalizedEcmaVersion} The latest ECMAScript version. */ export function getLatestEcmaVersion() { - return SUPPORTED_VERSIONS.at(-1); + return /** @type {normalizedEcmaVersion} */ (SUPPORTED_VERSIONS.at(-1)); } /** * Get the list of ECMAScript versions supported by Espree. - * @returns {number[]} An array containing the supported ECMAScript versions. + * @returns {normalizedEcmaVersion[]} An array containing the supported ECMAScript versions. */ export function getSupportedEcmaVersions() { return [...SUPPORTED_VERSIONS]; @@ -44,7 +52,7 @@ export function getSupportedEcmaVersions() { * Normalize ECMAScript version from the initial config * @param {(number|"latest")} ecmaVersion ECMAScript version from the initial config * @throws {Error} throws an error if the ecmaVersion is invalid. - * @returns {number} normalized ECMAScript version + * @returns {normalizedEcmaVersion} normalized ECMAScript version */ function normalizeEcmaVersion(ecmaVersion = 5) { @@ -60,18 +68,22 @@ function normalizeEcmaVersion(ecmaVersion = 5) { version -= 2009; } - if (!SUPPORTED_VERSIONS.includes(version)) { + if (!SUPPORTED_VERSIONS.includes( + + /** @type {normalizedEcmaVersion} */ + (version) + )) { throw new Error("Invalid ecmaVersion."); } - return version; + return /** @type {normalizedEcmaVersion} */ (version); } /** * Normalize sourceType from the initial config * @param {string} sourceType to normalize * @throws {Error} throw an error if sourceType is invalid - * @returns {string} normalized sourceType + * @returns {"script"|"module"} normalized sourceType */ function normalizeSourceType(sourceType = "script") { if (sourceType === "script" || sourceType === "module") { @@ -85,11 +97,31 @@ function normalizeSourceType(sourceType = "script") { throw new Error("Invalid sourceType."); } +/** + * @typedef {{ + * ecmaVersion: normalizedEcmaVersion, + * sourceType: "script"|"module", + * range?: boolean, + * loc?: boolean, + * allowReserved: boolean | "never", + * ecmaFeatures?: { + * jsx?: boolean, + * globalReturn?: boolean, + * impliedStrict?: boolean + * }, + * ranges: boolean, + * locations: boolean, + * allowReturnOutsideFunction: boolean, + * tokens?: boolean, + * comment?: boolean + * }} NormalizedParserOptions + */ + /** * Normalize parserOptions - * @param {Object} options the parser options to normalize + * @param {ParserOptions} options the parser options to normalize * @throws {Error} throw an error if found invalid option. - * @returns {Object} normalized options + * @returns {NormalizedParserOptions} normalized options */ export function normalizeOptions(options) { const ecmaVersion = normalizeEcmaVersion(options.ecmaVersion); diff --git a/packages/espree/lib/token-translator.js b/packages/espree/lib/token-translator.js index 6daf865a..cb9f6e32 100644 --- a/packages/espree/lib/token-translator.js +++ b/packages/espree/lib/token-translator.js @@ -3,6 +3,50 @@ * @author Nicholas C. Zakas */ +/** + * @import * as acorn from "acorn"; + * @import { EnhancedTokTypes } from "./espree.js" + * @import { ecmaVersion } from "../espree.js"; + * @import { normalizedEcmaVersion } from "./options.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 {{ + * value: any; + * start?: number; + * end?: number; + * loc?: acorn.SourceLocation; + * range?: [number, number]; + * regex?: {flags: string, pattern: string}; + * }} BaseEsprimaToken + * @typedef {{ + * jsxAttrValueToken: boolean; + * ecmaVersion: normalizedEcmaVersion; + * }} ExtraNoTokens + * @typedef {{ + * tokens: EsprimaToken[] + * } & ExtraNoTokens} Extra + */ + +/** + * @typedef {{ + * type: string; + * } & BaseEsprimaToken} EsprimaToken + */ + //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ @@ -33,7 +77,7 @@ const Token = { /** * Converts part of a template into an Esprima token. - * @param {AcornToken[]} tokens The Acorn tokens representing the template. + * @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 @@ -42,19 +86,20 @@ function convertTemplatePart(tokens, code) { const firstToken = tokens[0], lastTemplateToken = tokens.at(-1); + /** @type {EsprimaToken} */ const token = { type: Token.Template, - value: code.slice(firstToken.start, lastTemplateToken.end) + value: code.slice(firstToken.start, lastTemplateToken?.end) }; - if (firstToken.loc) { + if (firstToken.loc && lastTemplateToken?.loc) { token.loc = { start: firstToken.loc.start, end: lastTemplateToken.loc.end }; } - if (firstToken.range) { + if (firstToken.range && lastTemplateToken?.range) { token.start = firstToken.range[0]; token.end = lastTemplateToken.range[1]; token.range = [token.start, token.end]; @@ -63,59 +108,70 @@ function convertTemplatePart(tokens, code) { return token; } +/* eslint-disable jsdoc/check-types -- The API allows either */ /** * Contains logic to translate Acorn tokens into Esprima tokens. - * @param {Object} acornTokTypes The Acorn token types. - * @param {string} code The source code Acorn is parsing. This is necessary - * to correct the "value" property of some tokens. - * @constructor */ -function TokenTranslator(acornTokTypes, code) { +class TokenTranslator { - // token types - this._acornTokTypes = acornTokTypes; + /** + * 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 buffer for templates - this._tokens = []; + // token types + this._acornTokTypes = acornTokTypes; - // track the last curly brace - this._curlyBrace = null; + // token buffer for templates + /** @type {acorn.Token[]} */ + this._tokens = []; - // the source code - this._code = code; + // track the last curly brace + this._curlyBrace = null; -} + // the source code + this._code = code; -TokenTranslator.prototype = { - constructor: TokenTranslator, + } /** * 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 {AcornToken} token The Acorn token to translate. - * @param {Object} extra Espree extra object. + * @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; + 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) { - token.type = Token.Identifier; + newToken.type = Token.Identifier; // TODO: See if this is an Acorn bug - if (token.value === "static") { - token.type = Token.Keyword; + if ("value" in token && token.value === "static") { + newToken.type = Token.Keyword; } - if (extra.ecmaVersion > 5 && (token.value === "yield" || token.value === "let")) { - token.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) { - token.type = Token.PrivateIdentifier; + newToken.type = Token.PrivateIdentifier; } else if (type === tt.semi || type === tt.comma || type === tt.parenL || type === tt.parenR || @@ -127,54 +183,56 @@ TokenTranslator.prototype = { type === tt.incDec || type === tt.starstar || type === tt.jsxTagEnd || type === tt.prefix || type === tt.questionDot || - (type.binop && !type.keyword) || - type.isAssign) { + ("binop" in type && type.binop && !type.keyword) || + ("isAssign" in type && type.isAssign)) { - token.type = Token.Punctuator; - token.value = this._code.slice(token.start, token.end); + newToken.type = Token.Punctuator; + newToken.value = this._code.slice(token.start, token.end); } else if (type === tt.jsxName) { - token.type = Token.JSXIdentifier; + newToken.type = Token.JSXIdentifier; } else if (type.label === "jsxText" || type === tt.jsxAttrValueToken) { - token.type = Token.JSXText; + newToken.type = Token.JSXText; } else if (type.keyword) { if (type.keyword === "true" || type.keyword === "false") { - token.type = Token.Boolean; + newToken.type = Token.Boolean; } else if (type.keyword === "null") { - token.type = Token.Null; + newToken.type = Token.Null; } else { - token.type = Token.Keyword; + newToken.type = Token.Keyword; } } else if (type === tt.num) { - token.type = Token.Numeric; - token.value = this._code.slice(token.start, token.end); + newToken.type = Token.Numeric; + newToken.value = this._code.slice(token.start, token.end); } else if (type === tt.string) { if (extra.jsxAttrValueToken) { extra.jsxAttrValueToken = false; - token.type = Token.JSXText; + newToken.type = Token.JSXText; } else { - token.type = Token.String; + newToken.type = Token.String; } - token.value = this._code.slice(token.start, token.end); + newToken.value = this._code.slice(token.start, token.end); } else if (type === tt.regexp) { - token.type = Token.RegularExpression; - const value = token.value; + newToken.type = Token.RegularExpression; + const value = /** @type {{flags: string, pattern: string}} */ ( + "value" in token && token.value + ); - token.regex = { + newToken.regex = { flags: value.flags, pattern: value.pattern }; - token.value = `/${value.pattern}/${value.flags}`; + newToken.value = `/${value.pattern}/${value.flags}`; } - return token; - }, + return newToken; + } /** * Function to call during Acorn's onToken handler. - * @param {AcornToken} token The Acorn token. - * @param {Object} extra The Espree extra object. + * @param {acorn.Token} token The Acorn token. + * @param {Extra} extra The Espree extra object. * @returns {void} */ onToken(token, extra) { @@ -254,7 +312,7 @@ TokenTranslator.prototype = { tokens.push(this.translate(token, extra)); } -}; +} //------------------------------------------------------------------------------ // Public diff --git a/packages/espree/lib/types.js b/packages/espree/lib/types.js new file mode 100644 index 00000000..d58e6373 --- /dev/null +++ b/packages/espree/lib/types.js @@ -0,0 +1,63 @@ +/** + * @import * as acorn from "acorn"; + * @import { AcornJsxParserCtor, AcornJsxParser, TokTypes } from "acorn-jsx"; + * @import { ParserOptions } from "../espree.js"; + */ + +/** + * @typedef {number} Integer + */ + +/** + * @typedef {{ + * generator?: boolean + * } & acorn.Node} EsprimaNode + */ + +/** + * @typedef {"Block"|"Hashbang"|"Line"} CommentType + */ + +/** + * @typedef {{ + * tokenize: () => import('./token-translator.js').EsprimaToken[], + * parse: () => acorn.Node + * }} EspreeParser + */ + +/* eslint-disable jsdoc/valid-types -- Bug in older versions */ +/** + * @typedef {{ + * new (opts: ParserOptions | null, code: string | object): EspreeParser + * } & Pick} EspreeParserCtor + */ +/** + * @typedef {{ + * new (opts: ParserOptions | null, code: string | object): EspreeParser + * } & Pick} EspreeParserJsxCtor + */ + +/** + * @typedef {Pick & { + * acorn: { + * tokTypes: TokTypes, + * getLineInfo: (input: string, pos: number) => { + * line: number, + * column: number + * } + * } + * new (options: acorn.Options, input: string, startPos?: number): AcornJsxParser & { + * next: () => void, + * type: acorn.TokenType, + * curLine: number, + * start: number, + * end: number, + * finishNode (node: acorn.Node, type: string): acorn.Node, + * finishNodeAt (node: acorn.Node, type: string, pos: number, loc: acorn.Position): acorn.Node, + * parseTopLevel (node: acorn.Node): acorn.Node, + * nextToken (): void + * } + * }} AcornJsxParserCtorEnhanced + */ + +/* eslint-enable jsdoc/valid-types -- Bug in older versions */ diff --git a/packages/espree/package.json b/packages/espree/package.json index cec64e59..d11111c1 100644 --- a/packages/espree/package.json +++ b/packages/espree/package.json @@ -6,20 +6,21 @@ "main": "dist/espree.cjs", "type": "module", "exports": { - ".": [ - { - "import": "./espree.js", - "require": "./dist/espree.cjs", - "default": "./dist/espree.cjs" + ".": { + "types": { + "import": "./dist/espree.d.ts", + "require": "./dist/espree.d.cts" }, - "./dist/espree.cjs" - ], + "import": "./espree.js", + "require": "./dist/espree.cjs", + "default": "./dist/espree.cjs" + }, "./package.json": "./package.json" }, "version": "11.0.0", "files": [ "lib", - "dist/espree.cjs", + "dist", "espree.js" ], "engines": { @@ -37,15 +38,17 @@ "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", + "acorn-jsx": "brettz9/acorn-jsx#esm", "eslint-visitor-keys": "^5.0.0" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.2", "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.3.0", "rollup": "^2.79.1", - "shelljs": "^0.8.5" + "shelljs": "^0.8.5", + "typescript": "^5.9.3" }, "keywords": [ "ast", @@ -56,7 +59,9 @@ "acorn" ], "scripts": { - "build": "rollup -c rollup.config.js", + "attw": "attw --pack", + "tsc": "tsc && tsc -p tsconfig-cjs.json", + "build": "rollup -c rollup.config.js && npm run tsc", "build:debug": "npm run build -- -m", "build:docs": "node tools/sync-docs.js", "build:update-version": "node tools/update-version.js", diff --git a/packages/espree/tests/lib/parse.test.js b/packages/espree/tests/lib/parse.test.js index be8719ac..e8ab8440 100644 --- a/packages/espree/tests/lib/parse.test.js +++ b/packages/espree/tests/lib/parse.test.js @@ -214,6 +214,20 @@ describe("parse()", () => { assert.deepStrictEqual([ast.loc.end.line, ast.loc.end.column], [1, 5]); }); + it("should reset lastToken on each parse (block comment)", () => { + espree.parse("var foo = bar;"); + const ast = espree.parse("/* foo */", { + comment: true, + tokens: true, + range: true, + loc: true + }); + + assert.deepStrictEqual(ast.range, [0, 9]); + assert.deepStrictEqual([ast.loc.start.line, ast.loc.start.column], [1, 0]); + assert.deepStrictEqual([ast.loc.end.line, ast.loc.end.column], [1, 9]); + }); + it("should not mutate config", () => { espree.parse("foo", Object.freeze({ ecmaFeatures: Object.freeze({}) })); }); diff --git a/packages/espree/tsconfig-cjs.json b/packages/espree/tsconfig-cjs.json new file mode 100644 index 00000000..98a6d8a1 --- /dev/null +++ b/packages/espree/tsconfig-cjs.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "moduleResolution": "node", + "module": "commonjs", + "allowJs": true, + "esModuleInterop": true, + "noEmit": false, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "strict": true, + "target": "es5", + "outDir": "dist" + }, + "include": ["espree.cts"], + "exclude": ["node_modules"] +} diff --git a/packages/espree/tsconfig.json b/packages/espree/tsconfig.json new file mode 100644 index 00000000..e885e02b --- /dev/null +++ b/packages/espree/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "moduleResolution": "nodenext", + "module": "nodenext", + "allowJs": true, + "checkJs": true, + "noEmit": false, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "strict": true, + "target": "es5", + "outDir": "dist" + }, + "include": ["espree.js", "lib/**/*.js"], + "exclude": ["node_modules"] +}