use tsv_lang::is_js_whitespace;
use tsv_lang::printing::has_newline_between;
use tsv_lang::source_scan::{
TriviaProfile, find_char_skipping_comments, find_keyword, rfind_char_skipping_comments,
rfind_keyword, skip_trivia_run, whole_word_at,
};
#[derive(Clone, Copy)]
pub(super) struct Source<'s> {
pub text: &'s str,
pub bytes: &'s [u8],
}
impl<'s> Source<'s> {
pub(super) fn new(text: &'s str) -> Self {
Self {
text,
bytes: text.as_bytes(),
}
}
pub(super) fn ends_with_semicolon(self, end: u32) -> bool {
end > 0 && self.bytes.get(end as usize - 1) == Some(&b';')
}
pub(super) fn line_break_between(self, start: u32, end: u32) -> bool {
has_newline_between(self.text, start, end)
}
pub(super) fn next_significant(self, from: u32) -> u32 {
skip_trivia_run(
self.text,
from as usize,
TriviaProfile::COMMENTS,
is_js_whitespace,
) as u32
}
pub(super) fn continuation(self, from: u32) -> Option<Continuation> {
let at = self.next_significant(from) as usize;
match self.bytes.get(at)? {
b'(' | b'[' | b'`' | b'<' => Some(Continuation::Postfix),
b'/' | b'*' => Some(Continuation::Operator),
&sign @ (b'+' | b'-') => {
(self.bytes.get(at + 1) != Some(&sign)).then_some(Continuation::Operator)
}
b'i' => (whole_word_at(self.bytes, at, b"in")
|| whole_word_at(self.bytes, at, b"instanceof"))
.then_some(Continuation::Operator),
_ => None,
}
}
pub(super) fn find_char(self, start: u32, end: u32, target: u8) -> Option<u32> {
find_char_skipping_comments(self.bytes, start as usize, end as usize, target)
.map(|at| at as u32)
}
pub(super) fn rfind_char(self, start: u32, end: u32, target: u8) -> Option<u32> {
rfind_char_skipping_comments(self.bytes, start as usize, end as usize, target)
.map(|at| at as u32)
}
pub(super) fn find_keyword(self, start: u32, end: u32, keyword: &[u8]) -> Option<u32> {
find_keyword(
self.bytes,
start as usize,
end as usize,
keyword,
TriviaProfile::JS,
)
.map(|at| at as u32)
}
pub(super) fn rfind_keyword(self, start: u32, end: u32, keyword: &[u8]) -> Option<u32> {
rfind_keyword(
self.bytes,
start as usize,
end as usize,
keyword,
TriviaProfile::JS,
)
.map(|at| at as u32)
}
pub(super) fn end_with_trailing_comma(self, end: u32) -> u32 {
let next = self.next_significant(end);
if self.bytes.get(next as usize) == Some(&b',') {
next + 1
} else {
end
}
}
pub(super) fn binary_operator_precedence(self, from: u32) -> Option<u8> {
let at = self.next_significant(from) as usize;
let byte = |offset: usize| self.bytes.get(at + offset).copied();
match byte(0)? {
b'*' => match (byte(1), byte(2)) {
(Some(b'*'), Some(b'=')) | (Some(b'='), _) => None,
(Some(b'*'), _) => Some(15),
_ => Some(14),
},
b'/' | b'%' => (byte(1) != Some(b'=')).then_some(14),
sign @ (b'+' | b'-') => (byte(1) != Some(b'=') && byte(1) != Some(sign)).then_some(13),
b'<' => match (byte(1), byte(2)) {
(Some(b'<'), Some(b'=')) => None,
(Some(b'<'), _) => Some(12),
_ => Some(11),
},
b'>' => match (byte(1), byte(2), byte(3)) {
(Some(b'>'), Some(b'>'), Some(b'=')) | (Some(b'>'), Some(b'='), _) => None,
(Some(b'>'), _, _) => Some(12),
_ => Some(11),
},
b'=' | b'!' => (byte(1) == Some(b'=')).then_some(10),
b'&' => match (byte(1), byte(2)) {
(Some(b'&'), Some(b'=')) | (Some(b'='), _) => None,
(Some(b'&'), _) => Some(6),
_ => Some(9),
},
b'^' => (byte(1) != Some(b'=')).then_some(8),
b'|' => match (byte(1), byte(2)) {
(Some(b'|'), Some(b'=')) | (Some(b'='), _) => None,
(Some(b'|'), _) => Some(5),
_ => Some(7),
},
b'?' => match (byte(1), byte(2)) {
(Some(b'?'), Some(b'=')) => None,
(Some(b'?'), _) => Some(4),
_ => None,
},
b'i' => (whole_word_at(self.bytes, at, b"instanceof")
|| whole_word_at(self.bytes, at, b"in"))
.then_some(11),
_ => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum Continuation {
Postfix,
Operator,
}
pub(super) const EXPONENT_PRECEDENCE: u8 = 15;
#[cfg(test)]
mod tests {
use super::Source;
fn precedence(text: &str) -> Option<u8> {
Source::new(text).binary_operator_precedence(0)
}
#[test]
fn lexes_binary_operators_longest_match() {
for (text, expected) in [
("**", Some(15)),
("*", Some(14)),
("/ c", Some(14)),
("%", Some(14)),
("+", Some(13)),
("-", Some(13)),
("<<", Some(12)),
(">>", Some(12)),
(">>>", Some(12)),
("<", Some(11)),
("<=", Some(11)),
(">", Some(11)),
(">=", Some(11)),
("instanceof", Some(11)),
("in x", Some(11)),
("==", Some(10)),
("!==", Some(10)),
("&", Some(9)),
("^", Some(8)),
("|", Some(7)),
("&&", Some(6)),
("||", Some(5)),
("??", Some(4)),
] {
assert_eq!(precedence(text), expected, "{text}");
}
}
#[test]
fn rejects_everything_else() {
for text in [
"**=", "*=", "/=", "%=", "+=", "-=", "++", "--", "<<=", ">>=", ">>>=", "=", "=>", "&=",
"&&=", "|=", "||=", "^=", "??=", "?", "?.", "!", ")", ";", "inner", "index", "",
] {
assert_eq!(precedence(text), None, "{text}");
}
}
#[test]
fn continuation_tokens() {
use super::Continuation::{Operator, Postfix};
for (text, expected) in [
("(x)", Some(Postfix)),
("[k] = 1", Some(Postfix)),
("`t`", Some(Postfix)),
("<T>(x) => x", Some(Postfix)),
("/re/", Some(Operator)),
("*gen() {}", Some(Operator)),
("+x", Some(Operator)),
("- x", Some(Operator)),
("in = 1", Some(Operator)),
("instanceof()", Some(Operator)),
("++x", None),
("--x", None),
("inner", None),
("x", None),
("{}", None),
("", None),
] {
assert_eq!(Source::new(text).continuation(0), expected, "{text}");
}
}
#[test]
fn skips_trivia_before_the_operator() {
assert_eq!(precedence(" /* c */\n// d\n / e"), Some(14));
}
}