vectorcraft/crates/scriptcheck/src/lex.rs

850 lines
32 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 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 `#` and a name away from the start of a line.
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}')
}
fn ident_start(c: char) -> bool {
c == '$' || c == '_' || c.is_ascii_alphabetic() || (!c.is_ascii() && c.is_alphabetic())
}
/// A character a name may hold after its first.
pub(crate) fn ident_part(c: char) -> bool {
ident_start(c) || c.is_ascii_digit() || (!c.is_ascii() && c.is_alphanumeric())
}
/// 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): letters, digits, `$`, `_` and `\u`
/// escapes.
fn name(&mut self) {
loop {
match self.peek() {
Some('\\') if self.peek_at(1) == Some('u') => {
self.skip(2);
for _ in 0..4 {
match self.peek() {
Some(h) if h.is_ascii_hexdigit() => self.bump(h),
_ => break,
}
}
}
Some(c) if ident_part(c) => self.bump(c),
_ => break,
}
}
}
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> 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.until_line_break();
T::Directive
} else if c == '#' && self.peek_at(1).is_some_and(ident_start) {
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 == '\\' && self.peek_at(1) == Some('u')) {
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.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),
_ => {}
}
}
}
}
Some(Token { kind, text, line, problem, stmt, expr, start })
}
}