Merge pull request #4025 from vangel76/refactor/scriptcheck-feature-enums

refactor(scriptcheck): feature enums, split analyze.rs
This commit is contained in:
BFlat
2026-10-24 21:13:11 +07:00
committed by GitHub
8 changed files with 876 additions and 805 deletions
+22 -720
View File
@@ -23,10 +23,18 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use crate::catalog;
use crate::catalog::{self, FeatureId};
use crate::directives;
use crate::lex::{Lexer, T, Token, expr_after};
use crate::report::{self, Acc};
use crate::report::{self, Acc, OutsideId};
mod declared;
mod features;
mod sig;
pub(crate) use declared::{Declared, declarations};
use features::platform_name;
use sig::*;
/// An `#include` line: what it names, its line, and the folders `#includepath` named at that point
/// (one list, shared by every line it applies to).
@@ -84,24 +92,15 @@ pub(crate) struct Scan {
pub(crate) fn scan<'s>(src: &'s str, declared: &Declared<'s>, file: Option<&str>, paths: Rc<[String]>, known: &Known, acc: &mut Acc) -> Scan {
let mut lexer = Lexer::new(src, acc.limits.nesting);
let mut a = Analyzer::new(acc, file, paths, known, declared);
let mut nl = false;
let mut breaks = LineBreaks::default();
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;
}
match breaks.read(&t) {
Read::Skip => {}
Read::Directive => a.directive(&t),
Read::Token(sig) => a.token(sig),
}
}
if let Some(line) = lexer.unclosed_xml {
@@ -175,203 +174,6 @@ fn unescape_name(text: &str) -> String {
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> {
@@ -648,202 +450,6 @@ impl Brackets {
}
}
// ---------- 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)]
pub(crate) 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.
pub(crate) 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>,
@@ -1101,13 +707,13 @@ impl<'s, 'a, 'l> Analyzer<'s, 'a, 'l> {
self.acc.member(&c.path);
}
if c.path.strip_prefix("app.preferences").is_some_and(then) {
self.acc.feature("preferences", line);
self.acc.feature(FeatureId::Preferences, line);
if c.change_call || ending.is_some_and(|t| t.is("=")) {
self.acc.outside("preferences", line, None);
self.acc.outside(OutsideId::Preferences, line, None);
}
}
if ["app.name", "app.version"].iter().any(|p| c.path.strip_prefix(p).is_some_and(then)) {
self.acc.feature("hostCheck", line);
self.acc.feature(FeatureId::HostCheck, line);
}
}
// A chain that an operand in a grouped value's parentheses starts with is complete: the
@@ -1732,310 +1338,6 @@ impl<'s, 'a, 'l> Analyzer<'s, 'a, 'l> {
}
}
// ---------- 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, "]") && is(h2, "[") && word(h3, "prototype") && operator() {
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>) {
@@ -2055,10 +1357,10 @@ impl<'s, 'a, 'l> Analyzer<'s, 'a, 'l> {
return;
};
let feature = match name {
"target" => "directive.target",
"include" | "includepath" => "directive.include",
"targetengine" => "directive.targetengine",
_ => "directive.other",
"target" => FeatureId::DirectiveTarget,
"include" | "includepath" => FeatureId::DirectiveInclude,
"targetengine" => FeatureId::DirectiveTargetengine,
_ => FeatureId::DirectiveOther,
};
let line = self.agg(t.line);
self.acc.feature(feature, line);
+189
View File
@@ -0,0 +1,189 @@
//! The names declared, read before the analysis.
use super::*;
/// 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)]
pub(crate) 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.
pub(super) bodies: Vec<(usize, Vec<&'s str>)>,
/// The names the function declarations directly in the global code declare.
pub(super) 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.
pub(crate) 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 breaks = LineBreaks::default();
for t in lexer.by_ref() {
match breaks.read(&t) {
Read::Skip => {}
Read::Directive => d.directive(),
Read::Token(sig) => d.token(sig),
}
}
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;
}
}
+339
View File
@@ -0,0 +1,339 @@
//! The platform features, menu commands and later syntax the analysis recognises.
use super::*;
/// A global name of the platform the report looks for (`$`, `File`, `Socket`, `alert`, `eval`…).
pub(super) 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).
pub(super) fn window_type(value: &str) -> FeatureId {
let word = value.trim_start().split(|c: char| c.is_whitespace() || c == '{').next().unwrap_or_default();
if word.eq_ignore_ascii_case("dialog") {
FeatureId::ScriptuiDialog
} else if word.eq_ignore_ascii_case("palette") {
FeatureId::ScriptuiPalette
} else {
FeatureId::ScriptuiWindow
}
}
impl<'s, 'a, 'l> Analyzer<'s, 'a, 'l> {
pub(super) 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(FeatureId::ScriptuiWindow, l);
}
}
Pending::WindowType(l) => {
let feature = if t.kind == T::Str { window_type(&unquote(t.text, 64)) } else { FeatureId::ScriptuiWindow };
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(FeatureId::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(FeatureId::MenuCommand, line);
}
fn open_mode(&mut self, mode: &str, line: Option<usize>) {
if mode.eq_ignore_ascii_case("r") {
self.acc.feature(FeatureId::FileRead, line);
} else if let Some(m) = catalog::WRITE_MODES.iter().copied().find(|m| m.eq_ignore_ascii_case(mode)) {
self.acc.feature(FeatureId::FileWrite, line);
self.acc.outside(OutsideId::FilesWritten, line, Some(m));
}
}
/// What a pending feature waits for that never came.
pub(super) 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(FeatureId::ScriptuiWindow, 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(FeatureId::UnitValue, line),
catalog::FILE | catalog::FOLDER => self.acc.feature(FeatureId::FileObjects, line),
catalog::BRIDGE_TALK => {
self.acc.feature(FeatureId::BridgeTalk, line);
self.acc.outside(OutsideId::OtherApps, line, None);
}
catalog::SOCKET => {
self.acc.feature(FeatureId::Socket, line);
self.acc.outside(OutsideId::Network, line, None);
}
catalog::EXTERNAL_OBJECT => {
self.acc.feature(FeatureId::ExternalObject, line);
self.acc.outside(OutsideId::NativeCode, line, None);
}
catalog::WINDOW => self.pending = Pending::Window(line),
_ if catalog::XMP.contains(&n) => self.acc.feature(FeatureId::Xmp, line),
_ if catalog::XML.contains(&n) => self.acc.feature(FeatureId::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(FeatureId::DollarEnv, line);
self.acc.outside(OutsideId::Environment, line, None);
} else if n == catalog::DOLLAR_EVAL_FILE {
self.acc.feature(FeatureId::DollarEvalFile, line);
self.acc.outside(OutsideId::UnseenCode, line, None);
} else if n == catalog::LOCALIZE {
self.acc.feature(FeatureId::Localize, line);
} else {
self.acc.feature(FeatureId::Dollar, line);
}
} else if (of(catalog::FILE) && catalog::FILE_DIALOGS.contains(&n)) || (of(catalog::FOLDER) && catalog::FOLDER_DIALOGS.contains(&n)) {
self.acc.feature(FeatureId::FileDialog, line);
} else if of(catalog::FOLDER) && catalog::FOLDER_PLACES.contains(&n) {
self.acc.feature(FeatureId::FilePlaces, line);
}
if n == catalog::ON_DRAW {
self.acc.feature(FeatureId::ScriptuiDrawing, line);
}
if n == catalog::REFLECT {
self.acc.feature(FeatureId::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(FeatureId::E4x, line),
"each" if word(h0, "for") => self.acc.feature(FeatureId::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, "]") && is(h2, "[") && word(h3, "prototype") && operator() {
self.acc.feature(FeatureId::OperatorOverloading, line);
}
}
// E4X: attributes (`x.@id`, `@*`), descendants (`x..item`) and qualified names (`ns::x`).
".@" | ".." | "::" | "@" => self.acc.feature(FeatureId::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(FeatureId::ScriptuiDrawing, line);
} else if catalog::INSTANCE_DIALOGS.contains(&n) {
self.acc.feature(FeatureId::FileDialog, line);
} else if catalog::FOLDER_READS.contains(&n) || (file && catalog::FILE_READS.contains(&n)) {
self.acc.feature(FeatureId::FileRead, line);
} else if file && catalog::FILE_WRITES.contains(&n) {
self.acc.feature(FeatureId::FileWrite, line);
self.acc.outside(OutsideId::FilesWritten, line, None);
} else if n == catalog::EXECUTE {
self.acc.feature(FeatureId::FileExecute, line);
self.acc.outside(OutsideId::FilesOpened, line, None);
} else if catalog::ACTIONS.contains(&n) {
self.acc.feature(FeatureId::Actions, line);
} else if n == catalog::SELECT_TOOL {
self.acc.feature(FeatureId::SelectTool, line);
}
} else if !member_mark(h1) && !word(h1, "function") && !self.own_names.contains(n) {
if catalog::NOTIFY.contains(&n) {
self.acc.feature(FeatureId::Notify, line);
} else if n == catalog::LOCALIZE {
self.acc.feature(FeatureId::Localize, line);
} else if matches!(n, "eval" | "Function") {
self.acc.feature(FeatureId::DynamicCode, line);
self.acc.outside(OutsideId::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(FeatureId::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.
pub(super) 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.
pub(super) 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;
}
}
}
+203
View File
@@ -0,0 +1,203 @@
//! The significant tokens the analysis reads, and the checks on them that both passes share.
use crate::lex::{T, Token};
/// A significant token: what it is, its text, its line, and whether a line break comes before it.
#[derive(Clone, Copy, Debug)]
pub(super) struct Sig<'s> {
pub(super) kind: T,
pub(super) text: &'s str,
pub(super) line: usize,
pub(super) 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)`).
pub(super) file: bool,
/// A statement may start at the token ([`crate::lex::Token::stmt`]).
pub(super) stmt: bool,
/// An expression may start at the token ([`crate::lex::Token::expr`]).
pub(super) expr: bool,
/// The byte offset in the text where the token starts.
pub(super) start: usize,
}
impl<'s> Sig<'s> {
/// The significant token `t`, after a line break when `nl` is set.
pub(super) 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 }
}
pub(super) fn is(&self, punct: &str) -> bool {
self.kind == T::Punct && self.text == punct
}
pub(super) fn word(&self, word: &str) -> bool {
self.kind == T::Ident && self.text == word
}
}
/// What a token means to the passes that follow the significant tokens.
pub(super) enum Read<'s> {
/// A blank, or a comment or line break (remembered for the next token).
Skip,
/// A preprocessor line.
Directive,
Token(Sig<'s>),
}
/// Whether a line break came since the last significant token.
#[derive(Default)]
pub(super) struct LineBreaks(bool);
impl LineBreaks {
pub(super) fn read<'s>(&mut self, t: &Token<'s>) -> Read<'s> {
match t.kind {
T::Space => Read::Skip,
// A comment with a line break in it ends a line, as a line break does.
T::Comment => {
self.0 |= t.text.contains(['\n', '\r', '\u{2028}', '\u{2029}']);
Read::Skip
}
T::Newline => {
self.0 = true;
Read::Skip
}
T::Directive => {
self.0 = true;
Read::Directive
}
_ => Read::Token(Sig::of(t, std::mem::take(&mut self.0))),
}
}
}
pub(super) fn is(s: Option<Sig>, punct: &str) -> bool {
s.is_some_and(|s| s.is(punct))
}
pub(super) 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 `::`.
pub(super) 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`.
pub(super) 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`.
pub(super) 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.
pub(super) 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.
pub(super) fn ends_value(t: &Sig) -> bool {
(t.nl && !goes_on_with(t)) || (t.kind == T::Punct && matches!(t.text, ";" | "," | ":" | ")" | "]" | "}"))
}
/// Punctuators of later syntax.
pub(super) 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.
pub(super) const COMPOUND: &[&str] = &["*=", "/=", "%=", "+=", "-=", "<<=", ">>=", ">>>=", "&=", "^=", "|="];
/// A number in a later form: binary, octal (`0b1`, `0o7`), with separators (`1_000`) or big (`1n`).
pub(super) 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.
pub(super) 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
}
+64 -61
View File
@@ -114,66 +114,69 @@ pub(crate) const CONSTRUCTORS: &[&str] = &[
"XMPDateTime",
];
/// Every feature id a report can list, in the order the report lists them.
pub const FEATURES: &[&str] = &[
"actions",
"bridgeTalk",
"directive.include",
"directive.other",
"directive.target",
"directive.targetengine",
"dollar",
"dollar.env",
"dollar.evalFile",
"dynamicCode",
"e4x",
"externalObject",
"file.dialog",
"file.execute",
"file.objects",
"file.places",
"file.read",
"file.write",
"hostCheck",
"localize",
"menuCommand",
"notify",
"operatorOverloading",
"preferences",
"reflection",
"scriptui.dialog",
"scriptui.drawing",
"scriptui.palette",
"scriptui.window",
"selectTool",
"socket",
"unitValue",
"xml",
"xmp",
];
/// The need feature `id` sets.
pub(crate) fn need(id: &str) -> Option<Need> {
Some(match id {
"directive.target" | "directive.include" | "directive.other" | "dollar" | "dynamicCode" | "notify" | "localize" | "unitValue"
| "preferences" | "hostCheck" => Need::Language,
"dollar.env" | "dollar.evalFile" | "file.objects" | "file.read" | "file.write" | "file.dialog" | "file.places" | "file.execute" => {
Need::Files
/// Declares [`FeatureId`] from its table: each platform feature a report can list, with its id and
/// the need it sets. The table is sorted by id, which is the order the report lists them in.
macro_rules! features {
($($variant:ident $id:literal $need:ident,)*) => {
/// A platform feature a report can list ([`crate::Feature::id`] is its [`FeatureId::id`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum FeatureId {
$($variant),*
}
"menuCommand" => Need::MenuCommands,
"scriptui.dialog" | "scriptui.window" | "scriptui.drawing" => Need::Dialogs,
"directive.targetengine"
| "scriptui.palette"
| "bridgeTalk"
| "socket"
| "externalObject"
| "xmp"
| "xml"
| "e4x"
| "operatorOverloading"
| "reflection"
| "actions"
| "selectTool" => Need::HostOnly,
_ => return None,
})
impl FeatureId {
#[cfg(test)]
pub(crate) const ALL: &[FeatureId] = &[$(FeatureId::$variant),*];
pub(crate) fn id(self) -> &'static str {
match self {
$(FeatureId::$variant => $id),*
}
}
/// The need the feature sets.
pub(crate) fn need(self) -> Need {
match self {
$(FeatureId::$variant => Need::$need),*
}
}
}
};
}
features! {
Actions "actions" HostOnly,
BridgeTalk "bridgeTalk" HostOnly,
DirectiveInclude "directive.include" Language,
DirectiveOther "directive.other" Language,
DirectiveTarget "directive.target" Language,
DirectiveTargetengine "directive.targetengine" HostOnly,
Dollar "dollar" Language,
DollarEnv "dollar.env" Files,
DollarEvalFile "dollar.evalFile" Files,
DynamicCode "dynamicCode" Language,
E4x "e4x" HostOnly,
ExternalObject "externalObject" HostOnly,
FileDialog "file.dialog" Files,
FileExecute "file.execute" Files,
FileObjects "file.objects" Files,
FilePlaces "file.places" Files,
FileRead "file.read" Files,
FileWrite "file.write" Files,
HostCheck "hostCheck" Language,
Localize "localize" Language,
MenuCommand "menuCommand" MenuCommands,
Notify "notify" Language,
OperatorOverloading "operatorOverloading" HostOnly,
Preferences "preferences" Language,
Reflection "reflection" HostOnly,
ScriptuiDialog "scriptui.dialog" Dialogs,
ScriptuiDrawing "scriptui.drawing" Dialogs,
ScriptuiPalette "scriptui.palette" HostOnly,
ScriptuiWindow "scriptui.window" Dialogs,
SelectTool "selectTool" HostOnly,
Socket "socket" HostOnly,
UnitValue "unitValue" Language,
Xml "xml" HostOnly,
Xmp "xmp" HostOnly,
}
-2
View File
@@ -137,8 +137,6 @@ impl Need {
}
}
/// Every feature id a report can list, in the order the report lists them.
pub use catalog::FEATURES;
/// Every id in [`Report::outside`], in the order the report lists them.
pub use report::OUTSIDE;
/// Every problem code a report can list.
+45 -15
View File
@@ -3,7 +3,8 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use crate::{Kind, Limits, Need, catalog};
use crate::catalog::FeatureId;
use crate::{Kind, Limits, Need};
pub(crate) const UNTERMINATED_STRING: &str = "unterminatedString";
pub(crate) const UNTERMINATED_COMMENT: &str = "unterminatedComment";
@@ -34,10 +35,40 @@ pub const PROBLEMS: &[&str] = &[
TOO_DEEP,
];
/// Every id in [`Report::outside`], in the order the report lists them: files written, moved or
/// deleted; files opened in other apps; network connections; native code; messages to other apps;
/// environment variables; code the checker can't read; the reference app's preferences changed.
pub const OUTSIDE: &[&str] = &["filesWritten", "filesOpened", "network", "nativeCode", "otherApps", "environment", "unseenCode", "preferences"];
/// Declares [`OutsideId`] and [`OUTSIDE`] from one table, in the order the report lists them.
macro_rules! outside {
($($variant:ident $id:literal,)*) => {
/// Something the script does outside the document ([`Outside::id`] is its [`OutsideId::id`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum OutsideId {
$($variant),*
}
impl OutsideId {
pub(crate) fn id(self) -> &'static str {
match self {
$(OutsideId::$variant => $id),*
}
}
}
/// Every id in [`Report::outside`], in the order the report lists them: files written, moved or
/// deleted; files opened in other apps; network connections; native code; messages to other apps;
/// environment variables; code the checker can't read; the reference app's preferences changed.
pub const OUTSIDE: &[&str] = &[$($id),*];
};
}
outside! {
FilesWritten "filesWritten",
FilesOpened "filesOpened",
Network "network",
NativeCode "nativeCode",
OtherApps "otherApps",
Environment "environment",
UnseenCode "unseenCode",
Preferences "preferences",
}
/// What [`crate::check`] found in a script. Lists of located things (targets, directives,
/// includes, problems) give a line and, for an included file, its path (`file`); tallies (features,
@@ -54,7 +85,7 @@ pub struct Report {
pub targets: Vec<Target>,
pub directives: Vec<Directive>,
pub includes: Vec<Include>,
/// The platform features used, by id ([`crate::FEATURES`]).
/// The platform features used, by id.
pub features: Vec<Feature>,
/// The document-model member chains used, as written (`app.activeDocument.layers[].name`, with
/// `[]` for an index and `()` for a call), a variable assigned from a chain replaced by that
@@ -225,8 +256,8 @@ impl Tally {
pub(crate) struct Acc<'l> {
pub limits: &'l Limits,
pub steps: usize,
features: BTreeMap<&'static str, Tally>,
outside: BTreeMap<&'static str, (Tally, Vec<&'static str>)>,
features: BTreeMap<FeatureId, Tally>,
outside: BTreeMap<OutsideId, (Tally, Vec<&'static str>)>,
menu: BTreeMap<String, Tally>,
menu_dynamic: usize,
members: BTreeMap<String, usize>,
@@ -279,12 +310,12 @@ impl<'l> Acc<'l> {
room
}
pub fn feature(&mut self, id: &'static str, line: Option<usize>) {
pub fn feature(&mut self, id: FeatureId, line: Option<usize>) {
let max = self.limits.lines;
self.features.entry(id).or_default().add(line, max);
}
pub fn outside(&mut self, id: &'static str, line: Option<usize>, mode: Option<&'static str>) {
pub fn outside(&mut self, id: OutsideId, line: Option<usize>, mode: Option<&'static str>) {
let max = self.limits.lines;
let (tally, modes) = self.outside.entry(id).or_default();
tally.add(line, max);
@@ -385,11 +416,10 @@ impl<'l> Acc<'l> {
let mut menu_strings: Vec<MenuString> =
self.menu.into_iter().map(|(value, t)| MenuString { value, count: t.count, lines: t.lines }).collect();
menu_strings.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.value.cmp(&b.value)));
let features: Vec<Feature> = self.features.into_iter().map(|(id, t)| Feature { id, count: t.count, lines: t.lines }).collect();
let mut outside: Vec<Outside> =
self.outside.into_iter().map(|(id, (t, modes))| Outside { id, count: t.count, lines: t.lines, modes }).collect();
outside.sort_by_key(|o| OUTSIDE.iter().position(|id| *id == o.id));
let mut needs: BTreeSet<Need> = features.iter().filter_map(|f| catalog::need(f.id)).collect();
let mut needs: BTreeSet<Need> = self.features.keys().map(|id| id.need()).collect();
let features: Vec<Feature> = self.features.into_iter().map(|(id, t)| Feature { id: id.id(), count: t.count, lines: t.lines }).collect();
let outside: Vec<Outside> =
self.outside.into_iter().map(|(id, (t, modes))| Outside { id: id.id(), count: t.count, lines: t.lines, modes }).collect();
if !members.is_empty() || !types.is_empty() {
needs.insert(Need::Dom);
}
+14 -7
View File
@@ -2,6 +2,7 @@
use proptest::prelude::*;
use crate::catalog::FeatureId;
use crate::lex::{Lexer, T};
use crate::*;
@@ -95,7 +96,7 @@ fn each_feature_is_found_where_it_is_and_nowhere_else() {
assert_eq!(r.kind, Kind::Script, "{with}");
let f = feature(&r, id).unwrap_or_else(|| panic!("{id} not found in {with:?}: {:?}", r.features));
assert_eq!(f.lines, [1], "{with}");
let need = catalog::need(id).unwrap_or_else(|| panic!("{id} sets no need"));
let need = FeatureId::ALL.iter().find(|f| f.id() == *id).map(|f| f.need()).unwrap_or_else(|| panic!("{id} sets no need"));
assert!(r.needs.contains(&need), "{with}: {:?}", r.needs);
let r = report(without);
assert!(feature(&r, id).is_none(), "{id} found in {without:?}");
@@ -103,14 +104,20 @@ fn each_feature_is_found_where_it_is_and_nowhere_else() {
}
#[test]
fn every_feature_id_has_a_need_and_a_case() {
assert!(FEATURES.windows(2).all(|w| w[0] < w[1]), "FEATURES is sorted, as the report lists them");
for id in FEATURES {
assert!(catalog::need(id).is_some(), "{id}");
assert!(FEATURE_CASES.iter().any(|(case, ..)| case == id), "{id} has no case");
fn features_are_listed_in_alphabetical_order_of_their_ids() {
let ids: Vec<&str> = FeatureId::ALL.iter().map(|f| f.id()).collect();
assert!(ids.windows(2).all(|w| w[0] < w[1]), "{ids:?}");
assert!(FeatureId::ALL.windows(2).all(|w| w[0] < w[1]), "the enum is declared in the order of its ids");
}
#[test]
fn every_feature_id_has_a_case() {
for f in FeatureId::ALL {
let id = f.id();
assert!(FEATURE_CASES.iter().any(|(case, ..)| *case == id), "{id} has no case");
}
for (id, ..) in FEATURE_CASES {
assert!(FEATURES.contains(id), "{id} isn't in FEATURES");
assert!(FeatureId::ALL.iter().any(|f| f.id() == *id), "{id} isn't a FeatureId");
}
let unique = |ids: &[&str]| ids.iter().collect::<std::collections::BTreeSet<_>>().len() == ids.len();
assert!(unique(PROBLEMS) && unique(OUTSIDE));