Files
vectorcraft/crates/scriptcheck/src/lex.rs
T
dotCooCoo f388a795ce Script checker: a private name that starts with a directive's name is read to its end
Inside braces, a line such as `#include$size = 1` was taken for an `#include` line, because only the ASCII letters, digits and `_` after the `#` were compared with the directive names. The name now runs over every character a name may hold, written out or as an escape, before the comparison.
2026-11-11 22:49:43 -08:00

1010 lines
38 KiB
Rust

//! The lexer: script text into tokens that cover it exactly once, blanks, line breaks and comments
//! included. It reads ECMAScript 3 with E4X XML literals, the preprocessor lines (`#name …` at the
//! start of a line, and `//@name …` comments there that name a directive), and the later syntax a
//! script may hold (template literals, `=>`, `?.`), which the analysis reports. An XML literal's
//! markup gives tokens of its own, and the code in its braces gives the same tokens as code anywhere
//! else. Each step moves forward through the text, and nothing recurses: an XML literal's elements
//! are counted, and the open parentheses, the open braces, the `?` of conditionals at each bracket
//! level and the levels of XML literals and their code are kept on stacks capped at the nesting
//! limit.
use std::cmp::Ordering;
use unicode_properties::{GeneralCategory as G, UnicodeGeneralCategory};
use crate::catalog;
use crate::report::{TOO_DEEP, UNTERMINATED_COMMENT, UNTERMINATED_REGEXP, UNTERMINATED_STRING, UNTERMINATED_TEMPLATE};
/// What a token is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum T {
/// Blanks: spaces, tabs and other white space.
Space,
/// One line break: LF, CR LF, CR, U+2028 or U+2029.
Newline,
/// A `//` or `/* */` comment.
Comment,
/// A preprocessor line: `#name …` at the start of a line, or a `//@name …` comment there that
/// names a directive.
Directive,
Ident,
Number,
/// A `'…'` or `"…"` string literal.
Str,
/// A `` `…` `` template literal.
Template,
RegExp,
/// The start of an E4X XML literal (`<a b="1">…</a>`, `<>…</>`, `<!-- … -->`): its markup up to
/// its end, or up to and with the `{` of the code in it.
Xml,
/// More of an XML literal's markup: from the `}` that ends the code in it up to the literal's
/// end or the next `{`.
XmlMore,
Punct,
/// A character no token starts with, or a private name: `#` and a name away from the start of a
/// line, or at its start where the name starts with an escape, or inside braces where the name
/// is not a directive's.
Other,
}
/// A token: what it is, its text and the line it starts on (from 1), with the problem it has.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Token<'s> {
pub kind: T,
pub text: &'s str,
pub line: usize,
pub problem: Option<&'static str>,
/// A statement may start at the token (a `function` there is a declaration).
pub stmt: bool,
/// An expression may start at the token (a `/` there starts a regular expression).
pub expr: bool,
/// The byte offset in the text where the token starts.
pub start: usize,
}
/// The kinds of line break the text has used.
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Breaks {
pub lf: bool,
pub crlf: bool,
pub cr: bool,
}
/// Punctuators, longest first: ECMAScript 3's, E4X's (`..`, `.@`, `::`, `@`) and later ones (`=>`,
/// `...`, `?.`, `??`, `**` and the assignments built on them), which the analysis reports.
const PUNCTS: &[&str] = &[
">>>=", "===", "!==", ">>>", "<<=", ">>=", "**=", "...", "&&=", "||=", "??=", "==", "!=", "<=", ">=", "&&", "||", "++", "--", "+=", "-=", "*=",
"/=", "%=", "&=", "|=", "^=", "<<", ">>", "=>", "**", "?.", "??", "::", "..", ".@", "{", "}", "(", ")", "[", "]", ";", ",", "<", ">", "+", "-",
"*", "/", "%", "&", "|", "^", "!", "~", "?", ":", "=", ".", "@",
];
pub(crate) struct Lexer<'s> {
src: &'s str,
pos: usize,
line: usize,
/// Only blanks since the last line break (or the start): a `#` here starts a preprocessor line.
line_start: bool,
/// An expression may start here: a `/` starts a regular expression and a `<` an XML literal.
expr: bool,
/// A statement may start here: a `{` here opens a block. A `{` where only an expression may
/// start opens an object literal.
stmt: bool,
/// The last significant token was `return`, `throw`, `break` or `continue`: a line break here
/// ends the statement (ECMA-262 3rd edition, section 7.9.1).
restricted: bool,
/// The last significant token was `.`, `..`, `.@` or `::`: a name here is a property's, also when
/// it is a keyword such as `return`.
member: bool,
/// The last significant token was `if`, `while`, `for`, `with` or the `each` of `for each`: a
/// `(` here opens a condition.
cond: bool,
/// For each `(` still open, whether it opened a condition, up to `nesting` of them; `deeper`
/// counts the ones past that.
parens: Vec<bool>,
deeper: usize,
/// For each `{` still open, what it opened, up to `nesting` of them; `deeper_braces` counts the
/// ones past that.
braces: Vec<Brace>,
deeper_braces: usize,
/// `function` was read and its body's `{` is still to come: whether the function is an
/// expression (it starts where no statement may).
function: Option<bool>,
/// For the text outside any bracket and each bracket still open, how many `?` of a conditional
/// (`? :`) wait for their `:` there, up to `nesting` levels; `deeper_questions` counts the
/// levels past that.
questions: Vec<usize>,
deeper_questions: usize,
/// The XML literal being read, its innermost level last: its markup, the code in a `{…}` in
/// it, an XML literal in that code, and so on. Empty outside an XML literal.
xml: Vec<Level>,
/// The line the outermost XML literal being read starts on.
xml_line: usize,
/// The most open parentheses kept, and the most levels in `xml`.
nesting: usize,
/// The line of the XML literal the text ended in, when it ended in one.
pub unclosed_xml: Option<usize>,
pub breaks: Breaks,
/// One per character read (and per token): the work done.
pub steps: usize,
}
fn is_break(c: char) -> bool {
matches!(c, '\n' | '\r' | '\u{2028}' | '\u{2029}')
}
fn is_blank(c: char) -> bool {
!is_break(c) && (c.is_whitespace() || c == '\u{feff}')
}
/// A character a name may start with: a Unicode `ID_Start` character, `$` or `_` (the Names and
/// Keywords clause of current ECMA-262 editions). `ID_Start` is the letters (categories Lu, Ll, Lt,
/// Lm, Lo and Nl) and `Other_ID_Start`, less `Pattern_Syntax` and `Pattern_White_Space` (Unicode
/// Standard Annex #31). The categories come from Unicode 17.0.
pub(crate) fn ident_start(c: char) -> bool {
match c {
'$' | '_' => true,
_ if c.is_ascii() => c.is_ascii_alphabetic(),
_ if is_other_id_start(c) => true,
_ => {
matches!(
c.general_category(),
G::UppercaseLetter | G::LowercaseLetter | G::TitlecaseLetter | G::ModifierLetter | G::OtherLetter | G::LetterNumber
) && !is_pattern_syntax(c)
}
}
}
/// A character a name may hold after its first: a Unicode `ID_Continue` character or `$`.
/// `ID_Continue` adds to `ID_Start` the decimal digits (Nd), combining marks (Mn, Mc), connector
/// punctuation (Pc, which holds `_`) and `Other_ID_Continue` (which holds the zero width non-joiner
/// and joiner), less `Pattern_Syntax` and `Pattern_White_Space`.
pub(crate) fn ident_part(c: char) -> bool {
match c {
'0'..='9' => true,
_ if ident_start(c) => true,
_ if c.is_ascii() => false,
_ if is_other_id_continue(c) => true,
_ => matches!(c.general_category(), G::NonspacingMark | G::SpacingMark | G::DecimalNumber | G::ConnectorPunctuation) && !is_pattern_syntax(c),
}
}
/// The character the escape at the start of `s` stands for and the escape's length in bytes: `\u`
/// and four hexadecimal digits, or `\u{…}` with one or more hexadecimal digits of a value up to
/// U+10FFFF, leading zeros allowed (ECMA-262, UnicodeEscapeSequence and CodePoint). `None` when `s`
/// starts with neither, or when the escape gives a lone surrogate.
pub(crate) fn escape(s: &str) -> Option<(char, usize)> {
let rest = s.strip_prefix("\\u")?;
let Some(braced) = rest.strip_prefix('{') else {
let c = rest
.get(..4)
.filter(|hex| hex.bytes().all(|b| b.is_ascii_hexdigit()))
.and_then(|hex| u32::from_str_radix(hex, 16).ok())
.and_then(char::from_u32)?;
return Some((c, 6));
};
// The value stops growing past U+10FFFF, so any number of digits is read once and never
// overflows.
let mut value: u32 = 0;
for (i, b) in braced.bytes().enumerate() {
match b {
b'}' if i > 0 => return char::from_u32(value).map(|c| (c, i + 4)),
_ => {
let digit = char::from(b).to_digit(16)?;
value = (value * 16 + digit).min(0x11_0000);
}
}
}
None
}
/// `Other_ID_Start` (Unicode PropList 17.0): characters kept in `ID_Start` though their category is
/// not a letter.
fn is_other_id_start(c: char) -> bool {
matches!(c, '\u{1885}' | '\u{1886}' | '\u{2118}' | '\u{212E}' | '\u{309B}' | '\u{309C}')
}
/// `Other_ID_Continue` (Unicode PropList 17.0): characters kept in `ID_Continue` though their
/// category is not a letter, mark, digit or connector.
fn is_other_id_continue(c: char) -> bool {
matches!(c, '\u{B7}' | '\u{387}' | '\u{1369}'..='\u{1371}' | '\u{19DA}' | '\u{200C}' | '\u{200D}' | '\u{30FB}' | '\u{FF65}')
}
/// Non-ASCII `Pattern_Syntax` ranges (Unicode PropList). Unicode does not change this set, and no
/// identifier character belongs to it. `Pattern_White_Space` outside ASCII (U+0085, U+200E, U+200F,
/// U+2028 and U+2029) holds no letter, mark, digit or connector, so the category checks leave it out.
const PATTERN_SYNTAX: [(char, char); 23] = [
('\u{A1}', '\u{A7}'),
('\u{A9}', '\u{A9}'),
('\u{AB}', '\u{AC}'),
('\u{AE}', '\u{AE}'),
('\u{B0}', '\u{B1}'),
('\u{B6}', '\u{B6}'),
('\u{BB}', '\u{BB}'),
('\u{BF}', '\u{BF}'),
('\u{D7}', '\u{D7}'),
('\u{F7}', '\u{F7}'),
('\u{2010}', '\u{2027}'),
('\u{2030}', '\u{203E}'),
('\u{2041}', '\u{2053}'),
('\u{2055}', '\u{205E}'),
('\u{2190}', '\u{245F}'),
('\u{2500}', '\u{2775}'),
('\u{2794}', '\u{2BFF}'),
('\u{2E00}', '\u{2E7F}'),
('\u{3001}', '\u{3003}'),
('\u{3008}', '\u{3020}'),
('\u{3030}', '\u{3030}'),
('\u{FD3E}', '\u{FD3F}'),
('\u{FE45}', '\u{FE46}'),
];
/// Whether a non-ASCII character is `Pattern_Syntax`.
fn is_pattern_syntax(c: char) -> bool {
PATTERN_SYNTAX
.binary_search_by(|&(lo, hi)| {
if hi < c {
Ordering::Less
} else if lo > c {
Ordering::Greater
} else {
Ordering::Equal
}
})
.is_ok()
}
/// After `<`, the start of an XML literal: a name, `>` (a list), `!` (a comment or CDATA), `?` (a
/// processing instruction) or `{` (a computed name).
fn xml_start(c: char) -> bool {
c.is_ascii_alphabetic() || matches!(c, '_' | '>' | '!' | '?' | '{')
}
/// Whether an expression may start after a significant token. After a `}`, the brace's kind decides
/// it ([`Lexer::brace`]), after an XML literal's markup [`Lexer::markup_token`] sets it, and after a
/// property's name, a keyword included ([`Lexer::member`]), none starts.
pub(crate) fn expr_after(kind: T, text: &str) -> bool {
match kind {
T::Ident => matches!(text, "return" | "typeof" | "instanceof" | "in" | "new" | "delete" | "void" | "throw" | "case" | "do" | "else"),
T::Number | T::Str | T::Template | T::RegExp => false,
T::Punct => !matches!(text, ")" | "]" | "++" | "--"),
_ => true,
}
}
/// `rest`, the text after `//@`, starts with a directive's name.
fn names_directive(rest: &str) -> bool {
let end = rest.find(|c: char| !c.is_ascii_alphabetic()).unwrap_or(rest.len());
let name = rest.get(..end).unwrap_or_default();
catalog::DIRECTIVES.iter().any(|d| d.eq_ignore_ascii_case(name))
}
/// Where in its markup an XML literal is read.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum At {
/// The `<` of a tag, comment, CDATA section or processing instruction.
Markup,
/// A start tag (`end` false) or an end tag, after its `<`.
Tag { end: bool },
/// An element's text.
Text,
}
/// A level of the XML literal being read.
#[derive(Clone, Copy, Debug)]
enum Level {
/// The markup of an XML literal, `open` of its elements open.
Xml { open: usize, at: At },
/// The code in braces, `braces` of them open.
Code { braces: usize },
}
/// What a `{` opened.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Brace {
/// A block, or a function declaration's body: a statement may start after its `}`.
Block,
/// An object literal: a value.
Object,
/// A function expression's body: statements inside, and a value after its `}`.
Function,
}
/// What reading a little more of an XML literal's markup led to.
enum Step {
/// The markup goes on.
On,
/// The literal ends: none of its elements is open.
Up,
/// Code in braces starts at the next character, a `{`.
Code,
/// The text ended first.
End,
}
impl<'s> Lexer<'s> {
/// A lexer of `src` that keeps at most `nesting` open parentheses, open braces, bracket levels
/// of conditionals and levels of an XML literal ([`crate::Limits::nesting`]).
pub fn new(src: &'s str, nesting: usize) -> Self {
Self {
src,
pos: 0,
line: 1,
line_start: true,
expr: true,
stmt: true,
restricted: false,
member: false,
cond: false,
parens: vec![],
deeper: 0,
braces: vec![],
deeper_braces: 0,
function: None,
questions: vec![0],
deeper_questions: 0,
xml: vec![],
xml_line: 0,
nesting,
unclosed_xml: None,
breaks: Breaks::default(),
steps: 0,
}
}
/// The lines read so far: the line breaks, and one more for a last line without one.
pub fn lines(&self) -> usize {
let last = self.src.chars().next_back();
self.line - usize::from(last.is_none_or(is_break))
}
fn rest(&self) -> &'s str {
self.src.get(self.pos..).unwrap_or_default()
}
fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
fn peek_at(&self, n: usize) -> Option<char> {
self.rest().chars().nth(n)
}
fn starts(&self, s: &str) -> bool {
self.rest().starts_with(s)
}
fn bump(&mut self, c: char) {
self.pos += c.len_utf8();
self.steps += 1;
}
/// Read `n` ASCII characters.
fn skip(&mut self, n: usize) {
self.pos += n;
self.steps += 1;
}
/// A line break here, read: false when there is none.
fn line_break(&mut self) -> bool {
match self.peek() {
Some('\n') => {
self.pos += 1;
self.breaks.lf = true;
}
Some('\r') if self.starts("\r\n") => {
self.pos += 2;
self.breaks.crlf = true;
}
Some('\r') => {
self.pos += 1;
self.breaks.cr = true;
}
Some(c @ ('\u{2028}' | '\u{2029}')) => self.pos += c.len_utf8(),
_ => return false,
}
self.line += 1;
self.steps += 1;
true
}
/// Up to the next line break.
fn until_line_break(&mut self) {
while let Some(c) = self.peek().filter(|&c| !is_break(c)) {
self.bump(c);
}
}
/// The rest of a name (its first character included): the characters [`ident_part`] allows,
/// written out or as `\uXXXX` or `\u{…}` escapes ([`escape`]). An escape for any other
/// character, or a malformed one, ends the name. Each character of an escape is a step.
fn name(&mut self) {
loop {
match self.peek() {
Some('\\') => match escape(self.rest()) {
Some((c, len)) if ident_part(c) => {
self.pos += len;
self.steps += len;
}
_ => break,
},
Some(c) if ident_part(c) => self.bump(c),
_ => break,
}
}
}
/// A name starts right after the `#` here: its first character written out, or an escape for a
/// character a name may start with.
fn private_name_follows(&self) -> bool {
let after = self.rest().get(1..).unwrap_or_default();
match after.chars().next() {
Some('\\') => escape(after).is_some_and(|(c, _)| ident_start(c)),
Some(c) => ident_start(c),
None => false,
}
}
/// The `#` at the start of this line starts a private name, not a preprocessor line: the name
/// starts with an escape, which no directive's name holds, or [`Lexer::private_line`] holds for
/// it.
fn private_at_line_start(&self) -> bool {
if !self.private_name_follows() {
return false;
}
let after = self.rest().get(1..).unwrap_or_default();
after.starts_with('\\') || self.private_line(after)
}
/// A line that starts with `#` and then `rest` is a private name here, not a preprocessor line:
/// the line is inside braces or in the code of an XML literal, and the name `rest` starts with
/// is not a directive's. The name runs over every character a name may hold, written out or as
/// an escape, so `#include$size` is not `#include`.
pub fn private_line(&self, rest: &str) -> bool {
let in_code = !self.braces.is_empty() || self.deeper_braces > 0 || !self.xml.is_empty();
let mut end = 0;
while let Some(more) = rest.get(end..) {
let len = match more.chars().next() {
Some('\\') => escape(more).filter(|&(c, _)| ident_part(c)).map(|(_, len)| len),
Some(c) if ident_part(c) => Some(c.len_utf8()),
_ => None,
};
let Some(len) = len else { break };
end += len;
}
let name = rest.get(..end).unwrap_or_default();
in_code && !catalog::DIRECTIVES.iter().any(|d| d.eq_ignore_ascii_case(name))
}
fn digits(&mut self, ok: fn(char) -> bool) {
while let Some(c) = self.peek().filter(|&c| ok(c) || c == '_') {
self.bump(c);
}
}
/// A number: decimal (with a fraction and an exponent), hexadecimal, or the later binary,
/// octal and `n` (big integer) forms.
fn number(&mut self) {
let b = self.rest().as_bytes();
match (b.first(), b.get(1)) {
(Some(b'0'), Some(b'x' | b'X')) => {
self.skip(2);
self.digits(|c| c.is_ascii_hexdigit());
}
(Some(b'0'), Some(b'b' | b'B' | b'o' | b'O')) => {
self.skip(2);
self.digits(|c| c.is_ascii_digit());
}
_ => {
self.digits(|c| c.is_ascii_digit());
if self.peek() == Some('.') {
self.bump('.');
self.digits(|c| c.is_ascii_digit());
}
let signed = matches!(self.peek_at(1), Some('+' | '-')) && self.peek_at(2).is_some_and(|c| c.is_ascii_digit());
if matches!(self.peek(), Some('e' | 'E')) && (self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) || signed) {
self.skip(if signed { 2 } else { 1 });
self.digits(|c| c.is_ascii_digit());
}
}
}
if self.peek() == Some('n') {
self.bump('n');
}
}
/// A `'…'` or `"…"` string, which ends at its line's end when it isn't closed.
fn string(&mut self, quote: char) -> Option<&'static str> {
self.bump(quote);
loop {
match self.peek() {
None => return Some(UNTERMINATED_STRING),
Some(c) if c == quote => {
self.bump(c);
return None;
}
Some('\\') => {
self.bump('\\');
// A backslash before a line break carries the string on to the next line.
if !self.line_break()
&& let Some(c) = self.peek()
{
self.bump(c);
}
}
Some(c) if is_break(c) => return Some(UNTERMINATED_STRING),
Some(c) => self.bump(c),
}
}
}
fn template(&mut self) -> Option<&'static str> {
self.bump('`');
loop {
if self.line_break() {
continue;
}
match self.peek() {
None => return Some(UNTERMINATED_TEMPLATE),
Some('`') => {
self.bump('`');
return None;
}
Some('\\') => {
self.bump('\\');
if !self.line_break()
&& let Some(c) = self.peek()
{
self.bump(c);
}
}
Some(c) => self.bump(c),
}
}
}
/// A regular expression and its flags, which ends at its line's end when it isn't closed.
fn regexp(&mut self) -> Option<&'static str> {
self.bump('/');
let mut class = false;
loop {
match self.peek() {
None => return Some(UNTERMINATED_REGEXP),
Some(c) if is_break(c) => return Some(UNTERMINATED_REGEXP),
Some('\\') => {
self.bump('\\');
if let Some(c) = self.peek().filter(|&c| !is_break(c)) {
self.bump(c);
}
}
Some('/') if !class => {
self.bump('/');
break;
}
Some(c) => {
match c {
'[' => class = true,
']' => class = false,
_ => {}
}
self.bump(c);
}
}
}
while let Some(c) = self.peek().filter(|&c| ident_part(c)) {
self.bump(c);
}
None
}
fn block_comment(&mut self) -> Option<&'static str> {
self.skip(2);
loop {
if self.starts("*/") {
self.skip(2);
return None;
}
if self.line_break() {
continue;
}
match self.peek() {
Some(c) => self.bump(c),
None => return Some(UNTERMINATED_COMMENT),
}
}
}
/// Up to and past `end`: false when the text ends first.
fn past(&mut self, end: &str) -> bool {
loop {
if self.starts(end) {
self.skip(end.len());
return true;
}
if self.line_break() {
continue;
}
match self.peek() {
Some(c) => self.bump(c),
None => return false,
}
}
}
/// A token of `kind` that starts at `start` on `line`: the markup of the XML literal being
/// read, up to the literal's end or up to and with the `{` of the code in it. A `{` whose code
/// would take a level past `nesting` is read as markup, and the token has the problem
/// [`TOO_DEEP`]. When the text ends first, the literal is left unclosed
/// ([`Lexer::unclosed_xml`]). `stmt`: a statement may start at the token; `expr`: an expression
/// may start at it.
fn markup_token(&mut self, start: usize, line: usize, kind: T, stmt: bool, expr: bool) -> Token<'s> {
let mut problem = None;
while let Some(&Level::Xml { mut open, mut at }) = self.xml.last() {
let step = self.markup(&mut open, &mut at);
if let Some(level) = self.xml.last_mut() {
*level = Level::Xml { open, at };
}
match step {
Step::On => {}
Step::Code if self.xml.len() < self.nesting => {
self.bump('{');
self.xml.push(Level::Code { braces: 1 });
self.open_level();
self.expr = true;
break;
}
Step::Code => {
self.bump('{');
problem = Some(TOO_DEEP);
}
Step::Up => {
// An XML literal is a value: no expression starts after it.
self.xml.pop();
self.expr = false;
break;
}
Step::End => {
self.xml.clear();
self.unclosed_xml = Some(self.xml_line);
self.expr = false;
break;
}
}
}
// The code in an XML literal's braces is an expression, and so is the literal: no statement
// starts after either.
self.line_start = false;
self.stmt = false;
self.cond = false;
self.restricted = false;
self.member = false;
Token { kind, text: self.src.get(start..self.pos).unwrap_or_default(), line, problem, stmt, expr, start }
}
/// A little more of an XML literal's markup, `open` of its elements open and `at` where in it:
/// a comment, CDATA section or processing instruction whole, or one character of a tag or of
/// an element's text.
fn markup(&mut self, open: &mut usize, at: &mut At) -> Step {
match *at {
At::Markup => {
let read = if self.starts("<!--") {
self.past("-->")
} else if self.starts("<![CDATA[") {
self.past("]]>")
} else if self.starts("<?") {
self.past("?>")
} else {
let end = self.starts("</");
self.skip(if end { 2 } else { 1 });
*at = At::Tag { end };
return Step::On;
};
*at = At::Text;
match (read, *open) {
(false, _) => Step::End,
(true, 0) => Step::Up,
(true, _) => Step::On,
}
}
At::Tag { end } => {
if self.line_break() {
return Step::On;
}
// `>` ends a tag, and `/>` a tag whose element closes itself.
let closes_itself = match self.peek() {
None => return Step::End,
Some('>') => {
self.bump('>');
false
}
Some('/') if self.peek_at(1) == Some('>') => {
self.skip(2);
true
}
Some(q @ ('"' | '\'')) => {
self.bump(q);
return if self.past(if q == '"' { "\"" } else { "'" }) { Step::On } else { Step::End };
}
Some('{') => return Step::Code,
Some(c) => {
self.bump(c);
return Step::On;
}
};
*open = match (end, closes_itself) {
(true, _) => open.saturating_sub(1),
(false, false) => open.saturating_add(1),
(false, true) => *open,
};
*at = At::Text;
if *open == 0 { Step::Up } else { Step::On }
}
At::Text => {
if self.line_break() {
return Step::On;
}
match self.peek() {
None => Step::End,
Some('<') => {
*at = At::Markup;
Step::On
}
Some('{') => Step::Code,
Some(c) => {
self.bump(c);
Step::On
}
}
}
}
}
fn punct(&self) -> Option<&'static str> {
let rest = self.rest();
// `?.` before a digit is `?` and a number (`a ?.5 : 1`).
PUNCTS.iter().copied().find(|p| rest.starts_with(p) && !(*p == "?." && rest.as_bytes().get(2).is_some_and(u8::is_ascii_digit)))
}
/// The parentheses after the punctuator `punct`: `(` opens one, a condition when `cond` is set,
/// and `)` closes the last one open. True when `punct` closes a condition.
fn paren(&mut self, punct: &str) -> bool {
match punct {
"(" if self.parens.len() < self.nesting => self.parens.push(self.cond),
"(" => self.deeper = self.deeper.saturating_add(1),
")" if self.deeper > 0 => self.deeper -= 1,
")" => return self.parens.pop().unwrap_or(false),
_ => {}
}
false
}
/// The braces after the punctuator `punct`: `{` opens one and `}` closes the last one open. For
/// `{` and `}`, what the brace opened: the body of the function just read, a block where a
/// statement may start or no expression may, else an object literal. A brace past `nesting` is
/// taken as a block's when it closes.
fn brace(&mut self, punct: &str) -> Option<Brace> {
match punct {
"{" => {
let brace = match self.function.take() {
Some(true) => Brace::Function,
Some(false) => Brace::Block,
None if self.stmt || !self.expr => Brace::Block,
None => Brace::Object,
};
if self.braces.len() < self.nesting {
self.braces.push(brace);
} else {
self.deeper_braces = self.deeper_braces.saturating_add(1);
}
Some(brace)
}
"}" if self.deeper_braces > 0 => {
self.deeper_braces -= 1;
Some(Brace::Block)
}
"}" => Some(self.braces.pop().unwrap_or(Brace::Block)),
_ => None,
}
}
/// A bracket opens: the `?` inside it are counted apart from the ones outside.
fn open_level(&mut self) {
if self.questions.len() <= self.nesting {
self.questions.push(0);
} else {
self.deeper_questions = self.deeper_questions.saturating_add(1);
}
}
/// The innermost bracket closes.
fn close_level(&mut self) {
if self.deeper_questions > 0 {
self.deeper_questions -= 1;
} else if self.questions.len() > 1 {
self.questions.pop();
}
}
/// The punctuator `punct` at the innermost level: `?` opens a conditional there, and a `:`
/// there while one is open is that conditional's. True for that `:`.
fn conditional(&mut self, punct: &str) -> bool {
if self.deeper_questions > 0 {
return false;
}
let Some(open) = self.questions.last_mut() else { return false };
match punct {
"?" => *open = open.saturating_add(1),
":" if *open > 0 => {
*open -= 1;
return true;
}
_ => {}
}
false
}
/// A line break after the last significant token. After the end of an expression a statement
/// may start (JavaScript ends the statement at the line break when the next token can't go on
/// with it), and after `return`, `throw`, `break` and `continue` a new statement starts.
fn line_ended(&mut self) {
if std::mem::take(&mut self.restricted) {
self.expr = true;
self.stmt = true;
}
self.stmt |= !self.expr;
}
}
impl<'s> Lexer<'s> {
/// Update expression, statement and nesting state after reading a significant token.
fn update_context(&mut self, kind: T, text: &str) {
self.line_start = false;
// A name after `.`, `..`, `.@` or `::` is a property's, also when it is a keyword: no
// expression or statement starts after it.
let property = kind == T::Ident && self.member;
// After the `)` of a condition comes a statement, which may start with a regular
// expression or an XML literal.
let condition = kind == T::Punct && self.paren(text);
// A statement may also start after the `}` of a block. After the `}` of an object
// literal or of a function expression's body, `/` divides.
let brace = if kind == T::Punct { self.brace(text) } else { None };
self.expr = condition
|| match (text, brace) {
("}", Some(brace)) => brace == Brace::Block,
_ => !property && expr_after(kind, text),
};
if kind == T::Ident && !property && text == "function" {
self.function = Some(!self.stmt);
}
if kind == T::Punct {
match text {
"(" | "[" | "{" => self.open_level(),
")" | "]" | "}" => self.close_level(),
_ => {}
}
}
let ternary = kind == T::Punct && self.conditional(text);
// A statement may start inside a block or a function's body, after `;`, `=>`, a
// preprocessor line, `else`, `do`, `try` and `finally`, and after the `:` of a label or
// a `case` (not a conditional's, and outside an object literal).
self.stmt = condition
|| match (text, brace) {
("{", Some(brace)) => brace != Brace::Object,
("}", Some(brace)) => brace == Brace::Block,
_ => false,
}
|| kind == T::Directive
|| (kind == T::Punct && matches!(text, ";" | "=>"))
|| (kind == T::Punct && text == ":" && !ternary && self.braces.last() != Some(&Brace::Object))
|| (kind == T::Ident && !property && matches!(text, "else" | "do" | "try" | "finally"));
self.cond = kind == T::Ident && !property && (matches!(text, "if" | "while" | "for" | "with") || (text == "each" && self.cond));
self.restricted = kind == T::Ident && !property && matches!(text, "return" | "throw" | "break" | "continue");
self.member = kind == T::Punct && matches!(text, "." | ".." | ".@" | "::");
// The braces inside the code of an XML literal.
if kind == T::Punct
&& let Some(Level::Code { braces }) = self.xml.last_mut()
{
match text {
"{" => *braces = braces.saturating_add(1),
"}" => *braces = braces.saturating_sub(1),
_ => {}
}
}
}
}
impl<'s> Iterator for Lexer<'s> {
type Item = Token<'s>;
fn next(&mut self) -> Option<Token<'s>> {
let (start, line, stmt, expr) = (self.pos, self.line, self.stmt, self.expr);
let Some(c) = self.peek() else {
// The text ends in the code of an XML literal.
if !self.xml.is_empty() {
self.xml.clear();
self.unclosed_xml = Some(self.xml_line);
}
return None;
};
let mut problem = None;
let kind = if self.line_break() {
T::Newline
} else if is_blank(c) {
while let Some(c) = self.peek().filter(|&c| is_blank(c)) {
self.bump(c);
}
T::Space
} else if self.starts("//") {
let directive = self.line_start && self.rest().strip_prefix("//@").is_some_and(names_directive);
self.until_line_break();
if directive { T::Directive } else { T::Comment }
} else if self.starts("/*") {
problem = self.block_comment();
T::Comment
} else if c == '#' && self.line_start && !self.private_at_line_start() {
self.until_line_break();
T::Directive
} else if c == '#' && self.private_name_follows() {
self.bump('#');
self.name();
T::Other
} else if c == '\'' || c == '"' {
problem = self.string(c);
T::Str
} else if c == '`' {
problem = self.template();
T::Template
} else if c == '/' && self.expr {
problem = self.regexp();
T::RegExp
} else if c == '<' && self.expr && self.peek_at(1).is_some_and(xml_start) {
if self.xml.len() < self.nesting {
// An XML literal starts, in the script or in the code of another one.
if self.xml.is_empty() {
self.xml_line = line;
}
self.xml.push(Level::Xml { open: 0, at: At::Markup });
return Some(self.markup_token(start, line, T::Xml, stmt, expr));
}
// An XML literal more levels deep than are kept: its `<` is read as an operator.
self.skip(1);
problem = Some(TOO_DEEP);
T::Punct
} else if c == '}' && matches!(self.xml.last(), Some(Level::Code { braces: 1 })) {
// The `}` that ends the code in an XML literal's braces: the literal's markup goes on.
self.xml.pop();
self.close_level();
self.bump('}');
return Some(self.markup_token(start, line, T::XmlMore, stmt, expr));
} else if ident_start(c) || (c == '\\' && escape(self.rest()).is_some_and(|(c, _)| ident_start(c))) {
self.name();
T::Ident
} else if c.is_ascii_digit() || (c == '.' && self.peek_at(1).is_some_and(|d| d.is_ascii_digit())) {
self.number();
T::Number
} else if let Some(p) = self.punct() {
self.skip(p.len());
T::Punct
} else {
self.bump(c);
T::Other
};
let text = self.src.get(start..self.pos).unwrap_or_default();
match kind {
T::Space => {}
T::Newline => {
self.line_start = true;
self.line_ended();
}
// A comment that holds a line break counts as one.
T::Comment => {
self.line_start = false;
if text.contains(is_break) {
self.line_ended();
}
}
_ => self.update_context(kind, text),
}
Some(Token { kind, text, line, problem, stmt, expr, start })
}
}