tenko
Version:
A "pixel perfect" 100% spec compliant ES2021 JavaScript parser written in JS.
1,335 lines (1,211 loc) • 646 kB
JavaScript
// Char codes aren't really used in the parser (only in the lexer), except for asserts. Inlined for builds.
import {
$$A_61,
$$A_UC_41,
$$B_62,
$$B_UC_42,
$$C_63,
$$C_UC_43,
$$D_64,
$$D_UC_44,
$$E_65,
$$E_UC_45,
$$F_66,
$$F_UC_46,
$$G_67,
$$G_UC_47,
$$H_68,
$$H_UC_48,
$$I_69,
$$I_UC_49,
$$J_6A,
$$J_UC_4A,
$$K_6B,
$$K_UC_4B,
$$L_6C,
$$L_UC_4C,
$$M_6D,
$$M_UC_4D,
$$N_6E,
$$N_UC_4E,
$$O_6F,
$$O_UC_4F,
$$P_70,
$$P_UC_50,
$$Q_71,
$$Q_UC_51,
$$R_72,
$$R_UC_52,
$$S_73,
$$S_UC_53,
$$T_74,
$$T_UC_54,
$$U_75,
$$U_UC_55,
$$V_76,
$$V_UC_56,
$$W_77,
$$W_UC_57,
$$X_78,
$$X_UC_58,
$$Y_79,
$$Y_UC_59,
$$Z_7A,
$$Z_UC_5A,
$$0_30,
$$1_31,
$$2_32,
$$3_33,
$$4_34,
$$5_35,
$$6_36,
$$7_37,
$$8_38,
$$9_39,
$$NULL_00,
$$BACKSPACE_08,
$$TAB_09,
$$LF_0A,
$$VTAB_0B,
$$FF_0C,
$$CR_0D,
$$SPACE_20,
$$EXCL_21,
$$DQUOTE_22,
$$HASH_23,
$$$_24,
$$PERCENT_25,
$$AND_26,
$$SQUOTE_27,
$$PAREN_L_28,
$$PAREN_R_29,
$$STAR_2A,
$$PLUS_2B,
$$COMMA_2C,
$$DASH_2D,
$$DOT_2E,
$$FWDSLASH_2F,
$$COLON_3A,
$$SEMI_3B,
$$LT_3C,
$$IS_3D,
$$GT_3E,
$$QMARK_3F,
$$SQUARE_L_5B,
$$BACKSLASH_5C,
$$SQUARE_R_5D,
$$XOR_5E,
$$LODASH_5F,
$$TICK_60,
$$CURLY_L_7B,
$$CURLY_R_7D,
$$TILDE_7E,
$$OR_7C,
$$NBSP_A0,
$$ZWS_200B,
$$ZWNJ_200C,
$$ZWJ_200D,
$$LS_2029,
$$PS_2028,
$$BOM_FEFF,
} from './charcodes.mjs';
// Lexerflags are used to signal state from parser to lexer. And organically grew into a wider signaling system.
import {
INITIAL_LEXER_FLAGS,
LF_NO_FLAGS,
LF_CAN_NEW_DOT_TARGET,
LF_FOR_REGEX,
LF_IN_ASYNC,
LF_IN_CONSTRUCTOR,
LF_IN_FOR_LHS,
LF_IN_FUNC_ARGS,
LF_IN_GENERATOR,
LF_IN_GLOBAL,
LF_IN_ITERATION,
LF_IN_SWITCH,
LF_IN_TEMPLATE,
LF_NO_ASI,
LF_STRICT_MODE,
LF_SUPER_CALL,
LF_SUPER_PROP,
LF_NOT_KEYWORD,
LF_CHAINING,
L,
} from './lexerflags.mjs';
// Most of these aren't used, except for a few at ASSERT time. Auto-imported for convenience. Auto stripped from a build
import {
inspect,
ASSERT,
} from './utils.mjs';
// These define most of the token type manipulation things
import {
getTokenStart,
isWhiteToken,
isNewlineToken,
isCommentToken,
isIdentToken,
isNumberToken,
isBigintToken,
isStringToken,
isPunctuatorToken,
isRegexToken,
isTickToken,
isBadTickToken,
isNumberStringToken,
isNumberStringRegex,
// <SCRUB ASSERTS TO COMMENT>
// (These are only used to assert a token type group when skipping. For all other use cases use the `isXXXtoken()` funcs
$G_WHITE,
$G_NEWLINE,
$G_COMMENT,
$G_IDENT,
$G_NUMBER,
$G_NUMBER_BIG_INT,
$G_PUNCTUATOR,
$G_BINOP_ASSIGN,
$G_BINOP_NONASSIGN,
$G_STRING,
$G_REGEX,
$G_TICK,
$G_TICK_BAD_ESCAPE,
$G_OTHER,
// </SCRUB ASSERTS TO COMMENT>
$UNTYPED, // 0
$SPACE,
$TAB,
$NL_SOLO,
$NL_CRLF,
$COMMENT_SINGLE,
$COMMENT_MULTI,
$COMMENT_HTML,
$IDENT,
$ID_arguments,
$ID_as,
$ID_async,
$ID_await,
$ID_break,
$ID_case,
$ID_catch,
$ID_class,
$ID_const,
$ID_continue,
$ID_debugger,
$ID_default,
$ID_delete,
$ID_do,
$ID_else,
$ID_enum,
$ID_eval,
$ID_export,
$ID_extends,
$ID_false,
$ID_finally,
$ID_for,
$ID_from,
$ID_function,
$ID_get,
$ID_if,
$ID_implements,
$ID_import,
$ID_in,
$ID_instanceof,
$ID_interface,
$ID_let,
$ID_new,
$ID_null,
$ID_of,
$ID_package,
$ID_private,
$ID_protected,
$ID_public,
$ID_return,
$ID_set,
$ID_static,
$ID_super,
$ID_switch,
$ID_target,
$ID_this,
$ID_throw,
$ID_true,
$ID_try,
$ID_typeof,
$ID_var,
$ID_void,
$ID_while,
$ID_with,
$ID_yield,
$NUMBER_HEX,
$NUMBER_DEC,
$NUMBER_BIN,
$NUMBER_OCT,
$NUMBER_OLD,
$NUMBER_BIG_HEX,
$NUMBER_BIG_DEC,
$NUMBER_BIG_BIN,
$NUMBER_BIG_OCT,
$PUNC_EXCL,
$PUNC_EXCL_EQ,
$PUNC_EXCL_EQ_EQ,
$PUNC_PERCENT,
$PUNC_PERCENT_EQ,
$PUNC_AND,
$PUNC_AND_AND,
$PUNC_AND_AND_EQ,
$PUNC_AND_EQ,
$PUNC_PAREN_OPEN,
$PUNC_PAREN_CLOSE,
$PUNC_STAR,
$PUNC_STAR_STAR,
$PUNC_STAR_EQ,
$PUNC_STAR_STAR_EQ,
$PUNC_PLUS,
$PUNC_PLUS_PLUS,
$PUNC_PLUS_EQ,
$PUNC_COMMA,
$PUNC_MIN,
$PUNC_MIN_MIN,
$PUNC_MIN_EQ,
$PUNC_MIN_MIN_GT,
$PUNC_DOT,
$PUNC_DOT_DOT_DOT,
$PUNC_DIV,
$PUNC_DIV_EQ,
$PUNC_COLON,
$PUNC_SEMI,
$PUNC_LT,
$PUNC_LT_LT,
$PUNC_LT_EQ,
$PUNC_LT_LT_EQ,
$PUNC_LT_EXCL_MIN_MIN,
$PUNC_EQ,
$PUNC_EQ_EQ,
$PUNC_EQ_EQ_EQ,
$PUNC_EQ_GT,
$PUNC_GT,
$PUNC_GT_GT,
$PUNC_GT_GT_GT,
$PUNC_GT_EQ,
$PUNC_GT_GT_EQ,
$PUNC_GT_GT_GT_EQ,
$PUNC_QMARK,
$PUNC_QMARK_DOT,
$PUNC_QMARK_QMARK,
$PUNC_QMARK_QMARK_EQ,
$PUNC_BRACKET_OPEN,
$PUNC_BRACKET_CLOSE,
$PUNC_CARET,
$PUNC_CARET_EQ,
$PUNC_CURLY_OPEN,
$PUNC_OR,
$PUNC_OR_OR,
$PUNC_OR_OR_EQ,
$PUNC_OR_EQ,
$PUNC_CURLY_CLOSE,
$PUNC_TILDE,
$REGEXN,
$REGEXU,
$STRING_SINGLE,
$STRING_DOUBLE,
$TICK_HEAD,
$TICK_BODY,
$TICK_TAIL,
$TICK_PURE,
$TICK_BAD_HEAD,
$TICK_BAD_BODY,
$TICK_BAD_TAIL,
$TICK_BAD_PURE,
$EOF,
$ASI,
$ERROR,
T,
// <SCRUB ASSERTS TO COMMENT>
ALL_START_TYPES,
ALL_GEES,
ALL_TOKEN_GROUPS,
ALL_TOKEN_TYPES,
// </SCRUB ASSERTS TO COMMENT>
} from './tokentype.mjs';
// Flags
import {
COLLECT_TOKENS_NONE,
COLLECT_TOKENS_SOLID,
COLLECT_TOKENS_ALL,
COLLECT_TOKENS_TYPES,
FAIL_HARD,
GOAL_MODULE,
GOAL_SCRIPT,
RETURN_ANY_TOKENS,
RETURN_COMMENT_TOKENS,
RETURN_SOLID_TOKENS,
WEB_COMPAT_OFF,
WEB_COMPAT_ON,
} from './enum_lexer.mjs';
// Actual lexer
import {
Lexer,
} from './lexer.mjs';
function ASSERT_ASSIGN_EXPR(allowAssignment) {
ASSERT(allowAssignment === ASSIGN_EXPR_IS_OK || allowAssignment === ASSIGN_EXPR_IS_ERROR, 'allowAssignment is enum', allowAssignment);
}
function ASSERT_FDS(fdState) {
ASSERT([FDS_ILLEGAL, FDS_IFELSE, FDS_LEX, FDS_VAR].includes(fdState), 'FDS enum', fdState);
}
function ASSERT_BINDING_TYPE(bindingType) {
ASSERT([BINDING_TYPE_NONE,BINDING_TYPE_ARG,BINDING_TYPE_VAR,BINDING_TYPE_LET,BINDING_TYPE_CONST,BINDING_TYPE_CLASS,BINDING_TYPE_FUNC_VAR,BINDING_TYPE_FUNC_LEX,BINDING_TYPE_FUNC_STMT,BINDING_TYPE_CATCH_IDENT,BINDING_TYPE_CATCH_OTHER].includes(bindingType), 'bindingType is an enum', bindingType);
}
function ASSERT_BINDING_ORIGIN(bindingOrigin) {
ASSERT([FROM_STATEMENT_START, FROM_FOR_HEADER, FROM_CATCH, FROM_EXPORT_DECL, FROM_ASYNC_ARG, FROM_OTHER_FUNC_ARG].includes(bindingOrigin), 'binding origin enum');
}
import {
VERSION_EXPONENTIATION,
VERSION_ASYNC,
VERSION_TRAILING_FUNC_COMMAS,
VERSION_ASYNC_GEN,
VERSION_OBJECTSPREAD,
VERSION_TAGGED_TEMPLATE_BAD_ESCAPES,
VERSION_OPTIONAL_CATCH,
VERSION_DYNAMIC_IMPORT,
VERSION_EXPORT_STAR_AS,
VERSION_WHATEVER,
IS_ASYNC,
NOT_ASYNC,
IS_ASYNC_PREFIXED,
NOT_ASYNC_PREFIXED,
UNDEF_STATIC,
UNDEF_ASYNC,
UNDEF_STAR,
UNDEF_GET,
UNDEF_SET,
IS_FUNC_DECL,
NOT_FUNC_DECL,
IS_FUNC_EXPR,
NOT_FUNC_EXPR,
IDENT_OPTIONAL,
IDENT_REQUIRED,
PARSE_VALUE_MAYBE,
PARSE_VALUE_MUST,
YIELD_WITHOUT_VALUE,
WITH_ASSIGNABLE,
WITH_NON_ASSIGNABLE,
IS_ARROW,
NOT_ARROW,
FROM_STATEMENT_START,
FROM_FOR_HEADER,
FROM_EXPORT_DECL,
FROM_CATCH,
FROM_ASYNC_ARG,
FROM_OTHER_FUNC_ARG,
COAL_SEEN_NEITHER,
COAL_SEEN_NULLISH,
COAL_SEEN_LOGICAL,
BINDING_TYPE_NONE,
BINDING_TYPE_ARG,
BINDING_TYPE_VAR,
BINDING_TYPE_LET,
BINDING_TYPE_CONST,
BINDING_TYPE_CLASS,
BINDING_TYPE_FUNC_VAR,
BINDING_TYPE_FUNC_LEX,
BINDING_TYPE_FUNC_STMT,
BINDING_TYPE_CATCH_IDENT,
BINDING_TYPE_CATCH_OTHER,
HAS_NO_BINDINGS,
ASSIGNMENT_IS_INIT,
ASSIGNMENT_IS_DEFAULT,
IS_EXPRESSION,
IS_STATEMENT,
IS_NEW_ARG,
NOT_NEW_ARG,
IS_OPTIONAL,
NOT_OPTIONAL,
MIGHT_DESTRUCT,
CANT_DESTRUCT,
DESTRUCT_ASSIGN_ONLY,
MUST_DESTRUCT,
ASSIGNABLE_UNDETERMINED,
NOT_ASSIGNABLE,
IS_ASSIGNABLE,
PIGGY_BACK_SAW_AWAIT,
PIGGY_BACK_SAW_YIELD,
PIGGY_BACK_WAS_CONSTRUCTOR,
PIGGY_BACK_WAS_PROTO,
PIGGY_BACK_WAS_ARROW,
NO_SPREAD,
LAST_SPREAD,
MID_SPREAD,
PARSE_INIT,
SKIP_INIT,
IS_EXPORT,
NOT_EXPORT,
IS_QUASI_TAIL,
NOT_QUASI_TAIL,
PARAM_UNDETERMINED,
PARAM_WAS_SIMPLE,
PARAM_WAS_NON_STRICT_SIMPLE,
PARAM_WAS_COMPLEX,
PARAM_WAS_COMPLEX_HAD_INIT,
PARAMS_ALL_SIMPLE,
PARAMS_SOME_NONSTRICT,
PARAMS_SOME_COMPLEX,
IS_CONSTRUCTOR,
NOT_CONSTRUCTOR,
IS_METHOD,
NOT_METHOD,
ASSIGN_EXPR_IS_OK,
ASSIGN_EXPR_IS_ERROR,
NO_ID_TO_VERIFY,
NO_DUPE_PARAMS,
SCOPE_LAYER_GLOBAL,
SCOPE_LAYER_FOR_HEADER,
SCOPE_LAYER_BLOCK,
SCOPE_LAYER_FUNC_PARAMS,
SCOPE_LAYER_CATCH_HEAD,
SCOPE_LAYER_CATCH_BODY,
SCOPE_LAYER_FINALLY,
SCOPE_LAYER_SWITCH,
SCOPE_LAYER_FUNC_ROOT,
SCOPE_LAYER_FUNC_BODY,
SCOPE_LAYER_ARROW_PARAMS,
SCOPE_LAYER_FAKE_BLOCK,
DO_NOT_BIND,
UNDEF_EXPORTS,
FDS_ILLEGAL,
FDS_IFELSE,
FDS_LEX,
FDS_VAR,
IS_GLOBAL_TOPLEVEL,
NOT_GLOBAL_TOPLEVEL,
IS_LABELLED,
NOT_LABELLED,
NOT_LHSE,
ONLY_LHSE,
PARENT_NOT_LABEL,
EMPTY_LABEL_SET,
copyPiggies,
P,
} from './enum_parser.mjs';
let ASSERT_ASI_REGEX_NEXT = false; // When set, do not throw assertion error in the semi/asi parser for seeing a regex
function sansFlag(flags, flag) {
// This function is inlined by the build script...
ASSERT(sansFlag.length === arguments.length, 'arg count');
ASSERT(typeof flag === 'number', 'sansFlag flag 1 should be number;', flag, flags);
ASSERT(typeof flags === 'number', 'sansFlag flag 2 should be number;', flag, flags);
return (flags | flag) ^ flag;
}
function hasAllFlags(flags1, flags2) {
// This function is inlined by the build script...
ASSERT(hasAllFlags.length === arguments.length, 'arg count');
ASSERT(typeof flags1 === 'number', 'hasAllFlags flag 1 should be number;', flags1, flags2);
ASSERT(typeof flags2 === 'number', 'hasAllFlags flag 2 should be number;', flags1, flags2);
return (flags1 & flags2) === flags2;
}
function hasAnyFlag(flags1, flags2) {
// This function is inlined by the build script...
ASSERT(hasAnyFlag.length === arguments.length, 'arg count');
ASSERT(typeof flags1 === 'number', 'hasAnyFlag flag 1 should be a number;', flags1, flags2);
ASSERT(typeof flags2 === 'number', 'hasAnyFlag flag 2 should be a number;', flags1, flags2);
return (flags1 & flags2) !== 0;
}
function hasNoFlag(flags, flag) {
// This function is inlined by the build script...
ASSERT(hasNoFlag.length === arguments.length, 'arg count');
ASSERT(typeof flag === 'number', 'hasNoFlag flag 1 should be number;', flag, flags);
ASSERT(typeof flags === 'number', 'hasNoFlag flag 2 should be number;', flag, flags);
return (flags & flag) === 0;
}
function Parser(code, options = {}) {
let {
goalMode: options_goalMode = GOAL_SCRIPT, // GOAL_SCRIPT | GOAL_MODULE | "script" | "module"
collectTokens: options_collectTokens = COLLECT_TOKENS_NONE, // COLLECT_TOKENS_NONE | COLLECT_TOKENS_SOLID | COLLECT_TOKENS_ALL | COLLECT_TOKENS_TYPES | "none" | "solid" | "all" | "types"
webCompat: options_webCompat = WEB_COMPAT_ON,
strictMode: options_strictMode = false,
astRoot: options_astRoot = null,
tokenStorage: options_tokenStorage,
getLexer = null,
allowGlobalReturn = false, // you may need this to parse arbitrary code or eval code for example
targetEsVersion = VERSION_WHATEVER, // 6, 7, 8, 9, 10, 11, 12, 2015, 2016, 2017, 2018, 2019, 2020, 2021, Infinity
exposeScopes: options_exposeScopes = false, // put scopes in the AST under `$scope` property?
astUids = false, // add an incremental uid to all ast nodes for debugging
ranges: options_ranges = false, // Add `range` to each `loc` object for absolute start/stop index on input?
nodeRange: options_nodeRange = false, // Add a `range` to each node itself, being an array. `input.slice(range[0], range[1])` should get you the text of the node.
locationTracking: options_locationTracking = true, // Add the `loc` property to all entries? (Much faster without...)
templateNewlineNormalization = true, // normalize \r and \rn to \n in the `.raw` of template nodes? Estree spec says yes, but makes it hard to serialize lossless
errorCodeFrame = true, // Print input code at error point (you'll probably want this, it helps debugging a lot)
truncCodeFrame = true, // Print only context of error in the code frame? Or all the input regardless of size.
// You can override the logging functions
$log = console.log,
$warn = console.warn,
$error = console.error,
sourceField = '', // This value is used to set the `source` field of the `loc` object of each AST node
// ast compatibility stuff?
babelCompat = false,
babelTokenCompat = false, // Add locs to tokens
acornCompat = false,
// Should we parse directives as their own AST nodes? (Other parsers do not, they just use ExpressionStatement)
// I'm super confused since I read https://github.com/estree/estree/pull/99 as that directives get their own node
// and in https://github.com/estree/estree/issues/6 many authors indicate to have adopted this PR, yet none of the
// parsers use Directive nodes. So I'm clearly overlooking something silly. *shrug*
/* (This comment prevents the buildscript from detecting the ast prefix) */AST_directiveNodes = false,
} = options;
if (targetEsVersion >= 2015 && targetEsVersion <= 2021) {
targetEsVersion -= 2009; // es6 = 2015, etc. 2015-2009=6
}
let goalMode = GOAL_SCRIPT;
if (typeof options_goalMode === 'string') {
if (options_goalMode === 'module') goalMode = GOAL_MODULE;
else if (options_goalMode === 'script') goalMode = GOAL_SCRIPT;
else throw new Error('Unknown goal symbol value: `' + options_goalMode + '`');
} else {
goalMode = options_goalMode;
}
let collectTokens = COLLECT_TOKENS_NONE;
if (typeof options_collectTokens === 'string') {
if (options_collectTokens === 'all') collectTokens = COLLECT_TOKENS_ALL;
else if (options_collectTokens === 'solid') collectTokens = COLLECT_TOKENS_SOLID;
else if (options_collectTokens === 'none') collectTokens = COLLECT_TOKENS_NONE;
else if (options_collectTokens === 'types') collectTokens = COLLECT_TOKENS_TYPES;
else throw new Error('Unknown collectTokens value: `' + options_collectTokens + '`');
} else {
collectTokens = options_collectTokens;
}
let NODE_NAME_PROPERTY = babelCompat ? 'ObjectProperty' : 'Property';
let NODE_NAME_METHOD_OBJECT = babelCompat ? 'ObjectMethod' : 'Property';
let NODE_NAME_METHOD_CLASS = babelCompat ? 'ClassMethod' : 'MethodDefinition';
let tok = Lexer(code, {
targetEsVersion,
parsingGoal: goalMode,
collectTokens,
returnTokens: babelCompat ? RETURN_COMMENT_TOKENS : RETURN_SOLID_TOKENS,
webCompat: options_webCompat,
gracefulErrors: FAIL_HARD,
tokenStorageExternal: options_tokenStorage,
babelTokenCompat,
errorCodeFrame,
truncCodeFrame,
$log,
$warn,
$error,
});
let tok_throw = tok.throw;
let tok_lexError = tok.lexError;
let tok_asi = tok.asi;
let tok_prevEndColumn = tok.prevEndColumn;
let tok_prevEndLine = tok.prevEndLine;
let tok_prevEndPointer = tok.prevEndPointer;
let tok_currColumn = tok.currColumn;
let tok_currLine = tok.currLine;
let tok_currPointer = tok.currPointer;
let tok_nextToken = tok.nextToken;
let tok_getNlwas = tok.getNlwas;
let tok_getCanoN = tok.getCanoN;
let tok_getType = tok.getType;
let tok_getStart = tok.getStart;
let tok_getStop = tok.getStop;
let tok_getLine = tok.getLine;
let tok_getColumn = tok.getColumn;
let tok_sliceInput = tok.sliceInput;
let assertExpectedFail = '';
let $tp_assertExpected_start = tok_getStart();
let $tp_assertExpected_stop = tok_getStop();
function ASSERT_VALID(bool, msg) {
// An assert that must at least hold when the parser would otherwise accept the input.
// This assert Will only throw an assertion error if the parser tripped over this but did not throw any actual error
// (This helps with asserting certain syntax errors that would be properly caught without the assert)
if (!bool) {
assertExpectedFail = msg + '\n' + (new Error().stack);
$tp_assertExpected_start = tok_getStart();
$tp_assertExpected_stop = tok_getStop();
}
}
function ASSERT(bool, desc = '', ...rest) {
if (!bool) {
THROW_RANGE('Assertion fail: ' + (desc || '<no desc>') + '; ' + JSON.stringify(rest), tok_getStart(), tok_getStop(), ':', ...rest);
}
}
let allowExponentiation = (targetEsVersion >= VERSION_EXPONENTIATION || targetEsVersion === VERSION_WHATEVER);
let allowTrailingFunctionComma = targetEsVersion >= VERSION_TRAILING_FUNC_COMMAS || targetEsVersion === VERSION_WHATEVER;
let allowAsyncFunctions = targetEsVersion >= VERSION_ASYNC || targetEsVersion === VERSION_WHATEVER;
let allowAsyncGenerators = targetEsVersion >= VERSION_ASYNC_GEN || targetEsVersion === VERSION_WHATEVER;
let allowBadEscapesInTaggedTemplates = targetEsVersion >= VERSION_TAGGED_TEMPLATE_BAD_ESCAPES || targetEsVersion === VERSION_WHATEVER;
let allowOptionalCatchBinding = targetEsVersion >= VERSION_OPTIONAL_CATCH || targetEsVersion === VERSION_WHATEVER;
let allowDynamicImport = (targetEsVersion >= VERSION_DYNAMIC_IMPORT || targetEsVersion === VERSION_WHATEVER);
let allowExportStarAs = (targetEsVersion >= VERSION_EXPORT_STAR_AS || targetEsVersion === VERSION_WHATEVER);
ASSERT(goalMode === GOAL_SCRIPT || goalMode === GOAL_MODULE);
ASSERT((targetEsVersion >= 6 && targetEsVersion <= 12) || targetEsVersion === VERSION_WHATEVER, 'version should be 6 7 8 9 10 12 2015 2016 2017 2018 2019 2020 2021 or Infinity');
if (getLexer) getLexer(tok);
function THROW_RANGE(desc, tokenStart, tokenStop, ...args) {
if (arguments.length < 3) throw new Error('Expecting 3 args for THROW_RANGE, received ' + arguments.length);
if (tokenStart > tokenStop) throw new Error('range should be >=0, was [' + tokenStart + ', ' + tokenStop + ']');
$log('\n');
$log('Error in parser:', desc, 'remaining throw args;', args);
// The "at eof" suffix also helps for reducing fuzz cases
let fullErrmsg = 'Parser error! ' + desc + (tok_getType() === $EOF ? ' (at EOF)' : '');
tok_throw(fullErrmsg, tokenStart, tokenStop, '');
}
let uid_counter = 0;
// https://github.com/estree/estree
// https://github.com/estree/estree/blob/master/es5.md
// https://github.com/estree/estree/blob/master/es2015.md
// https://astexplorer.net/
let _tree = {
type: 'Program',
loc: undefined,
body: [],
};
if (babelCompat) {
_tree = {
type: 'Program',
loc: undefined,
body: [],
sourceType: goalMode === GOAL_SCRIPT ? 'script' : 'module',
interpreter: null, // https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md#interpreterdirective
};
}
if (acornCompat) {
_tree = {
type: 'Program',
loc: undefined,
body: [],
sourceType: goalMode === GOAL_SCRIPT ? 'script' : 'module',
};
}
let _path = [_tree];
let _pnames;
ASSERT(_pnames = ['ROOT'], '(dev-only verification and debugging tool)');
if (options_astRoot) {
options_astRoot.root = _tree;
options_astRoot.path = _path;
ASSERT(options_astRoot.pathNames = _pnames, '(dev-only verification and debugging tool)');
}
function AST_getClosedLoc($tp_first_start, $tp_first_line, $tp_first_column) {
ASSERT(AST_getClosedLoc.length === arguments.length, 'arg count');
ASSERT([$tp_first_start, $tp_first_line, $tp_first_column].every(d => typeof d === 'number' && d >= 0), 'should receive all numbers, all zero-positive');
// Create a loc that is immediately closed
return AST_getCloseLoc($tp_first_start, $tp_first_line, $tp_first_column, tok_prevEndPointer(), tok_prevEndLine(), tok_prevEndColumn());
}
function AST_getCloseLoc(startIndex, startLine, startColumn, endIndex, endLine, endColumn) {
ASSERT(AST_getCloseLoc.length === arguments.length, 'arg count');
ASSERT([startIndex, startLine, startColumn, endIndex, endLine, endColumn].every(d => typeof d === 'number' && d >= 0), 'should receive all numbers, all zero-positive');
if (!options_locationTracking) {
if (options_ranges) {
// Note: return two distinct object when using ranges to prevent deopt
return {
range: {
start: startIndex | 0,
end: endIndex | 0,
},
};
}
return undefined;
}
if (options_ranges) {
// Note: return two distinct object when using ranges to prevent deopt
return {
start: {
line: startLine | 0, // offset 1
column: startColumn | 0,
},
end: {
line: endLine | 0,
column: endColumn | 0,
},
range: {
start: startIndex | 0,
end: endIndex | 0,
},
source: sourceField, // File containing the code being parsed. Source maps may use this.
};
}
return {
start: {
line: startLine, // offset 1
column: startColumn,
},
end: {
line: endLine,
column: endColumn,
},
source: sourceField, // File containing the code being parsed. Source maps may use this.
};
}
function AST_open(prop, newNode) {
ASSERT(arguments.length === AST_open.length, 'arg count');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
ASSERT(typeof prop === 'string' && prop !== 'undefined', 'prop should be string');
// The column offsets at 0
AST_setNode(prop, newNode);
_path[_path.length] = newNode;
ASSERT(_pnames.push(prop), '(dev-only verification and debugging tool)');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
}
function AST_close($tp_open_start, $tp_open_line, $tp_open_column, names_ASSERT_ONLY) {
ASSERT(AST_close.length === arguments.length, 'arg count');
// Note: names_ASSERT_ONLY is stripped in the build...
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
ASSERT(!names_ASSERT_ONLY.includes('TemplateElement'), 'use AST_closeTemplateElement instead');
ASSERT(!names_ASSERT_ONLY.includes('CommentBlock'), 'use AST_closeComment instead');
ASSERT(!names_ASSERT_ONLY.includes('CommentLine'), 'use AST_closeComment instead');
ASSERT(!names_ASSERT_ONLY.includes('Identifier'), 'use AST_closeIdent instead');
AST_set('loc', AST_getCloseLoc($tp_open_start, $tp_open_line, $tp_open_column, tok_prevEndPointer(), tok_prevEndLine(), tok_prevEndColumn()));
if (options_nodeRange) AST_set('range', [$tp_open_start, tok_prevEndPointer()])
// <SCRUB ASSERTS>
let was =
// </SCRUB ASSERTS>
_path.pop();
ASSERT(!options_locationTracking || was.loc.start.line <= was.loc.end.line, 'end line should be same or later than start (1)', was.loc);
ASSERT(!options_locationTracking || was.loc.start.line < was.loc.end.line || was.loc.start.column <= was.loc.end.column, 'if the node does not span multiple lines then the start column should come before the end column', was.loc);
ASSERT(!options_locationTracking || was.loc.start.line >= 1, 'start line should be >= 1', was.loc);
ASSERT(!options_locationTracking || was.loc.start.column >= 0, 'start column should be >= 0', was.loc);
ASSERT(!options_locationTracking || was.loc.end.line >= 1, 'end line should be >= 1', was.loc);
ASSERT(!options_locationTracking || was.loc.end.column >= 0, 'end column should be >= 0', was.loc);
ASSERT(!void _pnames.pop(), '(dev-only verification and debugging tool)');
ASSERT(!names_ASSERT_ONLY || (typeof names_ASSERT_ONLY === 'string' && names_ASSERT_ONLY === was.type) || (names_ASSERT_ONLY instanceof Array && names_ASSERT_ONLY.indexOf(was.type) >= 0), 'Expecting to close a node with given name(s), expected: ' + names_ASSERT_ONLY + ' but closed: ' + was.type)
}
function AST_closeTemplateElement(isTemplateDouble, $tp_tick_start, $tp_tick_line, $tp_tick_column) {
ASSERT(AST_closeTemplateElement.length === arguments.length, 'arg count');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
// The column offsets at 0
// For template elements the backticks, `${`, and `}` characters are ignored in the location ranges... so -1
let colEnd = tok_prevEndColumn() - 1;
let pointerEnd = tok_prevEndPointer();
if (isTemplateDouble) {
// This is for TICK_HEAD and TICK_BODY which start with `${`
--colEnd;
--pointerEnd;
}
AST_set('loc', AST_getCloseLoc($tp_tick_start, $tp_tick_line, $tp_tick_column, pointerEnd, tok_prevEndLine(), colEnd))
if (options_nodeRange) {
AST_set('range', [$tp_tick_start, pointerEnd])
}
// <SCRUB ASSERTS>
let was =
// </SCRUB ASSERTS>
_path.pop();
ASSERT(!options_locationTracking || was.loc.start.line <= was.loc.end.line, 'end line should be same or later than start (2)', was.loc);
ASSERT(!options_locationTracking || was.loc.start.line < was.loc.end.line || was.loc.start.column <= was.loc.end.column, 'if the node does not span multiple lines then the start column should come before the end column', was.loc);
ASSERT(!options_locationTracking || was.loc.start.line >= 1, 'start line should be >= 1', was.loc);
ASSERT(!options_locationTracking || was.loc.start.column >= 0, 'start column should be >= 0', was.loc);
ASSERT(!options_locationTracking || was.loc.end.line >= 1, 'end line should be >= 1', was.loc);
ASSERT(!options_locationTracking || was.loc.end.column >= 0, 'end column should be >= 0', was.loc);
ASSERT(!void _pnames.pop(), '(dev-only verification and debugging tool)');
ASSERT(was.type === 'TemplateElement', 'Expecting to close a TemplateElement node but closed: ' + was.type)
}
function AST_set(prop, value) {
ASSERT(AST_set.length === arguments.length, 'expecting two args');
ASSERT(typeof prop === 'string', 'prop should be string');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
ASSERT(prop[0] === '$' || prop === 'range' || prop === 'directives' || prop === 'extra' || _path[_path.length - 1].hasOwnProperty(prop), 'all ast node members should be predefined', prop, _path[_path.length - 1].type);
// Set a property value and expect it to be undefined before
ASSERT(_path[_path.length - 1][prop] === undefined, 'use AST_clobber? This func doesnt clobber, prop=' + prop + ', val=' + value);
_path[_path.length - 1][prop] = value;
}
function AST_setNode(astProp, node) {
ASSERT(AST_setNode.length === arguments.length, 'arg count');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
ASSERT(typeof node === 'object' && node && typeof node.type === 'string', 'should receive ast node to set', node);
ASSERT(typeof astProp === 'string' && astProp !== 'undefined', 'prop should be string');
if (astUids) node.$uid = uid_counter++;
//if (options_nodeRange) node.range = [$tp_node_start, tok_prevEndPointer()];
let parentNode = _path[_path.length - 1];
let p = parentNode[astProp];
if (Array.isArray(p)) {
p[p.length] = node;
}
else {
ASSERT(p === undefined, `(this invariant does not hold without ASSERTs!) parentNode[astProp] should be empty or an array`, astProp, p && p.type);
parentNode[astProp] = node;
}
}
function AST_setClosedNode($tp_node_start, astProp, node) {
ASSERT(AST_setClosedNode.length === arguments.length, 'arg count');
// Set a node that is immediately closed
// This function's main purpose is to allow setting a range property on the node
if (options_nodeRange) node.range = [$tp_node_start, tok_prevEndPointer()];
AST_setNode(astProp, node);
}
function AST_setNodeDangerously(astProp, $tp_node_start, node) {
ASSERT(AST_setNodeDangerously.length === arguments.length, 'arg count');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
ASSERT(typeof node === 'object' && (!node || typeof node.type === 'string'), 'should receive ast node to set or null', node);
ASSERT(typeof $tp_node_start === 'number', '$tp_node_start should be the offset of the node', $tp_node_start);
ASSERT(typeof astProp === 'string' && astProp !== 'undefined', 'prop should be string', astProp);
if (astUids && node) node.$uid = uid_counter++;
if (options_nodeRange && node) node.range = [$tp_node_start, tok_prevEndPointer()];
let parentNode = _path[_path.length - 1];
let p = parentNode[astProp];
if (Array.isArray(p)) {
p[p.length] = node;
}
else {
ASSERT(p === undefined, `(this invariant does not hold without ASSERTs!) parentNode[astProp] should be empty or an array`);
parentNode[astProp] = node;
}
}
function AST_setIdent(astProp, $tp_ident_start, $tp_ident_stop, $tp_ident_line, $tp_ident_column, $tp_ident_canon) {
ASSERT(AST_setIdent.length === arguments.length, 'arg count');
ASSERT(typeof astProp === 'string' && astProp !== 'undefined', 'prop should be string');
ASSERT($tp_ident_start !== tok_getStart(), 'token should be consumed to ensure location data is correct');
let identNode = AST_getIdentNode($tp_ident_start, $tp_ident_stop, $tp_ident_line, $tp_ident_column, $tp_ident_canon);
AST_setNode(astProp, identNode);
}
function AST_getIdentNode($tp_ident_start, $tp_ident_stop, $tp_ident_line, $tp_ident_column, $tp_ident_canon) {
ASSERT(AST_getIdentNode.length === arguments.length, 'arg count');
ASSERT(typeof $tp_ident_canon === 'string');
ASSERT($tp_ident_canon === $tp_ident_canon);
let col = $tp_ident_column;
let line = $tp_ident_line;
let len = $tp_ident_stop - $tp_ident_start;
// Idents can't contain newlines so the end. column should be start. column+len
let colEnd = col + len;
let identNode = {
type: 'Identifier',
loc: AST_getCloseLoc($tp_ident_start, line, col, $tp_ident_stop, line, colEnd),
// name value doesn't seem to be specced in estree but it makes sense to use the canonical name here
name: $tp_ident_canon,
};
if (options_nodeRange) identNode.range = [$tp_ident_start, $tp_ident_stop];
if (babelCompat) identNode.loc.identifierName = $tp_ident_canon;
ASSERT(!options_locationTracking || (identNode.loc.end.column - identNode.loc.start.column === ($tp_ident_stop - $tp_ident_start)), 'for idents the location should only span exactly the length of the ident and cannot hold newlines');
return identNode;
}
function AST_setLiteral(astProp, $tp_lit_type, $tp_lit_start, $tp_lit_stop, $tp_lit_line, $tp_lit_column, $tp_lit_canon) {
_AST_setLiteral(astProp, $tp_lit_type, $tp_lit_start, $tp_lit_stop, $tp_lit_line, $tp_lit_column, $tp_lit_canon, false);
}
function _AST_setLiteral(astProp, $tp_lit_type, $tp_lit_start, $tp_lit_stop, $tp_lit_line, $tp_lit_column, $tp_lit_canon, fromDirective) {
ASSERT(_AST_setLiteral.length === arguments.length, 'arg count');
ASSERT(typeof astProp === 'string', 'prop is string');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
ASSERT(isNumberStringRegex($tp_lit_type), 'should be number or string');
ASSERT($tp_lit_start !== tok_getStart(), 'token should be consumed to ensure location data is correct');
if (isStringToken($tp_lit_type)) {
AST_setStringLiteral(astProp, $tp_lit_start, $tp_lit_stop, $tp_lit_line, $tp_lit_column, $tp_lit_canon, fromDirective);
}
else if (isNumberToken($tp_lit_type)) {
if (isBigintToken($tp_lit_type)) {
AST_setBigInt(astProp, $tp_lit_start, $tp_lit_stop, $tp_lit_line, $tp_lit_column);
} else {
AST_setNumberLiteral(astProp, $tp_lit_type, $tp_lit_start, $tp_lit_stop, $tp_lit_line, $tp_lit_column);
}
}
else {
ASSERT(isRegexToken($tp_lit_type), 'this must be regex now because this function is only called for strings, numbers, and regexes');
ASSERT(tok_sliceInput($tp_lit_start, $tp_lit_stop).split('/').length > 2, 'a regular expression should have at least two forward slashes', tok_sliceInput($tp_lit_start, $tp_lit_stop));
AST_setRegexLiteral(astProp, $tp_lit_start, $tp_lit_stop, $tp_lit_line, $tp_lit_column);
}
}
function AST_getStringNode($tp_string_start, $tp_string_stop, $tp_string_line, $tp_string_column, $tp_string_canon, fromDirective) {
ASSERT(AST_getStringNode.length === arguments.length, 'arg count');
if (babelCompat) return AST_babelGetStringNode($tp_string_start, $tp_string_stop, $tp_string_line, $tp_string_column, $tp_string_canon, fromDirective);
let node = {
type: 'Literal',
loc: AST_getCloseLoc($tp_string_start, $tp_string_line, $tp_string_column, tok_prevEndPointer(), tok_prevEndLine(), tok_prevEndColumn()),
value: $tp_string_canon,
raw: tok_sliceInput($tp_string_start, $tp_string_stop),
};
if (options_nodeRange) node.range = [$tp_string_start, $tp_string_stop];
return node;
}
function AST_setStringLiteral(astProp, $tp_string_start, $tp_string_stop, $tp_string_line, $tp_string_column, $tp_string_canon, fromDirective) {
ASSERT(AST_setStringLiteral.length === arguments.length, 'arg count');
ASSERT(typeof astProp === 'string' && astProp !== 'undefined', 'prop should be string');
ASSERT($tp_string_start !== tok_getStart(), 'token should be consumed to ensure location data is correct');
// Open a node and immediately close it. Only works if the column offsets do not depend on something being consumed
// between open and close (which is often the case). So this is used for literals (while idents have their own func)
let stringNode = AST_getStringNode($tp_string_start, $tp_string_stop, $tp_string_line, $tp_string_column, $tp_string_canon, fromDirective);
AST_setNode(astProp, stringNode); // for ASSERTs only!
}
function AST_getNumberNode($tp_number_type, $tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column) {
ASSERT(AST_getNumberNode.length === arguments.length, 'arg count');
if (isBigintToken($tp_number_type)) return AST_getBigIntNode($tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column);
if (babelCompat) return AST_babelGetNumberNode($tp_number_type, $tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column);
let str = tok_sliceInput($tp_number_start, $tp_number_stop);
// Remove numeric separators before using parseFloat/parseInt on it. Should be safe to drop them unconditionally.
// If perf is a concern I could always manually check this, or even pass a hint from the lexer. But meh.
let descored = str.includes('_') ? str.replace(/_/g, '') : str;
let value =
$tp_number_type === $NUMBER_DEC ? parseFloat(descored) : // parseFloat also deals with `e` cases
$tp_number_type === $NUMBER_HEX ? parseInt(descored.slice(2), 16) :
$tp_number_type === $NUMBER_BIN ? parseInt(descored.slice(2), 2) :
$tp_number_type === $NUMBER_OCT ? parseInt(descored.slice(2), 8) :
(
ASSERT($tp_number_type === $NUMBER_OLD, 'number types are enum and bigint should not reach this'),
ASSERT(descored !== '0', 'a zero should just be a decimal'),
descored.includes('8') || descored.includes('9')
? parseFloat(descored.slice(1))
: parseInt(descored.slice(1), 8)
);
const node = {
type: 'Literal',
loc: AST_getCloseLoc($tp_number_start, $tp_number_line, $tp_number_column, $tp_number_stop, $tp_number_line, $tp_number_column + ($tp_number_stop - $tp_number_start)),
value: value,
raw: str,
};
if (options_nodeRange) node.range = [$tp_number_start, $tp_number_stop];
return node;
}
function AST_setNumberLiteral(astProp, $tp_number_type, $tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column) {
ASSERT(AST_setNumberLiteral.length === arguments.length, 'arg count');
ASSERT(typeof astProp === 'string' && astProp !== 'undefined', 'prop should be string');
ASSERT($tp_number_start !== tok_getStart(), 'token should be consumed to ensure location data is correct');
// Open a node and immediately close it. Only works if the column offsets do not depend on something being consumed
// between open and close (which is often the case). So this is used for literals (while idents have their own func)
let numberNode = AST_getNumberNode($tp_number_type, $tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column);
AST_setNode(astProp, numberNode); // for ASSERTs only!
}
function AST_getBigIntNode($tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column ) {
// [v] `45n`
// [v] `0b100n`
// [v] `0o533n`
// [v] `0xabcn`
// https://github.com/estree/estree/pull/198/files
// Open a node and immediately close it. Only works if the column offsets do not depend on something being consumed
// between open and close (which is often the case). So this is used for literals (while idents have their own func)
if (acornCompat) return AST_acornGetBigIntNode($tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column);
if (babelCompat) return AST_babelGetBigIntNode($tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column);
const node = {
type: 'BigIntLiteral',
loc: AST_getCloseLoc($tp_number_start, $tp_number_line, $tp_number_column, $tp_number_stop, $tp_number_line, $tp_number_column + ($tp_number_stop - $tp_number_start)),
value: null,
bigint: tok_sliceInput($tp_number_start, $tp_number_stop - 1), // TODO: Normalize... https://github.com/estree/estree/issues/200
};
if (options_nodeRange) node.range = [$tp_number_start, $tp_number_stop];
return node;
}
function AST_setBigInt(astProp, $tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column) {
ASSERT(AST_setBigInt.length === arguments.length, 'arg count');
ASSERT(typeof astProp === 'string' && astProp !== 'undefined', 'prop should be string');
ASSERT($tp_number_start !== tok_getStart(), 'token should be consumed to ensure location data is correct');
let bigintNode = AST_getBigIntNode($tp_number_start, $tp_number_stop, $tp_number_line, $tp_number_column);
AST_setNode(astProp, bigintNode); // for ASSERTs only!
}
function AST_getRegexNode($tp_regex_start, $tp_regex_stop, $tp_regex_line, $tp_regex_column) {
ASSERT(AST_getRegexNode.length === arguments.length, 'arg count');
if (acornCompat) return AST_acornGetRegexNode($tp_regex_start, $tp_regex_stop, $tp_regex_line, $tp_regex_column);
if (babelCompat) return AST_babelGetRegexNode($tp_regex_start, $tp_regex_stop, $tp_regex_line, $tp_regex_column);
// Open a node and immediately close it. Only works if the column offsets do not depend on something being consumed
// between open and close (which is often the case). So this is used for literals (while idents have their own func)
let str = tok_sliceInput($tp_regex_start, $tp_regex_stop);
let pos = str.lastIndexOf('/');
let body = str.slice(1, pos);
let tail = str.slice(pos + 1);
// https://github.com/estree/estree/blob/master/es5.md#regexpliteral
const node = {
type: 'Literal',
loc: AST_getCloseLoc($tp_regex_start, $tp_regex_line, $tp_regex_column, $tp_regex_stop, $tp_regex_line, $tp_regex_column + ($tp_regex_stop - $tp_regex_start)),
value: null,
regex: {
pattern: body,
flags: tail,
},
raw: str,
};
if (options_nodeRange) node.range = [$tp_regex_start, $tp_regex_stop];
return node;
}
function AST_setRegexLiteral(astProp, $tp_regex_start, $tp_regex_stop, $tp_regex_line, $tp_regex_column) {
ASSERT(AST_setRegexLiteral.length === arguments.length, 'arg count');
ASSERT(typeof astProp === 'string' && astProp !== 'undefined', 'prop should be string');
ASSERT($tp_regex_start !== tok_getStart(), 'token should be consumed to ensure location data is correct');
let regexNode = AST_getRegexNode($tp_regex_start, $tp_regex_stop, $tp_regex_line, $tp_regex_column);
AST_setNode(astProp, regexNode); // for ASSERTs only!
}
function AST_add(prop, value) {
ASSERT(typeof prop === 'string', 'prop should be string');
ASSERT(arguments.length === 2, 'expecting two args');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
ASSERT(Array.isArray(_path[_path.length - 1][prop]), 'expecting to add to an existing array');
let arr = _path[_path.length - 1][prop];
arr[arr.length] = value;
}
function AST_popNode(prop) {
ASSERT(AST_popNode.length === arguments.length, 'arg count');
// Get the current "top" node and either remove it from the parent array, or mark it as `undefined` inside an ASSERT
let parent = _path[_path.length-1];
let p = parent[prop];
ASSERT(p || p === null, 'the prop should exist... (and be a node, or null for the init in `for (;;);`)', prop, parent.type);
if (Array.isArray(p)) {
ASSERT(Array.isArray(p), 'ast nodes do not have a `length` property so this duck type check should have sufficed');
ASSERT(p.length);
return p.pop();
} else {
ASSERT(!void(parent[prop] = undefined), '(mark as undefined so that assertions dont trip over the value existing)');
return p;
}
}
function AST_wrapClosedCustom(prop, newNode, newProp) { // TODO: a build can strip the second arg ... maybe we can formalize that a little bit
ASSERT(AST_wrapClosedCustom.length === arguments.length, 'arg count');
ASSERT(typeof prop === 'string', 'should be string');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths', 'pnames='+_pnames+', path=' + _path.map(p => p.type));
// wrap the "current" node (last of _tree) with an extra node
// so the parent of node becomes the parent of a new node and
// the old node becomes a child of the new node
// a(x:b) -> a(x:c(y:b))
// If the child is an array, replace the last element in the array but not the array itself
let child = AST_popNode(prop);
AST_open(prop, newNode);
// set it as child of new node
AST_set(newProp, child);
}
function AST_wrapClosedIntoArrayCustom(prop, newNode, newProp) {
ASSERT(AST_wrapClosedIntoArrayCustom.length === arguments.length, 'arg count');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
// same as AST_wrapClosed except the node is put in an array
let child = AST_popNode(prop);
AST_open(prop, newNode);
// set the node as the first child of the property as an array
AST_set(newProp, [child]);
}
function AST_destruct(prop) {
// rename object and array literal nodes to patterns to match the AST spec
// this happens when arr/obj literal was parsed (possibly nested) and
// then a destructuring assignment was encountered
// this is also called for function args and any other destructuring (like catch binding)
// recursively walk the tree from the prop in open node and visit
// any array and object expression as well as the left side of assignments
// for now dont bother with other nodes until we find a reason to
// note: this function is usually called after a few nodes have closed (the literal struct).
ASSERT(arguments.length === 1, 'arg count');
ASSERT(_path.length > 0, 'path shouldnt be empty');
ASSERT(_pnames.length === _path.length, 'pnames should have as many names as paths');
let parent = _path[_path.length-1];
let node = parent[prop];
ASSERT(node, 'top[' + prop + '] should be a node');
if (Array.isArray(node)) {
// The destruct applies to the node just closed, so last in list
let last = node.length - 1;
AST__destruct(node[last], node, last);
return;
}
AST__destruct(node, parent, prop);
}
function AST__destruct(node, parent, astProp) {
ASSERT(AST__destruct.length === arguments.length, 'arg count');
switch (node.type) {
case 'ArrayExpression':
node.type = 'ArrayPattern';
let elements = node.elements;
let e = elements.length;
for (let i = 0; i < e; ++i) {
let element = elements[i];
// note: children can be null (elided array destruct) but not undefined
if (element) AST__destruct(element, elements, i);
}
return;
case 'ObjectExpression':
node.type = 'ObjectPattern';
let properties = node.properties;
let n = properties.length;
for (let i = 0; i < n; ++i) {
if (properties[i].type === NODE_NAME_PROPERTY) {
ASSERT(properties[i].value, 'each property should have a value');
} else {
ASSERT(properties[i].type === 'SpreadElement', 'expecting only properties, spreads, and assignments here');
ASSERT(properties[i].argument, 'each property should have a value');
}
AST__destruct(properties[i], properties, i);
}
return;
case 'AssignmentExpression':
// walk the left of the assignment only
AST__destruct(node.left, node, 'left');
AST_destructReplaceAssignment(parent, astProp);
return;
case NODE_NAME_PROPERTY:
AST__destruct(node.value, node, 'value');
return;
case 'SpreadElement':
// `([...x]);` vs `([...x]) => x`
// `({...x});` vs `({...x}) => x`
// TODO: this seems to deopt. can also set the parent[prop] = {} new node appraoch. need to benchmark that.
node.type = 'RestElement';
AST__destruct(node.argument, node, 'argument');
return;
}
}
function AST_destructReplaceAssignment(parentNode, prop) {
let oldNode = parentNode[prop];
if (oldNode.operator !== '=') {
return THROW_RANGE('The destructuring assignment should be a regular assignment', tok_getStart(), tok_getStop());
}
let newNode = {
type: 'AssignmentPattern',
loc: oldNode.loc,
left: oldNode.left,
right: oldNode.right,
};
if (options_nodeRange) newNode.range = oldNode.range;
parentNode[prop] = newNode;
}
function AST_convertArrayToPattern($tp_eq_type, astProp) {
ASSERT(AST_convertArrayToPattern.length === arguments.length, 'arg count');
if ($tp_eq_type === $PUNC_EQ) {
let node = _path[_path.length - 1][astProp];
if (Array.isArray(node)) {
node = node[node.length - 1];
}
if (node.type === 'ArrayExpression' || node.type === 'ObjectExpression') {
AST_destruct(astProp);
}
}
}
function AST_throwIfIllegalUpdateArg(astProp) {
ASSERT(AST_throwIfIllegalUpdateArg.length === arguments.length, 'arg count');
ASSERT(typeof astProp === 'string', 'astprop string');
// Using the AST for this because in the current propagation system we can only tell whether the parsed part
// is assignable or not, and in this reading something that can be destructured can be assigned to.
let head = _path[_path.length - 1];
let prev = head && head[astProp];
// Note: the for-case is nasty because when parsing the lhs the AST is not yet populated with a `for` statement
// because that particular node type depends on `in`, `of`, or a semi. So the AST could be an array (block body)
if (
!prev ||
(
prev instanceof Array ?
// - `for (x--;;);`
!prev.length || (prev[prev.length - 1].type !== 'Identifier' && prev[prev.length - 1].type !== 'MemberExpression') :
// - `[]++`
(prev.type !== 'Identifier' && prev.type !== 'MemberExpression')
)
) {
// - `++[]`
// - `--f()`
// - `++this`
// - `[]++`
// - `f()--`
// - `this++`
return THROW_RANGE('Can only increment or decrement an identifier or member expression', tok_getStart(), tok_getStop());
}
}
function AST_patchAsyncCall($tp_async_start, $tp_async_stop, $tp_async_line, $tp_async_column, $tp_async_canon, astProp) {
ASSERT(AST_patchAsyncCall.length === arguments.length, 'arg count');
let node = _path[_path.length - 1];
let args = node[astProp];
if (args instanceof Array) args = args[0];
ASSERT(args, 'should have parsed someting v1');
if (args.type === 'SequenceExpression') args = args.expressions;
else args = [args];
ASSERT(args, 'should have parsed someting v2');
if (node[astProp] instanceof Array) node[astProp] = [];
else node[astProp] = undefined;
// TODO: verify the args
ASSERT(_path[_path.length - 1][astProp] instanceof Array || !void(_path[_path.length - 1][astProp] = undefined), '(there is an assert that confirms that the property is undefined and we expect this not to be the case here)');
AST_setClosedNode($tp_async_start, astProp, {
type: 'CallExpression',
loc: AST_getClosedLoc($tp_async_start, $tp_async_line, $tp_async_column),
optional: false,
callee: AST_getIdentNode($tp_async_start, $tp_async_stop, $tp_async_line, $tp_async_column, $tp_async_canon),
arguments: args,
});
}
function AST_babelDirectives() {
// Remove Dir