summaryrefslogtreecommitdiffstats
path: root/src/tools/jsondocck
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /src/tools/jsondocck
parentInitial commit. (diff)
downloadrustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz
rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/jsondocck')
-rw-r--r--src/tools/jsondocck/Cargo.toml13
-rw-r--r--src/tools/jsondocck/src/cache.rs77
-rw-r--r--src/tools/jsondocck/src/config.rs37
-rw-r--r--src/tools/jsondocck/src/error.rs28
-rw-r--r--src/tools/jsondocck/src/main.rs339
5 files changed, 494 insertions, 0 deletions
diff --git a/src/tools/jsondocck/Cargo.toml b/src/tools/jsondocck/Cargo.toml
new file mode 100644
index 000000000..ccabe6483
--- /dev/null
+++ b/src/tools/jsondocck/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "jsondocck"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+jsonpath_lib = "0.2"
+getopts = "0.2"
+regex = "1.4"
+shlex = "1.0"
+serde_json = "1.0"
+fs-err = "2.5.0"
+once_cell = "1.0"
diff --git a/src/tools/jsondocck/src/cache.rs b/src/tools/jsondocck/src/cache.rs
new file mode 100644
index 000000000..a188750c5
--- /dev/null
+++ b/src/tools/jsondocck/src/cache.rs
@@ -0,0 +1,77 @@
+use crate::error::CkError;
+use serde_json::Value;
+use std::collections::HashMap;
+use std::io;
+use std::path::{Path, PathBuf};
+
+use fs_err as fs;
+
+#[derive(Debug)]
+pub struct Cache {
+ root: PathBuf,
+ files: HashMap<PathBuf, String>,
+ values: HashMap<PathBuf, Value>,
+ pub variables: HashMap<String, Value>,
+ last_path: Option<PathBuf>,
+}
+
+impl Cache {
+ /// Create a new cache, used to read files only once and otherwise store their contents.
+ pub fn new(doc_dir: &str) -> Cache {
+ Cache {
+ root: Path::new(doc_dir).to_owned(),
+ files: HashMap::new(),
+ values: HashMap::new(),
+ variables: HashMap::new(),
+ last_path: None,
+ }
+ }
+
+ fn resolve_path(&mut self, path: &String) -> PathBuf {
+ if path != "-" {
+ let resolve = self.root.join(path);
+ self.last_path = Some(resolve.clone());
+ resolve
+ } else {
+ self.last_path
+ .as_ref()
+ // FIXME: Point to a line number
+ .expect("No last path set. Make sure to specify a full path before using `-`")
+ .clone()
+ }
+ }
+
+ fn read_file(&mut self, path: PathBuf) -> Result<String, io::Error> {
+ if let Some(f) = self.files.get(&path) {
+ return Ok(f.clone());
+ }
+
+ let file = fs::read_to_string(&path)?;
+
+ self.files.insert(path, file.clone());
+
+ Ok(file)
+ }
+
+ /// Get the text from a file. If called multiple times, the file will only be read once
+ pub fn get_file(&mut self, path: &String) -> Result<String, io::Error> {
+ let path = self.resolve_path(path);
+ self.read_file(path)
+ }
+
+ /// Parse the JSON from a file. If called multiple times, the file will only be read once.
+ pub fn get_value(&mut self, path: &String) -> Result<Value, CkError> {
+ let path = self.resolve_path(path);
+
+ if let Some(v) = self.values.get(&path) {
+ return Ok(v.clone());
+ }
+
+ let content = self.read_file(path.clone())?;
+ let val = serde_json::from_str::<Value>(&content)?;
+
+ self.values.insert(path, val.clone());
+
+ Ok(val)
+ }
+}
diff --git a/src/tools/jsondocck/src/config.rs b/src/tools/jsondocck/src/config.rs
new file mode 100644
index 000000000..9b3ba3f3f
--- /dev/null
+++ b/src/tools/jsondocck/src/config.rs
@@ -0,0 +1,37 @@
+use getopts::Options;
+
+#[derive(Debug)]
+pub struct Config {
+ /// The directory documentation output was generated in
+ pub doc_dir: String,
+ /// The file documentation was generated for, with docck commands to check
+ pub template: String,
+}
+
+/// Create a Config from a vector of command-line arguments
+pub fn parse_config(args: Vec<String>) -> Config {
+ let mut opts = Options::new();
+ opts.reqopt("", "doc-dir", "Path to the documentation directory", "PATH")
+ .reqopt("", "template", "Path to the template file", "PATH")
+ .optflag("h", "help", "show this message");
+
+ let (argv0, args_) = args.split_first().unwrap();
+ if args.len() == 1 {
+ let message = format!("Usage: {} <doc-dir> <template>", argv0);
+ println!("{}", opts.usage(&message));
+ std::process::exit(1);
+ }
+
+ let matches = opts.parse(args_).unwrap();
+
+ if matches.opt_present("h") || matches.opt_present("help") {
+ let message = format!("Usage: {} <doc-dir> <template>", argv0);
+ println!("{}", opts.usage(&message));
+ std::process::exit(1);
+ }
+
+ Config {
+ doc_dir: matches.opt_str("doc-dir").unwrap(),
+ template: matches.opt_str("template").unwrap(),
+ }
+}
diff --git a/src/tools/jsondocck/src/error.rs b/src/tools/jsondocck/src/error.rs
new file mode 100644
index 000000000..53b9af287
--- /dev/null
+++ b/src/tools/jsondocck/src/error.rs
@@ -0,0 +1,28 @@
+use crate::Command;
+use std::error::Error;
+use std::fmt;
+
+#[derive(Debug)]
+pub enum CkError {
+ /// A check failed. File didn't exist or failed to match the command
+ FailedCheck(String, Command),
+ /// An error triggered by some other error
+ Induced(Box<dyn Error>),
+}
+
+impl fmt::Display for CkError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ CkError::FailedCheck(msg, cmd) => {
+ write!(f, "Failed check: {} on line {}", msg, cmd.lineno)
+ }
+ CkError::Induced(err) => write!(f, "Check failed: {}", err),
+ }
+ }
+}
+
+impl<T: Error + 'static> From<T> for CkError {
+ fn from(err: T) -> CkError {
+ CkError::Induced(Box::new(err))
+ }
+}
diff --git a/src/tools/jsondocck/src/main.rs b/src/tools/jsondocck/src/main.rs
new file mode 100644
index 000000000..c44624666
--- /dev/null
+++ b/src/tools/jsondocck/src/main.rs
@@ -0,0 +1,339 @@
+use jsonpath_lib::select;
+use once_cell::sync::Lazy;
+use regex::{Regex, RegexBuilder};
+use serde_json::Value;
+use std::borrow::Cow;
+use std::{env, fmt, fs};
+
+mod cache;
+mod config;
+mod error;
+
+use cache::Cache;
+use config::parse_config;
+use error::CkError;
+
+fn main() -> Result<(), String> {
+ let config = parse_config(env::args().collect());
+
+ let mut failed = Vec::new();
+ let mut cache = Cache::new(&config.doc_dir);
+ let commands = get_commands(&config.template)
+ .map_err(|_| format!("Jsondocck failed for {}", &config.template))?;
+
+ for command in commands {
+ if let Err(e) = check_command(command, &mut cache) {
+ failed.push(e);
+ }
+ }
+
+ if failed.is_empty() {
+ Ok(())
+ } else {
+ for i in failed {
+ eprintln!("{}", i);
+ }
+ Err(format!("Jsondocck failed for {}", &config.template))
+ }
+}
+
+#[derive(Debug)]
+pub struct Command {
+ negated: bool,
+ kind: CommandKind,
+ args: Vec<String>,
+ lineno: usize,
+}
+
+#[derive(Debug)]
+pub enum CommandKind {
+ Has,
+ Count,
+ Is,
+ Set,
+}
+
+impl CommandKind {
+ fn validate(&self, args: &[String], command_num: usize, lineno: usize) -> bool {
+ let count = match self {
+ CommandKind::Has => (1..=3).contains(&args.len()),
+ CommandKind::Count | CommandKind::Is => 3 == args.len(),
+ CommandKind::Set => 4 == args.len(),
+ };
+
+ if !count {
+ print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
+ return false;
+ }
+
+ if args[0] == "-" && command_num == 0 {
+ print_err(&format!("Tried to use the previous path in the first command"), lineno);
+ return false;
+ }
+
+ if let CommandKind::Count = self {
+ if args[2].parse::<usize>().is_err() {
+ print_err(
+ &format!("Third argument to @count must be a valid usize (got `{}`)", args[2]),
+ lineno,
+ );
+ return false;
+ }
+ }
+
+ true
+ }
+}
+
+impl fmt::Display for CommandKind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let text = match self {
+ CommandKind::Has => "has",
+ CommandKind::Count => "count",
+ CommandKind::Is => "is",
+ CommandKind::Set => "set",
+ };
+ write!(f, "{}", text)
+ }
+}
+
+static LINE_PATTERN: Lazy<Regex> = Lazy::new(|| {
+ RegexBuilder::new(
+ r#"
+ \s(?P<invalid>!?)@(?P<negated>!?)
+ (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
+ (?P<args>.*)$
+ "#,
+ )
+ .ignore_whitespace(true)
+ .unicode(true)
+ .build()
+ .unwrap()
+});
+
+fn print_err(msg: &str, lineno: usize) {
+ eprintln!("Invalid command: {} on line {}", msg, lineno)
+}
+
+/// Get a list of commands from a file. Does the work of ensuring the commands
+/// are syntactically valid.
+fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
+ let mut commands = Vec::new();
+ let mut errors = false;
+ let file = fs::read_to_string(template).unwrap();
+
+ for (lineno, line) in file.split('\n').enumerate() {
+ let lineno = lineno + 1;
+
+ let cap = match LINE_PATTERN.captures(line) {
+ Some(c) => c,
+ None => continue,
+ };
+
+ let negated = cap.name("negated").unwrap().as_str() == "!";
+ let cmd = cap.name("cmd").unwrap().as_str();
+
+ let cmd = match cmd {
+ "has" => CommandKind::Has,
+ "count" => CommandKind::Count,
+ "is" => CommandKind::Is,
+ "set" => CommandKind::Set,
+ _ => {
+ print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
+ errors = true;
+ continue;
+ }
+ };
+
+ if let Some(m) = cap.name("invalid") {
+ if m.as_str() == "!" {
+ print_err(
+ &format!(
+ "`!@{0}{1}`, (help: try with `@!{1}`)",
+ if negated { "!" } else { "" },
+ cmd,
+ ),
+ lineno,
+ );
+ errors = true;
+ continue;
+ }
+ }
+
+ let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
+
+ let args = match args {
+ Some(args) => args,
+ None => {
+ print_err(
+ &format!(
+ "Invalid arguments to shlex::split: `{}`",
+ cap.name("args").unwrap().as_str()
+ ),
+ lineno,
+ );
+ errors = true;
+ continue;
+ }
+ };
+
+ if !cmd.validate(&args, commands.len(), lineno) {
+ errors = true;
+ continue;
+ }
+
+ commands.push(Command { negated, kind: cmd, args, lineno })
+ }
+
+ if !errors { Ok(commands) } else { Err(()) }
+}
+
+/// Performs the actual work of ensuring a command passes. Generally assumes the command
+/// is syntactically valid.
+fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
+ // FIXME: Be more granular about why, (e.g. syntax error, count not equal)
+ let result = match command.kind {
+ CommandKind::Has => {
+ match command.args.len() {
+ // @has <path> = file existence
+ 1 => cache.get_file(&command.args[0]).is_ok(),
+ // @has <path> <jsonpath> = check path exists
+ 2 => {
+ let val = cache.get_value(&command.args[0])?;
+ let results = select(&val, &command.args[1]).unwrap();
+ !results.is_empty()
+ }
+ // @has <path> <jsonpath> <value> = check *any* item matched by path equals value
+ 3 => {
+ let val = cache.get_value(&command.args[0])?;
+ let results = select(&val, &command.args[1]).unwrap();
+ let pat = string_to_value(&command.args[2], cache);
+ let has = results.contains(&pat.as_ref());
+ // Give better error for when @has check fails
+ if !command.negated && !has {
+ return Err(CkError::FailedCheck(
+ format!(
+ "{} matched to {:?} but didn't have {:?}",
+ &command.args[1],
+ results,
+ pat.as_ref()
+ ),
+ command,
+ ));
+ } else {
+ has
+ }
+ }
+ _ => unreachable!(),
+ }
+ }
+ CommandKind::Count => {
+ // @count <path> <jsonpath> <count> = Check that the jsonpath matches exactly [count] times
+ assert_eq!(command.args.len(), 3);
+ let expected: usize = command.args[2].parse().unwrap();
+
+ let val = cache.get_value(&command.args[0])?;
+ let results = select(&val, &command.args[1]).unwrap();
+ let eq = results.len() == expected;
+ if !command.negated && !eq {
+ return Err(CkError::FailedCheck(
+ format!(
+ "`{}` matched to `{:?}` with length {}, but expected length {}",
+ &command.args[1],
+ results,
+ results.len(),
+ expected
+ ),
+ command,
+ ));
+ } else {
+ eq
+ }
+ }
+ CommandKind::Is => {
+ // @has <path> <jsonpath> <value> = check *exactly one* item matched by path, and it equals value
+ assert_eq!(command.args.len(), 3);
+ let val = cache.get_value(&command.args[0])?;
+ let results = select(&val, &command.args[1]).unwrap();
+ let pat = string_to_value(&command.args[2], cache);
+ let is = results.len() == 1 && results[0] == pat.as_ref();
+ if !command.negated && !is {
+ return Err(CkError::FailedCheck(
+ format!(
+ "{} matched to {:?}, but expected {:?}",
+ &command.args[1],
+ results,
+ pat.as_ref()
+ ),
+ command,
+ ));
+ } else {
+ is
+ }
+ }
+ CommandKind::Set => {
+ // @set <name> = <path> <jsonpath>
+ assert_eq!(command.args.len(), 4);
+ assert_eq!(command.args[1], "=", "Expected an `=`");
+ let val = cache.get_value(&command.args[2])?;
+ let results = select(&val, &command.args[3]).unwrap();
+ assert_eq!(
+ results.len(),
+ 1,
+ "Expected 1 match for `{}` (because of @set): matched to {:?}",
+ command.args[3],
+ results
+ );
+ match results.len() {
+ 0 => false,
+ 1 => {
+ let r = cache.variables.insert(command.args[0].clone(), results[0].clone());
+ assert!(r.is_none(), "Name collision: {} is duplicated", command.args[0]);
+ true
+ }
+ _ => {
+ panic!(
+ "Got multiple results in `@set` for `{}`: {:?}",
+ &command.args[3], results
+ );
+ }
+ }
+ }
+ };
+
+ if result == command.negated {
+ if command.negated {
+ Err(CkError::FailedCheck(
+ format!(
+ "`@!{} {}` matched when it shouldn't",
+ command.kind,
+ command.args.join(" ")
+ ),
+ command,
+ ))
+ } else {
+ // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
+ Err(CkError::FailedCheck(
+ format!(
+ "`@{} {}` didn't match when it should",
+ command.kind,
+ command.args.join(" ")
+ ),
+ command,
+ ))
+ }
+ } else {
+ Ok(())
+ }
+}
+
+fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
+ if s.starts_with("$") {
+ Cow::Borrowed(&cache.variables.get(&s[1..]).unwrap_or_else(|| {
+ // FIXME(adotinthevoid): Show line number
+ panic!("No variable: `{}`. Current state: `{:?}`", &s[1..], cache.variables)
+ }))
+ } else {
+ Cow::Owned(serde_json::from_str(s).expect(&format!("Cannot convert `{}` to json", s)))
+ }
+}