//! What a check finds ([`Report`]), and the tallies it is built from ([`Acc`]), each list capped. use std::collections::{BTreeMap, BTreeSet}; use crate::{Kind, Limits, Need, catalog}; pub(crate) const UNTERMINATED_STRING: &str = "unterminatedString"; pub(crate) const UNTERMINATED_COMMENT: &str = "unterminatedComment"; pub(crate) const UNTERMINATED_REGEXP: &str = "unterminatedRegExp"; pub(crate) const UNTERMINATED_TEMPLATE: &str = "unterminatedTemplate"; pub(crate) const UNTERMINATED_XML: &str = "unterminatedXml"; pub(crate) const UNCLOSED_BRACKET: &str = "unclosedBracket"; pub(crate) const UNEXPECTED_BRACKET: &str = "unexpectedBracket"; pub(crate) const NEWER_SYNTAX: &str = "newerSyntax"; pub(crate) const UNKNOWN_DIRECTIVE: &str = "unknownDirective"; pub(crate) const NOT_UTF8: &str = "notUtf8"; pub(crate) const MIXED_LINE_ENDINGS: &str = "mixedLineEndings"; pub(crate) const TOO_DEEP: &str = "tooDeep"; /// Every problem code a report can list. pub const PROBLEMS: &[&str] = &[ UNTERMINATED_STRING, UNTERMINATED_COMMENT, UNTERMINATED_REGEXP, UNTERMINATED_TEMPLATE, UNTERMINATED_XML, UNCLOSED_BRACKET, UNEXPECTED_BRACKET, NEWER_SYNTAX, UNKNOWN_DIRECTIVE, NOT_UTF8, MIXED_LINE_ENDINGS, 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"]; /// 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, /// menu strings, outside effects) count the script and its included files together and list the /// script's own lines. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Report { pub kind: Kind, /// What the script needs from the app that runs it, in [`Need`]'s order. pub needs: Vec, /// The script's lines. pub lines: usize, /// Each `#target` line's application specifier. pub targets: Vec, pub directives: Vec, pub includes: Vec, /// The platform features used, by id ([`crate::FEATURES`]). pub features: Vec, /// 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 /// chain, and how often each occurs; the most used first. pub members: Vec, /// The type names compared with `.typename` or created with `new`, apart from the language's, /// the platform's and the script's own functions. pub types: Vec, /// The menu-command strings passed as string literals; the most used first. pub menu_strings: Vec, /// Menu-command calls whose argument isn't a string literal. pub menu_dynamic: usize, /// What the script does outside the document, in [`OUTSIDE`]'s order. pub outside: Vec, pub problems: Vec, /// A list of the report, or a table of names the check keeps, reached [`Limits::entries`], and /// entries were left out. pub truncated: bool, /// The steps the check took: the tests check that they grow linearly with the input. pub(crate) steps: usize, } /// A `#target` line's application specifier, split into its parts. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Target { pub value: String, pub app: String, pub instance: Option, pub version: Option, pub locale: Option, pub line: usize, pub file: Option, } /// A preprocessor line of a documented directive. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Directive { /// The directive (`target`, `include`…). pub name: &'static str, pub value: String, pub line: usize, /// `#` or `//@`. pub form: &'static str, pub file: Option, } /// What became of an `#include` line. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum IncludeStatus { /// Read and checked (or checked already, for a file included twice). Checked, /// No such file in the script's folder, or in the folders `#includepath` names inside it. Missing, /// Not followed: an absolute name, a name longer than 4,096 bytes, a name that leads out of the /// script's folder, or a file the caller may not read. NotFollowed, /// The file it names includes the file the line is in, directly or through other files. Cycle, /// Includes weren't read (no loader). NotRead, /// Not read: a limit was reached (files, depth, size, paths looked up). Limit, /// A compiled file, or not text. Unreadable, } impl IncludeStatus { pub const ALL: [IncludeStatus; 7] = [ IncludeStatus::Checked, IncludeStatus::Missing, IncludeStatus::NotFollowed, IncludeStatus::Cycle, IncludeStatus::NotRead, IncludeStatus::Limit, IncludeStatus::Unreadable, ]; pub fn id(self) -> &'static str { match self { IncludeStatus::Checked => "checked", IncludeStatus::Missing => "missing", IncludeStatus::NotFollowed => "notFollowed", IncludeStatus::Cycle => "cycle", IncludeStatus::NotRead => "notRead", IncludeStatus::Limit => "limit", IncludeStatus::Unreadable => "unreadable", } } } /// An `#include` line and what became of it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Include { /// The name as the line writes it. pub value: String, pub line: usize, /// The included file the line is in. pub file: Option, pub status: IncludeStatus, /// The file it names, as a path inside the script's folder, when one was found. pub path: Option, } /// A platform feature: how often it occurs, and the script's first lines with it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Feature { pub id: &'static str, pub count: usize, pub lines: Vec, } /// A name and how often it occurs. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Named { pub name: String, pub count: usize, } /// A menu-command string: how often it is passed, and the script's first lines with it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MenuString { pub value: String, pub count: usize, pub lines: Vec, } /// Something the script does outside the document ([`OUTSIDE`]). #[derive(Clone, Debug, PartialEq, Eq)] pub struct Outside { pub id: &'static str, pub count: usize, pub lines: Vec, /// For `filesWritten`: the open modes used (`w`, `a`, `e`). pub modes: Vec<&'static str>, } /// A problem that would stop the script loading or running as written. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Problem { pub code: &'static str, pub line: Option, pub file: Option, /// The text concerned, as written (an unknown directive, a construct of later syntax, a /// bracket). pub detail: Option, } /// A count and the first lines it was seen on. #[derive(Clone, Debug, Default)] struct Tally { count: usize, lines: Vec, } impl Tally { fn add(&mut self, line: Option, max: usize) { self.count = self.count.saturating_add(1); if let Some(line) = line && self.lines.len() < max && self.lines.last() != Some(&line) { self.lines.push(line); } } } /// What a check has found so far, across the script and its included files. 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>)>, menu: BTreeMap, menu_dynamic: usize, members: BTreeMap, types: BTreeMap, /// Functions the script declares: `new` of one is no document-model type. declared: BTreeSet, targets: Vec, directives: Vec, includes: Vec, problems: Vec, truncated: bool, } impl<'l> Acc<'l> { pub fn new(limits: &'l Limits) -> Self { Self { limits, steps: 0, features: BTreeMap::new(), outside: BTreeMap::new(), menu: BTreeMap::new(), menu_dynamic: 0, members: BTreeMap::new(), types: BTreeMap::new(), declared: BTreeSet::new(), targets: vec![], directives: vec![], includes: vec![], problems: vec![], truncated: false, } } /// `s` as the report echoes it: at most [`Limits::echo`] characters, the last one `…` when /// it was cut. pub fn echo(&self, s: &str) -> String { echo(s, self.limits.echo) } /// Room for one more entry in a list of the report, or in a table of names the check keeps, of /// `len` entries: false (and the report marked truncated) when it is full. pub fn room(&mut self, len: usize) -> bool { let room = len < self.limits.entries; self.truncated |= !room; room } pub fn feature(&mut self, id: &'static str, line: Option) { let max = self.limits.lines; self.features.entry(id).or_default().add(line, max); } pub fn outside(&mut self, id: &'static str, line: Option, mode: Option<&'static str>) { let max = self.limits.lines; let (tally, modes) = self.outside.entry(id).or_default(); tally.add(line, max); if let Some(m) = mode && !modes.contains(&m) { modes.push(m); } } pub fn menu_string(&mut self, value: &str, line: Option) { let key = self.echo(value); if !self.menu.contains_key(&key) && !self.room(self.menu.len()) { return; } let max = self.limits.lines; self.menu.entry(key).or_default().add(line, max); } pub fn menu_dynamic(&mut self) { self.menu_dynamic = self.menu_dynamic.saturating_add(1); } pub fn member(&mut self, path: &str) { if !self.members.contains_key(path) && !self.room(self.members.len()) { return; } let n = self.members.entry(path.to_string()).or_default(); *n = n.saturating_add(1); } pub fn type_name(&mut self, name: &str) { let key = self.echo(name); if !self.types.contains_key(&key) && !self.room(self.types.len()) { return; } let n = self.types.entry(key).or_default(); *n = n.saturating_add(1); } pub fn declared(&mut self, name: &str) { if self.declared.len() < self.limits.entries { self.declared.insert(self.echo(name)); } } pub fn problem(&mut self, code: &'static str, line: Option, file: Option<&str>, detail: Option<&str>) { if self.room(self.problems.len()) { let detail = detail.map(|d| self.echo(d)); self.problems.push(Problem { code, line, file: file.map(str::to_string), detail }); } } pub fn directive(&mut self, name: &'static str, value: &str, line: usize, form: &'static str, file: Option<&str>) { if self.room(self.directives.len()) { let value = self.echo(value); self.directives.push(Directive { name, value, line, form, file: file.map(str::to_string) }); } } pub fn target(&mut self, value: &str, line: usize, file: Option<&str>) { if !self.room(self.targets.len()) { return; } let t = crate::directives::target(value); let part = |s: Option<&str>| s.map(|s| echo(s, self.limits.echo)); let target = Target { value: self.echo(value), app: self.echo(t.app), instance: part(t.instance), version: part(t.version), locale: part(t.locale), line, file: file.map(str::to_string), }; self.targets.push(target); } pub fn include(&mut self, value: &str, line: usize, file: Option<&str>, status: IncludeStatus, path: Option<&str>) { if self.room(self.includes.len()) { let include = Include { value: self.echo(value), line, file: file.map(str::to_string), status, path: path.map(|p| self.echo(p)) }; self.includes.push(include); } } /// The report for a file of `kind` with `lines` lines. pub fn finish(self, kind: Kind, lines: usize) -> Report { let declared = self.declared; let mut members: Vec = self.members.into_iter().map(|(name, count)| Named { name, count }).collect(); members.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name))); let mut types: Vec = self.types.into_iter().filter(|(name, _)| !declared.contains(name)).map(|(name, count)| Named { name, count }).collect(); types.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name))); let mut menu_strings: Vec = 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 = self.features.into_iter().map(|(id, t)| Feature { id, count: t.count, lines: t.lines }).collect(); let mut outside: Vec = 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 = features.iter().filter_map(|f| catalog::need(f.id)).collect(); if !members.is_empty() || !types.is_empty() { needs.insert(Need::Dom); } Report { kind, needs: needs.into_iter().collect(), lines, targets: self.targets, directives: self.directives, includes: self.includes, features, members, types, menu_strings, menu_dynamic: self.menu_dynamic, outside, problems: self.problems, truncated: self.truncated, steps: self.steps, } } } /// `s` cut to at most `max` characters, the last one `…` when it was cut. pub(crate) fn echo(s: &str, max: usize) -> String { match s.char_indices().nth(max) { None => s.to_string(), Some(_) => { let keep = s.char_indices().nth(max.saturating_sub(1)).map_or(0, |(i, _)| i); let mut out = s.get(..keep).unwrap_or_default().to_string(); out.push('…'); out } } }