]> git.lizzy.rs Git - rust.git/blob - src/tools/jsondocck/src/main.rs
Rollup merge of #97249 - GuillaumeGomez:details-summary-fixes, r=notriddle
[rust.git] / src / tools / jsondocck / src / main.rs
1 use jsonpath_lib::select;
2 use once_cell::sync::Lazy;
3 use regex::{Regex, RegexBuilder};
4 use serde_json::Value;
5 use std::borrow::Cow;
6 use std::{env, fmt, fs};
7
8 mod cache;
9 mod config;
10 mod error;
11
12 use cache::Cache;
13 use config::parse_config;
14 use error::CkError;
15
16 fn main() -> Result<(), String> {
17     let config = parse_config(env::args().collect());
18
19     let mut failed = Vec::new();
20     let mut cache = Cache::new(&config.doc_dir);
21     let commands = get_commands(&config.template)
22         .map_err(|_| format!("Jsondocck failed for {}", &config.template))?;
23
24     for command in commands {
25         if let Err(e) = check_command(command, &mut cache) {
26             failed.push(e);
27         }
28     }
29
30     if failed.is_empty() {
31         Ok(())
32     } else {
33         for i in failed {
34             eprintln!("{}", i);
35         }
36         Err(format!("Jsondocck failed for {}", &config.template))
37     }
38 }
39
40 #[derive(Debug)]
41 pub struct Command {
42     negated: bool,
43     kind: CommandKind,
44     args: Vec<String>,
45     lineno: usize,
46 }
47
48 #[derive(Debug)]
49 pub enum CommandKind {
50     Has,
51     Count,
52     Is,
53     Set,
54 }
55
56 impl CommandKind {
57     fn validate(&self, args: &[String], command_num: usize, lineno: usize) -> bool {
58         let count = match self {
59             CommandKind::Has => (1..=3).contains(&args.len()),
60             CommandKind::Count | CommandKind::Is => 3 == args.len(),
61             CommandKind::Set => 4 == args.len(),
62         };
63
64         if !count {
65             print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
66             return false;
67         }
68
69         if args[0] == "-" && command_num == 0 {
70             print_err(&format!("Tried to use the previous path in the first command"), lineno);
71             return false;
72         }
73
74         if let CommandKind::Count = self {
75             if args[2].parse::<usize>().is_err() {
76                 print_err(
77                     &format!("Third argument to @count must be a valid usize (got `{}`)", args[2]),
78                     lineno,
79                 );
80                 return false;
81             }
82         }
83
84         true
85     }
86 }
87
88 impl fmt::Display for CommandKind {
89     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90         let text = match self {
91             CommandKind::Has => "has",
92             CommandKind::Count => "count",
93             CommandKind::Is => "is",
94             CommandKind::Set => "set",
95         };
96         write!(f, "{}", text)
97     }
98 }
99
100 static LINE_PATTERN: Lazy<Regex> = Lazy::new(|| {
101     RegexBuilder::new(
102         r#"
103         \s(?P<invalid>!?)@(?P<negated>!?)
104         (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
105         (?P<args>.*)$
106     "#,
107     )
108     .ignore_whitespace(true)
109     .unicode(true)
110     .build()
111     .unwrap()
112 });
113
114 fn print_err(msg: &str, lineno: usize) {
115     eprintln!("Invalid command: {} on line {}", msg, lineno)
116 }
117
118 /// Get a list of commands from a file. Does the work of ensuring the commands
119 /// are syntactically valid.
120 fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
121     let mut commands = Vec::new();
122     let mut errors = false;
123     let file = fs::read_to_string(template).unwrap();
124
125     for (lineno, line) in file.split('\n').enumerate() {
126         let lineno = lineno + 1;
127
128         let cap = match LINE_PATTERN.captures(line) {
129             Some(c) => c,
130             None => continue,
131         };
132
133         let negated = cap.name("negated").unwrap().as_str() == "!";
134         let cmd = cap.name("cmd").unwrap().as_str();
135
136         let cmd = match cmd {
137             "has" => CommandKind::Has,
138             "count" => CommandKind::Count,
139             "is" => CommandKind::Is,
140             "set" => CommandKind::Set,
141             _ => {
142                 print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
143                 errors = true;
144                 continue;
145             }
146         };
147
148         if let Some(m) = cap.name("invalid") {
149             if m.as_str() == "!" {
150                 print_err(
151                     &format!(
152                         "`!@{0}{1}`, (help: try with `@!{1}`)",
153                         if negated { "!" } else { "" },
154                         cmd,
155                     ),
156                     lineno,
157                 );
158                 errors = true;
159                 continue;
160             }
161         }
162
163         let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
164
165         let args = match args {
166             Some(args) => args,
167             None => {
168                 print_err(
169                     &format!(
170                         "Invalid arguments to shlex::split: `{}`",
171                         cap.name("args").unwrap().as_str()
172                     ),
173                     lineno,
174                 );
175                 errors = true;
176                 continue;
177             }
178         };
179
180         if !cmd.validate(&args, commands.len(), lineno) {
181             errors = true;
182             continue;
183         }
184
185         commands.push(Command { negated, kind: cmd, args, lineno })
186     }
187
188     if !errors { Ok(commands) } else { Err(()) }
189 }
190
191 /// Performs the actual work of ensuring a command passes. Generally assumes the command
192 /// is syntactically valid.
193 fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
194     // FIXME: Be more granular about why, (e.g. syntax error, count not equal)
195     let result = match command.kind {
196         CommandKind::Has => {
197             match command.args.len() {
198                 // @has <path> = file existence
199                 1 => cache.get_file(&command.args[0]).is_ok(),
200                 // @has <path> <jsonpath> = check path exists
201                 2 => {
202                     let val = cache.get_value(&command.args[0])?;
203                     let results = select(&val, &command.args[1]).unwrap();
204                     !results.is_empty()
205                 }
206                 // @has <path> <jsonpath> <value> = check *any* item matched by path equals value
207                 3 => {
208                     let val = cache.get_value(&command.args[0])?;
209                     let results = select(&val, &command.args[1]).unwrap();
210                     let pat = string_to_value(&command.args[2], cache);
211                     let has = results.contains(&pat.as_ref());
212                     // Give better error for when @has check fails
213                     if !command.negated && !has {
214                         return Err(CkError::FailedCheck(
215                             format!(
216                                 "{} matched to {:?} but didn't have {:?}",
217                                 &command.args[1],
218                                 results,
219                                 pat.as_ref()
220                             ),
221                             command,
222                         ));
223                     } else {
224                         has
225                     }
226                 }
227                 _ => unreachable!(),
228             }
229         }
230         CommandKind::Count => {
231             // @count <path> <jsonpath> <count> = Check that the jsonpath matches exactly [count] times
232             assert_eq!(command.args.len(), 3);
233             let expected: usize = command.args[2].parse().unwrap();
234
235             let val = cache.get_value(&command.args[0])?;
236             let results = select(&val, &command.args[1]).unwrap();
237             let eq = results.len() == expected;
238             if !command.negated && !eq {
239                 return Err(CkError::FailedCheck(
240                     format!(
241                         "`{}` matched to `{:?}` with length {}, but expected length {}",
242                         &command.args[1],
243                         results,
244                         results.len(),
245                         expected
246                     ),
247                     command,
248                 ));
249             } else {
250                 eq
251             }
252         }
253         CommandKind::Is => {
254             // @has <path> <jsonpath> <value> = check *exactly one* item matched by path, and it equals value
255             assert_eq!(command.args.len(), 3);
256             let val = cache.get_value(&command.args[0])?;
257             let results = select(&val, &command.args[1]).unwrap();
258             let pat = string_to_value(&command.args[2], cache);
259             let is = results.len() == 1 && results[0] == pat.as_ref();
260             if !command.negated && !is {
261                 return Err(CkError::FailedCheck(
262                     format!(
263                         "{} matched to {:?}, but expected {:?}",
264                         &command.args[1],
265                         results,
266                         pat.as_ref()
267                     ),
268                     command,
269                 ));
270             } else {
271                 is
272             }
273         }
274         CommandKind::Set => {
275             // @set <name> = <path> <jsonpath>
276             assert_eq!(command.args.len(), 4);
277             assert_eq!(command.args[1], "=", "Expected an `=`");
278             let val = cache.get_value(&command.args[2])?;
279             let results = select(&val, &command.args[3]).unwrap();
280             assert_eq!(
281                 results.len(),
282                 1,
283                 "Expected 1 match for `{}` (because of @set): matched to {:?}",
284                 command.args[3],
285                 results
286             );
287             match results.len() {
288                 0 => false,
289                 1 => {
290                     let r = cache.variables.insert(command.args[0].clone(), results[0].clone());
291                     assert!(r.is_none(), "Name collision: {} is duplicated", command.args[0]);
292                     true
293                 }
294                 _ => {
295                     panic!(
296                         "Got multiple results in `@set` for `{}`: {:?}",
297                         &command.args[3], results
298                     );
299                 }
300             }
301         }
302     };
303
304     if result == command.negated {
305         if command.negated {
306             Err(CkError::FailedCheck(
307                 format!(
308                     "`@!{} {}` matched when it shouldn't",
309                     command.kind,
310                     command.args.join(" ")
311                 ),
312                 command,
313             ))
314         } else {
315             // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
316             Err(CkError::FailedCheck(
317                 format!(
318                     "`@{} {}` didn't match when it should",
319                     command.kind,
320                     command.args.join(" ")
321                 ),
322                 command,
323             ))
324         }
325     } else {
326         Ok(())
327     }
328 }
329
330 fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
331     if s.starts_with("$") {
332         Cow::Borrowed(&cache.variables.get(&s[1..]).unwrap_or_else(|| {
333             // FIXME(adotinthevoid): Show line number
334             panic!("No variable: `{}`. Current state: `{:?}`", &s[1..], cache.variables)
335         }))
336     } else {
337         Cow::Owned(serde_json::from_str(s).expect(&format!("Cannot convert `{}` to json", s)))
338     }
339 }