418 lines
15 KiB
Rust
418 lines
15 KiB
Rust
//! 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<Need>,
|
|
/// The script's lines.
|
|
pub lines: usize,
|
|
/// Each `#target` line's application specifier.
|
|
pub targets: Vec<Target>,
|
|
pub directives: Vec<Directive>,
|
|
pub includes: Vec<Include>,
|
|
/// The platform features used, by id ([`crate::FEATURES`]).
|
|
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
|
|
/// chain, and how often each occurs; the most used first.
|
|
pub members: Vec<Named>,
|
|
/// 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<Named>,
|
|
/// The menu-command strings passed as string literals; the most used first.
|
|
pub menu_strings: Vec<MenuString>,
|
|
/// 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<Outside>,
|
|
pub problems: Vec<Problem>,
|
|
/// 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<String>,
|
|
pub version: Option<String>,
|
|
pub locale: Option<String>,
|
|
pub line: usize,
|
|
pub file: Option<String>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
pub status: IncludeStatus,
|
|
/// The file it names, as a path inside the script's folder, when one was found.
|
|
pub path: Option<String>,
|
|
}
|
|
|
|
/// 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<usize>,
|
|
}
|
|
|
|
/// 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<usize>,
|
|
}
|
|
|
|
/// 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<usize>,
|
|
/// 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<usize>,
|
|
pub file: Option<String>,
|
|
/// The text concerned, as written (an unknown directive, a construct of later syntax, a
|
|
/// bracket).
|
|
pub detail: Option<String>,
|
|
}
|
|
|
|
/// A count and the first lines it was seen on.
|
|
#[derive(Clone, Debug, Default)]
|
|
struct Tally {
|
|
count: usize,
|
|
lines: Vec<usize>,
|
|
}
|
|
|
|
impl Tally {
|
|
fn add(&mut self, line: Option<usize>, 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<String, Tally>,
|
|
menu_dynamic: usize,
|
|
members: BTreeMap<String, usize>,
|
|
types: BTreeMap<String, usize>,
|
|
/// Functions the script declares: `new` of one is no document-model type.
|
|
declared: BTreeSet<String>,
|
|
targets: Vec<Target>,
|
|
directives: Vec<Directive>,
|
|
includes: Vec<Include>,
|
|
problems: Vec<Problem>,
|
|
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<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>) {
|
|
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<usize>) {
|
|
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<usize>, 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<Named> = 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<Named> =
|
|
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<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();
|
|
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
|
|
}
|
|
}
|
|
}
|