mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
1400 lines
59 KiB
Rust
1400 lines
59 KiB
Rust
//! The analysis: a pass over a file's tokens that finds the names each function body and the global
|
|
//! code declare ([`Declared`]), then one pass that finds its directives, platform features,
|
|
//! document-model member chains, menu commands and problems. Each token is looked at together with
|
|
//! the four significant tokens before it. Open brackets are kept on a capped stack, a member chain
|
|
//! whose index or call is still open waits on a second one, and the function bodies being read are
|
|
//! kept on a third, each with what its parameters, the names it declares and the names its
|
|
//! assignments empty held outside it. A function body's parameters and the names it declares are
|
|
//! its own from its `{` on, and the names the global code's function declarations declare are the
|
|
//! script's own from the start of the file. The `catch` blocks being read are kept on a fourth,
|
|
//! each with what its parameter held outside it. The values in grouping parentheses being read
|
|
//! (`name = (…)`) are kept on a fifth, each with the operand read so far in its innermost pair, and
|
|
//! the name holds the value when the token after the last `)` ends it. A value without parentheses
|
|
//! that starts with `File`, `Folder` or a variable holding a File (`name = new File(…)`) is kept
|
|
//! there too, as one operand, and the name holds it when the token after the operand ends the
|
|
//! value. The value of a compound assignment (`name += …`) is kept there as well: the name holds what
|
|
//! it held while the value is read, and nothing the check follows after it. The conditional
|
|
//! expressions whose `:` is still to come are kept on a sixth, so that the `:` of one inside a value
|
|
//! doesn't end the value. All grouping parentheses being read are kept on a seventh, each with the
|
|
//! operand read so far at its depth, and a call on a File or Folder object in them (`(f).remove()`)
|
|
//! is a call on the object. Nothing recurses.
|
|
|
|
use std::borrow::Cow;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::rc::Rc;
|
|
|
|
use crate::catalog::{self, FeatureId};
|
|
use crate::directives;
|
|
use crate::lex::{Lexer, T, Token, expr_after};
|
|
use crate::report::{self, Acc, OutsideId};
|
|
|
|
mod declared;
|
|
mod features;
|
|
mod sig;
|
|
|
|
pub(crate) use declared::{Declared, declarations};
|
|
use features::platform_name;
|
|
use sig::*;
|
|
|
|
/// An `#include` line: what it names, its line, and the folders `#includepath` named at that point
|
|
/// (one list, shared by every line it applies to).
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct IncludeRef {
|
|
pub value: String,
|
|
pub line: usize,
|
|
pub paths: Rc<[String]>,
|
|
}
|
|
|
|
/// What variables hold, as a check learns it: the member chains assigned to them, and the ones
|
|
/// holding a File or Folder object.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct Known {
|
|
pub bound: HashMap<String, String>,
|
|
pub files: HashSet<String>,
|
|
}
|
|
|
|
impl Known {
|
|
/// The names known.
|
|
pub fn len(&self) -> usize {
|
|
self.bound.len() + self.files.len()
|
|
}
|
|
|
|
/// What `other` adds, up to [`crate::Limits::entries`] names of each kind: a name already
|
|
/// bound keeps its chain.
|
|
pub fn add(&mut self, other: Known, entries: usize) {
|
|
for (name, chain) in other.bound {
|
|
if self.bound.len() < entries || self.bound.contains_key(&name) {
|
|
self.bound.entry(name).or_insert(chain);
|
|
}
|
|
}
|
|
for name in other.files {
|
|
if self.files.len() < entries {
|
|
self.files.insert(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What [`scan`] found besides the tallies it added to.
|
|
pub(crate) struct Scan {
|
|
pub includes: Vec<IncludeRef>,
|
|
/// The file is a JavaScript module (top-level `import` or `export`).
|
|
pub module: bool,
|
|
pub lines: usize,
|
|
/// What the file's variables hold at its end.
|
|
pub known: Known,
|
|
}
|
|
|
|
/// Scan `src` (already through [`unescaped`]): the script itself (`file` none) or a file it includes
|
|
/// (`file`: its path inside the script's folder), with the names it declares (`declared`, from
|
|
/// [`declarations`] on the same `src`), the `#includepath` folders `paths` in force at its start and
|
|
/// its variables holding what `known` says.
|
|
pub(crate) fn scan<'s>(src: &'s str, declared: &Declared<'s>, file: Option<&str>, paths: Rc<[String]>, known: &Known, acc: &mut Acc) -> Scan {
|
|
let mut lexer = Lexer::new(src, acc.limits.nesting);
|
|
let mut a = Analyzer::new(acc, file, paths, known, declared);
|
|
let mut breaks = LineBreaks::default();
|
|
for t in lexer.by_ref() {
|
|
if let Some(code) = t.problem {
|
|
a.problem(code, t.line);
|
|
}
|
|
match breaks.read(&t) {
|
|
Read::Skip => {}
|
|
Read::Directive => a.directive(&t),
|
|
Read::Token(sig) => a.token(sig),
|
|
}
|
|
}
|
|
if let Some(line) = lexer.unclosed_xml {
|
|
a.problem(report::UNTERMINATED_XML, line);
|
|
}
|
|
let (includes, module, known) = a.finish();
|
|
acc.steps = acc.steps.saturating_add(lexer.steps);
|
|
let b = lexer.breaks;
|
|
if usize::from(b.lf) + usize::from(b.crlf) + usize::from(b.cr) > 1 {
|
|
acc.problem(report::MIXED_LINE_ENDINGS, None, file, None);
|
|
}
|
|
Scan { includes, module, lines: lexer.lines(), known }
|
|
}
|
|
|
|
/// `src` with the `\uXXXX` escapes in its names written as the characters they stand for (ECMA-262
|
|
/// 3rd edition, section 7.6), and `src` itself when no name has one. An escape for a character no
|
|
/// name may hold stays as written. Line breaks don't change, so every line keeps its number.
|
|
pub(crate) fn unescaped<'a>(src: Cow<'a, str>, acc: &mut Acc) -> Cow<'a, str> {
|
|
match unescape_names(&src, acc) {
|
|
Some(out) => Cow::Owned(out),
|
|
None => src,
|
|
}
|
|
}
|
|
|
|
fn unescape_names(src: &str, acc: &mut Acc) -> Option<String> {
|
|
if !src.contains("\\u") {
|
|
return None;
|
|
}
|
|
let mut out = String::with_capacity(src.len());
|
|
let mut changed = false;
|
|
let mut lexer = Lexer::new(src, acc.limits.nesting);
|
|
for t in lexer.by_ref() {
|
|
if t.kind == T::Ident && t.text.contains("\\u") {
|
|
let name = unescape_name(t.text);
|
|
changed |= name != t.text;
|
|
out.push_str(&name);
|
|
} else {
|
|
out.push_str(t.text);
|
|
}
|
|
}
|
|
acc.steps = acc.steps.saturating_add(lexer.steps);
|
|
changed.then_some(out)
|
|
}
|
|
|
|
/// The name `text` with each `\uXXXX` escape for a character a name may hold written as that
|
|
/// character.
|
|
fn unescape_name(text: &str) -> String {
|
|
let mut out = String::with_capacity(text.len());
|
|
let mut rest = text;
|
|
while let Some(i) = rest.find("\\u") {
|
|
out.push_str(rest.get(..i).unwrap_or_default());
|
|
let after = rest.get(i + 2..).unwrap_or_default();
|
|
let c = after
|
|
.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)
|
|
.filter(|&c| crate::lex::ident_part(c));
|
|
match c {
|
|
Some(c) => {
|
|
out.push(c);
|
|
rest = after.get(4..).unwrap_or_default();
|
|
}
|
|
None => {
|
|
out.push_str("\\u");
|
|
rest = after;
|
|
}
|
|
}
|
|
}
|
|
out.push_str(rest);
|
|
out
|
|
}
|
|
|
|
/// What the token after a few others decides.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
enum Pending<'s> {
|
|
#[default]
|
|
None,
|
|
/// `Window`: `(` and a string literal with the window's type may follow.
|
|
Window(Option<usize>),
|
|
WindowType(Option<usize>),
|
|
/// `.executeMenuCommand(`: a string literal and `)` may follow.
|
|
Menu(Option<usize>),
|
|
MenuString(Option<usize>, &'s str),
|
|
/// `.open(`: a string literal with the open mode may follow.
|
|
Open(Option<usize>),
|
|
/// A platform name where a property key or a label may stand (first, or after `{`, `,`, `;` or
|
|
/// `}`): it is a key or a label when `:` follows.
|
|
Name(&'s str, Option<usize>),
|
|
}
|
|
|
|
/// `name =` read: the next token says what the variable holds.
|
|
struct Assign {
|
|
name: String,
|
|
/// `new` came first.
|
|
after_new: bool,
|
|
}
|
|
|
|
/// What a value the check follows holds: a member chain, or a File or Folder object.
|
|
enum Held {
|
|
Chain(String),
|
|
File,
|
|
}
|
|
|
|
/// `name = (`: a value in grouping parentheses. Its value is the last operand in each pair (ECMA-262
|
|
/// 3rd edition, sections 11.1.6 and 11.14), and the name holds it when the token after the last `)`
|
|
/// ends the value. A value without parentheses that starts with `File`, `Folder` or a variable
|
|
/// holding a File is read as one operand at the depth it starts at, and the name holds it when the
|
|
/// token after the operand ends the value. The value of a compound assignment is read the same way,
|
|
/// and the name holds nothing the check follows once it ends.
|
|
struct Group {
|
|
/// The number the member chain an operand starts with carries in [`Chain::group`].
|
|
id: usize,
|
|
name: String,
|
|
/// The bracket depth outside the first `(`; for a value without parentheses, the depth it starts
|
|
/// at.
|
|
base: usize,
|
|
/// The bracket depth inside the innermost pair being read; for a value without parentheses, the
|
|
/// depth it starts at.
|
|
level: usize,
|
|
/// The operand being read in that pair, or in a value without parentheses; after the last `)`, the
|
|
/// value.
|
|
operand: Operand,
|
|
/// The last `)` was read.
|
|
closed: bool,
|
|
/// A value without parentheses: the first token at [`Group::level`] that ends a value ends it.
|
|
bare: bool,
|
|
/// The conditionals whose `:` was still to come when the value started ([`Analyzer::conds`]):
|
|
/// the `:` of one read inside the value doesn't end it.
|
|
conds: usize,
|
|
}
|
|
|
|
/// An operand in a grouped value's parentheses, a value without parentheses, or the operand in
|
|
/// grouping parentheses ([`Paren`]), as far as it has been read.
|
|
enum Operand {
|
|
/// Its first token is next.
|
|
Start,
|
|
/// `new` came first.
|
|
New,
|
|
/// `File` or `Folder` came first, after `new` or not.
|
|
FileName { new: bool, folder: bool },
|
|
/// `File.` or `Folder.` came first: a member's name is next.
|
|
FileMember { folder: bool },
|
|
/// `File.openDialog`, `File.saveDialog` or `Folder.selectDialog` came first: its `(` is next.
|
|
FileDialog,
|
|
/// A call of `File`, `Folder` or one of their dialogs came first, and its `)` is still to come.
|
|
FileCall,
|
|
/// Grouping parentheses came first, and their `)` is still to come ([`Paren`] only).
|
|
Pair,
|
|
/// A member chain came first and is still being read.
|
|
Chain,
|
|
/// A complete value the check follows: the pair's value when its `)` comes next.
|
|
Done(Held),
|
|
/// Anything else.
|
|
Other,
|
|
}
|
|
|
|
impl Operand {
|
|
/// The operand after `t`, a token at the operand's depth, as far as `t` makes it a File or Folder
|
|
/// object: `[new] File|Folder(…)`, `new File|Folder`, `File.openDialog(…)`, `File.saveDialog(…)`,
|
|
/// `Folder.selectDialog(…)`, `Folder.<place>` ([`catalog::FOLDER_PLACES`]) or a variable holding
|
|
/// one. `file`: `t` is the platform's `File` or `Folder`; `alias`: `t` names a variable holding a
|
|
/// File or Folder object.
|
|
fn file_step(self, t: &Sig, file: bool, alias: bool) -> Operand {
|
|
match self {
|
|
Operand::Start if t.word("new") => Operand::New,
|
|
Operand::Start if file => Operand::FileName { new: false, folder: t.text == catalog::FOLDER },
|
|
Operand::New if file => Operand::FileName { new: true, folder: t.text == catalog::FOLDER },
|
|
Operand::Start if alias => Operand::Done(Held::File),
|
|
Operand::FileName { .. } | Operand::FileDialog if t.is("(") => Operand::FileCall,
|
|
Operand::FileName { new: false, folder } if t.is(".") => Operand::FileMember { folder },
|
|
Operand::FileMember { folder: true } if catalog::FOLDER_PLACES.contains(&t.text) => Operand::Done(Held::File),
|
|
Operand::FileMember { folder } if (if folder { catalog::FOLDER_DIALOGS } else { catalog::FILE_DIALOGS }).contains(&t.text) => {
|
|
Operand::FileDialog
|
|
}
|
|
_ => Operand::Other,
|
|
}
|
|
}
|
|
|
|
/// The operand is a File or Folder object when the `)` of its parentheses comes next: a complete
|
|
/// one, or `new File` or `new Folder` without arguments.
|
|
fn holds_file(&self) -> bool {
|
|
matches!(self, Operand::Done(Held::File) | Operand::FileName { new: true, .. })
|
|
}
|
|
}
|
|
|
|
/// Grouping parentheses (`(f)`, `(new File(p))`) being read: the bracket depth inside them, and the
|
|
/// operand read so far at that depth. Their value is the last operand (ECMA-262 3rd edition, sections
|
|
/// 11.1.6 and 11.14), and when it is a File or Folder object, their `)` ends one ([`Sig::file`]): a
|
|
/// call on it is a call on the object.
|
|
struct Paren {
|
|
level: usize,
|
|
operand: Operand,
|
|
}
|
|
|
|
/// A document-model member chain being read (`app.activeDocument.layers[0].name`).
|
|
struct Chain {
|
|
/// The chain as written: the root (a variable's chain in place of the variable), `.name` for a
|
|
/// member, `[]` for an index and `()` for a call.
|
|
path: String,
|
|
chars: usize,
|
|
/// The path reached [`crate::Limits::echo`] and ends with `…`.
|
|
cut: bool,
|
|
/// Members, indexes and calls after the root.
|
|
segments: usize,
|
|
/// The root is `activeDocument`, `documents` or `selection`, which count as a use alone.
|
|
bare: bool,
|
|
line: usize,
|
|
/// A `.` came last: a member's name comes next.
|
|
after_dot: bool,
|
|
/// The last member's name starts with `set` or `remove`.
|
|
last_change: bool,
|
|
/// A member whose name starts with `set` or `remove` was called.
|
|
change_call: bool,
|
|
/// While its index or call is open: the depth of brackets outside it.
|
|
wait: usize,
|
|
/// The variable it is assigned to: bound to it when the statement ends with the chain.
|
|
assign: Option<String>,
|
|
/// The grouped value ([`Group::id`]) whose operand starts with the chain.
|
|
group: Option<usize>,
|
|
}
|
|
|
|
impl Chain {
|
|
fn new(root: String, bare: bool, line: usize) -> Self {
|
|
let (chars, cut) = (root.chars().count(), root.ends_with('…'));
|
|
Chain {
|
|
path: root,
|
|
chars,
|
|
cut,
|
|
segments: 0,
|
|
bare,
|
|
line,
|
|
after_dot: false,
|
|
last_change: false,
|
|
change_call: false,
|
|
wait: 0,
|
|
assign: None,
|
|
group: None,
|
|
}
|
|
}
|
|
|
|
fn append(&mut self, s: &str, cap: usize) {
|
|
for c in s.chars() {
|
|
if self.cut {
|
|
return;
|
|
}
|
|
if self.chars + 1 >= cap {
|
|
self.path.push('…');
|
|
self.chars += 1;
|
|
self.cut = true;
|
|
return;
|
|
}
|
|
self.path.push(c);
|
|
self.chars += 1;
|
|
}
|
|
}
|
|
|
|
fn member(&mut self, name: &str, cap: usize) {
|
|
self.segments += 1;
|
|
self.append(".", cap);
|
|
self.append(name, cap);
|
|
self.last_change = name.starts_with("set") || name.starts_with("remove");
|
|
}
|
|
|
|
fn index_or_call(&mut self, call: bool, cap: usize) {
|
|
self.segments += 1;
|
|
self.change_call |= call && self.last_change;
|
|
self.last_change = false;
|
|
self.append(if call { "()" } else { "[]" }, cap);
|
|
}
|
|
}
|
|
|
|
/// A function body being read: the bracket depth inside it, and what each name declared in it
|
|
/// (its parameters, `var`, `function name`) or emptied by an assignment in it held outside it, put
|
|
/// back when the body closes. The names it declares are its own from its `{` ([`Declared`]). A
|
|
/// member chain or a File object assigned in it to an outer name stays.
|
|
struct Scope {
|
|
depth: usize,
|
|
saved: HashMap<String, Saved>,
|
|
/// The bracket depth of the `var` statement the body is in, if any: it goes on when the body
|
|
/// closes.
|
|
var_depth: Option<usize>,
|
|
}
|
|
|
|
/// A `catch` block being read: the bracket depth inside it, its parameter, and what that name held
|
|
/// outside the block, put back when the block closes. The parameter is the block's own (ECMA-262
|
|
/// 3rd edition, section 12.14); the other names in the block are those of the code around it.
|
|
struct Catch {
|
|
depth: usize,
|
|
name: String,
|
|
saved: Saved,
|
|
/// The depth of the `catch` block around this one with the same parameter, if any.
|
|
outer: Option<usize>,
|
|
}
|
|
|
|
/// What a name held outside the function body or `catch` block that declares it.
|
|
struct Saved {
|
|
/// The member chain assigned to it.
|
|
chain: Option<String>,
|
|
/// It held a File or Folder object.
|
|
file: bool,
|
|
/// For a root of the document model: whether the script's own variable hid the root.
|
|
shadowed: Option<bool>,
|
|
/// It was a platform name the script had a meaning of its own for.
|
|
own: bool,
|
|
}
|
|
|
|
/// The open brackets and their lines, up to [`crate::Limits::nesting`]; the brackets past that are
|
|
/// counted, not kept. Both passes over a file follow brackets with it.
|
|
#[derive(Default)]
|
|
struct Brackets {
|
|
open: Vec<(char, usize)>,
|
|
overflow: usize,
|
|
}
|
|
|
|
impl Brackets {
|
|
fn depth(&self) -> usize {
|
|
self.open.len() + self.overflow
|
|
}
|
|
|
|
/// Open the bracket `t`: false when `cap` brackets are kept already, and `t` is only counted.
|
|
fn open(&mut self, t: &Sig, cap: usize) -> bool {
|
|
if self.open.len() < cap {
|
|
self.open.push((t.text.chars().next().unwrap_or('('), t.line));
|
|
return true;
|
|
}
|
|
self.overflow += 1;
|
|
false
|
|
}
|
|
|
|
/// Close the innermost bracket when `t` matches it: false when it doesn't.
|
|
fn close(&mut self, t: &Sig) -> bool {
|
|
if self.overflow > 0 {
|
|
self.overflow -= 1;
|
|
return true;
|
|
}
|
|
let opening = match t.text {
|
|
")" => '(',
|
|
"]" => '[',
|
|
_ => '{',
|
|
};
|
|
if self.open.last().is_some_and(|(c, _)| *c == opening) {
|
|
self.open.pop();
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
struct Analyzer<'s, 'a, 'l> {
|
|
acc: &'a mut Acc<'l>,
|
|
file: Option<&'a str>,
|
|
/// The four significant tokens before this one, the latest first.
|
|
h: [Option<Sig<'s>>; 4],
|
|
brackets: Brackets,
|
|
/// Nesting too deep to follow was reported: it is reported once.
|
|
deep: bool,
|
|
active: Option<Chain>,
|
|
/// Chains whose index or call is open, the innermost last.
|
|
suspended: Vec<Chain>,
|
|
/// The active chain waits for the bracket this token opens.
|
|
suspend: bool,
|
|
/// The document-model roots the script declares a variable of its own for.
|
|
shadowed: [bool; 4],
|
|
/// Variables assigned from a member chain, and the chain.
|
|
bound: HashMap<String, String>,
|
|
/// Variables holding a File or Folder object.
|
|
files: HashSet<String>,
|
|
/// The platform names ([`platform_name`]) the script declares or assigns a meaning of its own
|
|
/// for: they no longer stand for the platform's objects.
|
|
own_names: HashSet<String>,
|
|
/// The function bodies being read, the innermost last, up to [`crate::Limits::nesting`].
|
|
scopes: Vec<Scope>,
|
|
/// The names each function body and the global code declare, read before the analysis.
|
|
declared: &'a Declared<'s>,
|
|
/// The first entry of `declared.bodies` whose body's `{` hasn't been read yet.
|
|
next_body: usize,
|
|
/// `function` was read at this bracket depth, and its body's `{` is still to come.
|
|
function: Option<usize>,
|
|
/// The parameters of the function whose body's `{` is still to come, and the bracket depth
|
|
/// inside the parentheses they are read in.
|
|
params: Option<(usize, Vec<String>)>,
|
|
/// The name of the function expression whose parameters are still to come: a variable inside
|
|
/// that function only.
|
|
expression_name: Option<String>,
|
|
/// `catch` was read at this bracket depth and its block's `{` is still to come, with the name
|
|
/// in its parentheses once read.
|
|
catch: Option<(usize, Option<String>)>,
|
|
/// The `catch` blocks being read, the innermost last, up to [`crate::Limits::nesting`].
|
|
catches: Vec<Catch>,
|
|
/// The parameters of the `catch` blocks being read, each with the depth inside the innermost
|
|
/// block it names.
|
|
catch_names: HashMap<String, usize>,
|
|
/// The calls of `File`, `Folder` and their dialogs still open, by the bracket depth outside their
|
|
/// `(`, up to [`crate::Limits::nesting`] of them.
|
|
file_calls: Vec<usize>,
|
|
/// The values in grouping parentheses, the values without them that may hold a File, and the
|
|
/// values of compound assignments, being read, the innermost last, up to
|
|
/// [`crate::Limits::nesting`] of them.
|
|
groups: Vec<Group>,
|
|
/// The id of the last value put on `groups`.
|
|
group_id: usize,
|
|
/// The grouping parentheses being read, the innermost last, up to [`crate::Limits::nesting`] of
|
|
/// them.
|
|
parens: Vec<Paren>,
|
|
/// The bracket depth of each `?` whose `:` is still to come, the latest last, up to
|
|
/// [`crate::Limits::nesting`] of them.
|
|
conds: Vec<usize>,
|
|
assign: Option<Assign>,
|
|
/// The bracket depth of the `var` statement being read.
|
|
var_depth: Option<usize>,
|
|
pending: Pending<'s>,
|
|
module_candidate: bool,
|
|
module: bool,
|
|
includes: Vec<IncludeRef>,
|
|
/// The folders the last `#includepath` named.
|
|
paths: Rc<[String]>,
|
|
/// The last construct of later syntax reported, and its line: reported once per line.
|
|
last_newer: Option<(usize, &'s str)>,
|
|
}
|
|
|
|
impl<'s, 'a, 'l> Analyzer<'s, 'a, 'l> {
|
|
fn new(acc: &'a mut Acc<'l>, file: Option<&'a str>, paths: Rc<[String]>, known: &Known, declared: &'a Declared<'s>) -> Self {
|
|
let global: Vec<&'s str> = declared.global.iter().copied().collect();
|
|
let mut a = Analyzer {
|
|
acc,
|
|
file,
|
|
h: [None; 4],
|
|
brackets: Brackets::default(),
|
|
deep: false,
|
|
active: None,
|
|
suspended: vec![],
|
|
suspend: false,
|
|
shadowed: [false; 4],
|
|
bound: known.bound.clone(),
|
|
files: known.files.clone(),
|
|
own_names: HashSet::new(),
|
|
scopes: vec![],
|
|
declared,
|
|
next_body: 0,
|
|
function: None,
|
|
params: None,
|
|
expression_name: None,
|
|
catch: None,
|
|
catches: vec![],
|
|
catch_names: HashMap::new(),
|
|
file_calls: vec![],
|
|
groups: vec![],
|
|
group_id: 0,
|
|
parens: vec![],
|
|
conds: vec![],
|
|
assign: None,
|
|
var_depth: None,
|
|
pending: Pending::None,
|
|
module_candidate: false,
|
|
module: false,
|
|
includes: vec![],
|
|
paths,
|
|
last_newer: None,
|
|
};
|
|
// The names the global code's function declarations declare are the script's own from the
|
|
// start of the file, before any assignment runs: a member chain or a File object that another
|
|
// file's variable of that name holds stays.
|
|
for name in global {
|
|
a.acc.steps = a.acc.steps.saturating_add(1);
|
|
a.make_own(name);
|
|
a.hide_root(name);
|
|
}
|
|
a
|
|
}
|
|
|
|
/// `line` for a tally's lines: only the script's own lines are listed.
|
|
fn agg(&self, line: usize) -> Option<usize> {
|
|
self.file.is_none().then_some(line)
|
|
}
|
|
|
|
fn depth(&self) -> usize {
|
|
self.brackets.depth()
|
|
}
|
|
|
|
/// A problem at `line` of the file.
|
|
fn problem(&mut self, code: &'static str, line: usize) {
|
|
if code == report::TOO_DEEP {
|
|
if self.deep {
|
|
return;
|
|
}
|
|
self.deep = true;
|
|
}
|
|
self.acc.problem(code, Some(line), self.file, None);
|
|
}
|
|
|
|
/// This token names a variable being declared: it follows `var` or `function`, a `,` in a `var`
|
|
/// statement, the `(` or a `,` of a function's parameters, or the `(` after `catch`.
|
|
fn declaring(&self) -> bool {
|
|
word(self.h[0], "var")
|
|
|| word(self.h[0], "function")
|
|
|| (is(self.h[0], ",") && self.var_depth == Some(self.depth()))
|
|
|| self.parameter()
|
|
|| self.catch_parameter()
|
|
}
|
|
|
|
/// This token names a `catch` block's parameter: it follows the `(` after `catch` (not after a
|
|
/// property named `catch`).
|
|
fn catch_parameter(&self) -> bool {
|
|
is(self.h[0], "(") && word(self.h[1], "catch") && !member_mark(self.h[2])
|
|
}
|
|
|
|
/// This token names one of a function's parameters: it follows the `(` or a `,` of the
|
|
/// parentheses after `function`.
|
|
fn parameter(&self) -> bool {
|
|
(is(self.h[0], "(") || is(self.h[0], ",")) && self.params.as_ref().is_some_and(|(depth, _)| *depth == self.depth())
|
|
}
|
|
|
|
fn token(&mut self, mut t: Sig<'s>) {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
// The name of a grouped value whose last `)` was read, or of a value without parentheses that
|
|
// `t` ends, holds the value, or nothing the check follows, before `t` starts a member chain or
|
|
// closes a function body.
|
|
self.settle_groups(&t);
|
|
// A line break ends a `var` statement when it is outside the statement's brackets, and the
|
|
// tokens on either side of it don't go on with the expression.
|
|
if t.nl && self.var_depth == Some(self.depth()) && !goes_on_after(self.h[0]) && !goes_on_with(&t) {
|
|
self.var_depth = None;
|
|
}
|
|
self.declaration_without_value(&t);
|
|
let consumed = self.continue_chain(&t);
|
|
let before = self.depth();
|
|
if t.kind == T::Punct {
|
|
t.file = self.bracket(&t);
|
|
} else {
|
|
t.file = self.folder_place(&t);
|
|
}
|
|
self.conditional(&t);
|
|
self.add_parameter(&t);
|
|
let started = !consumed && t.kind == T::Ident && self.start_chain(&t);
|
|
self.group_step(&t, started, before);
|
|
self.paren_step(&t, before);
|
|
self.assignment(&t, started);
|
|
self.features(&t);
|
|
self.newer(&t);
|
|
self.module_check(&t);
|
|
self.h = [Some(t), self.h[0], self.h[1], self.h[2]];
|
|
}
|
|
|
|
// ---------- member chains ----------
|
|
|
|
/// Whether the active chain takes `t` (a `.`, a member's name, `[` or `(`); any other token ends
|
|
/// it.
|
|
fn continue_chain(&mut self, t: &Sig<'s>) -> bool {
|
|
let cap = self.acc.limits.echo;
|
|
let Some(c) = self.active.as_mut() else { return false };
|
|
if c.after_dot {
|
|
c.after_dot = false;
|
|
if t.kind == T::Ident {
|
|
c.member(t.text, cap);
|
|
return true;
|
|
}
|
|
} else if t.is(".") {
|
|
c.after_dot = true;
|
|
return true;
|
|
} else if t.is("[") || t.is("(") {
|
|
c.index_or_call(t.is("("), cap);
|
|
self.suspend = true;
|
|
return true;
|
|
}
|
|
self.end_active(Some(t));
|
|
false
|
|
}
|
|
|
|
/// Start a chain at `t` when it names a root of the document model or a variable assigned
|
|
/// from a chain.
|
|
fn start_chain(&mut self, t: &Sig<'s>) -> bool {
|
|
if member_mark(self.h[0]) || self.declaring() {
|
|
return false;
|
|
}
|
|
let (root, bare) = if let Some(path) = self.bound.get(t.text) {
|
|
(path.clone(), false)
|
|
} else if let Some(i) = catalog::DOM_ROOTS.iter().position(|r| *r == t.text)
|
|
&& !self.shadowed.get(i).copied().unwrap_or(true)
|
|
{
|
|
(t.text.to_string(), i > 0)
|
|
} else {
|
|
return false;
|
|
};
|
|
self.active = Some(Chain::new(root, bare, t.line));
|
|
true
|
|
}
|
|
|
|
fn end_active(&mut self, ending: Option<&Sig<'s>>) {
|
|
if let Some(c) = self.active.take() {
|
|
self.record(c, ending);
|
|
}
|
|
}
|
|
|
|
/// A chain read to its end (`ending`: the token after it, none at the end of a statement).
|
|
fn record(&mut self, c: Chain, ending: Option<&Sig<'s>>) {
|
|
let line = self.agg(c.line);
|
|
// `{selection: …}` names a property.
|
|
let key = c.segments == 0 && ending.is_some_and(|t| t.is(":"));
|
|
let then = |rest: &str| rest.is_empty() || rest.starts_with(['.', '(', '[']);
|
|
if !key {
|
|
let method = c.path.strip_prefix("app.").is_some_and(|rest| catalog::APP_METHODS.iter().any(|m| rest.strip_prefix(m).is_some_and(then)));
|
|
if (c.segments > 0 || c.bare) && !method {
|
|
self.acc.member(&c.path);
|
|
}
|
|
if c.path.strip_prefix("app.preferences").is_some_and(then) {
|
|
self.acc.feature(FeatureId::Preferences, line);
|
|
if c.change_call || ending.is_some_and(|t| t.is("=")) {
|
|
self.acc.outside(OutsideId::Preferences, line, None);
|
|
}
|
|
}
|
|
if ["app.name", "app.version"].iter().any(|p| c.path.strip_prefix(p).is_some_and(then)) {
|
|
self.acc.feature(FeatureId::HostCheck, line);
|
|
}
|
|
}
|
|
// A chain that an operand in a grouped value's parentheses starts with is complete: the
|
|
// operand is the chain when the pair's `)` comes next.
|
|
if let Some(id) = c.group
|
|
&& let Some(g) = self.groups.last_mut()
|
|
&& g.id == id
|
|
&& matches!(g.operand, Operand::Chain)
|
|
{
|
|
g.operand = Operand::Done(Held::Chain(c.path.clone()));
|
|
}
|
|
if let Some(name) = c.assign {
|
|
let ends = ending.is_none_or(ends_value);
|
|
if ends && !key {
|
|
self.bind(name, c.path);
|
|
} else {
|
|
self.unbind(&name);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn bind(&mut self, name: String, path: String) {
|
|
self.files.remove(&name);
|
|
if self.bound.contains_key(&name) || self.acc.room(self.bound.len()) {
|
|
self.bound.insert(name, path);
|
|
}
|
|
}
|
|
|
|
/// `name` holds a File or Folder object.
|
|
fn hold_file(&mut self, name: String) {
|
|
self.bound.remove(&name);
|
|
if self.files.contains(&name) || self.acc.room(self.files.len()) {
|
|
self.files.insert(name);
|
|
}
|
|
}
|
|
|
|
/// `t`, when it names a parameter of the function whose body's `{` is still to come: kept, up to
|
|
/// [`crate::Limits::entries`] of them, for the body to declare. A `catch` block's parameter is
|
|
/// kept for the block.
|
|
fn add_parameter(&mut self, t: &Sig<'s>) {
|
|
if t.kind != T::Ident {
|
|
return;
|
|
}
|
|
if self.catch_parameter() {
|
|
if let Some((_, name)) = self.catch.as_mut() {
|
|
*name = Some(t.text.to_string());
|
|
}
|
|
return;
|
|
}
|
|
if !self.parameter() {
|
|
return;
|
|
}
|
|
if let Some((_, names)) = self.params.as_mut()
|
|
&& self.acc.room(names.len())
|
|
{
|
|
names.push(t.text.to_string());
|
|
}
|
|
}
|
|
|
|
/// `name` is declared (`var name`, `function name`, a parameter), or an assignment empties it.
|
|
/// Inside a function body, what it held outside the body is kept, to put back when the body
|
|
/// closes.
|
|
fn keep_outer(&mut self, name: &str) {
|
|
// The parameter of a `catch` block inside the body is the block's own: the block puts back
|
|
// what it held.
|
|
if self.in_catch(name) {
|
|
return;
|
|
}
|
|
let Some(scope) = self.scopes.last() else { return };
|
|
if scope.saved.contains_key(name) || !self.acc.room(scope.saved.len()) {
|
|
return;
|
|
}
|
|
let saved = self.saved(name);
|
|
if let Some(scope) = self.scopes.last_mut() {
|
|
scope.saved.insert(name.to_string(), saved);
|
|
}
|
|
}
|
|
|
|
/// The names [`Declared`] has for the function body whose `{` starts at `start`.
|
|
fn declared_in(&mut self, start: usize) -> &'a [&'s str] {
|
|
let declared = self.declared;
|
|
let bodies = &declared.bodies;
|
|
while bodies.get(self.next_body).is_some_and(|(at, _)| *at < start) {
|
|
self.next_body += 1;
|
|
}
|
|
match bodies.get(self.next_body) {
|
|
Some((at, names)) if *at == start => names,
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
/// `name`, declared in the innermost function body, is the body's own from its `{`: what it held
|
|
/// outside is kept to put back when the body closes. A name the body's table has no room for
|
|
/// stays as it is.
|
|
fn hoist(&mut self, name: &str) {
|
|
self.keep_outer(name);
|
|
if self.scopes.last().is_some_and(|s| s.saved.contains_key(name)) {
|
|
self.empty(name);
|
|
}
|
|
}
|
|
|
|
/// `name` was made its own by what [`Declared`] has for the innermost function body this token
|
|
/// is directly in, or for the global code this token is directly in.
|
|
fn hoisted(&self, name: &str) -> bool {
|
|
match self.scopes.last() {
|
|
Some(s) => s.depth == self.depth() && s.saved.contains_key(name),
|
|
None => self.depth() == 0 && self.declared.global.contains(name),
|
|
}
|
|
}
|
|
|
|
/// `name` is the parameter of a `catch` block being read inside the innermost function body, or
|
|
/// outside any function.
|
|
fn in_catch(&self, name: &str) -> bool {
|
|
self.catch_names.get(name).is_some_and(|&depth| self.scopes.last().is_none_or(|s| depth > s.depth))
|
|
}
|
|
|
|
/// What `name` holds now.
|
|
fn saved(&self, name: &str) -> Saved {
|
|
let shadowed = catalog::DOM_ROOTS.iter().position(|r| *r == name).and_then(|i| self.shadowed.get(i).copied());
|
|
Saved { chain: self.bound.get(name).cloned(), file: self.files.contains(name), shadowed, own: self.own_names.contains(name) }
|
|
}
|
|
|
|
/// `name` holds `saved` again.
|
|
fn restore(&mut self, name: String, saved: Saved) {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
match saved.chain {
|
|
Some(chain) => {
|
|
self.bound.insert(name.clone(), chain);
|
|
}
|
|
None => {
|
|
self.bound.remove(&name);
|
|
}
|
|
}
|
|
if saved.file {
|
|
self.files.insert(name.clone());
|
|
} else {
|
|
self.files.remove(&name);
|
|
}
|
|
if saved.own {
|
|
self.own_names.insert(name.clone());
|
|
} else {
|
|
self.own_names.remove(&name);
|
|
}
|
|
if let Some(hidden) = saved.shadowed
|
|
&& let Some(i) = catalog::DOM_ROOTS.iter().position(|r| *r == name)
|
|
&& let Some(s) = self.shadowed.get_mut(i)
|
|
{
|
|
*s = hidden;
|
|
}
|
|
}
|
|
|
|
/// The function bodies and `catch` blocks that the bracket just read closed: each name declared
|
|
/// in them gets back what it held outside them, and the `var` statement a body was in goes on.
|
|
fn close_scopes(&mut self) {
|
|
while self.scopes.last().is_some_and(|s| self.depth() < s.depth) {
|
|
let Some(scope) = self.scopes.pop() else { return };
|
|
self.var_depth = scope.var_depth;
|
|
for (name, saved) in scope.saved {
|
|
self.restore(name, saved);
|
|
}
|
|
}
|
|
while self.catches.last().is_some_and(|c| self.depth() < c.depth) {
|
|
let Some(c) = self.catches.pop() else { return };
|
|
match c.outer {
|
|
Some(depth) => self.catch_names.insert(c.name.clone(), depth),
|
|
None => self.catch_names.remove(&c.name),
|
|
};
|
|
self.restore(c.name, c.saved);
|
|
}
|
|
}
|
|
|
|
/// `name` has a meaning of the script's own from here: a platform name no longer stands for the
|
|
/// platform's object.
|
|
fn make_own(&mut self, name: &str) {
|
|
if platform_name(name) {
|
|
self.own_names.insert(name.to_string());
|
|
}
|
|
}
|
|
|
|
/// `name` now holds something else: no chain, no file, no longer a root of the document model,
|
|
/// and no longer a platform's object. Inside a function body this lasts until the body closes.
|
|
fn unbind(&mut self, name: &str) {
|
|
self.keep_outer(name);
|
|
self.empty(name);
|
|
}
|
|
|
|
/// `name` holds nothing the check follows, with a meaning of the script's own.
|
|
fn empty(&mut self, name: &str) {
|
|
self.make_own(name);
|
|
self.bound.remove(name);
|
|
self.files.remove(name);
|
|
self.hide_root(name);
|
|
}
|
|
|
|
/// A root of the document model named `name` no longer stands for the root.
|
|
fn hide_root(&mut self, name: &str) {
|
|
if let Some(i) = catalog::DOM_ROOTS.iter().position(|r| *r == name)
|
|
&& let Some(s) = self.shadowed.get_mut(i)
|
|
{
|
|
*s = true;
|
|
}
|
|
}
|
|
|
|
// ---------- brackets ----------
|
|
|
|
/// An open or close bracket: true for the `)` that ends a call of `File` or `Folder`, or grouping
|
|
/// parentheses that hold a File or Folder object ([`Analyzer::close_parens`]).
|
|
fn bracket(&mut self, t: &Sig<'s>) -> bool {
|
|
match t.text {
|
|
"(" | "[" | "{" => {
|
|
if std::mem::take(&mut self.suspend)
|
|
&& let Some(mut c) = self.active.take()
|
|
{
|
|
if self.suspended.len() < self.acc.limits.nesting {
|
|
c.wait = self.depth();
|
|
self.suspended.push(c);
|
|
} else {
|
|
self.record(c, Some(t));
|
|
}
|
|
}
|
|
// The first `{` after `function` at this depth opens the function's body, and a `(`
|
|
// right after `function` or its name opens its parameters.
|
|
let body = t.text == "{" && self.function.take() == Some(self.depth());
|
|
let params = t.text == "(" && self.function == Some(self.depth()) && (word(self.h[0], "function") || word(self.h[1], "function"));
|
|
// The first `{` after `catch` and its parentheses at this depth opens its block.
|
|
let depth = self.depth();
|
|
let catch = if t.text == "{" { self.catch.take().filter(|(d, _)| *d == depth).and_then(|(_, name)| name) } else { None };
|
|
let file_call = t.text == "(" && self.calls_for_file();
|
|
if file_call && self.file_calls.len() < self.acc.limits.nesting {
|
|
self.file_calls.push(self.depth());
|
|
}
|
|
self.open(t);
|
|
if let Some(name) = catch
|
|
&& self.catches.len() < self.acc.limits.nesting
|
|
{
|
|
let saved = self.saved(&name);
|
|
let outer = self.catch_names.insert(name.clone(), self.depth());
|
|
self.empty(&name);
|
|
self.catches.push(Catch { depth: self.depth(), name, saved, outer });
|
|
}
|
|
if params {
|
|
// A function expression's name is the body's own, as its parameters are.
|
|
self.params = Some((self.depth(), self.expression_name.take().into_iter().collect()));
|
|
}
|
|
if body {
|
|
let params = self.params.take().map(|(_, names)| names).unwrap_or_default();
|
|
if self.scopes.len() < self.acc.limits.nesting {
|
|
self.scopes.push(Scope { depth: self.depth(), saved: HashMap::new(), var_depth: self.var_depth.take() });
|
|
// The parameters are the body's own names, and so are the names it declares.
|
|
for name in params {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
self.keep_outer(&name);
|
|
self.unbind(&name);
|
|
}
|
|
for &name in self.declared_in(t.start) {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
self.hoist(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
")" | "]" | "}" => {
|
|
self.end_active(Some(t));
|
|
if self.close(t) {
|
|
if self.var_depth.is_some_and(|d| self.depth() < d) {
|
|
self.var_depth = None;
|
|
}
|
|
if self.suspended.last().is_some_and(|c| c.wait == self.depth()) {
|
|
self.active = self.suspended.pop();
|
|
}
|
|
self.close_scopes();
|
|
let grouped = self.close_parens(t);
|
|
if t.text == ")" && self.file_calls.last() == Some(&self.depth()) {
|
|
self.file_calls.pop();
|
|
return true;
|
|
}
|
|
return grouped;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn open(&mut self, t: &Sig<'s>) {
|
|
if !self.brackets.open(t, self.acc.limits.nesting) {
|
|
self.problem(report::TOO_DEEP, t.line);
|
|
}
|
|
}
|
|
|
|
/// Close the innermost bracket when `t` matches it: false (and a problem) when it doesn't.
|
|
fn close(&mut self, t: &Sig<'s>) -> bool {
|
|
if self.brackets.close(t) {
|
|
return true;
|
|
}
|
|
self.acc.problem(report::UNEXPECTED_BRACKET, Some(t.line), self.file, Some(t.text));
|
|
false
|
|
}
|
|
|
|
/// A `(` here calls something that gives a File or Folder object: `File` or `Folder` (with or
|
|
/// without `new`), or `File.openDialog`, `File.saveDialog` or `Folder.selectDialog`, each the
|
|
/// platform's.
|
|
fn calls_for_file(&self) -> bool {
|
|
let [h0, h1, h2, h3] = self.h;
|
|
let platform = |s: Option<Sig>, name: &str| word(s, name) && !self.own_names.contains(name);
|
|
let constructor = (platform(h0, catalog::FILE) || platform(h0, catalog::FOLDER)) && !member_mark(h1);
|
|
let dialog = h0.filter(|s| s.kind == T::Ident).is_some_and(|m| {
|
|
is(h1, ".")
|
|
&& !member_mark(h3)
|
|
&& ((platform(h2, catalog::FILE) && catalog::FILE_DIALOGS.contains(&m.text))
|
|
|| (platform(h2, catalog::FOLDER) && catalog::FOLDER_DIALOGS.contains(&m.text)))
|
|
});
|
|
constructor || dialog
|
|
}
|
|
|
|
/// `t` names one of the platform's `Folder` places after `Folder.` (`Folder.temp`): a Folder
|
|
/// object.
|
|
fn folder_place(&self, t: &Sig<'s>) -> bool {
|
|
let [h0, h1, h2, _] = self.h;
|
|
t.kind == T::Ident
|
|
&& catalog::FOLDER_PLACES.contains(&t.text)
|
|
&& is(h0, ".")
|
|
&& word(h1, catalog::FOLDER)
|
|
&& !self.own_names.contains(catalog::FOLDER)
|
|
&& !member_mark(h2)
|
|
}
|
|
|
|
// ---------- variables ----------
|
|
|
|
/// `var name` without `= value`. In a function body that doesn't have the name yet, it is a new
|
|
/// variable of the body's own, which may hide a root of the document model. A `var` without a
|
|
/// value doesn't change a variable that exists (ECMA-262 3rd edition, section 10.1.3), so a name
|
|
/// the body already has, the parameter of a `catch` block it is in, and any name outside a
|
|
/// function keep what they hold.
|
|
fn declaration_without_value(&mut self, t: &Sig<'s>) {
|
|
let Some(name) = self.h[0].filter(|s| s.kind == T::Ident) else { return };
|
|
let declared = word(self.h[1], "var") || (is(self.h[1], ",") && self.var_depth == Some(self.depth()));
|
|
if !declared || t.is("=") {
|
|
return;
|
|
}
|
|
if self.in_catch(name.text) {
|
|
self.declare_in_catch(name.text);
|
|
} else if self.scopes.last().is_some_and(|s| !s.saved.contains_key(name.text)) {
|
|
self.keep_outer(name.text);
|
|
self.unbind(name.text);
|
|
}
|
|
}
|
|
|
|
/// `var name` in a `catch` block whose parameter is `name`. In a function body that doesn't have
|
|
/// the name yet, the declaration makes it the function's own variable (ECMA-262 3rd edition,
|
|
/// section 10.1.3), with no value: the parameter keeps what it holds, and after the outermost
|
|
/// `catch` block in the body with that parameter the name holds nothing.
|
|
fn declare_in_catch(&mut self, name: &str) {
|
|
let Some(&innermost) = self.catch_names.get(name) else { return };
|
|
let Some(scope) = self.scopes.last() else { return };
|
|
let body = scope.depth;
|
|
if scope.saved.contains_key(name) || !self.acc.room(scope.saved.len()) {
|
|
return;
|
|
}
|
|
// From the innermost `catch` block with this parameter out to the outermost one in the body.
|
|
let mut depth = innermost;
|
|
let i = loop {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
let Ok(i) = self.catches.binary_search_by_key(&depth, |c| c.depth) else { return };
|
|
match self.catches.get(i).and_then(|c| c.outer) {
|
|
Some(outer) if outer > body => depth = outer,
|
|
_ => break i,
|
|
}
|
|
};
|
|
let Some(c) = self.catches.get_mut(i) else { return };
|
|
let empty = Saved { chain: None, file: false, shadowed: catalog::DOM_ROOTS.contains(&name).then_some(true), own: platform_name(name) };
|
|
let outside = std::mem::replace(&mut c.saved, empty);
|
|
if let Some(scope) = self.scopes.last_mut() {
|
|
scope.saved.insert(name.to_string(), outside);
|
|
}
|
|
}
|
|
|
|
fn assignment(&mut self, t: &Sig<'s>, started: bool) {
|
|
// The first token of what `name =` assigns.
|
|
if let Some(mut a) = self.assign.take() {
|
|
if started {
|
|
if let Some(c) = self.active.as_mut() {
|
|
c.assign = Some(a.name);
|
|
}
|
|
} else if t.word("new") && !a.after_new {
|
|
a.after_new = true;
|
|
self.assign = Some(a);
|
|
} else if (t.word(catalog::FILE) || t.word(catalog::FOLDER)) && !self.own_names.contains(t.text) {
|
|
self.start_value(a.name, Operand::FileName { new: a.after_new, folder: t.text == catalog::FOLDER });
|
|
} else if !a.after_new && t.kind == T::Ident && self.files.contains(t.text) {
|
|
self.start_value(a.name, Operand::Done(Held::File));
|
|
} else if !a.after_new && t.is("(") {
|
|
self.start_group(a.name);
|
|
} else {
|
|
self.unbind(&a.name);
|
|
}
|
|
return;
|
|
}
|
|
if t.word("var") {
|
|
self.var_depth = Some(self.depth());
|
|
}
|
|
if (t.is(";") || for_in(t, self.h[0], self.h[1])) && self.var_depth == Some(self.depth()) {
|
|
self.var_depth = None;
|
|
}
|
|
if t.is("=")
|
|
&& let Some(name) = self.h[0].filter(|s| s.kind == T::Ident)
|
|
{
|
|
let h1 = self.h[1];
|
|
let declared = word(h1, "var") || (is(h1, ",") && self.var_depth.is_some());
|
|
if declared {
|
|
if self.in_catch(name.text) {
|
|
self.declare_in_catch(name.text);
|
|
}
|
|
self.keep_outer(name.text);
|
|
self.make_own(name.text);
|
|
}
|
|
// A name after `.`, `..`, `.@`, `::` or `@` is a property's or an attribute's; any other
|
|
// name before `=` is a variable, at the start of a statement or inside an expression.
|
|
if !member_mark(h1) && !is(h1, "@") {
|
|
self.assign = Some(Assign { name: name.text.to_string(), after_new: false });
|
|
}
|
|
}
|
|
match self.updated(t) {
|
|
// The value on the right is read at this depth, and the name holds nothing the check
|
|
// follows once that value ends ([`Analyzer::settle_groups`]).
|
|
Some((name, true)) => self.start_value(name.to_string(), Operand::Other),
|
|
Some((name, false)) => self.unbind(name),
|
|
None => {}
|
|
}
|
|
}
|
|
|
|
/// The variable that `t` changes to a number or a string, nothing the check follows, and whether
|
|
/// the change waits for the value on the right. `name += …` and the other compound assignments
|
|
/// (ECMA-262 3rd edition, section 11.13.2) store their result after that value is read, so the
|
|
/// name holds what it held until the value ends. `name++` and `name--` (section 11.3), read at
|
|
/// their operator, and `++name` and `--name` (section 11.4), read at the token after the name when
|
|
/// no member, index or call follows it, change the name at once. A `++` or `--` is the name's
|
|
/// after it when it is on the name's line, and before it when no operand ends just before the
|
|
/// `++` on that line. A property changed so (`o.name += 1`, `++o.name`) leaves the variable as it
|
|
/// is.
|
|
fn updated(&self, t: &Sig<'s>) -> Option<(&'s str, bool)> {
|
|
let [h0, h1, h2, _] = self.h;
|
|
let name = h0.filter(|s| s.kind == T::Ident)?;
|
|
let compound = t.kind == T::Punct && COMPOUND.contains(&t.text);
|
|
let postfix = (t.is("++") || t.is("--")) && !t.nl;
|
|
if (compound || postfix) && !expr_after(T::Ident, name.text) && !member_mark(h1) && !is(h1, "@") {
|
|
return Some((name.text, compound));
|
|
}
|
|
let prefix = h1.is_some_and(|op| (op.is("++") || op.is("--")) && (op.nl || h2.is_none_or(|s| expr_after(s.kind, s.text))));
|
|
(prefix && !member_mark(Some(*t)) && !t.is("[") && !t.is("(")).then_some((name.text, false))
|
|
}
|
|
|
|
// ---------- values in grouping parentheses, and values that may hold a File ----------
|
|
|
|
/// `name = (`, its `(` just read: the value is read inside the parentheses.
|
|
fn start_group(&mut self, name: String) {
|
|
let level = self.depth();
|
|
self.push_group(Group { id: 0, name, base: level.saturating_sub(1), level, operand: Operand::Start, closed: false, bare: false, conds: 0 });
|
|
}
|
|
|
|
/// `name =` and the first token of a value without parentheses that may hold a File (`File`,
|
|
/// `Folder`, or a variable holding a File), read as far as `operand`, or a compound assignment's
|
|
/// operator (`name += …`), with [`Operand::Other`]: the value is read at this depth.
|
|
fn start_value(&mut self, name: String, operand: Operand) {
|
|
let level = self.depth();
|
|
self.push_group(Group { id: 0, name, base: level, level, operand, closed: false, bare: true, conds: 0 });
|
|
}
|
|
|
|
/// Read the value `g` from the next token on. Past [`crate::Limits::nesting`] values open at
|
|
/// once, its name holds nothing the check follows.
|
|
fn push_group(&mut self, mut g: Group) {
|
|
if self.groups.len() >= self.acc.limits.nesting {
|
|
self.unbind(&g.name);
|
|
return;
|
|
}
|
|
self.group_id = self.group_id.wrapping_add(1);
|
|
g.id = self.group_id;
|
|
g.conds = self.conds.len();
|
|
self.groups.push(g);
|
|
}
|
|
|
|
/// `t` in the innermost value still open: in its innermost pair of parentheses, or, for a value
|
|
/// without parentheses, at the depth it started at. `before` is the bracket depth before `t`, and
|
|
/// `started` is true when `t` started a member chain.
|
|
fn group_step(&mut self, t: &Sig<'s>, started: bool, before: usize) {
|
|
let after = self.depth();
|
|
let file = (t.word(catalog::FILE) || t.word(catalog::FOLDER)) && !self.own_names.contains(t.text);
|
|
let alias = t.kind == T::Ident && self.files.contains(t.text);
|
|
let Some(g) = self.groups.last_mut().filter(|g| !g.closed) else { return };
|
|
if after < g.level {
|
|
// The `)` of the innermost pair: the operand, when complete, is the pair's value and the
|
|
// operand of the pair around it.
|
|
let held = match std::mem::replace(&mut g.operand, Operand::Other) {
|
|
Operand::Done(held) => Some(held),
|
|
Operand::FileName { new: true, .. } => Some(Held::File),
|
|
_ => None,
|
|
};
|
|
g.operand = held.map_or(Operand::Other, Operand::Done);
|
|
g.level = after;
|
|
g.closed = after <= g.base;
|
|
return;
|
|
}
|
|
if before > g.level {
|
|
// Inside a bracket of the operand: the `)` back at the pair's depth ends a call of `File`,
|
|
// `Folder` or one of their dialogs.
|
|
if after == g.level && matches!(g.operand, Operand::FileCall) {
|
|
g.operand = Operand::Done(Held::File);
|
|
}
|
|
return;
|
|
}
|
|
if t.is(";") {
|
|
// A `;` inside the parentheses ends the value: the name holds nothing the check follows.
|
|
if let Some(g) = self.groups.pop() {
|
|
self.unbind(&g.name);
|
|
}
|
|
return;
|
|
}
|
|
let next = match std::mem::replace(&mut g.operand, Operand::Other) {
|
|
_ if t.is(",") => Operand::Start,
|
|
Operand::Start if t.is("(") => {
|
|
g.level = after;
|
|
Operand::Start
|
|
}
|
|
Operand::Start if started => {
|
|
if let Some(c) = self.active.as_mut() {
|
|
c.group = Some(g.id);
|
|
}
|
|
Operand::Chain
|
|
}
|
|
Operand::Chain => Operand::Chain,
|
|
operand => operand.file_step(t, file, alias),
|
|
};
|
|
g.operand = next;
|
|
}
|
|
|
|
/// `t` in the innermost grouping parentheses being read ([`Paren`]): at their depth, or the `)` that
|
|
/// brings a call or a pair of brackets in them back to it. A `(` where an expression may start
|
|
/// opens grouping parentheses, up to [`crate::Limits::nesting`] of them, except after a `.`, where
|
|
/// it opens an E4X filter (ECMA-357 2nd edition, section 11.2.4); `before` is the bracket depth
|
|
/// before `t`.
|
|
fn paren_step(&mut self, t: &Sig<'s>, before: usize) {
|
|
let after = self.depth();
|
|
let file = (t.word(catalog::FILE) || t.word(catalog::FOLDER)) && !self.own_names.contains(t.text);
|
|
let alias = t.kind == T::Ident && self.files.contains(t.text);
|
|
if let Some(p) = self.parens.last_mut() {
|
|
if before == p.level {
|
|
p.operand = match std::mem::replace(&mut p.operand, Operand::Other) {
|
|
_ if t.is(",") => Operand::Start,
|
|
Operand::Start if t.is("(") => Operand::Pair,
|
|
operand => operand.file_step(t, file, alias),
|
|
};
|
|
} else if after == p.level && before > after {
|
|
// The `)` of a call of `File`, `Folder` or a dialog, or of grouping parentheses, ends a
|
|
// File or Folder object when [`Analyzer::bracket`] set its [`Sig::file`].
|
|
p.operand = match p.operand {
|
|
Operand::FileCall | Operand::Pair if t.file => Operand::Done(Held::File),
|
|
_ => Operand::Other,
|
|
};
|
|
}
|
|
}
|
|
if t.is("(") && t.expr && !member_mark(self.h[0]) && self.parens.len() < self.acc.limits.nesting {
|
|
self.parens.push(Paren { level: after, operand: Operand::Start });
|
|
}
|
|
}
|
|
|
|
/// The grouping parentheses that the bracket `t` closed, if any (a bracket closes one level): true
|
|
/// when `t` is their `)` and their operand is a File or Folder object.
|
|
fn close_parens(&mut self, t: &Sig<'s>) -> bool {
|
|
let depth = self.depth();
|
|
self.parens.pop_if(|p| depth < p.level).is_some_and(|p| t.is(")") && p.operand.holds_file())
|
|
}
|
|
|
|
/// `t` follows the last `)` of a grouped value, or ends a value without parentheses at the depth
|
|
/// it started at: the name holds the value when `t` ends it.
|
|
fn settle_groups(&mut self, t: &Sig<'s>) {
|
|
// A line break ends a value only when the token before it doesn't go on with the expression
|
|
// either (`File.` before `openDialog(…)` on the next line).
|
|
let ends = ends_value(t) && !(t.nl && goes_on_after(self.h[0]));
|
|
let depth = self.depth();
|
|
// A `:` that answers a `?` read inside a value without parentheses goes on with that value
|
|
// (`f += ok ? '' : f.remove()`), and any other `:` ends it (`ok ? f = new File(p) : 0`).
|
|
let open = if t.is(":") && self.conds.last() == Some(&depth) { self.conds.len() } else { 0 };
|
|
let done = |g: &mut Group| g.closed || (g.bare && ends && g.level == depth && open <= g.conds);
|
|
while let Some(g) = self.groups.pop_if(done) {
|
|
self.settle(g, ends);
|
|
}
|
|
}
|
|
|
|
/// The name of the value `g` holds it when the value is complete (a grouped value's last `)` was
|
|
/// read, or a value without parentheses ended) and `ends` (the value ends after it), and
|
|
/// otherwise nothing the check follows.
|
|
fn settle(&mut self, g: Group, ends: bool) {
|
|
let complete = (g.closed || g.bare) && ends;
|
|
match g.operand {
|
|
Operand::Done(Held::Chain(path)) if complete => self.bind(g.name, path),
|
|
Operand::Done(Held::File) | Operand::FileName { new: true, .. } if complete => self.hold_file(g.name),
|
|
_ => self.unbind(&g.name),
|
|
}
|
|
}
|
|
|
|
/// The statement ends (a preprocessor line, or the end of the file), and with it every value still
|
|
/// being read: the name of a grouped value whose last `)` was read, or of a value without
|
|
/// parentheses whose operand is complete, holds the value, and any other name holds nothing the
|
|
/// check follows.
|
|
fn end_groups(&mut self) {
|
|
while let Some(g) = self.groups.pop() {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
self.settle(g, true);
|
|
}
|
|
}
|
|
|
|
/// The conditional expressions being read after `t`: a `?` opens one at this depth, and the `:`
|
|
/// at its depth, a closing bracket around it or a `;` at its depth ends it.
|
|
fn conditional(&mut self, t: &Sig<'s>) {
|
|
let depth = self.depth();
|
|
let end = if t.is(";") { depth } else { depth.saturating_add(1) };
|
|
while self.conds.pop_if(|d| *d >= end).is_some() {}
|
|
if t.is("?") && self.conds.len() < self.acc.limits.nesting {
|
|
self.conds.push(depth);
|
|
} else if t.is(":") && self.conds.last() == Some(&depth) {
|
|
self.conds.pop();
|
|
}
|
|
}
|
|
|
|
// ---------- preprocessor lines ----------
|
|
|
|
fn directive(&mut self, t: &Token<'s>) {
|
|
// A preprocessor line ends the statement before it.
|
|
self.end_active(None);
|
|
self.end_groups();
|
|
self.settle_pending();
|
|
self.assign = None;
|
|
self.var_depth = None;
|
|
self.conds.clear();
|
|
self.h = [None; 4];
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
let d = directives::parse(t.text);
|
|
let Some(name) = d.known else {
|
|
let detail = if d.name.is_empty() { t.text.trim().to_string() } else { format!("{}{}", d.form, d.name) };
|
|
self.acc.problem(report::UNKNOWN_DIRECTIVE, Some(t.line), self.file, Some(&detail));
|
|
return;
|
|
};
|
|
let feature = match name {
|
|
"target" => FeatureId::DirectiveTarget,
|
|
"include" | "includepath" => FeatureId::DirectiveInclude,
|
|
"targetengine" => FeatureId::DirectiveTargetengine,
|
|
_ => FeatureId::DirectiveOther,
|
|
};
|
|
let line = self.agg(t.line);
|
|
self.acc.feature(feature, line);
|
|
self.acc.directive(name, d.value, t.line, d.form, self.file);
|
|
match name {
|
|
"target" => self.acc.target(d.value, t.line, self.file),
|
|
"include" if self.includes.len() < self.acc.limits.entries => {
|
|
self.includes.push(IncludeRef { value: d.value.to_string(), line: t.line, paths: Rc::clone(&self.paths) });
|
|
}
|
|
"includepath" => self.paths = directives::include_paths(d.value).into(),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
/// At the end of the file: the chains still open, what was pending, and the brackets never
|
|
/// closed → the `#include` lines read, whether the file is a module, and what its variables
|
|
/// hold.
|
|
fn finish(mut self) -> (Vec<IncludeRef>, bool, Known) {
|
|
self.end_active(None);
|
|
while let Some(c) = self.suspended.pop() {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
self.record(c, None);
|
|
}
|
|
self.end_groups();
|
|
self.settle_pending();
|
|
let module = self.module || self.module_candidate;
|
|
for (bracket, line) in std::mem::take(&mut self.brackets.open) {
|
|
self.acc.steps = self.acc.steps.saturating_add(1);
|
|
let mut buf = [0; 4];
|
|
let bracket: &str = bracket.encode_utf8(&mut buf);
|
|
self.acc.problem(report::UNCLOSED_BRACKET, Some(line), self.file, Some(bracket));
|
|
}
|
|
let known = Known { bound: std::mem::take(&mut self.bound), files: std::mem::take(&mut self.files) };
|
|
(std::mem::take(&mut self.includes), module, known)
|
|
}
|
|
}
|