mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #4025 from vangel76/refactor/scriptcheck-feature-enums
refactor(scriptcheck): feature enums, split analyze.rs
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user