use crate::compiler_error::{CompileError, CompilerFailure, CompilerStage};
use crate::{
ArrayLiteralElement, ArrowBody, Ast, BinOp, Binding, CatchClause, Diagnostic, EnumInitializer,
EnumMember, ExportedDecl, Expr, ExprId, ExprKind, FileId, Ident, ImportKind, ImportSpecifier,
ObjectLiteralField, ObjectLiteralMember, ObjectPatternField, ParamDecl, PostfixOp, Severity,
Span, Stmt, StmtId, StmtKind, SwitchCase, SwitchDefault, Token, TokenKind, TypeAnnotation,
TypeAnnotationField, TypeAnnotationKind, UnOp,
};
const MAX_ERRORS: usize = 20;
const MAX_PARSE_DEPTH: usize = 128;
const BUILT_IN_TYPE_NAMES: &[&str] = &[
"any",
"bigint",
"boolean",
"never",
"number",
"object",
"string",
"symbol",
"undefined",
"unknown",
"void",
];
#[derive(Clone, Copy, PartialEq)]
enum TypePos {
Anywhere,
ArrowReturn,
}
pub fn parse(source: &str, tokens: Vec<Token>, file: FileId) -> (Ast, Vec<Diagnostic>) {
match parse_checked(source, tokens, file) {
Ok(result) => result,
Err(error) => (Ast::new(), error.into_diagnostics(file)),
}
}
pub fn parse_checked(
source: &str,
tokens: Vec<Token>,
file: FileId,
) -> Result<(Ast, Vec<Diagnostic>), CompileError> {
crate::source::SourceError::check_source_len(source.len())
.map_err(|error| error.into_compiler_failure(CompilerStage::Parse))?;
validate_token_stream(source, &tokens, file).map_err(|message| CompilerFailure::Internal {
stage: CompilerStage::Parse,
span: None,
message: message.into(),
})?;
let eof = tokens
.last()
.cloned()
.unwrap_or_else(|| Token::new(TokenKind::Eof, Span::at(file)));
let mut p = Parser {
source,
tokens,
file,
pos: 0,
ast: Ast::new(),
diagnostics: Vec::new(),
block_depth: 0,
class_member_body_depth: 0,
function_expression_body_depth: 0,
recursion_depth: 0,
recursion_limit_span: None,
last_as_type_end: None,
eof,
fatal: None,
};
p.parse_program();
if let Some(span) = p.recursion_limit_span {
p.fatal = Some(CompilerFailure::Limit {
stage: CompilerStage::Parse,
span: Some(span),
message: "parser recursion limit exceeded".into(),
help: vec!["simplify nested syntax or split it into separate declarations".into()],
});
}
if let Some(fatal) = p.fatal {
return Err(CompileError {
diagnostics: p.diagnostics,
fatal: Some(fatal),
});
}
p.ast
.validate_source(source, file)
.map_err(|error| CompileError {
diagnostics: p.diagnostics.clone(),
fatal: Some(error.into_compiler_failure(CompilerStage::Parse)),
})?;
crate::tree_height::check_syntax(&p.ast).map_err(|fatal| CompileError {
diagnostics: p.diagnostics.clone(),
fatal: Some(fatal),
})?;
Ok((p.ast, p.diagnostics))
}
fn validate_token_stream(source: &str, tokens: &[Token], file: FileId) -> Result<(), &'static str> {
if !matches!(tokens.last().map(|token| &token.kind), Some(TokenKind::Eof)) {
return Err("token stream must end with EOF");
}
let mut previous = 0;
for (index, token) in tokens.iter().enumerate() {
let span = token.span;
if span.file != file
|| span.start < previous
|| span.start > span.end
|| source.get(span.start as usize..span.end as usize).is_none()
{
return Err("token span is outside its source or out of order");
}
if matches!(token.kind, TokenKind::Eof) && index != tokens.len() - 1 {
return Err("tokens follow EOF");
}
if let Some(doc) = &token.leading_doc {
let span = doc.span;
if span.file != file
|| span.start > span.end
|| span.end > token.span.start
|| source.get(span.start as usize..span.end as usize) != Some(doc.text.as_str())
|| !doc.text.starts_with("/**")
|| !doc.text.ends_with("*/")
{
return Err("documentation metadata does not match its source");
}
}
previous = span.start;
}
Ok(())
}
pub(crate) struct Parser<'a> {
source: &'a str,
tokens: Vec<Token>,
file: FileId,
pos: usize,
ast: Ast,
diagnostics: Vec<Diagnostic>,
block_depth: u32,
class_member_body_depth: u32,
function_expression_body_depth: u32,
recursion_depth: usize,
recursion_limit_span: Option<Span>,
last_as_type_end: Option<u32>,
eof: Token,
fatal: Option<CompilerFailure>,
}
impl<'a> Parser<'a> {
fn span(&self, start: u32, end: u32) -> Span {
Span {
file: self.file,
start,
end,
}
}
fn type_name(&mut self, span: Span) -> Option<Ident> {
match span.text(self.source, self.file) {
Ok(name) => Some(Ident {
name: name.to_string(),
span,
}),
Err(error) => {
self.fatal = Some(error.into_compiler_failure(CompilerStage::Parse));
None
}
}
}
fn parse_program(&mut self) {
while !self.is_at_eof() {
if self.fatal.is_some() || self.error_count() >= MAX_ERRORS {
break;
}
if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance();
continue;
}
let pos_before = self.pos;
if let Some(id) = self.parse_statement() {
self.ast.top_level.push(id);
} else {
if self.fatal.is_some() {
break;
}
self.recover();
if self.pos != pos_before && matches!(self.peek().kind, TokenKind::RightBrace) {
self.advance();
continue;
}
if self.pos == pos_before && !self.is_at_eof() {
self.advance();
}
}
}
}
fn parse_statement(&mut self) -> Option<StmtId> {
self.with_recursion_limit(Self::parse_statement_inner)
}
fn parse_statement_inner(&mut self) -> Option<StmtId> {
match self.peek().kind {
TokenKind::Let => self.parse_let_or_const(false),
TokenKind::Const => self.parse_let_or_const(true),
TokenKind::Function => self.parse_function_decl(),
TokenKind::Class => self.parse_class_decl(),
TokenKind::Interface => self.parse_interface_decl(),
TokenKind::Enum => self.parse_enum_decl(),
TokenKind::Identifier if self.at_type_alias_head() => self.parse_type_alias_decl(),
TokenKind::Export => self.parse_export(),
TokenKind::Import => self.parse_import_decl(),
TokenKind::If => self.parse_if(),
TokenKind::While => self.parse_while(),
TokenKind::Do => self.parse_do_while(),
TokenKind::For => self.parse_for(),
TokenKind::Switch => self.parse_switch(),
TokenKind::Break => self.parse_break(),
TokenKind::Continue => self.parse_continue(),
TokenKind::Return => self.parse_return(),
TokenKind::Throw => self.parse_throw(),
TokenKind::Try => self.parse_try(),
TokenKind::LeftBrace => self.parse_block(),
_ => self.parse_expression_statement(),
}
}
fn parse_expression_statement(&mut self) -> Option<StmtId> {
let expr_id = self.parse_expression()?;
let expr_span = parse_arena_result(self.ast.try_expr(expr_id), &mut self.fatal)?.span;
if !self.at_statement_end() {
let what = if matches!(
parse_arena_result(self.ast.try_expr(expr_id), &mut self.fatal)?.kind,
ExprKind::Assign { .. }
) {
"assignment"
} else {
"expression"
};
self.error_at_peek(format!("expected `;` after {what}"));
return None;
}
let end = self.finish_statement();
let span = self.span(expr_span.start, end);
let kind = self.statement_kind_for(expr_id)?;
parse_arena_result(self.ast.try_push_stmt(Stmt { kind, span }), &mut self.fatal)
}
fn statement_kind_for(&mut self, expr_id: ExprId) -> Option<StmtKind> {
let ExprKind::Assign {
target,
op,
op_span,
value,
} = parse_arena_result(self.ast.try_expr(expr_id), &mut self.fatal)?
.kind
.clone()
else {
return Some(StmtKind::Expr(expr_id));
};
Some(
match (
parse_arena_result(self.ast.try_expr(target), &mut self.fatal)?
.kind
.clone(),
op,
) {
(ExprKind::Identifier(target), None) => StmtKind::Assign { target, value },
(ExprKind::Identifier(target), Some(op)) => StmtKind::CompoundAssign {
target,
op,
op_span,
value,
},
(ExprKind::FieldAccess { receiver, name }, None) => StmtKind::AssignField {
receiver,
field_name: name,
value,
},
(ExprKind::FieldAccess { receiver, name }, Some(op)) => {
StmtKind::CompoundAssignField {
receiver,
field_name: name,
op,
op_span,
value,
}
}
(ExprKind::IndexAccess { receiver, index }, None) => StmtKind::AssignIndex {
receiver,
index,
value,
},
(ExprKind::IndexAccess { receiver, index }, Some(op)) => {
StmtKind::CompoundAssignIndex {
receiver,
index,
op,
op_span,
value,
}
}
_ => return self.invariant_failure("invalid assignment target"),
},
)
}
fn parse_let_or_const(&mut self, is_const: bool) -> Option<StmtId> {
let doc = self.take_leading_doc();
let kw = self.advance();
let binding = match self.peek().kind {
TokenKind::LeftBrace | TokenKind::LeftBracket => Some(self.parse_binding()?),
_ => None,
};
let name = if binding.is_none() {
let name_tok = self.expect_identifier("expected identifier after `let`/`const`")?;
Some(self.ident_from_token(&name_tok))
} else {
None
};
let ty = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
Some(self.parse_type_annotation()?)
} else {
None
};
let implicit = !is_const
&& ty.is_some()
&& !matches!(self.peek().kind, TokenKind::Equals)
&& matches!(self.peek().kind, TokenKind::Semicolon);
let value = if let (true, Some(name)) = (implicit, &name) {
let value = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Identifier(Ident {
name: "undefined".to_string(),
span: name.span,
}),
span: name.span,
}),
&mut self.fatal,
)?;
self.ast.implicit_initializers.insert(value);
self.ast.synthetic_undefined.insert(value);
value
} else {
if !matches!(self.peek().kind, TokenKind::Equals) {
let (msg, help) = if is_const {
(
"`const` declaration requires an initializer",
vec!["const x: T = expr;".to_string()],
)
} else {
(
"`let` declaration requires an initializer",
vec![
"let x: T = expr;".to_string(),
"or declare one that starts empty: `let x: T | undefined;`".to_string(),
],
)
};
self.error_at_peek_with_help(msg, help);
return None;
}
self.advance();
self.parse_expression()?
};
if !self.at_statement_end() {
self.error_at_peek("expected `;` after declaration");
return None;
}
let end = self.finish_statement();
let span = self.span(kw.span.start, end);
let kind = match (binding, name) {
(Some(binding), _) => {
if is_const {
StmtKind::ConstPattern {
binding,
ty,
value,
doc,
}
} else {
StmtKind::LetPattern {
binding,
ty,
value,
doc,
}
}
}
(None, Some(name)) => {
if is_const {
StmtKind::Const {
name,
ty,
value,
doc,
}
} else {
StmtKind::Let {
name,
ty,
value,
doc,
}
}
}
(None, None) => return self.invariant_failure("either binding or name must be Some"),
};
parse_arena_result(self.ast.try_push_stmt(Stmt { kind, span }), &mut self.fatal)
}
fn parse_binding(&mut self) -> Option<Binding> {
match self.peek().kind {
TokenKind::LeftBrace => self.parse_object_binding(),
TokenKind::LeftBracket => self.parse_array_binding(),
_ => {
self.error_at_peek("expected `{` or `[` to start a destructuring pattern");
None
}
}
}
fn parse_object_binding(&mut self) -> Option<Binding> {
let open = self.advance();
let mut fields: Vec<ObjectPatternField> = Vec::new();
let mut rest: Option<Ident> = None;
if matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("empty object destructuring pattern");
return None;
}
loop {
if matches!(self.peek().kind, TokenKind::DotDotDot) {
let dots = self.advance();
let name_tok = self.expect_identifier("expected identifier after `...`")?;
let name = self.ident_from_token(&name_tok);
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek_with_help(
"rest element must be the last element in a destructuring pattern",
vec!["move `...` after every other binding".to_string()],
);
return None;
}
rest = Some(Ident {
name: name.name,
span: self.span(dots.span.start, name.span.end),
});
break;
}
let source_tok = self.expect_property_name("expected field name in object pattern")?;
let source = self.ident_from_token(&source_tok);
let field_span_start = source.span.start;
let (local, field_end) = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
match self.peek().kind {
TokenKind::LeftBrace | TokenKind::LeftBracket => {
self.error_at_peek_with_help(
"nested destructuring is not supported",
vec![
"destructure once and access nested fields explicitly: const { a } = obj; const inner = a.b;"
.to_string(),
],
);
return None;
}
_ => {
let local_tok =
self.expect_identifier("expected identifier after `:` in pattern")?;
let local = self.ident_from_token(&local_tok);
let end = local.span.end;
(local, end)
}
}
} else {
if !matches!(source_tok.kind, TokenKind::Identifier) {
self.error_at_peek("expected `:` after keyword field name in object pattern");
return None;
}
self.reject_strict_mode_reserved_word(&source_tok);
let end = source.span.end;
(source.clone(), end)
};
let default = if matches!(self.peek().kind, TokenKind::Equals) {
self.advance();
Some(self.parse_expression()?)
} else {
None
};
let field_end = match default {
Some(value) => {
parse_arena_result(self.ast.try_expr(value), &mut self.fatal)?
.span
.end
}
None => field_end,
};
fields.push(ObjectPatternField {
source,
local,
default,
span: self.span(field_span_start, field_end),
});
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::RightBrace) {
break;
}
}
TokenKind::RightBrace => break,
_ => {
self.error_at_peek("expected `,` or `}` in object pattern");
return None;
}
}
}
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}` to close object pattern");
return None;
}
let close = self.advance();
Some(Binding::Object {
fields,
rest,
span: self.span(open.span.start, close.span.end),
})
}
fn parse_array_binding(&mut self) -> Option<Binding> {
let open = self.advance();
let mut elems: Vec<Option<Ident>> = Vec::new();
let mut defaults = Vec::new();
let mut rest: Option<Ident> = None;
if matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("empty array destructuring pattern");
return None;
}
loop {
match self.peek().kind {
TokenKind::Comma => {
elems.push(None);
defaults.push(None);
self.advance();
if matches!(self.peek().kind, TokenKind::RightBracket) {
break;
}
continue;
}
TokenKind::RightBracket => break,
TokenKind::DotDotDot => {
let dots = self.advance();
let name_tok = self.expect_identifier("expected identifier after `...`")?;
let name = self.ident_from_token(&name_tok);
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek_with_help(
"rest element must be the last element in a destructuring pattern",
vec!["move `...` after every other binding".to_string()],
);
return None;
}
rest = Some(Ident {
name: name.name,
span: self.span(dots.span.start, name.span.end),
});
break;
}
TokenKind::LeftBrace | TokenKind::LeftBracket => {
self.error_at_peek_with_help(
"nested destructuring is not supported",
vec![
"destructure once and access nested elements explicitly: const [a] = arr; const inner = a[0];"
.to_string(),
],
);
return None;
}
_ => {
let name_tok =
self.expect_identifier("expected identifier in array pattern")?;
let name = self.ident_from_token(&name_tok);
let default = if matches!(self.peek().kind, TokenKind::Equals) {
self.advance();
Some(self.parse_expression()?)
} else {
None
};
defaults.push(default);
elems.push(Some(name));
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::RightBracket) {
break;
}
}
TokenKind::RightBracket => break,
_ => {
self.error_at_peek("expected `,` or `]` in array pattern");
return None;
}
}
}
}
}
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("expected `]` to close array pattern");
return None;
}
let close = self.advance();
Some(Binding::Array {
elems,
defaults,
rest,
span: self.span(open.span.start, close.span.end),
})
}
fn parse_function_decl(&mut self) -> Option<StmtId> {
let doc = self.take_leading_doc();
let kw = self.advance();
let name_tok = self.expect_identifier("expected function name")?;
let name = self.ident_from_token(&name_tok);
let generics = if matches!(self.peek().kind, TokenKind::LessThan) {
self.parse_generic_param_list()?
} else {
Vec::new()
};
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` after function name");
return None;
}
self.advance();
let params = self.parse_param_list(false)?;
self.advance();
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek_with_help(
"expected `:` and return type",
vec!["function name(): T { … }".to_string()],
);
return None;
}
self.advance();
let (return_type, type_predicate) =
self.parse_predicate_or_return_type(TypePos::Anywhere)?;
let body = self.parse_outer_this_boundary(Self::parse_block)?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Function {
name,
generics,
params,
return_type,
type_predicate,
body,
doc,
},
span: self.span(kw.span.start, body_end),
}),
&mut self.fatal,
)
}
fn parse_class_decl(&mut self) -> Option<StmtId> {
let doc = self.take_leading_doc();
let kw = self.advance();
if self.block_depth > 0 {
self.error_at_with_help(
kw.span,
"a `class` must be declared at the top level of the module",
vec!["move this `class` outside any block or function body".to_string()],
);
}
let name = self.parse_type_decl_name("a", "class")?;
let generics = if matches!(self.peek().kind, TokenKind::LessThan) {
self.parse_generic_param_list()?
} else {
Vec::new()
};
let extends = if matches!(self.peek().kind, TokenKind::Extends) {
self.advance();
Some(self.parse_type_annotation()?)
} else {
None
};
let mut implements: Vec<crate::TypeAnnotation> = Vec::new();
if matches!(self.peek().kind, TokenKind::Implements) {
self.advance();
loop {
implements.push(self.parse_type_annotation()?);
if matches!(self.peek().kind, TokenKind::Comma) {
self.advance();
continue;
}
break;
}
}
if !matches!(self.peek().kind, TokenKind::LeftBrace) {
self.error_at_peek("expected `{` after class header");
return None;
}
self.advance();
let mut members: Vec<crate::ClassMember> = Vec::new();
while !matches!(self.peek().kind, TokenKind::RightBrace | TokenKind::Eof) {
if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance();
continue;
}
members.push(self.parse_class_member()?);
}
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}` to close class");
return None;
}
let close = self.advance();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::ClassDecl {
name,
generics,
extends,
implements,
members,
doc,
},
span: self.span(kw.span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn parse_class_member(&mut self) -> Option<crate::ClassMember> {
let doc = self.take_leading_doc();
let member_start = self.peek().span.start;
if self.peek_identifier_text_is("protected") {
let tok = self.advance();
self.error_at_with_help(
tok.span,
"`protected` is not supported",
vec!["use `private` or `public`".to_string()],
);
return None;
}
if self.peek_identifier_text_is("abstract") {
let tok = self.advance();
self.error_at_with_help(
tok.span,
"abstract classes are not supported",
vec!["provide a concrete implementation instead of `abstract`".to_string()],
);
return None;
}
let modifiers = self.parse_class_modifiers();
for (kw, kind) in [
("get", crate::AccessorKind::Get),
("set", crate::AccessorKind::Set),
] {
if self.peek_identifier_text_is(kw)
&& is_property_name(&self.peek_at(1).kind)
&& matches!(self.peek_at(2).kind, TokenKind::LeftParen)
{
if let Some(span) = modifiers.readonly {
self.reject_readonly_modifier(span);
}
if let Some(span) = modifiers.static_span {
self.error_at_with_help(
span,
"static accessors are not supported",
vec!["use a static method: `static name(): T { … }`".to_string()],
);
return None;
}
return self.parse_accessor(member_start, modifiers, kind, doc);
}
}
if self.peek_identifier_text_is("constructor")
&& matches!(self.peek_at(1).kind, TokenKind::LeftParen)
{
if let Some(span) = modifiers.static_span {
self.error_at_with_help(
span,
"a constructor cannot be `static`",
vec![
"remove `static` — the constructor already belongs to the class, not \
instances"
.to_string(),
],
);
return None;
}
if let Some(span) = modifiers.readonly {
self.error_at_with_help(
span,
"a constructor cannot be `readonly`",
vec!["remove `readonly`; it applies to fields".to_string()],
);
return None;
}
return self.parse_constructor(member_start, modifiers.visibility, doc);
}
let name = self.expect_property_ident("expected class member name")?;
let optional_span = if matches!(self.peek().kind, TokenKind::Question) {
Some(self.advance().span)
} else {
None
};
let optional = optional_span.is_some();
if matches!(self.peek().kind, TokenKind::LessThan | TokenKind::LeftParen) {
if let Some(span) = modifiers.readonly {
self.reject_readonly_modifier(span);
}
let generics = if matches!(self.peek().kind, TokenKind::LessThan) {
self.parse_generic_param_list()?
} else {
Vec::new()
};
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` to start the method parameter list");
return None;
}
self.advance();
let params = self.parse_class_member_params(false)?;
self.advance(); if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek_with_help(
"expected `:` and return type",
vec!["method name(): T { … }".to_string()],
);
return None;
}
self.advance();
let return_type = self.parse_type_annotation()?;
if self.at_inserted_semicolon() && matches!(self.peek_at(1).kind, TokenKind::LeftBrace)
{
self.advance();
}
if optional
&& matches!(
self.peek().kind,
TokenKind::Semicolon | TokenKind::RightBrace
)
{
if !generics.is_empty() {
self.error_at(
name.span,
"generic optional method declarations are not supported",
);
return None;
}
let end = if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance().span.end
} else {
return_type.span.end
};
let ty = self.method_declaration_type(
params,
return_type,
self.span(name.span.start, end),
)?;
return Some(crate::ClassMember::Field {
name,
modifiers,
optional: true,
ty,
initializer: None,
span: self.span(member_start, end),
doc,
});
}
let body = self.parse_class_member_body()?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
return Some(crate::ClassMember::Method {
name,
optional,
modifiers,
generics,
params,
return_type,
body,
span: self.span(member_start, body_end),
doc,
});
}
if let Some(span) = optional_span
&& modifiers.static_span.is_some()
{
self.error_at_with_help(
span,
"a static field cannot be optional",
vec!["a static field must be initialized; give it a value with `= …`".to_string()],
);
return None;
}
if !matches!(self.peek().kind, TokenKind::Colon) {
if matches!(self.peek().kind, TokenKind::Equals) {
self.error_at_peek_with_help(
"class fields require a type annotation",
vec!["add `: T` before the initializer, e.g. `name: string = …`".to_string()],
);
} else {
self.error_at_peek("expected `:` and a type for the class field");
}
return None;
}
self.advance();
let ty = self.parse_type_annotation()?;
let initializer = if matches!(self.peek().kind, TokenKind::Equals) {
self.advance();
self.class_member_body_depth += 1;
let init = self.parse_expression();
self.class_member_body_depth -= 1;
Some(init?)
} else {
None
};
if !matches!(self.peek().kind, TokenKind::Semicolon) {
self.error_at_peek("expected `;` after class field");
return None;
}
let semi = self.advance();
Some(crate::ClassMember::Field {
name,
modifiers,
optional,
ty,
initializer,
span: self.span(member_start, semi.span.end),
doc,
})
}
fn parse_constructor(
&mut self,
member_start: u32,
visibility: crate::Visibility,
doc: Option<crate::DocComment>,
) -> Option<crate::ClassMember> {
self.advance(); self.advance(); let params = self.parse_class_member_params(true)?;
self.advance(); if matches!(self.peek().kind, TokenKind::Colon) {
let colon = self.advance();
let _ = self.parse_type_annotation();
self.error_at_with_help(
colon.span,
"a constructor cannot declare a return type",
vec![
"drop the `: T` — a constructor always returns the class instance".to_string(),
],
);
}
let body = self.parse_class_member_body()?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
Some(crate::ClassMember::Constructor {
visibility,
params,
body,
span: self.span(member_start, body_end),
doc,
})
}
fn parse_accessor(
&mut self,
member_start: u32,
modifiers: crate::ClassModifiers,
kind: crate::AccessorKind,
doc: Option<crate::DocComment>,
) -> Option<crate::ClassMember> {
self.advance(); let name = self.expect_property_ident("expected accessor name")?;
self.advance(); let mut params = self.parse_class_member_params(false)?;
self.advance(); let param = match kind {
crate::AccessorKind::Get => {
if !params.is_empty() {
self.error_at(name.span, "a getter cannot declare parameters");
}
None
}
crate::AccessorKind::Set => {
if params.len() != 1 {
self.error_at(name.span, "a setter must declare exactly one parameter");
return None;
}
Some(Box::new(params.remove(0)))
}
};
let return_type = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
Some(self.parse_type_annotation()?)
} else {
if kind == crate::AccessorKind::Get {
self.error_at_peek_with_help(
"expected `:` and return type",
vec!["get name(): T { … }".to_string()],
);
}
None
};
let body = self.parse_class_member_body()?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
Some(crate::ClassMember::Accessor {
name,
modifiers,
kind,
param,
return_type,
body,
span: self.span(member_start, body_end),
doc,
})
}
fn parse_class_modifiers(&mut self) -> crate::ClassModifiers {
let mut visibility = crate::Visibility::Public;
let mut visibility_span: Option<Span> = None;
let mut readonly: Option<Span> = None;
let mut static_span: Option<Span> = None;
loop {
if self.peek_word_is_class_modifier("public")
|| self.peek_word_is_class_modifier("private")
{
let (kind, word) = if self.peek_identifier_text_is("private") {
(crate::Visibility::Private, "private")
} else {
(crate::Visibility::Public, "public")
};
let tok = self.advance();
if visibility_span.is_some() {
self.error_at_with_help(
tok.span,
"a class member may have at most one visibility modifier",
vec!["keep a single `public` or `private`".to_string()],
);
continue;
}
let preceding = static_span
.map(|_| "static")
.or(readonly.map(|_| "readonly"));
if let Some(preceding) = preceding {
self.error_at_with_help(
tok.span,
format!("`{word}` must come before `{preceding}`"),
vec![format!("write `{word} {preceding} <name>`")],
);
}
visibility = kind;
visibility_span = Some(tok.span);
continue;
}
if self.peek_word_is_class_modifier("static") {
let tok = self.advance();
if static_span.is_some() {
self.error_at(tok.span, "duplicate `static` modifier");
} else {
static_span = Some(tok.span);
}
continue;
}
if self.peek_word_is_class_modifier("readonly") {
let tok = self.advance();
if readonly.is_some() {
self.error_at(tok.span, "duplicate `readonly` modifier");
} else {
readonly = Some(tok.span);
}
continue;
}
break;
}
crate::ClassModifiers {
visibility,
visibility_span,
readonly,
static_span,
}
}
fn peek_word_is_class_modifier(&self, word: &str) -> bool {
if !self.peek_identifier_text_is(word) {
return false;
}
if word != "static" && self.line_break_after_peek() {
return false;
}
let next = &self.peek_at(1).kind;
is_property_name(next) || matches!(next, TokenKind::StringLiteral(_))
}
fn parse_class_member_params(
&mut self,
allow_param_properties: bool,
) -> Option<Vec<ParamDecl>> {
let saved = self.class_member_body_depth;
self.class_member_body_depth = 1;
let params = self.parse_param_list(allow_param_properties);
self.class_member_body_depth = saved;
params
}
fn parse_class_member_body(&mut self) -> Option<StmtId> {
self.class_member_body_depth += 1;
let body = self.parse_block();
self.class_member_body_depth -= 1;
body
}
fn parse_interface_decl(&mut self) -> Option<StmtId> {
let doc = self.take_leading_doc();
let kw = self.advance();
let name = self.parse_type_decl_name("an", "interface")?;
let generics = if matches!(self.peek().kind, TokenKind::LessThan) {
self.parse_generic_param_list()?
} else {
Vec::new()
};
let mut extends = Vec::new();
if matches!(self.peek().kind, TokenKind::Extends) {
self.advance();
loop {
extends.push(self.parse_type_annotation()?);
if !matches!(self.peek().kind, TokenKind::Comma) {
break;
}
self.advance();
}
}
if !matches!(self.peek().kind, TokenKind::LeftBrace) {
self.error_at_peek("expected `{` after interface name");
return None;
}
self.advance();
let mut members: Vec<crate::InterfaceMember> = Vec::new();
while !matches!(self.peek().kind, TokenKind::RightBrace | TokenKind::Eof)
&& !self.peek_starts_declaration()
{
let member_doc = self.take_leading_doc();
if matches!(self.peek().kind, TokenKind::LeftParen) {
let open = self.advance();
let params = self.parse_param_list(false)?;
self.advance();
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` and return type");
return None;
}
self.advance();
let return_type = self.parse_type_annotation()?;
let end = self.finish_interface_member(return_type.span.end)?;
let span = self.span(open.span.start, end);
let sentinel = Ident {
name: "@call".to_string(),
span: open.span,
};
members.push(crate::InterfaceMember::Method {
name: sentinel,
optional: false,
generics: Vec::new(),
params,
return_type,
span,
doc: member_doc,
});
continue;
}
if matches!(self.peek().kind, TokenKind::LessThan) {
self.error_at_peek("generic call signatures are not yet supported");
return None;
}
let member_start = self.peek().span.start;
let readonly = self.eat_readonly_property_modifier();
if self.peek_is_parameterless_index_signature() {
self.skip_parameterless_index_signature(member_start)?;
self.finish_interface_member(self.prev_token_end())?;
continue;
}
if matches!(self.peek().kind, TokenKind::LeftBracket) {
let signature = self.parse_index_signature(readonly)?;
self.finish_interface_member(signature.span.end)?;
members.push(crate::InterfaceMember::IndexSignature(signature));
continue;
}
let member_name = self.expect_property_ident("expected interface member name")?;
let optional = matches!(self.peek().kind, TokenKind::Question);
if optional {
self.advance();
}
if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
let ty = self.parse_type_annotation()?;
let end = self.finish_interface_member(ty.span.end)?;
let span = self.span(member_name.span.start, end);
members.push(crate::InterfaceMember::Property {
name: member_name,
ty,
optional,
readonly,
span,
doc: member_doc,
});
continue;
}
if readonly {
self.reject_readonly_modifier(self.readonly_modifier_span(member_start));
}
let m_generics = if matches!(self.peek().kind, TokenKind::LessThan) {
self.parse_generic_param_list()?
} else {
Vec::new()
};
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek(
"expected `(` to start a method signature or `:` to start a property",
);
return None;
}
self.advance();
let params = self.parse_param_list(false)?;
self.advance();
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` and return type");
return None;
}
self.advance();
let return_type = self.parse_type_annotation()?;
let end = self.finish_interface_member(return_type.span.end)?;
let span = self.span(member_name.span.start, end);
if optional {
if !m_generics.is_empty() {
self.error_at(
member_name.span,
"generic optional method declarations are not supported",
);
return None;
}
let ty = self.method_declaration_type(params, return_type, span)?;
members.push(crate::InterfaceMember::Property {
name: member_name,
ty,
optional: true,
readonly,
span,
doc: member_doc,
});
} else {
members.push(crate::InterfaceMember::Method {
name: member_name,
optional,
generics: m_generics,
params,
return_type,
span,
doc: member_doc,
});
}
}
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}` to close interface");
return None;
}
let close = self.advance();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::InterfaceDecl {
name,
generics,
extends,
members,
doc,
},
span: self.span(kw.span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn method_declaration_type(
&mut self,
params: Vec<ParamDecl>,
return_type: TypeAnnotation,
span: Span,
) -> Option<TypeAnnotation> {
let mut fields = Vec::new();
for param in params {
if param.pattern.is_some() || param.default.is_some() {
self.error_at(
param.name.span,
"a method declaration cannot contain a parameter pattern or default value",
);
return None;
}
let Some(ty) = param.ty else {
return self.invariant_failure("method parameter annotation was not retained");
};
fields.push(TypeAnnotationField {
name: param.name,
ty,
optional: param.optional,
readonly: false,
rest: param.rest,
method: false,
});
}
Some(TypeAnnotation {
kind: TypeAnnotationKind::Function {
params: fields,
return_type: Box::new(return_type),
},
span,
})
}
fn finish_interface_member(&mut self, body_end: u32) -> Option<u32> {
self.finish_type_member(body_end, "expected `;`, `,`, or `}` after interface member")
}
fn finish_type_member(&mut self, body_end: u32, message: &str) -> Option<u32> {
match self.peek().kind {
TokenKind::Semicolon | TokenKind::Comma => Some(self.advance().span.end),
TokenKind::RightBrace => Some(body_end),
_ if self.line_break_before_peek() => Some(body_end),
_ => {
self.error_at_peek(message);
None
}
}
}
fn parse_type_alias_decl(&mut self) -> Option<StmtId> {
let doc = self.take_leading_doc();
let kw = self.advance();
let name = self.parse_type_decl_name("a", "type alias")?;
let generics: Vec<Ident> = if matches!(self.peek().kind, TokenKind::LessThan) {
self.parse_generic_param_list()?
} else {
Vec::new()
};
if !matches!(self.peek().kind, TokenKind::Equals) {
self.error_at_peek("expected `=` after type alias name");
return None;
}
self.advance();
let ty = self.parse_type_annotation()?;
let end = if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance().span.end
} else if self.line_break_before_peek() {
self.prev_token_end()
} else {
self.error_at_peek("expected `;` after type alias body");
return None;
};
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::TypeAliasDecl {
name,
generics,
ty,
doc,
},
span: self.span(kw.span.start, end),
}),
&mut self.fatal,
)
}
fn parse_enum_decl(&mut self) -> Option<StmtId> {
#[derive(Copy, Clone)]
enum Kind {
Number,
String,
}
let doc = self.take_leading_doc();
let kw = self.advance();
let name = self.parse_type_decl_name("an", "enum")?;
if !matches!(self.peek().kind, TokenKind::LeftBrace) {
self.error_at_peek("expected `{` after enum name");
return None;
}
self.advance();
let mut members: Vec<EnumMember> = Vec::new();
let mut first_explicit: Option<(Kind, Span)> = None;
while !matches!(self.peek().kind, TokenKind::RightBrace | TokenKind::Eof) {
if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance();
continue;
}
let member_doc = self.take_leading_doc();
let member_name_tok = self.expect_identifier_name("expected enum member name")?;
let member_name = self.ident_from_token(&member_name_tok);
let value = if matches!(self.peek().kind, TokenKind::Equals) {
self.advance();
let init = self.parse_enum_initializer()?;
let (this_kind, this_span) = match &init {
EnumInitializer::Number { span, .. } => (Kind::Number, *span),
EnumInitializer::String { span, .. } => (Kind::String, *span),
};
match first_explicit {
None => first_explicit = Some((this_kind, this_span)),
Some((prev, _)) => {
let mismatched = matches!(
(prev, this_kind),
(Kind::Number, Kind::String) | (Kind::String, Kind::Number)
);
if mismatched {
self.error_at(
this_span,
"mixed numeric and string enum members are not allowed",
);
}
}
}
Some(init)
} else {
None
};
let end_span = value
.as_ref()
.map_or(member_name.span, super::ast::EnumInitializer::span);
let member_span = self.span(member_name.span.start, end_span.end);
members.push(EnumMember {
name: member_name,
value,
span: member_span,
doc: member_doc,
});
match self.peek().kind {
TokenKind::Comma | TokenKind::Semicolon => {
self.advance();
}
TokenKind::RightBrace | TokenKind::Eof => {}
_ => {
self.error_at_peek("expected `,` or `}` after enum member");
return None;
}
}
}
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}` to close enum");
return None;
}
let close = self.advance();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::EnumDecl { name, members, doc },
span: self.span(kw.span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn parse_export(&mut self) -> Option<StmtId> {
let export_tok = self.advance();
let export_span = export_tok.span;
let nested = self.block_depth > 0;
if nested {
self.error_at_with_help(
export_span,
"`export` statements must appear at the top of the file",
vec!["move this `export` to the top level of the module".to_string()],
);
}
if self.peek_identifier_text_is("type")
&& matches!(self.peek_at(1).kind, TokenKind::LeftBrace)
{
self.advance();
}
let declares = matches!(
self.peek().kind,
TokenKind::Function
| TokenKind::Class
| TokenKind::Const
| TokenKind::Let
| TokenKind::Interface
| TokenKind::Enum
) || self.at_type_alias_head();
if declares {
if export_tok.leading_doc.is_some() && self.peek().leading_doc.is_none() {
if let Some(token) = self.tokens.get_mut(self.pos) {
token.leading_doc = export_tok.leading_doc;
} else {
return self.invariant_failure("export cursor is outside token stream");
}
}
let id = self.parse_statement()?;
if nested {
return None;
}
self.ast.exported_decls.push(ExportedDecl {
stmt: id,
export_span,
});
return Some(id);
}
match self.peek().kind {
TokenKind::LeftBrace => {
let doc = export_tok
.leading_doc
.as_ref()
.and_then(|raw| self.parse_doc(raw))
.or_else(|| self.take_leading_doc());
self.parse_export_from(export_span, nested, doc)
}
TokenKind::Default => {
self.error_at_peek_with_help(
"`export default` is not supported",
vec![
"default exports are out of scope; drop `default` — a named \
`export` marks the declaration visible to sibling modules"
.to_string(),
],
);
None
}
_ => {
self.error_at_peek_with_help(
"expected a declaration after `export`",
vec![
"`export` may precede `function`, `class`, `const`, `let`, `type`, \
`interface`, or `enum`, or take the form `export { … }`"
.to_string(),
],
);
None
}
}
}
fn parse_export_from(
&mut self,
export_span: Span,
nested: bool,
doc: Option<crate::DocComment>,
) -> Option<StmtId> {
let (specs, open_span) = self.parse_specifier_list(SpecifierList::Export)?;
if specs.is_empty() {
self.error_at_with_help(
open_span,
"empty export specifier list",
vec!["name what you re-export: `export { name } from \"./util\";`".to_string()],
);
return None;
}
let source = if self.peek_identifier_text_is("from") {
self.advance();
let module_tok = self.peek().clone();
let module = if let TokenKind::StringLiteral(s) = &module_tok.kind {
s.clone()
} else {
self.error_at_peek("expected module specifier string after `from`");
return None;
};
self.advance();
Some((module, module_tok.span))
} else {
None
};
if !matches!(self.peek().kind, TokenKind::Semicolon) {
self.error_at_peek("expected `;` after export statement");
return None;
}
let semi = self.advance();
if nested {
return None;
}
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::ExportFrom { specs, source, doc },
span: self.span(export_span.start, semi.span.end),
}),
&mut self.fatal,
)
}
fn parse_import_decl(&mut self) -> Option<StmtId> {
let doc = self.take_leading_doc();
let kw = self.advance();
let kw_span = kw.span;
let nested = self.block_depth > 0;
if nested {
self.error_at_with_help(
kw_span,
"`import` statements must appear at the top of the file",
vec!["move this `import` outside any block / function body".to_string()],
);
}
let kind = self.parse_import_kind()?;
if !self.peek_identifier_text_is("from") {
self.error_at_peek("expected `from` after import specifier list");
return None;
}
self.advance();
let module_tok = self.peek().clone();
let module = if let TokenKind::StringLiteral(s) = &module_tok.kind {
s.clone()
} else {
self.error_at_peek("expected module specifier string after `from`");
return None;
};
let module_span = module_tok.span;
self.advance();
if !matches!(self.peek().kind, TokenKind::Semicolon) {
self.error_at_peek("expected `;` after import statement");
return None;
}
let semi = self.advance();
if nested {
return None;
}
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Import {
module,
module_span,
kind,
doc,
},
span: self.span(kw_span.start, semi.span.end),
}),
&mut self.fatal,
)
}
fn parse_import_kind(&mut self) -> Option<ImportKind> {
if self.peek_identifier_text_is("type")
&& match self.peek_at(1).kind {
TokenKind::LeftBrace | TokenKind::Star => true,
TokenKind::Identifier => {
!(self.peek_at_is_word(1, "from")
&& matches!(self.peek_at(2).kind, TokenKind::StringLiteral(_)))
}
_ => false,
}
{
self.advance();
}
match self.peek().kind {
TokenKind::LeftBrace => self.parse_named_imports(),
TokenKind::Identifier => {
let local_tok = self.advance();
let local_name = self.ident_from_token(&local_tok);
if matches!(self.peek().kind, TokenKind::Comma) {
self.error_at_peek_with_help(
"combining default and named imports is not supported",
vec!["use two separate `import` statements".to_string()],
);
return None;
}
Some(ImportKind::Namespace { local_name })
}
TokenKind::Star => {
self.advance();
if !self.peek_identifier_text_is("as") {
self.error_at_peek_with_help(
"expected `as <name>` after `import *`",
vec![
"name the namespace binding: \
`import * as ns from \"pkg\";` (or use the \
short form: `import ns from \"pkg\";`)"
.to_string(),
],
);
return None;
}
self.advance();
let local_tok = self.expect_identifier("expected namespace name after `as`")?;
let local_name = self.ident_from_token(&local_tok);
Some(ImportKind::Namespace { local_name })
}
TokenKind::StringLiteral(_) => {
self.error_at_peek_with_help(
"side-effect-only imports are not supported",
vec![
"name what you import: `import { name } from \"pkg\";` \
or `import ns from \"pkg\";`"
.to_string(),
],
);
None
}
_ => {
self.error_at_peek("expected `{` or namespace name after `import`");
None
}
}
}
fn parse_named_imports(&mut self) -> Option<ImportKind> {
let (specs, open_span) = self.parse_specifier_list(SpecifierList::Import)?;
if specs.is_empty() {
self.error_at_with_help(
open_span,
"empty import specifier list",
vec!["name what you import: `import { name } from \"pkg\";`".to_string()],
);
return None;
}
Some(ImportKind::Named(specs))
}
fn parse_specifier_list(
&mut self,
list: SpecifierList,
) -> Option<(Vec<ImportSpecifier>, Span)> {
let what = list.noun();
let open = self.advance();
let mut specs: Vec<ImportSpecifier> = Vec::new();
while !matches!(self.peek().kind, TokenKind::RightBrace | TokenKind::Eof) {
if self.peek_identifier_text_is("type")
&& matches!(self.peek_at(1).kind, TokenKind::Identifier)
&& !self.peek_at_is_word(1, "as")
{
self.advance();
}
let imported_tok =
self.expect_identifier_name(&format!("expected {what} specifier name"))?;
let imported_name = self.ident_from_token(&imported_tok);
let binds_local = list == SpecifierList::Import;
let local_name = if self.peek_identifier_text_is("as") {
self.advance();
let local_tok = self.expect_identifier_name("expected local name after `as`")?;
if binds_local {
self.reject_strict_mode_reserved_word(&local_tok);
}
self.ident_from_token(&local_tok)
} else {
if binds_local {
self.reject_strict_mode_reserved_word(&imported_tok);
}
imported_name.clone()
};
specs.push(ImportSpecifier {
imported_name,
local_name,
});
match self.peek().kind {
TokenKind::Comma => {
self.advance();
}
TokenKind::RightBrace | TokenKind::Eof => {}
_ => {
self.error_at_peek(format!("expected `,` or `}}` after {what} specifier"));
return None;
}
}
}
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek(format!("expected `}}` to close {what} specifier list"));
return None;
}
self.advance();
Some((specs, open.span))
}
fn parse_enum_initializer(&mut self) -> Option<EnumInitializer> {
let neg = if matches!(self.peek().kind, TokenKind::Minus) {
Some(self.advance())
} else {
None
};
let tok = self.advance();
match &tok.kind {
TokenKind::NumberLiteral(n) => {
let value = if neg.is_some() { -n } else { *n };
let value = if value == 0.0 { 0.0 } else { value };
let start = neg.as_ref().map_or(tok.span.start, |t| t.span.start);
Some(EnumInitializer::Number {
value,
span: self.span(start, tok.span.end),
})
}
TokenKind::StringLiteral(s) => {
if let Some(neg_tok) = &neg {
self.error_at(
self.span(neg_tok.span.start, tok.span.end),
"cannot negate a string enum initializer",
);
return None;
}
Some(EnumInitializer::String {
value: s.clone(),
span: tok.span,
})
}
_ => {
let span = neg.map_or(tok.span, |t| self.span(t.span.start, tok.span.end));
self.error_at(
span,
"expected a number or string literal for enum initializer",
);
None
}
}
}
fn is_contextual_type_operator(&self, index: usize, operator: &str) -> bool {
let Some(tok) = self.tokens.get(index) else {
return false;
};
if !self.token_is_word(tok, operator) {
return false;
}
matches!(
self.tokens.get(index + 1).map(|t| &t.kind),
Some(
TokenKind::Identifier
| TokenKind::Void
| TokenKind::LeftBrace
| TokenKind::LeftParen
| TokenKind::LeftBracket
| TokenKind::StringLiteral(_)
| TokenKind::NumberLiteral(_)
| TokenKind::BooleanLiteral(_)
| TokenKind::NullLiteral
)
)
}
fn parse_type_argument_list(&mut self) -> Option<(Vec<TypeAnnotation>, u32)> {
let lt = self.advance();
if matches!(self.peek().kind, TokenKind::GreaterThan) {
self.error_at(lt.span, "empty generic argument list `<>` is not allowed");
return None;
}
let mut args = Vec::new();
loop {
args.push(self.parse_type_annotation()?);
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::GreaterThan) {
let gt = self.advance();
return Some((args, gt.span.end));
}
}
TokenKind::GreaterThan => {
let gt = self.advance();
return Some((args, gt.span.end));
}
_ => {
self.error_at(
lt.span,
"expected `,` or `>` to close generic argument list",
);
return None;
}
}
}
}
fn parse_type_decl_name(&mut self, article: &str, noun: &str) -> Option<Ident> {
let name_tok = self.expect_identifier(&format!("expected {noun} name"))?;
let name = self.ident_from_token(&name_tok);
if BUILT_IN_TYPE_NAMES.contains(&name.name.as_str()) {
self.error_at_with_help(
name.span,
format!(
"`{}` is a built-in type and can't be used as {article} {noun} name",
name.name
),
vec![format!("rename the {noun}")],
);
}
Some(name)
}
fn parse_generic_param_list(&mut self) -> Option<Vec<Ident>> {
let lt = self.advance();
if matches!(self.peek().kind, TokenKind::GreaterThan) {
self.error_at(lt.span, "empty generic parameter list `<>` is not allowed");
return None;
}
let mut generics = Vec::new();
loop {
let name_tok = self.expect_identifier("expected generic parameter name")?;
let name = self.ident_from_token(&name_tok);
if generics.iter().any(|g: &Ident| g.name == name.name) {
self.error_at_with_help(
name.span,
format!("duplicate type parameter `{}`", name.name),
vec!["give each type parameter its own name".to_string()],
);
} else {
generics.push(name);
}
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::GreaterThan) {
self.advance();
return Some(generics);
}
}
TokenKind::GreaterThan => {
self.advance();
return Some(generics);
}
_ => {
self.error_at_peek("expected `,` or `>`");
return None;
}
}
}
}
fn parse_param_list(&mut self, allow_param_properties: bool) -> Option<Vec<ParamDecl>> {
if matches!(self.peek().kind, TokenKind::RightParen) {
return Some(Vec::new());
}
let mut params = Vec::new();
loop {
let param = self.parse_param(allow_param_properties)?;
let is_rest = param.rest;
params.push(param);
match self.peek().kind {
TokenKind::Comma => {
if is_rest {
let comma = self.advance();
self.error_at(comma.span, "rest parameter must be the last parameter");
return None;
}
self.advance();
if matches!(self.peek().kind, TokenKind::RightParen) {
break;
}
}
TokenKind::RightParen => break,
_ => {
self.error_at_peek("expected `,` or `)`");
return None;
}
}
}
self.check_parameter_order(params.iter().map(ParamOrder::of_param));
Some(params)
}
fn parse_param(&mut self, allow_param_properties: bool) -> Option<ParamDecl> {
let modifiers = if self.peek_word_is_class_modifier("public")
|| self.peek_word_is_class_modifier("private")
|| self.peek_word_is_class_modifier("readonly")
{
let span = self.peek().span;
let parsed = self.parse_class_modifiers();
if allow_param_properties {
Some(parsed)
} else {
self.error_at_with_help(
span,
"parameter properties are only allowed in a constructor",
vec![
"move the `public`/`private`/`readonly` modifier to a constructor parameter"
.to_string(),
],
);
None
}
} else {
None
};
let rest = if matches!(self.peek().kind, TokenKind::DotDotDot) {
self.advance();
true
} else {
false
};
if modifiers.is_some() && rest {
self.error_at_peek("a parameter property cannot be a rest parameter");
return None;
}
let (name, pattern) = match self.peek().kind {
TokenKind::LeftBrace | TokenKind::LeftBracket => {
if rest {
self.error_at_peek("rest parameter cannot be destructured");
return None;
}
if modifiers.is_some() {
self.error_at_peek("a parameter property cannot be destructured");
return None;
}
let binding = self.parse_binding()?;
let placeholder = Ident {
name: String::new(),
span: binding.span(),
};
(placeholder, Some(binding))
}
_ => {
let name_tok = self.expect_identifier(if rest {
"expected identifier after `...`"
} else {
"expected parameter name"
})?;
let name = self.ident_from_token(&name_tok);
(name, None)
}
};
let optional = self.parse_parameter_optional(rest, pattern.is_some())?;
let ty = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
Some(self.parse_type_annotation()?)
} else if !rest && matches!(self.peek().kind, TokenKind::Equals) {
None
} else {
if rest {
self.error_at_peek_with_help(
"rest parameter requires a type annotation",
vec!["function name(...args: T[]): R { … }".to_string()],
);
} else {
self.error_at_peek_with_help(
"parameter requires a type annotation",
vec!["add a type (`x: T`) or a default initializer (`x = value`)".to_string()],
);
}
return None;
};
let default = self.parse_parameter_default(rest, optional)?;
Some(ParamDecl {
name,
ty,
default,
optional,
pattern,
rest,
modifiers,
})
}
fn parse_parameter_optional(&mut self, rest: bool, pattern: bool) -> Option<bool> {
if !matches!(self.peek().kind, TokenKind::Question) {
return Some(false);
}
let question = self.advance();
if rest || pattern {
self.error_at(
question.span,
if rest {
"a rest parameter cannot be optional"
} else {
"a destructured parameter cannot be optional; use a default value"
},
);
return None;
}
Some(true)
}
fn parse_parameter_default(&mut self, rest: bool, optional: bool) -> Option<Option<ExprId>> {
if !matches!(self.peek().kind, TokenKind::Equals) {
return Some(None);
}
if rest {
self.error_rest_parameter_default();
return None;
}
if optional {
self.error_at_peek_with_help(
"an optional parameter cannot have a default value",
vec!["drop the `?`: a parameter with a default is already optional".to_string()],
);
}
self.advance();
Some(Some(self.parse_expression()?))
}
fn error_rest_parameter_default(&mut self) {
self.error_at_peek_with_help(
"rest parameter cannot have a default value",
vec!["omit the `= …`; an unspecified rest defaults to `[]`".to_string()],
);
}
fn check_parameter_order<'p>(&mut self, params: impl IntoIterator<Item = ParamOrder<'p>>) {
let mut follows_optional = false;
for param in params {
if param.optional {
follows_optional = true;
continue;
}
if !follows_optional || !param.required {
continue;
}
let label = match param.name {
Some(name) => format!("parameter `{name}`"),
None => "destructured parameter".to_string(),
};
self.error_at_with_help(
param.span,
format!("required {label} cannot follow an optional parameter"),
vec!["make it optional too (`x?: T`), give it a default (`x: T = …`), or move it before the optional parameters".to_string()],
);
return;
}
}
fn eat_asi_semicolon_before(&mut self, next: TokenKind) {
if matches!(self.peek().kind, TokenKind::Semicolon) && self.peek_at(1).kind == next {
self.advance();
}
}
fn parse_block(&mut self) -> Option<StmtId> {
self.eat_asi_semicolon_before(TokenKind::LeftBrace);
if !matches!(self.peek().kind, TokenKind::LeftBrace) {
self.error_at_peek("expected `{`");
return None;
}
let open = self.advance();
self.block_depth += 1;
let mut stmts = Vec::new();
while !matches!(self.peek().kind, TokenKind::RightBrace | TokenKind::Eof) {
if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance();
continue;
}
let pos_before = self.pos;
if let Some(id) = self.parse_statement() {
stmts.push(id);
} else {
self.recover();
if self.pos == pos_before && !self.is_at_eof() {
self.advance();
}
}
}
self.block_depth -= 1;
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}`");
return None;
}
let close = self.advance();
let span = self.span(open.span.start, close.span.end);
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Block(stmts),
span,
}),
&mut self.fatal,
)
}
fn parse_control_body(&mut self) -> Option<StmtId> {
if matches!(self.peek().kind, TokenKind::LeftBrace) {
return self.parse_block();
}
let keyword = match self.peek().kind {
TokenKind::Let => Some(("a", "let")),
TokenKind::Const => Some(("a", "const")),
TokenKind::Function => Some(("a", "function")),
TokenKind::Interface => Some(("an", "interface")),
TokenKind::Enum => Some(("an", "enum")),
_ if self.peek_identifier_text_is("type") => Some(("a", "type")),
_ => None,
};
let declares = match self.peek_at(1).kind {
TokenKind::Identifier => true,
TokenKind::LeftBracket | TokenKind::LeftBrace => !matches!(keyword, Some((_, "type"))),
_ => false,
};
if let Some((article, keyword)) = keyword
&& declares
{
self.error_at_peek_with_help(
format!(
"{article} `{keyword}` declaration can't be the body of a statement without braces"
),
vec![format!("wrap it in braces: `{{ {keyword} … }}`")],
);
}
self.block_depth += 1;
let stmt = self.parse_statement();
self.block_depth -= 1;
let stmt = stmt?;
let span = parse_arena_result(self.ast.try_stmt(stmt), &mut self.fatal)?.span;
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Block(vec![stmt]),
span,
}),
&mut self.fatal,
)
}
fn parse_loop_body(&mut self) -> Option<StmtId> {
if !matches!(self.peek().kind, TokenKind::Semicolon) {
return self.parse_control_body();
}
self.empty_body_at_semicolon()
}
fn parse_branch_body(&mut self, keyword: &str) -> Option<StmtId> {
if !matches!(self.peek().kind, TokenKind::Semicolon) {
return self.parse_control_body();
}
self.error_at_peek_with_help(
format!("`{keyword}` has an empty body"),
vec![format!("remove the `;` after `{keyword}`")],
);
self.empty_body_at_semicolon()
}
fn empty_body_at_semicolon(&mut self) -> Option<StmtId> {
let semi = self.advance();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Block(vec![]),
span: semi.span,
}),
&mut self.fatal,
)
}
fn parse_if(&mut self) -> Option<StmtId> {
self.with_recursion_limit(Self::parse_if_inner)
}
fn parse_if_inner(&mut self) -> Option<StmtId> {
let kw = self.advance();
let condition = self.parse_paren_condition()?;
let then_block = self.parse_branch_body("if")?;
let else_block = if matches!(self.peek().kind, TokenKind::Else) {
self.advance();
if matches!(self.peek().kind, TokenKind::If) {
Some(self.parse_if()?)
} else {
Some(self.parse_branch_body("else")?)
}
} else {
None
};
let end = match else_block {
Some(id) => {
parse_arena_result(self.ast.try_stmt(id), &mut self.fatal)?
.span
.end
}
None => {
parse_arena_result(self.ast.try_stmt(then_block), &mut self.fatal)?
.span
.end
}
};
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::If {
condition,
then_block,
else_block,
},
span: self.span(kw.span.start, end),
}),
&mut self.fatal,
)
}
fn parse_while(&mut self) -> Option<StmtId> {
let kw = self.advance();
let condition = self.parse_paren_condition()?;
let body = self.parse_loop_body()?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::While { condition, body },
span: self.span(kw.span.start, body_end),
}),
&mut self.fatal,
)
}
fn parse_for(&mut self) -> Option<StmtId> {
let kw = self.advance();
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` after `for`");
return None;
}
self.advance();
if self.scan_for_of_shape() {
self.parse_for_of_tail(kw)
} else {
self.parse_c_for_tail(kw)
}
}
fn scan_for_of_shape(&self) -> bool {
if !matches!(self.peek().kind, TokenKind::Let | TokenKind::Const) {
return false;
}
let (start, mut depth) = match self.peek_at(1).kind {
TokenKind::Identifier => match self.peek_at(2).kind {
TokenKind::Identifier if self.peek_at_is_word(2, "of") => return true,
TokenKind::Colon => (3, 0i32),
_ => return false,
},
TokenKind::LeftBracket | TokenKind::LeftBrace => (2, 1i32),
_ => return false,
};
let mut i = start;
loop {
match self.peek_at(i).kind {
TokenKind::Eof => return false,
TokenKind::LessThan
| TokenKind::LeftParen
| TokenKind::LeftBracket
| TokenKind::LeftBrace => {
depth += 1;
}
TokenKind::GreaterThan
| TokenKind::RightParen
| TokenKind::RightBracket
| TokenKind::RightBrace => {
if depth == 0 {
return false;
}
depth -= 1;
}
TokenKind::Identifier if depth == 0 && self.peek_at_is_word(i, "of") => {
return true;
}
TokenKind::Equals | TokenKind::Semicolon if depth == 0 => return false,
_ => {}
}
i += 1;
}
}
fn parse_c_for_tail(&mut self, kw: Token) -> Option<StmtId> {
let init = if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance();
None
} else {
let init_id = match self.peek().kind {
TokenKind::Let => self.parse_let_or_const(false)?,
TokenKind::Const => self.parse_let_or_const(true)?,
_ => self.parse_expression_statement()?,
};
Some(init_id)
};
let for_stmt = self.finish_c_for(kw, init);
if for_stmt.is_none()
&& let Some(init) = init
{
self.discard_detached_pattern(init);
}
for_stmt
}
fn finish_c_for(&mut self, kw: Token, init: Option<StmtId>) -> Option<StmtId> {
let condition = if matches!(self.peek().kind, TokenKind::Semicolon) {
None
} else {
Some(self.parse_expression()?)
};
if !matches!(self.peek().kind, TokenKind::Semicolon) {
self.error_at_peek("expected `;` after `for` condition");
return None;
}
self.advance();
let update = if matches!(self.peek().kind, TokenKind::RightParen) {
None
} else {
Some(self.parse_for_update_stmt()?)
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)` after `for` update");
return None;
}
self.advance();
let body = self.parse_loop_body()?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::For {
init,
condition,
update,
body,
},
span: self.span(kw.span.start, body_end),
}),
&mut self.fatal,
)
}
fn discard_detached_pattern(&mut self, init: StmtId) {
let Some(stmt) = parse_arena_result(self.ast.try_stmt_mut(init), &mut self.fatal) else {
return;
};
if matches!(
stmt.kind,
StmtKind::LetPattern { .. } | StmtKind::ConstPattern { .. }
) {
stmt.kind = StmtKind::Block(Vec::new());
}
}
fn parse_for_update_stmt(&mut self) -> Option<StmtId> {
let expr_id = self.parse_expression()?;
let span = parse_arena_result(self.ast.try_expr(expr_id), &mut self.fatal)?.span;
let kind = self.statement_kind_for(expr_id)?;
parse_arena_result(self.ast.try_push_stmt(Stmt { kind, span }), &mut self.fatal)
}
fn parse_for_of_tail(&mut self, kw: Token) -> Option<StmtId> {
let binding_tok = self.advance();
let binding_kind = match binding_tok.kind {
TokenKind::Let => crate::BindingKind::Let,
TokenKind::Const => crate::BindingKind::Const,
_ => return self.invariant_failure("scan_for_of_shape gated this"),
};
let (name, binding) = match self.peek().kind {
TokenKind::LeftBracket => {
let b = self.parse_binding()?;
(None, Some(b))
}
TokenKind::LeftBrace => {
let b = self.parse_binding()?;
self.error_at(
b.span(),
"object destructuring is not supported in `for-of`; use array destructuring or unpack inside the loop body",
);
return None;
}
_ => {
let name_tok =
self.expect_identifier("expected identifier after `let`/`const` in `for-of`")?;
(Some(self.ident_from_token(&name_tok)), None)
}
};
let ty = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
Some(self.parse_type_annotation()?)
} else {
None
};
if !self.peek_identifier_text_is("of") {
self.error_at_peek("expected `of` in `for-of`");
return None;
}
self.advance();
let iter = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)` after `for-of` iterable");
return None;
}
self.advance();
let body = self.parse_loop_body()?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
let kind = match (binding, name) {
(Some(binding), _) => StmtKind::ForOfPattern {
binding_kind,
binding,
ty,
iter,
body,
},
(None, Some(name)) => StmtKind::ForOf {
binding_kind,
name,
ty,
iter,
body,
},
(None, None) => return self.invariant_failure("either binding or name must be Some"),
};
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind,
span: self.span(kw.span.start, body_end),
}),
&mut self.fatal,
)
}
fn parse_do_while(&mut self) -> Option<StmtId> {
let kw = self.advance();
let body = self.parse_loop_body()?;
self.eat_asi_semicolon_before(TokenKind::While);
if !matches!(self.peek().kind, TokenKind::While) {
self.error_at_peek("expected `while` after `do` body");
return None;
}
self.advance();
let condition = self.parse_paren_condition()?;
let end = if matches!(self.peek().kind, TokenKind::Semicolon) {
self.advance().span.end
} else {
parse_arena_result(self.ast.try_expr(condition), &mut self.fatal)?
.span
.end
};
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::DoWhile { body, condition },
span: self.span(kw.span.start, end),
}),
&mut self.fatal,
)
}
fn parse_switch(&mut self) -> Option<StmtId> {
let kw = self.advance();
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` after `switch`");
return None;
}
self.advance();
let discriminant = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)` after `switch` discriminant");
return None;
}
self.advance();
if !matches!(self.peek().kind, TokenKind::LeftBrace) {
self.error_at_peek("expected `{` to start `switch` body");
return None;
}
let open = self.advance();
self.block_depth += 1;
let mut cases: Vec<SwitchCase> = Vec::new();
let mut default: Option<SwitchDefault> = None;
let mut pending_values: Vec<ExprId> = Vec::new();
let mut pending_label_start: Option<u32> = None;
let mut pending_default: Option<Span> = None;
loop {
match self.peek().kind {
TokenKind::Eof => break,
TokenKind::Semicolon => {
self.advance();
}
TokenKind::RightBrace => {
if pending_label_start.is_some() {
self.error_at_peek(
"expected case body before `}` — case labels must be followed by statements",
);
return None;
}
break;
}
TokenKind::Case => {
let case_kw = self.advance();
if pending_label_start.is_none() {
pending_label_start = Some(case_kw.span.start);
}
let value = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` after `case` label");
return None;
}
self.advance();
pending_values.push(value);
}
TokenKind::Default => {
let kw_tok = self.advance();
if default.is_some() {
self.error_at(kw_tok.span, "duplicate `default` clause in `switch`");
return None;
}
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` after `default`");
return None;
}
self.advance();
if pending_label_start.is_none() {
pending_label_start = Some(kw_tok.span.start);
}
pending_default = Some(kw_tok.span);
}
_ => {
if pending_label_start.is_none() {
self.error_at_peek(
"expected `case` or `default` at the start of a `switch` body",
);
return None;
}
let Some(label_start) = pending_label_start else {
return self.invariant_failure("switch arm has no label");
};
let body = self.parse_switch_arm_body(label_start)?;
let arm_span =
parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?.span;
if !pending_values.is_empty() {
let first = parse_arena_result(
self.ast.try_expr(pending_values[0]),
&mut self.fatal,
)?
.span
.start;
let case_span = self.span(first, arm_span.end);
cases.push(SwitchCase {
values: std::mem::take(&mut pending_values),
body,
span: case_span,
});
}
if let Some(def_kw_span) = pending_default.take() {
default = Some(SwitchDefault {
body,
span: self.span(def_kw_span.start, arm_span.end),
});
}
pending_label_start = None;
}
}
}
self.block_depth -= 1;
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}` to close `switch` body");
return None;
}
let close = self.advance();
let _ = open;
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Switch {
discriminant,
cases,
default,
},
span: self.span(kw.span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn parse_switch_arm_body(&mut self, body_start: u32) -> Option<StmtId> {
let mut stmts: Vec<StmtId> = Vec::new();
let mut end_offset = body_start;
loop {
if matches!(
self.peek().kind,
TokenKind::Case | TokenKind::Default | TokenKind::RightBrace | TokenKind::Eof
) {
break;
}
if matches!(self.peek().kind, TokenKind::Semicolon) {
end_offset = self.peek().span.end;
self.advance();
continue;
}
let pos_before = self.pos;
if let Some(id) = self.parse_statement() {
end_offset = parse_arena_result(self.ast.try_stmt(id), &mut self.fatal)?
.span
.end;
stmts.push(id);
} else {
self.recover();
if self.pos == pos_before && !self.is_at_eof() {
self.advance();
}
end_offset = end_offset.max(self.prev_token_end());
}
}
let span = self.span(body_start, end_offset);
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Block(stmts),
span,
}),
&mut self.fatal,
)
}
fn parse_break(&mut self) -> Option<StmtId> {
let kw = self.advance();
if !matches!(self.peek().kind, TokenKind::Semicolon) {
self.error_at_peek("expected `;` after `break`");
return None;
}
let semi = self.advance();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Break,
span: self.span(kw.span.start, semi.span.end),
}),
&mut self.fatal,
)
}
fn parse_continue(&mut self) -> Option<StmtId> {
let kw = self.advance();
if !matches!(self.peek().kind, TokenKind::Semicolon) {
self.error_at_peek("expected `;` after `continue`");
return None;
}
let semi = self.advance();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Continue,
span: self.span(kw.span.start, semi.span.end),
}),
&mut self.fatal,
)
}
fn parse_return(&mut self) -> Option<StmtId> {
let kw = self.advance();
let value = if matches!(self.peek().kind, TokenKind::Semicolon) {
None
} else {
Some(self.parse_expression()?)
};
if !self.at_statement_end() {
self.error_at_peek("expected `;` after return");
return None;
}
let end = self.finish_statement();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Return(value),
span: self.span(kw.span.start, end),
}),
&mut self.fatal,
)
}
fn parse_throw(&mut self) -> Option<StmtId> {
let kw = self.advance();
if matches!(self.peek().kind, TokenKind::Semicolon) {
self.error_at_peek_with_help(
"expected expression after `throw`",
vec!["throw new Error(\"message\");".to_string()],
);
return None;
}
let value = self.parse_expression()?;
if !self.at_statement_end() {
self.error_at_peek("expected `;` after `throw`");
return None;
}
let end = self.finish_statement();
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Throw { value },
span: self.span(kw.span.start, end),
}),
&mut self.fatal,
)
}
fn parse_try(&mut self) -> Option<StmtId> {
let kw = self.advance();
let body = self.parse_block()?;
let mut catches: Vec<CatchClause> = Vec::new();
let mut finally: Option<StmtId> = None;
let mut tail_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
loop {
match self.peek().kind {
TokenKind::Catch => {
if finally.is_some() {
self.error_at_peek("`catch` clause must appear before `finally`");
return None;
}
let clause = self.parse_catch_clause()?;
tail_end = clause.span.end;
catches.push(clause);
}
TokenKind::Finally => {
if finally.is_some() {
self.error_at_peek("duplicate `finally` clause");
return None;
}
self.advance();
let block = self.parse_block()?;
tail_end = parse_arena_result(self.ast.try_stmt(block), &mut self.fatal)?
.span
.end;
finally = Some(block);
}
_ => break,
}
}
if catches.is_empty() && finally.is_none() {
self.error_at(
self.span(kw.span.start, tail_end),
"try requires at least one of `catch` or `finally`",
);
return None;
}
parse_arena_result(
self.ast.try_push_stmt(Stmt {
kind: StmtKind::Try {
body,
catches,
finally,
},
span: self.span(kw.span.start, tail_end),
}),
&mut self.fatal,
)
}
fn parse_catch_clause(&mut self) -> Option<CatchClause> {
let kw = self.advance();
let (binding, ty) = if matches!(self.peek().kind, TokenKind::LeftBrace) {
(
Ident {
name: "#catch".into(),
span: kw.span,
},
None,
)
} else {
self.parse_catch_binding()?
};
let body = self.parse_block()?;
let body_end = parse_arena_result(self.ast.try_stmt(body), &mut self.fatal)?
.span
.end;
Some(CatchClause {
binding,
ty,
body,
span: self.span(kw.span.start, body_end),
})
}
fn parse_catch_binding(&mut self) -> Option<(Ident, Option<TypeAnnotation>)> {
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` after `catch`");
return None;
}
self.advance();
let name_tok = self.expect_identifier("expected catch binding name")?;
let binding = self.ident_from_token(&name_tok);
let ty = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
if self.reject_any_catch_annotation() {
None
} else {
Some(self.parse_type_annotation()?)
}
} else {
None
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)` after catch binding");
return None;
}
self.advance();
Some((binding, ty))
}
fn reject_any_catch_annotation(&mut self) -> bool {
let is_bare_any = self.peek_identifier_text_is("any")
&& matches!(self.peek_at(1).kind, TokenKind::RightParen);
if !is_bare_any {
return false;
}
let any_tok = self.advance();
self.error_at_with_help(
any_tok.span,
"`any` is not supported",
vec![
"write `catch (e)` to catch every thrown error, or name an error class, \
as in `catch (e: RangeError)`, to catch only that type"
.to_string(),
],
);
true
}
fn parse_paren_condition(&mut self) -> Option<ExprId> {
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(`");
return None;
}
self.advance();
let expr = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
self.advance();
Some(expr)
}
fn parse_predicate_or_return_type(
&mut self,
type_pos: TypePos,
) -> Option<(
Option<TypeAnnotation>,
Option<crate::TypePredicateAnnotation>,
)> {
if matches!(self.peek().kind, TokenKind::Identifier) && self.peek_at_is_word(1, "is") {
let param_tok = self.advance();
let param = self.ident_from_token(¶m_tok);
self.advance();
let asserted = self.parse_type_annotation_in(type_pos)?;
let span = self.span(param.span.start, asserted.span.end);
return Some((
None,
Some(crate::TypePredicateAnnotation {
param,
asserted,
span,
}),
));
}
Some((Some(self.parse_type_annotation_in(type_pos)?), None))
}
fn parse_type_annotation(&mut self) -> Option<TypeAnnotation> {
self.parse_type_annotation_in(TypePos::Anywhere)
}
fn parse_type_annotation_in(&mut self, type_pos: TypePos) -> Option<TypeAnnotation> {
let leading_pipe = matches!(self.peek().kind, TokenKind::Pipe);
if leading_pipe {
self.advance();
}
let first = self.parse_type_array(type_pos)?;
if !matches!(self.peek().kind, TokenKind::Pipe) {
return Some(first);
}
let start = first.span.start;
let mut end = first.span.end;
let mut members = vec![first];
while matches!(self.peek().kind, TokenKind::Pipe) {
self.advance();
let member = self.parse_type_array(type_pos)?;
end = member.span.end;
members.push(member);
}
Some(TypeAnnotation {
kind: TypeAnnotationKind::Union(members),
span: self.span(start, end),
})
}
fn parse_type_array(&mut self, type_pos: TypePos) -> Option<TypeAnnotation> {
self.with_recursion_limit(|parser| parser.parse_type_array_inner(type_pos))
}
fn parse_type_array_inner(&mut self, type_pos: TypePos) -> Option<TypeAnnotation> {
if self.is_contextual_type_operator(self.pos, "keyof") {
let kw = self.advance();
let operand = self.parse_type_array(type_pos)?;
let span = self.span(kw.span.start, operand.span.end);
return Some(TypeAnnotation {
kind: TypeAnnotationKind::KeyOf(Box::new(operand)),
span,
});
}
if self.is_contextual_type_operator(self.pos, "readonly") {
return self.parse_readonly_type(type_pos);
}
let mut ty = match self.peek().kind {
TokenKind::Typeof => {
let kw = self.advance();
let first = self.expect_identifier("expected a value name after `typeof`")?;
let mut path = vec![self.type_name(first.span)?];
let mut end = first.span.end;
while matches!(self.peek().kind, TokenKind::Dot) {
self.advance();
let seg = self.expect_identifier_name("expected a property name after `.`")?;
end = seg.span.end;
path.push(self.type_name(seg.span)?);
}
TypeAnnotation {
kind: TypeAnnotationKind::TypeOf { path },
span: self.span(kw.span.start, end),
}
}
TokenKind::Identifier | TokenKind::Void => {
let leading_is_identifier = matches!(self.peek().kind, TokenKind::Identifier);
let tok = self.advance();
let first_span = tok.span;
let mut path_end = first_span.end;
let first_name = self.type_name(first_span)?;
let mut path = vec![first_name.clone()];
if leading_is_identifier {
while matches!(self.peek().kind, TokenKind::Dot) {
self.advance();
if !matches!(self.peek().kind, TokenKind::Identifier) {
self.error_at_peek("expected identifier after `.` in type name");
return None;
}
let seg = self.advance();
path_end = seg.span.end;
path.push(self.type_name(seg.span)?);
}
}
let (args, end) = if matches!(self.peek().kind, TokenKind::LessThan)
&& !self.line_break_before_peek()
{
self.parse_type_argument_list()?
} else {
(Vec::new(), path_end)
};
let outer_span = self.span(first_span.start, end);
let kind = if path.len() == 1 {
if first_name.name == "any" {
self.error_at_with_help(
first_span,
"`any` is not supported",
vec![
"annotate a concrete type instead — an object shape \
like `{ id: string }`, a named `interface`, or \
`unknown` (a safe dynamic type you narrow before use)"
.to_string(),
],
);
}
TypeAnnotationKind::Name {
name: first_name,
args,
}
} else {
TypeAnnotationKind::Qualified { path, args }
};
TypeAnnotation {
kind,
span: outer_span,
}
}
TokenKind::NullLiteral => {
let tok = self.advance();
TypeAnnotation {
kind: TypeAnnotationKind::Name {
name: self.type_name(tok.span)?,
args: Vec::new(),
},
span: tok.span,
}
}
TokenKind::StringLiteral(_) => {
let tok = self.advance();
let span = tok.span;
let TokenKind::StringLiteral(s) = tok.kind else {
unreachable!("advance preserves the matched StringLiteral variant");
};
TypeAnnotation {
kind: TypeAnnotationKind::StringLiteral(s),
span,
}
}
TokenKind::NumberLiteral(_) => {
let tok = self.advance();
let span = tok.span;
let TokenKind::NumberLiteral(v) = tok.kind else {
unreachable!("advance preserves the matched NumberLiteral variant");
};
let canonical = if v == 0.0 { 0.0 } else { v };
TypeAnnotation {
kind: TypeAnnotationKind::NumberLiteral(crate::types::LiteralF64(canonical)),
span,
}
}
TokenKind::BigIntLiteral(_) => {
let tok = self.advance();
let TokenKind::BigIntLiteral(digits) = tok.kind else {
unreachable!("advance preserves the matched BigIntLiteral variant");
};
TypeAnnotation {
kind: TypeAnnotationKind::BigIntLiteral(digits),
span: tok.span,
}
}
TokenKind::Minus
if matches!(
self.peek_at(1).kind,
TokenKind::NumberLiteral(_) | TokenKind::BigIntLiteral(_)
) =>
{
let minus = self.advance().span;
let tok = self.advance();
let span = self.span(minus.start, tok.span.end);
let kind =
match tok.kind {
TokenKind::NumberLiteral(v) => TypeAnnotationKind::NumberLiteral(
crate::types::LiteralF64(if v == 0.0 { 0.0 } else { -v }),
),
TokenKind::BigIntLiteral(digits) => TypeAnnotationKind::BigIntLiteral(
crate::types::negate_bigint_digits(&digits),
),
_ => unreachable!("guarded to a numeric literal after `-`"),
};
TypeAnnotation { kind, span }
}
TokenKind::BooleanLiteral(value) => {
let span = self.advance().span;
TypeAnnotation {
kind: TypeAnnotationKind::BooleanLiteral(value),
span,
}
}
TokenKind::LeftBrace => self.parse_object_type_annotation()?,
TokenKind::LeftParen if self.paren_at_opens_function_type(self.pos, type_pos) => {
self.parse_function_type_annotation()?
}
TokenKind::LeftParen => self.parse_grouped_or_function_type()?,
TokenKind::LeftBracket => self.parse_tuple_type_annotation()?,
_ => {
self.error_at_peek("expected type");
return None;
}
};
let mut array_suffixes = 0usize;
while matches!(self.peek().kind, TokenKind::LeftBracket) && !self.line_break_before_peek() {
array_suffixes += 1;
if self.recursion_depth.saturating_add(array_suffixes) >= MAX_PARSE_DEPTH {
self.stop_at_recursion_limit();
return None;
}
let open = self.advance();
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at(open.span, "expected `]` to close array type");
return None;
}
let close = self.advance();
let span = self.span(ty.span.start, close.span.end);
ty = TypeAnnotation {
kind: TypeAnnotationKind::Array(Box::new(ty)),
span,
};
}
Some(ty)
}
fn parse_readonly_type(&mut self, type_pos: TypePos) -> Option<TypeAnnotation> {
let kw = self.advance();
let opens_group = matches!(self.peek().kind, TokenKind::LeftParen);
let operand = self.parse_type_array(type_pos)?;
let grouped = opens_group
&& matches!(
self.pos
.checked_sub(1)
.and_then(|index| self.tokens.get(index))
.map(|token| &token.kind),
Some(TokenKind::RightParen)
);
if grouped
|| !matches!(
operand.kind,
TypeAnnotationKind::Array(_) | TypeAnnotationKind::Tuple(_)
)
{
self.error_at_with_help(
kw.span,
"`readonly` only applies to array and tuple types",
vec![
"write `readonly T[]` or `readonly [A, B]`; to protect an object's \
properties, mark each one `readonly` instead"
.to_string(),
],
);
return Some(operand);
}
let span = self.span(kw.span.start, operand.span.end);
Some(TypeAnnotation {
kind: TypeAnnotationKind::Readonly(Box::new(operand)),
span,
})
}
fn paren_at_opens_function_type(&self, index: usize, type_pos: TypePos) -> bool {
let after = |offset: usize| self.tokens.get(index + offset).map(|t| &t.kind);
match (after(1), after(2)) {
(Some(TokenKind::RightParen | TokenKind::DotDotDot), _) => true,
(Some(TokenKind::Identifier), Some(TokenKind::Colon | TokenKind::Question)) => true,
(Some(TokenKind::Identifier), Some(TokenKind::Comma | TokenKind::RightParen)) => {
type_pos == TypePos::Anywhere
&& self.index_after_matching_paren(index).is_some_and(|i| {
matches!(self.tokens.get(i).map(|t| &t.kind), Some(TokenKind::Arrow))
})
}
_ => false,
}
}
fn parse_grouped_or_function_type(&mut self) -> Option<TypeAnnotation> {
let open = self.pos;
let diagnostics_before = self.diagnostics.len();
if let Some(ty) = self.parse_grouped_type_annotation() {
return Some(ty);
}
let arrow_follows = self
.index_after_matching_paren(open)
.is_some_and(|i| matches!(self.tokens.get(i).map(|t| &t.kind), Some(TokenKind::Arrow)));
if !arrow_follows {
return None;
}
self.pos = open;
self.diagnostics.truncate(diagnostics_before);
self.parse_function_type_annotation()
}
fn parse_grouped_type_annotation(&mut self) -> Option<TypeAnnotation> {
let open = self.advance();
let inner = self.parse_type_annotation()?;
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)` to close a parenthesized type");
return None;
}
let close = self.advance();
Some(TypeAnnotation {
span: self.span(open.span.start, close.span.end),
..inner
})
}
fn parse_function_type_annotation(&mut self) -> Option<TypeAnnotation> {
let open = self.advance();
let params = self.parse_function_type_params()?;
if !matches!(self.peek().kind, TokenKind::Arrow) {
self.error_at_peek("expected `=>` in function-type annotation");
return None;
}
self.advance();
let return_type = self.parse_type_annotation()?;
let end = return_type.span.end;
Some(TypeAnnotation {
kind: TypeAnnotationKind::Function {
params,
return_type: Box::new(return_type),
},
span: self.span(open.span.start, end),
})
}
fn parse_function_type_params(&mut self) -> Option<Vec<TypeAnnotationField>> {
let mut params: Vec<TypeAnnotationField> = Vec::new();
if !matches!(self.peek().kind, TokenKind::RightParen) {
loop {
let rest = if matches!(self.peek().kind, TokenKind::DotDotDot) {
self.advance();
true
} else {
false
};
let name_tok = self.expect_identifier(if rest {
"expected identifier after `...` in function-type annotation"
} else {
"expected parameter name in function-type annotation"
})?;
let name = self.ident_from_token(&name_tok);
let optional = self.parse_parameter_optional(rest, false)?;
if !matches!(self.peek().kind, TokenKind::Colon) {
if rest {
self.error_at_peek_with_help(
"rest parameter requires a type annotation",
vec!["(...args: T[]) => R".to_string()],
);
} else {
self.error_at_peek(
"expected `:` after parameter name (function-type params \
require named annotations in v1)",
);
}
return None;
}
self.advance();
let ty = self.parse_type_annotation()?;
if rest && !is_rest_array_annotation(&ty) {
self.error_at(ty.span, "rest parameter type must be an array");
return None;
}
if matches!(self.peek().kind, TokenKind::Equals) {
if rest {
self.error_rest_parameter_default();
} else {
self.error_at_peek(
"default values in function-type annotations are not yet supported",
);
}
return None;
}
params.push(TypeAnnotationField {
name,
ty,
optional,
readonly: false,
rest,
method: false,
});
match self.peek().kind {
TokenKind::Comma => {
if rest {
let comma = self.advance();
self.error_at(comma.span, "rest parameter must be the last parameter");
return None;
}
self.advance();
if matches!(self.peek().kind, TokenKind::RightParen) {
break;
}
}
TokenKind::RightParen => break,
_ => {
self.error_at_peek("expected `,` or `)`");
return None;
}
}
}
self.check_parameter_order(params.iter().map(ParamOrder::of_field));
}
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
self.advance();
Some(params)
}
fn parse_tuple_type_annotation(&mut self) -> Option<TypeAnnotation> {
let open = self.advance();
if matches!(self.peek().kind, TokenKind::RightBracket) {
let close = self.peek().span;
self.error_at(
self.span(open.span.start, close.end),
"tuple types must have at least one element",
);
return None;
}
let mut elements: Vec<TypeAnnotation> = Vec::new();
loop {
if matches!(self.peek().kind, TokenKind::DotDotDot) {
self.error_at_with_help(
self.peek().span,
"rest elements in tuple types are not supported",
vec!["use an array type `T[]` for a list of varying length".to_string()],
);
return None;
}
let label = self.skip_tuple_element_label();
let mut ty = self.parse_type_annotation()?;
let suffix_optional = matches!(self.peek().kind, TokenKind::Question);
if suffix_optional && label != TupleElementLabel::None {
self.error_at_peek_with_help(
"put the optional marker after the tuple element label",
vec!["write `name?: T` instead of `name: T?`".to_string()],
);
return None;
}
let optional = label == TupleElementLabel::Optional || suffix_optional;
if optional {
let end = if suffix_optional {
self.advance().span.end
} else {
ty.span.end
};
let span = self.span(ty.span.start, end);
ty = TypeAnnotation {
kind: TypeAnnotationKind::Optional(Box::new(ty)),
span,
};
} else if elements
.iter()
.any(|element| matches!(element.kind, TypeAnnotationKind::Optional(_)))
{
self.error_at(
ty.span,
"a required tuple element cannot follow an optional element",
);
return None;
}
elements.push(ty);
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::RightBracket) {
break;
}
}
TokenKind::RightBracket => break,
_ => {
self.error_at_peek("expected `,` or `]`");
return None;
}
}
}
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("expected `]`");
return None;
}
let close = self.advance();
Some(TypeAnnotation {
kind: TypeAnnotationKind::Tuple(elements),
span: self.span(open.span.start, close.span.end),
})
}
fn skip_tuple_element_label(&mut self) -> TupleElementLabel {
if !is_property_name(&self.peek().kind) {
return TupleElementLabel::None;
}
match self.peek_at(1).kind {
TokenKind::Colon => {
self.advance();
self.advance();
TupleElementLabel::Required
}
TokenKind::Question if matches!(self.peek_at(2).kind, TokenKind::Colon) => {
self.advance();
self.advance();
self.advance();
TupleElementLabel::Optional
}
_ => TupleElementLabel::None,
}
}
fn peek_is_parameterless_index_signature(&self) -> bool {
matches!(self.peek().kind, TokenKind::LeftBracket)
&& matches!(self.peek_at(1).kind, TokenKind::RightBracket)
}
fn skip_parameterless_index_signature(&mut self, member_start: u32) -> Option<()> {
self.advance();
let close = self.advance();
self.error_at_with_help(
self.span(member_start, close.span.end),
"an index signature must declare exactly one parameter",
vec!["write `[key: string]: V`".to_string()],
);
if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
self.parse_type_annotation()?;
}
Some(())
}
fn parse_index_signature(&mut self, readonly: bool) -> Option<crate::IndexSignatureAnnotation> {
let open = self.advance();
self.expect_property_ident("expected index parameter name")?;
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` after index parameter name");
return None;
}
self.advance();
if !self.peek_identifier_text_is("string") {
self.error_at_peek("index signatures require string keys; use `[key: string]: V`");
return None;
}
self.advance();
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("expected `]` after string index key type");
return None;
}
self.advance();
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` before index value type");
return None;
}
self.advance();
let value = self.parse_type_annotation()?;
Some(crate::IndexSignatureAnnotation {
span: self.span(open.span.start, value.span.end),
value,
readonly,
})
}
fn parse_object_type_annotation(&mut self) -> Option<TypeAnnotation> {
let open = self.advance();
let mut fields: Vec<TypeAnnotationField> = Vec::new();
let mut index = None;
while !matches!(self.peek().kind, TokenKind::RightBrace | TokenKind::Eof)
&& !self.peek_starts_declaration()
{
let member_start = self.peek().span.start;
let readonly = self.eat_readonly_property_modifier();
if self.peek_is_parameterless_index_signature() {
self.skip_parameterless_index_signature(member_start)?;
} else if self.peek_is_construct_signature() {
self.reject_construct_signature()?;
} else if matches!(self.peek().kind, TokenKind::LeftBracket) {
let signature = self.parse_index_signature(readonly)?;
if index.is_some() {
self.error_at_with_help(
signature.span,
"duplicate string index signature",
vec![],
);
}
index = Some(Box::new(signature));
} else {
let field = self.parse_object_type_field(readonly, member_start)?;
if let Some(existing) = fields.iter().find(|f| f.name.name == field.name.name) {
self.diagnostics.push(Diagnostic {
severity: Severity::Error,
span: field.name.span,
message: format!("duplicate field `{}` in object type", field.name.name),
help: vec![],
notes: vec![(existing.name.span, "first defined here".into())],
});
}
fields.push(field);
}
self.finish_type_member(self.prev_token_end(), "expected `;`, `,`, or `}`")?;
}
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}`");
return None;
}
let close = self.advance();
Some(TypeAnnotation {
kind: TypeAnnotationKind::Object { index, fields },
span: self.span(open.span.start, close.span.end),
})
}
fn peek_starts_declaration(&self) -> bool {
if matches!(self.peek().kind, TokenKind::Export)
&& matches!(
self.peek_at(1).kind,
TokenKind::Default | TokenKind::LeftBrace | TokenKind::Star
)
{
return true;
}
let mut offset = 0;
while self.peek_at_is_declaration_modifier(offset) {
offset += 1;
}
let declaration_keyword = matches!(
self.peek_at(offset).kind,
TokenKind::Function
| TokenKind::Class
| TokenKind::Let
| TokenKind::Const
| TokenKind::Enum
| TokenKind::Interface
) || self.peek_at_is_word(offset, "type");
declaration_keyword && matches!(self.peek_at(offset + 1).kind, TokenKind::Identifier)
}
fn peek_at_is_declaration_modifier(&self, offset: usize) -> bool {
matches!(self.peek_at(offset).kind, TokenKind::Export)
|| ["declare", "async", "abstract"]
.iter()
.any(|word| self.peek_at_is_word(offset, word))
}
fn peek_is_construct_signature(&self) -> bool {
matches!(self.peek().kind, TokenKind::New)
&& matches!(
self.peek_at(1).kind,
TokenKind::LeftParen | TokenKind::LessThan
)
}
fn reject_construct_signature(&mut self) -> Option<()> {
let keyword = self.advance();
self.error_at_with_help(
keyword.span,
"construct signatures are not supported in object types",
vec![
"declare an `interface` with the `new (…)` signature, or pass a factory \
function such as `() => T`"
.to_string(),
],
);
if matches!(self.peek().kind, TokenKind::LessThan) {
self.parse_generic_param_list()?;
}
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` after construct signature type parameters");
return None;
}
self.parse_object_type_method_signature(keyword.span.start)?;
Some(())
}
fn parse_object_type_field(
&mut self,
readonly: bool,
member_start: u32,
) -> Option<TypeAnnotationField> {
let name = self.expect_property_ident("expected field name in object type")?;
let optional = matches!(self.peek().kind, TokenKind::Question);
if optional {
self.advance();
}
let method = matches!(self.peek().kind, TokenKind::LeftParen);
let ty = if method {
if readonly {
self.reject_readonly_modifier(self.readonly_modifier_span(member_start));
}
self.parse_object_type_method_signature(name.span.start)?
} else {
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` after field name");
return None;
}
self.advance();
self.parse_type_annotation()?
};
Some(TypeAnnotationField {
name,
ty,
optional,
readonly,
rest: false,
method,
})
}
fn parse_object_type_method_signature(&mut self, start: u32) -> Option<TypeAnnotation> {
self.advance();
let params = self.parse_function_type_params()?;
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` and return type after method parameters");
return None;
}
self.advance();
let return_type = self.parse_type_annotation()?;
let end = return_type.span.end;
Some(TypeAnnotation {
kind: TypeAnnotationKind::Function {
params,
return_type: Box::new(return_type),
},
span: self.span(start, end),
})
}
fn expect_identifier(&mut self, message: &str) -> Option<Token> {
let tok = self.expect_identifier_name(message)?;
self.reject_strict_mode_reserved_word(&tok);
Some(tok)
}
fn expect_identifier_name(&mut self, message: &str) -> Option<Token> {
if matches!(self.peek().kind, TokenKind::Identifier) {
return Some(self.advance());
}
if is_reserved_identifier_word(&self.peek().kind) {
let tok = self.advance();
let keyword = &self.source[tok.span.start as usize..tok.span.end as usize];
self.error_at_with_help(
tok.span,
format!("`{keyword}` is a reserved keyword and can't be used as a name"),
vec![format!(
"rename it, for example: `{}`",
reserved_keyword_rename_example(keyword)
)],
);
return None;
}
self.error_at_peek(message.to_string());
None
}
fn reject_strict_mode_reserved_word(&mut self, tok: &Token) {
let word = &self.source[tok.span.start as usize..tok.span.end as usize];
if !STRICT_MODE_RESERVED_WORDS.contains(&word) {
return;
}
self.error_at_with_help(
tok.span,
format!("`{word}` is a reserved word in strict mode and can't be used as a name"),
vec![format!(
"rename it, for example: `{}`",
reserved_keyword_rename_example(word)
)],
);
}
fn ident_from_token(&self, tok: &Token) -> Ident {
Ident {
name: self.source[tok.span.start as usize..tok.span.end as usize].to_string(),
span: tok.span,
}
}
fn property_ident_from_token(&self, tok: &Token) -> Ident {
if let TokenKind::StringLiteral(s) = &tok.kind {
Ident {
name: s.clone(),
span: tok.span,
}
} else {
self.ident_from_token(tok)
}
}
fn expect_property_name(&mut self, message: &str) -> Option<Token> {
if !is_property_name(&self.peek().kind) {
self.error_at_peek(message.to_string());
return None;
}
Some(self.advance())
}
fn expect_property_ident(&mut self, message: &str) -> Option<Ident> {
if !is_property_name(&self.peek().kind)
&& !matches!(self.peek().kind, TokenKind::StringLiteral(_))
{
self.error_at_peek(message.to_string());
return None;
}
let tok = self.advance();
Some(self.property_ident_from_token(&tok))
}
fn readonly_modifier_span(&self, member_start: u32) -> Span {
self.span(member_start, member_start + "readonly".len() as u32)
}
fn reject_readonly_modifier(&mut self, span: Span) {
self.error_at_with_help(
span,
"`readonly` can only modify a property or index signature",
vec!["remove `readonly`".to_string()],
);
}
fn eat_readonly_property_modifier(&mut self) -> bool {
if self.peek_identifier_text_is("readonly")
&& !self.line_break_after_peek()
&& (self.peek_starts_property_after_readonly()
|| matches!(self.peek_at(1).kind, TokenKind::LeftBracket))
{
self.advance();
return true;
}
false
}
fn peek_starts_property_after_readonly(&self) -> bool {
let name = &self.peek_at(1).kind;
let after_name = &self.peek_at(2).kind;
let after_optional = &self.peek_at(3).kind;
let starts_member_type =
|kind: &TokenKind| matches!(kind, TokenKind::Colon | TokenKind::LeftParen);
(is_property_name(name) || matches!(name, TokenKind::StringLiteral(_)))
&& (starts_member_type(after_name)
|| matches!(after_name, TokenKind::Question) && starts_member_type(after_optional))
}
fn peek_identifier_text_is(&self, expected: &str) -> bool {
self.token_is_word(self.peek(), expected)
}
fn peek_at_is_word(&self, offset: usize, expected: &str) -> bool {
self.token_is_word(self.peek_at(offset), expected)
}
fn token_is_word(&self, tok: &Token, expected: &str) -> bool {
matches!(tok.kind, TokenKind::Identifier)
&& &self.source[tok.span.start as usize..tok.span.end as usize] == expected
}
fn at_delete_operator(&self) -> bool {
if !self.peek_identifier_text_is("delete") {
return false;
}
if !matches!(
self.peek_at(1).kind,
TokenKind::Identifier | TokenKind::This
) {
return false;
}
let next_is_cast_keyword =
self.peek_at_is_word(1, "as") || self.peek_at_is_word(1, "satisfies");
!next_is_cast_keyword || cannot_start_type(&self.peek_at(2).kind)
}
fn at_type_alias_head(&self) -> bool {
self.peek_identifier_text_is("type")
&& matches!(self.peek_at(1).kind, TokenKind::Identifier)
}
fn parse_expression(&mut self) -> Option<ExprId> {
self.with_recursion_limit(Self::parse_expression_inner)
}
fn parse_expression_inner(&mut self) -> Option<ExprId> {
let written = self.parse_conditional()?;
let shift_assignment = self.peek_shift().filter(|shift| shift.assignment);
if !is_assign_lookahead(&self.peek().kind) && shift_assignment.is_none() {
return Some(written);
}
let target_span = parse_arena_result(self.ast.try_expr(written), &mut self.fatal)?.span;
let target = self.assignment_target(written)?;
let op_tok = self.advance();
let mut op_span = op_tok.span;
let op = if let Some(ShiftToken { op, tokens, .. }) = shift_assignment {
for _ in 1..tokens {
op_span.end = self.advance().span.end;
}
Some(op)
} else {
compound_op_for_token(&op_tok.kind)
};
let value = self.parse_expression()?;
let value_span = parse_arena_result(self.ast.try_expr(value), &mut self.fatal)?.span;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Assign {
target,
op,
op_span,
value,
},
span: self.span(target_span.start, value_span.end),
}),
&mut self.fatal,
)
}
fn assignment_target(&mut self, written: ExprId) -> Option<ExprId> {
let mut target = written;
while let ExprKind::Paren(inner) =
parse_arena_result(self.ast.try_expr(target), &mut self.fatal)?.kind
{
target = inner;
}
if matches!(
parse_arena_result(self.ast.try_expr(target), &mut self.fatal)?.kind,
ExprKind::Identifier(_) | ExprKind::FieldAccess { .. } | ExprKind::IndexAccess { .. }
) {
return Some(target);
}
let error_span = parse_arena_result(self.ast.try_expr(written), &mut self.fatal)?.span;
self.error_at_with_help(
error_span,
"invalid assignment target",
vec![
"assign to a variable, a field (`o.f = …`), or an element (`a[i] = …`)".to_string(),
],
);
None
}
fn parse_conditional(&mut self) -> Option<ExprId> {
if self.is_arrow_start() {
return self.parse_arrow();
}
let cond = self.parse_binary(0)?;
if !matches!(self.peek().kind, TokenKind::Question) {
return Some(cond);
}
self.advance();
let then_ = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek_with_help(
"expected `:` to complete ternary expression",
vec!["const x = cond ? then : else;".to_string()],
);
return None;
}
self.advance();
let else_ = self.parse_expression()?;
let cond_span = parse_arena_result(self.ast.try_expr(cond), &mut self.fatal)?.span;
let else_span = parse_arena_result(self.ast.try_expr(else_), &mut self.fatal)?.span;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Ternary { cond, then_, else_ },
span: self.span(cond_span.start, else_span.end),
}),
&mut self.fatal,
)
}
fn is_arrow_start(&self) -> bool {
match self.peek().kind {
TokenKind::Identifier => matches!(self.peek_at(1).kind, TokenKind::Arrow),
TokenKind::LeftParen => self.find_arrow_after_matching_paren(),
_ => false,
}
}
fn index_after_matching_paren(&self, index: usize) -> Option<usize> {
if !matches!(
self.tokens.get(index).map(|t| &t.kind),
Some(TokenKind::LeftParen)
) {
return None;
}
self.scan_past_balanced(
index,
|k| matches!(k, TokenKind::LeftParen),
|k| matches!(k, TokenKind::RightParen),
)
}
fn find_arrow_after_matching_paren(&self) -> bool {
let Some(i) = self.index_after_matching_paren(self.pos) else {
return false;
};
let after_type = if matches!(self.tokens.get(i).map(|t| &t.kind), Some(TokenKind::Colon)) {
match self.scan_past_type_annotation(i + 1, TypePos::ArrowReturn) {
Some(p) => p,
None => return false,
}
} else {
i
};
matches!(
self.tokens.get(after_type).map(|t| &t.kind),
Some(TokenKind::Arrow),
)
}
fn scan_past_type_annotation(&self, start: usize, type_pos: TypePos) -> Option<usize> {
self.scan_past_type_annotation_at_depth(start, type_pos, 0)
}
fn scan_past_type_annotation_at_depth(
&self,
start: usize,
type_pos: TypePos,
depth: usize,
) -> Option<usize> {
if depth >= MAX_PARSE_DEPTH {
return None;
}
if matches!(
self.tokens.get(start).map(|t| &t.kind),
Some(TokenKind::Identifier)
) && self
.tokens
.get(start + 1)
.is_some_and(|t| self.token_is_word(t, "is"))
{
return self.scan_past_type_annotation_at_depth(start + 2, type_pos, depth + 1);
}
let start = if matches!(
self.tokens.get(start).map(|t| &t.kind),
Some(TokenKind::Pipe)
) {
start + 1
} else {
start
};
let mut i = self.scan_past_single_type_member(start, type_pos, depth + 1)?;
while matches!(self.tokens.get(i).map(|t| &t.kind), Some(TokenKind::Pipe)) {
i = self.scan_past_single_type_member(i + 1, type_pos, depth + 1)?;
}
Some(i)
}
fn scan_past_single_type_member(
&self,
start: usize,
type_pos: TypePos,
depth: usize,
) -> Option<usize> {
if depth >= MAX_PARSE_DEPTH {
return None;
}
if self.is_contextual_type_operator(start, "keyof")
|| self.is_contextual_type_operator(start, "readonly")
{
return self.scan_past_single_type_member(start + 1, type_pos, depth + 1);
}
let mut i = start;
match self.tokens.get(i)?.kind {
TokenKind::Identifier | TokenKind::Void => {
i += 1;
while matches!(self.tokens.get(i).map(|t| &t.kind), Some(TokenKind::Dot)) {
if !matches!(
self.tokens.get(i + 1).map(|t| &t.kind),
Some(TokenKind::Identifier)
) {
return None;
}
i += 2;
}
if matches!(
self.tokens.get(i).map(|t| &t.kind),
Some(TokenKind::LessThan)
) {
i = self.scan_past_balanced(
i,
|k| matches!(k, TokenKind::LessThan),
|k| matches!(k, TokenKind::GreaterThan),
)?;
}
}
TokenKind::NullLiteral
| TokenKind::StringLiteral(_)
| TokenKind::NumberLiteral(_)
| TokenKind::BooleanLiteral(_) => {
i += 1;
}
TokenKind::LeftBrace => {
i = self.scan_past_balanced(
i,
|k| matches!(k, TokenKind::LeftBrace),
|k| matches!(k, TokenKind::RightBrace),
)?;
}
TokenKind::LeftBracket => {
i = self.scan_past_balanced(
i,
|k| matches!(k, TokenKind::LeftBracket),
|k| matches!(k, TokenKind::RightBracket),
)?;
}
TokenKind::LeftParen => {
let is_function_type = self.paren_at_opens_function_type(i, type_pos);
i = self.scan_past_balanced(
i,
|k| matches!(k, TokenKind::LeftParen),
|k| matches!(k, TokenKind::RightParen),
)?;
if is_function_type {
if !matches!(self.tokens.get(i).map(|t| &t.kind), Some(TokenKind::Arrow)) {
return None;
}
return self.scan_past_type_annotation_at_depth(
i + 1,
TypePos::Anywhere,
depth + 1,
);
}
}
_ => return None,
}
while matches!(
self.tokens.get(i).map(|t| &t.kind),
Some(TokenKind::LeftBracket)
) {
i += 1;
if !matches!(
self.tokens.get(i).map(|t| &t.kind),
Some(TokenKind::RightBracket)
) {
return None;
}
i += 1;
}
Some(i)
}
fn scan_past_balanced(
&self,
start: usize,
is_open: fn(&TokenKind) -> bool,
is_close: fn(&TokenKind) -> bool,
) -> Option<usize> {
let mut depth: i32 = 1;
let mut i = start + 1;
while i < self.tokens.len() && depth > 0 {
let token = self.tokens.get(i)?;
let kind = &token.kind;
if is_open(kind) {
depth += 1;
} else if is_close(kind) {
depth -= 1;
} else if matches!(kind, TokenKind::Eof) {
return None;
}
i += 1;
}
if depth == 0 { Some(i) } else { None }
}
fn parse_arrow(&mut self) -> Option<ExprId> {
let start_span = self.peek().span;
let params = match self.peek().kind {
TokenKind::Identifier => {
let name_tok = self.advance();
let name = self.ident_from_token(&name_tok);
vec![ParamDecl {
name,
ty: None,
default: None,
optional: false,
pattern: None,
rest: false,
modifiers: None,
}]
}
TokenKind::LeftParen => {
self.advance();
let params = if matches!(self.peek().kind, TokenKind::RightParen) {
Vec::new()
} else {
self.parse_arrow_param_list()?
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
self.advance();
params
}
_ => return self.invariant_failure("is_arrow_start guarded the entry"),
};
let (return_type, type_predicate) = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
self.parse_predicate_or_return_type(TypePos::ArrowReturn)?
} else {
(None, None)
};
if !matches!(self.peek().kind, TokenKind::Arrow) {
self.error_at_peek("expected `=>` in arrow function");
return None;
}
self.advance();
let (body, end_pos) = if matches!(self.peek().kind, TokenKind::LeftBrace) {
let block = self.parse_block()?;
let span = parse_arena_result(self.ast.try_stmt(block), &mut self.fatal)?.span;
(ArrowBody::Block(block), span.end)
} else {
let expr = self.parse_expression()?;
let span = parse_arena_result(self.ast.try_expr(expr), &mut self.fatal)?.span;
(ArrowBody::Expr(expr), span.end)
};
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Arrow {
params,
return_type,
type_predicate,
body,
},
span: self.span(start_span.start, end_pos),
}),
&mut self.fatal,
)
}
fn parse_function_expression(&mut self) -> Option<ExprId> {
let kw = self.advance();
if matches!(self.peek().kind, TokenKind::LessThan) {
self.error_at_peek("generic function expressions are not supported");
return None;
}
let self_name = if matches!(self.peek().kind, TokenKind::Identifier) {
let name_tok = self.advance();
Some(self.ident_from_token(&name_tok))
} else {
None
};
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` after `function`");
return None;
}
self.advance();
let this_type = if matches!(self.peek().kind, TokenKind::This) {
self.advance();
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected a type annotation after `this`");
return None;
}
self.advance();
let ty = self.parse_type_annotation()?;
if matches!(self.peek().kind, TokenKind::Comma) {
self.advance();
} else if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `,` or `)` after the `this` parameter");
return None;
}
Some(ty)
} else {
None
};
let params = if matches!(self.peek().kind, TokenKind::RightParen) {
Vec::new()
} else {
let saved_class = self.class_member_body_depth;
let saved_function = self.function_expression_body_depth;
self.class_member_body_depth = 0;
self.function_expression_body_depth = 1;
let params = self.parse_arrow_param_list();
self.class_member_body_depth = saved_class;
self.function_expression_body_depth = saved_function;
params?
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
self.advance();
let (return_type, type_predicate) = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
self.parse_predicate_or_return_type(TypePos::ArrowReturn)?
} else {
(None, None)
};
if !matches!(self.peek().kind, TokenKind::LeftBrace) {
self.error_at_peek("expected `{` to open the function body");
return None;
}
let saved = self.class_member_body_depth;
self.class_member_body_depth = 0;
self.function_expression_body_depth += 1;
let block = self.parse_block();
self.function_expression_body_depth -= 1;
self.class_member_body_depth = saved;
let block = block?;
let end = parse_arena_result(self.ast.try_stmt(block), &mut self.fatal)?
.span
.end;
let function = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Arrow {
params,
return_type,
type_predicate,
body: ArrowBody::Block(block),
},
span: self.span(kw.span.start, end),
}),
&mut self.fatal,
)?;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::FunctionExpression {
name: self_name,
this_type,
function,
},
span: self.span(kw.span.start, end),
}),
&mut self.fatal,
)
}
fn parse_outer_this_boundary<T>(
&mut self,
parse: impl FnOnce(&mut Self) -> Option<T>,
) -> Option<T> {
let saved = self.class_member_body_depth;
self.class_member_body_depth = 0;
let saved_function = self.function_expression_body_depth;
self.function_expression_body_depth = 0;
let parsed = parse(self);
self.class_member_body_depth = saved;
self.function_expression_body_depth = saved_function;
parsed
}
fn parse_arrow_param_list(&mut self) -> Option<Vec<ParamDecl>> {
let mut params = Vec::new();
loop {
let rest = if matches!(self.peek().kind, TokenKind::DotDotDot) {
self.advance();
true
} else {
false
};
let (name, pattern) = match self.peek().kind {
TokenKind::LeftBrace | TokenKind::LeftBracket => {
if rest {
self.error_at_peek("rest parameter cannot be destructured");
return None;
}
let binding = self.parse_binding()?;
let placeholder = Ident {
name: String::new(),
span: binding.span(),
};
(placeholder, Some(binding))
}
_ => {
let name_tok = self.expect_identifier(if rest {
"expected identifier after `...`"
} else {
"expected parameter name"
})?;
let name = self.ident_from_token(&name_tok);
(name, None)
}
};
let optional = self.parse_parameter_optional(rest, pattern.is_some())?;
let ty = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
Some(self.parse_type_annotation()?)
} else {
None
};
let default = self.parse_parameter_default(rest, optional)?;
params.push(ParamDecl {
name,
ty,
default,
optional,
pattern,
rest,
modifiers: None,
});
match self.peek().kind {
TokenKind::Comma => {
if rest {
let comma = self.advance();
self.error_at(comma.span, "rest parameter must be the last parameter");
return None;
}
self.advance();
if matches!(self.peek().kind, TokenKind::RightParen) {
break;
}
}
TokenKind::RightParen => break,
_ => {
self.error_at_peek("expected `,` or `)`");
return None;
}
}
}
self.check_parameter_order(params.iter().map(ParamOrder::of_param));
Some(params)
}
fn parse_binary(&mut self, min_prec: u8) -> Option<ExprId> {
self.with_recursion_limit(|parser| parser.parse_binary_inner(min_prec))
}
fn peek_shift(&self) -> Option<ShiftToken> {
let first = self.peek();
let second = self.peek_at(1);
if first.span.end != second.span.start {
return None;
}
match (&first.kind, &second.kind) {
(TokenKind::LessThan, TokenKind::LessThan) => {
Some(ShiftToken::new(BinOp::Shl, 2, false))
}
(TokenKind::LessThan, TokenKind::LessEquals) => {
Some(ShiftToken::new(BinOp::Shl, 2, true))
}
(TokenKind::GreaterThan, TokenKind::GreaterEquals) => {
Some(ShiftToken::new(BinOp::Shr, 2, true))
}
(TokenKind::GreaterThan, TokenKind::GreaterThan) => {
let third = self.peek_at(2);
if second.span.end == third.span.start {
match third.kind {
TokenKind::GreaterThan => {
return Some(ShiftToken::new(BinOp::UnsignedShr, 3, false));
}
TokenKind::GreaterEquals => {
return Some(ShiftToken::new(BinOp::UnsignedShr, 3, true));
}
_ => {}
}
}
Some(ShiftToken::new(BinOp::Shr, 2, false))
}
_ => None,
}
}
fn peek_binary(&self) -> Option<BinaryToken> {
if let Some(shift) = self.peek_shift() {
return (!shift.assignment).then_some(BinaryToken {
op: shift.op,
precedence: SHIFT_PRECEDENCE,
tokens: shift.tokens,
});
}
peek_binop(&self.peek().kind).map(|(op, precedence)| BinaryToken {
op,
precedence,
tokens: 1,
})
}
fn parse_binary_inner(&mut self, min_prec: u8) -> Option<ExprId> {
let mut lhs = self.parse_cast()?;
let mut last_op: Option<BinOp> = None;
let mut reported_mixing = false;
while let Some(BinaryToken {
op,
precedence: prec,
tokens: count,
}) = self.peek_binary()
{
if prec < min_prec {
break;
}
if !reported_mixing
&& let Some(last) = last_op
&& mixed_logical(last, op)
{
reported_mixing = true;
let op_span = self.peek().span;
self.error_at_with_help(
op_span,
"mixing `??` with `||` / `&&` requires parentheses",
vec![mixed_logical_help(last, op)],
);
}
if op == BinOp::Pow
&& matches!(
parse_arena_result(self.ast.try_expr(lhs), &mut self.fatal)?.kind,
ExprKind::Unary { .. }
| ExprKind::Typeof { .. }
| ExprKind::Delete { .. }
| ExprKind::Void { .. }
)
{
let error_span = parse_arena_result(self.ast.try_expr(lhs), &mut self.fatal)?.span;
self.error_at_with_help(
error_span,
"an unparenthesized unary expression cannot be the left operand of `**`",
vec!["choose the grouping explicitly: `(-x) ** 2` or `-(x ** 2)`".into()],
);
return None;
}
let loose = matches!(self.peek().kind, TokenKind::EqEq | TokenKind::BangEq);
for _ in 0..count {
self.advance();
}
let rhs = self.parse_binary(right_operand_min_prec(op, prec))?;
let lhs_span = parse_arena_result(self.ast.try_expr(lhs), &mut self.fatal)?.span;
let rhs_span = parse_arena_result(self.ast.try_expr(rhs), &mut self.fatal)?.span;
if loose {
self.reject_loose_nullish_comparison(op, lhs, rhs)?;
}
lhs = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Binary { op, lhs, rhs },
span: self.span(lhs_span.start, rhs_span.end),
}),
&mut self.fatal,
)?;
last_op = Some(op);
}
Some(lhs)
}
fn parse_cast(&mut self) -> Option<ExprId> {
let mut expr = self.parse_unary()?;
while matches!(self.peek().kind, TokenKind::Instanceof)
|| self.peek_identifier_text_is("as")
{
let is_instanceof = matches!(self.peek().kind, TokenKind::Instanceof);
self.advance();
let ty = self.parse_type_annotation()?;
let start = parse_arena_result(self.ast.try_expr(expr), &mut self.fatal)?
.span
.start;
let end = ty.span.end;
let kind = if is_instanceof {
ExprKind::InstanceOf { value: expr, ty }
} else {
self.last_as_type_end = Some(end);
ExprKind::As { expr, ty }
};
expr = parse_arena_result(
self.ast.try_push_expr(Expr {
kind,
span: self.span(start, end),
}),
&mut self.fatal,
)?;
}
Some(expr)
}
fn parse_unary(&mut self) -> Option<ExprId> {
self.with_recursion_limit(Self::parse_unary_inner)
}
fn parse_unary_inner(&mut self) -> Option<ExprId> {
if matches!(self.peek().kind, TokenKind::Typeof) {
return self.parse_prefix_expr(|operand| ExprKind::Typeof { operand });
}
if matches!(self.peek().kind, TokenKind::Void) {
return self.parse_prefix_expr(|operand| ExprKind::Void { operand });
}
if self.at_delete_operator() {
return self.parse_prefix_expr(|operand| ExprKind::Delete { operand });
}
if matches!(self.peek().kind, TokenKind::New) {
let op_tok = self.advance();
let mut callee = self.parse_atom()?;
while matches!(self.peek().kind, TokenKind::Dot) {
self.advance();
if !matches!(self.peek().kind, TokenKind::Identifier) {
self.error_at_peek("expected identifier after `.` in constructor name");
return None;
}
let ident_tok = self.advance();
let name = self.ident_from_token(&ident_tok);
let receiver_span =
parse_arena_result(self.ast.try_expr(callee), &mut self.fatal)?.span;
let combined_span = self.span(receiver_span.start, ident_tok.span.end);
callee = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::FieldAccess {
receiver: callee,
name,
},
span: combined_span,
}),
&mut self.fatal,
)?;
}
let type_args = if matches!(self.peek().kind, TokenKind::LessThan) {
let saved_pos = self.pos;
let saved_diag_len = self.diagnostics.len();
if let Some(args) = self.try_parse_type_args_only() {
Some(args)
} else {
self.pos = saved_pos;
self.diagnostics.truncate(saved_diag_len);
None
}
} else {
None
};
if !matches!(self.peek().kind, TokenKind::LeftParen) {
self.error_at_peek("expected `(` after constructor name in `new` expression");
return None;
}
self.advance();
let args = if matches!(self.peek().kind, TokenKind::RightParen) {
Vec::new()
} else {
self.parse_call_args()?
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
let close = self.advance();
let new_expr = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::New {
callee,
type_args,
args,
},
span: self.span(op_tok.span.start, close.span.end),
}),
&mut self.fatal,
)?;
return self.parse_postfix_from(new_expr);
}
let op = match self.peek().kind {
TokenKind::Bang => UnOp::Not,
TokenKind::Tilde => UnOp::BitNot,
TokenKind::Minus => UnOp::Neg,
TokenKind::Plus => UnOp::Pos,
_ => return self.parse_postfix(),
};
self.parse_prefix_expr(|operand| ExprKind::Unary { op, operand })
}
fn parse_prefix_expr(&mut self, kind: impl FnOnce(ExprId) -> ExprKind) -> Option<ExprId> {
let op_tok = self.advance();
if matches!(op_tok.kind, TokenKind::Void) && self.at_inserted_semicolon() {
self.advance();
}
let operand = self.parse_unary()?;
let operand_span = parse_arena_result(self.ast.try_expr(operand), &mut self.fatal)?.span;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: kind(operand),
span: self.span(op_tok.span.start, operand_span.end),
}),
&mut self.fatal,
)
}
fn parse_postfix(&mut self) -> Option<ExprId> {
let expr = self.parse_atom()?;
self.parse_postfix_from(expr)
}
fn parse_postfix_from(&mut self, mut expr: ExprId) -> Option<ExprId> {
let mut chain_parts: Option<(ExprId, Vec<crate::ChainPart>)> = None;
loop {
match self.peek().kind {
TokenKind::LeftParen => {
if let Some((_, parts)) = &mut chain_parts {
let part = self.parse_chain_call_tail( false)?;
parts.push(part);
} else {
expr = self.parse_call_tail(expr, None)?;
}
}
TokenKind::Dot => {
if let Some((_, parts)) = &mut chain_parts {
let part = self.parse_chain_field_tail( false)?;
parts.push(part);
} else {
expr = self.parse_field_access_tail(expr)?;
}
}
TokenKind::LeftBracket => {
if let Some((_, parts)) = &mut chain_parts {
let part = self.parse_chain_index_tail( false)?;
parts.push(part);
} else {
expr = self.parse_index_access_tail(expr)?;
}
}
TokenKind::QuestionDot => {
if chain_parts.is_none() {
chain_parts = Some((expr, Vec::new()));
}
self.advance();
let part = match self.peek().kind {
TokenKind::LeftParen => self.parse_chain_call_tail(true)?,
TokenKind::LeftBracket => self.parse_chain_index_tail(true)?,
_ if is_property_name(&self.peek().kind) => {
self.parse_chain_field_tail(true)?
}
_ => {
self.error_at_peek(
"expected field name, `(` for call, or `[` for index after `?.`",
);
return None;
}
};
if let Some((_, parts)) = &mut chain_parts {
parts.push(part);
}
}
TokenKind::LessThan => {
if chain_parts.is_some() {
break;
}
let saved_pos = self.pos;
let saved_diag_len = self.diagnostics.len();
if let Some(call_id) = self.try_parse_generic_call_tail(expr) {
expr = call_id;
} else {
self.pos = saved_pos;
self.diagnostics.truncate(saved_diag_len);
break;
}
}
TokenKind::Bang => {
if let Some((_, parts)) = &mut chain_parts {
let tok = self.advance();
parts.push(crate::ChainPart::NonNull { span: tok.span });
continue;
}
let tok = self.advance();
let start = parse_arena_result(self.ast.try_expr(expr), &mut self.fatal)?
.span
.start;
expr = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::PostfixUnary {
op: PostfixOp::NonNullAssert,
operand: expr,
},
span: self.span(start, tok.span.end),
}),
&mut self.fatal,
)?;
}
TokenKind::PlusPlus | TokenKind::MinusMinus => {
if chain_parts.is_some() {
break;
}
let tok = self.advance();
let op = if matches!(tok.kind, TokenKind::PlusPlus) {
PostfixOp::Inc
} else {
PostfixOp::Dec
};
let start = parse_arena_result(self.ast.try_expr(expr), &mut self.fatal)?
.span
.start;
expr = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::PostfixUnary { op, operand: expr },
span: self.span(start, tok.span.end),
}),
&mut self.fatal,
)?;
break;
}
_ => break,
}
}
if let Some((base, parts)) = chain_parts {
let base_span = parse_arena_result(self.ast.try_expr(base), &mut self.fatal)?.span;
let end = parts.last().map_or(base_span.end, |p| p.span().end);
Some(parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::OptionalChain { base, parts },
span: self.span(base_span.start, end),
}),
&mut self.fatal,
)?)
} else {
Some(expr)
}
}
fn parse_chain_field_tail(&mut self, optional: bool) -> Option<crate::ChainPart> {
let start_pos = self.peek().span.start;
if !optional {
self.advance();
}
let name_tok = if is_property_name(&self.peek().kind) {
self.advance()
} else {
let what = if optional { "?." } else { "." };
self.error_at_peek(format!("expected field name after `{what}`"));
return None;
};
let name = self.ident_from_token(&name_tok);
let end = name.span.end;
Some(crate::ChainPart::Field {
name,
optional,
span: self.span(start_pos, end),
})
}
fn parse_chain_index_tail(&mut self, optional: bool) -> Option<crate::ChainPart> {
let open = self.advance();
let idx = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("expected `]`");
return None;
}
let close = self.advance();
Some(crate::ChainPart::Index {
idx,
optional,
span: self.span(open.span.start, close.span.end),
})
}
fn parse_chain_call_tail(&mut self, optional: bool) -> Option<crate::ChainPart> {
let open = self.advance();
let args = if matches!(self.peek().kind, TokenKind::RightParen) {
Vec::new()
} else {
self.parse_call_args()?
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
let close = self.advance();
Some(crate::ChainPart::Call {
args,
type_args: None,
optional,
span: self.span(open.span.start, close.span.end),
})
}
fn parse_field_access_tail(&mut self, receiver: ExprId) -> Option<ExprId> {
self.advance();
let name_tok = if is_property_name(&self.peek().kind) {
self.advance()
} else {
self.error_at_peek("expected field name after `.`");
return None;
};
let name = self.ident_from_token(&name_tok);
let receiver_span = parse_arena_result(self.ast.try_expr(receiver), &mut self.fatal)?.span;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::FieldAccess {
receiver,
name: name.clone(),
},
span: self.span(receiver_span.start, name.span.end),
}),
&mut self.fatal,
)
}
fn parse_index_access_tail(&mut self, receiver: ExprId) -> Option<ExprId> {
self.advance();
let index = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("expected `]`");
return None;
}
let close = self.advance();
let receiver_span = parse_arena_result(self.ast.try_expr(receiver), &mut self.fatal)?.span;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::IndexAccess { receiver, index },
span: self.span(receiver_span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn parse_call_tail(
&mut self,
callee: ExprId,
type_args: Option<Vec<TypeAnnotation>>,
) -> Option<ExprId> {
self.advance();
let args = if matches!(self.peek().kind, TokenKind::RightParen) {
Vec::new()
} else {
self.parse_call_args()?
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
let close = self.advance();
let callee_span = parse_arena_result(self.ast.try_expr(callee), &mut self.fatal)?.span;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Call {
callee,
type_args,
args,
},
span: self.span(callee_span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn try_parse_generic_call_tail(&mut self, callee: ExprId) -> Option<ExprId> {
let type_args = self.try_parse_type_args_only()?;
if !matches!(self.peek().kind, TokenKind::LeftParen) {
return None;
}
self.parse_call_tail(callee, Some(type_args))
}
fn try_parse_type_args_only(&mut self) -> Option<Vec<TypeAnnotation>> {
if !matches!(self.peek().kind, TokenKind::LessThan) {
return self.invariant_failure("type argument parser expected `<`");
}
self.advance();
if matches!(self.peek().kind, TokenKind::GreaterThan) {
return None;
}
let mut type_args = Vec::new();
loop {
let ta = self.parse_type_annotation()?;
type_args.push(ta);
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::GreaterThan) {
self.advance();
break;
}
}
TokenKind::GreaterThan => {
self.advance();
break;
}
_ => return None,
}
}
Some(type_args)
}
fn parse_call_args(&mut self) -> Option<Vec<ExprId>> {
let mut args = Vec::new();
loop {
let arg = self.parse_expression()?;
args.push(arg);
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::RightParen) {
return Some(args);
}
}
TokenKind::RightParen => return Some(args),
_ => {
self.error_at_peek("expected `,` or `)`");
return None;
}
}
}
}
fn parse_angle_cast(&mut self) -> Option<ExprId> {
let open = self.advance();
let ty = self.parse_type_annotation()?;
if !matches!(self.peek().kind, TokenKind::GreaterThan) {
self.error_at_peek("expected `>` to close a type assertion");
return None;
}
self.advance();
let expr = self.parse_unary()?;
let end = parse_arena_result(self.ast.try_expr(expr), &mut self.fatal)?
.span
.end;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::As { expr, ty },
span: self.span(open.span.start, end),
}),
&mut self.fatal,
)
}
fn parse_atom(&mut self) -> Option<ExprId> {
match self.peek().kind {
TokenKind::LessThan => return self.parse_angle_cast(),
TokenKind::LeftParen => return self.parse_paren(),
TokenKind::LeftBrace => return self.parse_object_literal(),
TokenKind::LeftBracket => return self.parse_array_literal(),
TokenKind::TemplateHead(_) => return self.parse_template_literal(),
TokenKind::This => return self.parse_this_or_super(true),
TokenKind::Super => return self.parse_this_or_super(false),
TokenKind::Function => return self.parse_function_expression(),
_ => {}
}
if !matches!(
self.peek().kind,
TokenKind::NumberLiteral(_)
| TokenKind::BigIntLiteral(_)
| TokenKind::StringLiteral(_)
| TokenKind::TemplateNoSubstitution(_)
| TokenKind::BooleanLiteral(_)
| TokenKind::NullLiteral
| TokenKind::Identifier
| TokenKind::RegexLiteral { .. }
) {
self.error_at_peek("expected expression");
return None;
}
let tok = self.advance();
let span = tok.span;
let kind = match tok.kind {
TokenKind::NumberLiteral(v) => ExprKind::Number(v),
TokenKind::BigIntLiteral(s) => ExprKind::BigInt(s),
TokenKind::StringLiteral(s) => ExprKind::String(s),
TokenKind::TemplateNoSubstitution(s) => ExprKind::String(s),
TokenKind::BooleanLiteral(b) => ExprKind::Boolean(b),
TokenKind::NullLiteral => ExprKind::Null,
TokenKind::Identifier => ExprKind::Identifier(Ident {
name: self.source[span.start as usize..span.end as usize].to_string(),
span,
}),
TokenKind::RegexLiteral { source, flags } => ExprKind::Regex { source, flags },
_ => unreachable!("advance preserves the accepted expression token"),
};
parse_arena_result(self.ast.try_push_expr(Expr { kind, span }), &mut self.fatal)
}
fn parse_this_or_super(&mut self, is_this: bool) -> Option<ExprId> {
let tok = self.advance();
let span = tok.span;
if !is_this && self.class_member_body_depth == 0 {
self.error_at_with_help(
span,
"`super` is only valid inside a class method or constructor body",
vec![
"call `super(...)` in a subclass constructor or `super.method(...)` \
in a subclass method"
.to_string(),
],
);
}
let kind = if is_this
&& self.class_member_body_depth == 0
&& self.function_expression_body_depth == 0
{
ExprKind::ThisOutsideReceiver
} else if is_this {
ExprKind::This
} else {
ExprKind::Super
};
parse_arena_result(self.ast.try_push_expr(Expr { kind, span }), &mut self.fatal)
}
fn parse_template_literal(&mut self) -> Option<ExprId> {
let head_tok = self.advance();
let TokenKind::TemplateHead(head) = head_tok.kind else {
unreachable!("template dispatch requires TemplateHead");
};
let start = head_tok.span.start;
let mut parts: Vec<String> = vec![head];
let mut exprs: Vec<ExprId> = Vec::new();
let mut substitution_spans: Vec<Span> = Vec::new();
let mut substitution_start = head_tok.span.end.saturating_sub(2);
loop {
let expr = self.parse_expression()?;
exprs.push(expr);
let closing = self.peek().span.start;
let substitution_span = self.span(substitution_start, closing.saturating_add(1));
match self.peek().kind.clone() {
TokenKind::TemplateMiddle(s) => {
let tok = self.advance();
substitution_spans.push(substitution_span);
substitution_start = tok.span.end.saturating_sub(2);
parts.push(s);
}
TokenKind::TemplateTail(s) => {
let tok = self.advance();
substitution_spans.push(substitution_span);
parts.push(s);
return parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::TemplateLiteral {
parts,
exprs,
substitution_spans,
},
span: self.span(start, tok.span.end),
}),
&mut self.fatal,
);
}
_ => {
self.error_at_peek("expected `}` to close template interpolation");
return None;
}
}
}
}
fn parse_object_literal(&mut self) -> Option<ExprId> {
let open = self.advance();
let mut members: Vec<ObjectLiteralMember> = Vec::new();
if !matches!(self.peek().kind, TokenKind::RightBrace) {
loop {
if matches!(self.peek().kind, TokenKind::DotDotDot) {
let dots = self.advance();
let value = self.parse_expression()?;
let value_span =
parse_arena_result(self.ast.try_expr(value), &mut self.fatal)?.span;
members.push(ObjectLiteralMember::Spread {
value,
span: self.span(dots.span.start, value_span.end),
});
} else if matches!(self.peek().kind, TokenKind::LeftBracket) {
self.advance();
let key = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("expected `]` after computed property key");
return None;
}
self.advance();
if !matches!(self.peek().kind, TokenKind::Colon) {
self.error_at_peek("expected `:` after computed property key; computed methods are not supported");
return None;
}
self.advance();
let value = self.parse_expression()?;
members.push(ObjectLiteralMember::Computed { key, value });
} else {
let field = self.parse_object_literal_field()?;
if let Some(existing) = members.iter().find_map(|m| match m {
ObjectLiteralMember::Field(f) if f.name.name == field.name.name => Some(f),
_ => None,
}) {
self.diagnostics.push(Diagnostic {
severity: Severity::Error,
span: field.name.span,
message: format!(
"duplicate field `{}` in object literal",
field.name.name
),
help: vec![],
notes: vec![(existing.name.span, "first defined here".into())],
});
}
members.push(ObjectLiteralMember::Field(field));
}
if self.at_inserted_semicolon()
&& matches!(self.peek_at(1).kind, TokenKind::RightBrace)
{
self.advance();
}
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::RightBrace) {
break;
}
}
TokenKind::RightBrace => break,
_ => {
self.error_at_peek("expected `,` or `}`");
return None;
}
}
}
}
if !matches!(self.peek().kind, TokenKind::RightBrace) {
self.error_at_peek("expected `}`");
return None;
}
let close = self.advance();
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::ObjectLiteral { members },
span: self.span(open.span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn parse_object_literal_field(&mut self) -> Option<ObjectLiteralField> {
let (name, shorthandable) = match self.peek().kind.clone() {
TokenKind::Identifier => {
let tok = self.advance();
(self.ident_from_token(&tok), true)
}
kind if is_property_name(&kind) => {
let tok = self.advance();
(self.ident_from_token(&tok), false)
}
TokenKind::StringLiteral(_) => {
let tok = self.advance();
(self.property_ident_from_token(&tok), false)
}
_ => {
self.error_at_peek("expected field name");
return None;
}
};
if matches!(self.peek().kind, TokenKind::LeftParen) {
let value = self.parse_method_shorthand_body(name.span)?;
return Some(ObjectLiteralField { name, value });
}
if !matches!(self.peek().kind, TokenKind::Colon) {
if shorthandable {
let value = parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Identifier(name.clone()),
span: name.span,
}),
&mut self.fatal,
)?;
return Some(ObjectLiteralField { name, value });
}
self.error_at_peek("expected `:` after field name");
return None;
}
self.advance();
let value = self.parse_expression()?;
Some(ObjectLiteralField { name, value })
}
fn parse_method_shorthand_body(&mut self, name_span: Span) -> Option<ExprId> {
self.advance();
let params = if matches!(self.peek().kind, TokenKind::RightParen) {
Vec::new()
} else {
self.parse_arrow_param_list()?
};
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
self.advance();
let (return_type, type_predicate) = if matches!(self.peek().kind, TokenKind::Colon) {
self.advance();
self.parse_predicate_or_return_type(TypePos::ArrowReturn)?
} else {
(None, None)
};
if !matches!(self.peek().kind, TokenKind::LeftBrace) {
self.error_at_peek("expected `{` to open the method body");
return None;
}
let block = self.parse_outer_this_boundary(Self::parse_block)?;
let end = parse_arena_result(self.ast.try_stmt(block), &mut self.fatal)?
.span
.end;
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Arrow {
params,
return_type,
type_predicate,
body: ArrowBody::Block(block),
},
span: self.span(name_span.start, end),
}),
&mut self.fatal,
)
}
fn parse_array_literal(&mut self) -> Option<ExprId> {
let open = self.advance();
let mut elements: Vec<ArrayLiteralElement> = Vec::new();
if !matches!(self.peek().kind, TokenKind::RightBracket) {
loop {
let element = if matches!(self.peek().kind, TokenKind::DotDotDot) {
let dots = self.advance();
let value = self.parse_expression()?;
let value_span =
parse_arena_result(self.ast.try_expr(value), &mut self.fatal)?.span;
ArrayLiteralElement::Spread {
value,
span: self.span(dots.span.start, value_span.end),
}
} else {
ArrayLiteralElement::Value(self.parse_expression()?)
};
elements.push(element);
match self.peek().kind {
TokenKind::Comma => {
self.advance();
if matches!(self.peek().kind, TokenKind::RightBracket) {
break;
}
}
TokenKind::RightBracket => break,
_ => {
self.error_at_peek("expected `,` or `]`");
return None;
}
}
}
}
if !matches!(self.peek().kind, TokenKind::RightBracket) {
self.error_at_peek("expected `]`");
return None;
}
let close = self.advance();
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::ArrayLiteral { elements },
span: self.span(open.span.start, close.span.end),
}),
&mut self.fatal,
)
}
fn parse_paren(&mut self) -> Option<ExprId> {
let open = self.advance();
let inner = self.parse_expression()?;
if !matches!(self.peek().kind, TokenKind::RightParen) {
self.error_at_peek("expected `)`");
return None;
}
let close = self.advance();
let span = self.span(open.span.start, close.span.end);
parse_arena_result(
self.ast.try_push_expr(Expr {
kind: ExprKind::Paren(inner),
span,
}),
&mut self.fatal,
)
}
fn with_recursion_limit<T>(&mut self, parse: impl FnOnce(&mut Self) -> Option<T>) -> Option<T> {
if self.fatal.is_some() {
return None;
}
if self.recursion_limit_span.is_some() || self.recursion_depth >= MAX_PARSE_DEPTH {
self.stop_at_recursion_limit();
return None;
}
self.recursion_depth += 1;
let result = parse(self);
self.recursion_depth -= 1;
result
}
fn stop_at_recursion_limit(&mut self) {
self.recursion_limit_span.get_or_insert(self.peek().span);
self.pos = self.tokens.len().saturating_sub(1);
}
fn recover(&mut self) {
while !self.is_at_eof() {
match self.peek().kind {
TokenKind::Semicolon => {
self.advance();
return;
}
TokenKind::RightBrace
| TokenKind::Let
| TokenKind::Const
| TokenKind::Function
| TokenKind::Export
| TokenKind::If
| TokenKind::While
| TokenKind::Return
| TokenKind::Try
| TokenKind::Throw => return,
_ => {
self.advance();
}
}
}
}
fn peek(&self) -> &Token {
self.tokens.get(self.pos).unwrap_or(&self.eof)
}
fn peek_at(&self, offset: usize) -> &Token {
self.pos
.checked_add(offset)
.and_then(|index| self.tokens.get(index))
.unwrap_or(&self.eof)
}
fn at_inserted_semicolon(&self) -> bool {
let token = self.peek();
matches!(token.kind, TokenKind::Semicolon) && token.span.start == token.span.end
}
fn advance(&mut self) -> Token {
let tok = self.peek().clone();
if self.pos < self.tokens.len().saturating_sub(1) {
self.pos += 1;
}
tok
}
fn is_at_eof(&self) -> bool {
self.fatal.is_some() || matches!(self.peek().kind, TokenKind::Eof)
}
fn line_break_before_peek(&self) -> bool {
self.source_has_line_break(self.prev_token_end(), self.peek().span.start)
}
fn line_break_after_peek(&self) -> bool {
self.source_has_line_break(self.peek().span.end, self.peek_at(1).span.start)
}
fn source_has_line_break(&self, start: u32, end: u32) -> bool {
self.source
.get(start as usize..end as usize)
.is_some_and(|gap| gap.contains(['\n', '\r']))
}
fn at_statement_end(&self) -> bool {
matches!(self.peek().kind, TokenKind::Semicolon)
|| (self.last_as_type_end == Some(self.prev_token_end())
&& self.line_break_before_peek())
}
fn finish_statement(&mut self) -> u32 {
if matches!(self.peek().kind, TokenKind::Semicolon) {
return self.advance().span.end;
}
self.prev_token_end()
}
fn prev_token_end(&self) -> u32 {
self.tokens
.get(self.pos.saturating_sub(1))
.unwrap_or(&self.eof)
.span
.end
}
fn take_leading_doc(&mut self) -> Option<crate::DocComment> {
let parsed = self
.peek()
.leading_doc
.as_ref()
.map(crate::parse_doc_comment)
.transpose();
match parsed {
Ok(doc) => doc,
Err(error) => {
self.fatal = Some(error.into_compiler_failure(CompilerStage::Parse));
None
}
}
}
fn parse_doc(&mut self, raw: &crate::RawDoc) -> Option<crate::DocComment> {
match crate::parse_doc_comment(raw) {
Ok(doc) => Some(doc),
Err(error) => {
self.fatal = Some(error.into_compiler_failure(CompilerStage::Parse));
None
}
}
}
fn error_at(&mut self, span: Span, message: impl Into<String>) {
self.diagnostics.push(Diagnostic {
severity: Severity::Error,
span,
message: message.into(),
help: vec![],
notes: vec![],
});
}
fn reject_loose_nullish_comparison(
&mut self,
op: BinOp,
lhs: ExprId,
rhs: ExprId,
) -> Option<()> {
let lhs_span = parse_arena_result(self.ast.try_expr(lhs), &mut self.fatal)?.span;
let rhs_span = parse_arena_result(self.ast.try_expr(rhs), &mut self.fatal)?.span;
let (nullish, operand, nullish_first) =
match (self.nullish_operand(lhs)?, self.nullish_operand(rhs)?) {
(Some(left), Some(right)) => {
self.reject_nullish_against_nullish(op, lhs_span, rhs_span, [left, right]);
return Some(());
}
(None, Some(nullish)) => (nullish, lhs, false),
(Some(nullish), None) => (nullish, rhs, true),
(None, None) => return Some(()),
};
let comparison = LooseComparison {
nullish,
nullish_first,
negated: op == BinOp::NotEq,
};
let operand_span = parse_arena_result(self.ast.try_expr(operand), &mut self.fatal)?.span;
let x = self.single_line_source(operand_span).unwrap_or("x");
let strict = self.strict_suggestion(&comparison, x);
let either = self.either_suggestion(&comparison, operand, x)?;
let literal = comparison.nullish.literal.text();
let written = self
.single_line_source(self.span(lhs_span.start, rhs_span.end))
.map_or_else(
|| {
let written_op = if comparison.negated { "!=" } else { "==" };
comparison.ordered(x, written_op, literal)
},
str::to_string,
);
self.error_at_with_help(
self.span(lhs_span.start, rhs_span.end),
format!(
"`{written}` does not also match `{}`: `==` compares like `===`",
comparison.nullish.literal.other().text()
),
vec![
either,
format!("to test for `{literal}` alone, write `{strict}`"),
],
);
Some(())
}
fn reject_nullish_against_nullish(
&mut self,
op: BinOp,
lhs: Span,
rhs: Span,
sides: [NullishOperand; 2],
) {
let span = self.span(lhs.start, rhs.end);
let subject = self.single_line_source(span).map_or_else(
|| "this comparison".to_string(),
|written| format!("`{written}`"),
);
let value = if op == BinOp::NotEq { "false" } else { "true" };
let effects: Vec<String> = sides
.iter()
.filter_map(|side| side.effect)
.map(|effect| {
self.single_line_source(effect.operand).map_or_else(
|| "the `void` operand's expression".to_string(),
|statement| format!("`{statement};`"),
)
})
.collect();
let help = if effects.is_empty() {
format!("in TypeScript this is always `{value}`; write `{value}`")
} else {
format!(
"in TypeScript this is always `{value}`; run {} {}, then use `{value}`",
effects.join(" and "),
if effects.len() > 1 {
"each as its own statement"
} else {
"as its own statement"
}
)
};
self.error_at_with_help(
span,
format!("{subject} compares two nullish values: `==` compares like `===`"),
vec![help],
);
}
fn strict_suggestion(&self, comparison: &LooseComparison, x: &str) -> String {
let eq = comparison.strict_op();
match comparison.nullish.effect {
Some(effect) => {
let void = self.single_line_source(effect.void).unwrap_or("void expr");
comparison.ordered(x, eq, void)
}
None => format!("{x} {eq} {}", comparison.nullish.literal.text()),
}
}
fn either_suggestion(
&mut self,
comparison: &LooseComparison,
operand: ExprId,
x: &str,
) -> Option<String> {
let mut steps = Vec::new();
let tested = if self.repeats_without_effect(operand)? {
x
} else {
steps.push(format!("bind `{x}` to a `const` (`const value = {x};`)"));
"value"
};
if let Some(effect) = comparison.nullish.effect {
let statement = self.single_line_source(effect.operand).unwrap_or("expr");
let step = format!("run `{statement};` as its own statement");
if comparison.nullish_first {
steps.insert(0, step);
} else {
steps.push(step);
}
}
let eq = comparison.strict_op();
let joiner = if comparison.negated { "&&" } else { "||" };
let both = format!("{tested} {eq} null {joiner} {tested} {eq} undefined");
Some(if steps.is_empty() {
format!("to test for either, write `{both}`")
} else {
format!(
"to test for either, first {}, then write `{both}`",
steps.join(", then ")
)
})
}
fn repeats_without_effect(&mut self, id: ExprId) -> Option<bool> {
let unwrapped = self.unparenthesized(id)?;
let expr = parse_arena_result(self.ast.try_expr(unwrapped), &mut self.fatal)?;
if is_literal(&expr.kind) {
return Some(true);
}
if self
.nullish_operand(unwrapped)?
.is_some_and(|nullish| nullish.effect.is_none())
{
return Some(true);
}
self.is_reference_path(id)
}
fn nullish_operand(&mut self, id: ExprId) -> Option<Option<NullishOperand>> {
let id = self.unparenthesized(id)?;
let expr = parse_arena_result(self.ast.try_expr(id), &mut self.fatal)?;
let span = expr.span;
Some(match &expr.kind {
ExprKind::Null => Some(NullishOperand {
literal: Nullish::Null,
effect: None,
}),
ExprKind::Identifier(ident) if ident.name == "undefined" => Some(NullishOperand {
literal: Nullish::Undefined,
effect: None,
}),
ExprKind::Void { operand } => {
let operand_id = self.unparenthesized(*operand)?;
let operand =
parse_arena_result(self.ast.try_expr(operand_id), &mut self.fatal)?.clone();
let effect_free =
is_literal(&operand.kind) || self.is_reference_path(operand_id)?;
let effect = (!effect_free).then_some(VoidEffect {
void: span,
operand: operand.span,
});
Some(NullishOperand {
literal: Nullish::Undefined,
effect,
})
}
_ => None,
})
}
fn is_reference_path(&mut self, id: ExprId) -> Option<bool> {
let mut id = id;
loop {
match &parse_arena_result(self.ast.try_expr(id), &mut self.fatal)?.kind {
ExprKind::Identifier(_) | ExprKind::This => return Some(true),
ExprKind::FieldAccess { receiver, .. } => id = *receiver,
ExprKind::Paren(inner) => id = *inner,
_ => return Some(false),
}
}
}
fn unparenthesized(&mut self, id: ExprId) -> Option<ExprId> {
let mut id = id;
loop {
match parse_arena_result(self.ast.try_expr(id), &mut self.fatal)?.kind {
ExprKind::Paren(inner) => id = inner,
_ => return Some(id),
}
}
}
fn single_line_source(&self, span: Span) -> Option<&'a str> {
self.source
.get(span.start as usize..span.end as usize)
.filter(|text| !text.contains('\n'))
}
fn error_at_with_help(&mut self, span: Span, message: impl Into<String>, help: Vec<String>) {
self.diagnostics.push(Diagnostic {
severity: Severity::Error,
span,
message: message.into(),
help,
notes: vec![],
});
}
fn invariant_failure<T>(&mut self, message: &str) -> Option<T> {
self.fatal.get_or_insert_with(|| CompilerFailure::Internal {
stage: CompilerStage::Parse,
span: None,
message: message.into(),
});
self.pos = self.tokens.len();
None
}
fn error_at_peek(&mut self, message: impl Into<String>) {
let span = self.peek().span;
self.error_at(span, message);
}
fn error_at_peek_with_help(&mut self, message: impl Into<String>, help: Vec<String>) {
let span = self.peek().span;
self.error_at_with_help(span, message, help);
}
fn error_count(&self) -> usize {
self.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.count()
}
}
fn is_property_name(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::Identifier
| TokenKind::BooleanLiteral(_)
| TokenKind::NullLiteral
| TokenKind::Let
| TokenKind::Const
| TokenKind::Function
| TokenKind::If
| TokenKind::Else
| TokenKind::While
| TokenKind::Do
| TokenKind::For
| TokenKind::Break
| TokenKind::Continue
| TokenKind::Return
| TokenKind::Switch
| TokenKind::Case
| TokenKind::Default
| TokenKind::Void
| TokenKind::Interface
| TokenKind::Enum
| TokenKind::In
| TokenKind::Typeof
| TokenKind::Import
| TokenKind::Export
| TokenKind::New
| TokenKind::Try
| TokenKind::Catch
| TokenKind::Finally
| TokenKind::Throw
| TokenKind::Class
| TokenKind::Extends
| TokenKind::Implements
| TokenKind::Super
| TokenKind::This
)
}
fn is_reserved_identifier_word(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::BooleanLiteral(_)
| TokenKind::NullLiteral
| TokenKind::Let
| TokenKind::Const
| TokenKind::Function
| TokenKind::If
| TokenKind::Else
| TokenKind::While
| TokenKind::Do
| TokenKind::For
| TokenKind::Break
| TokenKind::Continue
| TokenKind::Return
| TokenKind::Switch
| TokenKind::Case
| TokenKind::Default
| TokenKind::Export
| TokenKind::Void
| TokenKind::Interface
| TokenKind::Enum
| TokenKind::In
| TokenKind::Typeof
| TokenKind::Import
| TokenKind::New
| TokenKind::Try
| TokenKind::Catch
| TokenKind::Finally
| TokenKind::Throw
| TokenKind::Class
| TokenKind::Extends
| TokenKind::Implements
| TokenKind::Super
| TokenKind::This
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum SpecifierList {
Import,
Export,
}
impl SpecifierList {
fn noun(self) -> &'static str {
match self {
SpecifierList::Import => "import",
SpecifierList::Export => "export",
}
}
}
const STRICT_MODE_RESERVED_WORDS: [&str; 6] = [
"package",
"private",
"protected",
"public",
"static",
"yield",
];
fn reserved_keyword_rename_example(keyword: &str) -> &'static str {
match keyword {
"default" => "defaultValue",
_ => "valueName",
}
}
fn mixed_logical(prev: BinOp, next: BinOp) -> bool {
let prev_logical = matches!(prev, BinOp::Or | BinOp::And);
let next_logical = matches!(next, BinOp::Or | BinOp::And);
let prev_nullish = matches!(prev, BinOp::NullishCoalesce);
let next_nullish = matches!(next, BinOp::NullishCoalesce);
(prev_logical && next_nullish) || (prev_nullish && next_logical)
}
fn mixed_logical_help(prev: BinOp, next: BinOp) -> String {
let symbol = |op: BinOp| if op == BinOp::And { "&&" } else { "||" };
let (first, second) = if prev == BinOp::NullishCoalesce {
("??", symbol(next))
} else {
(symbol(prev), "??")
};
format!(
"add parentheses to choose the grouping: `(a {first} b) {second} c` or `a {first} (b {second} c)`"
)
}
const LOGICAL_AND_PREC: u8 = 2;
#[derive(Clone, Copy)]
struct NullishOperand {
literal: Nullish,
effect: Option<VoidEffect>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Nullish {
Null,
Undefined,
}
impl Nullish {
fn text(self) -> &'static str {
match self {
Nullish::Null => "null",
Nullish::Undefined => "undefined",
}
}
fn other(self) -> Self {
match self {
Nullish::Null => Nullish::Undefined,
Nullish::Undefined => Nullish::Null,
}
}
}
struct LooseComparison {
nullish: NullishOperand,
nullish_first: bool,
negated: bool,
}
impl LooseComparison {
fn strict_op(&self) -> &'static str {
if self.negated { "!==" } else { "===" }
}
fn ordered(&self, x: &str, op: &str, nullish: &str) -> String {
if self.nullish_first {
format!("{nullish} {op} {x}")
} else {
format!("{x} {op} {nullish}")
}
}
}
#[derive(Clone, Copy)]
struct VoidEffect {
void: Span,
operand: Span,
}
fn is_literal(kind: &ExprKind) -> bool {
matches!(
kind,
ExprKind::Number(_)
| ExprKind::BigInt(_)
| ExprKind::String(_)
| ExprKind::Boolean(_)
| ExprKind::Null
)
}
struct ParamOrder<'p> {
optional: bool,
required: bool,
name: Option<&'p str>,
span: Span,
}
impl<'p> ParamOrder<'p> {
fn of_param(param: &'p ParamDecl) -> Self {
let (name, span) = match ¶m.pattern {
Some(binding) => (None, binding.span()),
None => (Some(param.name.name.as_str()), param.name.span),
};
Self {
optional: param.optional,
required: !param.is_omittable() && !param.rest,
name,
span,
}
}
fn of_field(field: &'p TypeAnnotationField) -> Self {
Self {
optional: field.optional,
required: !field.optional && !field.rest,
name: Some(field.name.name.as_str()),
span: field.name.span,
}
}
}
struct BinaryToken {
op: BinOp,
precedence: u8,
tokens: usize,
}
struct ShiftToken {
op: BinOp,
tokens: usize,
assignment: bool,
}
impl ShiftToken {
fn new(op: BinOp, tokens: usize, assignment: bool) -> Self {
Self {
op,
tokens,
assignment,
}
}
}
const SHIFT_PRECEDENCE: u8 = 8;
#[derive(Clone, Copy, PartialEq)]
enum TupleElementLabel {
None,
Required,
Optional,
}
fn peek_binop(kind: &TokenKind) -> Option<(BinOp, u8)> {
Some(match kind {
TokenKind::QuestionQuestion => (BinOp::NullishCoalesce, 0),
TokenKind::PipePipe => (BinOp::Or, 1),
TokenKind::AmpAmp => (BinOp::And, LOGICAL_AND_PREC),
TokenKind::Pipe => (BinOp::BitOr, 3),
TokenKind::Caret => (BinOp::BitXor, 4),
TokenKind::Amp => (BinOp::BitAnd, 5),
TokenKind::EqEqEq | TokenKind::EqEq => (BinOp::Eq, 6),
TokenKind::BangEqEq | TokenKind::BangEq => (BinOp::NotEq, 6),
TokenKind::LessThan => (BinOp::Lt, 7),
TokenKind::GreaterThan => (BinOp::Gt, 7),
TokenKind::LessEquals => (BinOp::Le, 7),
TokenKind::GreaterEquals => (BinOp::Ge, 7),
TokenKind::In => (BinOp::In, 7),
TokenKind::Plus => (BinOp::Add, 9),
TokenKind::Minus => (BinOp::Sub, 9),
TokenKind::Star => (BinOp::Mul, 10),
TokenKind::Slash => (BinOp::Div, 10),
TokenKind::Percent => (BinOp::Rem, 10),
TokenKind::StarStar => (BinOp::Pow, 11),
_ => return None,
})
}
fn cannot_start_type(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::Dot
| TokenKind::QuestionDot
| TokenKind::LeftBracket
| TokenKind::Bang
| TokenKind::Semicolon
| TokenKind::RightParen
| TokenKind::RightBrace
| TokenKind::Comma
| TokenKind::Eof
)
}
fn right_operand_min_prec(op: BinOp, prec: u8) -> u8 {
match op {
BinOp::Pow => prec,
BinOp::NullishCoalesce => LOGICAL_AND_PREC + 1,
_ => prec + 1,
}
}
fn compound_op_for_token(kind: &TokenKind) -> Option<BinOp> {
Some(match kind {
TokenKind::PlusEquals => BinOp::Add,
TokenKind::MinusEquals => BinOp::Sub,
TokenKind::StarEquals => BinOp::Mul,
TokenKind::SlashEquals => BinOp::Div,
TokenKind::PercentEquals => BinOp::Rem,
TokenKind::StarStarEquals => BinOp::Pow,
TokenKind::AmpEquals => BinOp::BitAnd,
TokenKind::PipeEquals => BinOp::BitOr,
TokenKind::CaretEquals => BinOp::BitXor,
_ => return None,
})
}
fn is_assign_lookahead(kind: &TokenKind) -> bool {
matches!(kind, TokenKind::Equals) || compound_op_for_token(kind).is_some()
}
fn parse_arena_result<T>(
result: Result<T, crate::arena::ArenaError>,
fatal: &mut Option<CompilerFailure>,
) -> Option<T> {
match result {
Ok(value) => Some(value),
Err(error) => {
fatal.get_or_insert_with(|| error.into_compiler_failure(CompilerStage::Parse));
None
}
}
}
fn is_rest_array_annotation(ty: &crate::TypeAnnotation) -> bool {
use crate::ast::TypeAnnotationKind;
match &ty.kind {
TypeAnnotationKind::Array(_) => true,
TypeAnnotationKind::Readonly(inner) => matches!(inner.kind, TypeAnnotationKind::Array(_)),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::{MAX_ERRORS, MAX_PARSE_DEPTH, Parser, parse};
use crate::source::Sources;
use crate::{Asi, Ast, ExprKind, FileId, ImportKind, StmtKind, Token, TokenKind, diagnostics};
const F: FileId = FileId(0);
#[test]
fn malformed_direct_token_streams_return_typed_failures() {
use crate::compiler_error::CompilerFailure;
for tokens in [
Vec::new(),
vec![Token::new(
TokenKind::Identifier,
crate::Span::new(F, 0, 2).unwrap(),
)],
vec![
Token::new(
TokenKind::Identifier,
crate::Span {
file: F,
start: 1,
end: 2,
},
),
Token::new(TokenKind::Eof, crate::Span::new(F, 2, 2).unwrap()),
],
vec![
Token::new(TokenKind::Eof, crate::Span::at(F)),
Token::new(TokenKind::Eof, crate::Span::at(F)),
],
vec![
Token::new(
TokenKind::Identifier,
crate::Span {
file: F,
start: 2,
end: 0,
},
),
Token::new(TokenKind::Eof, crate::Span::new(F, 2, 2).unwrap()),
],
vec![Token::new(TokenKind::Eof, crate::Span::at(FileId(9)))],
] {
let error = super::parse_checked("é", tokens, F).expect_err("invalid token metadata");
assert!(matches!(
error.fatal,
Some(CompilerFailure::Internal { .. })
));
}
}
#[test]
fn malformed_documentation_metadata_returns_a_fatal_error() {
let source = "/** hello */ function main(): void {}";
let valid = tokens_of(source);
for doc in [
crate::RawDoc {
text: "/** hi */".into(),
span: crate::Span {
file: F,
start: u32::MAX,
end: u32::MAX,
},
},
crate::RawDoc {
text: "/** different */".into(),
span: crate::Span::new(F, 0, 12).unwrap(),
},
crate::RawDoc {
text: "/** hello */".into(),
span: crate::Span::new(FileId(9), 0, 12).unwrap(),
},
] {
let mut tokens = valid.clone();
tokens[0].leading_doc = Some(doc);
let error = super::parse_checked(source, tokens, F).expect_err("invalid doc metadata");
assert!(error.to_string().contains("documentation metadata"));
}
assert!(super::parse_checked(source, valid, F).is_ok());
}
#[test]
fn invalid_parser_dispatch_survives_diagnostic_rollback() {
let mut parser = parser_from_source("let x = 1;");
assert!(parser.try_parse_type_args_only().is_none());
parser.diagnostics.clear();
assert!(parser.fatal.is_some());
assert!(parser.parse_expression().is_none());
}
#[test]
fn invalid_arena_read_stops_parser_and_survives_diagnostic_rollback() {
use crate::{
ExprId,
compiler_error::{CompilerFailure, CompilerStage},
};
let mut parser = parser_from_source("let x = 1;");
assert!(parser.statement_kind_for(ExprId(u32::MAX)).is_none());
assert!(matches!(
parser.fatal,
Some(CompilerFailure::Internal {
stage: CompilerStage::Parse,
span: None,
..
})
));
parser.diagnostics.clear();
let position = parser.pos;
assert!(parser.parse_statement().is_none());
parser.recover();
assert_eq!(parser.pos, position);
assert!(parser.ast.expr_ids().unwrap().next().is_none());
}
fn tokens_of(source: &str) -> Vec<Token> {
let mut asi = Asi::new(source, crate::FileId(0));
let mut tokens = Vec::new();
loop {
let tok = asi.next_token();
let is_eof = matches!(tok.kind, TokenKind::Eof);
tokens.push(tok);
if is_eof {
break;
}
}
let diags = asi.into_diagnostics();
assert!(diags.is_empty(), "unexpected lexer diagnostics: {diags:?}");
tokens
}
fn parse_str(source: &str) -> (Ast, Vec<crate::Diagnostic>) {
parse(source, tokens_of(source), crate::FileId(0))
}
fn parser_from_source(source: &str) -> Parser<'_> {
Parser {
source,
tokens: tokens_of(source),
file: F,
pos: 0,
ast: Ast::new(),
diagnostics: Vec::new(),
block_depth: 0,
class_member_body_depth: 0,
function_expression_body_depth: 0,
recursion_depth: 0,
recursion_limit_span: None,
last_as_type_end: None,
eof: Token::new(
TokenKind::Eof,
crate::Span::new(F, source.len() as u32, source.len() as u32).unwrap(),
),
fatal: None,
}
}
#[test]
fn recursion_budget_accepts_boundary_and_restores_depth() {
let source = format!("{}true", "!".repeat(MAX_PARSE_DEPTH - 1));
let mut parser = parser_from_source(&source);
assert!(parser.parse_unary().is_some());
assert_eq!(parser.recursion_depth, 0);
assert!(parser.recursion_limit_span.is_none());
let source = format!("{}true", "!".repeat(MAX_PARSE_DEPTH));
let mut parser = parser_from_source(&source);
assert!(parser.parse_unary().is_none());
assert_eq!(parser.recursion_depth, 0);
assert!(parser.recursion_limit_span.is_some());
}
#[test]
fn parse_empty_input() {
let (ast, diags) = parse_str("");
assert!(diags.is_empty());
assert!(ast.top_level.is_empty());
}
#[test]
fn any_type_is_rejected() {
for src in [
"function main(): void { const x: any = 1; }",
"function main(): void { const x = 1 as any; }",
"function main(): void { const x: any[] = []; }",
] {
let (_ast, diags) = parse_str(src);
assert!(
diags
.iter()
.any(|d| d.message.contains("`any` is not supported")),
"expected an `any` rejection for {src:?}, got: {diags:?}",
);
assert!(
diags
.iter()
.any(|d| d.help.iter().any(|h| h.contains("`unknown`"))),
"expected the fix-shape help for {src:?}, got: {diags:?}",
);
}
}
#[test]
fn any_catch_binding_help_suggests_catch_forms() {
let (_ast, diags) = parse_str("function main(): void { try { } catch (e: any) { } }");
let any_diags: Vec<_> = diags
.iter()
.filter(|d| d.message.contains("`any` is not supported"))
.collect();
assert_eq!(any_diags.len(), 1, "got: {diags:?}");
assert!(
any_diags[0].help.iter().any(|h| h.contains("`catch (e)`")),
"got: {diags:?}"
);
assert!(
!any_diags[0].help.iter().any(|h| h.contains("`unknown`")),
"got: {diags:?}"
);
}
#[test]
fn arrow_return_type_scanner_matches_type_grammar() {
for ty in [
"number",
"void",
"null",
"ns.Foo",
"Map<string, number>",
"ns.Foo<string>[]",
"number[]",
"[number, number]",
"[number, [string, boolean]]",
"[number, string][]",
"{ a: number }",
"{ a: number }[]",
"\"on\" | \"off\"",
"1 | 2",
"Point | null",
"| number | string",
"(n: number) => number",
"(n: number) => [number, number]",
"readonly number[]",
"readonly number[][]",
"readonly [number, string]",
"readonly number[] | null",
"[first: number, second: string]",
"[new: number, string]",
"keyof { a: number }",
] {
let annotation = format!("function f(): void {{ const a: {ty} = x; }}");
let (_ast, diags) = parse_str(&annotation);
assert!(
diags.is_empty(),
"annotation grammar rejected {ty:?}: {diags:?}",
);
let arrow = format!("function f(): void {{ const g = (): {ty} => x; }}");
let (_ast, diags) = parse_str(&arrow);
assert!(
diags.is_empty(),
"arrow lookahead rejected return type {ty:?}: {diags:?}",
);
}
let (_ast, diags) =
parse_str("function f(): void { const g = (v: unknown): v is string => x; }");
assert!(
diags.is_empty(),
"arrow lookahead rejected predicate return: {diags:?}",
);
}
#[test]
fn bare_semicolons_are_silent_empty_statements() {
let (ast, diags) = parse_str(";");
assert!(diags.is_empty());
assert!(ast.top_level.is_empty());
}
#[test]
fn parse_single_non_expression_token_errors() {
let (ast, diags) = parse_str("}");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected expression");
assert!(ast.top_level.is_empty());
}
#[test]
fn recovery_across_many_bad_starts() {
let (ast, diags) = parse_str("} } } x;");
assert_eq!(diags.len(), 3);
for d in &diags {
assert_eq!(d.message, "expected expression");
}
assert_eq!(ast.top_level.len(), 1);
}
#[test]
fn recovery_consumes_semicolon() {
let mut p = parser_from_source("foo ;");
p.error_at_peek("test");
p.recover();
assert!(matches!(
p.peek().kind,
TokenKind::Semicolon | TokenKind::Eof
));
assert!(matches!(p.peek().kind, TokenKind::Eof));
}
#[test]
fn recovery_stops_at_right_brace() {
let mut p = parser_from_source("foo }");
p.error_at_peek("test");
p.recover();
assert!(matches!(p.peek().kind, TokenKind::RightBrace));
}
#[test]
fn recovery_stops_at_let_keyword() {
let mut p = parser_from_source("foo let x");
p.error_at_peek("test");
p.recover();
assert!(matches!(p.peek().kind, TokenKind::Let));
}
#[test]
fn recovery_stops_at_function_keyword() {
let mut p = parser_from_source("foo function bar");
p.error_at_peek("test");
p.recover();
assert!(matches!(p.peek().kind, TokenKind::Function));
}
#[test]
fn error_cap_at_20() {
let source = "} ".repeat(25);
let (_, diags) = parse_str(&source);
assert_eq!(diags.len(), MAX_ERRORS);
}
#[test]
fn driver_makes_progress_past_unmatched_brace() {
let (ast, diags) = parse_str("} x;");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected expression");
assert_eq!(ast.top_level.len(), 1);
}
#[test]
fn snapshot_multi_error_render() {
let source = "foo bar;\nbaz qux;\nhello world;";
let (_, diags) = parse_str(source);
let (sources, _) = Sources::single("script.subm", source).unwrap();
let rendered: String = diags
.iter()
.map(|d| diagnostics::render(d, &sources))
.collect::<Vec<_>>()
.join("\n");
insta::assert_snapshot!(rendered);
}
fn single_stmt(ast: &Ast) -> &crate::Stmt {
assert_eq!(ast.top_level.len(), 1);
ast.try_stmt(ast.top_level[0]).unwrap()
}
fn expr_of_single_stmt(ast: &Ast) -> &crate::Expr {
let stmt = single_stmt(ast);
match stmt.kind {
crate::StmtKind::Expr(id) => ast.try_expr(id).unwrap(),
_ => panic!("expected expression statement, got {:?}", stmt.kind),
}
}
#[test]
fn parse_number_literal() {
let (ast, diags) = parse_str("42;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
assert_eq!(expr.kind, crate::ExprKind::Number(42.0));
assert_eq!(expr.span, crate::Span::new(F, 0, 2).unwrap());
}
#[test]
fn parse_string_literal() {
let (ast, diags) = parse_str(r#""s";"#);
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
assert_eq!(expr.kind, crate::ExprKind::String("s".to_string()));
assert_eq!(expr.span, crate::Span::new(F, 0, 3).unwrap());
}
#[test]
fn parse_boolean_true() {
let (ast, diags) = parse_str("true;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
assert_eq!(expr.kind, crate::ExprKind::Boolean(true));
assert_eq!(expr.span, crate::Span::new(F, 0, 4).unwrap());
}
#[test]
fn parse_boolean_false() {
let (ast, diags) = parse_str("false;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
assert_eq!(expr.kind, crate::ExprKind::Boolean(false));
assert_eq!(expr.span, crate::Span::new(F, 0, 5).unwrap());
}
#[test]
fn parse_null_literal() {
let (ast, diags) = parse_str("null;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
assert_eq!(expr.kind, crate::ExprKind::Null);
assert_eq!(expr.span, crate::Span::new(F, 0, 4).unwrap());
}
#[test]
fn parse_identifier() {
let (ast, diags) = parse_str("x;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
assert!(matches!(expr.kind, crate::ExprKind::Identifier(_)));
assert_eq!(expr.span, crate::Span::new(F, 0, 1).unwrap());
}
#[test]
fn reserved_keywords_remain_invalid_declaration_names() {
let (_ast, diags) = parse_str("function default(): void {}");
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(
diags[0].message,
"`default` is a reserved keyword and can't be used as a name"
);
assert!(
diags[0]
.help
.iter()
.any(|h| h == "rename it, for example: `defaultValue`"),
"expected rename help, got: {diags:?}"
);
}
#[test]
fn reserved_keywords_remain_invalid_parameter_names() {
let (_ast, diags) = parse_str("function f(default: string): void {}");
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(
diags[0].message,
"`default` is a reserved keyword and can't be used as a name"
);
assert!(
diags[0]
.help
.iter()
.any(|h| h == "rename it, for example: `defaultValue`"),
"expected rename help, got: {diags:?}"
);
}
#[test]
fn contextual_keywords_are_valid_names() {
for src in [
"const type = \"x\";",
"const from = 1;",
"let of = 2;",
"const as = 3;",
"const is = 4;",
"function f(from: string): void {}",
] {
let (_ast, diags) = parse_str(src);
assert!(
diags.is_empty(),
"unexpected diagnostics for {src:?}: {diags:#?}"
);
}
}
#[test]
fn type_alias_dispatch_table() {
let (ast, diags) = parse_str("type X = number;");
assert!(diags.is_empty(), "{diags:#?}");
assert!(matches!(
ast.try_stmt(ast.top_level[0]).unwrap().kind,
crate::StmtKind::TypeAliasDecl { .. }
));
for src in ["type = 5;", "type;", "type(1);", "type < 3;", "type.foo;"] {
let (ast, diags) = parse_str(src);
assert!(
diags.is_empty(),
"unexpected diagnostics for {src:?}: {diags:#?}"
);
assert!(
!matches!(
ast.try_stmt(ast.top_level[0]).unwrap().kind,
crate::StmtKind::TypeAliasDecl { .. }
),
"{src:?} must not parse as a type alias"
);
}
let (_ast, diags) = parse_str("type X;");
assert!(
diags
.iter()
.any(|d| d.message == "expected `=` after type alias name"),
"bare `type X` should commit to the alias diagnostic, got: {diags:#?}"
);
}
#[test]
fn export_type_alias_dispatch() {
let (ast, diags) = parse_str("export type X = number;");
assert!(diags.is_empty(), "{diags:#?}");
assert_eq!(ast.exported_decls.len(), 1);
assert!(matches!(
ast.try_stmt(ast.exported_decls[0].stmt).unwrap().kind,
crate::StmtKind::TypeAliasDecl { .. }
));
let (_ast, diags) = parse_str("export type = 5;");
assert!(
diags
.iter()
.any(|d| d.message == "expected a declaration after `export`"),
"`export type = 5` must not become an exported assignment, got: {diags:#?}"
);
}
#[test]
fn parse_paren_simple() {
let (ast, diags) = parse_str("(x);");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
let crate::ExprKind::Paren(inner_id) = expr.kind else {
panic!("expected Paren, got {:?}", expr.kind);
};
assert_eq!(expr.span, crate::Span::new(F, 0, 3).unwrap());
let inner = ast.try_expr(inner_id).unwrap();
assert!(matches!(inner.kind, crate::ExprKind::Identifier(_)));
assert_eq!(inner.span, crate::Span::new(F, 1, 2).unwrap());
}
#[test]
fn parse_paren_nested() {
let (ast, diags) = parse_str("((x));");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let crate::ExprKind::Paren(mid_id) = outer.kind else {
panic!("expected outer Paren");
};
assert_eq!(outer.span, crate::Span::new(F, 0, 5).unwrap());
let mid = ast.try_expr(mid_id).unwrap();
let crate::ExprKind::Paren(inner_id) = mid.kind else {
panic!("expected inner Paren");
};
assert_eq!(mid.span, crate::Span::new(F, 1, 4).unwrap());
let inner = ast.try_expr(inner_id).unwrap();
assert!(matches!(inner.kind, crate::ExprKind::Identifier(_)));
assert_eq!(inner.span, crate::Span::new(F, 2, 3).unwrap());
}
#[test]
fn parse_sequence_of_atoms() {
let (ast, diags) = parse_str(r#"42; "hi"; true; null; x; (x);"#);
assert!(diags.is_empty());
assert_eq!(ast.top_level.len(), 6);
insta::assert_debug_snapshot!(ast);
}
#[test]
fn missing_semicolon_after_expression() {
let (ast, diags) = parse_str("42 43");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected `;` after expression");
assert!(ast.top_level.is_empty());
}
#[test]
fn empty_paren_diagnoses() {
let (ast, diags) = parse_str("();");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected expression");
assert!(ast.top_level.is_empty());
}
#[test]
fn unterminated_paren_diagnoses() {
let (ast, diags) = parse_str("(x;");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected `)`");
assert!(ast.top_level.is_empty());
}
fn binary(e: &crate::Expr) -> (crate::BinOp, crate::ExprId, crate::ExprId) {
match e.kind {
crate::ExprKind::Binary { op, lhs, rhs } => (op, lhs, rhs),
_ => panic!("expected Binary, got {:?}", e.kind),
}
}
#[test]
fn parse_binary_add() {
let (ast, diags) = parse_str("a + b;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Add);
assert!(matches!(
ast.try_expr(lhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(outer.span, crate::Span::new(F, 0, 5).unwrap());
}
#[test]
fn bitwise_precedence_and_shift_adjacency() {
let (ast, diags) = parse_str("a | b ^ c & d == e >> f + g;");
assert!(diags.is_empty(), "{diags:?}");
let (op, _, mut rhs) = binary(expr_of_single_stmt(&ast));
assert_eq!(op, crate::BinOp::BitOr);
for expected in [
crate::BinOp::BitXor,
crate::BinOp::BitAnd,
crate::BinOp::Eq,
crate::BinOp::Shr,
crate::BinOp::Add,
] {
let (op, _, next) = binary(ast.try_expr(rhs).unwrap());
assert_eq!(op, expected);
rhs = next;
}
for source in ["a > > b;", "a >/*gap*/> b;", "a < < b;"] {
let (_, diags) = parse_str(source);
assert!(!diags.is_empty(), "{source}");
}
}
#[test]
fn bitwise_shift_assignment_span() {
let (ast, diags) = parse_str("a >>>= b;");
assert!(diags.is_empty(), "{diags:?}");
let crate::StmtKind::CompoundAssign { op, op_span, .. } = single_stmt(&ast).kind else {
panic!("expected assignment");
};
assert_eq!(op, crate::BinOp::UnsignedShr);
assert_eq!(op_span, crate::Span::new(F, 2, 6).unwrap());
}
#[test]
fn precedence_mul_binds_tighter_than_add() {
let (ast, diags) = parse_str("a + b * c;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Add);
assert!(matches!(
ast.try_expr(lhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
let (rop, rlhs, rrhs) = binary(ast.try_expr(rhs).unwrap());
assert_eq!(rop, crate::BinOp::Mul);
assert!(matches!(
ast.try_expr(rlhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(
ast.try_expr(rrhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn precedence_mul_left_of_add() {
let (ast, diags) = parse_str("a * b + c;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Add);
let (lop, ..) = binary(ast.try_expr(lhs).unwrap());
assert_eq!(lop, crate::BinOp::Mul);
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn left_associative_same_precedence() {
let (ast, diags) = parse_str("a - b - c;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Sub);
let (lop, llhs, lrhs) = binary(ast.try_expr(lhs).unwrap());
assert_eq!(lop, crate::BinOp::Sub);
assert!(matches!(
ast.try_expr(llhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(
ast.try_expr(lrhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn equality_inside_logical() {
let (ast, diags) = parse_str("a === b && c === d;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::And);
let (lop, ..) = binary(ast.try_expr(lhs).unwrap());
assert_eq!(lop, crate::BinOp::Eq);
let (rop, ..) = binary(ast.try_expr(rhs).unwrap());
assert_eq!(rop, crate::BinOp::Eq);
}
#[test]
fn double_equal_is_same_variant_as_triple() {
let (ast, _) = parse_str("a == b;");
let outer = expr_of_single_stmt(&ast);
let (op, ..) = binary(outer);
assert_eq!(op, crate::BinOp::Eq);
}
#[test]
fn bang_eq_is_not_eq() {
let (ast, _) = parse_str("a != b;");
let outer = expr_of_single_stmt(&ast);
let (op, ..) = binary(outer);
assert_eq!(op, crate::BinOp::NotEq);
}
#[test]
fn comparison_has_higher_prec_than_logical() {
let (ast, _) = parse_str("a < b && c;");
let outer = expr_of_single_stmt(&ast);
let (op, lhs, _) = binary(outer);
assert_eq!(op, crate::BinOp::And);
let (lop, ..) = binary(ast.try_expr(lhs).unwrap());
assert_eq!(lop, crate::BinOp::Lt);
}
#[test]
fn full_precedence_chain_snapshot() {
let (ast, diags) = parse_str("a || b && c === d < e + f * g;");
assert!(diags.is_empty());
insta::assert_debug_snapshot!(ast);
}
#[test]
fn paren_overrides_precedence() {
let (ast, _) = parse_str("(a + b) * c;");
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Mul);
let crate::ExprKind::Paren(inner_id) = ast.try_expr(lhs).unwrap().kind else {
panic!("expected Paren on lhs");
};
let (inner_op, ..) = binary(ast.try_expr(inner_id).unwrap());
assert_eq!(inner_op, crate::BinOp::Add);
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn binary_missing_rhs_diagnoses() {
let (ast, diags) = parse_str("a +;");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected expression");
assert!(ast.top_level.is_empty());
}
fn unary(e: &crate::Expr) -> (crate::UnOp, crate::ExprId) {
match e.kind {
crate::ExprKind::Unary { op, operand } => (op, operand),
_ => panic!("expected Unary, got {:?}", e.kind),
}
}
#[test]
fn parse_unary_not() {
let (ast, _) = parse_str("!x;");
let outer = expr_of_single_stmt(&ast);
let (op, operand) = unary(outer);
assert_eq!(op, crate::UnOp::Not);
assert!(matches!(
ast.try_expr(operand).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(outer.span, crate::Span::new(F, 0, 2).unwrap());
}
#[test]
fn parse_unary_neg() {
let (ast, _) = parse_str("-x;");
let (op, _) = unary(expr_of_single_stmt(&ast));
assert_eq!(op, crate::UnOp::Neg);
}
#[test]
fn parse_unary_pos() {
let (ast, _) = parse_str("+x;");
let (op, _) = unary(expr_of_single_stmt(&ast));
assert_eq!(op, crate::UnOp::Pos);
}
#[test]
fn repeated_unary_not() {
let (ast, _) = parse_str("!!x;");
let outer = expr_of_single_stmt(&ast);
let (op, operand) = unary(outer);
assert_eq!(op, crate::UnOp::Not);
let (inner_op, inner_operand) = unary(ast.try_expr(operand).unwrap());
assert_eq!(inner_op, crate::UnOp::Not);
assert!(matches!(
ast.try_expr(inner_operand).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn unary_binds_tighter_than_binary() {
let (ast, _) = parse_str("-x + 1;");
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Add);
let (unop, _) = unary(ast.try_expr(lhs).unwrap());
assert_eq!(unop, crate::UnOp::Neg);
assert_eq!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Number(1.0)
);
}
#[test]
fn unary_over_paren() {
let (ast, _) = parse_str("-(x + 1);");
let outer = expr_of_single_stmt(&ast);
let (op, operand) = unary(outer);
assert_eq!(op, crate::UnOp::Neg);
let crate::ExprKind::Paren(inner_id) = ast.try_expr(operand).unwrap().kind else {
panic!("expected Paren");
};
let (inner_op, ..) = binary(ast.try_expr(inner_id).unwrap());
assert_eq!(inner_op, crate::BinOp::Add);
}
#[test]
fn parse_is_as_expression_rejected() {
let (_, diags) = parse_str("x is number;");
assert!(!diags.is_empty(), "expected parse error for `x is T`");
}
fn as_cast(e: &crate::Expr) -> (crate::ExprId, &crate::TypeAnnotation) {
match &e.kind {
crate::ExprKind::As { expr, ty } => (*expr, ty),
other => panic!("expected ExprKind::As, got {other:?}"),
}
}
#[test]
fn parse_instanceof_class() {
let (ast, diags) = parse_str("x instanceof Foo;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let crate::ExprKind::InstanceOf { value, ty } = &outer.kind else {
panic!("expected ExprKind::InstanceOf, got {:?}", outer.kind);
};
assert!(matches!(
ast.try_expr(*value).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Name { .. }));
}
#[test]
fn parse_instanceof_binds_tighter_than_logical_and() {
let (ast, diags) = parse_str("a instanceof B && c;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::And);
assert!(matches!(
ast.try_expr(lhs).unwrap().kind,
crate::ExprKind::InstanceOf { .. }
));
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn parse_angle_cast_lowers_to_the_as_node() {
let (ast, diags) = parse_str("<number>x;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (inner, ty) = as_cast(outer);
assert!(matches!(
ast.try_expr(inner).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Name { .. }));
}
#[test]
fn a_less_than_comparison_is_not_read_as_a_cast() {
let (ast, diags) = parse_str("a < b;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let e = expr_of_single_stmt(&ast);
assert!(matches!(e.kind, crate::ExprKind::Binary { .. }));
}
#[test]
fn parse_typeof_in_type_position() {
let (_ast, diags) = parse_str("let a: typeof b = 1;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
#[test]
fn parse_typeof_with_a_dotted_path() {
let (_ast, diags) = parse_str("let a: typeof b.c.d = 1;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
#[test]
fn parse_as_primitive() {
let (ast, diags) = parse_str("x as number;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (inner, ty) = as_cast(outer);
assert!(matches!(
ast.try_expr(inner).unwrap().kind,
crate::ExprKind::Identifier(_)
));
match &ty.kind {
crate::TypeAnnotationKind::Name { args, .. } => {
assert!(args.is_empty());
}
other => panic!("expected Name target, got {other:?}"),
}
assert_eq!(outer.span, crate::Span::new(F, 0, 11).unwrap());
}
#[test]
fn parse_as_object_literal_type() {
let (ast, diags) = parse_str("x as { a: number };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (_, ty) = as_cast(outer);
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Object { .. }));
}
#[test]
fn parse_as_array_short() {
let (ast, diags) = parse_str("x as string[];");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (_, ty) = as_cast(outer);
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Array { .. }));
}
#[test]
fn parse_as_array_generic() {
let (ast, diags) = parse_str("x as T[];");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (_, ty) = as_cast(outer);
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Array { .. }));
}
#[test]
fn parse_as_generic_instantiation() {
let (ast, diags) = parse_str("x as Foo<Bar>;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (_, ty) = as_cast(outer);
match &ty.kind {
crate::TypeAnnotationKind::Name { args, .. } => {
assert_eq!(args.len(), 1, "Foo<Bar> has one type arg");
}
other => panic!("expected Name with type args, got {other:?}"),
}
}
#[test]
fn parse_as_precedence_higher_than_additive() {
let (ast, diags) = parse_str("x as number + 1;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Add);
assert!(matches!(
ast.try_expr(lhs).unwrap().kind,
crate::ExprKind::As { .. }
));
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Number(_)
));
}
#[test]
fn parse_as_precedence_lower_than_unary() {
let (ast, diags) = parse_str("!x as boolean;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (inner, _) = as_cast(outer);
assert!(matches!(
ast.try_expr(inner).unwrap().kind,
crate::ExprKind::Unary {
op: crate::UnOp::Not,
..
}
));
}
#[test]
fn parse_as_left_associative_chain() {
let (ast, diags) = parse_str("x as A as B;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (inner, _outer_ty) = as_cast(outer);
let (innermost, _inner_ty) = as_cast(ast.try_expr(inner).unwrap());
assert!(matches!(
ast.try_expr(innermost).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn parse_as_inside_call_arg() {
let (ast, diags) = parse_str("f(x as T);");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
match &outer.kind {
crate::ExprKind::Call { args, .. } => {
assert_eq!(args.len(), 1);
assert!(matches!(
ast.try_expr(args[0]).unwrap().kind,
crate::ExprKind::As { .. }
));
}
other => panic!("expected Call, got {other:?}"),
}
}
#[test]
fn parse_as_after_field_access() {
let (ast, diags) = parse_str("obj.field as T;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (inner, _) = as_cast(outer);
assert!(matches!(
ast.try_expr(inner).unwrap().kind,
crate::ExprKind::FieldAccess { .. }
));
}
#[test]
fn parse_type_guard_return_annotation_on_function() {
let src = "function isCircle(s: Shape): s is Circle { return true; }";
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function {
ref return_type,
ref type_predicate,
..
} => {
assert!(
return_type.is_none(),
"predicate form leaves return_type empty",
);
let tp = type_predicate.as_ref().expect("predicate parsed");
assert_eq!(tp.param.name, "s");
assert!(matches!(
tp.asserted.kind,
crate::TypeAnnotationKind::Name { .. }
));
assert_eq!(tp.span, crate::Span::new(F, 29, 40).unwrap());
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_type_guard_return_annotation_on_arrow() {
let src = "const f = (s: Shape): s is Circle => true;";
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let value = match &ast.try_stmt(ast.top_level[0]).unwrap().kind {
crate::StmtKind::Const { value, .. } => *value,
_ => panic!("expected const"),
};
match &ast.try_expr(value).unwrap().kind {
crate::ExprKind::Arrow {
return_type,
type_predicate,
..
} => {
assert!(return_type.is_none());
let tp = type_predicate.as_ref().expect("predicate parsed");
assert_eq!(tp.param.name, "s");
}
other => panic!("expected Arrow, got {other:?}"),
}
}
#[test]
fn parse_plain_return_type_unaffected_by_predicate_parser() {
let src = "function f(s: Shape): boolean { return true; }";
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function {
ref return_type,
ref type_predicate,
..
} => {
assert!(return_type.is_some(), "plain form has return_type");
assert!(type_predicate.is_none(), "plain form has no type_predicate",);
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_type_guard_rejects_non_return_position() {
let (_, diags) = parse_str("let x: y is Foo = null;");
assert!(
!diags.is_empty(),
"expected parse error for `is` outside return-type position",
);
}
#[test]
fn parse_typeof_prefix() {
let (ast, diags) = parse_str("typeof x;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let crate::ExprKind::Typeof { operand } = outer.kind else {
panic!("expected Typeof, got {:?}", outer.kind);
};
assert!(matches!(
ast.try_expr(operand).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(outer.span, crate::Span::new(F, 0, 8).unwrap());
}
#[test]
fn parse_typeof_in_equality() {
let (ast, diags) = parse_str(r#"typeof x === "number";"#);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Eq);
assert!(matches!(
ast.try_expr(lhs).unwrap().kind,
crate::ExprKind::Typeof { .. }
));
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::String(ref s) if s == "number"
));
}
#[test]
fn parse_let_without_type() {
let (ast, diags) = parse_str("let x = 1;");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
assert_eq!(stmt.span, crate::Span::new(F, 0, 10).unwrap());
match stmt.kind {
crate::StmtKind::Let {
ref name,
ref ty,
value,
..
} => {
assert_eq!(name.name, "x");
assert_eq!(name.span, crate::Span::new(F, 4, 5).unwrap());
assert!(ty.is_none());
assert_eq!(
ast.try_expr(value).unwrap().kind,
crate::ExprKind::Number(1.0)
);
}
_ => panic!("expected Let"),
}
}
#[test]
fn parse_const_with_type_and_string() {
let (ast, diags) = parse_str(r#"const y: string = "hi";"#);
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Const {
ref name,
ref ty,
value,
..
} => {
assert_eq!(name.name, "y");
assert_eq!(name.span, crate::Span::new(F, 6, 7).unwrap());
let ty = ty.as_ref().expect("expected type annotation");
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Name { .. }));
assert_eq!(ty.span, crate::Span::new(F, 9, 15).unwrap());
assert_eq!(
ast.try_expr(value).unwrap().kind,
crate::ExprKind::String("hi".to_string())
);
}
_ => panic!("expected Const"),
}
}
#[test]
fn parse_let_with_type_and_binary_initializer() {
let (ast, diags) = parse_str("let z: number = 1 + 2;");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Let { ref ty, value, .. } => {
assert!(ty.is_some());
let (op, ..) = binary(ast.try_expr(value).unwrap());
assert_eq!(op, crate::BinOp::Add);
}
_ => panic!("expected Let"),
}
}
#[test]
fn void_type_annotation_is_accepted_syntactically() {
let (ast, diags) = parse_str("let v: void = null;");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Let { ref ty, .. } => {
let ty = ty.as_ref().unwrap();
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Name { .. }));
assert_eq!(ty.span, crate::Span::new(F, 7, 11).unwrap());
}
_ => panic!("expected Let"),
}
}
#[test]
fn const_missing_initializer_diagnoses() {
let (ast, diags) = parse_str("const x;");
assert_eq!(diags.len(), 1);
assert_eq!(
diags[0].message,
"`const` declaration requires an initializer"
);
assert!(ast.top_level.is_empty());
}
#[test]
fn parse_const_object_shorthand_pattern() {
let (ast, diags) = parse_str("const { a, b } = obj;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::ConstPattern {
binding:
crate::Binding::Object {
ref fields,
ref rest,
..
},
..
} => {
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].source.name, "a");
assert_eq!(fields[0].local.name, "a");
assert_eq!(fields[1].source.name, "b");
assert_eq!(fields[1].local.name, "b");
assert!(rest.is_none());
}
_ => panic!("expected ConstPattern Object, got {:?}", stmt.kind),
}
}
#[test]
fn parse_const_object_pattern_keyword_source_key() {
let (ast, diags) = parse_str("const { type: statusType, default: fallback } = obj;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::ConstPattern {
binding: crate::Binding::Object { ref fields, .. },
..
} = stmt.kind
else {
panic!("expected object const pattern");
};
let names: Vec<(&str, &str)> = fields
.iter()
.map(|f| (f.source.name.as_str(), f.local.name.as_str()))
.collect();
assert_eq!(names, vec![("type", "statusType"), ("default", "fallback")]);
}
#[test]
fn parse_const_object_pattern_keyword_shorthand_rejected() {
let (_ast, diags) = parse_str("const { default } = obj;");
assert!(
diags
.iter()
.any(|d| d.message == "expected `:` after keyword field name in object pattern"),
"expected keyword-shorthand diagnostic, got: {diags:?}"
);
}
#[test]
fn parse_const_object_renamed_pattern() {
let (ast, diags) = parse_str("const { a: x, b: y } = obj;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
match single_stmt(&ast).kind {
crate::StmtKind::ConstPattern {
binding: crate::Binding::Object { ref fields, .. },
..
} => {
assert_eq!(fields[0].source.name, "a");
assert_eq!(fields[0].local.name, "x");
assert_eq!(fields[1].source.name, "b");
assert_eq!(fields[1].local.name, "y");
}
ref other => panic!("expected ConstPattern Object, got {other:?}"),
}
}
#[test]
fn parse_const_object_with_rest() {
let (ast, diags) = parse_str("const { a, ...rest } = obj;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
match single_stmt(&ast).kind {
crate::StmtKind::ConstPattern {
binding:
crate::Binding::Object {
ref fields,
ref rest,
..
},
..
} => {
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].source.name, "a");
let r = rest.as_ref().expect("expected rest");
assert_eq!(r.name, "rest");
}
ref other => panic!("expected ConstPattern Object, got {other:?}"),
}
}
#[test]
fn parse_const_array_pattern() {
let (ast, diags) = parse_str("const [x, y] = arr;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
match single_stmt(&ast).kind {
crate::StmtKind::ConstPattern {
binding:
crate::Binding::Array {
ref elems,
ref rest,
..
},
..
} => {
assert_eq!(elems.len(), 2);
assert_eq!(elems[0].as_ref().unwrap().name, "x");
assert_eq!(elems[1].as_ref().unwrap().name, "y");
assert!(rest.is_none());
}
ref other => panic!("expected ConstPattern Array, got {other:?}"),
}
}
#[test]
fn parse_const_array_with_hole_and_rest() {
let (ast, diags) = parse_str("const [, x, ...rest] = arr;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
match single_stmt(&ast).kind {
crate::StmtKind::ConstPattern {
binding:
crate::Binding::Array {
ref elems,
ref rest,
..
},
..
} => {
assert_eq!(elems.len(), 2);
assert!(elems[0].is_none(), "first slot should be a hole");
assert_eq!(elems[1].as_ref().unwrap().name, "x");
assert_eq!(rest.as_ref().unwrap().name, "rest");
}
ref other => panic!("expected ConstPattern Array, got {other:?}"),
}
}
#[test]
fn parse_let_object_pattern() {
let (ast, diags) = parse_str("let { a, b } = obj;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
assert!(matches!(
single_stmt(&ast).kind,
crate::StmtKind::LetPattern {
binding: crate::Binding::Object { .. },
..
}
));
}
#[test]
fn parse_function_param_object_pattern() {
let src = "function f({ a, b }: T): void { return; }";
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected: {diags:?}");
match single_stmt(&ast).kind {
crate::StmtKind::Function { ref params, .. } => {
assert_eq!(params.len(), 1);
let p = ¶ms[0];
assert!(p.pattern.is_some(), "expected pattern param");
assert_eq!(p.name.name, "", "pattern placeholder uses empty name");
match p.pattern.as_ref().unwrap() {
crate::Binding::Object { fields, .. } => {
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].source.name, "a");
}
other => panic!("expected Object binding, got {other:?}"),
}
}
ref other => panic!("expected Function, got {other:?}"),
}
}
#[test]
fn parse_arrow_param_array_pattern() {
let src = "const g = ([x, y]: number[]): number => x + y;";
let (_ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected: {diags:?}");
}
#[test]
fn parse_destructure_nested_rejected() {
let (_ast, diags) = parse_str("const { a: { b } } = obj;");
assert!(
diags
.iter()
.any(|d| d.message.contains("nested destructuring")),
"expected nested-destructuring diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_destructure_array_nested_rejected() {
let (_ast, diags) = parse_str("const [[a]] = arr;");
assert!(
diags
.iter()
.any(|d| d.message.contains("nested destructuring")),
"expected nested-destructuring diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_destructure_pattern_defaults() {
let (ast, diags) = parse_str("const { a = 1, b: c = 2 } = obj;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::ConstPattern { ref binding, .. } = single_stmt(&ast).kind else {
panic!("expected pattern")
};
let crate::Binding::Object { fields, .. } = binding else {
panic!("expected object")
};
assert_eq!(fields.len(), 2);
assert!(fields.iter().all(|field| field.default.is_some()));
assert_eq!(fields[1].local.name, "c");
}
#[test]
fn parse_destructure_param_default() {
let (ast, diags) = parse_str("function f({ a = 1 }: T = obj): void { return; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Function { ref params, .. } = single_stmt(&ast).kind else {
panic!("expected function")
};
assert!(params[0].default.is_some());
assert!(params[0].pattern.is_some());
}
#[test]
fn parse_destructure_rest_not_last_rejected() {
let (_ast, diags) = parse_str("const { ...rest, a } = obj;");
assert!(
diags
.iter()
.any(|d| d.message.contains("rest element must be the last element")),
"expected rest-not-last diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_destructure_empty_object_rejected() {
let (_ast, diags) = parse_str("const { } = obj;");
assert!(
diags
.iter()
.any(|d| d.message.contains("empty object destructuring pattern")),
"expected empty-pattern diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_ternary_simple() {
let (ast, diags) = parse_str("const x: number = a ? 1 : 2;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const, got {:?}", stmt.kind);
};
assert!(matches!(
ast.try_expr(value).unwrap().kind,
crate::ExprKind::Ternary { .. }
));
}
#[test]
fn parse_ternary_nested_right_assoc() {
let (ast, diags) = parse_str("const x: number = a ? 1 : b ? 2 : 3;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::Ternary { else_, .. } = ast.try_expr(value).unwrap().kind else {
panic!("expected outer Ternary");
};
assert!(matches!(
ast.try_expr(else_).unwrap().kind,
crate::ExprKind::Ternary { .. }
));
}
#[test]
fn parse_ternary_missing_colon_diagnoses() {
let (_, diags) = parse_str("const x: number = a ? 1;");
assert!(
diags
.iter()
.any(|d| d.message.contains("expected `:` to complete ternary")),
"expected colon diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_nullish_coalesce_binop() {
let (ast, diags) = parse_str("const x: number = a ?? 1;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
match ast.try_expr(value).unwrap().kind {
crate::ExprKind::Binary { op, .. } => {
assert_eq!(op, crate::BinOp::NullishCoalesce);
}
ref other => panic!("expected Binary NullishCoalesce, got {other:?}"),
}
}
#[test]
fn parse_nullish_left_associative() {
let (ast, diags) = parse_str("const x: number = a ?? b ?? c;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::Binary { lhs, .. } = ast.try_expr(value).unwrap().kind else {
panic!("expected outer Binary");
};
match ast.try_expr(lhs).unwrap().kind {
crate::ExprKind::Binary { op, .. } => {
assert_eq!(op, crate::BinOp::NullishCoalesce);
}
ref other => panic!("expected inner Binary, got {other:?}"),
}
}
#[test]
fn parse_pow_is_right_associative() {
let (ast, diags) = parse_str("const x: number = 2 ** 3 ** 2;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::Binary { op, rhs, .. } = ast.try_expr(value).unwrap().kind else {
panic!("expected outer Binary");
};
assert_eq!(op, crate::BinOp::Pow);
match ast.try_expr(rhs).unwrap().kind {
crate::ExprKind::Binary { op: inner_op, .. } => {
assert_eq!(inner_op, crate::BinOp::Pow);
}
ref other => panic!("expected inner Pow Binary on RHS, got {other:?}"),
}
}
#[test]
fn parse_pow_higher_than_multiply() {
let (ast, diags) = parse_str("const x: number = 2 * 3 ** 2;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::Binary { op, rhs, .. } = ast.try_expr(value).unwrap().kind else {
panic!("expected outer Binary");
};
assert_eq!(op, crate::BinOp::Mul);
match ast.try_expr(rhs).unwrap().kind {
crate::ExprKind::Binary { op: inner_op, .. } => {
assert_eq!(inner_op, crate::BinOp::Pow);
}
ref other => panic!("expected Pow on the RHS of *, got {other:?}"),
}
}
#[test]
fn parse_compound_assign_ident() {
let (ast, diags) = parse_str("function main(): void { let x: number = 0; x += 5; }");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let func = ast.top_level.iter().find_map(|sid| {
if let crate::StmtKind::Function { body, .. } = &ast.try_stmt(*sid).unwrap().kind {
Some(*body)
} else {
None
}
});
let crate::StmtKind::Block(stmts) = &ast.try_stmt(func.expect("main")).unwrap().kind else {
panic!("expected function body to be a block");
};
let target = stmts
.iter()
.find_map(|sid| match &ast.try_stmt(*sid).unwrap().kind {
crate::StmtKind::CompoundAssign { target, op, .. } => {
Some((target.name.clone(), *op))
}
_ => None,
});
let (name, op) = target.expect("expected a CompoundAssign in the body");
assert_eq!(name, "x");
assert_eq!(op, crate::BinOp::Add);
}
#[test]
fn parse_nullish_mixed_with_or_rejected() {
let (_, diags) = parse_str("const x: number = a || b ?? c;");
assert!(
diags
.iter()
.any(|d| d.message.contains("mixing `??` with `||` / `&&`")),
"expected mixing diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_nullish_mixed_with_and_rejected() {
let (_, diags) = parse_str("const x: number = a && b ?? c;");
assert!(
diags
.iter()
.any(|d| d.message.contains("mixing `??` with `||` / `&&`")),
"expected mixing diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_nullish_followed_by_logical_rejected() {
for (source, help) in [
(
"const x: number = a ?? b || c;",
"`(a ?? b) || c` or `a ?? (b || c)`",
),
(
"const x: number = a ?? b && c;",
"`(a ?? b) && c` or `a ?? (b && c)`",
),
(
"const x: number = a ?? b ?? c || d;",
"`(a ?? b) || c` or `a ?? (b || c)`",
),
(
"const x: number = a || b ?? c || d;",
"`(a || b) ?? c` or `a || (b ?? c)`",
),
] {
let (_, diags) = parse_str(source);
assert_eq!(diags.len(), 1, "diags for {source:?}: {diags:?}");
assert!(
diags[0].message.contains("mixing `??` with `||` / `&&`"),
"message for {source:?}: {diags:?}",
);
assert!(
diags[0].help.iter().any(|h| h.contains(help)),
"help for {source:?}: {diags:?}",
);
}
}
#[test]
fn parse_nullish_operand_keeps_tighter_operators() {
let (ast, diags) = parse_str("const x: boolean = a ?? b == c;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::Binary { op, rhs, .. } = ast.try_expr(value).unwrap().kind else {
panic!("expected outer Binary");
};
assert_eq!(op, crate::BinOp::NullishCoalesce);
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Binary {
op: crate::BinOp::Eq,
..
}
));
}
#[test]
fn parse_nullish_with_parens_ok() {
let (_, diags) = parse_str("const x: number = (a || b) ?? c;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
}
#[test]
fn parse_optional_field_chain() {
let (ast, diags) = parse_str("const x: number = a?.b;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
match ast.try_expr(value).unwrap().kind {
crate::ExprKind::OptionalChain { ref parts, .. } => {
assert_eq!(parts.len(), 1);
match &parts[0] {
crate::ChainPart::Field { name, optional, .. } => {
assert_eq!(name.name, "b");
assert!(*optional);
}
other => panic!("expected Field, got {other:?}"),
}
}
ref other => panic!("expected OptionalChain, got {other:?}"),
}
}
#[test]
fn parse_optional_chain_continues_after_first_dot() {
let (ast, diags) = parse_str("const x: number = a?.b.c;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
match ast.try_expr(value).unwrap().kind {
crate::ExprKind::OptionalChain { ref parts, .. } => {
assert_eq!(parts.len(), 2);
if let crate::ChainPart::Field { optional, .. } = &parts[0] {
assert!(*optional);
}
if let crate::ChainPart::Field { optional, .. } = &parts[1] {
assert!(!*optional);
}
}
ref other => panic!("expected OptionalChain, got {other:?}"),
}
}
#[test]
fn parse_optional_chain_multiple_short_circuits() {
let (ast, diags) = parse_str("const x: number = a?.b?.c;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::OptionalChain { parts, .. } = &ast.try_expr(value).unwrap().kind
else {
panic!("expected OptionalChain");
};
assert_eq!(parts.len(), 2);
for p in parts {
if let crate::ChainPart::Field { optional, .. } = p {
assert!(*optional, "both parts should be optional");
}
}
}
#[test]
fn parse_optional_call_and_index() {
let (_, diags) = parse_str("const x: number = f?.();");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let (_, diags) = parse_str("const y: number = arr?.[0];");
assert!(diags.is_empty(), "unexpected: {diags:?}");
}
#[test]
fn parse_optional_chain_terminates_at_questionmark_for_ternary() {
let (ast, diags) = parse_str("const x: number = a?.b ? c : d;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::Ternary { cond, .. } = ast.try_expr(value).unwrap().kind else {
panic!(
"expected top-level Ternary, got {:?}",
ast.try_expr(value).unwrap().kind
);
};
assert!(matches!(
ast.try_expr(cond).unwrap().kind,
crate::ExprKind::OptionalChain { .. }
));
}
#[test]
fn parse_nullish_below_ternary() {
let (ast, diags) = parse_str("const x: number = a ?? b ? c : d;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Const { value, .. } = stmt.kind else {
panic!("expected Const");
};
let crate::ExprKind::Ternary { cond, .. } = ast.try_expr(value).unwrap().kind else {
panic!("expected top-level Ternary");
};
match ast.try_expr(cond).unwrap().kind {
crate::ExprKind::Binary { op, .. } => {
assert_eq!(op, crate::BinOp::NullishCoalesce);
}
ref other => panic!("expected Binary ??, got {other:?}"),
}
}
#[test]
fn let_missing_initializer_diagnoses() {
let (ast, diags) = parse_str("let x;");
assert_eq!(diags.len(), 1);
assert_eq!(
diags[0].message,
"`let` declaration requires an initializer"
);
assert!(ast.top_level.is_empty());
}
#[test]
fn missing_identifier_after_let_diagnoses() {
let (ast, diags) = parse_str("let = 1;");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected identifier after `let`/`const`");
assert!(ast.top_level.is_empty());
}
#[test]
fn missing_type_after_colon_diagnoses() {
let (ast, diags) = parse_str("let x: = 1;");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected type");
assert!(ast.top_level.is_empty());
}
#[test]
fn missing_semicolon_after_declaration_diagnoses() {
let (ast, diags) = parse_str("let x = 1 y;");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected `;` after declaration");
assert!(ast.top_level.is_empty());
}
#[test]
fn expression_statements_still_parse() {
let (ast, diags) = parse_str("42;");
assert!(diags.is_empty());
assert_eq!(ast.top_level.len(), 1);
}
#[test]
fn snapshot_mixed_declarations() {
let (ast, diags) =
parse_str(r#"let x = 1; const y: string = "hi"; let z: number = 1 + 2;"#);
assert!(diags.is_empty());
assert_eq!(ast.top_level.len(), 3);
insta::assert_debug_snapshot!(ast);
}
#[test]
fn parse_function_typical() {
let (ast, diags) = parse_str("function f(a: number, b: string): boolean { }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function {
ref name,
ref generics,
ref params,
ref return_type,
body,
..
} => {
assert_eq!(name.name, "f");
assert_eq!(name.span, crate::Span::new(F, 9, 10).unwrap());
assert!(generics.is_empty());
assert_eq!(params.len(), 2);
assert_eq!(params[0].name.name, "a");
assert_eq!(params[0].name.span, crate::Span::new(F, 11, 12).unwrap());
let p0_ty = params[0]
.ty
.as_ref()
.expect("function-decl param requires annotation");
assert!(matches!(p0_ty.kind, crate::TypeAnnotationKind::Name { .. }));
assert_eq!(p0_ty.span, crate::Span::new(F, 14, 20).unwrap()); assert_eq!(params[1].name.name, "b");
assert_eq!(params[1].name.span, crate::Span::new(F, 22, 23).unwrap());
let p1_ty = params[1]
.ty
.as_ref()
.expect("function-decl param requires annotation");
assert_eq!(p1_ty.span, crate::Span::new(F, 25, 31).unwrap()); let return_type = return_type.as_ref().expect("plain return type");
assert!(matches!(
return_type.kind,
crate::TypeAnnotationKind::Name { .. }
));
assert_eq!(return_type.span, crate::Span::new(F, 34, 41).unwrap()); let block = ast.try_stmt(body).unwrap();
assert!(matches!(block.kind, crate::StmtKind::Block(ref v) if v.is_empty()));
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_function_with_doc_comment() {
let (ast, diags) = parse_str(
"/**\n * Sum two numbers.\n * @param a First.\n * @param b Second.\n */\nfunction add(a: number, b: number): number { return a + b; }",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match &stmt.kind {
crate::StmtKind::Function { doc, .. } => {
let d = doc.as_ref().expect("doc attached");
assert_eq!(d.summary, "Sum two numbers.");
assert_eq!(d.params.len(), 2);
assert_eq!(d.params[0].name, "a");
assert_eq!(d.params[1].name, "b");
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_interface_member_docs() {
let (ast, diags) = parse_str(
"interface Foo {\n /** First method. */ first(): void;\n /** A field. */ field: number;\n}",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match &stmt.kind {
crate::StmtKind::InterfaceDecl { members, .. } => {
assert_eq!(members.len(), 2);
match &members[0] {
crate::InterfaceMember::Method { doc, .. } => {
assert_eq!(doc.as_ref().unwrap().summary, "First method.");
}
_ => panic!("expected Method first"),
}
match &members[1] {
crate::InterfaceMember::Property { doc, .. } => {
assert_eq!(doc.as_ref().unwrap().summary, "A field.");
}
_ => panic!("expected Property second"),
}
}
_ => panic!("expected InterfaceDecl"),
}
}
#[test]
fn parse_numeric_enum_all_implicit() {
let (ast, diags) = parse_str("enum Direction { Up, Down, Left, Right }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match &stmt.kind {
crate::StmtKind::EnumDecl { name, members, doc } => {
assert_eq!(name.name, "Direction");
assert!(doc.is_none());
assert_eq!(members.len(), 4);
for (m, expected) in members.iter().zip(["Up", "Down", "Left", "Right"]) {
assert_eq!(m.name.name, expected);
assert!(m.value.is_none(), "expected no initializer on {expected}");
}
}
_ => panic!("expected EnumDecl"),
}
}
#[test]
fn parse_string_enum_all_explicit() {
let (ast, diags) =
parse_str("enum Status { Active = \"active\", Inactive = \"inactive\" }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match &stmt.kind {
crate::StmtKind::EnumDecl { name, members, .. } => {
assert_eq!(name.name, "Status");
assert_eq!(members.len(), 2);
match &members[0].value {
Some(crate::EnumInitializer::String { value, .. }) => {
assert_eq!(value, "active");
}
other => panic!("expected String init, got {other:?}"),
}
match &members[1].value {
Some(crate::EnumInitializer::String { value, .. }) => {
assert_eq!(value, "inactive");
}
other => panic!("expected String init, got {other:?}"),
}
}
_ => panic!("expected EnumDecl"),
}
}
#[test]
fn parse_numeric_enum_with_negative_init() {
let (ast, diags) = parse_str("enum D { Up = 1, Down = -1 }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match &stmt.kind {
crate::StmtKind::EnumDecl { members, .. } => {
assert_eq!(members.len(), 2);
match &members[0].value {
Some(crate::EnumInitializer::Number { value, .. }) => {
assert_eq!(*value, 1.0);
}
other => panic!("expected Number init, got {other:?}"),
}
match &members[1].value {
Some(crate::EnumInitializer::Number { value, .. }) => {
assert_eq!(*value, -1.0);
}
other => panic!("expected Number init, got {other:?}"),
}
}
_ => panic!("expected EnumDecl"),
}
}
#[test]
fn parse_enum_mixed_kinds_is_parse_error() {
let (_ast, diags) = parse_str("enum Mix { A = 1, B = \"two\" }");
assert!(
diags
.iter()
.any(|d| d.message.contains("mixed numeric and string")),
"expected mixed-kind diagnostic, got: {diags:?}"
);
}
#[test]
fn parse_enum_member_docs() {
let (ast, diags) = parse_str(
"enum D {\n /** The first one. */ A = 1,\n /** The second one. */ B = 2,\n}",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match &stmt.kind {
crate::StmtKind::EnumDecl { members, .. } => {
assert_eq!(members.len(), 2);
assert_eq!(
members[0].doc.as_ref().expect("first doc").summary,
"The first one."
);
assert_eq!(
members[1].doc.as_ref().expect("second doc").summary,
"The second one."
);
}
_ => panic!("expected EnumDecl"),
}
}
#[test]
fn parse_enum_trailing_comma_ok() {
let (ast, diags) = parse_str("enum D { A, B, }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match &stmt.kind {
crate::StmtKind::EnumDecl { members, .. } => assert_eq!(members.len(), 2),
_ => panic!("expected EnumDecl"),
}
}
#[test]
fn parse_function_empty_params() {
let (ast, diags) = parse_str("function noop(): void { }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function { ref params, .. } => assert!(params.is_empty()),
_ => panic!("expected Function"),
}
}
#[test]
fn parse_generic_function_single_param() {
let (ast, diags) = parse_str("function identity<T>(x: T): T { return x; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function {
ref name,
ref generics,
ref params,
..
} => {
assert_eq!(name.name, "identity");
assert_eq!(generics.len(), 1);
assert_eq!(generics[0].name, "T");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name.name, "x");
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_generic_function_multiple_params() {
let (ast, diags) = parse_str("function pair<T, U>(a: T, b: U): T { return a; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function { ref generics, .. } => {
assert_eq!(generics.len(), 2);
assert_eq!(generics[0].name, "T");
assert_eq!(generics[1].name, "U");
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_non_generic_function_has_empty_generics() {
let (ast, diags) = parse_str("function noop(): void { }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function { ref generics, .. } => assert!(generics.is_empty()),
_ => panic!("expected Function"),
}
}
#[test]
fn parse_empty_generic_list_diagnoses() {
let (_ast, diags) = parse_str("function f<>(): void { }");
assert!(
diags
.iter()
.any(|d| d.message.contains("empty generic parameter list")),
"expected empty-generic-list diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_trailing_comma_in_generic_list_ok() {
let (ast, diags) = parse_str("function f<T,>(): void { }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
match single_stmt(&ast).kind {
crate::StmtKind::Function { ref generics, .. } => assert_eq!(generics.len(), 1),
_ => panic!("expected Function"),
}
}
#[test]
fn parse_generic_call_single_type_arg() {
let (ast, diags) = parse_str("identity<number>(42);");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Expr(call_id) = stmt.kind else {
panic!("expected Expr stmt");
};
match &ast.try_expr(call_id).unwrap().kind {
crate::ExprKind::Call {
type_args, args, ..
} => {
let ta = type_args.as_ref().expect("type_args set for generic call");
assert_eq!(ta.len(), 1);
assert_eq!(args.len(), 1);
}
other => panic!("expected Call, got {other:?}"),
}
}
#[test]
fn parse_generic_call_trailing_comma_ok() {
let (ast, diags) = parse_str("identity<number,>(42);");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Expr(call_id) = stmt.kind else {
panic!("expected Expr stmt");
};
match &ast.try_expr(call_id).unwrap().kind {
crate::ExprKind::Call {
type_args, args, ..
} => {
assert_eq!(type_args.as_ref().expect("type_args set").len(), 1);
assert_eq!(args.len(), 1);
}
other => panic!("expected Call, got {other:?}"),
}
}
#[test]
fn parse_generic_call_multi_type_args() {
let (ast, diags) = parse_str("pair<number, string>(1, \"x\");");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Expr(call_id) = stmt.kind else {
panic!("expected Expr stmt");
};
match &ast.try_expr(call_id).unwrap().kind {
crate::ExprKind::Call { type_args, .. } => {
let ta = type_args.as_ref().expect("type_args set for generic call");
assert_eq!(ta.len(), 2);
}
other => panic!("expected Call, got {other:?}"),
}
}
#[test]
fn parse_comparison_chain_unaffected_by_generic_lookahead() {
let (ast, diags) = parse_str("let z = a < b > c;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert!(
!ast_contains_call_with_type_args(&ast),
"should not have parsed any generic call",
);
}
#[test]
fn parse_lt_with_paren_rhs_is_comparison_not_generic() {
let (ast, diags) = parse_str("let z = a < (b);");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert!(!ast_contains_call_with_type_args(&ast));
}
#[test]
fn parse_generic_call_no_args() {
let (ast, diags) = parse_str("none<number>();");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Expr(call_id) = stmt.kind else {
panic!("expected Expr stmt");
};
match &ast.try_expr(call_id).unwrap().kind {
crate::ExprKind::Call {
type_args, args, ..
} => {
assert_eq!(type_args.as_ref().unwrap().len(), 1);
assert!(args.is_empty());
}
other => panic!("expected Call, got {other:?}"),
}
}
fn ast_contains_call_with_type_args(ast: &crate::Ast) -> bool {
for id in 0..ast.top_level.len() {
let stmt = ast.try_stmt(ast.top_level[id]).unwrap();
if walk_stmt_for_typed_call(ast, stmt) {
return true;
}
}
false
}
fn walk_stmt_for_typed_call(ast: &crate::Ast, stmt: &crate::Stmt) -> bool {
match &stmt.kind {
crate::StmtKind::Let { value, .. } | crate::StmtKind::Const { value, .. } => {
walk_expr_for_typed_call(ast, *value)
}
crate::StmtKind::Expr(e) => walk_expr_for_typed_call(ast, *e),
crate::StmtKind::Block(stmts) => stmts
.iter()
.any(|sid| walk_stmt_for_typed_call(ast, ast.try_stmt(*sid).unwrap())),
_ => false,
}
}
fn walk_expr_for_typed_call(ast: &crate::Ast, id: crate::ExprId) -> bool {
match &ast.try_expr(id).unwrap().kind {
crate::ExprKind::Call {
type_args: Some(_), ..
} => true,
crate::ExprKind::Call { callee, args, .. } => {
walk_expr_for_typed_call(ast, *callee)
|| args.iter().any(|a| walk_expr_for_typed_call(ast, *a))
}
crate::ExprKind::Binary { lhs, rhs, .. } => {
walk_expr_for_typed_call(ast, *lhs) || walk_expr_for_typed_call(ast, *rhs)
}
crate::ExprKind::Unary { operand, .. } => walk_expr_for_typed_call(ast, *operand),
crate::ExprKind::Paren(e) => walk_expr_for_typed_call(ast, *e),
crate::ExprKind::FieldAccess { receiver, .. } => {
walk_expr_for_typed_call(ast, *receiver)
}
crate::ExprKind::IndexAccess { receiver, index } => {
walk_expr_for_typed_call(ast, *receiver) || walk_expr_for_typed_call(ast, *index)
}
_ => false,
}
}
#[test]
fn parse_function_body_with_statements() {
let (ast, diags) = parse_str("function g(x: number): number { let y = x; y; }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function { body, .. } => {
let block = ast.try_stmt(body).unwrap();
match block.kind {
crate::StmtKind::Block(ref v) => assert_eq!(v.len(), 2),
_ => panic!("expected Block"),
}
}
_ => panic!("expected Function"),
}
}
#[test]
fn function_missing_return_type_diagnoses() {
let (ast, diags) = parse_str("function f() { }");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected `:` and return type");
assert!(ast.top_level.is_empty());
}
#[test]
fn function_missing_param_type_diagnoses() {
let (ast, diags) = parse_str("function f(a): void { }");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "parameter requires a type annotation");
assert!(ast.top_level.is_empty());
}
#[test]
fn function_missing_name_diagnoses() {
let (ast, diags) = parse_str("function (x: number): void { }");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected function name");
assert!(ast.top_level.is_empty());
}
#[test]
fn function_trailing_param_comma_ok() {
let (ast, diags) = parse_str("function f(a: number,): void { }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
match single_stmt(&ast).kind {
crate::StmtKind::Function { ref params, .. } => assert_eq!(params.len(), 1),
_ => panic!("expected Function"),
}
}
#[test]
fn function_missing_open_brace_diagnoses() {
let (ast, diags) = parse_str("function f(): void");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "expected `{`");
assert!(ast.top_level.is_empty());
}
#[test]
fn snapshot_function_declaration() {
let (ast, diags) =
parse_str("function add(a: number, b: number): number { let sum = a + b; sum; }");
assert!(diags.is_empty());
insta::assert_debug_snapshot!(ast);
}
#[test]
fn rest_param_basic_parses() {
let (_, diags) = parse_str("function sum(...nums: number[]): number { return 0; }");
assert!(diags.is_empty(), "expected clean parse, got {diags:?}");
}
#[test]
fn rest_param_with_fixed_prefix_parses() {
let (_, diags) =
parse_str("function tag(label: string, ...vals: number[]): string { return label; }");
assert!(diags.is_empty(), "expected clean parse, got {diags:?}");
}
#[test]
fn rest_param_in_arrow_parses() {
let (_, diags) = parse_str("const f = (...n: number[]) => n.length;");
assert!(diags.is_empty(), "expected clean parse, got {diags:?}");
}
#[test]
fn rest_param_in_function_type_annotation_parses() {
let (_, diags) =
parse_str("let f: (a: number, ...rest: string[]) => void = (a, ...r) => {};");
assert!(diags.is_empty(), "expected clean parse, got {diags:?}");
}
#[test]
fn rest_param_not_last_rejected() {
let (_, diags) = parse_str("function f(...xs: number[], y: number): void {}");
assert!(
diags.iter().any(|d| d
.message
.contains("rest parameter must be the last parameter")),
"expected 'must be last' diagnostic, got {diags:?}",
);
}
#[test]
fn rest_param_with_default_rejected() {
let (_, diags) = parse_str("function f(...xs: number[] = []): void {}");
assert!(
diags.iter().any(|d| d
.message
.contains("rest parameter cannot have a default value")),
"expected default-rejection diagnostic, got {diags:?}",
);
}
#[test]
fn rest_param_without_annotation_rejected() {
let (_, diags) = parse_str("function f(...xs): void {}");
assert!(
diags.iter().any(|d| d
.message
.contains("rest parameter requires a type annotation")),
"expected missing-annotation diagnostic, got {diags:?}",
);
}
#[test]
fn rest_param_destructured_rejected() {
let (_, diags) = parse_str("function f(...{a, b}): void {}");
assert!(
diags
.iter()
.any(|d| d.message.contains("rest parameter cannot be destructured")),
"expected destructured-rejection diagnostic, got {diags:?}",
);
}
#[test]
fn rest_param_in_function_type_with_non_array_rejected() {
let (_, diags) = parse_str("let f: (...xs: number) => void = () => {};");
assert!(
diags
.iter()
.any(|d| d.message.contains("rest parameter type must be an array")),
"expected non-array-type diagnostic, got {diags:?}",
);
}
fn call(e: &crate::Expr) -> (crate::ExprId, &[crate::ExprId]) {
match e.kind {
crate::ExprKind::Call {
callee, ref args, ..
} => (callee, args.as_slice()),
_ => panic!("expected Call, got {:?}", e.kind),
}
}
#[test]
fn parse_call_no_args() {
let (ast, diags) = parse_str("f();");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (callee, args) = call(outer);
assert!(matches!(
ast.try_expr(callee).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(args.is_empty());
assert_eq!(outer.span, crate::Span::new(F, 0, 3).unwrap());
}
#[test]
fn parse_call_one_arg() {
let (ast, diags) = parse_str("f(1);");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (callee, args) = call(outer);
assert!(matches!(
ast.try_expr(callee).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(args.len(), 1);
assert_eq!(
ast.try_expr(args[0]).unwrap().kind,
crate::ExprKind::Number(1.0)
);
}
#[test]
fn parse_call_three_args() {
let (ast, diags) = parse_str("f(1, 2, 3);");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (_callee, args) = call(outer);
assert_eq!(args.len(), 3);
assert_eq!(
ast.try_expr(args[0]).unwrap().kind,
crate::ExprKind::Number(1.0)
);
assert_eq!(
ast.try_expr(args[1]).unwrap().kind,
crate::ExprKind::Number(2.0)
);
assert_eq!(
ast.try_expr(args[2]).unwrap().kind,
crate::ExprKind::Number(3.0)
);
}
#[test]
fn parse_call_nested() {
let (ast, diags) = parse_str("f(g(x));");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (_callee, args) = call(outer);
assert_eq!(args.len(), 1);
let (_inner_callee, inner_args) = call(ast.try_expr(args[0]).unwrap());
assert_eq!(inner_args.len(), 1);
assert!(matches!(
ast.try_expr(inner_args[0]).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn parse_curried_call() {
let (ast, diags) = parse_str("f()(x);");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (callee, args) = call(outer);
assert_eq!(args.len(), 1);
let (_inner_callee, inner_args) = call(ast.try_expr(callee).unwrap());
assert!(inner_args.is_empty());
}
#[test]
fn unary_binds_looser_than_call() {
let (ast, diags) = parse_str("-f();");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (op, operand) = unary(outer);
assert_eq!(op, crate::UnOp::Neg);
let (_callee, args) = call(ast.try_expr(operand).unwrap());
assert!(args.is_empty());
}
#[test]
fn call_inside_binary() {
let (ast, diags) = parse_str("f() + 1;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (op, lhs, rhs) = binary(outer);
assert_eq!(op, crate::BinOp::Add);
let (_callee, args) = call(ast.try_expr(lhs).unwrap());
assert!(args.is_empty());
assert_eq!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Number(1.0)
);
}
#[test]
fn call_with_complex_args() {
let (ast, diags) = parse_str("f(1 + 2, g(x));");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (_callee, args) = call(outer);
assert_eq!(args.len(), 2);
let (op, ..) = binary(ast.try_expr(args[0]).unwrap());
assert_eq!(op, crate::BinOp::Add);
let (_, inner_args) = call(ast.try_expr(args[1]).unwrap());
assert_eq!(inner_args.len(), 1);
}
#[test]
fn call_trailing_comma_ok() {
let (ast, diags) = parse_str("f(1, 2,);");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let (_callee, args) = call(expr_of_single_stmt(&ast));
assert_eq!(args.len(), 2);
}
#[test]
fn unclosed_call_diagnoses() {
let (ast, diags) = parse_str("f(1, 2");
assert!(!diags.is_empty());
assert!(diags[0].message == "expected `,` or `)`" || diags[0].message == "expected `)`");
assert!(ast.top_level.is_empty());
}
#[test]
fn call_expression_statement() {
let (ast, diags) = parse_str(r#"greet("world");"#);
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (callee, args) = call(outer);
assert!(matches!(
ast.try_expr(callee).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(args.len(), 1);
assert_eq!(
ast.try_expr(args[0]).unwrap().kind,
crate::ExprKind::String("world".to_string())
);
}
#[test]
fn snapshot_multi_call_program() {
let (ast, diags) = parse_str("f(); g(1); h(f(2), 3);");
assert!(diags.is_empty());
assert_eq!(ast.top_level.len(), 3);
insta::assert_debug_snapshot!(ast);
}
#[test]
fn parse_if_simple() {
let (ast, diags) = parse_str("if (a) { }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::If {
condition,
then_block,
else_block,
} => {
assert!(matches!(
ast.try_expr(condition).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(
ast.try_stmt(then_block).unwrap().kind,
crate::StmtKind::Block(ref v) if v.is_empty()
));
assert!(else_block.is_none());
}
_ => panic!("expected If"),
}
}
#[test]
fn parse_if_else() {
let (ast, diags) = parse_str("if (a) { } else { }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::If { else_block, .. } => {
let else_id = else_block.expect("expected else block");
assert!(matches!(
ast.try_stmt(else_id).unwrap().kind,
crate::StmtKind::Block(ref v) if v.is_empty()
));
}
_ => panic!("expected If"),
}
}
#[test]
fn parse_else_if_chain() {
let (ast, diags) = parse_str("if (a) { } else if (b) { } else { }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
let crate::StmtKind::If {
else_block: Some(inner_id),
..
} = stmt.kind
else {
panic!("expected outer If with else_block");
};
let inner = ast.try_stmt(inner_id).unwrap();
let crate::StmtKind::If {
else_block: Some(tail_id),
..
} = inner.kind
else {
panic!("expected inner If for `else if`");
};
assert!(matches!(
ast.try_stmt(tail_id).unwrap().kind,
crate::StmtKind::Block(_)
));
}
#[test]
fn parse_if_with_body_statements() {
let (ast, diags) = parse_str("if (a) { let x = 1; }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::If { then_block, .. } => {
let crate::StmtKind::Block(ref body) = ast.try_stmt(then_block).unwrap().kind
else {
panic!("expected Block")
};
assert_eq!(body.len(), 1);
}
_ => panic!("expected If"),
}
}
#[test]
fn parse_if_braceless_body() {
let (ast, diags) = parse_str("if (a) return b;");
assert!(diags.is_empty(), "{diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::If {
then_block,
else_block,
..
} = stmt.kind
else {
panic!("expected If");
};
let crate::StmtKind::Block(ref body) = ast.try_stmt(then_block).unwrap().kind else {
panic!("braceless body should wrap in a Block");
};
assert_eq!(body.len(), 1);
assert!(matches!(
ast.try_stmt(body[0]).unwrap().kind,
crate::StmtKind::Return(_)
));
assert!(else_block.is_none());
}
#[test]
fn parse_braceless_dangling_else_binds_nearest_if() {
let (ast, diags) = parse_str("if (a) if (b) x(); else y();");
assert!(diags.is_empty(), "{diags:?}");
let crate::StmtKind::If {
then_block,
else_block: outer_else,
..
} = single_stmt(&ast).kind
else {
panic!("expected outer If");
};
assert!(outer_else.is_none(), "else must bind to the inner if");
let crate::StmtKind::Block(ref body) = ast.try_stmt(then_block).unwrap().kind else {
panic!("expected wrapped Block");
};
let crate::StmtKind::If {
else_block: Some(_),
..
} = ast.try_stmt(body[0]).unwrap().kind
else {
panic!("inner If should own the else");
};
}
#[test]
fn parse_while_braceless_body() {
let (ast, diags) = parse_str("while (a) x();");
assert!(diags.is_empty(), "{diags:?}");
let crate::StmtKind::While { body, .. } = single_stmt(&ast).kind else {
panic!("expected While");
};
assert!(matches!(
ast.try_stmt(body).unwrap().kind,
crate::StmtKind::Block(ref v) if v.len() == 1
));
}
#[test]
fn parse_loop_with_empty_body() {
for src in [
"while (a());",
"for (const x of xs);",
"for (let i = 0; i < 3; i++);",
"do ; while (a());",
] {
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "{src}: {diags:?}");
let body = match single_stmt(&ast).kind {
crate::StmtKind::While { body, .. }
| crate::StmtKind::DoWhile { body, .. }
| crate::StmtKind::For { body, .. }
| crate::StmtKind::ForOf { body, .. } => body,
ref other => panic!("{src}: expected a loop, got {other:?}"),
};
assert!(
matches!(ast.try_stmt(body).unwrap().kind, crate::StmtKind::Block(ref v) if v.is_empty()),
"{src}"
);
}
}
#[test]
fn parse_if_with_empty_body_is_an_error() {
let (_ast, diags) = parse_str("if (a);");
assert_eq!(diags[0].message, "`if` has an empty body");
}
#[test]
fn parse_empty_branch_body_reports_once() {
let (_ast, diags) = parse_str("if (a) ; else b();");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].message, "`if` has an empty body");
let (_ast, diags) = parse_str("if (a) b(); else ;");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].message, "`else` has an empty body");
let (_ast, diags) = parse_str("if (a) ; else if (b) ; else ;");
let messages: Vec<_> = diags.iter().map(|d| d.message.as_str()).collect();
assert_eq!(
messages,
[
"`if` has an empty body",
"`if` has an empty body",
"`else` has an empty body"
]
);
}
#[test]
fn parse_for_of_braceless_body() {
let (ast, diags) = parse_str("for (const x of xs) f(x);");
assert!(diags.is_empty(), "{diags:?}");
let crate::StmtKind::ForOf { body, .. } = single_stmt(&ast).kind else {
panic!("expected ForOf");
};
assert!(matches!(
ast.try_stmt(body).unwrap().kind,
crate::StmtKind::Block(ref v) if v.len() == 1
));
}
#[test]
fn parse_nested_if() {
let (ast, diags) = parse_str("if (a) { if (b) { } }");
assert!(diags.is_empty());
let outer = single_stmt(&ast);
let crate::StmtKind::If { then_block, .. } = outer.kind else {
panic!("expected outer If");
};
let crate::StmtKind::Block(ref body) = ast.try_stmt(then_block).unwrap().kind else {
panic!("expected Block")
};
assert_eq!(body.len(), 1);
assert!(matches!(
ast.try_stmt(body[0]).unwrap().kind,
crate::StmtKind::If { .. }
));
}
#[test]
fn if_missing_open_paren_diagnoses() {
let (ast, diags) = parse_str("if a { }");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected `(`");
assert!(ast.top_level.is_empty());
}
#[test]
fn if_missing_close_paren_diagnoses() {
let (ast, diags) = parse_str("if (a { }");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected `)`");
assert!(ast.top_level.is_empty());
}
#[test]
fn if_braceless_body_parses() {
let (ast, diags) = parse_str("if (a) x;");
assert!(diags.is_empty(), "{diags:?}");
let crate::StmtKind::If { then_block, .. } = single_stmt(&ast).kind else {
panic!("expected If");
};
assert!(matches!(
ast.try_stmt(then_block).unwrap().kind,
crate::StmtKind::Block(ref v) if v.len() == 1
));
}
#[test]
fn snapshot_else_if_chain() {
let (ast, diags) = parse_str("if (a) { } else if (b) { } else { }");
assert!(diags.is_empty());
insta::assert_debug_snapshot!(ast);
}
#[test]
fn parse_while_simple() {
let (ast, diags) = parse_str("while (a) { }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::While { condition, body } => {
assert!(matches!(
ast.try_expr(condition).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert!(matches!(
ast.try_stmt(body).unwrap().kind,
crate::StmtKind::Block(ref v) if v.is_empty()
));
}
_ => panic!("expected While"),
}
}
#[test]
fn parse_while_with_body() {
let (ast, diags) = parse_str("while (a) { x; y; }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
let crate::StmtKind::While { body, .. } = stmt.kind else {
panic!("expected While");
};
let crate::StmtKind::Block(ref stmts) = ast.try_stmt(body).unwrap().kind else {
panic!("expected Block")
};
assert_eq!(stmts.len(), 2);
}
#[test]
fn while_missing_paren_diagnoses() {
let (ast, diags) = parse_str("while a { }");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected `(`");
assert!(ast.top_level.is_empty());
}
#[test]
fn parse_for_all_slots() {
let (ast, diags) = parse_str("for (let i = 0; i < 10; i = i + 1) { x; }");
assert!(diags.is_empty(), "{diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::For {
init,
condition,
update,
body,
} = stmt.kind
else {
panic!("expected For");
};
assert!(init.is_some(), "init missing");
assert!(condition.is_some(), "condition missing");
assert!(update.is_some(), "update missing");
assert!(matches!(
ast.try_stmt(body).unwrap().kind,
crate::StmtKind::Block(_)
));
}
#[test]
fn parse_for_empty_slots() {
let (ast, diags) = parse_str("for (;;) { break; }");
assert!(diags.is_empty(), "{diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::For {
init,
condition,
update,
..
} = stmt.kind
else {
panic!("expected For");
};
assert!(init.is_none() && condition.is_none() && update.is_none());
}
#[test]
fn parse_for_of_const_binding() {
let (ast, diags) = parse_str("for (const x of arr) { y; }");
assert!(diags.is_empty(), "{diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::ForOf {
binding_kind,
ref name,
ref ty,
..
} = stmt.kind
else {
panic!("expected ForOf");
};
assert!(matches!(binding_kind, crate::BindingKind::Const));
assert_eq!(name.name, "x");
assert!(ty.is_none());
}
#[test]
fn parse_for_of_let_with_annotation() {
let (ast, diags) = parse_str("for (let x: number of arr) { y; }");
assert!(diags.is_empty(), "{diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::ForOf {
binding_kind,
ref ty,
..
} = stmt.kind
else {
panic!("expected ForOf");
};
assert!(matches!(binding_kind, crate::BindingKind::Let));
assert!(ty.is_some());
}
#[test]
fn parse_do_while_simple() {
let (ast, diags) = parse_str("do { x; } while (a);");
assert!(diags.is_empty(), "{diags:?}");
let stmt = single_stmt(&ast);
assert!(matches!(stmt.kind, crate::StmtKind::DoWhile { .. }));
}
#[test]
fn parse_break_and_continue() {
let (ast, diags) = parse_str("while (a) { break; continue; }");
assert!(diags.is_empty(), "{diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::While { body, .. } = stmt.kind else {
panic!("expected While");
};
let crate::StmtKind::Block(ref stmts) = ast.try_stmt(body).unwrap().kind else {
panic!("expected Block");
};
assert!(matches!(
ast.try_stmt(stmts[0]).unwrap().kind,
crate::StmtKind::Break
));
assert!(matches!(
ast.try_stmt(stmts[1]).unwrap().kind,
crate::StmtKind::Continue
));
}
#[test]
fn parse_bare_return_in_function() {
let (ast, diags) = parse_str("function f(): void { return; }");
assert!(diags.is_empty());
let f = single_stmt(&ast);
let crate::StmtKind::Function { body, .. } = f.kind else {
panic!("expected Function");
};
let crate::StmtKind::Block(ref stmts) = ast.try_stmt(body).unwrap().kind else {
panic!("expected Block")
};
assert_eq!(stmts.len(), 1);
match ast.try_stmt(stmts[0]).unwrap().kind {
crate::StmtKind::Return(None) => {}
_ => panic!("expected Return(None)"),
}
}
#[test]
fn parse_return_with_value() {
let (ast, diags) = parse_str("function f(): number { return 1; }");
assert!(diags.is_empty());
let f = single_stmt(&ast);
let crate::StmtKind::Function { body, .. } = f.kind else {
panic!("expected Function");
};
let crate::StmtKind::Block(ref stmts) = ast.try_stmt(body).unwrap().kind else {
panic!("expected Block")
};
match ast.try_stmt(stmts[0]).unwrap().kind {
crate::StmtKind::Return(Some(value)) => {
assert_eq!(
ast.try_expr(value).unwrap().kind,
crate::ExprKind::Number(1.0)
);
}
_ => panic!("expected Return(Some)"),
}
}
#[test]
fn parse_return_with_expression() {
let (ast, diags) = parse_str("function f(): number { return 1 + 2; }");
assert!(diags.is_empty());
let f = single_stmt(&ast);
let crate::StmtKind::Function { body, .. } = f.kind else {
panic!("expected Function");
};
let crate::StmtKind::Block(ref stmts) = ast.try_stmt(body).unwrap().kind else {
panic!("expected Block")
};
match ast.try_stmt(stmts[0]).unwrap().kind {
crate::StmtKind::Return(Some(value)) => {
let (op, ..) = binary(ast.try_expr(value).unwrap());
assert_eq!(op, crate::BinOp::Add);
}
_ => panic!("expected Return(Some(Binary))"),
}
}
#[test]
fn return_missing_semicolon_diagnoses() {
let (_, diags) = parse_str("function f(): number { return x x; }");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected `;` after return");
}
#[test]
fn parse_bare_block() {
let (ast, diags) = parse_str("{ x; y; }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
let crate::StmtKind::Block(ref stmts) = stmt.kind else {
panic!("expected Block");
};
assert_eq!(stmts.len(), 2);
}
#[test]
fn parse_empty_bare_block() {
let (ast, diags) = parse_str("{ }");
assert!(diags.is_empty());
let stmt = single_stmt(&ast);
let crate::StmtKind::Block(ref stmts) = stmt.kind else {
panic!("expected Block");
};
assert!(stmts.is_empty());
}
#[test]
fn parse_nested_bare_blocks() {
let (ast, diags) = parse_str("{ { } }");
assert!(diags.is_empty());
let outer = single_stmt(&ast);
let crate::StmtKind::Block(ref stmts) = outer.kind else {
panic!("expected outer Block");
};
assert_eq!(stmts.len(), 1);
assert!(matches!(
ast.try_stmt(stmts[0]).unwrap().kind,
crate::StmtKind::Block(ref v) if v.is_empty()
));
}
#[test]
fn snapshot_function_with_if_and_return() {
let source = "function isPositive(n: number): boolean {\n if (n > 0) {\n return true;\n } else {\n return false;\n }\n}";
let (ast, diags) = parse_str(source);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
insta::assert_debug_snapshot!(ast);
}
fn object_literal(e: &crate::Expr) -> &[crate::ObjectLiteralMember] {
match e.kind {
crate::ExprKind::ObjectLiteral { ref members } => members.as_slice(),
_ => panic!("expected ObjectLiteral, got {:?}", e.kind),
}
}
fn object_literal_field(m: &crate::ObjectLiteralMember) -> &crate::ObjectLiteralField {
match m {
crate::ObjectLiteralMember::Field(f) => f,
crate::ObjectLiteralMember::Spread { .. }
| crate::ObjectLiteralMember::Computed { .. } => {
panic!("expected Field member, got Spread")
}
}
}
fn array_literal(e: &crate::Expr) -> &[crate::ArrayLiteralElement] {
match e.kind {
crate::ExprKind::ArrayLiteral { ref elements } => elements.as_slice(),
_ => panic!("expected ArrayLiteral, got {:?}", e.kind),
}
}
fn array_literal_value(el: &crate::ArrayLiteralElement) -> crate::ExprId {
match el {
crate::ArrayLiteralElement::Value(id) => *id,
crate::ArrayLiteralElement::Spread { .. } => {
panic!("expected Value element, got Spread")
}
}
}
#[test]
fn parse_empty_object_literal() {
let (ast, diags) = parse_str("let x = {};");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
assert!(object_literal(ast.try_expr(value).unwrap()).is_empty());
}
#[test]
fn parse_object_literal_simple() {
let (ast, diags) = parse_str("let p = { x: 1, y: 2 };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let members = object_literal(ast.try_expr(value).unwrap());
assert_eq!(members.len(), 2);
let f0 = object_literal_field(&members[0]);
let f1 = object_literal_field(&members[1]);
assert_eq!(f0.name.name, "x");
assert_eq!(f1.name.name, "y");
assert_eq!(
ast.try_expr(f0.value).unwrap().kind,
crate::ExprKind::Number(1.0)
);
assert_eq!(
ast.try_expr(f1.value).unwrap().kind,
crate::ExprKind::Number(2.0)
);
}
#[test]
fn parse_object_literal_keyword_keys() {
let (ast, diags) = parse_str("let p = { type: 1, default: 2, null: 3, true: 4 };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let names: Vec<&str> = object_literal(ast.try_expr(value).unwrap())
.iter()
.map(object_literal_field)
.map(|f| f.name.name.as_str())
.collect();
assert_eq!(names, vec!["type", "default", "null", "true"]);
}
#[test]
fn parse_object_literal_string_key() {
let (ast, diags) = parse_str(r#"let p = { "hello": 1 };"#);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let members = object_literal(ast.try_expr(value).unwrap());
assert_eq!(members.len(), 1);
let f0 = object_literal_field(&members[0]);
assert_eq!(f0.name.name, "hello");
}
#[test]
fn parse_object_literal_string_key_decodes_escape() {
let (ast, diags) = parse_str(r#"let p = { "content\u002dtype": 1 };"#);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let members = object_literal(ast.try_expr(value).unwrap());
assert_eq!(members.len(), 1);
let f0 = object_literal_field(&members[0]);
assert_eq!(f0.name.name, "content-type");
}
#[test]
fn parse_object_literal_shorthand() {
let (ast, diags) = parse_str("let p = { x, y: 2 };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let members = object_literal(ast.try_expr(value).unwrap());
assert_eq!(members.len(), 2);
let f0 = object_literal_field(&members[0]);
assert_eq!(f0.name.name, "x");
assert_eq!(
ast.try_expr(f0.value).unwrap().kind,
crate::ExprKind::Identifier(crate::Ident {
name: "x".into(),
span: f0.name.span,
})
);
let f1 = object_literal_field(&members[1]);
assert_eq!(f1.name.name, "y");
assert_eq!(
ast.try_expr(f1.value).unwrap().kind,
crate::ExprKind::Number(2.0)
);
}
#[test]
fn parse_object_literal_string_key_shorthand_rejected() {
let (_ast, diags) = parse_str(r#"let p = { "x" };"#);
assert!(
diags
.iter()
.any(|d| d.message.contains("expected `:` after field name")),
"string-key shorthand should be rejected: {diags:?}"
);
}
#[test]
fn parse_object_literal_trailing_comma() {
let (ast, diags) = parse_str("let p = { x: 1, y: 2, };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
assert_eq!(object_literal(ast.try_expr(value).unwrap()).len(), 2);
}
#[test]
fn parse_object_literal_duplicate_key_diagnoses() {
let (_, diags) = parse_str("let p = { x: 1, x: 2 };");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "duplicate field `x` in object literal");
}
#[test]
fn parse_empty_array_literal() {
let (ast, diags) = parse_str("let xs = [];");
assert!(diags.is_empty());
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
assert!(array_literal(ast.try_expr(value).unwrap()).is_empty());
}
#[test]
fn parse_array_literal_simple() {
let (ast, diags) = parse_str("let xs = [1, 2, 3];");
assert!(diags.is_empty());
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let elements = array_literal(ast.try_expr(value).unwrap());
assert_eq!(elements.len(), 3);
assert_eq!(
ast.try_expr(array_literal_value(&elements[0]))
.unwrap()
.kind,
crate::ExprKind::Number(1.0)
);
assert_eq!(
ast.try_expr(array_literal_value(&elements[2]))
.unwrap()
.kind,
crate::ExprKind::Number(3.0)
);
}
#[test]
fn parse_array_literal_trailing_comma() {
let (ast, diags) = parse_str("let xs = [1, 2,];");
assert!(diags.is_empty());
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
assert_eq!(array_literal(ast.try_expr(value).unwrap()).len(), 2);
}
#[test]
fn parse_nested_array_in_object() {
let (ast, diags) = parse_str("let p = { items: [1, 2] };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let members = object_literal(ast.try_expr(value).unwrap());
let f0 = object_literal_field(&members[0]);
assert_eq!(f0.name.name, "items");
let inner = array_literal(ast.try_expr(f0.value).unwrap());
assert_eq!(inner.len(), 2);
}
#[test]
fn parse_nested_object_in_array() {
let (ast, diags) = parse_str("let xs = [{ x: 1 }, { x: 2 }];");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let elements = array_literal(ast.try_expr(value).unwrap());
assert_eq!(elements.len(), 2);
assert_eq!(
object_literal(ast.try_expr(array_literal_value(&elements[0])).unwrap()).len(),
1
);
assert_eq!(
object_literal(ast.try_expr(array_literal_value(&elements[1])).unwrap()).len(),
1
);
}
#[test]
fn brace_at_statement_start_is_block_not_object() {
let (_, diags) = parse_str("{ x: 1 }");
assert!(
!diags.is_empty(),
"expected a diagnostic for `x:` inside block"
);
}
fn field_access(e: &crate::Expr) -> (crate::ExprId, &str) {
match e.kind {
crate::ExprKind::FieldAccess { receiver, ref name } => (receiver, name.name.as_str()),
_ => panic!("expected FieldAccess, got {:?}", e.kind),
}
}
fn index_access(e: &crate::Expr) -> (crate::ExprId, crate::ExprId) {
match e.kind {
crate::ExprKind::IndexAccess { receiver, index } => (receiver, index),
_ => panic!("expected IndexAccess, got {:?}", e.kind),
}
}
#[test]
fn parse_field_access_simple() {
let (ast, diags) = parse_str("a.b;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (receiver, name) = field_access(outer);
assert!(matches!(
ast.try_expr(receiver).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(name, "b");
}
#[test]
fn parse_field_access_keyword_name() {
let (ast, diags) = parse_str("status.type.default.null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (receiver, name) = field_access(outer);
assert_eq!(name, "null");
let (receiver, name) = field_access(ast.try_expr(receiver).unwrap());
assert_eq!(name, "default");
let (_receiver, name) = field_access(ast.try_expr(receiver).unwrap());
assert_eq!(name, "type");
}
#[test]
fn parse_field_access_chain_left_assoc() {
let (ast, diags) = parse_str("a.b.c;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (mid_id, c) = field_access(outer);
assert_eq!(c, "c");
let (a_id, b) = field_access(ast.try_expr(mid_id).unwrap());
assert_eq!(b, "b");
assert!(matches!(
ast.try_expr(a_id).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn parse_index_access_simple() {
let (ast, diags) = parse_str("a[0];");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (receiver, idx) = index_access(outer);
assert!(matches!(
ast.try_expr(receiver).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(
ast.try_expr(idx).unwrap().kind,
crate::ExprKind::Number(0.0)
);
}
#[test]
fn parse_index_access_chain() {
let (ast, diags) = parse_str("a[0][1];");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (mid_id, one) = index_access(outer);
assert_eq!(
ast.try_expr(one).unwrap().kind,
crate::ExprKind::Number(1.0)
);
let (a_id, zero) = index_access(ast.try_expr(mid_id).unwrap());
assert_eq!(
ast.try_expr(zero).unwrap().kind,
crate::ExprKind::Number(0.0)
);
assert!(matches!(
ast.try_expr(a_id).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn parse_mixed_access_and_call_chain() {
let (ast, diags) = parse_str("a.b[c].d();");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let outer = expr_of_single_stmt(&ast);
let (callee, args) = call(outer);
assert!(args.is_empty());
let (idx_id, d_name) = field_access(ast.try_expr(callee).unwrap());
assert_eq!(d_name, "d");
let (b_id, c_idx) = index_access(ast.try_expr(idx_id).unwrap());
assert!(matches!(
ast.try_expr(c_idx).unwrap().kind,
crate::ExprKind::Identifier(_)
));
let (a_id, b_name) = field_access(ast.try_expr(b_id).unwrap());
assert_eq!(b_name, "b");
assert!(matches!(
ast.try_expr(a_id).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn parse_call_then_field() {
let (ast, diags) = parse_str("f().x;");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (call_id, x_name) = field_access(outer);
assert_eq!(x_name, "x");
let (_callee, args) = call(ast.try_expr(call_id).unwrap());
assert!(args.is_empty());
}
#[test]
fn parse_index_with_complex_expression() {
let (ast, diags) = parse_str("a[i + 1];");
assert!(diags.is_empty());
let outer = expr_of_single_stmt(&ast);
let (_, idx) = index_access(outer);
let (op, ..) = binary(ast.try_expr(idx).unwrap());
assert_eq!(op, crate::BinOp::Add);
}
#[test]
fn field_access_missing_name_diagnoses() {
let (_, diags) = parse_str("a.;");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected field name after `.`");
}
#[test]
fn parse_postfix_increment() {
let (ast, diags) = parse_str("x++;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let expr = expr_of_single_stmt(&ast);
let crate::ExprKind::PostfixUnary { op, operand } = expr.kind else {
panic!("expected PostfixUnary, got {:?}", expr.kind);
};
assert_eq!(op, crate::PostfixOp::Inc);
assert!(matches!(
ast.try_expr(operand).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(expr.span, crate::Span::new(F, 0, 3).unwrap());
}
#[test]
fn parse_postfix_decrement() {
let (ast, diags) = parse_str("x--;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
let crate::ExprKind::PostfixUnary { op, .. } = expr.kind else {
panic!("expected PostfixUnary");
};
assert_eq!(op, crate::PostfixOp::Dec);
}
#[test]
fn parse_postfix_non_null_assertion() {
let (ast, diags) = parse_str("x!;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let expr = expr_of_single_stmt(&ast);
let crate::ExprKind::PostfixUnary { op, operand } = expr.kind else {
panic!("expected PostfixUnary, got {:?}", expr.kind);
};
assert_eq!(op, crate::PostfixOp::NonNullAssert);
assert!(matches!(
ast.try_expr(operand).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(expr.span, crate::Span::new(F, 0, 2).unwrap());
}
#[test]
fn parse_non_null_assertion_continues_chain() {
let (ast, diags) = parse_str("x!.y;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let expr = expr_of_single_stmt(&ast);
let crate::ExprKind::FieldAccess { receiver, name } = &expr.kind else {
panic!("expected FieldAccess, got {:?}", expr.kind);
};
assert_eq!(name.name, "y");
assert!(matches!(
ast.try_expr(*receiver).unwrap().kind,
crate::ExprKind::PostfixUnary {
op: crate::PostfixOp::NonNullAssert,
..
}
));
}
#[test]
fn parse_postfix_on_field_access() {
let (ast, diags) = parse_str("o.f++;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
let crate::ExprKind::PostfixUnary { operand, .. } = expr.kind else {
panic!("expected PostfixUnary");
};
assert!(matches!(
ast.try_expr(operand).unwrap().kind,
crate::ExprKind::FieldAccess { .. }
));
}
#[test]
fn parse_postfix_on_index_access() {
let (ast, diags) = parse_str("xs[0]++;");
assert!(diags.is_empty());
let expr = expr_of_single_stmt(&ast);
let crate::ExprKind::PostfixUnary { operand, .. } = expr.kind else {
panic!("expected PostfixUnary");
};
assert!(matches!(
ast.try_expr(operand).unwrap().kind,
crate::ExprKind::IndexAccess { .. }
));
}
#[test]
fn parse_postfix_terminates_chain() {
let (_, diags) = parse_str("x++.y;");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected `;` after expression");
}
#[test]
fn parse_postfix_in_binary_continues() {
let (ast, diags) = parse_str("let y = x++ + 1;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let (op, lhs, _) = binary(ast.try_expr(value).unwrap());
assert_eq!(op, crate::BinOp::Add);
assert!(matches!(
ast.try_expr(lhs).unwrap().kind,
crate::ExprKind::PostfixUnary { .. }
));
}
#[test]
fn parse_index_assignment() {
let (ast, diags) = parse_str("xs[0] = 1;");
assert!(diags.is_empty(), "unexpected: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::AssignIndex {
receiver,
index,
value,
} = stmt.kind
else {
panic!("expected AssignIndex, got {:?}", stmt.kind);
};
assert!(matches!(
ast.try_expr(receiver).unwrap().kind,
crate::ExprKind::Identifier(_)
));
assert_eq!(
ast.try_expr(index).unwrap().kind,
crate::ExprKind::Number(0.0)
);
assert_eq!(
ast.try_expr(value).unwrap().kind,
crate::ExprKind::Number(1.0)
);
}
#[test]
fn parse_for_update_postfix() {
let (ast, diags) = parse_str("function main(): void { for (let i = 0; i < 3; i++) {} }");
assert!(diags.is_empty(), "unexpected: {diags:?}");
assert_eq!(ast.top_level.len(), 1);
}
#[test]
fn parse_for_update_index_assignment() {
let (ast, diags) =
parse_str("function main(): void { for (let i = 0; i < 3; xs[i] = i) {} }");
assert!(diags.is_empty(), "unexpected: {diags:?}");
assert_eq!(ast.top_level.len(), 1);
}
#[test]
fn index_access_missing_close_bracket_diagnoses() {
let (_, diags) = parse_str("a[1;");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected `]`");
}
#[test]
fn field_access_inside_object_literal_value_does_not_steal_dot() {
let (ast, diags) = parse_str("let x = { a: 1 }.foo;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Let { value, .. } = single_stmt(&ast).kind else {
panic!("expected Let");
};
let (recv_id, name) = field_access(ast.try_expr(value).unwrap());
assert_eq!(name, "foo");
assert_eq!(object_literal(ast.try_expr(recv_id).unwrap()).len(), 1);
}
fn type_of_let(stmt: &crate::Stmt) -> &crate::TypeAnnotation {
match stmt.kind {
crate::StmtKind::Let { ref ty, .. } => ty.as_ref().expect("expected type annotation"),
_ => panic!("expected Let"),
}
}
#[test]
fn parse_array_type_annotation() {
let (ast, diags) = parse_str("let xs: number[] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
match ty.kind {
crate::TypeAnnotationKind::Array(ref inner) => {
assert!(matches!(inner.kind, crate::TypeAnnotationKind::Name { .. }));
}
_ => panic!("expected Array, got {:?}", ty.kind),
}
}
#[test]
fn parse_nested_array_type_annotation() {
let (ast, diags) = parse_str("let xs: number[][] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Array(ref inner) = ty.kind else {
panic!("expected outer Array");
};
let crate::TypeAnnotationKind::Array(ref inner2) = inner.kind else {
panic!("expected inner Array");
};
assert!(matches!(
inner2.kind,
crate::TypeAnnotationKind::Name { .. }
));
}
#[test]
fn parse_object_type_annotation_simple() {
let (ast, diags) = parse_str("let p: { x: number; y: number } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object, got {:?}", ty.kind);
};
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name.name, "x");
assert_eq!(fields[1].name.name, "y");
}
#[test]
fn readonly_is_rejected_on_methods_accessors_and_method_signatures() {
let source = "class K {\n\
readonly m(): number { return 1; }\n\
public readonly n<T>(): number { return 1; }\n\
readonly get g(): number { return 1; }\n\
readonly p: number = 1;\n\
}\n\
type T = { readonly r(a: number): boolean; readonly f: () => void };\n\
interface I { readonly s(): void; readonly q: number; }\n";
let (_, diags) = parse_str(source);
let lines: Vec<usize> = diags
.iter()
.map(|d| {
assert_eq!(
d.message,
"`readonly` can only modify a property or index signature"
);
source[..d.span.start as usize].matches('\n').count() + 1
})
.collect();
assert_eq!(lines, vec![2, 3, 4, 7, 8]);
}
#[test]
fn parse_object_type_method_members() {
let (ast, diags) =
parse_str("let p: { m(): number; opt?(): string; r(a: number): boolean } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object, got {:?}", ty.kind);
};
assert_eq!(fields.len(), 3);
for field in fields {
assert!(
matches!(field.ty.kind, crate::TypeAnnotationKind::Function { .. }),
"member `{}` should be function-typed, got {:?}",
field.name.name,
field.ty.kind,
);
}
assert!(fields[1].optional, "`opt?()` is an optional member");
let crate::TypeAnnotationKind::Function { ref params, .. } = fields[2].ty.kind else {
unreachable!("checked above");
};
assert_eq!(params.len(), 1);
assert_eq!(params[0].name.name, "a");
}
#[test]
fn parse_object_type_annotation_with_commas() {
let (ast, diags) = parse_str("let p: { x: number, y: number } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
assert_eq!(fields.len(), 2);
}
#[test]
fn parse_object_type_annotation_empty() {
let (ast, diags) = parse_str("let p: {} = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
assert!(fields.is_empty());
}
#[test]
fn parse_object_type_with_optional_field() {
let (ast, diags) = parse_str("let p: { id: number; nick?: string } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name.name, "id");
assert!(!fields[0].optional);
assert_eq!(fields[1].name.name, "nick");
assert!(fields[1].optional);
}
#[test]
fn parse_object_type_with_readonly_fields() {
let (ast, diags) = parse_str(
"let issue: { readonly id: string; readonly title?: string; body: string } = null;",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
let fields: Vec<(&str, bool, bool)> = fields
.iter()
.map(|field| (field.name.name.as_str(), field.optional, field.readonly))
.collect();
assert_eq!(
fields,
vec![
("id", false, true),
("title", true, true),
("body", false, false),
]
);
}
#[test]
fn parse_object_type_mixed_optional_and_required() {
let (ast, diags) = parse_str("let p: { a?: number; b: string; c?: boolean } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
let opt: Vec<(&str, bool)> = fields
.iter()
.map(|f| (f.name.name.as_str(), f.optional))
.collect();
assert_eq!(opt, vec![("a", true), ("b", false), ("c", true)]);
}
#[test]
fn parse_object_type_keyword_fields() {
let (ast, diags) =
parse_str("let p: { type: string; default?: number; null: boolean } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
let opt: Vec<(&str, bool)> = fields
.iter()
.map(|f| (f.name.name.as_str(), f.optional))
.collect();
assert_eq!(
opt,
vec![("type", false), ("default", true), ("null", false)]
);
}
#[test]
fn parse_object_type_quoted_fields() {
let (ast, diags) =
parse_str(r#"let p: { "content-type": string; "x\u002drequest-id"?: string } = null;"#);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
let opt: Vec<(&str, bool)> = fields
.iter()
.map(|f| (f.name.name.as_str(), f.optional))
.collect();
assert_eq!(opt, vec![("content-type", false), ("x-request-id", true)]);
}
#[test]
fn parse_interface_with_optional_property() {
let (ast, diags) = parse_str("interface User { id: number; deletedAt?: string; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::InterfaceDecl { ref members, .. } = stmt.kind else {
panic!("expected interface decl");
};
let opt: Vec<(&str, bool)> = members
.iter()
.map(|m| match m {
crate::InterfaceMember::Property { name, optional, .. } => {
(name.name.as_str(), *optional)
}
_ => panic!("expected property"),
})
.collect();
assert_eq!(opt, vec![("id", false), ("deletedAt", true)]);
}
#[test]
fn parse_interface_with_readonly_properties() {
let (ast, diags) = parse_str(
"interface Issue { readonly id: string; readonly title?: string; body: string; }",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::InterfaceDecl { ref members, .. } = stmt.kind else {
panic!("expected interface decl");
};
let opt: Vec<(&str, bool, bool)> = members
.iter()
.map(|member| match member {
crate::InterfaceMember::Property {
name,
optional,
readonly,
..
} => (name.name.as_str(), *optional, *readonly),
_ => panic!("expected property"),
})
.collect();
assert_eq!(
opt,
vec![
("id", false, true),
("title", true, true),
("body", false, false),
]
);
}
#[test]
fn parse_readonly_property_name_without_modifier() {
let (ast, diags) = parse_str(
"interface Meta { readonly: boolean; }\nlet meta: { readonly: boolean } = null;",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::InterfaceDecl { ref members, .. } =
ast.try_stmt(ast.top_level[0]).unwrap().kind
else {
panic!("expected interface decl");
};
let crate::InterfaceMember::Property {
ref name, readonly, ..
} = members[0]
else {
panic!("expected property");
};
assert_eq!(name.name, "readonly");
assert!(
!readonly,
"`readonly` here is the property name, not a modifier"
);
let ty = type_of_let(ast.try_stmt(ast.top_level[1]).unwrap());
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
assert_eq!(fields[0].name.name, "readonly");
assert!(!fields[0].readonly);
}
#[test]
fn parse_interface_keyword_members() {
let (ast, diags) =
parse_str("interface IssueStatus { type: string; default(): string; null: boolean; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::InterfaceDecl { ref members, .. } = stmt.kind else {
panic!("expected interface decl");
};
let names: Vec<&str> = members
.iter()
.map(|m| match m {
crate::InterfaceMember::Method { name, .. }
| crate::InterfaceMember::Property { name, .. } => name.name.as_str(),
crate::InterfaceMember::IndexSignature(_) => panic!("unexpected index signature"),
})
.collect();
assert_eq!(names, vec!["type", "default", "null"]);
}
#[test]
fn parse_interface_quoted_property_members() {
let (ast, diags) = parse_str(
r#"interface Headers { "content-type": string; "x\u002drequest-id"?: string; }"#,
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::InterfaceDecl { ref members, .. } = stmt.kind else {
panic!("expected interface decl");
};
let names: Vec<(&str, bool)> = members
.iter()
.map(|m| match m {
crate::InterfaceMember::Property { name, optional, .. } => {
(name.name.as_str(), *optional)
}
_ => panic!("expected property"),
})
.collect();
assert_eq!(names, vec![("content-type", false), ("x-request-id", true)]);
}
#[test]
fn parse_param_optional_marker() {
let (ast, diags) = parse_str("function f(x?: number): void {}");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::Function { ref params, .. } = single_stmt(&ast).kind else {
panic!("expected function")
};
assert!(params[0].optional);
assert!(params[0].default.is_none());
}
#[test]
fn parse_interface_optional_method() {
let (ast, diags) = parse_str("interface F { foo?(x?: number): void; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::InterfaceDecl { ref members, .. } = single_stmt(&ast).kind else {
panic!("expected interface")
};
let crate::InterfaceMember::Property { optional, ty, .. } = &members[0] else {
panic!("expected callable property")
};
assert!(*optional);
let crate::TypeAnnotationKind::Function { params, .. } = &ty.kind else {
panic!("expected function type")
};
assert!(params[0].optional);
}
#[test]
fn parse_interface_with_call_signature() {
let (ast, diags) = parse_str("interface F { (s: string): number; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::InterfaceDecl { ref members, .. } = stmt.kind else {
panic!("expected interface decl");
};
assert_eq!(members.len(), 1);
match &members[0] {
crate::InterfaceMember::Method { name, params, .. } => {
assert_eq!(name.name, "@call");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name.name, "s");
}
other => panic!("expected method (call signature), got {other:?}"),
}
}
#[test]
fn parse_interface_mixed_call_signature_and_method() {
let (ast, diags) = parse_str("interface F { greet(): string; (x: number): number; }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::InterfaceDecl { ref members, .. } = stmt.kind else {
panic!("expected interface decl");
};
let names: Vec<&str> = members
.iter()
.map(|m| match m {
crate::InterfaceMember::Method { name, .. } => name.name.as_str(),
_ => "<property>",
})
.collect();
assert_eq!(names, vec!["greet", "@call"]);
}
#[test]
fn parse_interface_rejects_generic_call_signature() {
let (_ast, diags) = parse_str("interface F { <T>(x: T): T; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("generic call signatures")),
"expected generic-call-signature diagnostic, got {diags:?}"
);
}
#[test]
fn parse_object_type_with_array_field() {
let (ast, diags) = parse_str("let p: { xs: number[] } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object");
};
assert_eq!(fields[0].name.name, "xs");
assert!(matches!(
fields[0].ty.kind,
crate::TypeAnnotationKind::Array(_)
));
}
#[test]
fn parse_array_of_objects() {
let (ast, diags) = parse_str("let xs: { x: number }[] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Array(ref inner) = ty.kind else {
panic!("expected Array");
};
assert!(matches!(
inner.kind,
crate::TypeAnnotationKind::Object { .. }
));
}
#[test]
fn parse_tuple_type_annotation() {
let (ast, diags) = parse_str("let p: [string, number] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Tuple(ref elems) = ty.kind else {
panic!("expected Tuple, got {:?}", ty.kind);
};
assert_eq!(elems.len(), 2);
assert!(matches!(
elems[0].kind,
crate::TypeAnnotationKind::Name { .. }
));
assert!(matches!(
elems[1].kind,
crate::TypeAnnotationKind::Name { .. }
));
}
#[test]
fn parse_singleton_tuple_type() {
let (ast, diags) = parse_str("let p: [number] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Tuple(ref elems) = ty.kind else {
panic!("expected Tuple");
};
assert_eq!(elems.len(), 1);
}
#[test]
fn parse_nested_tuple_type() {
let (ast, diags) = parse_str("let p: [[number, string], boolean] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Tuple(ref outer) = ty.kind else {
panic!("expected outer Tuple");
};
assert_eq!(outer.len(), 2);
let crate::TypeAnnotationKind::Tuple(ref inner) = outer[0].kind else {
panic!("expected inner Tuple");
};
assert_eq!(inner.len(), 2);
}
#[test]
fn parse_array_of_tuple_type() {
let (ast, diags) = parse_str("let xs: [string, number][] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Array(ref inner) = ty.kind else {
panic!("expected outer Array, got {:?}", ty.kind);
};
assert!(matches!(inner.kind, crate::TypeAnnotationKind::Tuple(_)));
}
#[test]
fn parse_trailing_comma_tuple_type() {
let (ast, diags) = parse_str("let p: [number, string,] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Tuple(ref elems) = ty.kind else {
panic!("expected Tuple");
};
assert_eq!(elems.len(), 2);
}
#[test]
fn parse_empty_tuple_type_rejected() {
let (_ast, diags) = parse_str("let p: [] = null;");
assert!(
diags.iter().any(|d| d
.message
.contains("tuple types must have at least one element")),
"expected empty-tuple diagnostic, got: {diags:?}"
);
}
#[test]
fn parse_labeled_tuple_drops_labels() {
let (ast, diags) = parse_str("let p: [first: number, string] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Tuple(ref elems) = ty.kind else {
panic!("expected Tuple, got {:?}", ty.kind);
};
assert_eq!(elems.len(), 2);
assert!(
elems
.iter()
.all(|e| matches!(e.kind, crate::TypeAnnotationKind::Name { .. }))
);
}
#[test]
fn parse_labeled_optional_tuple_element() {
let (ast, diags) = parse_str("let p: [first: number, second?: string] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Tuple(elements) = &ty.kind else {
panic!("expected tuple")
};
assert!(matches!(
elements[1].kind,
crate::TypeAnnotationKind::Optional(_)
));
}
#[test]
fn parse_readonly_binds_to_the_postfix_type() {
let (ast, diags) = parse_str("let p: readonly number[][] | null = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Union(ref members) = ty.kind else {
panic!("expected Union, got {:?}", ty.kind);
};
let crate::TypeAnnotationKind::Readonly(ref operand) = members[0].kind else {
panic!("expected Readonly, got {:?}", members[0].kind);
};
let crate::TypeAnnotationKind::Array(ref inner) = operand.kind else {
panic!("expected Array, got {:?}", operand.kind);
};
assert!(matches!(inner.kind, crate::TypeAnnotationKind::Array(_)));
}
#[test]
fn parse_readonly_as_a_type_name_is_not_the_operator() {
let (ast, diags) = parse_str("let p: readonly = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Name { .. }));
}
#[test]
fn parse_generic_one_arg() {
let (ast, diags) = parse_str("let x: Foo<number> = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Name { ref name, ref args } = ty.kind else {
panic!("expected Name, got {:?}", ty.kind);
};
assert_eq!(name.span, crate::Span::new(F, 7, 10).unwrap());
assert_eq!(ty.span, crate::Span::new(F, 7, 18).unwrap());
assert_eq!(args.len(), 1);
assert!(matches!(
args[0].kind,
crate::TypeAnnotationKind::Name { .. }
));
}
#[test]
fn parse_generic_two_args() {
let (ast, diags) = parse_str("let m: Map<string, number> = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Name { ref args, .. } = ty.kind else {
panic!("expected Name");
};
assert_eq!(args.len(), 2);
}
#[test]
fn parse_generic_nested() {
let (ast, diags) = parse_str("let b: Box<Box<T>> = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Name { ref args, .. } = ty.kind else {
panic!("expected outer Name");
};
assert_eq!(args.len(), 1);
let crate::TypeAnnotationKind::Name {
args: ref inner, ..
} = args[0].kind
else {
panic!("expected inner Name");
};
assert_eq!(inner.len(), 1);
}
#[test]
fn parse_generic_array_postfix() {
let (ast, diags) = parse_str("let xs: Foo<number>[] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Array(ref inner) = ty.kind else {
panic!("expected Array");
};
assert!(
matches!(inner.kind, crate::TypeAnnotationKind::Name { ref args, .. } if args.len() == 1)
);
}
#[test]
fn parse_generic_empty_diagnoses() {
let (_ast, diags) = parse_str("let x: Foo<> = null;");
assert!(
diags
.iter()
.any(|d| d.message.contains("empty generic argument list")),
"expected empty-generic-args diagnostic, got: {diags:?}"
);
}
#[test]
fn parse_generic_trailing_comma_ok() {
let (_ast, diags) = parse_str("let x: Foo<number,> = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
#[test]
fn parse_generic_missing_close_diagnoses() {
let (_ast, diags) = parse_str("let x: Foo<number = null;");
assert!(
diags
.iter()
.any(|d| d.message.contains("expected `,` or `>`")),
"expected unterminated-generic diagnostic, got: {diags:?}"
);
}
#[test]
fn parse_qualified_two_segments() {
let (ast, diags) = parse_str("let x: Foo.Bar = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Qualified { ref path, ref args } = ty.kind else {
panic!("expected Qualified, got {:?}", ty.kind);
};
assert_eq!(path.len(), 2);
assert_eq!(path[0].span, crate::Span::new(F, 7, 10).unwrap());
assert_eq!(path[1].span, crate::Span::new(F, 11, 14).unwrap());
assert!(args.is_empty());
assert_eq!(ty.span, crate::Span::new(F, 7, 14).unwrap());
}
#[test]
fn parse_qualified_three_segments() {
let (ast, diags) = parse_str("let x: Foo.Bar.Baz = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Qualified { ref path, .. } = ty.kind else {
panic!("expected Qualified, got {:?}", ty.kind);
};
assert_eq!(path.len(), 3);
}
#[test]
fn parse_qualified_with_generic_args() {
let (ast, diags) = parse_str("let x: Foo.Box<number> = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Qualified { ref path, ref args } = ty.kind else {
panic!("expected Qualified, got {:?}", ty.kind);
};
assert_eq!(path.len(), 2);
assert_eq!(args.len(), 1);
assert!(matches!(
args[0].kind,
crate::TypeAnnotationKind::Name { .. }
));
}
#[test]
fn parse_qualified_with_array_postfix() {
let (ast, diags) = parse_str("let xs: Foo.Bar[] = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Array(ref inner) = ty.kind else {
panic!("expected Array, got {:?}", ty.kind);
};
assert!(matches!(
inner.kind,
crate::TypeAnnotationKind::Qualified { .. }
));
}
#[test]
fn parse_qualified_trailing_dot_diagnoses() {
let (_ast, diags) = parse_str("let x: Foo. = null;");
assert!(
diags.iter().any(|d| d
.message
.contains("expected identifier after `.` in type name")),
"expected dotted-name diagnostic, got: {diags:?}"
);
}
#[test]
fn parse_qualified_void_root_rejected_dot() {
let (ast, diags) = parse_str("function f(): void.Bar { }");
assert!(!diags.is_empty(), "expected a diagnostic for `void.Bar`");
let _ = ast;
}
fn union_members(ty: &crate::TypeAnnotation) -> &[crate::TypeAnnotation] {
match &ty.kind {
crate::TypeAnnotationKind::Union(members) => members,
other => panic!("expected Union, got {other:?}"),
}
}
#[test]
fn parse_union_two_members() {
let (ast, diags) = parse_str("let x: number | string = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let members = union_members(ty);
assert_eq!(members.len(), 2);
assert!(matches!(
members[0].kind,
crate::TypeAnnotationKind::Name { .. }
));
assert_eq!(members[0].span, crate::Span::new(F, 7, 13).unwrap()); assert!(matches!(
members[1].kind,
crate::TypeAnnotationKind::Name { .. }
));
assert_eq!(members[1].span, crate::Span::new(F, 16, 22).unwrap()); assert_eq!(ty.span, crate::Span::new(F, 7, 22).unwrap());
}
#[test]
fn parse_union_three_members() {
let (ast, diags) = parse_str("let x: A | B | C = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let members = union_members(ty);
assert_eq!(members.len(), 3);
for m in members {
assert!(matches!(m.kind, crate::TypeAnnotationKind::Name { .. }));
}
}
#[test]
fn parse_union_lower_than_array() {
let (ast, diags) = parse_str("let x: number[] | string = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let members = union_members(ty);
assert_eq!(members.len(), 2);
assert!(
matches!(members[0].kind, crate::TypeAnnotationKind::Array(_)),
"expected first member to be Array, got {:?}",
members[0].kind
);
assert!(matches!(
members[1].kind,
crate::TypeAnnotationKind::Name { .. }
));
}
#[test]
fn parse_union_with_null_member() {
let (ast, diags) = parse_str("let x: number | null = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let members = union_members(ty);
assert_eq!(members.len(), 2);
assert_eq!(members[0].span, crate::Span::new(F, 7, 13).unwrap()); assert!(matches!(
members[1].kind,
crate::TypeAnnotationKind::Name { .. }
));
assert_eq!(members[1].span, crate::Span::new(F, 16, 20).unwrap()); }
#[test]
fn parse_union_in_function_return_type() {
let (ast, diags) = parse_str("function f(): number | string { }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function {
ref return_type, ..
} => {
let return_type = return_type.as_ref().expect("plain return type");
let members = union_members(return_type);
assert_eq!(members.len(), 2);
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_union_in_function_param_type() {
let (ast, diags) = parse_str("function f(x: number | string): void { }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = single_stmt(&ast);
match stmt.kind {
crate::StmtKind::Function { ref params, .. } => {
let p_ty = params[0].ty.as_ref().expect("param type");
let members = union_members(p_ty);
assert_eq!(members.len(), 2);
}
_ => panic!("expected Function"),
}
}
#[test]
fn parse_union_in_object_field_type() {
let (ast, diags) = parse_str("let p: { x: number | string } = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let crate::TypeAnnotationKind::Object { ref fields, .. } = ty.kind else {
panic!("expected Object, got {:?}", ty.kind);
};
assert_eq!(fields.len(), 1);
let members = union_members(&fields[0].ty);
assert_eq!(members.len(), 2);
}
#[test]
fn parse_union_leading_pipe_is_forgiven() {
let (ast, diags) = parse_str("let x: | number | string = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
let members = union_members(ty);
assert_eq!(members.len(), 2);
}
#[test]
fn parse_union_leading_pipe_single_member_unwraps() {
let (ast, diags) = parse_str("let x: | number = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let ty = type_of_let(single_stmt(&ast));
assert!(matches!(ty.kind, crate::TypeAnnotationKind::Name { .. }));
}
#[test]
fn parse_union_trailing_pipe_diagnoses() {
let (_, diags) = parse_str("let x: number | = null;");
assert!(!diags.is_empty());
assert_eq!(diags[0].message, "expected type");
}
#[test]
fn snapshot_union_type_mix() {
let (ast, diags) = parse_str("let xs: number[] | string | null = null;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn parse_object_type_duplicate_field_diagnoses() {
let (_, diags) = parse_str("let p: { readonly x: number; x: string } = null;");
assert!(!diags.is_empty());
assert!(
diags
.iter()
.any(|d| d.message == "duplicate field `x` in object type"),
"expected duplicate-field diagnostic, got: {diags:?}"
);
}
#[test]
fn snapshot_object_and_array_literals() {
let (ast, diags) = parse_str(r#"let mix = [{ a: 1, b: "hi" }, {}];"#);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn snapshot_function_with_while() {
let source = "function loop(n: number): void {\n while (n > 0) {\n log(n);\n }\n}";
let (ast, diags) = parse_str(source);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
insta::assert_debug_snapshot!(ast);
}
fn parse_arrow_const(
source: &str,
) -> (
Ast,
Vec<crate::ParamDecl>,
Option<crate::TypeAnnotation>,
crate::ArrowBody,
) {
let (ast, diags) = parse_str(source);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert_eq!(ast.top_level.len(), 1);
let stmt = ast.try_stmt(ast.top_level[0]).unwrap();
let value = match &stmt.kind {
StmtKind::Const { value, .. } => *value,
_ => panic!("expected const declaration"),
};
let value = match ast.try_expr(value).unwrap().kind {
ExprKind::FunctionExpression { function, .. } => function,
_ => value,
};
let expr = ast.try_expr(value).unwrap().clone();
let (params, return_type, body) = match expr.kind {
ExprKind::Arrow {
params,
return_type,
body,
..
} => (params, return_type, body),
other => panic!("expected Arrow, got {other:?}"),
};
(ast, params, return_type, body)
}
#[test]
fn parses_anonymous_function_expression_as_arrow() {
let (ast, params, return_type, body) =
parse_arrow_const("const f = function (x: number): number { return x + 1; };");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name.name, "x");
assert!(return_type.is_some());
assert!(matches!(body, crate::ArrowBody::Block(_)));
let _ = ast;
}
#[test]
fn parses_function_expression_with_no_params() {
let (_ast, params, _return_type, body) =
parse_arrow_const("const f = function (): void {};");
assert!(params.is_empty());
assert!(matches!(body, crate::ArrowBody::Block(_)));
}
#[test]
fn parses_named_function_expression_signature() {
let (_ast, params, _return_type, _body) =
parse_arrow_const("const f = function named(x: number): number { return x + 1; };");
assert_eq!(params.len(), 1);
}
#[test]
fn accepts_a_named_function_expression_that_calls_itself() {
let (_ast, diags) =
parse_str("const f = function bar(x: number): number { return bar(x); };");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn accepts_a_named_function_expression_using_the_name_as_a_property() {
let (_ast, diags) = parse_str("const f = function bar(o: O): number { return o.bar; };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
#[test]
fn accepts_a_named_function_expression_using_the_name_as_an_object_key() {
let (_ast, diags) = parse_str(
"const f = function bar(x: number): number { const o = { bar: 1 }; return o.bar + x; };",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
#[test]
fn accepts_a_named_function_expression_with_its_name_in_a_string() {
let (_ast, diags) =
parse_str("const f = function bar(x: number): string { return \"bar\"; };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
#[test]
fn accepts_a_self_call_inside_a_template_substitution() {
let (_ast, diags) =
parse_str("const f = function bar(x: number): string { return `${bar(0)}`; };");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn rejects_a_generic_function_expression() {
let (_ast, diags) = parse_str("const f = function <T>(x: T): T { return x; };");
assert!(
diags
.iter()
.any(|d| d.message.contains("generic function expressions")),
"generic function expression should be rejected: {diags:?}"
);
}
#[test]
fn accepts_this_inside_a_function_expression_in_a_class_method() {
let (_ast, diags) = parse_str(
"class C { x: number = 1; m(): number { \
const f = function (this: { x: number }): number { return this.x; }; return f(); } }",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn retains_this_boundary_inside_method_shorthand() {
let (ast, diags) = parse_str(
"class C { x: number = 1; m(): number { \
const o = { x: 2, g(): number { return this.x; } }; return o.g(); } }",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert!((0..ast.exprs_len()).any(|index| matches!(
ast.try_expr(crate::ExprId(index as u32)).unwrap().kind,
ExprKind::ThisOutsideReceiver
)));
}
#[test]
fn this_still_parses_in_an_ordinary_class_method() {
let (_ast, diags) = parse_str("class C { x: number = 1; m(): number { return this.x; } }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
#[test]
fn function_keyword_still_parses_as_an_object_key() {
let (_ast, diags) = parse_str("let o = { function: 1 };");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
}
fn object_literal_member_values(source: &str) -> (Ast, Vec<crate::ExprId>) {
let (ast, diags) = parse_str(source);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = ast.try_stmt(ast.top_level[0]).unwrap();
let value = match &stmt.kind {
StmtKind::Const { value, .. } => *value,
other => panic!("expected a const declaration, got {other:?}"),
};
let members = match &ast.try_expr(value).unwrap().kind {
ExprKind::ObjectLiteral { members } => members
.iter()
.map(crate::ObjectLiteralMember::value)
.collect::<Vec<_>>(),
other => panic!("expected ObjectLiteral, got {other:?}"),
};
(ast, members)
}
#[test]
fn parses_object_literal_method_shorthand_as_arrow() {
let (ast, members) =
object_literal_member_values("const o = { m(x: number): number { return x; } };");
assert_eq!(members.len(), 1);
match &ast.try_expr(members[0]).unwrap().kind {
ExprKind::Arrow { params, body, .. } => {
assert_eq!(params.len(), 1);
assert_eq!(params[0].name.name, "x");
assert!(matches!(body, crate::ArrowBody::Block(_)));
}
other => panic!("expected Arrow, got {other:?}"),
}
}
#[test]
fn parses_method_shorthand_with_a_keyword_name() {
let (ast, members) =
object_literal_member_values("const o = { if(x: number): number { return x; } };");
assert!(matches!(
ast.try_expr(members[0]).unwrap().kind,
ExprKind::Arrow { .. }
));
}
#[test]
fn parses_method_shorthand_alongside_plain_and_shorthand_properties() {
let (ast, members) =
object_literal_member_values("const o = { a: 1, m(): void {}, b: 2 };");
assert_eq!(members.len(), 3);
assert!(matches!(
ast.try_expr(members[1]).unwrap().kind,
ExprKind::Arrow { .. }
));
}
#[test]
fn parses_bare_ident_arrow_expression_body() {
let (_ast, params, return_type, body) = parse_arrow_const("const f = x => x + 1;");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name.name, "x");
assert!(params[0].ty.is_none());
assert!(return_type.is_none());
assert!(matches!(body, crate::ArrowBody::Expr(_)));
}
#[test]
fn parses_zero_param_arrow() {
let (_ast, params, return_type, body) = parse_arrow_const("const f = () => 0;");
assert!(params.is_empty());
assert!(return_type.is_none());
assert!(matches!(body, crate::ArrowBody::Expr(_)));
}
#[test]
fn parses_paren_unannotated_arrow() {
let (_ast, params, _return_type, _body) = parse_arrow_const("const f = (x) => x;");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name.name, "x");
assert!(params[0].ty.is_none());
}
#[test]
fn parses_typed_param_arrow() {
let (_ast, params, return_type, body) =
parse_arrow_const("const f = (x: number) => x * 2;");
assert_eq!(params.len(), 1);
assert!(params[0].ty.is_some());
assert!(return_type.is_none());
assert!(matches!(body, crate::ArrowBody::Expr(_)));
}
#[test]
fn parses_typed_param_and_return_arrow() {
let (_ast, params, return_type, body) =
parse_arrow_const("const f = (x: number): number => x * 2;");
assert_eq!(params.len(), 1);
assert!(params[0].ty.is_some());
assert!(return_type.is_some());
assert!(matches!(body, crate::ArrowBody::Expr(_)));
}
#[test]
fn parses_multi_param_unannotated_arrow() {
let (_ast, params, _return_type, _body) = parse_arrow_const("const f = (x, y) => x + y;");
assert_eq!(params.len(), 2);
assert_eq!(params[0].name.name, "x");
assert_eq!(params[1].name.name, "y");
assert!(params[0].ty.is_none());
assert!(params[1].ty.is_none());
}
#[test]
fn parses_multi_param_typed_arrow() {
let (_ast, params, _return_type, _body) =
parse_arrow_const("const f = (x: number, y: number) => x + y;");
assert_eq!(params.len(), 2);
assert!(params[0].ty.is_some());
assert!(params[1].ty.is_some());
}
#[test]
fn parses_block_body_arrow() {
let (_ast, params, _return_type, body) =
parse_arrow_const("const f = (x: number) => { return x; };");
assert_eq!(params.len(), 1);
assert!(matches!(body, crate::ArrowBody::Block(_)));
}
#[test]
fn rejects_typed_bare_ident_arrow() {
let (_ast, diags) = parse_str("const f = x: number => x;");
assert!(
!diags.is_empty(),
"expected diagnostics for `x: number => …`",
);
}
#[test]
fn arrow_inside_call_argument_parses() {
let (ast, diags) = parse_str("f((x: number) => x + 1, 2);");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = ast.try_stmt(ast.top_level[0]).unwrap();
let call = match &stmt.kind {
StmtKind::Expr(eid) => ast.try_expr(*eid).unwrap(),
_ => panic!("expected expression statement"),
};
let args = match &call.kind {
ExprKind::Call { args, .. } => args.clone(),
other => panic!("expected Call, got {other:?}"),
};
assert_eq!(args.len(), 2);
assert!(matches!(
ast.try_expr(args[0]).unwrap().kind,
ExprKind::Arrow { .. }
));
}
#[test]
fn function_decl_param_still_requires_annotation() {
let (_ast, diags) = parse_str("function f(x): number { return x; }");
assert!(
diags.iter().any(|d| d.message.contains("type annotation")),
"expected `parameter requires a type annotation` diagnostic, got {diags:?}",
);
}
#[test]
fn paren_expression_still_parses_when_no_arrow() {
let (ast, diags) = parse_str("const a = (1 + 2);");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let stmt = ast.try_stmt(ast.top_level[0]).unwrap();
let value = match &stmt.kind {
StmtKind::Const { value, .. } => *value,
_ => panic!("expected const declaration"),
};
assert!(matches!(
ast.try_expr(value).unwrap().kind,
ExprKind::Paren(_)
));
}
#[test]
fn parse_named_import_single() {
let (ast, diags) = parse_str("import { v4 } from \"submilli:uuid\";");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert_eq!(ast.top_level.len(), 1);
let stmt = ast.try_stmt(ast.top_level[0]).unwrap();
let (module, kind) = match &stmt.kind {
StmtKind::Import { module, kind, .. } => (module, kind),
other => panic!("expected Import, got {other:?}"),
};
assert_eq!(module, "submilli:uuid");
let specs = match kind {
ImportKind::Named(specs) => specs,
other => panic!("expected Named, got {other:?}"),
};
assert_eq!(specs.len(), 1);
assert_eq!(specs[0].imported_name.name, "v4");
assert_eq!(specs[0].local_name.name, "v4");
}
#[test]
fn parse_named_import_multi_and_alias() {
let (ast, diags) =
parse_str("import { v4, v7 as makeId, validate } from \"submilli:uuid\";");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let specs = match &ast.try_stmt(ast.top_level[0]).unwrap().kind {
StmtKind::Import {
kind: ImportKind::Named(s),
..
} => s.clone(),
other => panic!("expected named import, got {other:?}"),
};
assert_eq!(specs.len(), 3);
assert_eq!(specs[0].imported_name.name, "v4");
assert_eq!(specs[0].local_name.name, "v4");
assert_eq!(specs[1].imported_name.name, "v7");
assert_eq!(specs[1].local_name.name, "makeId");
assert_eq!(specs[2].imported_name.name, "validate");
assert_eq!(specs[2].local_name.name, "validate");
}
#[test]
fn parse_namespace_import() {
let (ast, diags) = parse_str("import uuid from \"submilli:uuid\";");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let kind = match &ast.try_stmt(ast.top_level[0]).unwrap().kind {
StmtKind::Import { kind, .. } => kind.clone(),
other => panic!("expected import, got {other:?}"),
};
let local = match kind {
ImportKind::Namespace { local_name } => local_name,
other => panic!("expected Namespace, got {other:?}"),
};
assert_eq!(local.name, "uuid");
}
#[test]
fn parse_wildcard_namespace_import_synonym() {
let (ast, diags) = parse_str("import * as uuid from \"submilli:uuid\";");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let kind = match &ast.try_stmt(ast.top_level[0]).unwrap().kind {
StmtKind::Import { kind, .. } => kind.clone(),
other => panic!("expected import, got {other:?}"),
};
let local = match kind {
ImportKind::Namespace { local_name } => local_name,
other => panic!("expected Namespace, got {other:?}"),
};
assert_eq!(local.name, "uuid");
}
#[test]
fn parse_wildcard_namespace_import_without_as_rejected() {
let (_ast, diags) = parse_str("import * from \"submilli:uuid\";");
assert!(
diags
.iter()
.any(|d| d.message.contains("expected `as <name>`")),
"expected missing-as diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_combined_default_and_named_rejected() {
let (_ast, diags) = parse_str("import uuid, { v4 } from \"submilli:uuid\";");
assert!(
diags
.iter()
.any(|d| d.message.contains("combining default and named")),
"expected combined-import diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_side_effect_import_rejected() {
let (_ast, diags) = parse_str("import \"submilli:uuid\";");
assert!(
diags
.iter()
.any(|d| d.message.contains("side-effect-only imports")),
"expected side-effect-import diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_empty_specifier_list_rejected() {
let (_ast, diags) = parse_str("import {} from \"submilli:uuid\";");
assert!(
diags
.iter()
.any(|d| d.message.contains("empty import specifier list")),
"expected empty-specifier diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_import_inside_function_body_rejected() {
let (_ast, diags) =
parse_str("function main(): void { import { v4 } from \"submilli:uuid\"; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("must appear at the top of the file")),
"expected top-level diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_missing_from_rejected() {
let (_ast, diags) = parse_str("import { v4 } \"submilli:uuid\";");
assert!(
diags.iter().any(|d| d.message.contains("expected `from`")),
"expected missing-from diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_missing_module_specifier_rejected() {
let (_ast, diags) = parse_str("import { v4 } from;");
assert!(
diags
.iter()
.any(|d| d.message.contains("expected module specifier")),
"expected missing-module diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_export_function_marks_exported() {
let (ast, diags) = parse_str("export function main(): void {}");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let stmt = single_stmt(&ast);
assert!(
matches!(stmt.kind, crate::StmtKind::Function { .. }),
"expected Function decl, got {:?}",
stmt.kind,
);
assert_eq!(ast.exported_decls.len(), 1, "one exported decl");
assert_eq!(ast.exported_decls[0].stmt, ast.top_level[0]);
}
#[test]
fn parse_export_const_type_interface_enum_are_marked() {
for src in [
"export const X: number = 1;",
"export type Id = string;",
"export interface P { x: number; }",
"export enum E { A, B }",
] {
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected diags for {src:?}: {diags:?}");
assert_eq!(ast.top_level.len(), 1, "one decl for {src:?}");
assert_eq!(ast.exported_decls.len(), 1, "one exported decl for {src:?}");
}
}
#[test]
fn parse_export_default_is_rejected_with_focused_message() {
let (_ast, diags) = parse_str("export default function main(): void {}");
assert!(
diags
.iter()
.any(|d| d.message.contains("`export default` is not supported")),
"expected focused export-default diagnostic, got: {diags:?}",
);
assert!(
!diags.iter().any(|d| d.message.contains("expected `;`")),
"the spurious missing-semicolon error must be gone: {diags:?}",
);
}
#[test]
fn parse_export_list_parses_as_export_from() {
let (ast, diags) = parse_str("function main(): void {}\nexport { main };");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
assert_eq!(ast.top_level.len(), 2, "function + export-from");
let export = ast.try_stmt(ast.top_level[1]).unwrap();
let crate::StmtKind::ExportFrom { specs, source, .. } = &export.kind else {
panic!("expected ExportFrom, got {:?}", export.kind);
};
assert!(source.is_none(), "bare `export {{ x }}` has no source");
assert_eq!(specs.len(), 1);
assert_eq!(specs[0].imported_name.name, "main");
assert_eq!(specs[0].local_name.name, "main");
}
#[test]
fn parse_export_from_with_source_and_alias_parses() {
let (ast, diags) = parse_str("export { foo, bar as baz } from \"./util\";");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let export = ast.try_stmt(ast.top_level[0]).unwrap();
let crate::StmtKind::ExportFrom { specs, source, .. } = &export.kind else {
panic!("expected ExportFrom, got {:?}", export.kind);
};
let Some((module, _)) = source else {
panic!("expected a `from` source");
};
assert_eq!(module, "./util");
assert_eq!(specs.len(), 2);
assert_eq!(specs[1].imported_name.name, "bar");
assert_eq!(specs[1].local_name.name, "baz");
}
#[test]
fn parse_export_inside_function_body_rejected() {
let (_ast, diags) = parse_str("function main(): void { export const X: number = 1; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("must appear at the top of the file")),
"expected top-level diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_throw_with_expression() {
let (ast, diags) = parse_str(r#"throw new Error("oops");"#);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Throw { value } = stmt.kind else {
panic!("expected Throw, got {:?}", stmt.kind);
};
assert!(matches!(
ast.try_expr(value).unwrap().kind,
crate::ExprKind::New { .. }
));
}
#[test]
fn parse_new_with_dotted_callee() {
let (ast, diags) = parse_str("new Temporal.Duration({});");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::StmtKind::Expr(expr_id) = single_stmt(&ast).kind else {
panic!("expected expression statement");
};
let crate::ExprKind::New { callee, .. } = &ast.try_expr(expr_id).unwrap().kind else {
panic!("expected New");
};
let crate::ExprKind::FieldAccess { name, .. } = &ast.try_expr(*callee).unwrap().kind else {
panic!("expected FieldAccess callee");
};
assert_eq!(name.name, "Duration");
}
#[test]
fn parse_new_with_three_level_dotted_callee() {
let (ast, diags) = parse_str("new Foo.Bar.Baz();");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::StmtKind::Expr(expr_id) = single_stmt(&ast).kind else {
panic!("expected expression statement");
};
let crate::ExprKind::New { callee, .. } = &ast.try_expr(expr_id).unwrap().kind else {
panic!("expected New");
};
let crate::ExprKind::FieldAccess { receiver, name } = &ast.try_expr(*callee).unwrap().kind
else {
panic!("expected outer FieldAccess");
};
assert_eq!(name.name, "Baz");
let crate::ExprKind::FieldAccess { name: mid_name, .. } =
&ast.try_expr(*receiver).unwrap().kind
else {
panic!("expected inner FieldAccess");
};
assert_eq!(mid_name.name, "Bar");
}
#[test]
fn parse_new_with_trailing_dot_diagnoses() {
let (_ast, diags) = parse_str("new Foo.;");
assert!(
diags.iter().any(|d| d
.message
.contains("expected identifier after `.` in constructor name")),
"expected trailing-dot diagnostic, got: {diags:?}",
);
}
#[test]
fn throw_without_expression_diagnoses() {
let (_ast, diags) = parse_str("throw;");
assert!(
diags
.iter()
.any(|d| d.message == "expected expression after `throw`"),
"expected bare-throw diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_try_catch() {
let (ast, diags) = parse_str("try { } catch (e: Error) { }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Try {
body,
ref catches,
ref finally,
} = stmt.kind
else {
panic!("expected Try, got {:?}", stmt.kind);
};
assert!(matches!(
ast.try_stmt(body).unwrap().kind,
crate::StmtKind::Block(ref v) if v.is_empty()
));
let [clause] = catches.as_slice() else {
panic!("expected one catch clause, got {catches:?}");
};
assert_eq!(clause.binding.name, "e");
assert!(clause.ty.is_some());
assert!(finally.is_none());
}
#[test]
fn parse_try_finally() {
let (ast, diags) = parse_str("try { } finally { }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Try {
ref catches,
ref finally,
..
} = stmt.kind
else {
panic!("expected Try");
};
assert!(catches.is_empty());
let finally_id = finally.expect("finally block");
assert!(matches!(
ast.try_stmt(finally_id).unwrap().kind,
crate::StmtKind::Block(_)
));
}
#[test]
fn parse_try_catch_finally() {
let (ast, diags) = parse_str("try { } catch (e: Error) { } finally { }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Try {
ref catches,
ref finally,
..
} = stmt.kind
else {
panic!("expected Try");
};
assert_eq!(catches.len(), 1);
assert!(finally.is_some());
}
#[test]
fn bare_try_diagnoses() {
let (_ast, diags) = parse_str("try { }");
assert!(
diags
.iter()
.any(|d| d.message == "try requires at least one of `catch` or `finally`"),
"expected bare-try diagnostic, got: {diags:?}",
);
}
#[test]
fn parse_untyped_catch() {
let (ast, diags) = parse_str("try { } catch (e) { }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Try {
ref catches,
ref finally,
..
} = stmt.kind
else {
panic!("expected Try");
};
let [clause] = catches.as_slice() else {
panic!("expected one catch clause, got {catches:?}");
};
assert_eq!(clause.binding.name, "e");
assert!(clause.ty.is_none());
assert!(finally.is_none());
}
#[test]
fn parse_multiple_catch_clauses() {
let (ast, diags) = parse_str("try { } catch (e: HttpError) { } catch (f) { }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let stmt = single_stmt(&ast);
let crate::StmtKind::Try {
ref catches,
ref finally,
..
} = stmt.kind
else {
panic!("expected Try");
};
let [first, second] = catches.as_slice() else {
panic!("expected two catch clauses, got {catches:?}");
};
assert_eq!(first.binding.name, "e");
assert!(first.ty.is_some());
assert_eq!(second.binding.name, "f");
assert!(second.ty.is_none());
assert!(finally.is_none());
}
#[test]
fn catch_after_finally_diagnoses() {
let (_ast, diags) = parse_str("try { } finally { } catch (e: Error) { }");
assert!(
!diags.is_empty(),
"expected diagnostic for catch-after-finally"
);
}
#[test]
fn catch_missing_open_paren_diagnoses() {
let (_ast, diags) = parse_str("try { } catch e: Error) { }");
assert!(
diags
.iter()
.any(|d| d.message == "expected `(` after `catch`"),
"expected open-paren diagnostic, got: {diags:?}",
);
}
fn class_members(ast: &Ast) -> &[crate::ClassMember] {
match &single_stmt(ast).kind {
StmtKind::ClassDecl { members, .. } => members,
other => panic!("expected ClassDecl, got {other:?}"),
}
}
#[test]
fn parse_animal_class() {
let src = r#"
class Animal {
name: string;
private sound: string;
readonly species: string;
constructor(name: string, sound: string, species: string) {
this.name = name;
this.sound = sound;
this.species = species;
}
speak(): string {
return this.name + " says " + this.sound;
}
}
"#;
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let StmtKind::ClassDecl {
name,
generics,
extends,
implements,
members,
..
} = &single_stmt(&ast).kind
else {
panic!("expected ClassDecl");
};
assert_eq!(name.name, "Animal");
assert!(generics.is_empty());
assert!(extends.is_none());
assert!(implements.is_empty());
assert_eq!(members.len(), 5);
let crate::ClassMember::Field {
name, modifiers, ..
} = &members[0]
else {
panic!("member 0 should be a field");
};
assert_eq!(name.name, "name");
assert_eq!(modifiers.visibility, crate::Visibility::Public);
assert!(modifiers.readonly.is_none());
let crate::ClassMember::Field { modifiers, .. } = &members[1] else {
panic!("member 1 should be a field");
};
assert_eq!(modifiers.visibility, crate::Visibility::Private);
let crate::ClassMember::Field { modifiers, .. } = &members[2] else {
panic!("member 2 should be a field");
};
assert!(modifiers.readonly.is_some());
assert_eq!(modifiers.visibility, crate::Visibility::Public);
assert!(matches!(
&members[3],
crate::ClassMember::Constructor { .. }
));
let crate::ClassMember::Method { name, .. } = &members[4] else {
panic!("member 4 should be a method");
};
assert_eq!(name.name, "speak");
}
#[test]
fn parse_dog_class_extends_super_and_void_method() {
let src = r#"
class Dog extends Animal {
private tricks: string[];
constructor(name: string) {
super(name, "woof", "canis familiaris");
this.tricks = [];
}
learn(trick: string): void {
this.tricks.push(trick);
}
}
"#;
let (ast, diags) = parse_str(src);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let StmtKind::ClassDecl {
extends, members, ..
} = &single_stmt(&ast).kind
else {
panic!("expected ClassDecl");
};
assert!(extends.is_some(), "Dog should extend Animal");
assert_eq!(members.len(), 3);
assert!(matches!(&members[0], crate::ClassMember::Field { .. }));
assert!(matches!(
&members[1],
crate::ClassMember::Constructor { .. }
));
assert!(matches!(&members[2], crate::ClassMember::Method { .. }));
}
#[test]
fn parse_class_implements_list() {
let (ast, diags) = parse_str("class C implements I, J { run(): void {} }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let StmtKind::ClassDecl { implements, .. } = &single_stmt(&ast).kind else {
panic!("expected ClassDecl");
};
assert_eq!(implements.len(), 2);
}
#[test]
fn parse_class_extends_and_implements() {
let (ast, diags) = parse_str("class C extends B implements I { run(): void {} }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let StmtKind::ClassDecl {
extends,
implements,
..
} = &single_stmt(&ast).kind
else {
panic!("expected ClassDecl");
};
assert!(extends.is_some());
assert_eq!(implements.len(), 1);
}
#[test]
fn parse_class_generics_optional_field_and_initializer() {
let (ast, diags) = parse_str("class Box<T> { value?: T; items: T[] = []; }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let StmtKind::ClassDecl {
generics, members, ..
} = &single_stmt(&ast).kind
else {
panic!("expected ClassDecl");
};
assert_eq!(generics.len(), 1);
assert_eq!(generics[0].name, "T");
let crate::ClassMember::Field {
optional,
initializer,
..
} = &members[0]
else {
panic!("member 0 should be a field");
};
assert!(*optional);
assert!(initializer.is_none());
let crate::ClassMember::Field { initializer, .. } = &members[1] else {
panic!("member 1 should be a field");
};
assert!(initializer.is_some());
}
#[test]
fn export_class_is_marked() {
let (ast, diags) = parse_str("export class Foo {}");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
assert_eq!(ast.top_level.len(), 1);
assert_eq!(ast.exported_decls.len(), 1);
assert!(matches!(
&single_stmt(&ast).kind,
StmtKind::ClassDecl { .. }
));
}
#[test]
fn parameterless_index_signature_is_reported_from_its_member_start() {
for (source, expected) in [
("interface P { readonly []: number }", "readonly []"),
("let p: { readonly []: number } = {};", "readonly []"),
("let q: { []: number } = {};", "[]"),
] {
let (_ast, diags) = parse_str(source);
let diag = diags
.iter()
.find(|d| d.message.contains("must declare exactly one parameter"))
.unwrap_or_else(|| panic!("no diagnostic for {source}"));
let reported = &source[diag.span.start as usize..diag.span.end as usize];
assert_eq!(reported, expected, "{source}");
}
}
#[test]
fn unterminated_object_type_expects_closing_brace() {
let (_ast, diags) = parse_str("let p: { a: number\n");
assert!(
diags.iter().any(|d| d.message == "expected `}`"),
"{diags:?}"
);
}
#[test]
fn conflicting_visibility_modifiers_diagnose() {
let (_ast, diags) = parse_str("class C { public private x: number; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("at most one visibility modifier")),
"got: {diags:?}",
);
}
#[test]
fn duplicate_readonly_modifier_diagnoses() {
let (_ast, diags) = parse_str("class C { readonly readonly x: number; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("duplicate `readonly`")),
"got: {diags:?}",
);
}
#[test]
fn field_named_like_a_modifier_is_allowed() {
let (ast, diags) = parse_str("class C { private: number; }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::ClassMember::Field {
name, modifiers, ..
} = &class_members(&ast)[0]
else {
panic!("expected a field");
};
assert_eq!(name.name, "private");
assert_eq!(modifiers.visibility, crate::Visibility::Public);
}
#[test]
fn method_named_get_is_allowed() {
let (ast, diags) = parse_str("class C { get(): number { return 1; } }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::ClassMember::Method { name, .. } = &class_members(&ast)[0] else {
panic!("expected a method");
};
assert_eq!(name.name, "get");
}
#[test]
fn static_method_parses() {
let (ast, diags) = parse_str("class C { static f(): number { return 1; } }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::ClassMember::Method {
name, modifiers, ..
} = &class_members(&ast)[0]
else {
panic!("expected a method");
};
assert_eq!(name.name, "f");
assert!(modifiers.static_span.is_some());
}
#[test]
fn static_readonly_field_parses() {
let (ast, diags) = parse_str("class C { static readonly x: number = 1; }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::ClassMember::Field {
name,
modifiers,
initializer,
..
} = &class_members(&ast)[0]
else {
panic!("expected a field");
};
assert_eq!(name.name, "x");
assert!(modifiers.static_span.is_some());
assert!(modifiers.readonly.is_some());
assert!(initializer.is_some());
}
#[test]
fn private_static_method_parses() {
let (ast, diags) = parse_str("class C { private static f(): void {} }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::ClassMember::Method { modifiers, .. } = &class_members(&ast)[0] else {
panic!("expected a method");
};
assert_eq!(modifiers.visibility, crate::Visibility::Private);
assert!(modifiers.static_span.is_some());
}
#[test]
fn method_named_static_is_allowed() {
let (ast, diags) = parse_str("class C { static(): number { return 1; } }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let crate::ClassMember::Method {
name, modifiers, ..
} = &class_members(&ast)[0]
else {
panic!("expected a method");
};
assert_eq!(name.name, "static");
assert!(modifiers.static_span.is_none());
}
#[test]
fn visibility_after_static_diagnoses() {
let (_ast, diags) = parse_str("class C { static private f(): void {} }");
assert!(
diags
.iter()
.any(|d| d.message.contains("`private` must come before `static`")),
"got: {diags:?}",
);
}
#[test]
fn duplicate_static_modifier_diagnoses() {
let (_ast, diags) = parse_str("class C { static static f(): void {} }");
assert!(
diags
.iter()
.any(|d| d.message.contains("duplicate `static` modifier")),
"got: {diags:?}",
);
}
#[test]
fn static_accessor_is_rejected() {
let (_ast, diags) = parse_str("class C { static get x(): number { return 1; } }");
assert!(
diags
.iter()
.any(|d| d.message.contains("static accessors are not supported")),
"got: {diags:?}",
);
}
#[test]
fn static_constructor_is_rejected() {
let (_ast, diags) = parse_str("class C { static constructor() {} }");
assert!(
diags
.iter()
.any(|d| d.message.contains("a constructor cannot be `static`")),
"got: {diags:?}",
);
}
#[test]
fn optional_static_field_is_rejected() {
let (_ast, diags) = parse_str("class C { static x?: number; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("a static field cannot be optional")),
"got: {diags:?}",
);
}
#[test]
fn protected_member_is_rejected() {
let (_ast, diags) = parse_str("class C { protected x: number; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("`protected` is not supported")),
"got: {diags:?}",
);
}
#[test]
fn abstract_member_is_rejected() {
let (_ast, diags) = parse_str("class C { abstract run(): void; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("abstract classes are not supported")),
"got: {diags:?}",
);
}
#[test]
fn getter_parses_as_accessor() {
let (ast, diags) = parse_str("class C { get value(): number { return 1; } }");
assert!(diags.is_empty(), "got: {diags:?}");
let crate::ClassMember::Accessor { kind, name, .. } = &class_members(&ast)[0] else {
panic!("expected an accessor member");
};
assert_eq!(*kind, crate::AccessorKind::Get);
assert_eq!(name.name, "value");
}
#[test]
fn getter_without_return_type_is_rejected() {
let (ast, diags) = parse_str("class C { get value() { return 1; } }");
assert_eq!(diags.len(), 1, "got: {diags:?}");
assert_eq!(diags[0].message, "expected `:` and return type");
assert_eq!(diags[0].help, vec!["get name(): T { … }".to_string()]);
assert!(
matches!(
&class_members(&ast)[0],
crate::ClassMember::Accessor {
return_type: None,
..
}
),
"the getter is still recovered as a member"
);
}
#[test]
fn setter_parses_as_accessor() {
let (ast, diags) = parse_str("class C { set value(v: number) {} }");
assert!(diags.is_empty(), "got: {diags:?}");
let crate::ClassMember::Accessor { kind, param, .. } = &class_members(&ast)[0] else {
panic!("expected an accessor member");
};
assert_eq!(*kind, crate::AccessorKind::Set);
assert!(param.is_some(), "setter should have a parameter");
}
#[test]
fn param_property_modifier_outside_constructor_is_rejected() {
let (_ast, diags) = parse_str("function f(public x: number): void {}");
assert!(
diags.iter().any(|d| d
.message
.contains("parameter properties are only allowed in a constructor")),
"got: {diags:?}",
);
}
#[test]
fn constructor_return_type_is_rejected() {
let (_ast, diags) = parse_str("class C { constructor(): void {} }");
assert!(
diags.iter().any(|d| d
.message
.contains("constructor cannot declare a return type")),
"got: {diags:?}",
);
}
#[test]
fn field_initializer_without_type_is_rejected() {
let (_ast, diags) = parse_str("class C { x = 1; }");
assert!(
diags
.iter()
.any(|d| d.message.contains("class fields require a type annotation")),
"got: {diags:?}",
);
}
#[test]
fn this_outside_method_is_left_to_typechecker() {
let (_ast, diags) = parse_str("function f(): string { return this.name; }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
}
#[test]
fn super_outside_method_is_rejected() {
let (_ast, diags) = parse_str("function f(): void { super(1); }");
assert!(
diags.iter().any(|d| d
.message
.contains("`super` is only valid inside a class method or constructor body")),
"got: {diags:?}",
);
}
#[test]
fn this_inside_method_is_accepted_by_parser() {
let (_ast, diags) = parse_str("class C { x: number; read(): number { return this.x; } }");
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
}
#[test]
fn parse_void_expression_preserves_operand_and_precedence() {
let (ast, diags) = parse_str("void effect() === undefined;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let expression = expr_of_single_stmt(&ast);
let crate::ExprKind::Binary { lhs, rhs, .. } = expression.kind else {
panic!("expected equality")
};
let crate::ExprKind::Void { operand } = ast.try_expr(lhs).unwrap().kind else {
panic!("expected void")
};
assert!(matches!(
ast.try_expr(operand).unwrap().kind,
crate::ExprKind::Call { .. }
));
assert!(matches!(
ast.try_expr(rhs).unwrap().kind,
crate::ExprKind::Identifier(_)
));
}
#[test]
fn parse_void_newline_and_object_operands() {
for source in [
"void\n0;",
"void { value: 1 };",
"void\n{ value: 1 };",
"void /x/;",
"void void 0;",
] {
let (_, diags) = parse_str(source);
assert!(diags.is_empty(), "{source}: {diags:?}");
}
let (_, diags) = parse_str("void; 0;");
assert!(
!diags.is_empty(),
"an explicit semicolon cannot split void from its operand"
);
}
#[test]
fn parse_optional_parameters_and_defaults_in_callable_forms() {
for source in [
"function f(x = 1): number { return x; }",
"function f(x: number = 1, y: number): number { return y; }",
"function f({ x } = { x: 1 }): number { return x; }",
"const f = (x?: number) => x;",
"const f = (x: number = effect()) => x;",
"const f = function(x: number = effect()) { return x; };",
"const f = { m(x: number = effect()): number { return x; } };",
"const f = ([x = 1]: number[] = values) => x;",
"let f: (x?: number, ...rest: string[]) => void = value;",
"let f: { method?(x?: number): void } = value;",
"function f(x: number = 1, y?: number): void {}",
] {
let (_, diags) = parse_str(source);
assert!(diags.is_empty(), "{source}: {diags:?}");
}
}
#[test]
fn parse_invalid_optional_parameters() {
for source in [
"function f(x?: number, y: number): void {}",
"const f = (x?: number, y: number) => y;",
"let f: (x?: number, y: number) => void = value;",
"function f(...x?: number[]): void {}",
"function f(x?: number = 1): void {}",
"const f = (x?: number = 1) => x;",
"function f({x}?: T): void {}",
] {
let (_, diags) = parse_str(source);
assert!(!diags.is_empty(), "expected diagnostic for {source}");
}
}
#[test]
fn parse_optional_tuple_forms_and_order() {
for source in [
"let x: [number, string?] = value;",
"let x: [x?: number, y?: string] = value;",
"let x: readonly [(number | null)?, string?] = value;",
] {
let (_, diags) = parse_str(source);
assert!(diags.is_empty(), "{source}: {diags:?}");
}
for source in [
"let x: [number?, string] = value;",
"let x: [x: number?] = value;",
] {
let (_, diags) = parse_str(source);
assert!(!diags.is_empty(), "expected diagnostic for {source}");
}
}
#[test]
fn parse_array_pattern_defaults_preserve_holes() {
let (ast, diags) = parse_str("const [, a = effect(), b] = values;");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let crate::StmtKind::ConstPattern { ref binding, .. } = single_stmt(&ast).kind else {
panic!("expected pattern")
};
let crate::Binding::Array {
elems, defaults, ..
} = binding
else {
panic!("expected array")
};
assert_eq!(elems.len(), 3);
assert_eq!(defaults.len(), elems.len());
assert!(elems[0].is_none());
assert!(defaults[0].is_none());
assert!(defaults[1].is_some());
assert!(defaults[2].is_none());
}
#[test]
fn parse_optional_class_method_forms() {
let (ast, diags) =
parse_str("class C { declared?(x?: number): void; implemented?(): void\n{} }");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
let members = class_members(&ast);
assert!(matches!(
&members[0],
crate::ClassMember::Field {
optional: true,
initializer: None,
ty: crate::TypeAnnotation {
kind: crate::TypeAnnotationKind::Function { .. },
..
},
..
}
));
assert!(matches!(
&members[1],
crate::ClassMember::Method { optional: true, .. }
));
}
#[test]
fn defaults_retain_their_callable_receiver_scope() {
for source in [
"class C { value: number = 1; method(value: number = this.value): number { return value; } }",
"class C { constructor(value: number = this.value) {} value: number = 1; }",
"const f = function(this: { value: number }, value: number = this.value): number { return value; };",
] {
let (ast, diagnostics) = parse_str(source);
assert!(diagnostics.is_empty(), "{source}: {diagnostics:?}");
assert!(
ast.source_expressions()
.iter()
.any(|expr| matches!(expr.kind, ExprKind::This))
);
assert!(
!ast.source_expressions()
.iter()
.any(|expr| matches!(expr.kind, ExprKind::ThisOutsideReceiver))
);
}
}
}