use super::scan::{identifier_starts_at, skip_identifier, skip_numeric_literal, skip_whitespace};
use tsv_lang::source_scan::{
BracedOperandWalk, OperandAnchor, OperandGrammar, ScanStart, TriviaProfile, skip_regex_literal,
skip_trivia,
};
pub(crate) fn closes_braced_operand(
bytes: &[u8],
rbrace: usize,
lower_bound: usize,
start: ScanStart,
resume: &mut BracedOperandWalk,
) -> bool {
let mut walk = resume
.take::<Walk>()
.filter(|walk| walk.resumes(rbrace, lower_bound, start))
.unwrap_or_else(|| Box::new(Walk::new(lower_bound, start)));
let closed = walk.reach(bytes, rbrace);
if closed.is_some() {
resume.put(walk);
}
closed.is_some_and(Closed::ends_operand)
}
const WALK_GRAMMAR: OperandGrammar = crate::__private::OPERAND_GRAMMAR;
const BRACE_ENDS_OPERAND: OperandGrammar = OperandGrammar {
closes_braced_operand: |_, _, _, _, _| true,
..WALK_GRAMMAR
};
struct Walk {
lower_bound: usize,
start: ScanStart,
at: usize,
region: Region,
outer: Vec<Region>,
contexts: TokenContexts,
}
impl Walk {
fn new(lower_bound: usize, start: ScanStart) -> Self {
Self {
lower_bound,
start,
at: lower_bound,
region: Region::new(lower_bound),
outer: Vec::new(),
contexts: TokenContexts::new(start),
}
}
fn resumes(&self, rbrace: usize, lower_bound: usize, start: ScanStart) -> bool {
self.lower_bound == lower_bound && self.start == start && self.at <= rbrace
}
fn reach(&mut self, bytes: &[u8], rbrace: usize) -> Option<Closed> {
while self.at <= rbrace {
let i = self.at;
let past_whitespace = skip_whitespace(bytes, i);
if past_whitespace > i {
self.contexts.newline |= holds_line_terminator(&bytes[i..past_whitespace]);
self.at = past_whitespace;
continue;
}
let b = *bytes.get(i)?;
if b == b'`' {
self.at = self.template_text(bytes, i + 1, rbrace)?;
continue;
}
if let Some(past) = skip_trivia(bytes, i, bytes.len(), TriviaProfile::JS) {
self.region.anchor.skipped_trivia(
bytes,
i,
past,
self.region.lower_bound,
WALK_GRAMMAR,
);
if b == b'/' {
self.contexts.newline |=
bytes[i + 1] == b'/' || holds_line_terminator(&bytes[i..past]);
} else {
self.contexts.token(Token::Operand);
}
self.at = past;
continue;
}
if b == b'/' {
let regex = self
.region
.divides_after_brace(bytes, i, &self.contexts)
.map_or_else(
|| {
self.region.anchor.starts_regex(
bytes,
i,
self.region.lower_bound,
WALK_GRAMMAR,
)
},
|divides| !divides,
);
if regex && let Some(past) = skip_regex_literal(bytes, i, bytes.len()) {
self.region.anchor.skipped_operand(past);
self.contexts.token(Token::Operand);
self.at = past;
continue;
}
self.contexts.token(Token::Operator);
self.at = i + 1;
continue;
}
if identifier_starts_at(bytes, i) || b == b'#' {
let word_start = if b == b'#' { i + 1 } else { i };
let word_end = skip_identifier(bytes, word_start).max(i + 1);
self.contexts.word(&bytes[i..word_end]);
self.at = word_end;
continue;
}
if b.is_ascii_digit() || (b == b'.' && bytes.get(i + 1).is_some_and(u8::is_ascii_digit))
{
self.at = skip_numeric_literal(bytes, i).max(i + 1);
self.contexts.token(Token::Operand);
continue;
}
if b == b'}' && self.contexts.top() == Context::Interpolation {
self.region = self.outer.pop()?;
self.contexts.stack.pop();
self.at = self.template_text(bytes, i + 1, rbrace)?;
continue;
}
let (token, len) = punctuator_at(bytes, i);
self.contexts.token(token);
self.at = i + len;
if token == Token::BraceR {
let closed = self.contexts.closed.as_mut()?;
closed.bang = non_null_follows(bytes, i + 1);
if i == rbrace {
return Some(*closed);
}
}
}
None
}
fn template_text(&mut self, bytes: &[u8], mut i: usize, limit: usize) -> Option<usize> {
while i < limit {
match bytes[i] {
b'\\' => i += 2,
b'`' => {
self.region.anchor.skipped_operand(i + 1);
self.contexts.token(Token::Operand);
return Some(i + 1);
}
b'$' if bytes.get(i + 1) == Some(&b'{') => {
self.contexts.open_interpolation();
self.outer
.push(std::mem::replace(&mut self.region, Region::new(i + 2)));
return Some(i + 2);
}
_ => i += 1,
}
}
None
}
}
struct Region {
anchor: OperandAnchor,
lower_bound: usize,
}
impl Region {
fn new(lower_bound: usize) -> Self {
Self {
anchor: OperandAnchor::new(lower_bound),
lower_bound,
}
}
fn divides_after_brace(
&self,
bytes: &[u8],
slash: usize,
contexts: &TokenContexts,
) -> Option<bool> {
let closed = contexts.closed?;
let ends_on_brace = contexts.prev == Token::BraceR
|| (self
.anchor
.starts_regex(bytes, slash, self.lower_bound, WALK_GRAMMAR)
&& !self
.anchor
.starts_regex(bytes, slash, self.lower_bound, BRACE_ENDS_OPERAND));
ends_on_brace.then(|| closed.ends_operand())
}
}
fn non_null_follows(bytes: &[u8], after: usize) -> bool {
let mut i = after;
loop {
let past_whitespace = skip_whitespace(bytes, i);
match bytes.get(past_whitespace) {
Some(b'!') => return past_whitespace == i,
Some(b'/') if bytes.get(past_whitespace + 1) == Some(&b'*') => {}
_ => return false,
}
let Some(past) = skip_trivia(bytes, past_whitespace, bytes.len(), TriviaProfile::JS) else {
return false;
};
if holds_line_terminator(&bytes[past_whitespace..past]) {
return false;
}
i = past;
}
}
fn holds_line_terminator(bytes: &[u8]) -> bool {
bytes.iter().enumerate().any(|(k, &b)| {
matches!(b, b'\n' | b'\r')
|| (b == 0xE2 && matches!(bytes.get(k + 1..k + 3), Some([0x80, 0xA8 | 0xA9])))
})
}
fn punctuator_at(bytes: &[u8], i: usize) -> (Token, usize) {
let next = bytes.get(i + 1).copied();
match bytes[i] {
b'(' => (Token::ParenL, 1),
b')' => (Token::ParenR, 1),
b'{' => (Token::BraceL, 1),
b'}' => (Token::BraceR, 1),
b']' | b'@' => (Token::Operand, 1),
b';' => (Token::Semi, 1),
b':' => (Token::Colon, 1),
b'!' => (Token::Bang, 1),
b'.' if next == Some(b'.') && bytes.get(i + 2) == Some(&b'.') => (Token::Operator, 3),
b'.' => (Token::Dot, 1),
b'?' if next == Some(b'.') && !bytes.get(i + 2).is_some_and(u8::is_ascii_digit) => {
(Token::Dot, 2)
}
b'?' if next == Some(b'?') => (Token::Operator, 2),
b'?' => (Token::Question, 1),
b'=' if next == Some(b'>') => (Token::Arrow, 2),
b'+' | b'-' if next == Some(bytes[i]) => (Token::Operand, 2),
_ => (Token::Operator, 1),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Token {
Start,
Name,
Operand,
Operator,
Bang,
ParenL,
ParenR,
BraceL,
BraceR,
Semi,
Colon,
Question,
Dot,
Arrow,
Extends,
Function,
Class,
}
impl Token {
fn before_expr(self) -> bool {
matches!(
self,
Token::Operator
| Token::Bang
| Token::ParenL
| Token::BraceL
| Token::Semi
| Token::Colon
| Token::Question
| Token::Arrow
| Token::Extends
)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Context {
BraceStatement,
BraceExpression,
Interpolation,
Paren,
FunctionStatement,
FunctionExpression,
}
impl Context {
fn is_expr(self) -> bool {
matches!(
self,
Context::BraceExpression | Context::Paren | Context::FunctionExpression
)
}
fn is_function(self) -> bool {
matches!(
self,
Context::FunctionStatement | Context::FunctionExpression
)
}
}
#[derive(Clone, Copy, Debug)]
struct Frame {
context: Context,
conditionals: u32,
statement_level: bool,
}
#[derive(Clone, Copy, Debug)]
struct Closed {
operand: bool,
operand_before_bang: bool,
bang: bool,
}
impl Closed {
fn of(popped: Option<Frame>) -> Self {
let frame = popped.filter(|frame| !frame.statement_level);
let operand = frame.is_some_and(|frame| {
matches!(
frame.context,
Context::BraceExpression | Context::FunctionExpression
)
});
Self {
operand,
operand_before_bang: operand || frame.is_some_and(|frame| frame.context.is_function()),
bang: false,
}
}
fn ends_operand(self) -> bool {
if self.bang {
self.operand_before_bang
} else {
self.operand
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Role {
None,
AsyncName,
ClassHead,
HeritageExtends,
ConditionalColon,
AsyncFunction,
}
struct TokenContexts {
stack: Vec<Frame>,
expr_allowed: bool,
prev: Token,
prev_role: Role,
newline: bool,
closed: Option<Closed>,
}
impl TokenContexts {
fn new(start: ScanStart) -> Self {
let base = Frame {
context: Context::BraceStatement,
conditionals: 0,
statement_level: false,
};
let mut contexts = Self {
stack: vec![base],
expr_allowed: true,
prev: Token::Start,
prev_role: Role::None,
newline: false,
closed: None,
};
if start == ScanStart::Interpolation {
contexts.open_interpolation();
}
contexts
}
fn top(&self) -> Context {
self.stack
.last()
.map_or(Context::BraceStatement, |frame| frame.context)
}
fn push(&mut self, context: Context) {
let statement_level = self.stack.last().is_some_and(|top| match top.context {
Context::BraceStatement => self.stack.len() > 1,
Context::BraceExpression | Context::FunctionStatement | Context::FunctionExpression => {
top.statement_level
}
Context::Interpolation | Context::Paren => false,
});
self.stack.push(Frame {
context,
conditionals: 0,
statement_level,
});
}
fn pop_close(&mut self) -> Option<Frame> {
let mut out = self.stack.pop()?;
if out.context == Context::BraceStatement && self.top().is_function() {
out = self.stack.pop()?;
}
Some(out)
}
fn open_interpolation(&mut self) {
self.push(Context::Interpolation);
self.expr_allowed = true;
self.prev = Token::Operator;
self.prev_role = Role::None;
self.newline = false;
self.closed = None;
}
fn brace_is_block(&self, role: Role) -> bool {
let parent = self.top();
if parent.is_function() {
return role != Role::HeritageExtends;
}
match self.prev {
Token::Arrow | Token::Semi => true,
Token::Name if self.expr_allowed => self.newline,
Token::Name => false,
Token::Colon
if matches!(parent, Context::BraceStatement | Context::BraceExpression) =>
{
parent == Context::BraceStatement && role != Role::ConditionalColon
}
_ => !self.expr_allowed,
}
}
fn word(&mut self, word: &[u8]) {
if self.prev == Token::Dot || word.first() == Some(&b'#') {
self.token(Token::Name);
return;
}
let token = match word {
b"function" => Token::Function,
b"class" => Token::Class,
b"extends" => Token::Extends,
b"case" | b"default" | b"do" | b"else" | b"return" | b"throw" | b"new" | b"in"
| b"instanceof" | b"typeof" | b"void" | b"delete" => Token::Operator,
_ => {
let allowed = word == b"of" && !self.expr_allowed;
let names_class = self.prev_role == Role::ClassHead;
self.token(Token::Name);
self.expr_allowed = allowed;
self.prev_role = if word == b"async" {
Role::AsyncName
} else if names_class {
Role::ClassHead
} else {
Role::None
};
return;
}
};
self.token(token);
}
fn token(&mut self, token: Token) {
let role = std::mem::replace(&mut self.prev_role, Role::None);
let closed = self.closed.take();
match token {
Token::ParenL => {
self.push(Context::Paren);
self.expr_allowed = true;
}
Token::ParenR => {
self.expr_allowed = self.pop_close().is_none_or(|out| !out.context.is_expr());
}
Token::BraceR => {
let out = self.pop_close();
self.expr_allowed = out.is_none_or(|out| !out.context.is_expr());
self.closed = Some(Closed::of(out));
}
Token::BraceL => {
self.push(if self.brace_is_block(role) {
Context::BraceStatement
} else {
Context::BraceExpression
});
self.expr_allowed = true;
}
Token::Function | Token::Class => {
let expression = self.prev.before_expr()
&& !(self.prev == Token::Colon && self.top() == Context::BraceStatement);
self.push(if expression {
Context::FunctionExpression
} else {
Context::FunctionStatement
});
self.expr_allowed = false;
self.prev_role = if token == Token::Class {
Role::ClassHead
} else if role == Role::AsyncName && !self.newline {
Role::AsyncFunction
} else {
Role::None
};
}
Token::Extends => {
if self.top().is_function() && (role == Role::ClassHead || self.prev != Token::Name)
{
self.prev_role = Role::HeritageExtends;
}
self.expr_allowed = true;
}
Token::Colon => {
if self.top().is_function() {
self.stack.pop();
} else if let Some(frame) = self.stack.last_mut()
&& frame.conditionals > 0
{
frame.conditionals -= 1;
self.prev_role = Role::ConditionalColon;
}
self.expr_allowed = true;
}
Token::Question => {
if let Some(frame) = self.stack.last_mut() {
frame.conditionals += 1;
}
self.expr_allowed = true;
}
Token::Bang => {
self.closed = closed;
self.expr_allowed = true;
}
_ => self.expr_allowed = token.before_expr(),
}
if role == Role::AsyncFunction
&& let Some(top) = self.stack.last_mut()
&& top.context == Context::FunctionStatement
{
top.context = Context::FunctionExpression;
}
self.prev = token;
self.newline = false;
}
}
#[cfg(test)]
mod tests {
use super::closes_braced_operand;
use tsv_lang::source_scan::{BracedOperandWalk, ScanStart};
fn closes(src: &[u8], rbrace: usize, lower_bound: usize, start: ScanStart) -> bool {
closes_braced_operand(
src,
rbrace,
lower_bound,
start,
&mut BracedOperandWalk::default(),
)
}
fn braces_before_a_slash(src: &str) -> impl DoubleEndedIterator<Item = usize> {
src.bytes()
.enumerate()
.filter(|&(at, b)| b == b'}' && src[at + 1..].trim_start().starts_with('/'))
.map(|(at, _)| at)
}
fn divides(src: &str, start: ScanStart) -> bool {
let rbrace = braces_before_a_slash(src)
.next()
.expect("a `}` before a `/` to ask about");
closes(src.as_bytes(), rbrace, 0, start)
}
fn island_divides(src: &str) -> bool {
divides(src, ScanStart::Expression)
}
#[test]
fn an_object_literal_ends_an_operand() {
for src in [
"{} / 2",
"{ a: 1 } / 2",
"x + {} / 2",
"c ? {} / 2 : d",
"c ? d : {} / 2",
"[{} / 2]",
"{ a: function () {} } / 2",
"{ class: 1 } / 2",
"{ function: 1 } / 2",
"`${a}` + {} / 2",
"a.function + {} / 2",
"f(c ? d : {} / 2)",
"() => { f({} / 2) }",
"(function* () { f(yield {} / 2); })",
"[async function (a = {} / 2) {}]",
"c ?.5 : {} / 2",
] {
assert!(island_divides(src), "{src:?}");
assert!(
divides(src, ScanStart::Interpolation),
"{src:?} interpolated"
);
}
}
#[test]
fn a_function_or_class_acorn_reads_as_an_expression_ends_an_operand() {
for src in [
"x + function () {} / 2",
"c ? function () {} / 2 : d",
"f(c ? d : function () {} / 2)",
"!function () {} / 2",
"a = function () {} / 2",
"x + class {} / 2",
"x + class A extends f() { m() {} } / 2",
"new class {} / 2",
"x + function* () {} / 2",
"[...function () {} / 2]",
"typeof function () {} / 2",
"void function () {} / 2",
"delete function () {} / 2",
"x in function () {} / 2",
"x instanceof class {} / 2",
"() => { for (;function () {} / 2;) {} }",
"() => { for (;;function () {} / 2) {} }",
] {
assert!(island_divides(src), "{src:?}");
}
}
#[test]
fn a_type_literal_is_braced_as_the_tokenizer_reads_it() {
for src in [
"x + function (): {} {} / 2",
"x + function (): { a: 1 } {} / 2",
"x + function (): string {} / 2",
"c ? x + function (): {} {} / 2 : d",
"[a?.b, x + function (): {} {} / 2]",
] {
assert!(island_divides(src), "{src:?}");
}
for src in [
"(x + function (): {} {} / 2)",
"{ k: x + function (): {} {} / 2 }",
] {
assert!(!island_divides(src), "{src:?}");
}
}
#[test]
fn a_function_or_class_acorn_reads_as_a_statement_ends_none() {
for src in [
"function () {} / 2",
"class {} / 2",
"class extends B {} / 2",
"c ? d : function () {} / 2",
"[c ? d : function () {} / 2]",
"x ? y : z ? {} : function () {} / 2",
"c ? d : class {} / 2",
"await function () {} / 2",
"x + await class {} / 2",
"yield function () {} / 2",
"(function* () { f(yield function () {} / 2); })",
"x + @dec class {} / 2",
"[{function: 1}, c ? d : function () {} / 2]",
"[{class: 1}, c ? d : class {} / 2]",
"[a?.b, c ? d : function () {} / 2]",
] {
assert!(!island_divides(src), "{src:?}");
}
}
#[test]
fn an_async_function_is_an_expression_unless_anonymous() {
for src in [
"async function f() {} / 2",
"x + async function f() {} / 2",
"async function* () {} / 2",
"async function* f() {} / 2",
"c ? x : async function f() {} / 2",
"[async function f() {} / 2]",
"async function <T>() {} / 2",
"async function f(a = {}) {} / 2",
"x + async /* c */ function f() {} / 2",
] {
assert!(island_divides(src), "{src:?}");
assert!(
divides(src, ScanStart::Interpolation),
"{src:?} interpolated"
);
}
for src in [
"async function () {} / 2",
"x + async function () {} / 2",
"f(async function () {} / 2)",
"async function (a = {}) {} / 2",
"async\nfunction f() {} / 2",
"async // c\nfunction f() {} / 2",
"x.async function f() {} / 2",
] {
assert!(!island_divides(src), "{src:?}");
assert!(
!divides(src, ScanStart::Interpolation),
"{src:?} interpolated"
);
}
}
#[test]
fn a_class_heritage_object_is_an_object_literal() {
assert!(!island_divides("class extends {}.x {} / 2"));
assert!(!island_divides("class A extends {}.x {} / 2"));
assert!(island_divides("x + class extends {}.x {} / 2"));
assert!(island_divides("x + class A extends {}.x {} / 2"));
assert!(island_divides("x + class extends {} {} / 2"));
assert!(!island_divides("class A<T> extends {}.x {} / 2"));
assert!(island_divides("x + class A<T> extends {}.x {} / 2"));
assert!(!island_divides("x + function <T extends {}>() {} / 2"));
assert!(island_divides("x + class A<T extends {}> {} / 2"));
}
#[test]
fn an_interpolation_reads_its_first_function_as_an_expression() {
assert!(divides("function () {} / 2", ScanStart::Interpolation));
assert!(divides(
"c ? d : function () {} / 2",
ScanStart::Interpolation
));
}
#[test]
fn a_block_ends_none() {
for src in [
"() => { if (c) {} /}/ }",
"() => { function f() {} /}/ }",
"() => { a; {} /}/ }",
"() => { a; function f() {} /}/.test(s) }",
"() => { return\nfunction f() {}\n/}/.test(s) }",
"() => { switch (a) { case 1: {} } /}/ }",
"() => { switch (a) { case 1: {} /}/.test(s) } }",
"() => { l: {} /}/.test(s) }",
"() => { a?.b; l: {} /}/.test(s) }",
"() => { try {} finally {} /}/ }",
"() => { return // c\n{} /}/.test(s) }",
"() => { return /* c\n */ {} /}/.test(s) }",
"(function* () { yield\n{}\n/}/.test(s) })",
"(a, b) => {} /}/",
] {
assert!(!island_divides(src), "{src:?}");
}
}
#[test]
fn a_brace_at_statement_level_ends_none() {
for src in [
"() => { a\n{} /}/.test(s) }",
"() => { a = b\n{} /}/.test(s) }",
"function () { a\n{} /}/.test(s) }",
"() => { function f(): void {}\n/}/.test(s) }",
"() => { function f(): string {}\n/[{]/.test(s) }",
"() => { enum E { A }\n/}/.test(s) }",
"() => { interface I {}\n/}/.test(s) }",
"() => { namespace N {}\n/}/.test(s) }",
"() => { type T = {}\n/}/.test(s) }",
"() => { namespace N { function f(): void {}\n/}/.test(s) } }",
"x + class { static { a\n{} /}/.test(s) } }",
] {
assert!(!island_divides(src), "{src:?}");
}
for src in [
"() => { x = {} / 2 }",
"() => { x = function () {} / 2 }",
"() => { x = async function f() {} / 2 }",
"(function* () { yield {} / 2; })",
"(async function () { await {} / 2; })",
"(function* () { yield x + function () {} / 2; })",
"(async function () { await x + function () {} / 2; })",
"x + class { a = {} / 2 }",
] {
assert!(!island_divides(src), "{src:?}");
}
assert!(!island_divides("() => { class A extends {} / 2 {} }"));
}
#[test]
fn a_for_headers_object_literal_reads_as_the_tokenizers_block() {
for src in [
"() => { for (;{} / 2;) {} }",
"() => { for (const x of\n{} / 2) {} }",
] {
assert!(!island_divides(src), "{src:?}");
}
assert!(island_divides("() => { for (const x of {} / 2) {} }"));
}
#[test]
fn a_brace_opened_before_the_walk_ends_none() {
assert!(!closes(b"a } / 2", 2, 0, ScanStart::Interpolation));
assert!(!closes(b"a } / 2", 2, 0, ScanStart::Expression));
}
#[test]
fn a_brace_inside_a_literal_is_no_token_of_the_walk() {
assert!(!closes(b"'{}' / 2", 2, 0, ScanStart::Expression));
assert!(!closes(b"`{}` / 2", 2, 0, ScanStart::Expression));
assert!(!closes(b"`${a}{}` / 2", 6, 0, ScanStart::Expression));
}
#[test]
fn comments_and_line_breaks_are_transparent() {
assert!(island_divides("x + {} /* c */ / 2"));
assert!(island_divides("x + /* { */ {} / 2"));
assert!(island_divides("x + // {\n{} / 2"));
assert!(!island_divides("/* c */ function () {} / 2"));
let src = b"x + {} // c\n/ 2";
assert!(closes(src, 5, 0, ScanStart::Expression));
}
fn last_divides(src: &str) -> bool {
let rbrace = braces_before_a_slash(src)
.next_back()
.expect("a `}` before a `/` to ask about");
closes(src.as_bytes(), rbrace, 0, ScanStart::Expression)
}
#[test]
fn the_walk_reads_its_own_earlier_slashes_as_it_answers() {
for src in [
"[{} / 1, {} / 2]",
"{} / {} / 2",
"x + function () {} / {} / 2",
"[/a/ / 1, {} / 2]",
"`${a}` + f<T> / {} / 2",
] {
assert!(last_divides(src), "{src:?}");
}
assert!(!last_divides("function () {} / {} / 2"));
}
#[test]
fn a_template_literal_is_tokenized_in_line() {
for src in [
"`${{} / 1}` + {} / 2",
"[`${{} / 1}`, {} / 2]",
"`${`${{} / 1}`}` + {} / 2",
"`a${b}c${{} / 1}d` + {} / 2",
"`\\${` + {} / 2",
"`${function () {} / 1}` + {} / 2",
"[() => { x = `${{} / 1}` }, {} / 2]",
] {
assert!(last_divides(src), "{src:?}");
}
let src = "x + `${function () {} / 1}`";
let rbrace = braces_before_a_slash(src)
.next()
.expect("a `}` before a `/`");
let interpolation = src.bytes().position(|b| b == b'$').expect("a `${`") + 2;
assert!(closes(src.as_bytes(), rbrace, 0, ScanStart::Expression));
assert!(closes(
src.as_bytes(),
rbrace,
interpolation,
ScanStart::Interpolation
));
}
#[test]
fn template_nesting_is_walked_once() {
let mut src = String::from("{} / 1");
for _ in 0..200 {
src = format!("`${{{src}}}` + {{}} / 1");
}
assert!(last_divides(&src));
}
#[test]
fn a_non_null_run_leaves_the_question_the_braces() {
let bang = |src: &str| {
let rbrace = src
.bytes()
.zip(src.bytes().skip(1))
.position(|pair| pair == (b'}', b'!'))
.expect("a `}!` to ask about");
closes(src.as_bytes(), rbrace, 0, ScanStart::Expression)
};
for src in [
"{}! / 2",
"{}!! / 2",
"x + function () {}! / 2",
"function () {}! / 2",
"class {}! / 2",
"x + async function () {}! / 2",
"c ? x : function () {}! / 2",
"[c ? x : function () {}! / 2]",
"function () {}!! / 2",
] {
assert!(bang(src), "{src:?}");
}
for src in [
"() => {}! / 2",
"() => { function f() {}!/}/.test(s) }",
"() => { if (c) {}!/}/.test(s) }",
"() => { x = function () {}! / 2 }",
] {
assert!(!bang(src), "{src:?}");
}
assert!(closes(b"{} /* c */! / 2", 1, 0, ScanStart::Expression));
for src in [
"function () {} /* c */! / 2",
"function () {}/* c */ /* d */! / 2",
] {
assert!(
closes(src.as_bytes(), 13, 0, ScanStart::Expression),
"{src:?}"
);
}
for src in [
"function () {} /* c */ ! / 2",
"function () {} /* c\n */! / 2",
"function () {} // c\n! / 2",
] {
assert!(
!closes(src.as_bytes(), 13, 0, ScanStart::Expression),
"{src:?}"
);
}
assert!(last_divides("{}! / {} / 2"));
assert!(last_divides("function () {}! / {} / 2"));
assert!(last_divides("{} /* c */! / {} / 2"));
assert!(!last_divides("{} ! / {} / 2"));
}
#[test]
fn a_resumed_walk_answers_as_a_fresh_one() {
for src in [
"[{} / 1, {} / 2, x + function () {} / 3, function () {} / 4]",
"{} / {} / 2 + c ? d : function () {} / 5",
"`${{} / 1}` + {} / 2 + `a${`${function () {} / 3}`}b` + {} / 4",
"() => { a\n{} /}/.test(s) } + {} / 2",
"{}! / {} / 2 + function () {}! / {} / 3",
"x + async function f() {} / async function () {} / 2",
"x + class extends {} {} / {} / 2",
"{ a: {} / 1, b: () => { x = {} / 2 }, c: {} / 3 } / 4",
"'}' / {} / 1 + /}/ / {} / 2",
"f<T> / {} / 1 + {} / f<T> / 2",
] {
for start in [ScanStart::Expression, ScanStart::Interpolation] {
let mut walk = BracedOperandWalk::default();
for rbrace in braces_before_a_slash(src).chain(braces_before_a_slash(src)) {
let fresh = closes(src.as_bytes(), rbrace, 0, start);
let resumed =
closes_braced_operand(src.as_bytes(), rbrace, 0, start, &mut walk);
assert_eq!(resumed, fresh, "{src:?} at {rbrace}");
}
}
}
let src = b"x + function () {} / {} / 2";
let mut walk = BracedOperandWalk::default();
assert!(closes_braced_operand(
src,
17,
0,
ScanStart::Expression,
&mut walk
));
assert!(!closes_braced_operand(
src,
22,
4,
ScanStart::Expression,
&mut walk
));
let src = b"function () {} / 2 + {} / 3";
let mut walk = BracedOperandWalk::default();
assert!(closes_braced_operand(
src,
13,
0,
ScanStart::Interpolation,
&mut walk
));
assert!(!closes_braced_operand(
src,
22,
0,
ScanStart::Expression,
&mut walk
));
}
#[test]
fn a_region_of_many_braced_operands_is_walked_once() {
let pairs = 200_000;
let src = format!("[{}]", vec!["{} / 1"; pairs].join(", "));
let mut walk = BracedOperandWalk::default();
let mut divided = 0;
for rbrace in braces_before_a_slash(&src) {
divided += usize::from(closes_braced_operand(
src.as_bytes(),
rbrace,
0,
ScanStart::Expression,
&mut walk,
));
}
assert_eq!(divided, pairs);
}
}