Files
vectorcraft/crates/scriptcheck/src/analyze.rs
T
dotCooCoo 3374d0778d Scripts: Check Script… reports what a .jsx script needs and does, without running it
VectorCraft does not run scripts written for the reference app, and
File › Scripts held only the disabled Other Script…. File › Scripts ›
Check Script… picks a .jsx, .jsxinc or .jsxbin file, reads it without
running any of it, and shows a report: what the script needs (the
script language's own objects, the document model, files, menu
commands, dialogs, or features only the reference app has), what it
would do outside the document, the menu-command strings it passes, the
document-model member chains and type names it uses, its directives,
the files it includes, and the problems that would stop it loading.
Copy Report copies the report as JSON. File › Open and drag-and-drop
show the report for those files too.

- vectorcraft-scriptcheck, a new Standalone crate with no
  dependencies, holds the checker: a lexer for ECMAScript 3 with E4X
  literals and the #name and //@name directive lines, and an analysis
  of the tokens that follows variables, function scopes and the files
  a script includes. It never recurses: open brackets, the levels of
  an XML literal and the function bodies being read are kept on stacks
  capped at 4,096. It reads at most 8 MiB of a file and 32 MiB with the
  included files, follows 64 included files 16 deep, looks up at most
  1,024 paths for them, keeps 1,000 entries in a list, tracks 1,000
  variables holding member chains and 1,000 holding File objects, and
  cuts echoed text to 120 characters. A compiled script is reported as
  compiled and isn't read.
- script.check (engine, a query) takes path, data or dataBase64. With a
  path and includes: true, it reads the files the #include lines name
  from inside the script's folder, through the automation read root,
  and when the script or one of those files assigns a member chain or a
  File object to a variable, the check treats the variable as holding
  it in every file. It doesn't follow absolute names, names, folders or
  links that lead out of the folder, names or folders longer than 4,096
  bytes, or JSINCLUDE, and it reads only regular files.
- ui.checkScript shows the report (dialog scriptCheck). File › Open
  lists Scripts (.jsx, .jsxinc, .jsxbin) and sends a script's path to
  script.check, which reads at most 8 MiB of it.

The language rules come from ECMA-262 3rd edition and ECMA-357 2nd
edition. The directive, platform and document-model root names come
from the public JavaScript Tools Guide and scripting guide pages that
catalog.rs cites by URL, and executeMenuCommand, doScript, loadAction,
unloadAction and selectTool come from the public forum posts cited
there; only names are used. Every script in the tests is written from
scratch with invented names.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-13 13:36:41 +07:00

2092 lines
88 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::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<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`: 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<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
}
/// 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<Sig>, punct: &str) -> bool {
s.is_some_and(|s| s.is(punct))
}
fn word(s: Option<Sig>, 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<Sig>) -> 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<Sig>) -> 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<Sig>, h1: Option<Sig>) -> 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<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
}
}
// ---------- 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<usize>,
}
/// 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<Sig<'s>>; 2],
/// `function` was read at this bracket depth, and its body's `{` is still to come.
function: Option<usize>,
/// The bracket depth of the `var` statement being read.
var_depth: Option<usize>,
/// `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<bool>,
/// The function bodies being read, the innermost last, up to [`crate::Limits::nesting`].
bodies: Vec<Body<'s>>,
/// The bracket depth inside the outermost function body past that limit being read. The names
/// declared inside it are not kept.
untracked: Option<usize>,
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<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: 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: 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("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<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();
}
}
// ---------- 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<usize>) {
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<usize>) {
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<usize>) {
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<usize>) {
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<usize>) {
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<usize>) {
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<usize>) {
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<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)
}
}