Module refinery.lib.scripts.js.parser
Expand source code Browse git
from __future__ import annotations
from contextlib import contextmanager
from refinery.lib.scripts.js.lexer import (
JsLexer,
JsLexerState,
decode_js_string_body,
decode_js_template_body,
identifier_string_value,
)
from refinery.lib.scripts.js.model import (
Expression,
JsArrayExpression,
JsArrayPattern,
JsArrowFunctionExpression,
JsAssignmentExpression,
JsAssignmentPattern,
JsAwaitExpression,
JsBigIntLiteral,
JsBinaryExpression,
JsBlockStatement,
JsBooleanLiteral,
JsBreakStatement,
JsCallExpression,
JsCatchClause,
JsClassBody,
JsClassDeclaration,
JsClassExpression,
JsConditionalExpression,
JsContinueStatement,
JsDebuggerStatement,
JsDecorator,
JsDoWhileStatement,
JsEmptyStatement,
JsErrorNode,
JsExportAllDeclaration,
JsExportDefaultDeclaration,
JsExportNamedDeclaration,
JsExportSpecifier,
JsExpressionStatement,
JsForInStatement,
JsForOfStatement,
JsForStatement,
JsFunctionDeclaration,
JsFunctionExpression,
JsIdentifier,
JsIfStatement,
JsImportAttribute,
JsImportDeclaration,
JsImportDefaultSpecifier,
JsImportExpression,
JsImportNamespaceSpecifier,
JsImportSpecifier,
JsLabeledStatement,
JsLogicalExpression,
JsMemberExpression,
JsMetaProperty,
JsMethodDefinition,
JsMethodKind,
JsNewExpression,
JsNullLiteral,
JsNumericLiteral,
JsObjectExpression,
JsObjectPattern,
JsParenthesizedExpression,
JsPrivateIdentifier,
JsProperty,
JsPropertyDefinition,
JsPropertyKind,
JsRegExpLiteral,
JsRestElement,
JsReturnStatement,
JsScript,
JsSequenceExpression,
JsSpreadElement,
JsStaticBlock,
JsStringLiteral,
JsSwitchCase,
JsSwitchStatement,
JsTaggedTemplateExpression,
JsTemplateElement,
JsTemplateLiteral,
JsThisExpression,
JsThrowStatement,
JsTryStatement,
JsUnaryExpression,
JsUpdateExpression,
JsVariableDeclaration,
JsVariableDeclarator,
JsVarKind,
JsWhileStatement,
JsWithStatement,
JsYieldExpression,
Statement,
)
from refinery.lib.scripts.js.strict import mark_directives, mark_module
from refinery.lib.scripts.js.token import RESERVED_WORD_NAMES, JsToken, JsTokenKind
_PREC_EXPONENTIATION = 15
_BINARY_PREC: dict[JsTokenKind, tuple[int, bool]] = {
JsTokenKind.QQ: ( 4, True), # noqa
JsTokenKind.OR: ( 5, True), # noqa
JsTokenKind.AND: ( 6, True), # noqa
JsTokenKind.PIPE: ( 7, False), # noqa
JsTokenKind.CARET: ( 8, False), # noqa
JsTokenKind.AMP: ( 9, False), # noqa
JsTokenKind.EQ2: (10, False), # noqa
JsTokenKind.BANG_EQ: (10, False), # noqa
JsTokenKind.EQ3: (10, False), # noqa
JsTokenKind.BANG_EQ2: (10, False), # noqa
JsTokenKind.LT: (11, False), # noqa
JsTokenKind.GT: (11, False), # noqa
JsTokenKind.LT_EQ: (11, False), # noqa
JsTokenKind.GT_EQ: (11, False), # noqa
JsTokenKind.INSTANCEOF: (11, False), # noqa
JsTokenKind.IN: (11, False), # noqa
JsTokenKind.LT2: (12, False), # noqa
JsTokenKind.GT2: (12, False), # noqa
JsTokenKind.GT3: (12, False), # noqa
JsTokenKind.PLUS: (13, False), # noqa
JsTokenKind.MINUS: (13, False), # noqa
JsTokenKind.STAR: (14, False), # noqa
JsTokenKind.SLASH: (14, False), # noqa
JsTokenKind.PERCENT: (14, False), # noqa
JsTokenKind.STAR2: (_PREC_EXPONENTIATION, False), # noqa
}
_VAR_KIND_MAP: dict[JsTokenKind, JsVarKind] = {
JsTokenKind.VAR: JsVarKind.VAR, # noqa
JsTokenKind.LET: JsVarKind.LET, # noqa
JsTokenKind.CONST: JsVarKind.CONST, # noqa
}
_PROP_KIND_MAP: dict[str, JsPropertyKind] = {
'get': JsPropertyKind.GET,
'set': JsPropertyKind.SET,
}
class JsParser:
@staticmethod
def _parse_int_text(text: str) -> int:
if text.startswith(('0x', '0X')):
return int(text, 16)
if text.startswith(('0o', '0O')):
return int(text, 8)
if text.startswith(('0b', '0B')):
return int(text, 2)
if len(text) > 1 and text[0] == '0' and all(d in '01234567' for d in text):
return int(text, 8)
return int(text)
def __init__(self, source: str, *, top_level_await: bool = False):
self._lexer = JsLexer(source)
self._source = source
self._tokens = self._lexer.tokenize()
self._current: JsToken = JsToken(JsTokenKind.EOF, '', 0)
self._preceded_by_newline: bool = False
self._ahead: JsToken | None = None
self._ahead_newline: bool = False
self._ahead_state: tuple[JsLexerState, int] | None = None
self._no_in: bool = False
self._in_async: bool = top_level_await
self._in_generator: bool = False
self._pending_comments: list[str] = []
self._recovered: bool = False
self._advance()
def _pull_token(self) -> tuple[JsToken, bool]:
had_newline = False
while True:
tok = next(self._tokens, JsToken(JsTokenKind.EOF, '', len(self._source)))
if tok.kind == JsTokenKind.NEWLINE:
had_newline = True
continue
if tok.kind == JsTokenKind.COMMENT:
self._pending_comments.append(tok.value)
continue
break
return tok, had_newline
def _advance(self) -> JsToken:
prev = self._current
if self._ahead is not None:
self._current = self._ahead
self._preceded_by_newline = self._ahead_newline
self._ahead = None
self._ahead_state = None
return prev
self._current, self._preceded_by_newline = self._pull_token()
return prev
def _drain_comments(self, node):
if self._pending_comments:
node.leading_comments.extend(self._pending_comments)
self._pending_comments.clear()
def _peek(self) -> JsToken:
return self._current
def _peek_next(self) -> JsToken:
if self._ahead is None:
self._ahead_state = self._lexer.capture(), len(self._pending_comments)
self._ahead, self._ahead_newline = self._pull_token()
return self._ahead
def _rescan_as_regexp(self) -> JsToken:
"""
Read the slash the parser is holding again, as the regular expression it begins. A slash is
the one character whose token depends on where the grammar stands rather than on what the
text says, and the lexer is not standing anywhere: it spells every slash as an operator, and
this is the single place that knows that an expression is about to begin. Rewinding is what
makes that affordable — reading a division that was a regular expression costs one token,
whereas the other way around has already swallowed the rest of the line.
Everything scanned since the slash is given back, because a lookahead token may have opened
or closed a template hole and a skipped comment would otherwise be collected twice. The
slash itself is given back too where no literal begins there, so that a scan which found no
terminator on its line leaves the operator standing rather than a literal nobody wrote.
"""
if self._ahead_state is None:
state, comments = self._lexer.capture(), len(self._pending_comments)
else:
state, comments = self._ahead_state
resume_pos, resume_state = self._lexer.pos, self._lexer.capture()
self._lexer.rewind(self._current.offset, state)
token = self._lexer.scan_regexp()
if token is None:
self._lexer.rewind(resume_pos, resume_state)
return self._current
del self._pending_comments[comments:]
self._ahead = None
self._ahead_newline = False
self._ahead_state = None
self._current = token
self._tokens = self._lexer.tokenize()
return self._current
def _at(self, *kinds: JsTokenKind) -> bool:
return self._current.kind in kinds
def _eat(self, kind: JsTokenKind) -> JsToken | None:
if self._current.kind == kind:
return self._advance()
return None
def _expect(self, kind: JsTokenKind) -> JsToken:
"""
The token that must stand here, where it does. Where it does not, the parser writes it
itself and steps over what was there, so that a file it cannot read is still answered with a
tree. That answer is a program the source does not hold, in both directions at once — a
bracket nobody wrote is invented and the token that stood in its place is dropped — so the
file is recorded as one the parser repaired. `JsScript.recovered` carries that to
`refinery.lib.scripts.is_well_formed`, which is what keeps a truncated payload from being
spliced into a host file as though it had been written whole.
"""
if self._current.kind == kind:
return self._advance()
tok = self._current
self._recovered = True
self._advance()
return JsToken(kind, tok.value, tok.offset, tok.terminated)
def _require(self, kind: JsTokenKind) -> None:
"""
The token the grammar requires here, consumed where it stands here. Where it does not the
parser goes on without it and records the repair, leaving what is here to be read as
whatever comes next.
This is what a list separator wants, and it is why `_expect` is the wrong primitive for one:
what follows a missing comma is the next element of the list, not a token to be thrown away,
so `{ a: 1 b: 2 }` is a file the parser reports having repaired and still prints everything
that was written in.
"""
if self._eat(kind) is None:
self._recovered = True
def _at_identifier_name(self) -> bool:
"""
Whether a word stands here. Every word the language has is an IdentifierName, a keyword no
less than a name, so a position taking one takes them all and what this refuses is a token
that spells no word at all.
"""
return self._at(JsTokenKind.IDENTIFIER) or self._current.kind.is_keyword
def _is_binding_identifier(self, token: JsToken) -> bool:
"""
Whether the token can serve as an ordinary binding or reference name. Several contextual
keywords (`as`, `from`, `of`, `let`, `async`) are always valid names, while `await` and
`yield` are valid names except inside an async function or a generator respectively. This is
the identifier acceptance of `_parse_primary_expression` itself, so a name-reading site
accepts exactly the tokens the expression grammar would treat as a reference.
"""
kind = token.kind
return (
kind in (
JsTokenKind.IDENTIFIER,
JsTokenKind.AS,
JsTokenKind.FROM,
JsTokenKind.OF,
JsTokenKind.LET,
JsTokenKind.ASYNC,
)
or (kind is JsTokenKind.AWAIT and not self._in_async)
or (kind is JsTokenKind.YIELD and not self._in_generator)
)
def _at_binding_identifier(self) -> bool:
return self._is_binding_identifier(self._current)
def _at_function_name(self) -> bool:
"""
Whether the token standing here names the function whose `function` keyword was just read.
Only a name or the parameter list may stand in that position, so `yield` and `await` are
read as the name wherever they appear rather than through `_at_binding_identifier`, whose
answer is about the enclosing function's kind.
That answer is the wrong one here in both directions. A function expression's name takes its
own kind and not the enclosing one, so `function* g() { var f = function yield() {}; }` is a
program whose name would otherwise be dropped; and where the name really is an early error
the tree must still spell it, so that
`refinery.lib.scripts.js.strict.reserved_by_function_kind` reports it rather than the
parameter list being read starting at the name.
"""
return (
self._at_binding_identifier()
or self._at(JsTokenKind.YIELD, JsTokenKind.AWAIT)
)
def _at_variable_declaration(self) -> bool:
"""
Whether a variable declaration begins here, rather than an expression that merely opens with
the same word. ECMA-262 reserves `let` in strict code only, so wherever a statement may also
be read as an expression, a `let` declares nothing unless a binding follows it: it is a name
being called in `let(1)`, divided in `let / 2` and read in `let.a`, and only `let [` is the
spelling a statement is forbidden to take as an expression.
"""
if self._at(JsTokenKind.VAR, JsTokenKind.CONST):
return True
if not self._at(JsTokenKind.LET):
return False
ahead = self._peek_next()
return (
self._is_binding_identifier(ahead)
or ahead.kind in (JsTokenKind.LBRACKET, JsTokenKind.LBRACE)
)
def _eat_semicolon(self) -> bool:
"""
The semicolon that ends a statement, whether the file wrote it or the language supplies it.
ECMA-262 supplies one before a token a line terminator separates from what came before,
before a closing brace, and at the end of the file, so those three are the whole of what a
statement may end with instead of a semicolon.
Where none of them stands here the parser writes the semicolon anyway, which is a repair
like any other: `x = 1 y = 2` is a file no engine reads, and left unrecorded it comes back
as the two statements the parser split it into. The one place the language inserts a
semicolon on no such condition is after a `do` loop's `while` clause, which reads the
semicolon it may find with `_eat` rather than asking here.
"""
if self._eat(JsTokenKind.SEMICOLON):
return True
if self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
return True
if self._preceded_by_newline:
return True
self._recovered = True
return False
@contextmanager
def _with_no_in(self, value: bool):
saved = self._no_in
self._no_in = value
try:
yield
finally:
self._no_in = saved
@contextmanager
def _function_body_context(self, is_async: bool, is_generator: bool):
saved = (self._in_async, self._in_generator)
self._in_async = is_async
self._in_generator = is_generator
try:
yield
finally:
self._in_async, self._in_generator = saved
def parse(self) -> JsScript:
script = self._parse_program()
mark_directives(script)
mark_module(script)
return script
def _parse_statement_list(self, *stop: JsTokenKind) -> list[Statement]:
"""
The statements standing between here and the first of *stop*. A statement the parser could
not read at all is the token it stopped on, kept as itself; one it gave up on partway is the
whole span it had reached, kept the same way. Handing back nothing for that span is what
would delete it: the tokens are already read, so the text they spell appears in no node, and
a file that lost a statement prints as a shorter program nobody wrote.
A statement list is where the `for` head's suppression of the `in` operator ends. The head
reaches an expression and only an expression, and the one way a statement stands inside one
is a function body, which is a fresh context: `for (function () { return 'k' in b; }; ; )`
is a program, and `for (q => 'k' in b; ; )` is not, because a concise arrow body is an
expression and inherits the suppression rather than ending it.
"""
body: list[Statement] = []
with self._with_no_in(False):
while not self._at(*stop):
mark = self._current.offset
comments = list(self._pending_comments)
self._pending_comments.clear()
try:
stmt = self._parse_statement()
except Exception:
stmt = None
if self._current.offset != mark:
stmt = self._unread_since(mark, 'a statement that could not be read')
if stmt is not None:
stmt.leading_comments.extend(comments)
body.append(stmt)
elif self._current.offset == mark:
tok = self._advance()
error = JsErrorNode(offset=tok.offset, text=tok.value)
error.leading_comments.extend(comments)
body.append(error)
return body
def _parse_program(self) -> JsScript:
offset = self._current.offset
body = self._parse_statement_list(JsTokenKind.EOF)
return JsScript(body=body, offset=offset, recovered=self._recovered)
def _parse_statement(self) -> Statement | None:
offset = self._current.offset
kind = self._current.kind
if kind == JsTokenKind.LBRACE:
return self._parse_block_statement()
if kind == JsTokenKind.SEMICOLON:
self._advance()
return JsEmptyStatement(offset=offset)
if self._at_variable_declaration():
return self._parse_variable_declaration()
if kind == JsTokenKind.IF:
return self._parse_if_statement()
if kind == JsTokenKind.WHILE:
return self._parse_while_statement()
if kind == JsTokenKind.DO:
return self._parse_do_while_statement()
if kind == JsTokenKind.FOR:
return self._parse_for_statement()
if kind == JsTokenKind.SWITCH:
return self._parse_switch_statement()
if kind == JsTokenKind.TRY:
return self._parse_try_statement()
if kind == JsTokenKind.WITH:
return self._parse_with_statement()
if kind == JsTokenKind.RETURN:
return self._parse_return_statement()
if kind == JsTokenKind.THROW:
return self._parse_throw_statement()
if kind == JsTokenKind.BREAK:
return self._parse_break_statement()
if kind == JsTokenKind.CONTINUE:
return self._parse_continue_statement()
if kind == JsTokenKind.FUNCTION:
return self._parse_function_declaration()
if kind == JsTokenKind.AT:
decorators = self._parse_decorators()
if self._at(JsTokenKind.EXPORT):
return self._parse_export_declaration(decorators)
if self._at(JsTokenKind.CLASS):
return self._parse_class_declaration(decorators)
return JsErrorNode(
text=self._source[offset:self._current.offset].rstrip(),
message='decorators must precede a class',
offset=offset,
)
if kind == JsTokenKind.CLASS:
return self._parse_class_declaration()
if kind == JsTokenKind.DEBUGGER:
self._advance()
self._eat_semicolon()
return JsDebuggerStatement(offset=offset)
if kind == JsTokenKind.IMPORT and self._peek_next().kind not in (
JsTokenKind.LPAREN, JsTokenKind.DOT,
):
return self._parse_import_declaration()
if kind == JsTokenKind.EXPORT:
return self._parse_export_declaration()
if self._at_async_function():
self._advance()
return self._parse_function_declaration(is_async=True)
expr = self._parse_expression()
if (
isinstance(expr, JsIdentifier)
and self._eat(JsTokenKind.COLON)
):
body = self._parse_statement()
return JsLabeledStatement(label=expr, body=body, offset=offset)
self._eat_semicolon()
if isinstance(expr, JsErrorNode):
return expr
return JsExpressionStatement(expression=expr, offset=offset)
def _parse_block_statement(self) -> JsBlockStatement:
offset = self._current.offset
self._expect(JsTokenKind.LBRACE)
body = self._parse_statement_list(JsTokenKind.RBRACE, JsTokenKind.EOF)
self._expect(JsTokenKind.RBRACE)
return JsBlockStatement(body=body, offset=offset)
def _parse_variable_declaration(self) -> JsVariableDeclaration:
offset = self._current.offset
kind_tok = self._advance()
kind = _VAR_KIND_MAP[kind_tok.kind]
declarations: list[JsVariableDeclarator] = []
declarations.append(self._parse_variable_declarator())
while self._eat(JsTokenKind.COMMA):
declarations.append(self._parse_variable_declarator())
self._eat_semicolon()
return JsVariableDeclaration(declarations=declarations, kind=kind, offset=offset)
def _parse_variable_declarator(self) -> JsVariableDeclarator:
offset = self._current.offset
id_node = self._parse_binding_pattern()
init = None
if self._eat(JsTokenKind.EQUALS):
init = self._parse_assignment_expression()
return JsVariableDeclarator(id=id_node, init=init, offset=offset)
def _parse_binding_pattern(self) -> Expression:
if self._at(JsTokenKind.LBRACKET):
return self._parse_array_pattern()
if self._at(JsTokenKind.LBRACE):
return self._parse_object_pattern()
return self._parse_binding_identifier()
def _parse_binding_identifier(self) -> Expression:
offset = self._current.offset
if self._at_binding_identifier():
tok = self._advance()
else:
tok = self._expect(JsTokenKind.IDENTIFIER)
return self._name_or_error(tok.value, offset, may_be_reserved=False)
def _name_or_error(self, text: str, offset: int, *, may_be_reserved: bool) -> Expression:
"""
The name a token spells, where it spells one. A file that ends in the middle of a
declaration leaves the position a name was expected in holding nothing, and a name spelled
by nothing has no text at all: printing it closes the source up over the gap, so `var` at
the end of a file would come back as `var ;`. What is handed back instead is the span
itself, which prints as what was written and states that the parser did not read it.
Two further texts spell no name. One holds an escape naming no character a name may hold,
which is a fact about the text alone and is refused wherever it stands. The other spells a
reserved word, which the language refuses only where the name could also be a variable —
`o.\\u0069f` reads a member and `var \\u0069f` declares nothing — and *may_be_reserved* is
which of the two positions this is. Both come back as the span, so the file prints as it
was written and no pass reads a name out of text no engine read one from.
"""
if not text:
return JsErrorNode(text=text, message='expected a name', offset=offset)
name = identifier_string_value(text)
if name is None:
return JsErrorNode(text=text, message='not a name', offset=offset)
if not may_be_reserved and name in RESERVED_WORD_NAMES:
return JsErrorNode(text=text, message='reserved word', offset=offset)
return JsIdentifier(name=name, raw=text if name != text else '', offset=offset)
def _identifier(self, tok: JsToken) -> JsIdentifier:
"""
The name a token spells, at a position whose slot holds a name and nothing else. A label
and the name a function or class declaration gives itself are these, and a text spelling
no name is kept there as it was written: what the model has no shape for, the parser has
no way to state.
"""
name = identifier_string_value(tok.value) or tok.value
return JsIdentifier(
name=name,
raw=tok.value if name != tok.value else '',
offset=tok.offset,
)
def _private_identifier(self, tok: JsToken, offset: int) -> Expression:
"""
The private name a token spells. The `#` opens the name and is no part of it, so what
follows it is an IdentifierName like any other and `this.#\\u0061` reads what `#a` declares.
"""
text = tok.value[1:]
name = identifier_string_value(text)
if name is None:
return JsErrorNode(text=tok.value, message='not a name', offset=offset)
return JsPrivateIdentifier(
name=name,
raw=text if name != text else '',
offset=offset,
)
def _parse_array_pattern(self) -> JsArrayPattern:
offset = self._current.offset
self._expect(JsTokenKind.LBRACKET)
elements: list[Expression | None] = []
while not self._at(JsTokenKind.RBRACKET, JsTokenKind.EOF):
if self._at(JsTokenKind.COMMA):
elements.append(None)
self._advance()
continue
if self._at(JsTokenKind.ELLIPSIS):
elements.append(self._parse_rest_element())
break
elem = self._parse_binding_pattern()
if self._eat(JsTokenKind.EQUALS):
right = self._parse_assignment_expression()
elem = JsAssignmentPattern(left=elem, right=right, offset=elem.offset)
elements.append(elem)
if not self._at(JsTokenKind.RBRACKET):
self._expect(JsTokenKind.COMMA)
self._expect(JsTokenKind.RBRACKET)
return JsArrayPattern(elements=elements, offset=offset)
def _parse_object_pattern(self) -> JsObjectPattern:
offset = self._current.offset
self._expect(JsTokenKind.LBRACE)
properties: list[JsProperty | JsRestElement] = []
while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
if self._at(JsTokenKind.ELLIPSIS):
properties.append(self._parse_rest_element())
break
prop = self._parse_object_pattern_property()
properties.append(prop)
if not self._at(JsTokenKind.RBRACE):
self._expect(JsTokenKind.COMMA)
self._expect(JsTokenKind.RBRACE)
return JsObjectPattern(properties=properties, offset=offset)
def _parse_object_pattern_property(self) -> JsProperty:
offset = self._current.offset
if self._at(JsTokenKind.LBRACKET):
self._advance()
key = self._parse_assignment_expression()
self._expect(JsTokenKind.RBRACKET)
self._expect(JsTokenKind.COLON)
value = self._parse_binding_pattern()
if self._eat(JsTokenKind.EQUALS):
right = self._parse_assignment_expression()
value = JsAssignmentPattern(left=value, right=right, offset=value.offset)
return JsProperty(
key=key, value=value, computed=True, shorthand=False, offset=offset)
key = self._parse_property_name()
if self._eat(JsTokenKind.COLON):
value = self._parse_binding_pattern()
if self._eat(JsTokenKind.EQUALS):
right = self._parse_assignment_expression()
value = JsAssignmentPattern(left=value, right=right, offset=value.offset)
return JsProperty(
key=key, value=value, computed=False, shorthand=False, offset=offset)
value = key
if self._eat(JsTokenKind.EQUALS):
right = self._parse_assignment_expression()
value = JsAssignmentPattern(left=key, right=right, offset=key.offset)
return JsProperty(key=key, value=value, computed=False, shorthand=True, offset=offset)
def _parse_rest_element(self) -> JsRestElement:
offset = self._current.offset
self._expect(JsTokenKind.ELLIPSIS)
argument = self._parse_binding_pattern()
return JsRestElement(argument=argument, offset=offset)
def _parse_if_statement(self) -> JsIfStatement:
offset = self._current.offset
self._expect(JsTokenKind.IF)
self._expect(JsTokenKind.LPAREN)
test = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
consequent = self._parse_statement()
alternate = None
if self._eat(JsTokenKind.ELSE):
alternate = self._parse_statement()
return JsIfStatement(
test=test, consequent=consequent, alternate=alternate, offset=offset)
def _parse_while_statement(self) -> JsWhileStatement:
offset = self._current.offset
self._expect(JsTokenKind.WHILE)
self._expect(JsTokenKind.LPAREN)
test = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
body = self._parse_statement()
return JsWhileStatement(test=test, body=body, offset=offset)
def _parse_do_while_statement(self) -> JsDoWhileStatement:
offset = self._current.offset
self._expect(JsTokenKind.DO)
body = self._parse_statement()
self._expect(JsTokenKind.WHILE)
self._expect(JsTokenKind.LPAREN)
test = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
self._eat(JsTokenKind.SEMICOLON)
return JsDoWhileStatement(test=test, body=body, offset=offset)
def _parse_for_statement(self) -> Statement:
offset = self._current.offset
self._expect(JsTokenKind.FOR)
is_await = False
if self._eat(JsTokenKind.AWAIT):
is_await = True
self._expect(JsTokenKind.LPAREN)
if self._at(JsTokenKind.SEMICOLON):
self._advance()
return self._parse_for_rest(None, offset)
if self._at_variable_declaration():
decl_offset = self._current.offset
kind_tok = self._advance()
kind = _VAR_KIND_MAP[kind_tok.kind]
with self._with_no_in(True):
declarator = self._parse_variable_declarator()
decl = JsVariableDeclaration(
declarations=[declarator], kind=kind, offset=decl_offset)
result = self._parse_for_in_or_of(decl, is_await, offset)
if result is not None:
return result
while self._eat(JsTokenKind.COMMA):
with self._with_no_in(True):
decl.declarations.append(self._parse_variable_declarator())
self._expect(JsTokenKind.SEMICOLON)
return self._parse_for_rest(decl, offset)
with self._with_no_in(True):
init_expr = self._parse_expression()
result = self._parse_for_in_or_of(init_expr, is_await, offset)
if result is not None:
return result
self._expect(JsTokenKind.SEMICOLON)
return self._parse_for_rest(init_expr, offset)
def _parse_for_in_or_of(
self,
left: Expression | Statement,
is_await: bool,
offset: int,
) -> JsForInStatement | JsForOfStatement | None:
if self._eat(JsTokenKind.IN):
right = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
body = self._parse_statement()
return JsForInStatement(left=left, right=right, body=body, offset=offset)
if self._at(JsTokenKind.OF):
self._advance()
right = self._parse_assignment_expression()
self._expect(JsTokenKind.RPAREN)
body = self._parse_statement()
return JsForOfStatement(
left=left, right=right, body=body, is_await=is_await, offset=offset)
return None
def _parse_for_rest(
self,
init: Expression | Statement | None,
offset: int,
) -> JsForStatement:
test = None
if not self._at(JsTokenKind.SEMICOLON):
test = self._parse_expression()
self._expect(JsTokenKind.SEMICOLON)
update = None
if not self._at(JsTokenKind.RPAREN):
update = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
body = self._parse_statement()
return JsForStatement(
init=init, test=test, update=update, body=body, offset=offset)
def _parse_switch_statement(self) -> JsSwitchStatement:
offset = self._current.offset
self._expect(JsTokenKind.SWITCH)
self._expect(JsTokenKind.LPAREN)
discriminant = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
self._expect(JsTokenKind.LBRACE)
cases: list[JsSwitchCase] = []
while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
cases.append(self._parse_switch_case())
self._expect(JsTokenKind.RBRACE)
return JsSwitchStatement(
discriminant=discriminant, cases=cases, offset=offset)
def _parse_switch_case(self) -> JsSwitchCase:
offset = self._current.offset
test = None
if self._eat(JsTokenKind.CASE):
test = self._parse_expression()
self._expect(JsTokenKind.COLON)
elif self._eat(JsTokenKind.DEFAULT):
self._expect(JsTokenKind.COLON)
else:
self._recovered = True
self._advance()
body: list[Statement] = []
while not self._at(
JsTokenKind.CASE, JsTokenKind.DEFAULT, JsTokenKind.RBRACE, JsTokenKind.EOF,
):
stmt = self._parse_statement()
if stmt is not None:
body.append(stmt)
return JsSwitchCase(test=test, body=body, offset=offset)
def _parse_try_statement(self) -> JsTryStatement:
offset = self._current.offset
self._expect(JsTokenKind.TRY)
block = self._parse_block_statement()
handler = None
finalizer = None
if self._eat(JsTokenKind.CATCH):
handler = self._parse_catch_clause()
if self._eat(JsTokenKind.FINALLY):
finalizer = self._parse_block_statement()
if handler is None and finalizer is None:
self._recovered = True
return JsTryStatement(
block=block, handler=handler, finalizer=finalizer, offset=offset)
def _parse_catch_clause(self) -> JsCatchClause:
offset = self._current.offset
param = None
if self._eat(JsTokenKind.LPAREN):
param = self._parse_binding_pattern()
self._expect(JsTokenKind.RPAREN)
body = self._parse_block_statement()
return JsCatchClause(param=param, body=body, offset=offset)
def _parse_with_statement(self) -> JsWithStatement:
offset = self._current.offset
self._expect(JsTokenKind.WITH)
self._expect(JsTokenKind.LPAREN)
obj = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
body = self._parse_statement()
return JsWithStatement(object=obj, body=body, offset=offset)
def _parse_return_statement(self) -> JsReturnStatement:
offset = self._current.offset
self._expect(JsTokenKind.RETURN)
argument = None
if not self._preceded_by_newline and not self._at(
JsTokenKind.SEMICOLON, JsTokenKind.RBRACE, JsTokenKind.EOF,
):
argument = self._parse_expression()
self._eat_semicolon()
return JsReturnStatement(argument=argument, offset=offset)
def _parse_throw_statement(self) -> JsThrowStatement:
offset = self._current.offset
self._expect(JsTokenKind.THROW)
argument = None
if not self._preceded_by_newline:
argument = self._parse_expression()
self._eat_semicolon()
return JsThrowStatement(argument=argument, offset=offset)
def _parse_break_statement(self) -> JsBreakStatement:
offset = self._current.offset
self._expect(JsTokenKind.BREAK)
label = None
if not self._preceded_by_newline and self._at_binding_identifier():
tok = self._advance()
label = self._identifier(tok)
self._eat_semicolon()
return JsBreakStatement(label=label, offset=offset)
def _parse_continue_statement(self) -> JsContinueStatement:
offset = self._current.offset
self._expect(JsTokenKind.CONTINUE)
label = None
if not self._preceded_by_newline and self._at_binding_identifier():
tok = self._advance()
label = self._identifier(tok)
self._eat_semicolon()
return JsContinueStatement(label=label, offset=offset)
def _parse_function_impl(
self,
*,
as_expression: bool,
is_async: bool = False,
) -> JsFunctionDeclaration | JsFunctionExpression:
offset = self._current.offset
self._expect(JsTokenKind.FUNCTION)
generator = bool(self._eat(JsTokenKind.STAR))
id_node = None
if self._at_function_name():
tok = self._advance()
id_node = self._identifier(tok)
with self._function_body_context(is_async, generator):
params = self._parse_formal_parameters()
body = self._parse_block_statement()
if as_expression:
return JsFunctionExpression(
id=id_node, params=params, body=body,
generator=generator, is_async=is_async, offset=offset)
return JsFunctionDeclaration(
id=id_node, params=params, body=body,
generator=generator, is_async=is_async, offset=offset)
def _parse_function_declaration(
self,
is_async: bool = False,
) -> JsFunctionDeclaration:
return self._parse_function_impl(as_expression=False, is_async=is_async)
def _parse_formal_parameters(self) -> list[Expression]:
self._expect(JsTokenKind.LPAREN)
params: list[Expression] = []
while not self._at(JsTokenKind.RPAREN, JsTokenKind.EOF):
if self._at(JsTokenKind.ELLIPSIS):
params.append(self._parse_rest_element())
break
param = self._parse_binding_pattern()
if self._eat(JsTokenKind.EQUALS):
default = self._parse_assignment_expression()
param = JsAssignmentPattern(
left=param, right=default, offset=param.offset)
params.append(param)
if not self._at(JsTokenKind.RPAREN):
self._expect(JsTokenKind.COMMA)
self._expect(JsTokenKind.RPAREN)
return params
def _parse_decorators(self) -> list[JsDecorator]:
decorators: list[JsDecorator] = []
while self._at(JsTokenKind.AT):
decorators.append(self._parse_decorator())
return decorators
def _parse_decorator(self) -> JsDecorator:
offset = self._current.offset
self._expect(JsTokenKind.AT)
if self._at(JsTokenKind.LPAREN):
self._advance()
inner = self._parse_expression()
self._expect(JsTokenKind.RPAREN)
return JsDecorator(expression=inner, offset=offset)
if not self._at_binding_identifier():
return JsDecorator(
expression=JsErrorNode(
text=self._current.value, message='unexpected token', offset=offset),
offset=offset,
)
tok = self._advance()
expr: Expression = self._name_or_error(tok.value, tok.offset, may_be_reserved=False)
while self._eat(JsTokenKind.DOT):
prop = self._advance()
expr = JsMemberExpression(
object=expr,
property=self._name_or_error(prop.value, prop.offset, may_be_reserved=True),
computed=False,
offset=expr.offset,
)
if self._at(JsTokenKind.LPAREN):
expr = self._parse_call_arguments(expr, optional=False)
return JsDecorator(expression=expr, offset=offset)
def _parse_class_impl(
self,
*,
as_expression: bool,
decorators: list[JsDecorator] | None = None,
) -> JsClassDeclaration | JsClassExpression:
offset = self._current.offset
self._expect(JsTokenKind.CLASS)
id_node = None
if self._at_binding_identifier():
tok = self._advance()
id_node = self._identifier(tok)
super_class = None
if self._eat(JsTokenKind.EXTENDS):
super_class = self._parse_assignment_expression()
body = self._parse_class_body()
if as_expression:
return JsClassExpression(
id=id_node,
super_class=super_class,
body=body,
decorators=decorators or [],
offset=offset,
)
return JsClassDeclaration(
id=id_node,
super_class=super_class,
body=body,
decorators=decorators or [],
offset=offset,
)
def _parse_class_declaration(
self, decorators: list[JsDecorator] | None = None,
) -> JsClassDeclaration:
return self._parse_class_impl(as_expression=False, decorators=decorators)
def _parse_class_body(self) -> JsClassBody:
offset = self._current.offset
self._expect(JsTokenKind.LBRACE)
members: list[JsMethodDefinition | JsPropertyDefinition | JsStaticBlock] = []
while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
if self._eat(JsTokenKind.SEMICOLON):
continue
decorators = self._parse_decorators()
member = self._parse_class_member()
if decorators and isinstance(member, (JsMethodDefinition, JsPropertyDefinition)):
member.decorators = decorators
member._adopt(*decorators)
members.append(member)
self._expect(JsTokenKind.RBRACE)
return JsClassBody(body=members, offset=offset)
def _parse_static_block(self, offset: int) -> JsStaticBlock:
block = self._parse_block_statement()
return JsStaticBlock(body=block.body, offset=offset)
def _parse_class_member(self) -> JsMethodDefinition | JsPropertyDefinition | JsStaticBlock:
offset = self._current.offset
is_static = False
if self._at(JsTokenKind.IDENTIFIER) and self._current.value == 'static':
saved_pos = self._current
self._advance()
if self._at(JsTokenKind.LBRACE):
return self._parse_static_block(offset)
if self._at(JsTokenKind.LPAREN):
key = JsIdentifier(name='static', offset=saved_pos.offset)
return self._finish_class_member(key, False, False, offset)
if self._at_class_field_terminator():
key = JsIdentifier(name='static', offset=saved_pos.offset)
return self._finish_class_field(key, False, False, offset)
is_static = True
kind = JsMethodKind.METHOD
is_generator = bool(self._eat(JsTokenKind.STAR))
is_async = False
if (
not is_generator
and self._at(JsTokenKind.IDENTIFIER)
and self._current.value in ('get', 'set')
):
saved = self._current
self._advance()
if self._at(JsTokenKind.LPAREN):
key = JsIdentifier(name=saved.value, offset=saved.offset)
return self._finish_class_member(key, is_static, False, offset)
if self._at_class_field_terminator():
key = JsIdentifier(name=saved.value, offset=saved.offset)
return self._finish_class_field(key, is_static, False, offset)
kind = JsMethodKind.GET if saved.value == 'get' else JsMethodKind.SET
elif not is_generator and self._at(JsTokenKind.ASYNC):
saved = self._current
self._advance()
if self._at(JsTokenKind.LPAREN):
key = JsIdentifier(name='async', offset=saved.offset)
return self._finish_class_member(key, is_static, False, offset)
if self._preceded_by_newline or self._at_class_field_terminator():
key = JsIdentifier(name='async', offset=saved.offset)
return self._finish_class_field(key, is_static, False, offset)
is_async = True
if self._eat(JsTokenKind.STAR):
is_generator = True
key, computed = self._parse_property_key()
if kind == JsMethodKind.METHOD and not is_generator and not self._at(JsTokenKind.LPAREN):
return self._finish_class_field(key, is_static, computed, offset)
return self._finish_class_member(key, is_static, is_generator, offset, kind, computed, is_async=is_async)
def _at_class_field_terminator(self) -> bool:
"""
Whether the current token completes a class element as a field named by the identifier just consumed:
an initializer (`=`), an explicit terminator (`;`), or the end of the class body (`}` / end of input).
A modifier prefix (`static`/`get`/`set`/`async`) followed by one of these is an ordinary field whose
name happens to be that word, not a modifier.
"""
return self._at(
JsTokenKind.EQUALS,
JsTokenKind.SEMICOLON,
JsTokenKind.RBRACE,
JsTokenKind.EOF,
)
def _finish_class_field(
self,
key: Expression,
is_static: bool,
computed: bool,
offset: int,
) -> JsPropertyDefinition:
value = None
if self._eat(JsTokenKind.EQUALS):
value = self._parse_assignment_expression()
self._eat_semicolon()
return JsPropertyDefinition(
key=key,
value=value,
computed=computed,
is_static=is_static,
offset=offset,
)
def _finish_class_member(
self,
key: Expression,
is_static: bool,
is_generator: bool,
offset: int,
kind: JsMethodKind = JsMethodKind.METHOD,
computed: bool = False,
is_async: bool = False,
) -> JsMethodDefinition:
func_offset = self._current.offset
with self._function_body_context(is_async, is_generator):
params = self._parse_formal_parameters()
body = self._parse_block_statement()
value = JsFunctionExpression(
params=params,
body=body,
generator=is_generator,
is_async=is_async,
offset=func_offset,
)
if isinstance(key, JsIdentifier) and key.name == 'constructor' and kind == JsMethodKind.METHOD:
kind = JsMethodKind.CONSTRUCTOR
return JsMethodDefinition(
key=key,
value=value,
kind=kind,
computed=computed,
is_static=is_static,
offset=offset,
)
def _parse_import_expression(self, offset: int) -> Expression:
self._expect(JsTokenKind.IMPORT)
if self._eat(JsTokenKind.DOT):
prop = self._advance()
return JsMetaProperty(meta='import', property=prop.value, offset=offset)
if self._at(JsTokenKind.LPAREN):
self._advance()
source = self._parse_assignment_expression()
options = None
if self._eat(JsTokenKind.COMMA) and not self._at(JsTokenKind.RPAREN):
options = self._parse_assignment_expression()
self._eat(JsTokenKind.COMMA)
self._expect(JsTokenKind.RPAREN)
return JsImportExpression(source=source, options=options, offset=offset)
return JsErrorNode(text='import', message='unexpected token', offset=offset)
def _parse_import_attributes(self) -> tuple[str, list[JsImportAttribute]]:
if self._preceded_by_newline:
return '', []
if self._at(JsTokenKind.WITH):
keyword = 'with'
elif self._at(JsTokenKind.IDENTIFIER) and self._current.value == 'assert':
keyword = 'assert'
else:
return '', []
self._advance()
attributes: list[JsImportAttribute] = []
self._expect(JsTokenKind.LBRACE)
while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
key = self._parse_property_name()
self._expect(JsTokenKind.COLON)
value = self._parse_string_literal()
attributes.append(JsImportAttribute(key=key, value=value, offset=key.offset))
if not self._eat(JsTokenKind.COMMA):
break
self._expect(JsTokenKind.RBRACE)
return keyword, attributes
def _module_specifier(self) -> JsStringLiteral | None:
"""
The literal naming the module a declaration reads from, or `None` where none stands there.
It is the one part of these declarations the grammar gives no default for, so a source that
ends before writing it has not written the declaration at all; answering with a literal
spelled by nothing states a module whose name is the empty string, which is a module the
file could have named and did not.
"""
if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE):
return self._parse_string_literal()
return None
def _unread_since(self, offset: int, message: str) -> JsErrorNode:
"""
The source from *offset* up to where reading stands, handed back as itself. A declaration
the parser could not complete is kept whole rather than in the parts it did manage to read:
what prints is then what was written, and reading that print again finds the same thing,
where a half-built declaration prints the halves it has and reads back as something else.
"""
return JsErrorNode(
text=self._source[offset:self._current.offset].rstrip(),
message=message,
offset=offset,
)
def _parse_import_declaration(self) -> JsImportDeclaration | JsErrorNode:
offset = self._current.offset
self._expect(JsTokenKind.IMPORT)
if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE):
source = self._parse_string_literal()
keyword, attributes = self._parse_import_attributes()
self._eat_semicolon()
return JsImportDeclaration(
source=source, attributes=attributes, attributes_keyword=keyword, offset=offset)
specifiers: list[
JsImportSpecifier | JsImportDefaultSpecifier | JsImportNamespaceSpecifier
] = []
if self._at_binding_identifier():
tok = self._advance()
specifiers.append(JsImportDefaultSpecifier(
local=self._name_or_error(tok.value, tok.offset, may_be_reserved=False),
offset=tok.offset,
))
if self._eat(JsTokenKind.COMMA):
if self._at(JsTokenKind.STAR):
specifiers.append(self._parse_namespace_import())
elif self._at(JsTokenKind.LBRACE):
specifiers.extend(self._parse_named_imports())
else:
self._recovered = True
elif self._at(JsTokenKind.STAR):
specifiers.append(self._parse_namespace_import())
elif self._at(JsTokenKind.LBRACE):
specifiers.extend(self._parse_named_imports())
self._expect_contextual('from')
source = self._module_specifier()
if source is None:
return self._unread_since(offset, 'a module declaration with no specifier')
keyword, attributes = self._parse_import_attributes()
self._eat_semicolon()
return JsImportDeclaration(
specifiers=specifiers,
source=source,
attributes=attributes,
attributes_keyword=keyword,
offset=offset,
)
def _parse_module_export_name(self) -> Expression:
"""
The name an import or export list gives a binding on the far side of the module boundary. It
is an IdentifierName rather than a name this file could refer to, so every word the language
has stands here and none of them is a repair: `import { default as d } from 'm.js'` and
`export * as default from 'm.js'` are both ordinary declarations.
The shorthand `import { a }` writes one name in two positions at once, and this reads it as
the boundary one, which is the wider of the two: what the shorthand also does is bind `a`
locally, and no module may bind a word its strict code reserves. That rule is not applied
anywhere yet, so nothing is lost by reading the shorthand here rather than gained by reading
it as a binding.
A word and a string literal are the whole of what the grammar writes here, and the two
are different productions rather than two spellings of one. A string is read as the
literal it is, because what it names is the text it denotes and not the way that text
was written:
export { a as 'b c' } from 'm';
export { a as 'b\\u0020c' } from 'm';
reach the one name `b c`, which no word spells, and asking a name reader for either
would hand it a text that opens with a quote. Anything else is a token the parser steps
over in order to answer with a name at all, and reading `,` as the name a module exports
under is a repair however well it prints back.
"""
if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE):
return self._parse_string_literal()
if not self._at_identifier_name():
self._recovered = True
tok = self._advance()
return self._name_or_error(tok.value, tok.offset, may_be_reserved=True)
def _parse_namespace_import(self) -> JsImportNamespaceSpecifier:
offset = self._current.offset
self._expect(JsTokenKind.STAR)
self._expect_contextual('as')
return JsImportNamespaceSpecifier(
local=self._parse_binding_identifier(),
offset=offset,
)
def _parse_named_imports(self) -> list[JsImportSpecifier]:
self._expect(JsTokenKind.LBRACE)
specs: list[JsImportSpecifier] = []
while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
spec_offset = self._current.offset
imported = self._parse_module_export_name()
local = imported
if self._at(JsTokenKind.AS):
self._advance()
local = self._parse_binding_identifier()
specs.append(JsImportSpecifier(
imported=imported, local=local, offset=spec_offset))
if not self._at(JsTokenKind.RBRACE):
self._expect(JsTokenKind.COMMA)
self._expect(JsTokenKind.RBRACE)
return specs
def _parse_export_declaration(
self, decorators: list[JsDecorator] | None = None,
) -> Statement:
offset = self._current.offset
self._expect(JsTokenKind.EXPORT)
class_decorators = list(decorators or []) + self._parse_decorators()
if self._eat(JsTokenKind.DEFAULT):
class_decorators += self._parse_decorators()
if self._at(JsTokenKind.FUNCTION):
decl = self._parse_function_declaration()
return JsExportDefaultDeclaration(declaration=decl, offset=offset)
if self._at(JsTokenKind.CLASS):
decl = self._parse_class_declaration(class_decorators)
return JsExportDefaultDeclaration(declaration=decl, offset=offset)
if self._at_async_function():
self._advance()
decl = self._parse_function_declaration(is_async=True)
return JsExportDefaultDeclaration(declaration=decl, offset=offset)
expr = self._parse_assignment_expression()
self._eat_semicolon()
return JsExportDefaultDeclaration(declaration=expr, offset=offset)
if self._at(JsTokenKind.STAR):
self._advance()
exported = None
if self._at(JsTokenKind.AS):
self._advance()
exported = self._parse_module_export_name()
self._expect_contextual('from')
source = self._module_specifier()
if source is None:
return self._unread_since(offset, 'a module declaration with no specifier')
self._eat_semicolon()
return JsExportAllDeclaration(
source=source, exported=exported, offset=offset)
if self._at(JsTokenKind.LBRACE):
return self._parse_export_named(offset)
if self._at(JsTokenKind.VAR, JsTokenKind.LET, JsTokenKind.CONST):
decl = self._parse_variable_declaration()
return JsExportNamedDeclaration(declaration=decl, offset=offset)
if self._at(JsTokenKind.FUNCTION):
decl = self._parse_function_declaration()
return JsExportNamedDeclaration(declaration=decl, offset=offset)
if self._at(JsTokenKind.CLASS):
decl = self._parse_class_declaration(class_decorators)
return JsExportNamedDeclaration(declaration=decl, offset=offset)
if self._at_async_function():
self._advance()
decl = self._parse_function_declaration(is_async=True)
return JsExportNamedDeclaration(declaration=decl, offset=offset)
self._recovered = True
self._advance()
return JsExportNamedDeclaration(offset=offset)
def _parse_export_named(self, offset: int) -> JsExportNamedDeclaration | JsErrorNode:
self._expect(JsTokenKind.LBRACE)
specifiers: list[JsExportSpecifier] = []
while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
spec_offset = self._current.offset
local = self._parse_module_export_name()
exported = local
if self._at(JsTokenKind.AS):
self._advance()
exported = self._parse_module_export_name()
specifiers.append(JsExportSpecifier(
local=local, exported=exported, offset=spec_offset))
if not self._at(JsTokenKind.RBRACE):
self._expect(JsTokenKind.COMMA)
self._expect(JsTokenKind.RBRACE)
source = None
if self._at(JsTokenKind.FROM):
self._advance()
source = self._module_specifier()
if source is None:
return self._unread_since(offset, 'a module declaration with no specifier')
self._eat_semicolon()
return JsExportNamedDeclaration(
specifiers=specifiers, source=source, offset=offset)
def _at_async_function(self) -> bool:
"""
Whether the parser is positioned at `async function` with no line terminator between the two — the
one form in which a leading `async` opens a declaration rather than an ordinary expression. Every
other `async` (a call, member access, arrow, or bare reference) is left to the expression grammar,
which reaches it through `_parse_async_expression` and applies the full call/member and operator
parsing.
"""
return (
self._at(JsTokenKind.ASYNC)
and self._peek_next().kind == JsTokenKind.FUNCTION
and not self._ahead_newline
)
def _expect_contextual(self, keyword: str):
"""
The word that must stand here, which is a word and not a token kind: `as` and `from` are
names everywhere else, so the lexer hands them over as themselves in one form and as an
identifier in the other. Where neither is what stands here the parser steps over whatever
does, which drops it from the file, and that is a repair like any other.
"""
if self._at(JsTokenKind.FROM) and keyword == 'from':
self._advance()
return
if self._at(JsTokenKind.AS) and keyword == 'as':
self._advance()
return
if self._at(JsTokenKind.IDENTIFIER) and self._current.value == keyword:
self._advance()
return
self._recovered = True
self._advance()
def _parse_expression(self) -> Expression:
expr = self._parse_assignment_expression()
if self._at(JsTokenKind.COMMA):
exprs = [expr]
while self._eat(JsTokenKind.COMMA):
exprs.append(self._parse_assignment_expression())
return JsSequenceExpression(expressions=exprs, offset=expr.offset)
return expr
def _parse_assignment_expression(self) -> Expression:
"""
An AssignmentExpression, which is the only production a YieldExpression is one of. Reading
the `yield` here rather than among the primary expressions is what stops an operator from
attaching to it: a `yield` that the line terminator restriction left without an argument
ends the expression, and the slash that opens the next statement is not its divisor.
"""
if self._at(JsTokenKind.YIELD) and self._in_generator:
return self._parse_yield_expression()
left = self._parse_conditional_expression()
if self._current.kind.is_assignment:
op = self._advance().value
right = self._parse_assignment_expression()
left = self._to_param(left) if op == '=' else left
return JsAssignmentExpression(
left=left, operator=op, right=right, offset=left.offset)
return left
def _parse_conditional_expression(self) -> Expression:
expr = self._parse_binary_expression()
if self._eat(JsTokenKind.QUESTION):
with self._with_no_in(False):
consequent = self._parse_assignment_expression()
if self._eat(JsTokenKind.COLON):
alternate = self._parse_assignment_expression()
else:
# The colon a conditional requires is not here. Reading the alternate anyway would
# spend `_expect`, which steps over whatever stands here to invent the colon — and
# where that is the semicolon ending the statement, the next statement is read as
# the alternate and the whole tail of the block is pulled into one expression. The
# branch is left unwritten instead, so the boundary stays where it is and the
# statement after the conditional is read as itself.
self._recovered = True
alternate = JsErrorNode(offset=self._current.offset, message='expected :')
return JsConditionalExpression(
test=expr,
consequent=consequent,
alternate=alternate,
offset=expr.offset,
)
return expr
def _parse_binary_expression(self, min_prec: int = 0) -> Expression:
left = self._parse_unary_expression()
while True:
entry = _BINARY_PREC.get(self._current.kind)
if entry is None:
break
prec, logical = entry
if prec < min_prec:
break
if self._no_in and self._at(JsTokenKind.IN):
break
op = self._advance().value
next_prec = prec if prec == _PREC_EXPONENTIATION else prec + 1
right = self._parse_binary_expression(next_prec)
node_type = JsLogicalExpression if logical else JsBinaryExpression
left = node_type(
left=left, operator=op, right=right, offset=left.offset)
return left
def _parse_unary_expression(self) -> Expression:
if self._at(
JsTokenKind.BANG,
JsTokenKind.TILDE,
JsTokenKind.TYPEOF,
JsTokenKind.VOID,
JsTokenKind.DELETE,
):
tok = self._advance()
operand = self._parse_unary_expression()
return JsUnaryExpression(
operator=tok.value, operand=operand, prefix=True, offset=tok.offset)
if self._at(JsTokenKind.PLUS):
tok = self._advance()
operand = self._parse_unary_expression()
return JsUnaryExpression(
operator='+', operand=operand, prefix=True, offset=tok.offset)
if self._at(JsTokenKind.MINUS):
tok = self._advance()
operand = self._parse_unary_expression()
return JsUnaryExpression(
operator='-', operand=operand, prefix=True, offset=tok.offset)
if self._at(JsTokenKind.AWAIT) and self._in_async:
tok = self._advance()
operand = self._parse_unary_expression()
return JsAwaitExpression(argument=operand, offset=tok.offset)
return self._parse_update_expression()
def _parse_update_expression(self) -> Expression:
if self._at(JsTokenKind.INC, JsTokenKind.DEC):
tok = self._advance()
argument = self._parse_call_expression()
return JsUpdateExpression(
operator=tok.value, argument=argument, prefix=True, offset=tok.offset)
expr = self._parse_call_expression()
if not self._preceded_by_newline and self._at(
JsTokenKind.INC, JsTokenKind.DEC,
):
tok = self._advance()
return JsUpdateExpression(
operator=tok.value, argument=expr, prefix=False, offset=expr.offset)
return expr
def _parse_call_expression(self) -> Expression:
"""
A left-hand side expression: what may be called, indexed, or used to tag a template. An
arrow function is none of those. It is an AssignmentExpression and never a
LeftHandSideExpression, so nothing may attach to it, and the tail that would have attached
belongs to whatever follows instead — `f = a => {}` on one line and `[x].forEach(g)` on the
next are two statements, and reading the bracket as an index into the arrow makes them one.
"""
expr = self._parse_new_expression()
if isinstance(expr, JsArrowFunctionExpression):
return expr
while True:
if self._at(JsTokenKind.LPAREN):
expr = self._parse_call_arguments(expr, optional=False)
elif self._eat(JsTokenKind.DOT):
prop = self._member_property()
expr = JsMemberExpression(
object=expr, property=prop, computed=False, optional=False, offset=expr.offset)
elif self._at(JsTokenKind.LBRACKET):
expr = JsMemberExpression(
object=expr,
property=self._parse_computed_member_key(),
computed=True,
optional=False,
offset=expr.offset,
)
elif self._at(JsTokenKind.QUESTION_DOT) or (
self._at(JsTokenKind.QUESTION)
and self._peek_next().kind == JsTokenKind.DOT
):
# A question mark whose next token is a dot is a `?.` the source split with
# whitespace. That split is not a token the language has, and no well-formed program
# reaches it: a conditional whose consequent opens with a dot opens with a number,
# which the lexer reads as one float rather than a dot. Reading the two as the
# optional-chain operator recovers the program the source meant, recorded as a
# repair so the tree still reports that the file did not spell it whole.
if self._eat(JsTokenKind.QUESTION_DOT) is None:
self._recovered = True
self._advance()
self._advance()
if self._at(JsTokenKind.LPAREN):
expr = self._parse_call_arguments(expr, optional=True)
elif self._at(JsTokenKind.LBRACKET):
expr = JsMemberExpression(
object=expr,
property=self._parse_computed_member_key(),
computed=True,
optional=True,
offset=expr.offset,
)
else:
prop = self._member_property()
expr = JsMemberExpression(
object=expr, property=prop, computed=False, optional=True, offset=expr.offset)
elif self._at(
JsTokenKind.TEMPLATE_FULL, JsTokenKind.TEMPLATE_HEAD,
):
quasi = self._parse_template_literal()
expr = JsTaggedTemplateExpression(
tag=expr, quasi=quasi, offset=expr.offset)
else:
break
return expr
def _member_property(self) -> Expression:
"""
The name behind a dot. It is an IdentifierName, which is every word the language has and not
only the ones that may be a variable, so `a.if` and `a.default` are ordinary member reads.
Where the text behind the dot spells no word at all there is no name to build. An identifier
with no name spells nothing — printing it writes the dot and stops, which is not a program —
so what was read is handed back as itself instead.
"""
tok = self._advance()
if tok.kind is JsTokenKind.PRIVATE_IDENTIFIER:
return self._private_identifier(tok, tok.offset)
if tok.kind is JsTokenKind.IDENTIFIER or tok.kind.is_keyword:
return self._name_or_error(tok.value, tok.offset, may_be_reserved=True)
return JsErrorNode(
text=tok.value, message='expected a property name', offset=tok.offset)
def _parse_computed_member_key(self) -> Expression:
"""
The expression between the brackets of a computed member read. Like a call's arguments it is
written `[+In]`, so `for (t["k" in b] = "set"; ; )` is an ordinary head: the bracket ends the
reach of the suppression the head applies to a relational operator standing directly in it.
"""
self._expect(JsTokenKind.LBRACKET)
with self._with_no_in(False):
key = self._parse_expression()
self._expect(JsTokenKind.RBRACKET)
return key
def _parse_argument_list(self) -> list[Expression]:
"""
The arguments between the brackets of a call. Each is written `[+In]`, so `in` is an operator
here however the call was reached: what a `for` head suppresses is a relational operator
standing in the head itself, and a bracket the head encloses is a fresh expression again.
Owning that here rather than at each call site is what keeps `for (new Set("k" in b); ; )`
reading the same way `for (f("k" in b); ; )` does.
"""
args: list[Expression] = []
with self._with_no_in(False):
while not self._at(JsTokenKind.RPAREN, JsTokenKind.EOF):
if self._at(JsTokenKind.ELLIPSIS):
offset = self._current.offset
self._advance()
arg = self._parse_assignment_expression()
args.append(JsSpreadElement(argument=arg, offset=offset))
else:
args.append(self._parse_assignment_expression())
if not self._at(JsTokenKind.RPAREN):
self._expect(JsTokenKind.COMMA)
self._expect(JsTokenKind.RPAREN)
return args
def _parse_call_arguments(
self,
callee: Expression,
optional: bool,
) -> JsCallExpression:
self._expect(JsTokenKind.LPAREN)
args = self._parse_argument_list()
return JsCallExpression(
callee=callee, arguments=args, optional=optional, offset=callee.offset)
def _parse_new_expression(self) -> Expression:
if self._at(JsTokenKind.NEW):
offset = self._current.offset
self._advance()
if self._at(JsTokenKind.DOT):
self._advance()
return JsMemberExpression(
object=JsIdentifier(name='new', offset=offset),
property=self._member_property(),
computed=False,
offset=offset,
)
if self._at(JsTokenKind.ASYNC) and not self._at_async_function():
tok = self._advance()
callee = self._name_or_error(tok.value, tok.offset, may_be_reserved=False)
else:
callee = self._parse_new_expression()
while True:
if self._eat(JsTokenKind.DOT):
prop = self._member_property()
callee = JsMemberExpression(
object=callee, property=prop, computed=False, offset=callee.offset)
elif self._at(JsTokenKind.LBRACKET):
callee = JsMemberExpression(
object=callee,
property=self._parse_computed_member_key(),
computed=True,
offset=callee.offset,
)
else:
break
args: list[Expression] = []
if self._at(JsTokenKind.LPAREN):
self._advance()
args = self._parse_argument_list()
return JsNewExpression(callee=callee, arguments=args, offset=offset)
return self._parse_primary_expression()
def _parse_primary_expression(self) -> Expression:
tok = self._current
offset = tok.offset
if self._at(JsTokenKind.ASYNC):
return self._parse_async_expression()
if self._at_binding_identifier():
self._advance()
if self._at(JsTokenKind.ARROW) and not self._preceded_by_newline:
self._advance()
param = self._name_or_error(tok.value, offset, may_be_reserved=False)
body = self._parse_arrow_body()
return JsArrowFunctionExpression(
params=[param], body=body, offset=offset)
return self._name_or_error(tok.value, offset, may_be_reserved=False)
if self._at(JsTokenKind.PRIVATE_IDENTIFIER):
self._advance()
return self._private_identifier(tok, offset)
if self._at(JsTokenKind.IMPORT):
return self._parse_import_expression(offset)
if self._at(JsTokenKind.INTEGER):
self._advance()
raw = tok.value
value = self._parse_int_text(raw.replace('_', ''))
return JsNumericLiteral(value=value, raw=raw, offset=offset)
if self._at(JsTokenKind.FLOAT):
self._advance()
raw = tok.value
value = float(raw.replace('_', ''))
return JsNumericLiteral(value=value, raw=raw, offset=offset)
if self._at(JsTokenKind.BIGINT):
self._advance()
raw = tok.value
value = self._parse_int_text(raw.replace('_', '').rstrip('n'))
return JsBigIntLiteral(value=value, raw=raw, offset=offset)
if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE):
return self._parse_string_literal()
if self._at(JsTokenKind.SLASH, JsTokenKind.SLASH_ASSIGN):
tok = self._rescan_as_regexp()
if self._at(JsTokenKind.REGEXP):
self._advance()
raw = tok.value
last_slash = raw.rfind('/')
pattern = raw[1:last_slash]
flags = raw[last_slash + 1:]
return JsRegExpLiteral(
pattern=pattern, flags=flags, raw=raw, offset=offset)
if self._at(JsTokenKind.TEMPLATE_FULL, JsTokenKind.TEMPLATE_HEAD):
return self._parse_template_literal()
if self._at(JsTokenKind.TRUE):
self._advance()
return JsBooleanLiteral(value=True, offset=offset)
if self._at(JsTokenKind.FALSE):
self._advance()
return JsBooleanLiteral(value=False, offset=offset)
if self._at(JsTokenKind.NULL):
self._advance()
return JsNullLiteral(offset=offset)
if self._at(JsTokenKind.THIS):
self._advance()
return JsThisExpression(offset=offset)
if self._at(JsTokenKind.SUPER):
self._advance()
return JsIdentifier(name='super', offset=offset)
if self._at(JsTokenKind.LBRACKET):
return self._parse_array_literal()
if self._at(JsTokenKind.LBRACE):
return self._parse_object_literal()
if self._at(JsTokenKind.LPAREN):
return self._parse_paren_or_arrow()
if self._at(JsTokenKind.FUNCTION):
return self._parse_function_expression()
if self._at(JsTokenKind.CLASS):
return self._parse_class_expression()
self._advance()
return JsErrorNode(text=tok.value, message='unexpected token', offset=offset)
def _parse_string_literal(self) -> JsStringLiteral:
tok = self._advance()
raw = tok.value
end = len(raw) - 1 if tok.terminated else len(raw)
return JsStringLiteral(
value=decode_js_string_body(raw[1:end]),
raw=raw,
terminated=tok.terminated,
offset=tok.offset,
)
@staticmethod
def _template_element(tok: JsToken, tail: bool) -> JsTemplateElement:
"""
One run of text of a template literal, taken from the token without the delimiters around
it: one character opens every run, and the one that ends it is a backtick where the run
ends the literal and `${` where a hole follows. The text between them is cooked into what
it denotes, exactly as the body of a string literal is — a template that carries an escape
means what the escape means, and reading it as the characters that spell it is how a `\\t`
became two.
"""
raw = tok.value
end = len(raw) - (1 if tail else 2) if tok.terminated else len(raw)
text = raw[1:end]
return JsTemplateElement(
value=decode_js_template_body(text),
raw=text,
tail=tail,
terminated=tok.terminated,
offset=tok.offset,
)
def _parse_template_literal(self) -> JsTemplateLiteral:
offset = self._current.offset
quasis: list[JsTemplateElement] = []
expressions: list[Expression] = []
if self._at(JsTokenKind.TEMPLATE_FULL):
quasis.append(self._template_element(self._advance(), True))
return JsTemplateLiteral(
quasis=quasis, expressions=expressions, offset=offset)
quasis.append(self._template_element(self._advance(), False))
while True:
with self._with_no_in(False):
expressions.append(self._parse_expression())
if self._at(JsTokenKind.TEMPLATE_TAIL):
quasis.append(self._template_element(self._advance(), True))
break
elif self._at(JsTokenKind.TEMPLATE_MIDDLE):
quasis.append(self._template_element(self._advance(), False))
else:
quasis.append(JsTemplateElement(
value='',
raw='',
tail=True,
terminated=False,
offset=self._current.offset,
))
break
return JsTemplateLiteral(
quasis=quasis, expressions=expressions, offset=offset)
def _parse_array_literal(self) -> JsArrayExpression:
with self._with_no_in(False):
offset = self._current.offset
self._expect(JsTokenKind.LBRACKET)
elements: list[Expression | None] = []
while not self._at(JsTokenKind.RBRACKET, JsTokenKind.EOF):
if self._at(JsTokenKind.COMMA):
elements.append(None)
self._advance()
continue
if self._at(JsTokenKind.ELLIPSIS):
so = self._current.offset
self._advance()
arg = self._parse_assignment_expression()
elements.append(JsSpreadElement(argument=arg, offset=so))
else:
elements.append(self._parse_assignment_expression())
if not self._at(JsTokenKind.RBRACKET):
self._require(JsTokenKind.COMMA)
self._expect(JsTokenKind.RBRACKET)
return JsArrayExpression(elements=elements, offset=offset)
def _parse_object_literal(self) -> JsObjectExpression:
with self._with_no_in(False):
offset = self._current.offset
self._expect(JsTokenKind.LBRACE)
properties: list[JsProperty | JsSpreadElement] = []
while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF):
if self._at(JsTokenKind.ELLIPSIS):
so = self._current.offset
self._advance()
arg = self._parse_assignment_expression()
properties.append(JsSpreadElement(argument=arg, offset=so))
else:
properties.append(self._parse_object_property())
if not self._at(JsTokenKind.RBRACE):
self._require(JsTokenKind.COMMA)
self._expect(JsTokenKind.RBRACE)
return JsObjectExpression(properties=properties, offset=offset)
def _parse_object_property(self) -> JsProperty:
offset = self._current.offset
is_generator = bool(self._eat(JsTokenKind.STAR))
if (
self._at(JsTokenKind.IDENTIFIER)
and self._current.value in ('get', 'set')
and not is_generator
):
kind_val = _PROP_KIND_MAP[self._current.value]
saved = self._current
self._advance()
if self._at(JsTokenKind.LPAREN):
key = JsIdentifier(name=saved.value, offset=saved.offset)
return self._make_method_property(key, JsPropertyKind.INIT, False, offset)
if self._at(
JsTokenKind.COLON,
JsTokenKind.COMMA,
JsTokenKind.RBRACE,
JsTokenKind.EQUALS,
):
key = JsIdentifier(name=saved.value, offset=saved.offset)
return self._finish_property_value(key, False, offset)
key, computed = self._parse_property_key()
return self._make_method_property(key, kind_val, False, offset, computed=computed)
if self._at(JsTokenKind.ASYNC) and not is_generator:
saved = self._current
self._advance()
if self._preceded_by_newline or self._at(
JsTokenKind.COLON,
JsTokenKind.COMMA,
JsTokenKind.RBRACE,
JsTokenKind.EQUALS,
JsTokenKind.LPAREN,
):
if self._at(JsTokenKind.LPAREN):
key = JsIdentifier(name='async', offset=saved.offset)
return self._make_method_property(key, JsPropertyKind.INIT, False, offset)
key = JsIdentifier(name='async', offset=saved.offset)
return self._finish_property_value(key, False, offset)
gen = bool(self._eat(JsTokenKind.STAR))
key, computed = self._parse_property_key()
return self._make_method_property(
key, JsPropertyKind.INIT, gen, offset, computed=computed, is_async=True)
key, computed = self._parse_property_key()
if is_generator or self._at(JsTokenKind.LPAREN):
return self._make_method_property(key, JsPropertyKind.INIT, is_generator, offset, computed=computed)
return self._finish_property_value(key, computed, offset)
def _finish_property_value(
self,
key: Expression,
computed: bool,
offset: int,
) -> JsProperty:
if self._eat(JsTokenKind.COLON):
value = self._parse_assignment_expression()
return JsProperty(
key=key, value=value, computed=computed,
shorthand=False, offset=offset)
if not computed and self._eat(JsTokenKind.EQUALS):
right = self._parse_assignment_expression()
value = JsAssignmentPattern(left=key, right=right, offset=key.offset)
return JsProperty(
key=key, value=value, computed=computed,
shorthand=True, offset=offset)
return JsProperty(
key=key, value=key, computed=computed,
shorthand=True, offset=offset)
def _make_method_property(
self,
key: Expression,
kind: JsPropertyKind,
is_generator: bool,
offset: int,
computed: bool = False,
is_async: bool = False,
) -> JsProperty:
func_offset = self._current.offset
with self._function_body_context(is_async, is_generator):
params = self._parse_formal_parameters()
body = self._parse_block_statement()
value = JsFunctionExpression(
params=params, body=body, generator=is_generator,
is_async=is_async, offset=func_offset)
return JsProperty(
key=key, value=value, computed=computed,
shorthand=False, method=True, kind=kind, offset=offset)
def _parse_property_key(self) -> tuple[Expression, bool]:
if self._at(JsTokenKind.LBRACKET):
self._advance()
key = self._parse_assignment_expression()
self._expect(JsTokenKind.RBRACKET)
return key, True
return self._parse_property_name(), False
def _parse_property_name(self) -> Expression:
tok = self._current
if self._at(JsTokenKind.INTEGER, JsTokenKind.FLOAT):
self._advance()
raw = tok.value
text = raw.replace('_', '')
return JsNumericLiteral(
value=float(text) if tok.kind == JsTokenKind.FLOAT else self._parse_int_text(text),
raw=raw,
offset=tok.offset,
)
if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE):
return self._parse_string_literal()
if self._at(JsTokenKind.PRIVATE_IDENTIFIER):
self._advance()
return self._private_identifier(tok, tok.offset)
self._advance()
return self._name_or_error(tok.value, tok.offset, may_be_reserved=True)
def _parse_paren_or_arrow(self, is_async: bool = False) -> Expression:
"""
What ECMA-262 calls `CoverParenthesizedExpressionAndArrowParameterList`: a bracketed list
that the token behind the closing bracket decides the reading of, because nothing inside it
does. It is read as a list of assignment expressions either way and only then converted,
which is what lets one pass read a head no expression grammar accepts.
Three of its shapes belong to the parameter reading alone and are not expressions at all —
the empty list, a rest element, and a trailing comma — so a list holding one of them is an
arrow head or it is nothing. The rest element in particular may only stand last, and reading
it as one of the list rather than as a case of its own is the whole difference between
`(...a) => a` and `(b, ...a) => a`.
Where such a list has no arrow behind it, the head stands with nothing to give its
parameters, and what is missing is recorded where the body would be. Demanding the arrow
instead consumes whatever does stand there — the semicolon ending the statement, say —
and the body then reads the statement behind it, so `x = (a,); y = 2;` would take the
second line into a function nobody wrote and leave nothing to say that it had.
"""
with self._with_no_in(False):
offset = self._current.offset
self._expect(JsTokenKind.LPAREN)
items: list[Expression] = []
head_only = True
while not self._at(JsTokenKind.RPAREN, JsTokenKind.EOF):
if self._at(JsTokenKind.ELLIPSIS):
items.append(self._parse_rest_element())
head_only = True
break
items.append(self._parse_assignment_expression())
head_only = False
if not self._eat(JsTokenKind.COMMA):
break
head_only = self._at(JsTokenKind.RPAREN)
self._expect(JsTokenKind.RPAREN)
if self._at(JsTokenKind.ARROW) and not self._preceded_by_newline:
self._advance()
body = self._parse_arrow_body(is_async)
elif head_only:
body = JsErrorNode(
text='',
message='a parameter list with no arrow behind it',
offset=self._current.offset,
)
else:
expression = items[0] if len(items) == 1 else JsSequenceExpression(
expressions=items, offset=offset)
return JsParenthesizedExpression(expression=expression, offset=offset)
return JsArrowFunctionExpression(
params=[self._to_param(item) for item in items],
body=body,
offset=offset,
)
def _parse_arrow_body(self, is_async: bool = False) -> Expression | JsBlockStatement:
with self._function_body_context(is_async, False):
if self._at(JsTokenKind.LBRACE):
return self._parse_block_statement()
return self._parse_assignment_expression()
def _to_param(self, expr: Expression) -> Expression:
if isinstance(expr, JsIdentifier):
return expr
if isinstance(expr, JsAssignmentExpression) and expr.operator == '=':
return JsAssignmentPattern(
left=self._to_param(expr.left),
right=expr.right,
offset=expr.offset,
)
if isinstance(expr, JsSpreadElement):
return JsRestElement(argument=self._to_param(expr.argument), offset=expr.offset)
if isinstance(expr, JsArrayExpression):
elements = [
self._to_param(e) if e is not None else None
for e in expr.elements
]
return JsArrayPattern(elements=elements, offset=expr.offset)
if isinstance(expr, JsObjectExpression):
props: list[JsProperty | JsRestElement] = []
for p in expr.properties:
if isinstance(p, JsSpreadElement):
props.append(JsRestElement(
argument=self._to_param(p.argument), offset=p.offset))
else:
props.append(p)
return JsObjectPattern(properties=props, offset=expr.offset)
return expr
def _parse_function_expression(self) -> JsFunctionExpression:
return self._parse_function_impl(as_expression=True)
def _parse_class_expression(self) -> JsClassExpression:
return self._parse_class_impl(as_expression=True)
def _parse_async_expression(self) -> Expression:
offset = self._current.offset
self._advance()
return self._parse_expression_starting_with_async(offset)
def _parse_expression_starting_with_async(self, offset: int) -> Expression:
if not self._preceded_by_newline:
if self._at(JsTokenKind.FUNCTION):
return self._parse_function_impl(as_expression=True, is_async=True)
if self._at(JsTokenKind.ARROW):
self._advance()
param = JsIdentifier(name='async', offset=offset)
body = self._parse_arrow_body(False)
return JsArrowFunctionExpression(
params=[param], body=body, is_async=False, offset=offset)
if (
self._at_binding_identifier()
and self._peek_next().kind == JsTokenKind.ARROW
and not self._ahead_newline
):
tok = self._advance()
self._advance()
param = self._name_or_error(tok.value, tok.offset, may_be_reserved=False)
body = self._parse_arrow_body(True)
return JsArrowFunctionExpression(
params=[param], body=body, is_async=True, offset=offset)
if self._at(JsTokenKind.LPAREN):
self._advance()
args = self._parse_argument_list()
if self._at(JsTokenKind.ARROW) and not self._preceded_by_newline:
self._advance()
params = [self._to_param(arg) for arg in args]
body = self._parse_arrow_body(True)
return JsArrowFunctionExpression(
params=params, body=body, is_async=True, offset=offset)
return JsCallExpression(
callee=JsIdentifier(name='async', offset=offset),
arguments=args,
optional=False,
offset=offset,
)
return JsIdentifier(name='async', offset=offset)
def _parse_yield_expression(self) -> JsYieldExpression:
"""
A YieldExpression, whose one line terminator restriction sits between the `yield` and what
follows it. A newline there ends the expression, so neither a `*` nor an argument can still
belong to it; a newline anywhere after the `*` is ordinary whitespace, and the argument is
read across it.
A token that closes the construct the `yield` stands in is not an argument, and the hole of
a template is closed by the text that resumes it rather than by a brace of its own.
"""
offset = self._current.offset
self._advance()
if self._preceded_by_newline:
return JsYieldExpression(argument=None, delegate=False, offset=offset)
delegate = self._eat(JsTokenKind.STAR) is not None
argument = None
if not self._at(
JsTokenKind.SEMICOLON,
JsTokenKind.RBRACE,
JsTokenKind.RPAREN,
JsTokenKind.RBRACKET,
JsTokenKind.COMMA,
JsTokenKind.COLON,
JsTokenKind.TEMPLATE_MIDDLE,
JsTokenKind.TEMPLATE_TAIL,
JsTokenKind.EOF,
):
argument = self._parse_assignment_expression()
return JsYieldExpression(
argument=argument, delegate=delegate, offset=offset)
Classes
class JsParser (source, *, top_level_await=False)-
Expand source code Browse git
class JsParser: @staticmethod def _parse_int_text(text: str) -> int: if text.startswith(('0x', '0X')): return int(text, 16) if text.startswith(('0o', '0O')): return int(text, 8) if text.startswith(('0b', '0B')): return int(text, 2) if len(text) > 1 and text[0] == '0' and all(d in '01234567' for d in text): return int(text, 8) return int(text) def __init__(self, source: str, *, top_level_await: bool = False): self._lexer = JsLexer(source) self._source = source self._tokens = self._lexer.tokenize() self._current: JsToken = JsToken(JsTokenKind.EOF, '', 0) self._preceded_by_newline: bool = False self._ahead: JsToken | None = None self._ahead_newline: bool = False self._ahead_state: tuple[JsLexerState, int] | None = None self._no_in: bool = False self._in_async: bool = top_level_await self._in_generator: bool = False self._pending_comments: list[str] = [] self._recovered: bool = False self._advance() def _pull_token(self) -> tuple[JsToken, bool]: had_newline = False while True: tok = next(self._tokens, JsToken(JsTokenKind.EOF, '', len(self._source))) if tok.kind == JsTokenKind.NEWLINE: had_newline = True continue if tok.kind == JsTokenKind.COMMENT: self._pending_comments.append(tok.value) continue break return tok, had_newline def _advance(self) -> JsToken: prev = self._current if self._ahead is not None: self._current = self._ahead self._preceded_by_newline = self._ahead_newline self._ahead = None self._ahead_state = None return prev self._current, self._preceded_by_newline = self._pull_token() return prev def _drain_comments(self, node): if self._pending_comments: node.leading_comments.extend(self._pending_comments) self._pending_comments.clear() def _peek(self) -> JsToken: return self._current def _peek_next(self) -> JsToken: if self._ahead is None: self._ahead_state = self._lexer.capture(), len(self._pending_comments) self._ahead, self._ahead_newline = self._pull_token() return self._ahead def _rescan_as_regexp(self) -> JsToken: """ Read the slash the parser is holding again, as the regular expression it begins. A slash is the one character whose token depends on where the grammar stands rather than on what the text says, and the lexer is not standing anywhere: it spells every slash as an operator, and this is the single place that knows that an expression is about to begin. Rewinding is what makes that affordable — reading a division that was a regular expression costs one token, whereas the other way around has already swallowed the rest of the line. Everything scanned since the slash is given back, because a lookahead token may have opened or closed a template hole and a skipped comment would otherwise be collected twice. The slash itself is given back too where no literal begins there, so that a scan which found no terminator on its line leaves the operator standing rather than a literal nobody wrote. """ if self._ahead_state is None: state, comments = self._lexer.capture(), len(self._pending_comments) else: state, comments = self._ahead_state resume_pos, resume_state = self._lexer.pos, self._lexer.capture() self._lexer.rewind(self._current.offset, state) token = self._lexer.scan_regexp() if token is None: self._lexer.rewind(resume_pos, resume_state) return self._current del self._pending_comments[comments:] self._ahead = None self._ahead_newline = False self._ahead_state = None self._current = token self._tokens = self._lexer.tokenize() return self._current def _at(self, *kinds: JsTokenKind) -> bool: return self._current.kind in kinds def _eat(self, kind: JsTokenKind) -> JsToken | None: if self._current.kind == kind: return self._advance() return None def _expect(self, kind: JsTokenKind) -> JsToken: """ The token that must stand here, where it does. Where it does not, the parser writes it itself and steps over what was there, so that a file it cannot read is still answered with a tree. That answer is a program the source does not hold, in both directions at once — a bracket nobody wrote is invented and the token that stood in its place is dropped — so the file is recorded as one the parser repaired. `JsScript.recovered` carries that to `refinery.lib.scripts.is_well_formed`, which is what keeps a truncated payload from being spliced into a host file as though it had been written whole. """ if self._current.kind == kind: return self._advance() tok = self._current self._recovered = True self._advance() return JsToken(kind, tok.value, tok.offset, tok.terminated) def _require(self, kind: JsTokenKind) -> None: """ The token the grammar requires here, consumed where it stands here. Where it does not the parser goes on without it and records the repair, leaving what is here to be read as whatever comes next. This is what a list separator wants, and it is why `_expect` is the wrong primitive for one: what follows a missing comma is the next element of the list, not a token to be thrown away, so `{ a: 1 b: 2 }` is a file the parser reports having repaired and still prints everything that was written in. """ if self._eat(kind) is None: self._recovered = True def _at_identifier_name(self) -> bool: """ Whether a word stands here. Every word the language has is an IdentifierName, a keyword no less than a name, so a position taking one takes them all and what this refuses is a token that spells no word at all. """ return self._at(JsTokenKind.IDENTIFIER) or self._current.kind.is_keyword def _is_binding_identifier(self, token: JsToken) -> bool: """ Whether the token can serve as an ordinary binding or reference name. Several contextual keywords (`as`, `from`, `of`, `let`, `async`) are always valid names, while `await` and `yield` are valid names except inside an async function or a generator respectively. This is the identifier acceptance of `_parse_primary_expression` itself, so a name-reading site accepts exactly the tokens the expression grammar would treat as a reference. """ kind = token.kind return ( kind in ( JsTokenKind.IDENTIFIER, JsTokenKind.AS, JsTokenKind.FROM, JsTokenKind.OF, JsTokenKind.LET, JsTokenKind.ASYNC, ) or (kind is JsTokenKind.AWAIT and not self._in_async) or (kind is JsTokenKind.YIELD and not self._in_generator) ) def _at_binding_identifier(self) -> bool: return self._is_binding_identifier(self._current) def _at_function_name(self) -> bool: """ Whether the token standing here names the function whose `function` keyword was just read. Only a name or the parameter list may stand in that position, so `yield` and `await` are read as the name wherever they appear rather than through `_at_binding_identifier`, whose answer is about the enclosing function's kind. That answer is the wrong one here in both directions. A function expression's name takes its own kind and not the enclosing one, so `function* g() { var f = function yield() {}; }` is a program whose name would otherwise be dropped; and where the name really is an early error the tree must still spell it, so that `refinery.lib.scripts.js.strict.reserved_by_function_kind` reports it rather than the parameter list being read starting at the name. """ return ( self._at_binding_identifier() or self._at(JsTokenKind.YIELD, JsTokenKind.AWAIT) ) def _at_variable_declaration(self) -> bool: """ Whether a variable declaration begins here, rather than an expression that merely opens with the same word. ECMA-262 reserves `let` in strict code only, so wherever a statement may also be read as an expression, a `let` declares nothing unless a binding follows it: it is a name being called in `let(1)`, divided in `let / 2` and read in `let.a`, and only `let [` is the spelling a statement is forbidden to take as an expression. """ if self._at(JsTokenKind.VAR, JsTokenKind.CONST): return True if not self._at(JsTokenKind.LET): return False ahead = self._peek_next() return ( self._is_binding_identifier(ahead) or ahead.kind in (JsTokenKind.LBRACKET, JsTokenKind.LBRACE) ) def _eat_semicolon(self) -> bool: """ The semicolon that ends a statement, whether the file wrote it or the language supplies it. ECMA-262 supplies one before a token a line terminator separates from what came before, before a closing brace, and at the end of the file, so those three are the whole of what a statement may end with instead of a semicolon. Where none of them stands here the parser writes the semicolon anyway, which is a repair like any other: `x = 1 y = 2` is a file no engine reads, and left unrecorded it comes back as the two statements the parser split it into. The one place the language inserts a semicolon on no such condition is after a `do` loop's `while` clause, which reads the semicolon it may find with `_eat` rather than asking here. """ if self._eat(JsTokenKind.SEMICOLON): return True if self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): return True if self._preceded_by_newline: return True self._recovered = True return False @contextmanager def _with_no_in(self, value: bool): saved = self._no_in self._no_in = value try: yield finally: self._no_in = saved @contextmanager def _function_body_context(self, is_async: bool, is_generator: bool): saved = (self._in_async, self._in_generator) self._in_async = is_async self._in_generator = is_generator try: yield finally: self._in_async, self._in_generator = saved def parse(self) -> JsScript: script = self._parse_program() mark_directives(script) mark_module(script) return script def _parse_statement_list(self, *stop: JsTokenKind) -> list[Statement]: """ The statements standing between here and the first of *stop*. A statement the parser could not read at all is the token it stopped on, kept as itself; one it gave up on partway is the whole span it had reached, kept the same way. Handing back nothing for that span is what would delete it: the tokens are already read, so the text they spell appears in no node, and a file that lost a statement prints as a shorter program nobody wrote. A statement list is where the `for` head's suppression of the `in` operator ends. The head reaches an expression and only an expression, and the one way a statement stands inside one is a function body, which is a fresh context: `for (function () { return 'k' in b; }; ; )` is a program, and `for (q => 'k' in b; ; )` is not, because a concise arrow body is an expression and inherits the suppression rather than ending it. """ body: list[Statement] = [] with self._with_no_in(False): while not self._at(*stop): mark = self._current.offset comments = list(self._pending_comments) self._pending_comments.clear() try: stmt = self._parse_statement() except Exception: stmt = None if self._current.offset != mark: stmt = self._unread_since(mark, 'a statement that could not be read') if stmt is not None: stmt.leading_comments.extend(comments) body.append(stmt) elif self._current.offset == mark: tok = self._advance() error = JsErrorNode(offset=tok.offset, text=tok.value) error.leading_comments.extend(comments) body.append(error) return body def _parse_program(self) -> JsScript: offset = self._current.offset body = self._parse_statement_list(JsTokenKind.EOF) return JsScript(body=body, offset=offset, recovered=self._recovered) def _parse_statement(self) -> Statement | None: offset = self._current.offset kind = self._current.kind if kind == JsTokenKind.LBRACE: return self._parse_block_statement() if kind == JsTokenKind.SEMICOLON: self._advance() return JsEmptyStatement(offset=offset) if self._at_variable_declaration(): return self._parse_variable_declaration() if kind == JsTokenKind.IF: return self._parse_if_statement() if kind == JsTokenKind.WHILE: return self._parse_while_statement() if kind == JsTokenKind.DO: return self._parse_do_while_statement() if kind == JsTokenKind.FOR: return self._parse_for_statement() if kind == JsTokenKind.SWITCH: return self._parse_switch_statement() if kind == JsTokenKind.TRY: return self._parse_try_statement() if kind == JsTokenKind.WITH: return self._parse_with_statement() if kind == JsTokenKind.RETURN: return self._parse_return_statement() if kind == JsTokenKind.THROW: return self._parse_throw_statement() if kind == JsTokenKind.BREAK: return self._parse_break_statement() if kind == JsTokenKind.CONTINUE: return self._parse_continue_statement() if kind == JsTokenKind.FUNCTION: return self._parse_function_declaration() if kind == JsTokenKind.AT: decorators = self._parse_decorators() if self._at(JsTokenKind.EXPORT): return self._parse_export_declaration(decorators) if self._at(JsTokenKind.CLASS): return self._parse_class_declaration(decorators) return JsErrorNode( text=self._source[offset:self._current.offset].rstrip(), message='decorators must precede a class', offset=offset, ) if kind == JsTokenKind.CLASS: return self._parse_class_declaration() if kind == JsTokenKind.DEBUGGER: self._advance() self._eat_semicolon() return JsDebuggerStatement(offset=offset) if kind == JsTokenKind.IMPORT and self._peek_next().kind not in ( JsTokenKind.LPAREN, JsTokenKind.DOT, ): return self._parse_import_declaration() if kind == JsTokenKind.EXPORT: return self._parse_export_declaration() if self._at_async_function(): self._advance() return self._parse_function_declaration(is_async=True) expr = self._parse_expression() if ( isinstance(expr, JsIdentifier) and self._eat(JsTokenKind.COLON) ): body = self._parse_statement() return JsLabeledStatement(label=expr, body=body, offset=offset) self._eat_semicolon() if isinstance(expr, JsErrorNode): return expr return JsExpressionStatement(expression=expr, offset=offset) def _parse_block_statement(self) -> JsBlockStatement: offset = self._current.offset self._expect(JsTokenKind.LBRACE) body = self._parse_statement_list(JsTokenKind.RBRACE, JsTokenKind.EOF) self._expect(JsTokenKind.RBRACE) return JsBlockStatement(body=body, offset=offset) def _parse_variable_declaration(self) -> JsVariableDeclaration: offset = self._current.offset kind_tok = self._advance() kind = _VAR_KIND_MAP[kind_tok.kind] declarations: list[JsVariableDeclarator] = [] declarations.append(self._parse_variable_declarator()) while self._eat(JsTokenKind.COMMA): declarations.append(self._parse_variable_declarator()) self._eat_semicolon() return JsVariableDeclaration(declarations=declarations, kind=kind, offset=offset) def _parse_variable_declarator(self) -> JsVariableDeclarator: offset = self._current.offset id_node = self._parse_binding_pattern() init = None if self._eat(JsTokenKind.EQUALS): init = self._parse_assignment_expression() return JsVariableDeclarator(id=id_node, init=init, offset=offset) def _parse_binding_pattern(self) -> Expression: if self._at(JsTokenKind.LBRACKET): return self._parse_array_pattern() if self._at(JsTokenKind.LBRACE): return self._parse_object_pattern() return self._parse_binding_identifier() def _parse_binding_identifier(self) -> Expression: offset = self._current.offset if self._at_binding_identifier(): tok = self._advance() else: tok = self._expect(JsTokenKind.IDENTIFIER) return self._name_or_error(tok.value, offset, may_be_reserved=False) def _name_or_error(self, text: str, offset: int, *, may_be_reserved: bool) -> Expression: """ The name a token spells, where it spells one. A file that ends in the middle of a declaration leaves the position a name was expected in holding nothing, and a name spelled by nothing has no text at all: printing it closes the source up over the gap, so `var` at the end of a file would come back as `var ;`. What is handed back instead is the span itself, which prints as what was written and states that the parser did not read it. Two further texts spell no name. One holds an escape naming no character a name may hold, which is a fact about the text alone and is refused wherever it stands. The other spells a reserved word, which the language refuses only where the name could also be a variable — `o.\\u0069f` reads a member and `var \\u0069f` declares nothing — and *may_be_reserved* is which of the two positions this is. Both come back as the span, so the file prints as it was written and no pass reads a name out of text no engine read one from. """ if not text: return JsErrorNode(text=text, message='expected a name', offset=offset) name = identifier_string_value(text) if name is None: return JsErrorNode(text=text, message='not a name', offset=offset) if not may_be_reserved and name in RESERVED_WORD_NAMES: return JsErrorNode(text=text, message='reserved word', offset=offset) return JsIdentifier(name=name, raw=text if name != text else '', offset=offset) def _identifier(self, tok: JsToken) -> JsIdentifier: """ The name a token spells, at a position whose slot holds a name and nothing else. A label and the name a function or class declaration gives itself are these, and a text spelling no name is kept there as it was written: what the model has no shape for, the parser has no way to state. """ name = identifier_string_value(tok.value) or tok.value return JsIdentifier( name=name, raw=tok.value if name != tok.value else '', offset=tok.offset, ) def _private_identifier(self, tok: JsToken, offset: int) -> Expression: """ The private name a token spells. The `#` opens the name and is no part of it, so what follows it is an IdentifierName like any other and `this.#\\u0061` reads what `#a` declares. """ text = tok.value[1:] name = identifier_string_value(text) if name is None: return JsErrorNode(text=tok.value, message='not a name', offset=offset) return JsPrivateIdentifier( name=name, raw=text if name != text else '', offset=offset, ) def _parse_array_pattern(self) -> JsArrayPattern: offset = self._current.offset self._expect(JsTokenKind.LBRACKET) elements: list[Expression | None] = [] while not self._at(JsTokenKind.RBRACKET, JsTokenKind.EOF): if self._at(JsTokenKind.COMMA): elements.append(None) self._advance() continue if self._at(JsTokenKind.ELLIPSIS): elements.append(self._parse_rest_element()) break elem = self._parse_binding_pattern() if self._eat(JsTokenKind.EQUALS): right = self._parse_assignment_expression() elem = JsAssignmentPattern(left=elem, right=right, offset=elem.offset) elements.append(elem) if not self._at(JsTokenKind.RBRACKET): self._expect(JsTokenKind.COMMA) self._expect(JsTokenKind.RBRACKET) return JsArrayPattern(elements=elements, offset=offset) def _parse_object_pattern(self) -> JsObjectPattern: offset = self._current.offset self._expect(JsTokenKind.LBRACE) properties: list[JsProperty | JsRestElement] = [] while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): if self._at(JsTokenKind.ELLIPSIS): properties.append(self._parse_rest_element()) break prop = self._parse_object_pattern_property() properties.append(prop) if not self._at(JsTokenKind.RBRACE): self._expect(JsTokenKind.COMMA) self._expect(JsTokenKind.RBRACE) return JsObjectPattern(properties=properties, offset=offset) def _parse_object_pattern_property(self) -> JsProperty: offset = self._current.offset if self._at(JsTokenKind.LBRACKET): self._advance() key = self._parse_assignment_expression() self._expect(JsTokenKind.RBRACKET) self._expect(JsTokenKind.COLON) value = self._parse_binding_pattern() if self._eat(JsTokenKind.EQUALS): right = self._parse_assignment_expression() value = JsAssignmentPattern(left=value, right=right, offset=value.offset) return JsProperty( key=key, value=value, computed=True, shorthand=False, offset=offset) key = self._parse_property_name() if self._eat(JsTokenKind.COLON): value = self._parse_binding_pattern() if self._eat(JsTokenKind.EQUALS): right = self._parse_assignment_expression() value = JsAssignmentPattern(left=value, right=right, offset=value.offset) return JsProperty( key=key, value=value, computed=False, shorthand=False, offset=offset) value = key if self._eat(JsTokenKind.EQUALS): right = self._parse_assignment_expression() value = JsAssignmentPattern(left=key, right=right, offset=key.offset) return JsProperty(key=key, value=value, computed=False, shorthand=True, offset=offset) def _parse_rest_element(self) -> JsRestElement: offset = self._current.offset self._expect(JsTokenKind.ELLIPSIS) argument = self._parse_binding_pattern() return JsRestElement(argument=argument, offset=offset) def _parse_if_statement(self) -> JsIfStatement: offset = self._current.offset self._expect(JsTokenKind.IF) self._expect(JsTokenKind.LPAREN) test = self._parse_expression() self._expect(JsTokenKind.RPAREN) consequent = self._parse_statement() alternate = None if self._eat(JsTokenKind.ELSE): alternate = self._parse_statement() return JsIfStatement( test=test, consequent=consequent, alternate=alternate, offset=offset) def _parse_while_statement(self) -> JsWhileStatement: offset = self._current.offset self._expect(JsTokenKind.WHILE) self._expect(JsTokenKind.LPAREN) test = self._parse_expression() self._expect(JsTokenKind.RPAREN) body = self._parse_statement() return JsWhileStatement(test=test, body=body, offset=offset) def _parse_do_while_statement(self) -> JsDoWhileStatement: offset = self._current.offset self._expect(JsTokenKind.DO) body = self._parse_statement() self._expect(JsTokenKind.WHILE) self._expect(JsTokenKind.LPAREN) test = self._parse_expression() self._expect(JsTokenKind.RPAREN) self._eat(JsTokenKind.SEMICOLON) return JsDoWhileStatement(test=test, body=body, offset=offset) def _parse_for_statement(self) -> Statement: offset = self._current.offset self._expect(JsTokenKind.FOR) is_await = False if self._eat(JsTokenKind.AWAIT): is_await = True self._expect(JsTokenKind.LPAREN) if self._at(JsTokenKind.SEMICOLON): self._advance() return self._parse_for_rest(None, offset) if self._at_variable_declaration(): decl_offset = self._current.offset kind_tok = self._advance() kind = _VAR_KIND_MAP[kind_tok.kind] with self._with_no_in(True): declarator = self._parse_variable_declarator() decl = JsVariableDeclaration( declarations=[declarator], kind=kind, offset=decl_offset) result = self._parse_for_in_or_of(decl, is_await, offset) if result is not None: return result while self._eat(JsTokenKind.COMMA): with self._with_no_in(True): decl.declarations.append(self._parse_variable_declarator()) self._expect(JsTokenKind.SEMICOLON) return self._parse_for_rest(decl, offset) with self._with_no_in(True): init_expr = self._parse_expression() result = self._parse_for_in_or_of(init_expr, is_await, offset) if result is not None: return result self._expect(JsTokenKind.SEMICOLON) return self._parse_for_rest(init_expr, offset) def _parse_for_in_or_of( self, left: Expression | Statement, is_await: bool, offset: int, ) -> JsForInStatement | JsForOfStatement | None: if self._eat(JsTokenKind.IN): right = self._parse_expression() self._expect(JsTokenKind.RPAREN) body = self._parse_statement() return JsForInStatement(left=left, right=right, body=body, offset=offset) if self._at(JsTokenKind.OF): self._advance() right = self._parse_assignment_expression() self._expect(JsTokenKind.RPAREN) body = self._parse_statement() return JsForOfStatement( left=left, right=right, body=body, is_await=is_await, offset=offset) return None def _parse_for_rest( self, init: Expression | Statement | None, offset: int, ) -> JsForStatement: test = None if not self._at(JsTokenKind.SEMICOLON): test = self._parse_expression() self._expect(JsTokenKind.SEMICOLON) update = None if not self._at(JsTokenKind.RPAREN): update = self._parse_expression() self._expect(JsTokenKind.RPAREN) body = self._parse_statement() return JsForStatement( init=init, test=test, update=update, body=body, offset=offset) def _parse_switch_statement(self) -> JsSwitchStatement: offset = self._current.offset self._expect(JsTokenKind.SWITCH) self._expect(JsTokenKind.LPAREN) discriminant = self._parse_expression() self._expect(JsTokenKind.RPAREN) self._expect(JsTokenKind.LBRACE) cases: list[JsSwitchCase] = [] while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): cases.append(self._parse_switch_case()) self._expect(JsTokenKind.RBRACE) return JsSwitchStatement( discriminant=discriminant, cases=cases, offset=offset) def _parse_switch_case(self) -> JsSwitchCase: offset = self._current.offset test = None if self._eat(JsTokenKind.CASE): test = self._parse_expression() self._expect(JsTokenKind.COLON) elif self._eat(JsTokenKind.DEFAULT): self._expect(JsTokenKind.COLON) else: self._recovered = True self._advance() body: list[Statement] = [] while not self._at( JsTokenKind.CASE, JsTokenKind.DEFAULT, JsTokenKind.RBRACE, JsTokenKind.EOF, ): stmt = self._parse_statement() if stmt is not None: body.append(stmt) return JsSwitchCase(test=test, body=body, offset=offset) def _parse_try_statement(self) -> JsTryStatement: offset = self._current.offset self._expect(JsTokenKind.TRY) block = self._parse_block_statement() handler = None finalizer = None if self._eat(JsTokenKind.CATCH): handler = self._parse_catch_clause() if self._eat(JsTokenKind.FINALLY): finalizer = self._parse_block_statement() if handler is None and finalizer is None: self._recovered = True return JsTryStatement( block=block, handler=handler, finalizer=finalizer, offset=offset) def _parse_catch_clause(self) -> JsCatchClause: offset = self._current.offset param = None if self._eat(JsTokenKind.LPAREN): param = self._parse_binding_pattern() self._expect(JsTokenKind.RPAREN) body = self._parse_block_statement() return JsCatchClause(param=param, body=body, offset=offset) def _parse_with_statement(self) -> JsWithStatement: offset = self._current.offset self._expect(JsTokenKind.WITH) self._expect(JsTokenKind.LPAREN) obj = self._parse_expression() self._expect(JsTokenKind.RPAREN) body = self._parse_statement() return JsWithStatement(object=obj, body=body, offset=offset) def _parse_return_statement(self) -> JsReturnStatement: offset = self._current.offset self._expect(JsTokenKind.RETURN) argument = None if not self._preceded_by_newline and not self._at( JsTokenKind.SEMICOLON, JsTokenKind.RBRACE, JsTokenKind.EOF, ): argument = self._parse_expression() self._eat_semicolon() return JsReturnStatement(argument=argument, offset=offset) def _parse_throw_statement(self) -> JsThrowStatement: offset = self._current.offset self._expect(JsTokenKind.THROW) argument = None if not self._preceded_by_newline: argument = self._parse_expression() self._eat_semicolon() return JsThrowStatement(argument=argument, offset=offset) def _parse_break_statement(self) -> JsBreakStatement: offset = self._current.offset self._expect(JsTokenKind.BREAK) label = None if not self._preceded_by_newline and self._at_binding_identifier(): tok = self._advance() label = self._identifier(tok) self._eat_semicolon() return JsBreakStatement(label=label, offset=offset) def _parse_continue_statement(self) -> JsContinueStatement: offset = self._current.offset self._expect(JsTokenKind.CONTINUE) label = None if not self._preceded_by_newline and self._at_binding_identifier(): tok = self._advance() label = self._identifier(tok) self._eat_semicolon() return JsContinueStatement(label=label, offset=offset) def _parse_function_impl( self, *, as_expression: bool, is_async: bool = False, ) -> JsFunctionDeclaration | JsFunctionExpression: offset = self._current.offset self._expect(JsTokenKind.FUNCTION) generator = bool(self._eat(JsTokenKind.STAR)) id_node = None if self._at_function_name(): tok = self._advance() id_node = self._identifier(tok) with self._function_body_context(is_async, generator): params = self._parse_formal_parameters() body = self._parse_block_statement() if as_expression: return JsFunctionExpression( id=id_node, params=params, body=body, generator=generator, is_async=is_async, offset=offset) return JsFunctionDeclaration( id=id_node, params=params, body=body, generator=generator, is_async=is_async, offset=offset) def _parse_function_declaration( self, is_async: bool = False, ) -> JsFunctionDeclaration: return self._parse_function_impl(as_expression=False, is_async=is_async) def _parse_formal_parameters(self) -> list[Expression]: self._expect(JsTokenKind.LPAREN) params: list[Expression] = [] while not self._at(JsTokenKind.RPAREN, JsTokenKind.EOF): if self._at(JsTokenKind.ELLIPSIS): params.append(self._parse_rest_element()) break param = self._parse_binding_pattern() if self._eat(JsTokenKind.EQUALS): default = self._parse_assignment_expression() param = JsAssignmentPattern( left=param, right=default, offset=param.offset) params.append(param) if not self._at(JsTokenKind.RPAREN): self._expect(JsTokenKind.COMMA) self._expect(JsTokenKind.RPAREN) return params def _parse_decorators(self) -> list[JsDecorator]: decorators: list[JsDecorator] = [] while self._at(JsTokenKind.AT): decorators.append(self._parse_decorator()) return decorators def _parse_decorator(self) -> JsDecorator: offset = self._current.offset self._expect(JsTokenKind.AT) if self._at(JsTokenKind.LPAREN): self._advance() inner = self._parse_expression() self._expect(JsTokenKind.RPAREN) return JsDecorator(expression=inner, offset=offset) if not self._at_binding_identifier(): return JsDecorator( expression=JsErrorNode( text=self._current.value, message='unexpected token', offset=offset), offset=offset, ) tok = self._advance() expr: Expression = self._name_or_error(tok.value, tok.offset, may_be_reserved=False) while self._eat(JsTokenKind.DOT): prop = self._advance() expr = JsMemberExpression( object=expr, property=self._name_or_error(prop.value, prop.offset, may_be_reserved=True), computed=False, offset=expr.offset, ) if self._at(JsTokenKind.LPAREN): expr = self._parse_call_arguments(expr, optional=False) return JsDecorator(expression=expr, offset=offset) def _parse_class_impl( self, *, as_expression: bool, decorators: list[JsDecorator] | None = None, ) -> JsClassDeclaration | JsClassExpression: offset = self._current.offset self._expect(JsTokenKind.CLASS) id_node = None if self._at_binding_identifier(): tok = self._advance() id_node = self._identifier(tok) super_class = None if self._eat(JsTokenKind.EXTENDS): super_class = self._parse_assignment_expression() body = self._parse_class_body() if as_expression: return JsClassExpression( id=id_node, super_class=super_class, body=body, decorators=decorators or [], offset=offset, ) return JsClassDeclaration( id=id_node, super_class=super_class, body=body, decorators=decorators or [], offset=offset, ) def _parse_class_declaration( self, decorators: list[JsDecorator] | None = None, ) -> JsClassDeclaration: return self._parse_class_impl(as_expression=False, decorators=decorators) def _parse_class_body(self) -> JsClassBody: offset = self._current.offset self._expect(JsTokenKind.LBRACE) members: list[JsMethodDefinition | JsPropertyDefinition | JsStaticBlock] = [] while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): if self._eat(JsTokenKind.SEMICOLON): continue decorators = self._parse_decorators() member = self._parse_class_member() if decorators and isinstance(member, (JsMethodDefinition, JsPropertyDefinition)): member.decorators = decorators member._adopt(*decorators) members.append(member) self._expect(JsTokenKind.RBRACE) return JsClassBody(body=members, offset=offset) def _parse_static_block(self, offset: int) -> JsStaticBlock: block = self._parse_block_statement() return JsStaticBlock(body=block.body, offset=offset) def _parse_class_member(self) -> JsMethodDefinition | JsPropertyDefinition | JsStaticBlock: offset = self._current.offset is_static = False if self._at(JsTokenKind.IDENTIFIER) and self._current.value == 'static': saved_pos = self._current self._advance() if self._at(JsTokenKind.LBRACE): return self._parse_static_block(offset) if self._at(JsTokenKind.LPAREN): key = JsIdentifier(name='static', offset=saved_pos.offset) return self._finish_class_member(key, False, False, offset) if self._at_class_field_terminator(): key = JsIdentifier(name='static', offset=saved_pos.offset) return self._finish_class_field(key, False, False, offset) is_static = True kind = JsMethodKind.METHOD is_generator = bool(self._eat(JsTokenKind.STAR)) is_async = False if ( not is_generator and self._at(JsTokenKind.IDENTIFIER) and self._current.value in ('get', 'set') ): saved = self._current self._advance() if self._at(JsTokenKind.LPAREN): key = JsIdentifier(name=saved.value, offset=saved.offset) return self._finish_class_member(key, is_static, False, offset) if self._at_class_field_terminator(): key = JsIdentifier(name=saved.value, offset=saved.offset) return self._finish_class_field(key, is_static, False, offset) kind = JsMethodKind.GET if saved.value == 'get' else JsMethodKind.SET elif not is_generator and self._at(JsTokenKind.ASYNC): saved = self._current self._advance() if self._at(JsTokenKind.LPAREN): key = JsIdentifier(name='async', offset=saved.offset) return self._finish_class_member(key, is_static, False, offset) if self._preceded_by_newline or self._at_class_field_terminator(): key = JsIdentifier(name='async', offset=saved.offset) return self._finish_class_field(key, is_static, False, offset) is_async = True if self._eat(JsTokenKind.STAR): is_generator = True key, computed = self._parse_property_key() if kind == JsMethodKind.METHOD and not is_generator and not self._at(JsTokenKind.LPAREN): return self._finish_class_field(key, is_static, computed, offset) return self._finish_class_member(key, is_static, is_generator, offset, kind, computed, is_async=is_async) def _at_class_field_terminator(self) -> bool: """ Whether the current token completes a class element as a field named by the identifier just consumed: an initializer (`=`), an explicit terminator (`;`), or the end of the class body (`}` / end of input). A modifier prefix (`static`/`get`/`set`/`async`) followed by one of these is an ordinary field whose name happens to be that word, not a modifier. """ return self._at( JsTokenKind.EQUALS, JsTokenKind.SEMICOLON, JsTokenKind.RBRACE, JsTokenKind.EOF, ) def _finish_class_field( self, key: Expression, is_static: bool, computed: bool, offset: int, ) -> JsPropertyDefinition: value = None if self._eat(JsTokenKind.EQUALS): value = self._parse_assignment_expression() self._eat_semicolon() return JsPropertyDefinition( key=key, value=value, computed=computed, is_static=is_static, offset=offset, ) def _finish_class_member( self, key: Expression, is_static: bool, is_generator: bool, offset: int, kind: JsMethodKind = JsMethodKind.METHOD, computed: bool = False, is_async: bool = False, ) -> JsMethodDefinition: func_offset = self._current.offset with self._function_body_context(is_async, is_generator): params = self._parse_formal_parameters() body = self._parse_block_statement() value = JsFunctionExpression( params=params, body=body, generator=is_generator, is_async=is_async, offset=func_offset, ) if isinstance(key, JsIdentifier) and key.name == 'constructor' and kind == JsMethodKind.METHOD: kind = JsMethodKind.CONSTRUCTOR return JsMethodDefinition( key=key, value=value, kind=kind, computed=computed, is_static=is_static, offset=offset, ) def _parse_import_expression(self, offset: int) -> Expression: self._expect(JsTokenKind.IMPORT) if self._eat(JsTokenKind.DOT): prop = self._advance() return JsMetaProperty(meta='import', property=prop.value, offset=offset) if self._at(JsTokenKind.LPAREN): self._advance() source = self._parse_assignment_expression() options = None if self._eat(JsTokenKind.COMMA) and not self._at(JsTokenKind.RPAREN): options = self._parse_assignment_expression() self._eat(JsTokenKind.COMMA) self._expect(JsTokenKind.RPAREN) return JsImportExpression(source=source, options=options, offset=offset) return JsErrorNode(text='import', message='unexpected token', offset=offset) def _parse_import_attributes(self) -> tuple[str, list[JsImportAttribute]]: if self._preceded_by_newline: return '', [] if self._at(JsTokenKind.WITH): keyword = 'with' elif self._at(JsTokenKind.IDENTIFIER) and self._current.value == 'assert': keyword = 'assert' else: return '', [] self._advance() attributes: list[JsImportAttribute] = [] self._expect(JsTokenKind.LBRACE) while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): key = self._parse_property_name() self._expect(JsTokenKind.COLON) value = self._parse_string_literal() attributes.append(JsImportAttribute(key=key, value=value, offset=key.offset)) if not self._eat(JsTokenKind.COMMA): break self._expect(JsTokenKind.RBRACE) return keyword, attributes def _module_specifier(self) -> JsStringLiteral | None: """ The literal naming the module a declaration reads from, or `None` where none stands there. It is the one part of these declarations the grammar gives no default for, so a source that ends before writing it has not written the declaration at all; answering with a literal spelled by nothing states a module whose name is the empty string, which is a module the file could have named and did not. """ if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE): return self._parse_string_literal() return None def _unread_since(self, offset: int, message: str) -> JsErrorNode: """ The source from *offset* up to where reading stands, handed back as itself. A declaration the parser could not complete is kept whole rather than in the parts it did manage to read: what prints is then what was written, and reading that print again finds the same thing, where a half-built declaration prints the halves it has and reads back as something else. """ return JsErrorNode( text=self._source[offset:self._current.offset].rstrip(), message=message, offset=offset, ) def _parse_import_declaration(self) -> JsImportDeclaration | JsErrorNode: offset = self._current.offset self._expect(JsTokenKind.IMPORT) if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE): source = self._parse_string_literal() keyword, attributes = self._parse_import_attributes() self._eat_semicolon() return JsImportDeclaration( source=source, attributes=attributes, attributes_keyword=keyword, offset=offset) specifiers: list[ JsImportSpecifier | JsImportDefaultSpecifier | JsImportNamespaceSpecifier ] = [] if self._at_binding_identifier(): tok = self._advance() specifiers.append(JsImportDefaultSpecifier( local=self._name_or_error(tok.value, tok.offset, may_be_reserved=False), offset=tok.offset, )) if self._eat(JsTokenKind.COMMA): if self._at(JsTokenKind.STAR): specifiers.append(self._parse_namespace_import()) elif self._at(JsTokenKind.LBRACE): specifiers.extend(self._parse_named_imports()) else: self._recovered = True elif self._at(JsTokenKind.STAR): specifiers.append(self._parse_namespace_import()) elif self._at(JsTokenKind.LBRACE): specifiers.extend(self._parse_named_imports()) self._expect_contextual('from') source = self._module_specifier() if source is None: return self._unread_since(offset, 'a module declaration with no specifier') keyword, attributes = self._parse_import_attributes() self._eat_semicolon() return JsImportDeclaration( specifiers=specifiers, source=source, attributes=attributes, attributes_keyword=keyword, offset=offset, ) def _parse_module_export_name(self) -> Expression: """ The name an import or export list gives a binding on the far side of the module boundary. It is an IdentifierName rather than a name this file could refer to, so every word the language has stands here and none of them is a repair: `import { default as d } from 'm.js'` and `export * as default from 'm.js'` are both ordinary declarations. The shorthand `import { a }` writes one name in two positions at once, and this reads it as the boundary one, which is the wider of the two: what the shorthand also does is bind `a` locally, and no module may bind a word its strict code reserves. That rule is not applied anywhere yet, so nothing is lost by reading the shorthand here rather than gained by reading it as a binding. A word and a string literal are the whole of what the grammar writes here, and the two are different productions rather than two spellings of one. A string is read as the literal it is, because what it names is the text it denotes and not the way that text was written: export { a as 'b c' } from 'm'; export { a as 'b\\u0020c' } from 'm'; reach the one name `b c`, which no word spells, and asking a name reader for either would hand it a text that opens with a quote. Anything else is a token the parser steps over in order to answer with a name at all, and reading `,` as the name a module exports under is a repair however well it prints back. """ if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE): return self._parse_string_literal() if not self._at_identifier_name(): self._recovered = True tok = self._advance() return self._name_or_error(tok.value, tok.offset, may_be_reserved=True) def _parse_namespace_import(self) -> JsImportNamespaceSpecifier: offset = self._current.offset self._expect(JsTokenKind.STAR) self._expect_contextual('as') return JsImportNamespaceSpecifier( local=self._parse_binding_identifier(), offset=offset, ) def _parse_named_imports(self) -> list[JsImportSpecifier]: self._expect(JsTokenKind.LBRACE) specs: list[JsImportSpecifier] = [] while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): spec_offset = self._current.offset imported = self._parse_module_export_name() local = imported if self._at(JsTokenKind.AS): self._advance() local = self._parse_binding_identifier() specs.append(JsImportSpecifier( imported=imported, local=local, offset=spec_offset)) if not self._at(JsTokenKind.RBRACE): self._expect(JsTokenKind.COMMA) self._expect(JsTokenKind.RBRACE) return specs def _parse_export_declaration( self, decorators: list[JsDecorator] | None = None, ) -> Statement: offset = self._current.offset self._expect(JsTokenKind.EXPORT) class_decorators = list(decorators or []) + self._parse_decorators() if self._eat(JsTokenKind.DEFAULT): class_decorators += self._parse_decorators() if self._at(JsTokenKind.FUNCTION): decl = self._parse_function_declaration() return JsExportDefaultDeclaration(declaration=decl, offset=offset) if self._at(JsTokenKind.CLASS): decl = self._parse_class_declaration(class_decorators) return JsExportDefaultDeclaration(declaration=decl, offset=offset) if self._at_async_function(): self._advance() decl = self._parse_function_declaration(is_async=True) return JsExportDefaultDeclaration(declaration=decl, offset=offset) expr = self._parse_assignment_expression() self._eat_semicolon() return JsExportDefaultDeclaration(declaration=expr, offset=offset) if self._at(JsTokenKind.STAR): self._advance() exported = None if self._at(JsTokenKind.AS): self._advance() exported = self._parse_module_export_name() self._expect_contextual('from') source = self._module_specifier() if source is None: return self._unread_since(offset, 'a module declaration with no specifier') self._eat_semicolon() return JsExportAllDeclaration( source=source, exported=exported, offset=offset) if self._at(JsTokenKind.LBRACE): return self._parse_export_named(offset) if self._at(JsTokenKind.VAR, JsTokenKind.LET, JsTokenKind.CONST): decl = self._parse_variable_declaration() return JsExportNamedDeclaration(declaration=decl, offset=offset) if self._at(JsTokenKind.FUNCTION): decl = self._parse_function_declaration() return JsExportNamedDeclaration(declaration=decl, offset=offset) if self._at(JsTokenKind.CLASS): decl = self._parse_class_declaration(class_decorators) return JsExportNamedDeclaration(declaration=decl, offset=offset) if self._at_async_function(): self._advance() decl = self._parse_function_declaration(is_async=True) return JsExportNamedDeclaration(declaration=decl, offset=offset) self._recovered = True self._advance() return JsExportNamedDeclaration(offset=offset) def _parse_export_named(self, offset: int) -> JsExportNamedDeclaration | JsErrorNode: self._expect(JsTokenKind.LBRACE) specifiers: list[JsExportSpecifier] = [] while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): spec_offset = self._current.offset local = self._parse_module_export_name() exported = local if self._at(JsTokenKind.AS): self._advance() exported = self._parse_module_export_name() specifiers.append(JsExportSpecifier( local=local, exported=exported, offset=spec_offset)) if not self._at(JsTokenKind.RBRACE): self._expect(JsTokenKind.COMMA) self._expect(JsTokenKind.RBRACE) source = None if self._at(JsTokenKind.FROM): self._advance() source = self._module_specifier() if source is None: return self._unread_since(offset, 'a module declaration with no specifier') self._eat_semicolon() return JsExportNamedDeclaration( specifiers=specifiers, source=source, offset=offset) def _at_async_function(self) -> bool: """ Whether the parser is positioned at `async function` with no line terminator between the two — the one form in which a leading `async` opens a declaration rather than an ordinary expression. Every other `async` (a call, member access, arrow, or bare reference) is left to the expression grammar, which reaches it through `_parse_async_expression` and applies the full call/member and operator parsing. """ return ( self._at(JsTokenKind.ASYNC) and self._peek_next().kind == JsTokenKind.FUNCTION and not self._ahead_newline ) def _expect_contextual(self, keyword: str): """ The word that must stand here, which is a word and not a token kind: `as` and `from` are names everywhere else, so the lexer hands them over as themselves in one form and as an identifier in the other. Where neither is what stands here the parser steps over whatever does, which drops it from the file, and that is a repair like any other. """ if self._at(JsTokenKind.FROM) and keyword == 'from': self._advance() return if self._at(JsTokenKind.AS) and keyword == 'as': self._advance() return if self._at(JsTokenKind.IDENTIFIER) and self._current.value == keyword: self._advance() return self._recovered = True self._advance() def _parse_expression(self) -> Expression: expr = self._parse_assignment_expression() if self._at(JsTokenKind.COMMA): exprs = [expr] while self._eat(JsTokenKind.COMMA): exprs.append(self._parse_assignment_expression()) return JsSequenceExpression(expressions=exprs, offset=expr.offset) return expr def _parse_assignment_expression(self) -> Expression: """ An AssignmentExpression, which is the only production a YieldExpression is one of. Reading the `yield` here rather than among the primary expressions is what stops an operator from attaching to it: a `yield` that the line terminator restriction left without an argument ends the expression, and the slash that opens the next statement is not its divisor. """ if self._at(JsTokenKind.YIELD) and self._in_generator: return self._parse_yield_expression() left = self._parse_conditional_expression() if self._current.kind.is_assignment: op = self._advance().value right = self._parse_assignment_expression() left = self._to_param(left) if op == '=' else left return JsAssignmentExpression( left=left, operator=op, right=right, offset=left.offset) return left def _parse_conditional_expression(self) -> Expression: expr = self._parse_binary_expression() if self._eat(JsTokenKind.QUESTION): with self._with_no_in(False): consequent = self._parse_assignment_expression() if self._eat(JsTokenKind.COLON): alternate = self._parse_assignment_expression() else: # The colon a conditional requires is not here. Reading the alternate anyway would # spend `_expect`, which steps over whatever stands here to invent the colon — and # where that is the semicolon ending the statement, the next statement is read as # the alternate and the whole tail of the block is pulled into one expression. The # branch is left unwritten instead, so the boundary stays where it is and the # statement after the conditional is read as itself. self._recovered = True alternate = JsErrorNode(offset=self._current.offset, message='expected :') return JsConditionalExpression( test=expr, consequent=consequent, alternate=alternate, offset=expr.offset, ) return expr def _parse_binary_expression(self, min_prec: int = 0) -> Expression: left = self._parse_unary_expression() while True: entry = _BINARY_PREC.get(self._current.kind) if entry is None: break prec, logical = entry if prec < min_prec: break if self._no_in and self._at(JsTokenKind.IN): break op = self._advance().value next_prec = prec if prec == _PREC_EXPONENTIATION else prec + 1 right = self._parse_binary_expression(next_prec) node_type = JsLogicalExpression if logical else JsBinaryExpression left = node_type( left=left, operator=op, right=right, offset=left.offset) return left def _parse_unary_expression(self) -> Expression: if self._at( JsTokenKind.BANG, JsTokenKind.TILDE, JsTokenKind.TYPEOF, JsTokenKind.VOID, JsTokenKind.DELETE, ): tok = self._advance() operand = self._parse_unary_expression() return JsUnaryExpression( operator=tok.value, operand=operand, prefix=True, offset=tok.offset) if self._at(JsTokenKind.PLUS): tok = self._advance() operand = self._parse_unary_expression() return JsUnaryExpression( operator='+', operand=operand, prefix=True, offset=tok.offset) if self._at(JsTokenKind.MINUS): tok = self._advance() operand = self._parse_unary_expression() return JsUnaryExpression( operator='-', operand=operand, prefix=True, offset=tok.offset) if self._at(JsTokenKind.AWAIT) and self._in_async: tok = self._advance() operand = self._parse_unary_expression() return JsAwaitExpression(argument=operand, offset=tok.offset) return self._parse_update_expression() def _parse_update_expression(self) -> Expression: if self._at(JsTokenKind.INC, JsTokenKind.DEC): tok = self._advance() argument = self._parse_call_expression() return JsUpdateExpression( operator=tok.value, argument=argument, prefix=True, offset=tok.offset) expr = self._parse_call_expression() if not self._preceded_by_newline and self._at( JsTokenKind.INC, JsTokenKind.DEC, ): tok = self._advance() return JsUpdateExpression( operator=tok.value, argument=expr, prefix=False, offset=expr.offset) return expr def _parse_call_expression(self) -> Expression: """ A left-hand side expression: what may be called, indexed, or used to tag a template. An arrow function is none of those. It is an AssignmentExpression and never a LeftHandSideExpression, so nothing may attach to it, and the tail that would have attached belongs to whatever follows instead — `f = a => {}` on one line and `[x].forEach(g)` on the next are two statements, and reading the bracket as an index into the arrow makes them one. """ expr = self._parse_new_expression() if isinstance(expr, JsArrowFunctionExpression): return expr while True: if self._at(JsTokenKind.LPAREN): expr = self._parse_call_arguments(expr, optional=False) elif self._eat(JsTokenKind.DOT): prop = self._member_property() expr = JsMemberExpression( object=expr, property=prop, computed=False, optional=False, offset=expr.offset) elif self._at(JsTokenKind.LBRACKET): expr = JsMemberExpression( object=expr, property=self._parse_computed_member_key(), computed=True, optional=False, offset=expr.offset, ) elif self._at(JsTokenKind.QUESTION_DOT) or ( self._at(JsTokenKind.QUESTION) and self._peek_next().kind == JsTokenKind.DOT ): # A question mark whose next token is a dot is a `?.` the source split with # whitespace. That split is not a token the language has, and no well-formed program # reaches it: a conditional whose consequent opens with a dot opens with a number, # which the lexer reads as one float rather than a dot. Reading the two as the # optional-chain operator recovers the program the source meant, recorded as a # repair so the tree still reports that the file did not spell it whole. if self._eat(JsTokenKind.QUESTION_DOT) is None: self._recovered = True self._advance() self._advance() if self._at(JsTokenKind.LPAREN): expr = self._parse_call_arguments(expr, optional=True) elif self._at(JsTokenKind.LBRACKET): expr = JsMemberExpression( object=expr, property=self._parse_computed_member_key(), computed=True, optional=True, offset=expr.offset, ) else: prop = self._member_property() expr = JsMemberExpression( object=expr, property=prop, computed=False, optional=True, offset=expr.offset) elif self._at( JsTokenKind.TEMPLATE_FULL, JsTokenKind.TEMPLATE_HEAD, ): quasi = self._parse_template_literal() expr = JsTaggedTemplateExpression( tag=expr, quasi=quasi, offset=expr.offset) else: break return expr def _member_property(self) -> Expression: """ The name behind a dot. It is an IdentifierName, which is every word the language has and not only the ones that may be a variable, so `a.if` and `a.default` are ordinary member reads. Where the text behind the dot spells no word at all there is no name to build. An identifier with no name spells nothing — printing it writes the dot and stops, which is not a program — so what was read is handed back as itself instead. """ tok = self._advance() if tok.kind is JsTokenKind.PRIVATE_IDENTIFIER: return self._private_identifier(tok, tok.offset) if tok.kind is JsTokenKind.IDENTIFIER or tok.kind.is_keyword: return self._name_or_error(tok.value, tok.offset, may_be_reserved=True) return JsErrorNode( text=tok.value, message='expected a property name', offset=tok.offset) def _parse_computed_member_key(self) -> Expression: """ The expression between the brackets of a computed member read. Like a call's arguments it is written `[+In]`, so `for (t["k" in b] = "set"; ; )` is an ordinary head: the bracket ends the reach of the suppression the head applies to a relational operator standing directly in it. """ self._expect(JsTokenKind.LBRACKET) with self._with_no_in(False): key = self._parse_expression() self._expect(JsTokenKind.RBRACKET) return key def _parse_argument_list(self) -> list[Expression]: """ The arguments between the brackets of a call. Each is written `[+In]`, so `in` is an operator here however the call was reached: what a `for` head suppresses is a relational operator standing in the head itself, and a bracket the head encloses is a fresh expression again. Owning that here rather than at each call site is what keeps `for (new Set("k" in b); ; )` reading the same way `for (f("k" in b); ; )` does. """ args: list[Expression] = [] with self._with_no_in(False): while not self._at(JsTokenKind.RPAREN, JsTokenKind.EOF): if self._at(JsTokenKind.ELLIPSIS): offset = self._current.offset self._advance() arg = self._parse_assignment_expression() args.append(JsSpreadElement(argument=arg, offset=offset)) else: args.append(self._parse_assignment_expression()) if not self._at(JsTokenKind.RPAREN): self._expect(JsTokenKind.COMMA) self._expect(JsTokenKind.RPAREN) return args def _parse_call_arguments( self, callee: Expression, optional: bool, ) -> JsCallExpression: self._expect(JsTokenKind.LPAREN) args = self._parse_argument_list() return JsCallExpression( callee=callee, arguments=args, optional=optional, offset=callee.offset) def _parse_new_expression(self) -> Expression: if self._at(JsTokenKind.NEW): offset = self._current.offset self._advance() if self._at(JsTokenKind.DOT): self._advance() return JsMemberExpression( object=JsIdentifier(name='new', offset=offset), property=self._member_property(), computed=False, offset=offset, ) if self._at(JsTokenKind.ASYNC) and not self._at_async_function(): tok = self._advance() callee = self._name_or_error(tok.value, tok.offset, may_be_reserved=False) else: callee = self._parse_new_expression() while True: if self._eat(JsTokenKind.DOT): prop = self._member_property() callee = JsMemberExpression( object=callee, property=prop, computed=False, offset=callee.offset) elif self._at(JsTokenKind.LBRACKET): callee = JsMemberExpression( object=callee, property=self._parse_computed_member_key(), computed=True, offset=callee.offset, ) else: break args: list[Expression] = [] if self._at(JsTokenKind.LPAREN): self._advance() args = self._parse_argument_list() return JsNewExpression(callee=callee, arguments=args, offset=offset) return self._parse_primary_expression() def _parse_primary_expression(self) -> Expression: tok = self._current offset = tok.offset if self._at(JsTokenKind.ASYNC): return self._parse_async_expression() if self._at_binding_identifier(): self._advance() if self._at(JsTokenKind.ARROW) and not self._preceded_by_newline: self._advance() param = self._name_or_error(tok.value, offset, may_be_reserved=False) body = self._parse_arrow_body() return JsArrowFunctionExpression( params=[param], body=body, offset=offset) return self._name_or_error(tok.value, offset, may_be_reserved=False) if self._at(JsTokenKind.PRIVATE_IDENTIFIER): self._advance() return self._private_identifier(tok, offset) if self._at(JsTokenKind.IMPORT): return self._parse_import_expression(offset) if self._at(JsTokenKind.INTEGER): self._advance() raw = tok.value value = self._parse_int_text(raw.replace('_', '')) return JsNumericLiteral(value=value, raw=raw, offset=offset) if self._at(JsTokenKind.FLOAT): self._advance() raw = tok.value value = float(raw.replace('_', '')) return JsNumericLiteral(value=value, raw=raw, offset=offset) if self._at(JsTokenKind.BIGINT): self._advance() raw = tok.value value = self._parse_int_text(raw.replace('_', '').rstrip('n')) return JsBigIntLiteral(value=value, raw=raw, offset=offset) if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE): return self._parse_string_literal() if self._at(JsTokenKind.SLASH, JsTokenKind.SLASH_ASSIGN): tok = self._rescan_as_regexp() if self._at(JsTokenKind.REGEXP): self._advance() raw = tok.value last_slash = raw.rfind('/') pattern = raw[1:last_slash] flags = raw[last_slash + 1:] return JsRegExpLiteral( pattern=pattern, flags=flags, raw=raw, offset=offset) if self._at(JsTokenKind.TEMPLATE_FULL, JsTokenKind.TEMPLATE_HEAD): return self._parse_template_literal() if self._at(JsTokenKind.TRUE): self._advance() return JsBooleanLiteral(value=True, offset=offset) if self._at(JsTokenKind.FALSE): self._advance() return JsBooleanLiteral(value=False, offset=offset) if self._at(JsTokenKind.NULL): self._advance() return JsNullLiteral(offset=offset) if self._at(JsTokenKind.THIS): self._advance() return JsThisExpression(offset=offset) if self._at(JsTokenKind.SUPER): self._advance() return JsIdentifier(name='super', offset=offset) if self._at(JsTokenKind.LBRACKET): return self._parse_array_literal() if self._at(JsTokenKind.LBRACE): return self._parse_object_literal() if self._at(JsTokenKind.LPAREN): return self._parse_paren_or_arrow() if self._at(JsTokenKind.FUNCTION): return self._parse_function_expression() if self._at(JsTokenKind.CLASS): return self._parse_class_expression() self._advance() return JsErrorNode(text=tok.value, message='unexpected token', offset=offset) def _parse_string_literal(self) -> JsStringLiteral: tok = self._advance() raw = tok.value end = len(raw) - 1 if tok.terminated else len(raw) return JsStringLiteral( value=decode_js_string_body(raw[1:end]), raw=raw, terminated=tok.terminated, offset=tok.offset, ) @staticmethod def _template_element(tok: JsToken, tail: bool) -> JsTemplateElement: """ One run of text of a template literal, taken from the token without the delimiters around it: one character opens every run, and the one that ends it is a backtick where the run ends the literal and `${` where a hole follows. The text between them is cooked into what it denotes, exactly as the body of a string literal is — a template that carries an escape means what the escape means, and reading it as the characters that spell it is how a `\\t` became two. """ raw = tok.value end = len(raw) - (1 if tail else 2) if tok.terminated else len(raw) text = raw[1:end] return JsTemplateElement( value=decode_js_template_body(text), raw=text, tail=tail, terminated=tok.terminated, offset=tok.offset, ) def _parse_template_literal(self) -> JsTemplateLiteral: offset = self._current.offset quasis: list[JsTemplateElement] = [] expressions: list[Expression] = [] if self._at(JsTokenKind.TEMPLATE_FULL): quasis.append(self._template_element(self._advance(), True)) return JsTemplateLiteral( quasis=quasis, expressions=expressions, offset=offset) quasis.append(self._template_element(self._advance(), False)) while True: with self._with_no_in(False): expressions.append(self._parse_expression()) if self._at(JsTokenKind.TEMPLATE_TAIL): quasis.append(self._template_element(self._advance(), True)) break elif self._at(JsTokenKind.TEMPLATE_MIDDLE): quasis.append(self._template_element(self._advance(), False)) else: quasis.append(JsTemplateElement( value='', raw='', tail=True, terminated=False, offset=self._current.offset, )) break return JsTemplateLiteral( quasis=quasis, expressions=expressions, offset=offset) def _parse_array_literal(self) -> JsArrayExpression: with self._with_no_in(False): offset = self._current.offset self._expect(JsTokenKind.LBRACKET) elements: list[Expression | None] = [] while not self._at(JsTokenKind.RBRACKET, JsTokenKind.EOF): if self._at(JsTokenKind.COMMA): elements.append(None) self._advance() continue if self._at(JsTokenKind.ELLIPSIS): so = self._current.offset self._advance() arg = self._parse_assignment_expression() elements.append(JsSpreadElement(argument=arg, offset=so)) else: elements.append(self._parse_assignment_expression()) if not self._at(JsTokenKind.RBRACKET): self._require(JsTokenKind.COMMA) self._expect(JsTokenKind.RBRACKET) return JsArrayExpression(elements=elements, offset=offset) def _parse_object_literal(self) -> JsObjectExpression: with self._with_no_in(False): offset = self._current.offset self._expect(JsTokenKind.LBRACE) properties: list[JsProperty | JsSpreadElement] = [] while not self._at(JsTokenKind.RBRACE, JsTokenKind.EOF): if self._at(JsTokenKind.ELLIPSIS): so = self._current.offset self._advance() arg = self._parse_assignment_expression() properties.append(JsSpreadElement(argument=arg, offset=so)) else: properties.append(self._parse_object_property()) if not self._at(JsTokenKind.RBRACE): self._require(JsTokenKind.COMMA) self._expect(JsTokenKind.RBRACE) return JsObjectExpression(properties=properties, offset=offset) def _parse_object_property(self) -> JsProperty: offset = self._current.offset is_generator = bool(self._eat(JsTokenKind.STAR)) if ( self._at(JsTokenKind.IDENTIFIER) and self._current.value in ('get', 'set') and not is_generator ): kind_val = _PROP_KIND_MAP[self._current.value] saved = self._current self._advance() if self._at(JsTokenKind.LPAREN): key = JsIdentifier(name=saved.value, offset=saved.offset) return self._make_method_property(key, JsPropertyKind.INIT, False, offset) if self._at( JsTokenKind.COLON, JsTokenKind.COMMA, JsTokenKind.RBRACE, JsTokenKind.EQUALS, ): key = JsIdentifier(name=saved.value, offset=saved.offset) return self._finish_property_value(key, False, offset) key, computed = self._parse_property_key() return self._make_method_property(key, kind_val, False, offset, computed=computed) if self._at(JsTokenKind.ASYNC) and not is_generator: saved = self._current self._advance() if self._preceded_by_newline or self._at( JsTokenKind.COLON, JsTokenKind.COMMA, JsTokenKind.RBRACE, JsTokenKind.EQUALS, JsTokenKind.LPAREN, ): if self._at(JsTokenKind.LPAREN): key = JsIdentifier(name='async', offset=saved.offset) return self._make_method_property(key, JsPropertyKind.INIT, False, offset) key = JsIdentifier(name='async', offset=saved.offset) return self._finish_property_value(key, False, offset) gen = bool(self._eat(JsTokenKind.STAR)) key, computed = self._parse_property_key() return self._make_method_property( key, JsPropertyKind.INIT, gen, offset, computed=computed, is_async=True) key, computed = self._parse_property_key() if is_generator or self._at(JsTokenKind.LPAREN): return self._make_method_property(key, JsPropertyKind.INIT, is_generator, offset, computed=computed) return self._finish_property_value(key, computed, offset) def _finish_property_value( self, key: Expression, computed: bool, offset: int, ) -> JsProperty: if self._eat(JsTokenKind.COLON): value = self._parse_assignment_expression() return JsProperty( key=key, value=value, computed=computed, shorthand=False, offset=offset) if not computed and self._eat(JsTokenKind.EQUALS): right = self._parse_assignment_expression() value = JsAssignmentPattern(left=key, right=right, offset=key.offset) return JsProperty( key=key, value=value, computed=computed, shorthand=True, offset=offset) return JsProperty( key=key, value=key, computed=computed, shorthand=True, offset=offset) def _make_method_property( self, key: Expression, kind: JsPropertyKind, is_generator: bool, offset: int, computed: bool = False, is_async: bool = False, ) -> JsProperty: func_offset = self._current.offset with self._function_body_context(is_async, is_generator): params = self._parse_formal_parameters() body = self._parse_block_statement() value = JsFunctionExpression( params=params, body=body, generator=is_generator, is_async=is_async, offset=func_offset) return JsProperty( key=key, value=value, computed=computed, shorthand=False, method=True, kind=kind, offset=offset) def _parse_property_key(self) -> tuple[Expression, bool]: if self._at(JsTokenKind.LBRACKET): self._advance() key = self._parse_assignment_expression() self._expect(JsTokenKind.RBRACKET) return key, True return self._parse_property_name(), False def _parse_property_name(self) -> Expression: tok = self._current if self._at(JsTokenKind.INTEGER, JsTokenKind.FLOAT): self._advance() raw = tok.value text = raw.replace('_', '') return JsNumericLiteral( value=float(text) if tok.kind == JsTokenKind.FLOAT else self._parse_int_text(text), raw=raw, offset=tok.offset, ) if self._at(JsTokenKind.STRING_SINGLE, JsTokenKind.STRING_DOUBLE): return self._parse_string_literal() if self._at(JsTokenKind.PRIVATE_IDENTIFIER): self._advance() return self._private_identifier(tok, tok.offset) self._advance() return self._name_or_error(tok.value, tok.offset, may_be_reserved=True) def _parse_paren_or_arrow(self, is_async: bool = False) -> Expression: """ What ECMA-262 calls `CoverParenthesizedExpressionAndArrowParameterList`: a bracketed list that the token behind the closing bracket decides the reading of, because nothing inside it does. It is read as a list of assignment expressions either way and only then converted, which is what lets one pass read a head no expression grammar accepts. Three of its shapes belong to the parameter reading alone and are not expressions at all — the empty list, a rest element, and a trailing comma — so a list holding one of them is an arrow head or it is nothing. The rest element in particular may only stand last, and reading it as one of the list rather than as a case of its own is the whole difference between `(...a) => a` and `(b, ...a) => a`. Where such a list has no arrow behind it, the head stands with nothing to give its parameters, and what is missing is recorded where the body would be. Demanding the arrow instead consumes whatever does stand there — the semicolon ending the statement, say — and the body then reads the statement behind it, so `x = (a,); y = 2;` would take the second line into a function nobody wrote and leave nothing to say that it had. """ with self._with_no_in(False): offset = self._current.offset self._expect(JsTokenKind.LPAREN) items: list[Expression] = [] head_only = True while not self._at(JsTokenKind.RPAREN, JsTokenKind.EOF): if self._at(JsTokenKind.ELLIPSIS): items.append(self._parse_rest_element()) head_only = True break items.append(self._parse_assignment_expression()) head_only = False if not self._eat(JsTokenKind.COMMA): break head_only = self._at(JsTokenKind.RPAREN) self._expect(JsTokenKind.RPAREN) if self._at(JsTokenKind.ARROW) and not self._preceded_by_newline: self._advance() body = self._parse_arrow_body(is_async) elif head_only: body = JsErrorNode( text='', message='a parameter list with no arrow behind it', offset=self._current.offset, ) else: expression = items[0] if len(items) == 1 else JsSequenceExpression( expressions=items, offset=offset) return JsParenthesizedExpression(expression=expression, offset=offset) return JsArrowFunctionExpression( params=[self._to_param(item) for item in items], body=body, offset=offset, ) def _parse_arrow_body(self, is_async: bool = False) -> Expression | JsBlockStatement: with self._function_body_context(is_async, False): if self._at(JsTokenKind.LBRACE): return self._parse_block_statement() return self._parse_assignment_expression() def _to_param(self, expr: Expression) -> Expression: if isinstance(expr, JsIdentifier): return expr if isinstance(expr, JsAssignmentExpression) and expr.operator == '=': return JsAssignmentPattern( left=self._to_param(expr.left), right=expr.right, offset=expr.offset, ) if isinstance(expr, JsSpreadElement): return JsRestElement(argument=self._to_param(expr.argument), offset=expr.offset) if isinstance(expr, JsArrayExpression): elements = [ self._to_param(e) if e is not None else None for e in expr.elements ] return JsArrayPattern(elements=elements, offset=expr.offset) if isinstance(expr, JsObjectExpression): props: list[JsProperty | JsRestElement] = [] for p in expr.properties: if isinstance(p, JsSpreadElement): props.append(JsRestElement( argument=self._to_param(p.argument), offset=p.offset)) else: props.append(p) return JsObjectPattern(properties=props, offset=expr.offset) return expr def _parse_function_expression(self) -> JsFunctionExpression: return self._parse_function_impl(as_expression=True) def _parse_class_expression(self) -> JsClassExpression: return self._parse_class_impl(as_expression=True) def _parse_async_expression(self) -> Expression: offset = self._current.offset self._advance() return self._parse_expression_starting_with_async(offset) def _parse_expression_starting_with_async(self, offset: int) -> Expression: if not self._preceded_by_newline: if self._at(JsTokenKind.FUNCTION): return self._parse_function_impl(as_expression=True, is_async=True) if self._at(JsTokenKind.ARROW): self._advance() param = JsIdentifier(name='async', offset=offset) body = self._parse_arrow_body(False) return JsArrowFunctionExpression( params=[param], body=body, is_async=False, offset=offset) if ( self._at_binding_identifier() and self._peek_next().kind == JsTokenKind.ARROW and not self._ahead_newline ): tok = self._advance() self._advance() param = self._name_or_error(tok.value, tok.offset, may_be_reserved=False) body = self._parse_arrow_body(True) return JsArrowFunctionExpression( params=[param], body=body, is_async=True, offset=offset) if self._at(JsTokenKind.LPAREN): self._advance() args = self._parse_argument_list() if self._at(JsTokenKind.ARROW) and not self._preceded_by_newline: self._advance() params = [self._to_param(arg) for arg in args] body = self._parse_arrow_body(True) return JsArrowFunctionExpression( params=params, body=body, is_async=True, offset=offset) return JsCallExpression( callee=JsIdentifier(name='async', offset=offset), arguments=args, optional=False, offset=offset, ) return JsIdentifier(name='async', offset=offset) def _parse_yield_expression(self) -> JsYieldExpression: """ A YieldExpression, whose one line terminator restriction sits between the `yield` and what follows it. A newline there ends the expression, so neither a `*` nor an argument can still belong to it; a newline anywhere after the `*` is ordinary whitespace, and the argument is read across it. A token that closes the construct the `yield` stands in is not an argument, and the hole of a template is closed by the text that resumes it rather than by a brace of its own. """ offset = self._current.offset self._advance() if self._preceded_by_newline: return JsYieldExpression(argument=None, delegate=False, offset=offset) delegate = self._eat(JsTokenKind.STAR) is not None argument = None if not self._at( JsTokenKind.SEMICOLON, JsTokenKind.RBRACE, JsTokenKind.RPAREN, JsTokenKind.RBRACKET, JsTokenKind.COMMA, JsTokenKind.COLON, JsTokenKind.TEMPLATE_MIDDLE, JsTokenKind.TEMPLATE_TAIL, JsTokenKind.EOF, ): argument = self._parse_assignment_expression() return JsYieldExpression( argument=argument, delegate=delegate, offset=offset)Methods
def parse(self)-
Expand source code Browse git
def parse(self) -> JsScript: script = self._parse_program() mark_directives(script) mark_module(script) return script