//! 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::collections::{HashMap, HashSet}; use std::rc::Rc; use crate::catalog; use crate::directives; use crate::lex::{Lexer, T, Token, expr_after}; use crate::report::{self, Acc}; /// 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, pub files: HashSet, } 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, /// 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`: the script itself (`file` none) or a file it includes (`file`: its path inside the /// script's folder), with the `#includepath` folders `paths` in force at its start and its variables /// holding what `known` says. pub(crate) fn scan(src: &str, file: Option<&str>, paths: Rc<[String]>, known: &Known, acc: &mut Acc) -> Scan { // A name written with `\u` escapes is read as the name it spells. let unescaped = unescape_names(src, acc); let src = unescaped.as_deref().unwrap_or(src); let declared = declarations(src, acc); let mut lexer = Lexer::new(src, acc.limits.nesting); let mut a = Analyzer::new(acc, file, paths, known, declared); let mut nl = false; for t in lexer.by_ref() { if let Some(code) = t.problem { a.problem(code, t.line); } match t.kind { T::Space => {} // A comment with a line break in it ends a line, as a line break does. T::Comment => nl |= t.text.contains(['\n', '\r', '\u{2028}', '\u{2029}']), T::Newline => nl = true, T::Directive => { a.directive(&t); nl = true; } _ => { a.token(Sig::of(&t, nl)); nl = false; } } } 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), or none 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. fn unescape_names(src: &str, acc: &mut Acc) -> Option { 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 } /// A significant token: what it is, its text, its line, and whether a line break comes before it. #[derive(Clone, Copy, Debug)] struct Sig<'s> { kind: T, text: &'s str, line: usize, nl: bool, /// The token ends a File or Folder object: the `)` that ends a call of `File` or `Folder` (with /// or without `new`) or of `File.openDialog`, `File.saveDialog` or `Folder.selectDialog`, the /// name in `Folder.temp` and the other places of [`catalog::FOLDER_PLACES`], or the `)` of /// grouping parentheses that hold only such an object or a variable holding one (`(f)`). file: bool, /// A statement may start at the token ([`crate::lex::Token::stmt`]). stmt: bool, /// An expression may start at the token ([`crate::lex::Token::expr`]). expr: bool, /// The byte offset in the text where the token starts. start: usize, } impl<'s> Sig<'s> { /// The significant token `t`, after a line break when `nl` is set. fn of(t: &Token<'s>, nl: bool) -> Self { Sig { kind: t.kind, text: t.text, line: t.line, nl, file: false, stmt: t.stmt, expr: t.expr, start: t.start } } fn is(&self, punct: &str) -> bool { self.kind == T::Punct && self.text == punct } fn word(&self, word: &str) -> bool { self.kind == T::Ident && self.text == word } } fn is(s: Option, punct: &str) -> bool { s.is_some_and(|s| s.is(punct)) } fn word(s: Option, w: &str) -> bool { s.is_some_and(|s| s.word(w)) } /// A token after which a name is a member, not a variable: `.`, `.@`, `..` and `::`. fn member_mark(s: Option) -> bool { s.is_some_and(|s| s.kind == T::Punct && matches!(s.text, "." | ".@" | ".." | "::")) } /// A token after which an expression goes on, also past a line break: a punctuator other than `)`, /// `]`, `}`, `++`, `--` and `;`, or `in`, `instanceof`, `typeof`, `new`, `delete` or `void`. fn goes_on_after(s: Option) -> bool { s.is_some_and(|s| match s.kind { T::Punct => !matches!(s.text, ")" | "]" | "}" | "++" | "--" | ";"), T::Ident => matches!(s.text, "in" | "instanceof" | "typeof" | "new" | "delete" | "void"), _ => false, }) } /// A token that goes on with the expression on the line before when it starts a line: a /// punctuator other than `{`, `}`, `;`, `++`, `--`, `!` and `~`, or `in` or `instanceof`. fn goes_on_with(t: &Sig) -> bool { match t.kind { T::Punct => !matches!(t.text, "{" | "}" | ";" | "++" | "--" | "!" | "~"), T::Ident => matches!(t.text, "in" | "instanceof"), _ => false, } } /// `t` is the `in` of `for (var name in …)`, which ends the `var` statement's declarations: it comes /// right after a name the statement declares (ECMA-262 3rd edition, section 12.6.4). Outside a `for` /// statement's parentheses, `in` can't stand there. fn for_in(t: &Sig, h0: Option, h1: Option) -> bool { t.word("in") && h0.is_some_and(|s| s.kind == T::Ident) && (word(h1, "var") || is(h1, ",")) } /// A token after which the value before it is complete: `;`, `,`, `:` (a conditional expression's /// colon, ECMA-262 3rd edition, section 11.12), `)`, `]` or `}`, or a token on a new line that /// doesn't go on with the expression. fn ends_value(t: &Sig) -> bool { (t.nl && !goes_on_with(t)) || (t.kind == T::Punct && matches!(t.text, ";" | "," | ":" | ")" | "]" | "}")) } /// Punctuators of later syntax. const NEWER: &[&str] = &["=>", "...", "?.", "??", "**", "**=", "??=", "&&=", "||="]; /// The compound assignment operators (ECMA-262 3rd edition, section 11.13.2): each gives the name a /// number or a string worked out from its old value. const COMPOUND: &[&str] = &["*=", "/=", "%=", "+=", "-=", "<<=", ">>=", ">>>=", "&=", "^=", "|="]; /// A number in a later form: binary, octal (`0b1`, `0o7`), with separators (`1_000`) or big (`1n`). fn newer_number(text: &str) -> bool { let b = text.as_bytes(); text.contains('_') || (b.len() > 1 && b.last() == Some(&b'n')) || matches!((b.first(), b.get(1)), (Some(b'0'), Some(b'b' | b'B' | b'o' | b'O'))) } /// A string literal's value, its quotes removed and its escapes undone (`\n`, `\xHH`, `\uHHHH`, /// octal escapes, a backslash before a line break), up to `max` characters. fn unquote(text: &str, max: usize) -> String { let mut chars = text.chars(); let body = match chars.next() { Some(q @ ('"' | '\'')) => chars.as_str().strip_suffix(q).unwrap_or(chars.as_str()), _ => text, }; let mut out = String::new(); let mut n = 0; let mut it = body.chars().peekable(); while n < max && let Some(c) = it.next() { let c = if c == '\\' { match it.next() { None => break, Some('n') => '\n', Some('t') => '\t', Some('r') => '\r', Some('b') => '\u{8}', Some('f') => '\u{c}', Some('v') => '\u{b}', // A legacy octal escape (ECMA-262 3rd edition, annex B.1.2): up to three digits // after `\0` to `\3`, two after `\4` to `\7`. Some(d @ '0'..='7') => { let mut v = d.to_digit(8).unwrap_or_default(); for _ in 0..if v <= 3 { 2 } else { 1 } { match it.peek().and_then(|o| o.to_digit(8)) { Some(o) => { v = v * 8 + o; it.next(); } None => break, } } char::from_u32(v).unwrap_or('\u{fffd}') } Some(x @ ('x' | 'u')) => { let digits = if x == 'x' { 2 } else { 4 }; let mut v = 0u32; for _ in 0..digits { match it.peek().and_then(|d| d.to_digit(16)) { Some(d) => { v = v * 16 + d; it.next(); } None => break, } } char::from_u32(v).unwrap_or('\u{fffd}') } Some('\r') => { if it.peek() == Some(&'\n') { it.next(); } continue; } Some('\n' | '\u{2028}' | '\u{2029}') => continue, Some(c) => c, } } else { c }; out.push(c); n += 1; } out } /// A global name of the platform the report looks for (`$`, `File`, `Socket`, `alert`, `eval`…). fn platform_name(n: &str) -> bool { matches!( n, catalog::DOLLAR | catalog::LOCALIZE | catalog::UNIT_VALUE | catalog::FILE | catalog::FOLDER | catalog::WINDOW | catalog::BRIDGE_TALK | catalog::SOCKET | catalog::EXTERNAL_OBJECT | "eval" | "Function" ) || catalog::NOTIFY.contains(&n) || catalog::XMP.contains(&n) || catalog::XML.contains(&n) } /// The feature of a window of type `value` (`"dialog"`, or a resource string that starts with it). fn window_type(value: &str) -> &'static str { let word = value.trim_start().split(|c: char| c.is_whitespace() || c == '{').next().unwrap_or_default(); if word.eq_ignore_ascii_case("dialog") { "scriptui.dialog" } else if word.eq_ignore_ascii_case("palette") { "scriptui.palette" } else { "scriptui.window" } } /// 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), WindowType(Option), /// `.executeMenuCommand(`: a string literal and `)` may follow. Menu(Option), MenuString(Option, &'s str), /// `.open(`: a string literal with the open mode may follow. Open(Option), /// 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), } /// `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.` ([`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, /// The grouped value ([`Group::id`]) whose operand starts with the chain. group: Option, } 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, /// The bracket depth of the `var` statement the body is in, if any: it goes on when the body /// closes. var_depth: Option, } /// 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, } /// What a name held outside the function body or `catch` block that declares it. struct Saved { /// The member chain assigned to it. chain: Option, /// 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, /// 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 } } // ---------- the names declared, read before the analysis ---------- /// The names a file's function bodies and its global code declare, read before the analysis. A /// function's `var` statements and the function declarations directly in its body make the names /// they declare the function's own before any statement in it runs, and the function declarations /// directly in the global code do the same for the whole file (ECMA-262 3rd edition, section /// 10.1.3). A `var` statement in the global code does not change a property the global object /// already has, such as the host's `alert` (sections 10.1.3, 10.1.5 and 10.2.1). The global code's /// `var` statements are not read here. A function declaration inside a block, which ECMAScript 3 /// does not allow (sections 12.1 and 14), makes its name the script's own from where it stands. #[derive(Default)] struct Declared<'s> { /// For each function body, by the offset of its `{` in the text, the names it declares, in the /// order the bodies start. bodies: Vec<(usize, Vec<&'s str>)>, /// The names the function declarations directly in the global code declare. global: HashSet<&'s str>, } /// A function body [`Declarations`] is reading: the bracket depth inside it, its entry in /// [`Declared::bodies`], the names it declares so far, and the bracket depth of the `var` statement /// the body is in, which goes on when the body closes. struct Body<'s> { depth: usize, index: usize, names: HashSet<&'s str>, var_depth: Option, } /// The pass that finds [`Declared`]. It follows brackets, function bodies and `var` statements by /// the rules [`Analyzer`] follows them by, so that both passes find the same function bodies. struct Declarations<'s, 'a, 'l> { acc: &'a mut Acc<'l>, brackets: Brackets, /// The two significant tokens before this one, the latest first. h: [Option>; 2], /// `function` was read at this bracket depth, and its body's `{` is still to come. function: Option, /// The bracket depth of the `var` statement being read. var_depth: Option, /// `name =` was read, and the first token of the value is next (`true`: after `new`). As in /// [`Analyzer::assignment`], that token doesn't start or end a `var` statement. assign: Option, /// The function bodies being read, the innermost last, up to [`crate::Limits::nesting`]. bodies: Vec>, /// The bracket depth inside the outermost function body past that limit being read. The names /// declared inside it are not kept. untracked: Option, out: Declared<'s>, } /// What `src` declares ([`Declared`]), read in one pass over its tokens. fn declarations<'s>(src: &'s str, acc: &mut Acc) -> Declared<'s> { let mut lexer = Lexer::new(src, acc.limits.nesting); let mut d = Declarations { acc, brackets: Brackets::default(), h: [None; 2], function: None, var_depth: None, assign: None, bodies: vec![], untracked: None, out: Declared::default(), }; let mut nl = false; for t in lexer.by_ref() { match t.kind { T::Space => {} T::Comment => nl |= t.text.contains(['\n', '\r', '\u{2028}', '\u{2029}']), T::Newline => nl = true, T::Directive => { d.directive(); nl = true; } _ => { d.token(Sig::of(&t, nl)); nl = false; } } } let out = d.out; acc.steps = acc.steps.saturating_add(lexer.steps); out } impl<'s> Declarations<'s, '_, '_> { fn depth(&self) -> usize { self.brackets.depth() } fn token(&mut self, t: Sig<'s>) { let [h0, h1] = self.h; // As in [`Analyzer::token`], 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(h0) && !goes_on_with(&t) { self.var_depth = None; } if t.kind == T::Punct { self.bracket(&t); } else if t.kind == T::Ident { self.declaration(&t); } let depth = self.depth(); if let Some(after_new) = self.assign.take() { if t.word("new") && !after_new { self.assign = Some(true); } } else { if t.word("var") { self.var_depth = Some(depth); } if (t.is(";") || for_in(&t, h0, h1)) && self.var_depth == Some(depth) { self.var_depth = None; } if t.is("=") && h0.is_some_and(|s| s.kind == T::Ident) && !member_mark(h1) && !is(h1, "@") { self.assign = Some(false); } } // `function` that isn't a property's name: its body's `{` comes after its name and // parameters. if t.word("function") && !member_mark(h0) && !word(h0, "function") { self.function = Some(depth); } self.h = [Some(t), h0]; } /// An open or close bracket, by the rules of [`Analyzer::bracket`]: the first `{` after /// `function` at the depth of `function` opens the function's body. fn bracket(&mut self, t: &Sig<'s>) { match t.text { "(" | "[" | "{" => { let body = t.text == "{" && self.function.take() == Some(self.depth()); self.brackets.open(t, self.acc.limits.nesting); if !body { return; } if self.bodies.len() < self.acc.limits.nesting { let index = self.out.bodies.len(); self.out.bodies.push((t.start, vec![])); self.bodies.push(Body { depth: self.depth(), index, names: HashSet::new(), var_depth: self.var_depth.take() }); } else if self.untracked.is_none() { self.untracked = Some(self.depth()); } } ")" | "]" | "}" => { if !self.brackets.close(t) { return; } let depth = self.depth(); if self.var_depth.is_some_and(|d| depth < d) { self.var_depth = None; } while let Some(body) = self.bodies.pop_if(|b| depth < b.depth) { self.var_depth = body.var_depth; } if self.untracked.is_some_and(|d| depth < d) { self.untracked = None; } } _ => {} } } /// The name `t`, when a `var` statement or a function declaration declares it: kept for the /// innermost function body when the `var` statement is in that body or the declaration is /// directly in it, and for the global code when the declaration is directly in the global code. /// Each body keeps up to [`crate::Limits::entries`] names, and so does the global code. fn declaration(&mut self, t: &Sig<'s>) { let [h0, _] = self.h; let depth = self.depth(); let var = word(h0, "var") || (is(h0, ",") && self.var_depth == Some(depth)); let function = word(h0, "function") && h0.is_some_and(|f| f.stmt); if !(var || function) || self.untracked.is_some() { return; } if let Some(body) = self.bodies.last_mut() { if (var || body.depth == depth) && !body.names.contains(t.text) && self.acc.room(body.names.len()) { body.names.insert(t.text); if let Some((_, names)) = self.out.bodies.get_mut(body.index) { names.push(t.text); } } } else if function && depth == 0 && !self.out.global.contains(t.text) && self.acc.room(self.out.global.len()) { self.out.global.insert(t.text); } } /// A preprocessor line ends the statement before it, as in [`Analyzer::directive`]. fn directive(&mut self) { self.h = [None; 2]; self.var_depth = None; self.assign = None; } } 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>; 4], brackets: Brackets, /// Nesting too deep to follow was reported: it is reported once. deep: bool, active: Option, /// Chains whose index or call is open, the innermost last. suspended: Vec, /// 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, /// Variables holding a File or Folder object. files: HashSet, /// 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, /// The function bodies being read, the innermost last, up to [`crate::Limits::nesting`]. scopes: Vec, /// The names each function body and the global code declare, read before the analysis. declared: 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, /// 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)>, /// The name of the function expression whose parameters are still to come: a variable inside /// that function only. expression_name: Option, /// `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)>, /// The `catch` blocks being read, the innermost last, up to [`crate::Limits::nesting`]. catches: Vec, /// The parameters of the `catch` blocks being read, each with the depth inside the innermost /// block it names. catch_names: HashMap, /// 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, /// 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, /// 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, /// The bracket depth of each `?` whose `:` is still to come, the latest last, up to /// [`crate::Limits::nesting`] of them. conds: Vec, assign: Option, /// The bracket depth of the `var` statement being read. var_depth: Option, pending: Pending<'s>, module_candidate: bool, module: bool, includes: Vec, /// 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: 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 { 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("preferences", line); if c.change_call || ending.is_some_and(|t| t.is("=")) { self.acc.outside("preferences", line, None); } } if ["app.name", "app.version"].iter().any(|p| c.path.strip_prefix(p).is_some_and(then)) { self.acc.feature("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`, taken once. fn declared_in(&mut self, start: usize) -> Vec<&'s str> { let bodies = &mut self.declared.bodies; while bodies.get(self.next_body).is_some_and(|(at, _)| *at < start) { self.next_body += 1; } match bodies.get_mut(self.next_body) { Some((at, names)) if *at == start => std::mem::take(names), _ => vec![], } } /// `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, 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(); } } // ---------- features ---------- fn features(&mut self, t: &Sig<'s>) { let line = self.agg(t.line); let mut pending = std::mem::take(&mut self.pending); // A platform name that no `:` follows stands for the platform's object. if let Pending::Name(n, l) = pending { pending = Pending::None; if !t.is(":") { self.platform(n, l); pending = std::mem::take(&mut self.pending); } } match pending { Pending::None | Pending::Name(..) => {} Pending::Window(l) => { if t.is("(") { self.pending = Pending::WindowType(l); } else { self.acc.feature("scriptui.window", l); } } Pending::WindowType(l) => { let feature = if t.kind == T::Str { window_type(&unquote(t.text, 64)) } else { "scriptui.window" }; self.acc.feature(feature, l); } Pending::Menu(l) => { if t.kind == T::Str { self.pending = Pending::MenuString(l, t.text); } else { self.menu(None, l); } } Pending::MenuString(l, s) => self.menu(t.is(")").then_some(s), l), Pending::Open(l) => { if t.kind == T::Str { self.open_mode(&unquote(t.text, 2), l); } } } match t.kind { T::Ident => self.name(t, line), T::Punct => self.punct(t, line), T::Str => self.string(t, line), T::Xml => self.acc.feature("e4x", line), _ => {} } } /// A menu command run with the string literal `literal`, or with another value. fn menu(&mut self, literal: Option<&str>, line: Option) { match literal { Some(s) => { let value = unquote(s, self.acc.limits.echo.saturating_add(1)); self.acc.menu_string(&value, line); } None => self.acc.menu_dynamic(), } self.acc.feature("menuCommand", line); } fn open_mode(&mut self, mode: &str, line: Option) { if mode.eq_ignore_ascii_case("r") { self.acc.feature("file.read", line); } else if let Some(m) = catalog::WRITE_MODES.iter().copied().find(|m| m.eq_ignore_ascii_case(mode)) { self.acc.feature("file.write", line); self.acc.outside("filesWritten", line, Some(m)); } } /// What a pending feature waits for that never came. fn settle_pending(&mut self) { let mut pending = std::mem::take(&mut self.pending); if let Pending::Name(n, l) = pending { self.platform(n, l); pending = std::mem::take(&mut self.pending); } match pending { Pending::Window(l) | Pending::WindowType(l) => self.acc.feature("scriptui.window", l), Pending::Menu(l) | Pending::MenuString(l, _) => self.menu(None, l), Pending::Open(_) | Pending::Name(..) | Pending::None => {} } } /// The platform name `n`, used at `line` (a tally's line): the feature it is, and what it does /// outside the document. fn platform(&mut self, n: &'s str, line: Option) { match n { catalog::UNIT_VALUE => self.acc.feature("unitValue", line), catalog::FILE | catalog::FOLDER => self.acc.feature("file.objects", line), catalog::BRIDGE_TALK => { self.acc.feature("bridgeTalk", line); self.acc.outside("otherApps", line, None); } catalog::SOCKET => { self.acc.feature("socket", line); self.acc.outside("network", line, None); } catalog::EXTERNAL_OBJECT => { self.acc.feature("externalObject", line); self.acc.outside("nativeCode", line, None); } catalog::WINDOW => self.pending = Pending::Window(line), _ if catalog::XMP.contains(&n) => self.acc.feature("xmp", line), _ if catalog::XML.contains(&n) => self.acc.feature("xml", line), _ => {} } } fn name(&mut self, t: &Sig<'s>, line: Option) { let [h0, h1, h2, _] = self.h; let n = t.text; if is(h0, ".") { // A member: of `$`, `File` or `Folder` when they come first and are the platform's. let own = &self.own_names; let of = |name: &str| word(h1, name) && !member_mark(h2) && !own.contains(name); if of(catalog::DOLLAR) { if catalog::DOLLAR_ENV.contains(&n) { self.acc.feature("dollar.env", line); self.acc.outside("environment", line, None); } else if n == catalog::DOLLAR_EVAL_FILE { self.acc.feature("dollar.evalFile", line); self.acc.outside("unseenCode", line, None); } else if n == catalog::LOCALIZE { self.acc.feature("localize", line); } else { self.acc.feature("dollar", line); } } else if (of(catalog::FILE) && catalog::FILE_DIALOGS.contains(&n)) || (of(catalog::FOLDER) && catalog::FOLDER_DIALOGS.contains(&n)) { self.acc.feature("file.dialog", line); } else if of(catalog::FOLDER) && catalog::FOLDER_PLACES.contains(&n) { self.acc.feature("file.places", line); } if n == catalog::ON_DRAW { self.acc.feature("scriptui.drawing", line); } if n == catalog::REFLECT { self.acc.feature("reflection", line); } return; } if member_mark(h0) { return; } if word(h0, "function") { self.acc.declared(n); if h0.is_some_and(|f| f.stmt) { // A function declaration's name is a variable where the function is declared. A // declaration directly in a function body or in the global code makes it one from the // body's `{` or from the start of the file ([`Declared`]), and any other declaration // makes it one from here. if !self.hoisted(n) { self.keep_outer(n); self.unbind(n); } } else { // A function expression's name is a variable inside the function only. self.expression_name = Some(n.to_string()); } return; } if n == "catch" { // Its block's `{` comes after its parameter's parentheses. self.catch = Some((self.depth(), None)); } if n == "function" { // Its body's `{` comes after its name and parameters. self.function = Some(self.depth()); self.expression_name = None; // `name = function`: the script's own function too. if is(h0, "=") && let Some(f) = h1.filter(|s| s.kind == T::Ident) { self.acc.declared(f.text); } } if word(h0, "new") && n.starts_with(|c: char| c.is_ascii_uppercase()) && !catalog::CONSTRUCTORS.contains(&n) { self.acc.type_name(n); } // A platform name stands for the platform's object, except where the script declares a // variable, function or parameter of that name, and where it has a meaning of its own. // Where a property key or a label may stand, the token after it decides. if platform_name(n) && !self.declaring() && !self.own_names.contains(n) { if h0.is_none() || [",", "{", ";", "}"].iter().any(|p| is(h0, p)) { self.pending = Pending::Name(n, line); } else { self.platform(n, line); } } match n { // E4X: `default xml namespace = …` and `for each (…)`. "namespace" if word(h0, "xml") && word(h1, "default") => self.acc.feature("e4x", line), "each" if word(h0, "for") => self.acc.feature("e4x", line), _ => {} } } fn punct(&mut self, t: &Sig<'s>, line: Option) { let [h0, h1, h2, h3] = self.h; match t.text { "(" => self.call(line), // `Name.prototype["+"] = …` defines an operator. "=" => { let operator = h1.filter(|s| s.kind == T::Str).is_some_and(|s| catalog::OPERATORS.contains(&unquote(s.text, 4).as_str())); if is(h0, "]") && operator && is(h2, "[") && word(h3, "prototype") { self.acc.feature("operatorOverloading", line); } } // E4X: attributes (`x.@id`, `@*`), descendants (`x..item`) and qualified names (`ns::x`). ".@" | ".." | "::" | "@" => self.acc.feature("e4x", line), _ => {} } } /// `(` after a name: a call. fn call(&mut self, line: Option) { let [h0, h1, h2, h3] = self.h; let Some(callee) = h0.filter(|s| s.kind == T::Ident) else { return }; let n = callee.text; if is(h1, ".") { // The receiver is a variable holding a File or Folder object (not a property of the same // name), or a File or Folder object made in place or held in grouping parentheses // ([`Sig::file`]). let file = h2.is_some_and(|r| r.file || (r.kind == T::Ident && !member_mark(h3) && self.files.contains(r.text))); if n == catalog::MENU_COMMAND { self.pending = Pending::Menu(line); } else if n == catalog::OPEN && file { self.pending = Pending::Open(line); } else if catalog::DRAWING.contains(&n) { self.acc.feature("scriptui.drawing", line); } else if catalog::INSTANCE_DIALOGS.contains(&n) { self.acc.feature("file.dialog", line); } else if catalog::FOLDER_READS.contains(&n) || (file && catalog::FILE_READS.contains(&n)) { self.acc.feature("file.read", line); } else if file && catalog::FILE_WRITES.contains(&n) { self.acc.feature("file.write", line); self.acc.outside("filesWritten", line, None); } else if n == catalog::EXECUTE { self.acc.feature("file.execute", line); self.acc.outside("filesOpened", line, None); } else if catalog::ACTIONS.contains(&n) { self.acc.feature("actions", line); } else if n == catalog::SELECT_TOOL { self.acc.feature("selectTool", line); } } else if !member_mark(h1) && !word(h1, "function") && !self.own_names.contains(n) { if catalog::NOTIFY.contains(&n) { self.acc.feature("notify", line); } else if n == catalog::LOCALIZE { self.acc.feature("localize", line); } else if matches!(n, "eval" | "Function") { self.acc.feature("dynamicCode", line); self.acc.outside("unseenCode", line, None); } } } fn string(&mut self, t: &Sig<'s>, line: Option) { let [h0, h1, h2, _] = self.h; if t.text.contains(catalog::ACTION_TEXT) { self.acc.feature("actions", line); } let compares = h0.is_some_and(|s| s.kind == T::Punct && matches!(s.text, "==" | "===" | "!=" | "!==")); if compares && word(h1, "typename") && is(h2, ".") { let name = unquote(t.text, self.acc.limits.echo.saturating_add(1)); self.acc.type_name(&name); } } /// Syntax later than ECMAScript 3: reported once per construct and line. fn newer(&mut self, t: &Sig<'s>) { let [h0, h1, ..] = self.h; let construct: Option<&'s str> = match t.kind { T::Template => Some("`"), T::Punct if NEWER.contains(&t.text) => Some(t.text), T::Number if newer_number(t.text) => Some(t.text), T::Other if t.text.len() > 1 && t.text.starts_with('#') => Some(t.text), T::Ident if word(h0, "class") && !member_mark(h1) => Some("class"), // `let name` declares a variable. ES3 reads `let` as a variable's name in `let = 1`, // `let.x` and `let in o`. T::Ident if word(h0, "let") && !t.nl && !member_mark(h1) && !matches!(t.text, "in" | "instanceof") => Some("let"), T::Ident if t.text == "function" && word(h0, "async") && !t.nl => Some("async"), _ => None, }; if let Some(c) = construct && self.last_newer != Some((t.line, c)) { self.last_newer = Some((t.line, c)); self.acc.problem(report::NEWER_SYNTAX, Some(t.line), self.file, Some(c)); } } /// `import` or `export` starting a statement outside any bracket, unless a call, member or /// property (`import(…)`, `export.x`, `import: 1`) follows: a module. fn module_check(&mut self, t: &Sig<'s>) { if std::mem::take(&mut self.module_candidate) && !(t.kind == T::Punct && matches!(t.text, "(" | "." | "=" | ":" | "," | ")")) { self.module = true; } let starts = self.h[0].is_none() || t.nl || is(self.h[0], ";") || is(self.h[0], "}"); if t.kind == T::Ident && matches!(t.text, "import" | "export") && self.depth() == 0 && !member_mark(self.h[0]) && starts { self.module_candidate = true; } } // ---------- 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" => "directive.target", "include" | "includepath" => "directive.include", "targetengine" => "directive.targetengine", _ => "directive.other", }; 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, 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) } }