use crate::Goal;
use crate::ast::*;
use crate::lexer::escapes::{LegacyEscape, find_legacy_escape};
use crate::lexer::{KeywordKind, Lexer, LexerCheckpoint, Token, TokenKind, is_es_line_terminator};
use crate::printer::ParenContext;
use bumpalo::Bump;
use bumpalo::collections::Vec as BumpVec;
use std::collections::BTreeSet;
use std::ops::ControlFlow;
use tsv_lang::{DepthBudget, DepthLimit, Error, Span, SpineCount};
mod expression;
mod expression_arrow; mod expression_assignable; mod expression_braced_operand; mod expression_literals; mod expression_lookahead; mod expression_template; mod expression_type_args; mod parameters; mod scan; mod statement; mod type_members; mod types;
pub(crate) use expression::is_jsdoc_type_cast_comment;
pub(crate) use expression_braced_operand::closes_braced_operand;
pub(crate) use expression_type_args::{closes_statement_header, closes_type_arguments};
#[expect(rustdoc::private_intra_doc_links)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TopLevelAs {
Assertion,
HostSeparator,
}
fn comment_from_token(
source: &str,
token_start: usize,
token_end: usize,
content_start: usize,
is_block: bool,
base_offset: usize,
) -> (Comment, bool) {
let content_end = if is_block { token_end - 2 } else { token_end };
let content = &source[content_start..content_end];
let multiline = Comment::content_is_multiline(is_block, content);
let has_line_terminator =
is_block && (multiline || content.contains(|c| is_es_line_terminator(c) && c != '\n'));
let comment = Comment {
content_span: Span::new(
(content_start + base_offset) as u32,
(content_end + base_offset) as u32,
),
is_block,
multiline,
span: Span::new(
(token_start + base_offset) as u32,
(token_end + base_offset) as u32,
),
from_template_reader: false,
owned_by_node: false,
};
comment.debug_assert_span_len();
(comment, has_line_terminator)
}
#[derive(Clone, Copy, Default)]
struct LtRegion {
floor: u32,
lt_pos: usize,
}
#[derive(Clone, Copy, Default)]
struct LtRegions {
len: u8,
open: [LtRegion; Self::CAP],
}
impl LtRegions {
const CAP: usize = 3;
#[inline]
fn open(&mut self, region: LtRegion) {
if let Some(slot) = self.open.get_mut(usize::from(self.len)) {
*slot = region;
self.len += 1;
}
}
#[inline]
fn close_above(&mut self, depth: u32) {
while self.len > 0 && self.open[usize::from(self.len) - 1].floor > depth {
self.len -= 1;
}
}
#[inline]
fn reanchor_above(&mut self, depth: u32) {
for region in &mut self.open[..usize::from(self.len)] {
region.floor = region.floor.min(depth);
}
}
#[inline]
fn open_before(&self, operand_start: usize) -> usize {
self.open[..usize::from(self.len)]
.iter()
.take_while(|region| region.lt_pos < operand_start)
.count()
}
}
#[derive(Clone, Copy)]
pub(super) struct GroupingParen {
pub(super) inner: Span,
pub(super) open: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum RootEnd {
Anywhere,
BeforeAnyButComma,
ForClause,
AtParenClose,
}
pub(super) struct StatementFrame {
outer: SpineCount,
statements: SpineCount,
closed: usize,
outer_peak: u32,
peak: u32,
}
impl StatementFrame {
#[inline]
pub(super) fn next(&mut self, parser: &mut Parser<'_, '_>) {
self.closed += 1;
if self.closed > ListFrame::SUMMED || parser.spine.runs_long() {
self.statements = self.statements.max(parser.spine.take());
}
if parser.depth.is_deep() {
self.peak = self.peak.max(parser.depth.take_peak(parser.grouping_depth));
}
}
}
pub(super) struct ListFrame {
greatest: SpineCount,
peak: u32,
}
impl ListFrame {
const SUMMED: usize = 8;
#[inline]
pub(super) fn element(&mut self, parser: &mut Parser<'_, '_>, index: usize) {
if index >= Self::SUMMED {
self.greatest = self.greatest.max(parser.spine.take());
self.peak = self.peak.max(parser.depth.take_peak(parser.grouping_depth));
}
}
#[inline]
pub(super) fn close(self, parser: &mut Parser<'_, '_>) {
if !self.greatest.is_empty() {
parser.spine.fold(self.greatest);
}
if self.peak > 0 {
parser.depth.fold_peak(self.peak);
}
}
}
pub(super) const LINK_OPERATOR: u32 = 1;
pub(super) const LINK_SUBSCRIPT: u32 = 1;
pub(super) const LINK_NON_NULL: u32 = 2;
pub(super) const LINK_REENTRY: u32 = 10;
pub(super) const LINK_TAG: u32 = 14;
pub(super) const LINK_ASSERTION: u32 = 8;
pub(super) const LINK_TYPE_SUFFIX: u32 = 5;
const SPINE_WALK_LINKS: u32 = 4;
#[derive(Clone, Copy, Default)]
struct SpineMemo {
whole_at: usize,
whole: bool,
call_at: usize,
call_units: u32,
}
#[expect(clippy::inline_always)]
#[inline(always)]
fn call_spine_step<'e>(inner: &'e Expression<'e>) -> ControlFlow<u32, &'e Expression<'e>> {
tsv_lang::fanout_probe::predicate();
match &inner.kind {
ExpressionKind::TSNonNullExpression(n) => ControlFlow::Continue(n.expression),
ExpressionKind::TSInstantiationExpression(i) => ControlFlow::Continue(i.expression),
ExpressionKind::CallExpression(_) | ExpressionKind::TaggedTemplateExpression(_) => {
ControlFlow::Break(LINK_REENTRY)
}
_ => ControlFlow::Break(LINK_SUBSCRIPT),
}
}
#[derive(Clone)]
pub(super) struct Checkpoint<'arena> {
lexer: LexerCheckpoint,
current: Token,
current_decoded: Option<&'arena str>,
peek: Option<Token>,
peek_decoded: Option<&'arena str>,
had_line_terminator: bool,
peek_had_line_terminator: bool,
prev_end: usize,
lexer_error: Option<Error>,
comments_len: usize,
grouping_depth: u32,
depth: DepthBudget,
spine: SpineCount,
lt_regions: LtRegions,
abandoned_type_arguments_error: Option<Error>,
}
#[expect(clippy::struct_excessive_bools)]
pub(crate) struct Parser<'a, 'arena> {
arena: &'arena Bump,
source: &'a str,
lexer: Lexer<'a>,
current: Token,
current_decoded: Option<&'arena str>,
peek: Option<Token>,
peek_decoded: Option<&'arena str>,
base_offset: usize, comments: BumpVec<'arena, Comment>,
had_line_terminator: bool,
prev_end: usize,
top_level_as_is_assertion: bool,
grouping_depth: u32,
pub(crate) depth: DepthBudget,
spine: SpineCount,
spine_memo: SpineMemo,
pub(crate) preserve_parens: bool,
pub(crate) preserved_a_paren: bool,
record_grouping_parens: bool,
grouping_parens: Vec<GroupingParen>,
in_ambient_context: bool,
lexer_error: Option<Error>,
peek_had_line_terminator: bool,
allow_in: bool,
no_in_depth: u32,
in_for_header: bool,
pair_free_root: Option<(usize, RootEnd)>,
arrow_return_type_barred_at: Option<u32>,
lt_regions: LtRegions,
lt_regions_statement_base: LtRegions,
abandoned_type_argument_lists: BTreeSet<(u32, u8)>,
type_argument_lists_kept: BTreeSet<(u32, u8)>,
tsc_followers: bool,
gave_up_list_to_tsc_follower: bool,
kept_list_past_tsc_follower: bool,
abandoned_type_arguments_error: Option<Error>,
decorated_parameter_properties: Vec<u32>,
goal: Goal,
in_await: bool,
in_await_if_module: bool,
await_name_operand: Option<usize>,
strict: bool,
in_yield: bool,
fn_type_disallowed: bool,
conditional_type_disallowed: bool,
pending_conditional_extends: Option<&'arena TSType<'arena>>,
}
impl<'a, 'arena> Parser<'a, 'arena> {
#[inline]
fn alloc<T>(&self, val: T) -> &'arena T {
self.arena.alloc(val)
}
#[inline]
fn bvec<T>(&self) -> BumpVec<'arena, T> {
BumpVec::new_in(self.arena)
}
#[inline]
fn decoded_to_arena(&self) -> Option<&'arena str> {
if self.lexer.decoded_str().is_some() {
self.copy_decoded_to_arena()
} else {
None
}
}
#[cold]
#[inline(never)]
fn copy_decoded_to_arena(&self) -> Option<&'arena str> {
self.lexer
.decoded_str()
.map(|s| -> &'arena str { self.arena.alloc_str(s) })
}
#[inline]
fn typed_extra(&self, ta: TSTypeAnnotation<'arena>) -> &'arena IdentifierParamExtra<'arena> {
self.alloc(IdentifierParamExtra {
type_annotation: Some(ta),
decorators: None,
})
}
#[inline(never)]
fn new(source: &'a str, base_offset: usize, goal: Goal, arena: &'arena Bump) -> Self {
Self {
arena,
source,
lexer: Lexer::at_offset(source, base_offset),
current: Token {
kind: TokenKind::Eof,
start: 0,
end: 0,
},
current_decoded: None,
peek: None,
peek_decoded: None,
base_offset,
comments: BumpVec::new_in(arena),
had_line_terminator: false, prev_end: 0,
top_level_as_is_assertion: true, grouping_depth: 0, depth: DepthBudget::at(0), spine: SpineCount::default(), spine_memo: SpineMemo::default(), preserve_parens: false, preserved_a_paren: false, record_grouping_parens: false, grouping_parens: Vec::new(), in_ambient_context: false, lexer_error: None, peek_had_line_terminator: false, allow_in: true, in_for_header: false,
pair_free_root: None,
no_in_depth: 0, arrow_return_type_barred_at: None, lt_regions: LtRegions::default(), lt_regions_statement_base: LtRegions::default(), abandoned_type_argument_lists: BTreeSet::new(), type_argument_lists_kept: BTreeSet::new(), tsc_followers: true, gave_up_list_to_tsc_follower: false, kept_list_past_tsc_follower: false, abandoned_type_arguments_error: None,
decorated_parameter_properties: Vec::new(), goal,
in_await: matches!(goal, Goal::Module),
in_await_if_module: true,
await_name_operand: None,
strict: matches!(goal, Goal::Module),
in_yield: false,
fn_type_disallowed: false, conditional_type_disallowed: false, pending_conditional_extends: None,
}
}
pub(crate) fn prime(&mut self) -> Result<(), Error> {
self.lexer.next_token_into(&mut self.current)?;
self.current_decoded = self.decoded_to_arena();
self.collect_comments()?;
self.had_line_terminator = false;
Ok(())
}
pub(super) fn advance(&mut self) -> Result<(), Error> {
if let Some(err) = self.lexer_error.take() {
return Err(err);
}
self.advance_inner()
}
fn advance_inner(&mut self) -> Result<(), Error> {
self.prev_end = self.current.end as usize;
if let Some(peek) = self.peek.take() {
self.current = peek;
self.current_decoded = self.peek_decoded.take();
self.had_line_terminator = self.peek_had_line_terminator;
} else {
self.lexer.next_token_into(&mut self.current)?;
self.current_decoded = self.decoded_to_arena();
self.had_line_terminator = self.lexer.had_line_terminator();
}
self.collect_comments()
}
#[inline]
pub(super) fn collect_comments(&mut self) -> Result<(), Error> {
if matches!(self.current.kind, TokenKind::Comment { .. }) {
self.drain_comments()
} else {
Ok(())
}
}
#[cold]
#[inline(never)]
fn drain_comments(&mut self) -> Result<(), Error> {
while let TokenKind::Comment {
is_block,
content_start,
} = &self.current.kind
{
let (comment, has_line_terminator) = comment_from_token(
self.source,
self.current.start as usize,
self.current.end as usize,
*content_start as usize,
*is_block,
self.base_offset,
);
if has_line_terminator {
self.had_line_terminator = true;
}
self.comments.push(comment);
let token = self.lexer.next_token()?;
self.update_current(token);
if self.lexer.had_line_terminator() {
self.had_line_terminator = true;
}
}
Ok(())
}
#[inline]
pub(super) fn current_kind(&self) -> &TokenKind {
&self.current.kind
}
#[inline]
pub(super) fn update_current(&mut self, token: Token) {
self.current = token;
self.current_decoded = self.decoded_to_arena();
}
#[inline]
pub(super) fn current_pos(&self) -> (usize, usize) {
(
self.current.start as usize + self.base_offset,
self.current.end as usize + self.base_offset,
)
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn nested<T>(
&mut self,
units: u32,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
if !self.depth.try_enter_beside(units, self.grouping_depth) {
return Err(self.too_deep(DepthLimit::Nesting));
}
let result = f(self);
self.depth.leave(units);
result
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn chain<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let outer = self.spine.take();
let result = f(self);
self.spine.fold(outer);
result
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn statement_list<T>(
&mut self,
levels: u32,
f: impl FnOnce(&mut Self, &mut StatementFrame) -> Result<T, Error>,
) -> Result<T, Error> {
self.nested(levels, |p| {
let mut frame = StatementFrame {
outer: p.spine.take(),
statements: SpineCount::default(),
closed: 0,
outer_peak: p.depth.take_peak(p.grouping_depth),
peak: 0,
};
let result = f(p, &mut frame);
frame.next(p);
p.spine.fold(frame.outer.max(frame.statements));
p.depth.fold_peak(frame.outer_peak.max(frame.peak));
result
})
}
#[inline]
#[expect(clippy::unused_self)]
pub(super) fn list_frame(&self) -> ListFrame {
ListFrame {
greatest: SpineCount::default(),
peak: 0,
}
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn spine_link(&mut self, units: u32) -> Result<(), Error> {
if self.spine.try_link(units, &self.depth) {
Ok(())
} else {
Err(self.too_deep(DepthLimit::Spine))
}
}
#[inline]
pub(super) fn printer_pair(&mut self) -> Result<(), Error> {
self.spine
.try_pair(&self.depth)
.map_err(|limit| self.too_deep(limit))
}
#[inline]
pub(super) fn level_above_operand(&mut self, operand_peak: u32) -> Result<(), Error> {
self.depth
.try_raise_peak(operand_peak + 1, &self.spine)
.map_err(|limit| self.too_deep(limit))
}
#[inline]
pub(super) fn pair_free_root<T>(
&mut self,
end: RootEnd,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let outer = self.pair_free_root.replace((self.current_pos().0, end));
let result = f(self);
self.pair_free_root = outer;
result
}
#[inline]
pub(super) fn self_pair(&mut self, start: usize) -> Result<(), Error> {
let whole_root = self.pair_free_root.is_some_and(|(root, end)| {
root == start
&& match end {
RootEnd::Anywhere => true,
RootEnd::BeforeAnyButComma | RootEnd::ForClause => {
self.current.kind != TokenKind::Comma
}
RootEnd::AtParenClose => self.current.kind == TokenKind::ParenClose,
}
});
if whole_root {
Ok(())
} else {
self.printer_pair()
}
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn count_operand_pairs(
&mut self,
left: Option<(&Expression<'_>, ParenContext)>,
right: Option<(&Expression<'_>, ParenContext)>,
) -> Result<(), Error> {
for (operand, ctx) in [left, right].into_iter().flatten() {
if crate::printer::printer_adds_operand_pair(operand, ctx, self.in_for_header) {
self.printer_pair()?;
}
}
Ok(())
}
#[inline(never)]
pub(super) fn binary_operand_pairs(
&mut self,
operator: BinaryOperator,
left: &Expression<'_>,
right: &Expression<'_>,
expr_start: usize,
) -> Result<(), Error> {
let left_bare = expr_start == left.span().start as usize;
let right_bare = right.span().end as usize == self.prev_token_end();
self.count_operand_pairs(
left_bare.then_some((
left,
ParenContext::BinaryLeft {
parent_op: operator,
},
)),
right_bare.then_some((
right,
ParenContext::BinaryRight {
parent_op: operator,
},
)),
)
}
#[inline]
pub(super) fn sequence_pair(&mut self, start: usize) -> Result<(), Error> {
let whole_root = self.pair_free_root.is_some_and(|(root, end)| {
root == start
&& match end {
RootEnd::ForClause => true,
RootEnd::AtParenClose => self.current.kind == TokenKind::ParenClose,
RootEnd::Anywhere | RootEnd::BeforeAnyButComma => false,
}
});
if whole_root {
Ok(())
} else {
self.printer_pair()
}
}
#[inline]
pub(super) fn count_prefix_operand_pairs(
&mut self,
operand: &Expression<'_>,
ctx: ParenContext,
operand_token: usize,
holder: Option<(usize, usize)>,
shell: bool,
) -> Result<(), Error> {
if !shell
&& ctx.is_operator_position()
&& !matches!(
operand.kind,
ExpressionKind::AssignmentExpression(_)
| ExpressionKind::SequenceExpression(_)
| ExpressionKind::ParenthesizedExpression(_)
)
&& crate::printer::operator_operand_takes_no_pair(operand)
&& holder.is_none_or(|(operator_end, _)| {
self.comments
.last()
.is_none_or(|c| (c.span.start as usize) < operator_end)
})
{
return Ok(());
}
self.count_prefix_operand_pairs_by_rule(operand, ctx, operand_token, holder, shell)
}
#[inline(never)]
fn count_prefix_operand_pairs_by_rule(
&mut self,
operand: &Expression<'_>,
ctx: ParenContext,
operand_token: usize,
holder: Option<(usize, usize)>,
shell: bool,
) -> Result<(), Error> {
let mut operand = operand;
while let ExpressionKind::ParenthesizedExpression(paren) = &operand.kind {
operand = paren.expression;
}
let span = operand.span();
let bare = span.start as usize == operand_token;
let commented = holder.is_some_and(|(operator_end, node_end)| {
self.has_comment_between(operator_end, span.start as usize)
|| self.has_comment_between(span.end as usize, node_end)
});
let position_pair = (!bare
&& matches!(
operand.kind,
ExpressionKind::AssignmentExpression(_) | ExpressionKind::SequenceExpression(_)
))
|| crate::printer::printer_adds_operand_pair(operand, ctx, self.in_for_header);
let written = u32::from(position_pair) + u32::from(commented) + u32::from(shell);
if written == 0 {
return Ok(());
}
let authored = if bare {
0
} else {
self.authored_pairs(operand_token, span.start as usize)
};
for _ in authored..written {
self.printer_pair()?;
}
Ok(())
}
pub(super) fn authored_pairs(&self, from: usize, to: usize) -> u32 {
let bytes = self.source.as_bytes();
let mut comment = self
.comments
.partition_point(|c| (c.span.start as usize) < from);
let mut pos = from;
let mut pairs = 0;
while pos < to {
if let Some(c) = self.comments.get(comment)
&& c.span.start as usize == pos
{
pos = c.span.end as usize;
comment += 1;
continue;
}
pairs += u32::from(bytes[pos - self.base_offset] == b'(');
pos += 1;
}
pairs
}
#[inline]
pub(super) fn restricted_argument_pair(
&mut self,
argument: &Expression<'_>,
argument_token: usize,
) -> Result<(), Error> {
if matches!(argument.kind, ExpressionKind::BinaryExpression(_))
&& argument.span().start as usize == argument_token
{
self.printer_pair()
} else {
Ok(())
}
}
#[inline(never)]
pub(super) fn conditional_head_pairs(
&mut self,
test: &Expression<'_>,
expr_start: usize,
consequent: &Expression<'_>,
consequent_start: usize,
) -> Result<(), Error> {
if matches!(consequent.kind, ExpressionKind::ConditionalExpression(_))
&& consequent.span().start as usize == consequent_start
{
self.printer_pair()?;
}
if test.span().start as usize == expr_start && crate::printer::ternary_test_takes_pair(test)
{
self.printer_pair()?;
}
self.ternary_branch_pair(consequent, consequent_start)
}
#[inline]
pub(super) fn ternary_branch_pair(
&mut self,
branch: &Expression<'_>,
branch_token: usize,
) -> Result<(), Error> {
if branch.span().start as usize == branch_token
&& crate::printer::ternary_branch_takes_pair(branch)
{
self.printer_pair()
} else {
Ok(())
}
}
#[inline]
pub(super) fn function_head_pair(
&mut self,
head: &Expression<'_>,
start: u32,
ctx: ParenContext,
) -> Result<(), Error> {
if matches!(
head.kind,
ExpressionKind::FunctionExpression(_) | ExpressionKind::ClassExpression(_)
) && head.span().start == start
&& crate::printer::printer_adds_operand_pair(head, ctx, self.in_for_header)
{
self.printer_pair()
} else {
Ok(())
}
}
#[inline]
pub(super) fn arrow_return_pair(
&mut self,
return_type: Option<&TSTypeAnnotation<'_>>,
) -> Result<(), Error> {
if return_type.is_some_and(|r| matches!(r.type_annotation, TSType::Function(_))) {
self.printer_pair()
} else {
Ok(())
}
}
#[inline]
pub(super) fn is_bare_instantiation(operand: &Expression<'_>, start: u32) -> bool {
matches!(operand.kind, ExpressionKind::TSInstantiationExpression(_))
&& operand.span().start == start
}
#[inline]
pub(super) fn chain_base_pair(
&mut self,
object: &Expression<'_>,
start: u32,
) -> Result<(), Error> {
if matches!(object.kind, ExpressionKind::Literal(_))
&& object.span().start == start
&& crate::printer::printer_parenthesizes_chain_base(object)
{
self.printer_pair()
} else {
Ok(())
}
}
#[inline]
fn has_comment_between(&self, from: usize, to: usize) -> bool {
if to.saturating_sub(from) < Comment::MIN_SPAN_LEN as usize
|| self
.comments
.last()
.is_none_or(|c| (c.span.start as usize) < from)
{
return false;
}
let i = self
.comments
.partition_point(|c| (c.span.start as usize) < from);
self.comments
.get(i)
.is_some_and(|c| (c.span.start as usize) < to)
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn call_link_units(&mut self, operand: &Expression<'_>) -> u32 {
let mut inner = operand;
let mut links = SPINE_WALK_LINKS;
loop {
match call_spine_step(inner) {
ControlFlow::Continue(next) => inner = next,
ControlFlow::Break(units) => return units,
}
links -= 1;
if links == 0 {
return self.call_units_past(inner);
}
}
}
#[inline(never)]
fn call_units_past(&mut self, cut: &Expression<'_>) -> u32 {
let mut inner = cut;
let units = loop {
if std::ptr::from_ref(inner).addr() == self.spine_memo.call_at {
break self.spine_memo.call_units;
}
match call_spine_step(inner) {
ControlFlow::Continue(next) => inner = next,
ControlFlow::Break(units) => break units,
}
};
self.spine_memo.call_at = std::ptr::from_ref(cut).addr();
self.spine_memo.call_units = units;
units
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn member_link_units(&mut self, object: &Expression<'_>) -> u32 {
let whole = matches!(object.kind, ExpressionKind::TaggedTemplateExpression(_))
|| match crate::printer::spine_first_call_within(object, SPINE_WALK_LINKS) {
Ok(call) => call.is_some_and(crate::printer::call_may_print_whole),
Err(cut) => self.spine_whole_past(cut),
};
if whole { LINK_REENTRY } else { LINK_SUBSCRIPT }
}
#[inline(never)]
fn spine_whole_past(&mut self, cut: &Expression<'_>) -> bool {
let mut node = cut;
let whole = loop {
if std::ptr::from_ref(node).addr() == self.spine_memo.whole_at {
break self.spine_memo.whole;
}
match crate::printer::spine_first_call_within(node, 1) {
Ok(call) => break call.is_some_and(crate::printer::call_may_print_whole),
Err(next) => node = next,
}
};
self.spine_memo.whole_at = std::ptr::from_ref(cut).addr();
self.spine_memo.whole = whole;
whole
}
#[inline]
fn call_prints_whole(call: &Expression<'_>) -> bool {
match &call.kind {
ExpressionKind::CallExpression(c) => {
(matches!(
c.callee.kind,
ExpressionKind::CallExpression(_) | ExpressionKind::TSNonNullExpression(_)
) || matches!(
c.arguments,
[argument] if matches!(
argument.kind,
ExpressionKind::TemplateLiteral(_)
| ExpressionKind::TaggedTemplateExpression(_)
)
)) && crate::printer::call_may_print_whole(c)
}
_ => false,
}
}
#[inline]
pub(super) fn non_null_link_units(&mut self, operand: &Expression<'_>) -> u32 {
self.call_link_units(operand)
.max(self.member_link_units(operand))
.max(LINK_NON_NULL)
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn settle_call_link(&mut self, call: &Expression<'_>) -> Result<(), Error> {
if Self::call_prints_whole(call)
&& let ExpressionKind::CallExpression(c) = &call.kind
{
let charged = self.call_link_units(c.callee);
if charged < LINK_REENTRY {
self.spine_link(LINK_REENTRY - charged)?;
}
}
Ok(())
}
#[inline]
pub(super) fn binary_link_units(&self, operator: BinaryOperator, left: &Expression<'_>) -> u32 {
if !matches!(
left.kind,
ExpressionKind::BinaryExpression(_)
| ExpressionKind::TSAsExpression(_)
| ExpressionKind::TSSatisfiesExpression(_)
) {
return LINK_OPERATOR;
}
if crate::printer::binary_left_operand_reenters(operator, left, self.in_for_header) {
LINK_REENTRY
} else {
LINK_OPERATOR
}
}
#[cold]
#[inline(never)]
fn too_deep(&self, limit: DepthLimit) -> Error {
Error::too_deep(limit, self.current_pos().0)
}
#[inline]
pub(super) fn span_pos(&self, raw: usize) -> u32 {
(self.base_offset + raw) as u32
}
#[inline]
pub(super) fn local_pos(&self, span_pos: u32) -> usize {
debug_assert!(
span_pos as usize >= self.base_offset,
"span position {span_pos} is below the parser's base offset {} — a host \
coordinate from another parse, or a base that moved",
self.base_offset,
);
(span_pos as usize).saturating_sub(self.base_offset)
}
#[inline]
pub(super) fn prev_token_end(&self) -> usize {
self.prev_end + self.base_offset
}
#[inline]
pub(super) fn eat_optional_marker(&mut self, end: usize) -> Result<(bool, usize), Error> {
Ok(if self.eat(TokenKind::Question)? {
(true, self.prev_token_end())
} else {
(false, end)
})
}
pub(super) fn current_raw_end(&self) -> usize {
self.current.end as usize
}
pub(super) fn resolve_cooked<'s>(
&'s self,
cooked: &'s StringCooked<'arena>,
span: Span,
) -> &'s str {
let local = Span::new(
span.start - self.base_offset as u32,
span.end - self.base_offset as u32,
);
cooked.resolve(local, self.source)
}
pub(super) fn source_slice(&self, span: Span) -> &'a str {
&self.source[span.start as usize - self.base_offset..span.end as usize - self.base_offset]
}
#[inline]
pub(super) fn current_value(&self) -> &'a str {
&self.source[self.current.start as usize..self.current.end as usize]
}
pub(super) fn current_decoded(&self) -> Option<&str> {
self.current_decoded
}
#[inline]
fn current_name_plain_ascii(&self) -> bool {
self.lexer.ident_is_plain_ascii(self.current.start)
}
pub(super) fn current_ident_name(&self) -> IdentName<'arena> {
if let Some(decoded) = self.current_decoded {
IdentName {
escaped: Some(decoded),
raw_len: 0,
plain_ascii: false,
}
} else {
self.current_raw_ident_name()
}
}
pub(super) fn current_raw_ident_name(&self) -> IdentName<'arena> {
let len = self.current.end - self.current.start;
if len > u16::MAX as u32 {
return IdentName {
escaped: Some(self.oversized_raw_name()),
raw_len: 0,
plain_ascii: false,
};
}
IdentName {
escaped: None,
raw_len: len as u16,
plain_ascii: self.current_name_plain_ascii(),
}
}
#[cold]
#[inline(never)]
fn oversized_raw_name(&self) -> &'arena str {
self.arena.alloc_str(self.current_value())
}
pub(super) fn current_ident_name_or_await(&self) -> IdentName<'arena> {
if matches!(self.current_kind(), TokenKind::Identifier) {
self.current_ident_name()
} else {
self.current_raw_ident_name()
}
}
pub(super) fn private_name_is(
&self,
pid: &PrivateIdentifier<'_>,
span: Span,
expected: &str,
) -> bool {
match pid.name.escaped {
Some(s) => s == expected,
None => {
let start = span.start as usize + 1 - self.base_offset;
let raw = start..start + pid.name.raw_len as usize;
self.source.as_bytes().get(raw) == Some(expected.as_bytes())
}
}
}
pub(super) fn try_ident_or_contextual_name(&self) -> Option<IdentName<'arena>> {
match self.current_kind() {
TokenKind::Identifier => Some(self.current_ident_name()),
TokenKind::Keyword(kw) if kw.can_be_identifier() => Some(self.current_raw_ident_name()),
_ => None,
}
}
pub(super) fn try_function_name(&self) -> Option<IdentName<'arena>> {
if matches!(self.current_kind(), TokenKind::Keyword(KeywordKind::Await))
&& !self.await_is_binding_name()
{
return None;
}
self.try_ident_or_contextual_name()
}
pub(super) fn at_binding_name(&self) -> bool {
match self.current_kind() {
TokenKind::Identifier => true,
TokenKind::Keyword(kw) if kw.can_be_binding_name() => true,
TokenKind::Keyword(KeywordKind::Await) => self.await_is_binding_name(),
_ => false,
}
}
pub(super) fn try_binding_name(&self) -> Option<IdentName<'arena>> {
match self.current_kind() {
TokenKind::Identifier => Some(self.current_ident_name()),
TokenKind::Keyword(kw) if kw.can_be_binding_name() => {
Some(self.current_raw_ident_name())
}
TokenKind::Keyword(KeywordKind::Await) if self.await_is_binding_name() => {
Some(self.current_raw_ident_name())
}
_ => None,
}
}
pub(super) fn take_binding_identifier(&mut self) -> Result<Option<Identifier<'arena>>, Error> {
let Some(name) = self.try_binding_name() else {
return Ok(None);
};
let (id_start, id_end) = self.current_pos();
self.advance()?;
Ok(Some(Identifier::simple(
name,
Span::new(id_start as u32, id_end as u32),
)))
}
pub(super) fn await_is_identifier(&self) -> bool {
self.goal == Goal::Script && !self.in_await
}
pub(super) fn await_is_binding_name(&self) -> bool {
self.goal == Goal::Script
}
pub(super) fn yield_is_identifier(&self) -> bool {
!self.in_yield
}
pub(super) fn keyword_is_expression_identifier(&self, kw: KeywordKind) -> bool {
kw.can_be_binding_name()
&& (!matches!(kw, KeywordKind::Yield) || self.yield_is_identifier())
}
pub(super) fn at_reference_name(&self) -> bool {
self.at_binding_name()
&& match self.current_kind() {
TokenKind::Keyword(KeywordKind::Yield) => self.yield_is_identifier(),
TokenKind::Keyword(KeywordKind::Await) => self.await_is_identifier(),
_ => true,
}
}
#[inline]
pub(super) fn enter_grouping(&mut self) -> Result<(), Error> {
self.grouping_depth += 1;
if !self.depth.check_beside(self.grouping_depth) {
return Err(self.too_deep(DepthLimit::Nesting));
}
Ok(())
}
#[inline]
pub(super) fn exit_grouping(&mut self) {
debug_assert!(
self.grouping_depth > 0,
"exit_grouping without a matching enter_grouping"
);
self.grouping_depth -= 1;
self.lt_regions.close_above(self.grouping_depth);
}
#[inline]
pub(super) fn exit_stripped_grouping(&mut self) {
self.lt_regions
.reanchor_above(self.grouping_depth.saturating_sub(1));
self.exit_grouping();
}
#[inline]
pub(super) fn open_lt_regions(&mut self, lt_pos: usize, count: usize) {
for _ in 0..count {
self.lt_regions.open(LtRegion {
floor: self.grouping_depth,
lt_pos,
});
}
}
#[inline]
pub(super) fn lt_regions_open_before(&self, operand_start: usize) -> usize {
self.lt_regions.open_before(operand_start)
}
#[inline]
pub(super) fn reset_lt_region_for_statement(&mut self) {
self.lt_regions = self.lt_regions_statement_base;
}
pub(super) fn with_lt_region_inherited<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let saved = self.lt_regions;
let saved_base = std::mem::replace(&mut self.lt_regions_statement_base, saved);
let result = f(self);
self.lt_regions_statement_base = saved_base;
self.lt_regions = saved;
result
}
pub(super) fn with_context_flag<T>(
&mut self,
flag: fn(&mut Self) -> &mut bool,
value: bool,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let saved = std::mem::replace(flag(self), value);
let result = f(self);
*flag(self) = saved;
result
}
pub(super) fn with_arrow_return_type_barred<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let saved = self
.arrow_return_type_barred_at
.replace(self.grouping_depth);
let result = f(self);
self.arrow_return_type_barred_at = saved;
result
}
pub(super) fn with_arrow_return_type_allowed<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let saved = self.arrow_return_type_barred_at.take();
let result = f(self);
self.arrow_return_type_barred_at = saved;
result
}
#[inline]
pub(super) fn arrow_return_type_barred(&self) -> bool {
self.arrow_return_type_barred_at == Some(self.grouping_depth)
}
#[must_use]
pub(super) fn checkpoint(&self) -> Checkpoint<'arena> {
debug_assert!(self.pending_conditional_extends.is_none());
Checkpoint {
lexer: self.lexer.checkpoint(),
current: self.current.clone(),
current_decoded: self.current_decoded,
peek: self.peek.clone(),
peek_decoded: self.peek_decoded,
had_line_terminator: self.had_line_terminator,
peek_had_line_terminator: self.peek_had_line_terminator,
prev_end: self.prev_end,
lexer_error: self.lexer_error.clone(),
comments_len: self.comments.len(),
grouping_depth: self.grouping_depth,
depth: self.depth,
spine: self.spine,
lt_regions: self.lt_regions,
abandoned_type_arguments_error: self.abandoned_type_arguments_error.clone(),
}
}
pub(super) fn rewind(&mut self, checkpoint: Checkpoint<'arena>) {
let Checkpoint {
lexer,
current,
current_decoded,
peek,
peek_decoded,
had_line_terminator,
peek_had_line_terminator,
prev_end,
lexer_error,
comments_len,
grouping_depth,
depth,
spine,
lt_regions,
abandoned_type_arguments_error,
} = checkpoint;
self.lexer.rewind(lexer);
self.current = current;
self.current_decoded = current_decoded;
self.peek = peek;
self.peek_decoded = peek_decoded;
self.had_line_terminator = had_line_terminator;
self.peek_had_line_terminator = peek_had_line_terminator;
self.prev_end = prev_end;
self.lexer_error = lexer_error;
self.comments.truncate(comments_len);
self.grouping_depth = grouping_depth;
self.depth = depth;
self.spine = spine;
self.lt_regions = lt_regions;
self.abandoned_type_arguments_error = abandoned_type_arguments_error;
self.pending_conditional_extends = None;
}
#[inline]
pub(super) fn type_arguments_abandoned_at(&self, lt: u32) -> bool {
!self.abandoned_type_argument_lists.is_empty()
&& self
.abandoned_type_argument_lists
.contains(&(lt, self.type_arguments_context_key()))
}
fn type_arguments_context_key(&self) -> u8 {
[
self.in_await,
self.in_yield,
self.strict,
self.in_ambient_context,
]
.iter()
.fold(0, |key, &flag| (key << 1) | u8::from(flag))
}
#[inline]
pub(super) fn type_arguments_kept_at(&self, lt: u32) -> bool {
!self.type_argument_lists_kept.is_empty()
&& self
.type_argument_lists_kept
.contains(&(lt, self.type_arguments_context_key()))
}
pub(super) fn settle_type_arguments_follower(&mut self, lt: u32, abandoned: bool) {
let key = (lt, self.type_arguments_context_key());
if abandoned {
self.abandoned_type_argument_lists.insert(key);
} else {
self.type_argument_lists_kept.insert(key);
}
}
pub(super) fn abandon_type_arguments(&mut self, lt: u32, error: Error) {
self.abandoned_type_argument_lists
.insert((lt, self.type_arguments_context_key()));
let reaches_further = match &self.abandoned_type_arguments_error {
Some(kept) => error.position() > kept.position(),
None => true,
};
if reaches_further {
self.abandoned_type_arguments_error = Some(error);
}
}
pub(crate) fn prefer_abandoned_type_arguments_error(&mut self, error: Error) -> Error {
match (self.abandoned_type_arguments_error.take(), error.position()) {
(Some(kept), Some(reached))
if !error.is_goal_gated() && kept.position().is_some_and(|k| k >= reached) =>
{
kept
}
_ => error,
}
}
pub(super) fn with_fn_context<T>(
&mut self,
is_async: bool,
is_generator: bool,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let saved_await = std::mem::replace(&mut self.in_await, is_async);
let saved_await_if_module = std::mem::replace(&mut self.in_await_if_module, is_async);
let saved_yield = std::mem::replace(&mut self.in_yield, is_generator);
let result = f(self);
self.in_await = saved_await;
self.in_await_if_module = saved_await_if_module;
self.in_yield = saved_yield;
result
}
pub(super) fn with_strict_scope<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
let saved = self.strict;
let result = f(self);
self.strict = saved;
result
}
pub(super) fn with_strict_class_scope<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
self.with_context_flag(|p| &mut p.strict, true, f)
}
pub(super) fn with_fn_type_disallowed<T>(
&mut self,
value: bool,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
self.with_context_flag(|p| &mut p.fn_type_disallowed, value, f)
}
pub(super) fn with_conditional_type_disallowed<T>(
&mut self,
value: bool,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
self.with_context_flag(|p| &mut p.conditional_type_disallowed, value, f)
}
pub(super) fn with_full_type_context<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, Error>,
) -> Result<T, Error> {
self.with_fn_type_disallowed(false, |p| p.with_conditional_type_disallowed(false, f))
}
pub(super) fn try_param_name(&self) -> Option<IdentName<'arena>> {
match self.try_binding_name() {
Some(name) => Some(name),
None => self.this_as_name(),
}
}
pub(super) fn this_as_name(&self) -> Option<IdentName<'arena>> {
if matches!(self.current_kind(), TokenKind::Keyword(KeywordKind::This)) {
Some(self.current_raw_ident_name())
} else {
None
}
}
pub(super) fn try_identifier_name(&self) -> Option<IdentName<'arena>> {
match self.current_kind() {
TokenKind::Identifier => Some(self.current_ident_name()),
TokenKind::Keyword(_) => Some(self.current_raw_ident_name()),
_ => None,
}
}
pub(super) fn extract_string_cooked(&self) -> Result<StringCooked<'arena>, Error> {
match self.current_decoded {
Some(decoded) => {
if self.strict
&& let Some(err) = self.legacy_escape_error_in_current_string()
{
return Err(err);
}
Ok(StringCooked::Decoded(decoded))
}
None => Ok(StringCooked::Verbatim),
}
}
fn legacy_escape_error_in_current_string(&self) -> Option<Error> {
let start = self.current.start as usize;
let end = self.current.end as usize;
let inner = self.source.get(start + 1..end.saturating_sub(1))?;
self.legacy_escape_error(inner, start + 1 + self.base_offset)
}
fn legacy_escape_error(&self, raw: &str, raw_start: usize) -> Option<Error> {
let (offset, form) = find_legacy_escape(raw)?;
let message = match form {
LegacyEscape::NonOctalDecimal => {
"The '\\8' and '\\9' escape sequences are not allowed in strict mode."
}
LegacyEscape::LegacyOctal => {
"Octal escape sequences are not allowed in strict mode. Use '\\x' or '\\u'."
}
};
Some(self.error_msg_at(message, raw_start + offset))
}
pub(super) fn error_msg(&self, message: &str) -> Error {
Error::invalid_syntax(message.to_string(), self.current_pos().0)
}
pub(super) fn error_msg_at(&self, message: &str, position: usize) -> Error {
Error::invalid_syntax(message.to_string(), position)
}
pub(super) fn error_goal_gate_at(&self, message: &str, position: usize) -> Error {
Error::goal_gate(message.to_string(), position)
}
pub(super) fn error_goal_gate(&self, message: &str) -> Error {
self.error_goal_gate_at(message, self.current_pos().0)
}
pub(super) fn error_expected(&self, what: &str) -> Error {
Error::invalid_syntax(format!("Expected {what}"), self.current_pos().0)
}
pub(super) fn error_expected_found(&self, what: &str) -> Error {
let kind = &self.current.kind;
Error::invalid_syntax(
format!("Expected {what}, found {kind}"),
self.current_pos().0,
)
}
pub(super) fn error_expected_found_at(&self, what: &str, position: usize) -> Error {
let kind = &self.current.kind;
Error::invalid_syntax(format!("Expected {what}, found {kind}"), position)
}
pub(super) fn error_expected_after(&self, what: &str, after: &str) -> Error {
let kind = &self.current.kind;
Error::invalid_syntax(
format!("Expected {what} after '{after}', found {kind}"),
self.current_pos().0,
)
}
pub(super) fn error_unexpected_keyword(&self, kw: KeywordKind) -> Error {
Error::invalid_syntax(format!("Unexpected keyword '{kw}'"), self.current_pos().0)
}
pub(super) fn error_with_statement(&self) -> Error {
Error::invalid_syntax(
"The 'with' statement is not allowed in strict mode".to_owned(),
self.current_pos().0,
)
}
pub(super) fn error_list_separator(
&self,
separator: &TokenKind,
terminator: &TokenKind,
) -> Error {
let kind = &self.current.kind;
Error::invalid_syntax(
format!("Expected {separator} or {terminator} after list element, found {kind}"),
self.current_pos().0,
)
}
pub(super) fn check(&self, kind: &TokenKind) -> bool {
&self.current.kind == kind
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn try_assignment_operator(&self) -> Option<AssignmentOperator> {
match &self.current.kind {
TokenKind::Equals => Some(AssignmentOperator::Assign),
TokenKind::PlusEquals => Some(AssignmentOperator::AddAssign),
TokenKind::MinusEquals => Some(AssignmentOperator::SubtractAssign),
TokenKind::StarEquals => Some(AssignmentOperator::MultiplyAssign),
TokenKind::SlashEquals => Some(AssignmentOperator::DivideAssign),
TokenKind::PercentEquals => Some(AssignmentOperator::RemainderAssign),
TokenKind::StarStarEquals => Some(AssignmentOperator::ExponentiateAssign),
TokenKind::LeftShiftEquals => Some(AssignmentOperator::LeftShiftAssign),
TokenKind::RightShiftEquals => Some(AssignmentOperator::RightShiftAssign),
TokenKind::UnsignedRightShiftEquals => {
Some(AssignmentOperator::UnsignedRightShiftAssign)
}
TokenKind::AmpersandEquals => Some(AssignmentOperator::BitwiseAndAssign),
TokenKind::PipeEquals => Some(AssignmentOperator::BitwiseOrAssign),
TokenKind::CaretEquals => Some(AssignmentOperator::BitwiseXorAssign),
TokenKind::AmpersandAmpersandEquals => Some(AssignmentOperator::LogicalAndAssign),
TokenKind::PipePipeEquals => Some(AssignmentOperator::LogicalOrAssign),
TokenKind::QuestionQuestionEquals => Some(AssignmentOperator::NullishAssign),
_ => None,
}
}
pub(super) fn peek_kind(&mut self) -> TokenKind {
if self.peek.is_none() && self.lexer_error.is_none() {
self.peek_had_line_terminator = false;
loop {
match self.lexer.next_token() {
Ok(token) => {
if self.lexer.had_line_terminator() {
self.peek_had_line_terminator = true;
}
if let TokenKind::Comment {
is_block,
content_start,
} = &token.kind
{
let (comment, has_line_terminator) = comment_from_token(
self.source,
token.start as usize,
token.end as usize,
*content_start as usize,
*is_block,
self.base_offset,
);
if has_line_terminator {
self.peek_had_line_terminator = true;
}
self.comments.push(comment);
continue;
}
self.peek = Some(token);
self.peek_decoded = self.decoded_to_arena();
}
Err(err) => {
self.lexer_error = Some(err);
}
}
break;
}
}
self.peek
.as_ref()
.map_or(TokenKind::Eof, |t| t.kind.clone())
}
pub(super) fn peek_value(&self) -> &str {
self.peek
.as_ref()
.map_or("", |t| &self.source[t.start as usize..t.end as usize])
}
pub(super) fn peek_is_identifier(&mut self) -> bool {
matches!(self.peek_kind(), TokenKind::Identifier)
}
pub(super) fn peek_is(&mut self, kind: &TokenKind) -> bool {
self.peek_kind() == *kind
}
pub(super) fn peek_starts_parameter_binding(&mut self) -> bool {
let kind = self.peek_kind();
kind.is_binding_name_word()
|| matches!(
kind,
TokenKind::BracketOpen
| TokenKind::BraceOpen
| TokenKind::DotDotDot
| TokenKind::Keyword(KeywordKind::This)
)
}
pub(super) fn peek_start(&self) -> usize {
self.peek.as_ref().map_or(0, |t| t.start as usize)
}
pub(super) fn peek_preceded_by_line_terminator(&mut self) -> bool {
self.peek_kind(); let to = self.peek_start();
let from = (self.current.end as usize).min(to);
self.source[from..to].contains(is_es_line_terminator)
}
pub(super) fn peek_is_same_line_identifier(&mut self) -> bool {
self.peek_is_identifier() && !self.peek_preceded_by_line_terminator()
}
pub(super) fn peek_is_same_line_name_word(&mut self) -> bool {
self.peek_kind().is_binding_name_word() && !self.peek_preceded_by_line_terminator()
}
pub(super) fn require_same_line_declaration_name(&mut self, head: &str) -> Result<(), Error> {
if self.peek_is_same_line_declaration_name() {
Ok(())
} else {
Err(self.same_line_declaration_name_error(head))
}
}
pub(super) fn peek_is_same_line_declaration_name(&mut self) -> bool {
self.peek_is_same_line_name_word()
|| (self.current_value() == "module"
&& self.peek_kind() == TokenKind::String
&& !self.peek_preceded_by_line_terminator())
}
pub(super) fn same_line_declaration_name_error(&self, head: &str) -> Error {
self.error_msg(&format!("{head} name must be on the same line"))
}
pub(super) fn at_let_declaration(&mut self) -> bool {
let kind = self.peek_kind();
kind.is_binding_name_word()
|| matches!(kind, TokenKind::BraceOpen | TokenKind::BracketOpen)
|| (matches!(kind, TokenKind::Keyword(KeywordKind::Await))
&& self.await_is_binding_name())
}
pub(super) fn peek_is_same_line_binding_word(&mut self) -> bool {
self.peek_is_identifier_or_keyword()
&& !self.peek_preceded_by_line_terminator()
&& !matches!(self.peek_value(), "in" | "instanceof" | "as" | "satisfies")
}
pub(super) fn peek_starts_ambient_declaration(&mut self) -> bool {
if self.peek_preceded_by_line_terminator() {
return false;
}
match self.peek_kind() {
TokenKind::Keyword(
KeywordKind::Const
| KeywordKind::Let
| KeywordKind::Var
| KeywordKind::Function
| KeywordKind::Class
| KeywordKind::Enum,
) => true,
TokenKind::Keyword(KeywordKind::Async) => {
self.peek_followed_by_same_line_function_keyword()
}
TokenKind::Identifier => matches!(
self.peek_value(),
"abstract" | "namespace" | "module" | "interface" | "type" | "global"
),
_ => false,
}
}
fn scan_past_peek(&mut self) -> (usize, bool) {
self.peek_kind(); let after_peek = self.peek.as_ref().map_or(0, |t| t.end as usize);
let pos = scan::skip_whitespace_and_comments(self.source.as_bytes(), after_peek);
(
pos,
!self.source[after_peek..pos].contains(is_es_line_terminator),
)
}
pub(super) fn peek_followed_by_same_line_binding_word(&mut self) -> bool {
let (pos, same_line) = self.scan_past_peek();
let bytes = self.source.as_bytes();
same_line && scan::identifier_starts_at(bytes, pos) && {
let end = scan::skip_identifier(bytes, pos);
let word = &bytes[pos..end];
word != b"in" && word != b"instanceof" && word != b"as" && word != b"satisfies"
}
}
pub(super) fn peek_followed_by_declarator_continuation(&mut self) -> bool {
let (pos, _) = self.scan_past_peek();
let bytes = self.source.as_bytes();
match bytes.get(pos) {
Some(b';' | b':') => true,
Some(b'=') => !matches!(bytes.get(pos + 1), Some(b'=' | b'>')),
_ => false,
}
}
pub(super) fn peek_followed_by_same_line_function_keyword(&mut self) -> bool {
let (pos, same_line) = self.scan_past_peek();
let bytes = self.source.as_bytes();
same_line
&& pos < bytes.len()
&& &bytes[pos..scan::skip_identifier(bytes, pos)] == b"function"
}
pub(super) fn at_using_declaration(&mut self) -> bool {
*self.current_kind() == TokenKind::Identifier
&& self.current_value() == "using"
&& self.peek_is_same_line_binding_word()
}
pub(super) fn at_await_using_declaration(&mut self) -> bool {
*self.current_kind() == TokenKind::Keyword(KeywordKind::Await)
&& self.peek_is_same_line_identifier()
&& self.peek_value() == "using"
&& self.peek_followed_by_same_line_binding_word()
}
pub(super) fn peek_is_property_name(&mut self) -> bool {
matches!(
self.peek_kind(),
TokenKind::Identifier
| TokenKind::BracketOpen
| TokenKind::String
| TokenKind::Number
| TokenKind::Keyword(_)
)
}
pub(super) fn current_is_identifier_or_keyword(&self) -> bool {
matches!(
self.current.kind,
TokenKind::Identifier | TokenKind::Keyword(_)
)
}
pub(super) fn peek_is_identifier_or_keyword(&mut self) -> bool {
matches!(
self.peek_kind(),
TokenKind::Identifier | TokenKind::Keyword(_)
)
}
pub(super) fn current_property_name(&self) -> &'a str {
match &self.current.kind {
TokenKind::Identifier => self.current_value(),
TokenKind::Keyword(kw) => kw.as_str(),
_ => {
debug_assert!(
false,
"current_property_name called on non-identifier/keyword token"
);
""
}
}
}
pub(super) fn parse_identifier_name_node(&mut self) -> Result<Identifier<'arena>, Error> {
debug_assert!(self.current_is_identifier_or_keyword());
let (start, end) = self.current_pos();
let name = self.current_ident_name();
self.advance()?;
Ok(Identifier::simple(
name,
Span::new(start as u32, end as u32),
))
}
pub(super) fn peek_is_class_member_name(&mut self) -> bool {
matches!(
self.peek_kind(),
TokenKind::Identifier
| TokenKind::BracketOpen
| TokenKind::String
| TokenKind::Number
| TokenKind::Keyword(_)
| TokenKind::Hash
)
}
pub(super) fn parse_private_identifier(
&mut self,
) -> Result<(PrivateIdentifier<'arena>, Span), Error> {
debug_assert!(matches!(self.current_kind(), TokenKind::Hash));
let (start, hash_end) = self.current_pos();
self.advance()?;
let (name_start, end) = self.current_pos();
let Some(name) = self.try_identifier_name() else {
return Err(self.error_expected_after("identifier", "#"));
};
if name_start != hash_end {
return Err(self.error_msg_at("A private name must follow '#' immediately", hash_end));
}
self.advance()?;
Ok((
PrivateIdentifier { name },
Span::new(start as u32, end as u32),
))
}
pub(super) fn expect(&mut self, kind: &TokenKind) -> Result<(), Error> {
if self.check(kind) {
self.advance()
} else {
Err(self.unexpected_token_err(kind))
}
}
#[cold]
#[inline(never)]
fn unexpected_token_err(&self, kind: &TokenKind) -> Error {
Error::unexpected_token(
kind.to_string(),
self.current.kind.to_string(),
self.current_pos().0,
)
}
pub(super) fn greater_than_end_in_type(&mut self) -> Result<u32, Error> {
let end = (self.current_pos().0 + 1) as u32;
self.expect_greater_than_in_type()?;
Ok(end)
}
pub(super) fn expect_greater_than_in_type(&mut self) -> Result<(), Error> {
match self.current.kind {
TokenKind::GreaterThan => {
self.advance()
}
TokenKind::RightShift => {
self.current.start += 1;
self.prev_end = self.current.start as usize;
self.current.kind = TokenKind::GreaterThan;
Ok(())
}
TokenKind::UnsignedRightShift => {
self.current.start += 1;
self.prev_end = self.current.start as usize;
self.current.kind = TokenKind::RightShift;
Ok(())
}
TokenKind::GreaterThanEquals
| TokenKind::RightShiftEquals
| TokenKind::UnsignedRightShiftEquals => {
let new_start = self.current.start as usize + 1;
self.prev_end = new_start;
let relex_from = self.span_pos(new_start);
while self
.comments
.last()
.is_some_and(|c| c.span.start >= relex_from)
{
self.comments.pop();
}
let token = self.lexer.seek_and_next_token(new_start)?;
self.current = token;
self.current_decoded = self.decoded_to_arena();
self.peek = None;
self.peek_decoded = None;
Ok(())
}
_ => Err(Error::unexpected_token(
"'>'".to_string(),
self.current.kind.to_string(),
self.current_pos().0,
)),
}
}
pub(super) fn check_less_than_in_type(&self) -> bool {
matches!(
self.current.kind,
TokenKind::LessThan | TokenKind::LeftShift
)
}
pub(super) fn expect_less_than_in_type(&mut self) -> Result<(), Error> {
match self.current.kind {
TokenKind::LessThan => self.advance(),
TokenKind::LeftShift => {
self.current.start += 1;
self.prev_end = self.current.start as usize;
self.current.kind = TokenKind::LessThan;
Ok(())
}
_ => Err(Error::unexpected_token(
"'<'".to_string(),
self.current.kind.to_string(),
self.current_pos().0,
)),
}
}
pub(super) fn check_greater_than_in_type(&self) -> bool {
matches!(
self.current.kind,
TokenKind::GreaterThan
| TokenKind::RightShift
| TokenKind::UnsignedRightShift
| TokenKind::GreaterThanEquals
| TokenKind::RightShiftEquals
| TokenKind::UnsignedRightShiftEquals
)
}
pub(super) fn eat(&mut self, kind: TokenKind) -> Result<bool, Error> {
if self.check(&kind) {
self.advance()?;
Ok(true)
} else {
Ok(false)
}
}
#[inline]
pub(super) fn eat_contextual_keyword(&mut self, keyword: &str) -> Result<bool, Error> {
if matches!(self.current_kind(), TokenKind::Identifier) && self.current_value() == keyword {
self.advance()?;
Ok(true)
} else {
Ok(false)
}
}
#[inline]
pub(super) fn peek_is_contextual_keyword(&mut self, keyword: &str) -> bool {
matches!(self.peek_kind(), TokenKind::Identifier) && self.peek_value() == keyword
}
#[inline]
pub(super) fn peek_predicate_is_ahead(&mut self) -> bool {
self.peek_is_contextual_keyword("is") && !self.peek_preceded_by_line_terminator()
}
pub(super) fn eat_type_predicate_asserts(&mut self) -> Result<bool, Error> {
if matches!(self.current_kind(), TokenKind::Identifier)
&& self.current_value() == "asserts"
&& self.peek_is_identifier_or_keyword()
&& !self.peek_preceded_by_line_terminator()
{
self.advance()?;
Ok(true)
} else {
Ok(false)
}
}
pub(super) fn can_insert_semicolon(&self) -> bool {
matches!(self.current.kind, TokenKind::Eof | TokenKind::BraceClose)
|| self.had_line_terminator
}
pub(super) fn semicolon_end(&mut self) -> Result<u32, Error> {
self.semicolon()?;
Ok(self.prev_token_end() as u32)
}
pub(super) fn semicolon(&mut self) -> Result<(), Error> {
if self.eat(TokenKind::Semicolon)? {
return Ok(());
}
if self.can_insert_semicolon() {
return Ok(());
}
Err(self.error_expected("';'"))
}
pub(super) fn expect_list_separator(
&mut self,
separator: &TokenKind,
terminator: &TokenKind,
) -> Result<bool, Error> {
if self.check(separator) {
self.advance()?;
if self.check(terminator) {
Ok(false) } else {
Ok(true) }
} else if self.check(terminator) {
Ok(false) } else {
Err(self.error_list_separator(separator, terminator))
}
}
pub(crate) fn parse(&mut self) -> Result<Program<'arena>, Error> {
self.parse_program()
.map_err(|error| self.mark_await_operand_gate(error))
}
fn mark_await_operand_gate(&self, error: Error) -> Error {
if self.await_name_operand.is_some() && error.position() == self.await_name_operand {
error.into_goal_gate()
} else {
error
}
}
fn parse_program(&mut self) -> Result<Program<'arena>, Error> {
let start = self.base_offset;
let body = self.with_strict_scope(|p| {
p.statement_list(1, |p, frame| {
let mut body = p.bvec();
let mut in_prologue = true;
while p.current.kind != TokenKind::Eof {
let mut item = p.parse_module_item(statement::ModuleItemContext::TopLevel)?;
frame.next(p);
if in_prologue {
in_prologue = p.note_directive(&mut item, &body)?;
}
body.push(item);
}
Ok(body)
})
})?;
let (_, end) = self.current_pos();
Ok(Program {
body: body.into_bump_slice(),
comments: self.take_comments(),
span: Span::new(start as u32, end as u32),
goal: self.goal,
})
}
pub(crate) fn parse_expression_unbounded(&mut self) -> Result<Expression<'arena>, Error> {
self.parse_expression()
}
pub(crate) fn expect_end_of_input(&self, end: &str) -> Result<(), Error> {
if self.current.kind == TokenKind::Eof {
Ok(())
} else {
Err(self.error_expected_found(end))
}
}
pub(crate) fn parse_expression_with_comments(
&mut self,
end: &str,
) -> Result<(&'arena Expression<'arena>, &'arena [Comment]), Error> {
let expr = self.parse_expression_ref()?;
self.expect_end_of_input(end)?;
let comments = self.take_comments();
Ok((expr, comments))
}
pub(crate) fn take_comments(&mut self) -> &'arena [Comment] {
std::mem::replace(&mut self.comments, BumpVec::new_in(self.arena)).into_bump_slice()
}
pub(crate) fn parse_assignment_expression_partial(
&mut self,
top_level_as: TopLevelAs,
) -> Result<(&'arena Expression<'arena>, usize), Error> {
let saved = std::mem::replace(
&mut self.top_level_as_is_assertion,
top_level_as == TopLevelAs::Assertion,
);
let result = self.parse_assignment_expression_ref();
self.top_level_as_is_assertion = saved;
let expr = result?;
let next_pos = self.current.start as usize + self.base_offset;
Ok((expr, next_pos))
}
pub(crate) fn expression_to_svelte_pattern(
&mut self,
expr: Expression<'arena>,
) -> Result<Expression<'arena>, Error> {
self.refine_assignable(
expr,
expression_assignable::AssignableContext::SveltePattern,
)
}
pub(super) fn parse_string_literal(&mut self) -> Result<Literal<'arena>, Error> {
if !matches!(self.current_kind(), TokenKind::String) {
return Err(self.error_expected("string literal"));
}
let (start, end) = self.current_pos();
let cooked = self.extract_string_cooked()?;
self.advance()?;
Ok(Literal {
value: LiteralValue::String(cooked),
span: Span::new(start as u32, end as u32),
})
}
}
pub(crate) fn parse_typescript<'arena>(
source: &str,
arena: &'arena Bump,
) -> Result<Program<'arena>, Error> {
parse_typescript_with_goal(source, Goal::Module, arena)
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn parse_retrying_acorn_followers<'arena, T>(
source: &str,
base_offset: usize,
host_nesting: u32,
goal: Goal,
arena: &'arena Bump,
embedded: bool,
f: impl Fn(&mut Parser<'_, 'arena>) -> Result<T, Error>,
host_check: impl Fn(&T) -> Result<(), Error>,
) -> Result<T, Error> {
let checked = |parsed: T| {
host_check(&parsed)
.map(|()| parsed)
.map_err(|error| (error, true))
};
let mut parser = Parser::new(source, base_offset, goal, arena);
parser.depth = DepthBudget::at(host_nesting);
parser.prime()?;
let first = f(&mut parser)
.map_err(|error| (parser.prefer_abandoned_type_arguments_error(error), false))
.and_then(checked);
if !parser.gave_up_list_to_tsc_follower
|| (first.is_ok() && !parser.kept_list_past_tsc_follower)
{
return first.map_err(|(error, _)| error);
}
let mut retry = Parser::new(source, base_offset, goal, arena);
retry.depth = DepthBudget::at(host_nesting);
retry.tsc_followers = false;
let second = retry
.prime()
.and_then(|()| f(&mut retry))
.map_err(|error| (error, false))
.and_then(checked);
match (second, first) {
(Ok(parsed), _) | (Err(_), Ok(parsed)) => Ok(parsed),
(Err((_, false)), Err((error, false))) if !embedded => Err(error),
(Err((error, _)), Err(_)) => Err(error),
}
}
#[expect(clippy::redundant_closure_for_method_calls)]
pub(crate) fn parse_typescript_with_goal<'arena>(
source: &str,
goal: Goal,
arena: &'arena Bump,
) -> Result<Program<'arena>, Error> {
parse_retrying_acorn_followers(
source,
0,
0,
goal,
arena,
false,
|parser| parser.parse(),
|_| Ok(()),
)
}
pub(crate) fn parse_typescript_preserve_parens<'arena>(
source: &str,
arena: &'arena Bump,
) -> Result<Program<'arena>, Error> {
parse_retrying_acorn_followers(
source,
0,
0,
Goal::Module,
arena,
false,
|parser| {
parser.preserve_parens = true;
parser.parse()
},
|_| Ok(()),
)
}