]> git.proxmox.com Git - rustc.git/blame - src/tools/jsondocck/src/main.rs
New upstream version 1.61.0+dfsg1
[rustc.git] / src / tools / jsondocck / src / main.rs
CommitLineData
5869c6ff 1use jsonpath_lib::select;
17df50a5 2use once_cell::sync::Lazy;
5869c6ff
XL
3use regex::{Regex, RegexBuilder};
4use serde_json::Value;
6a06907d 5use std::borrow::Cow;
5869c6ff
XL
6use std::{env, fmt, fs};
7
8mod cache;
9mod config;
10mod error;
11
12use cache::Cache;
13use config::parse_config;
14use error::CkError;
15
16fn 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)]
41pub struct Command {
42 negated: bool,
43 kind: CommandKind,
44 args: Vec<String>,
45 lineno: usize,
46}
47
48#[derive(Debug)]
49pub enum CommandKind {
50 Has,
51 Count,
6a06907d
XL
52 Is,
53 Set,
5869c6ff
XL
54}
55
56impl 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()),
6a06907d
XL
60 CommandKind::Count | CommandKind::Is => 3 == args.len(),
61 CommandKind::Set => 4 == args.len(),
5869c6ff
XL
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() {
5e7ed085
FG
76 print_err(
77 &format!("Third argument to @count must be a valid usize (got `{}`)", args[2]),
78 lineno,
79 );
5869c6ff
XL
80 return false;
81 }
82 }
83
84 true
85 }
86}
87
88impl 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",
6a06907d
XL
93 CommandKind::Is => "is",
94 CommandKind::Set => "set",
5869c6ff
XL
95 };
96 write!(f, "{}", text)
97 }
98}
99
17df50a5
XL
100static LINE_PATTERN: Lazy<Regex> = Lazy::new(|| {
101 RegexBuilder::new(
5869c6ff
XL
102 r#"
103 \s(?P<invalid>!?)@(?P<negated>!?)
104 (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
105 (?P<args>.*)$
17df50a5 106 "#,
5869c6ff
XL
107 )
108 .ignore_whitespace(true)
109 .unicode(true)
110 .build()
17df50a5
XL
111 .unwrap()
112});
5869c6ff
XL
113
114fn 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.
120fn 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,
6a06907d
XL
139 "is" => CommandKind::Is,
140 "set" => CommandKind::Set,
5869c6ff
XL
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.
193fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
6a06907d 194 // FIXME: Be more granular about why, (e.g. syntax error, count not equal)
5869c6ff
XL
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])?;
6a06907d
XL
203 let results = select(&val, &command.args[1]).unwrap();
204 !results.is_empty()
5869c6ff
XL
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])?;
6a06907d
XL
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
5869c6ff
XL
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])?;
6a06907d 236 let results = select(&val, &command.args[1]).unwrap();
5099ac24
FG
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 }
6a06907d
XL
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);
cdc7bbd5
XL
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 }
6a06907d
XL
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,
5e7ed085 283 "Expected 1 match for `{}` (because of @set): matched to {:?}",
6a06907d
XL
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 }
5869c6ff
XL
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}
6a06907d
XL
329
330fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
331 if s.starts_with("$") {
cdc7bbd5
XL
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 }))
6a06907d 336 } else {
5099ac24 337 Cow::Owned(serde_json::from_str(s).expect(&format!("Cannot convert `{}` to json", s)))
6a06907d
XL
338 }
339}