use crate::{Diagnostic, FileId, Lexer, Span, Token, TokenKind};
#[derive(Clone, Copy, PartialEq)]
enum BraceKind {
Block,
Value,
MemberList,
}
struct BraceFrame {
kind: BraceKind,
bracket_base: u32,
}
pub struct Asi<'a> {
lexer: Lexer<'a>,
buffered: Option<Token>,
lookahead: std::collections::VecDeque<Token>,
last_emitted: Option<TokenKind>,
bracket_depth: u32,
braces: Vec<BraceFrame>,
paren_is_header: Vec<bool>,
closed_header_paren: bool,
closed_block_brace: bool,
cast_angle_depth: u32,
closed_cast_angle: bool,
in_decl_header: bool,
last_is_member_name: bool,
in_case_label: bool,
after_case_label_colon: bool,
brace_opens_specifier_list: bool,
}
impl<'a> Asi<'a> {
pub fn new(source: &'a str, file: FileId) -> Self {
Self {
lexer: Lexer::new(source, file),
buffered: None,
lookahead: std::collections::VecDeque::new(),
last_emitted: None,
bracket_depth: 0,
braces: Vec::new(),
paren_is_header: Vec::new(),
closed_header_paren: false,
closed_block_brace: false,
last_is_member_name: false,
in_case_label: false,
after_case_label_colon: false,
in_decl_header: false,
brace_opens_specifier_list: false,
cast_angle_depth: 0,
closed_cast_angle: false,
}
}
pub fn next_token(&mut self) -> Token {
if let Some(tok) = self.buffered.take() {
self.track(&tok.kind);
return tok;
}
let mut pending_newline = false;
loop {
let tok = self
.lookahead
.pop_front()
.unwrap_or_else(|| self.lexer.next_token());
match tok.kind {
TokenKind::Newline => {
pending_newline = true;
}
TokenKind::Eof => {
if self.needs_semi() {
return self.insert_semicolon_before(tok);
}
self.track(&tok.kind);
return tok;
}
_ => {
let boundary = match self.brace_closed_by(&tok.kind) {
Some(BraceKind::Value | BraceKind::MemberList) => false,
Some(BraceKind::Block) => true,
None => pending_newline,
};
if boundary
&& !self.inside_brackets()
&& !self.can_continue_here(&tok.kind)
&& self.needs_semi()
{
return self.insert_semicolon_before(tok);
}
self.track(&tok.kind);
return tok;
}
}
}
}
fn can_continue_here(&mut self, kind: &TokenKind) -> bool {
if matches!(
kind,
TokenKind::Else
| TokenKind::Catch
| TokenKind::Finally
| TokenKind::Extends
| TokenKind::Implements
) {
loop {
let next = self.lexer.next_token();
let newline = matches!(next.kind, TokenKind::Newline);
let member = matches!(next.kind, TokenKind::Colon | TokenKind::Question);
self.lookahead.push_back(next);
if !newline {
return !member;
}
}
}
can_continue(kind)
}
pub fn finish(self) -> Result<Vec<Diagnostic>, crate::compiler_error::CompileError> {
self.lexer.finish()
}
pub fn into_diagnostics(self) -> Vec<Diagnostic> {
self.lexer.into_diagnostics()
}
fn insert_semicolon_before(&mut self, tok: Token) -> Token {
let pos = tok.span.start;
let file = tok.span.file;
self.buffered = Some(tok);
let semi = Token::new(
TokenKind::Semicolon,
Span {
file,
start: pos,
end: pos,
},
);
self.track(&semi.kind);
semi
}
fn needs_semi(&self) -> bool {
if self.closed_header_paren || self.in_decl_header {
return false;
}
match &self.last_emitted {
None => false,
Some(kind) => self.last_is_member_name || !cannot_end_statement(kind),
}
}
fn at_statement_start(&self) -> bool {
matches!(
self.last_emitted,
None | Some(
TokenKind::LeftBrace
| TokenKind::RightBrace
| TokenKind::Semicolon
| TokenKind::Colon
)
)
}
fn brace_closed_by(&self, next: &TokenKind) -> Option<BraceKind> {
if !matches!(next, TokenKind::RightBrace) {
return None;
}
self.braces.last().map(|frame| frame.kind)
}
fn inside_brackets(&self) -> bool {
self.bracket_depth > self.braces.last().map_or(0, |frame| frame.bracket_base)
}
fn classify_brace(&self) -> BraceKind {
if self.brace_opens_specifier_list {
return BraceKind::Value;
}
if self.after_case_label_colon {
return BraceKind::Block;
}
if self.closed_cast_angle {
return BraceKind::Value;
}
match &self.last_emitted {
Some(TokenKind::Colon) => BraceKind::MemberList,
Some(kind) if opens_value_brace(kind) => BraceKind::Value,
_ => BraceKind::Block,
}
}
fn push_brace(&mut self) {
let kind = self.classify_brace();
let bracket_base = match kind {
BraceKind::Block | BraceKind::MemberList => self.bracket_depth,
BraceKind::Value => self.braces.last().map_or(0, |frame| frame.bracket_base),
};
self.braces.push(BraceFrame { kind, bracket_base });
}
fn track(&mut self, kind: &TokenKind) {
self.correct_bang_context(kind);
let closed_block = self.brace_closed_by(kind) == Some(BraceKind::Block);
let closed_header = self.track_brackets(kind);
let closed_cast = self.track_cast_angles(kind);
self.after_case_label_colon = self.next_after_case_label_colon(kind);
self.in_case_label = self.next_in_case_label(kind);
self.in_decl_header = self.next_in_decl_header(kind);
self.brace_opens_specifier_list = self.next_brace_opens_specifier_list(kind);
self.last_is_member_name = self.next_last_is_member_name();
self.closed_header_paren = closed_header;
self.closed_block_brace = closed_block;
self.closed_cast_angle = closed_cast;
self.last_emitted = Some(kind.clone());
}
fn track_cast_angles(&mut self, kind: &TokenKind) -> bool {
match kind {
TokenKind::LessThan if self.cast_angle_depth > 0 => self.cast_angle_depth += 1,
TokenKind::LessThan
if self.expects_operand()
&& !matches!(self.last_emitted, Some(TokenKind::LessThan)) =>
{
self.cast_angle_depth = 1;
}
TokenKind::GreaterThan if self.cast_angle_depth > 0 => {
self.cast_angle_depth -= 1;
return self.cast_angle_depth == 0;
}
_ => {}
}
false
}
fn expects_operand(&self) -> bool {
self.at_statement_start()
|| self
.last_emitted
.as_ref()
.is_some_and(|kind| opens_value_brace(kind) || matches!(kind, TokenKind::Arrow))
}
fn correct_bang_context(&mut self, kind: &TokenKind) {
if !matches!(kind, TokenKind::Bang) {
return;
}
if self.last_is_member_name {
self.lexer.set_bang_is_postfix(true);
} else if self.closed_header_paren
|| self.closed_block_brace
|| matches!(self.last_emitted, Some(TokenKind::Semicolon))
{
self.lexer.set_bang_is_postfix(false);
}
}
fn track_brackets(&mut self, kind: &TokenKind) -> bool {
match kind {
TokenKind::LeftParen => {
self.bracket_depth += 1;
self.paren_is_header.push(self.opens_control_header());
}
TokenKind::LeftBracket | TokenKind::TemplateHead(_) => self.bracket_depth += 1,
TokenKind::RightParen => {
self.bracket_depth = self.bracket_depth.saturating_sub(1);
return self.paren_is_header.pop().unwrap_or(false);
}
TokenKind::RightBracket | TokenKind::TemplateTail(_) => {
self.bracket_depth = self.bracket_depth.saturating_sub(1);
}
TokenKind::LeftBrace => self.push_brace(),
TokenKind::RightBrace => {
self.braces.pop();
}
_ => {}
}
false
}
fn next_after_case_label_colon(&self, kind: &TokenKind) -> bool {
matches!(kind, TokenKind::Colon) && self.in_case_label
}
fn next_in_case_label(&self, kind: &TokenKind) -> bool {
match kind {
TokenKind::Case | TokenKind::Default => {
self.at_statement_start()
&& self.braces.last().map(|frame| frame.kind) == Some(BraceKind::Block)
}
TokenKind::Colon
| TokenKind::Semicolon
| TokenKind::Comma
| TokenKind::LeftBrace
| TokenKind::RightBrace
| TokenKind::LeftParen
| TokenKind::RightParen => false,
_ => self.in_case_label,
}
}
fn next_in_decl_header(&self, kind: &TokenKind) -> bool {
match kind {
TokenKind::Class | TokenKind::Interface | TokenKind::Enum => {
self.at_statement_start() || matches!(self.last_emitted, Some(TokenKind::Export))
}
TokenKind::LeftBrace
| TokenKind::RightBrace
| TokenKind::Semicolon
| TokenKind::Colon
| TokenKind::Comma => false,
_ => self.in_decl_header,
}
}
fn next_brace_opens_specifier_list(&self, kind: &TokenKind) -> bool {
match kind {
TokenKind::Import | TokenKind::Export => true,
TokenKind::Identifier => self.brace_opens_specifier_list,
_ => false,
}
}
fn next_last_is_member_name(&self) -> bool {
matches!(
self.last_emitted,
Some(TokenKind::Dot | TokenKind::QuestionDot)
)
}
fn opens_control_header(&self) -> bool {
!self.last_is_member_name
&& matches!(
self.last_emitted,
Some(
TokenKind::If
| TokenKind::While
| TokenKind::For
| TokenKind::Switch
| TokenKind::Catch
)
)
}
}
fn cannot_end_statement(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::Semicolon
| TokenKind::LeftBrace
| TokenKind::LeftParen
| TokenKind::LeftBracket
| TokenKind::Comma
) || awaits_operand(kind)
|| awaits_body(kind)
}
fn cannot_end_expression(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::Equals
| TokenKind::PlusEquals
| TokenKind::MinusEquals
| TokenKind::StarEquals
| TokenKind::SlashEquals
| TokenKind::PercentEquals
| TokenKind::StarStarEquals
| TokenKind::Plus
| TokenKind::Minus
| TokenKind::Star
| TokenKind::Slash
| TokenKind::Percent
| TokenKind::StarStar
| TokenKind::EqEqEq
| TokenKind::EqEq
| TokenKind::BangEqEq
| TokenKind::BangEq
| TokenKind::LessThan
| TokenKind::GreaterThan
| TokenKind::LessEquals
| TokenKind::GreaterEquals
| TokenKind::AmpAmp
| TokenKind::Amp
| TokenKind::AmpEquals
| TokenKind::PipeEquals
| TokenKind::Caret
| TokenKind::CaretEquals
| TokenKind::Pipe
| TokenKind::PipePipe
| TokenKind::Question
| TokenKind::QuestionQuestion
| TokenKind::Colon
)
}
fn awaits_operand(kind: &TokenKind) -> bool {
cannot_end_expression(kind)
|| matches!(
kind,
TokenKind::Arrow
| TokenKind::Tilde
| TokenKind::Extends
| TokenKind::Implements
)
}
fn awaits_body(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::Try | TokenKind::Else | TokenKind::Do | TokenKind::Finally | TokenKind::Catch
)
}
fn opens_value_brace(kind: &TokenKind) -> bool {
if matches!(kind, TokenKind::GreaterThan) {
return false;
}
cannot_end_expression(kind)
|| matches!(
kind,
TokenKind::LeftParen
| TokenKind::Tilde
| TokenKind::LeftBracket
| TokenKind::Comma
| TokenKind::DotDotDot
| TokenKind::Return
| TokenKind::Throw
| TokenKind::Const
| TokenKind::Let
| TokenKind::TemplateHead(_)
| TokenKind::TemplateMiddle(_)
)
}
fn can_continue(kind: &TokenKind) -> bool {
if matches!(kind, TokenKind::Tilde) {
return false;
}
awaits_operand(kind)
|| matches!(
kind,
TokenKind::Dot
| TokenKind::QuestionDot
| TokenKind::LeftParen
| TokenKind::LeftBracket
| TokenKind::Else
| TokenKind::Catch
| TokenKind::Finally
| TokenKind::Extends
| TokenKind::Implements
| TokenKind::TemplateMiddle(_)
| TokenKind::TemplateTail(_)
)
}
#[cfg(test)]
mod tests {
use super::Asi;
use crate::TokenKind;
fn asi_kinds(source: &str) -> Vec<TokenKind> {
let mut asi = Asi::new(source, crate::FileId(0));
let mut kinds = Vec::new();
loop {
let tok = asi.next_token();
let is_eof = tok.kind == TokenKind::Eof;
kinds.push(tok.kind);
if is_eof {
break;
}
}
let diags = asi.into_diagnostics();
assert!(
diags.is_empty(),
"unexpected diagnostics for {source:?}: {diags:?}"
);
kinds
}
#[test]
fn strips_all_newlines() {
let kinds = asi_kinds("a\nb\n");
assert!(!kinds.iter().any(|k| matches!(k, TokenKind::Newline)));
}
#[test]
fn inserts_semicolon_between_statements() {
assert_eq!(
asi_kinds("a\nb"),
vec![
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn newline_before_as_splits_statement() {
assert_eq!(
asi_kinds("a\nas(b)"),
vec![
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Identifier,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn inserts_semicolon_before_close_brace() {
assert_eq!(
asi_kinds("{ a\n}"),
vec![
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn empty_block_no_semicolon_inside() {
assert_eq!(
asi_kinds("{\n}"),
vec![
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn does_not_double_semicolon() {
assert_eq!(
asi_kinds("a;\nb;\n"),
vec![
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn continues_across_newline_with_infix_operator() {
assert_eq!(
asi_kinds("a = b\n+ c"),
vec![
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::Identifier,
TokenKind::Plus,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn continues_across_newline_with_dot() {
assert_eq!(
asi_kinds("foo\n.bar"),
vec![
TokenKind::Identifier,
TokenKind::Dot,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn inserts_semicolon_at_eof() {
assert_eq!(
asi_kinds("a"),
vec![TokenKind::Identifier, TokenKind::Semicolon, TokenKind::Eof,]
);
}
#[test]
fn eof_with_trailing_semicolon_no_double() {
assert_eq!(
asi_kinds("a;"),
vec![TokenKind::Identifier, TokenKind::Semicolon, TokenKind::Eof,]
);
}
#[test]
fn no_semicolon_inside_parens() {
assert_eq!(
asi_kinds("foo(\n a,\n b\n)"),
vec![
TokenKind::Identifier,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::Comma,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_if_header_on_next_line() {
assert_eq!(
asi_kinds("if (a)\nb"),
vec![
TokenKind::If,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_while_header_on_next_line() {
assert_eq!(
asi_kinds("while (a)\nb"),
vec![
TokenKind::While,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn header_paren_only_suppresses_its_own_close() {
assert_eq!(
asi_kinds("if ((a))\nb"),
vec![
TokenKind::If,
TokenKind::LeftParen,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::RightParen,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn call_close_paren_still_inserts_semicolon() {
assert_eq!(
asi_kinds("f()\ng()"),
vec![
TokenKind::Identifier,
TokenKind::LeftParen,
TokenKind::RightParen,
TokenKind::Semicolon,
TokenKind::Identifier,
TokenKind::LeftParen,
TokenKind::RightParen,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_inside_brackets() {
assert_eq!(
asi_kinds("[\n1,\n2\n]"),
vec![
TokenKind::LeftBracket,
TokenKind::NumberLiteral(1.0),
TokenKind::Comma,
TokenKind::NumberLiteral(2.0),
TokenKind::RightBracket,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn else_continues_if_statement() {
assert_eq!(
asi_kinds("if (c) { }\nelse { }"),
vec![
TokenKind::If,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Else,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn catch_continues_try_statement() {
assert_eq!(
asi_kinds("try { }\ncatch (e) { }"),
vec![
TokenKind::Try,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Catch,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn finally_continues_try_statement() {
assert_eq!(
asi_kinds("try { }\nfinally { }"),
vec![
TokenKind::Try,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Finally,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_arrow() {
assert_eq!(
asi_kinds("const f = (x: number): number =>\nx + 1"),
vec![
TokenKind::Const,
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::LeftParen,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::Identifier,
TokenKind::RightParen,
TokenKind::Colon,
TokenKind::Identifier,
TokenKind::Arrow,
TokenKind::Identifier,
TokenKind::Plus,
TokenKind::NumberLiteral(1.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn arrow_block_body_still_gets_terminator() {
assert_eq!(
asi_kinds("const f = () => {\na\n}"),
vec![
TokenKind::Const,
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::LeftParen,
TokenKind::RightParen,
TokenKind::Arrow,
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn extends_continues_class_header() {
assert_eq!(
asi_kinds("class S\nextends B { }"),
vec![
TokenKind::Class,
TokenKind::Identifier,
TokenKind::Extends,
TokenKind::Identifier,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_extends() {
assert_eq!(
asi_kinds("class S extends\nB { }"),
vec![
TokenKind::Class,
TokenKind::Identifier,
TokenKind::Extends,
TokenKind::Identifier,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn implements_continues_class_header() {
assert_eq!(
asi_kinds("class S\nimplements I { }"),
vec![
TokenKind::Class,
TokenKind::Identifier,
TokenKind::Implements,
TokenKind::Identifier,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_implements() {
assert_eq!(
asi_kinds("class S implements\nI { }"),
vec![
TokenKind::Class,
TokenKind::Identifier,
TokenKind::Implements,
TokenKind::Identifier,
TokenKind::LeftBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn optional_chain_continues_across_newline() {
assert_eq!(
asi_kinds("foo\n?.bar"),
vec![
TokenKind::Identifier,
TokenKind::QuestionDot,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn empty_input() {
assert_eq!(asi_kinds(""), vec![TokenKind::Eof]);
}
#[test]
fn leading_newlines_no_semicolon() {
assert_eq!(
asi_kinds("\n\na"),
vec![TokenKind::Identifier, TokenKind::Semicolon, TokenKind::Eof,]
);
}
#[test]
fn no_semicolon_in_multiline_object_literal() {
assert_eq!(
asi_kinds("x = {\na: 1,\nb: 2\n}"),
vec![
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::NumberLiteral(1.0),
TokenKind::Comma,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::NumberLiteral(2.0),
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_in_multiline_object_literal_trailing_comma() {
assert_eq!(
asi_kinds("x = {\na: 1,\n}"),
vec![
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::NumberLiteral(1.0),
TokenKind::Comma,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn block_brace_still_gets_terminator() {
assert_eq!(
asi_kinds("{\na\n}"),
vec![
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn type_literal_members_get_separators() {
assert_eq!(
asi_kinds("let p: {\na: number\nb: number\n}"),
vec![
TokenKind::Let,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::Identifier,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn nested_object_value_suppresses_inner_close() {
assert_eq!(
asi_kinds("x = {\nouter: {\ninner: 1\n}\n}"),
vec![
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Colon,
TokenKind::NumberLiteral(1.0),
TokenKind::RightBrace,
TokenKind::RightBrace,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn multiline_destructuring_pattern_no_semicolon() {
assert_eq!(
asi_kinds("const {\na,\nb\n} = p"),
vec![
TokenKind::Const,
TokenKind::LeftBrace,
TokenKind::Identifier,
TokenKind::Comma,
TokenKind::Identifier,
TokenKind::RightBrace,
TokenKind::Equals,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_equals() {
assert_eq!(
asi_kinds("const x =\n5"),
vec![
TokenKind::Const,
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::NumberLiteral(5.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_binary_operator() {
assert_eq!(
asi_kinds("const x = 1 +\n2"),
vec![
TokenKind::Const,
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::NumberLiteral(1.0),
TokenKind::Plus,
TokenKind::NumberLiteral(2.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_assignment_reassign() {
assert_eq!(
asi_kinds("x =\n5"),
vec![
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::NumberLiteral(5.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_compound_assignment() {
assert_eq!(
asi_kinds("x +=\n5"),
vec![
TokenKind::Identifier,
TokenKind::PlusEquals,
TokenKind::NumberLiteral(5.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_logical_operator() {
assert_eq!(
asi_kinds("true &&\nfalse"),
vec![
TokenKind::BooleanLiteral(true),
TokenKind::AmpAmp,
TokenKind::BooleanLiteral(false),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_nullish_coalescing() {
assert_eq!(
asi_kinds("a ??\nb"),
vec![
TokenKind::Identifier,
TokenKind::QuestionQuestion,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_before_leading_ternary() {
assert_eq!(
asi_kinds("cond\n? 1 : 2"),
vec![
TokenKind::Identifier,
TokenKind::Question,
TokenKind::NumberLiteral(1.0),
TokenKind::Colon,
TokenKind::NumberLiteral(2.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_after_trailing_ternary_question_and_colon() {
assert_eq!(
asi_kinds("cond ?\n1 :\n2"),
vec![
TokenKind::Identifier,
TokenKind::Question,
TokenKind::NumberLiteral(1.0),
TokenKind::Colon,
TokenKind::NumberLiteral(2.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn no_semicolon_in_multiline_operator_chain() {
assert_eq!(
asi_kinds("const x = a +\nb +\nc"),
vec![
TokenKind::Const,
TokenKind::Identifier,
TokenKind::Equals,
TokenKind::Identifier,
TokenKind::Plus,
TokenKind::Identifier,
TokenKind::Plus,
TokenKind::Identifier,
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
#[test]
fn return_keeps_restricted_production() {
assert_eq!(
asi_kinds("return\n5"),
vec![
TokenKind::Return,
TokenKind::Semicolon,
TokenKind::NumberLiteral(5.0),
TokenKind::Semicolon,
TokenKind::Eof,
]
);
}
fn asi_marked(source: &str) -> String {
let mut asi = Asi::new(source, crate::FileId(0));
let mut inserted: Vec<usize> = Vec::new();
loop {
let tok = asi.next_token();
if tok.kind == TokenKind::Eof {
break;
}
if tok.kind == TokenKind::Semicolon && tok.span.start == tok.span.end {
inserted.push(tok.span.start as usize);
}
}
let mut out = String::new();
let mut last = 0;
for at in inserted {
out.push_str(&source[last..at]);
out.push_str("<;>");
last = at;
}
out.push_str(&source[last..]);
out
}
#[test]
fn clause_keyword_before_its_brace_gets_no_terminator() {
assert_eq!(
asi_marked("try\n{\na\n}\ncatch (e)\n{\n}"),
"try\n{\na\n<;>}\ncatch (e)\n{\n}<;>"
);
assert_eq!(
asi_marked("if (c)\n{\n}\nelse\n{\n}"),
"if (c)\n{\n}\nelse\n{\n}<;>"
);
assert_eq!(asi_marked("do\n{\n}\nwhile (c)"), "do\n{\n}\n<;>while (c)");
}
#[test]
fn a_newline_before_a_clause_keyword_still_ends_the_statement() {
assert_eq!(asi_marked("a\ntry\n{\n}"), "a\n<;>try\n{\n}<;>");
assert_eq!(
asi_marked("a\ndo\n{\n}\nwhile (c)"),
"a\n<;>do\n{\n}\n<;>while (c)"
);
}
#[test]
fn block_closing_brace_terminates_without_a_newline() {
assert_eq!(
asi_marked("function f(): number { return 1 }"),
"function f(): number { return 1 <;>}<;>"
);
assert_eq!(
asi_marked("class C { x: number = 1 }"),
"class C { x: number = 1 <;>}<;>"
);
}
#[test]
fn value_closing_brace_never_terminates() {
assert_eq!(asi_marked("x = { a: 1 }"), "x = { a: 1 }<;>");
assert_eq!(
asi_marked("import { a } from \"m\""),
"import { a } from \"m\"<;>"
);
assert_eq!(
asi_marked("export type { A } from \"m\""),
"export type { A } from \"m\"<;>"
);
}
#[test]
fn only_a_after_case_label_colon_opens_a_block() {
assert_eq!(
asi_marked("switch (n) {\ncase 1: { a }\n}"),
"switch (n) {\ncase 1: { a <;>}\n<;>}<;>"
);
assert_eq!(
asi_marked("x = c ? { a: 1 } : { a: 2 }"),
"x = c ? { a: 1 } : { a: 2 }<;>"
);
assert_eq!(
asi_marked("x = { default: { a: 1 } }"),
"x = { default: { a: 1 } }<;>"
);
}
#[test]
fn a_keyword_property_name_is_not_a_keyword() {
assert_eq!(asi_marked("a = o.do\nb = 1"), "a = o.do\n<;>b = 1<;>");
assert_eq!(asi_marked("a = o.class\nb = 1"), "a = o.class\n<;>b = 1<;>");
assert_eq!(
asi_marked("a = o.default\nb = 1"),
"a = o.default\n<;>b = 1<;>"
);
assert_eq!(asi_marked("a = o?.try\nb = 1"), "a = o?.try\n<;>b = 1<;>");
assert_eq!(
asi_marked("a = o.switch(1)\nb = 1"),
"a = o.switch(1)\n<;>b = 1<;>"
);
assert_eq!(
asi_marked("a = o.catch(1)\nb = 1"),
"a = o.catch(1)\n<;>b = 1<;>"
);
}
#[test]
fn a_generic_header_is_followed_by_a_block() {
assert_eq!(
asi_marked("function f(): Array<number> {\nreturn a\n}"),
"function f(): Array<number> {\nreturn a\n<;>}<;>"
);
assert_eq!(
asi_marked("class Box<T> {\nv: number = 1\n}"),
"class Box<T> {\nv: number = 1\n<;>}<;>"
);
}
#[test]
fn a_cast_is_followed_by_an_object_literal() {
assert_eq!(
asi_marked("x = <Foo>{ a: 1 }\ny()"),
"x = <Foo>{ a: 1 }\n<;>y()<;>"
);
assert_eq!(asi_marked("<Foo>{ a: 1 }\ny()"), "<Foo>{ a: 1 }\n<;>y()<;>");
assert_eq!(
asi_marked("f = x => <Foo>{ a: x }\ny()"),
"f = x => <Foo>{ a: x }\n<;>y()<;>"
);
assert_eq!(
asi_marked("x = <Box<Array<number>>>{ v: [] }\ny()"),
"x = <Box<Array<number>>>{ v: [] }\n<;>y()<;>"
);
assert_eq!(
asi_marked("if (a < b) {\nreturn a\n}"),
"if (a < b) {\nreturn a\n<;>}<;>"
);
}
#[test]
fn a_spread_operand_is_an_object_literal() {
assert_eq!(
asi_marked("x = { ...{ a: 1 }, b: 2 }\ny()"),
"x = { ...{ a: 1 }, b: 2 }\n<;>y()<;>"
);
}
#[test]
fn template_substitution_suspends_insertion() {
assert_eq!(
asi_marked("const s = `a${\nn\n}`"),
"const s = `a${\nn\n}`<;>"
);
assert_eq!(
asi_marked("const s = `a${\nn\n}b${\nn\n}c`"),
"const s = `a${\nn\n}b${\nn\n}c`<;>"
);
}
#[test]
fn declaration_header_suspends_insertion() {
assert_eq!(
asi_marked("class S\nextends B\nimplements I\n{\n}"),
"class S\nextends B\nimplements I\n{\n}<;>"
);
assert_eq!(asi_marked("interface I\n{\n}"), "interface I\n{\n}<;>");
assert_eq!(asi_marked("enum E\n{\nA\n}"), "enum E\n{\nA\n<;>}<;>");
}
#[test]
fn block_inside_parens_is_statement_context_again() {
assert_eq!(
asi_marked("f((x: number): number => {\nreturn x\n})"),
"f((x: number): number => {\nreturn x\n<;>})<;>"
);
assert_eq!(asi_marked("f(a,\nb)"), "f(a,\nb)<;>");
}
#[test]
fn mvp_fixture_snapshot() {
let source = "function greet(name: string): string {\n return \"Hello, \" + name;\n}\n\nfunction main(): string {\n const msg = greet(\"world\");\n console.log(msg);\n assert(msg === \"Hello, world\", \"greeting should match\");\n return msg;\n}\n";
let mut asi = Asi::new(source, crate::FileId(0));
let mut tokens = Vec::new();
loop {
let tok = asi.next_token();
let is_eof = tok.kind == TokenKind::Eof;
tokens.push(tok);
if is_eof {
break;
}
}
assert!(asi.into_diagnostics().is_empty());
let kinds: Vec<String> = tokens.iter().map(|t| format!("{:?}", t.kind)).collect();
insta::assert_debug_snapshot!(kinds);
}
}