Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0ce4667

Browse files
authoredDec 10, 2024··
Unrolled build for rust-lang#133478
Rollup merge of rust-lang#133478 - aDotInTheVoid:finally, r=fmease jsondocck: Parse, don't validate commands. Centralizes knowledge of jsondocck syntax into the parser, so the checker doesn't need to know what the indexes are. [Vaguely related zulip discussion](https://rust-lang.zulipchat.com/#narrow/channel/266220-t-rustdoc/topic/jsondocck.20rewrite) I'm very happy this is negative LoC, despite adding a big, documented enum! r? ``@fmease``
2 parents 4996052 + e6bc427 commit 0ce4667

File tree

3 files changed

+169
-262
lines changed

3 files changed

+169
-262
lines changed
 

‎src/tools/jsondocck/src/cache.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ impl Cache {
2828
}
2929
}
3030

31-
pub fn value(&self) -> &Value {
32-
&self.value
31+
// FIXME: Make this failible, so jsonpath syntax error has line number.
32+
pub fn select(&self, path: &str) -> Vec<&Value> {
33+
jsonpath_lib::select(&self.value, path).unwrap()
3334
}
3435
}

‎src/tools/jsondocck/src/error.rs‎

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,7 @@
1-
use std::error::Error;
2-
use std::fmt;
3-
41
use crate::Command;
52

63
#[derive(Debug)]
7-
pub enum CkError {
8-
/// A check failed. File didn't exist or failed to match the command
9-
FailedCheck(String, Command),
10-
/// An error triggered by some other error
11-
Induced(Box<dyn Error>),
12-
}
13-
14-
impl fmt::Display for CkError {
15-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16-
match self {
17-
CkError::FailedCheck(msg, cmd) => {
18-
write!(f, "Failed check: {} on line {}", msg, cmd.lineno)
19-
}
20-
CkError::Induced(err) => write!(f, "Check failed: {}", err),
21-
}
22-
}
23-
}
24-
25-
impl<T: Error + 'static> From<T> for CkError {
26-
fn from(err: T) -> CkError {
27-
CkError::Induced(Box::new(err))
28-
}
4+
pub struct CkError {
5+
pub message: String,
6+
pub command: Command,
297
}

‎src/tools/jsondocck/src/main.rs‎

Lines changed: 163 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::borrow::Cow;
2+
use std::process::ExitCode;
23
use std::sync::OnceLock;
3-
use std::{env, fmt, fs};
4+
use std::{env, fs};
45

5-
use jsonpath_lib::select;
66
use regex::{Regex, RegexBuilder};
77
use serde_json::Value;
88

@@ -14,90 +14,134 @@ use cache::Cache;
1414
use config::parse_config;
1515
use error::CkError;
1616

17-
fn main() -> Result<(), String> {
17+
fn main() -> ExitCode {
1818
let config = parse_config(env::args().collect());
1919

2020
let mut failed = Vec::new();
2121
let mut cache = Cache::new(&config);
22-
let commands = get_commands(&config.template)
23-
.map_err(|_| format!("Jsondocck failed for {}", &config.template))?;
22+
let Ok(commands) = get_commands(&config.template) else {
23+
eprintln!("Jsondocck failed for {}", &config.template);
24+
return ExitCode::FAILURE;
25+
};
2426

2527
for command in commands {
26-
if let Err(e) = check_command(command, &mut cache) {
27-
failed.push(e);
28+
if let Err(message) = check_command(&command, &mut cache) {
29+
failed.push(CkError { command, message });
2830
}
2931
}
3032

3133
if failed.is_empty() {
32-
Ok(())
34+
ExitCode::SUCCESS
3335
} else {
3436
for i in failed {
35-
eprintln!("{}", i);
37+
eprintln!("{}:{}, command failed", config.template, i.command.lineno);
38+
eprintln!("{}", i.message)
3639
}
37-
Err(format!("Jsondocck failed for {}", &config.template))
40+
ExitCode::FAILURE
3841
}
3942
}
4043

4144
#[derive(Debug)]
4245
pub struct Command {
43-
negated: bool,
4446
kind: CommandKind,
45-
args: Vec<String>,
47+
path: String,
4648
lineno: usize,
4749
}
4850

4951
#[derive(Debug)]
50-
pub enum CommandKind {
51-
Has,
52-
Count,
53-
Is,
54-
IsMany,
55-
Set,
52+
enum CommandKind {
53+
/// `//@ has <path>`
54+
///
55+
/// Checks the path exists.
56+
HasPath,
57+
58+
/// `//@ has <path> <value>`
59+
///
60+
/// Check one thing at the path is equal to the value.
61+
HasValue { value: String },
62+
63+
/// `//@ !has <path>`
64+
///
65+
/// Checks the path doesn't exist.
66+
HasNotPath,
67+
68+
/// `//@ is <path> <value>`
69+
///
70+
/// Check the path is the given value.
71+
Is { value: String },
72+
73+
/// `//@ is <path> <value> <value>...`
74+
///
75+
/// Check that the path matches to exactly every given value.
76+
IsMany { values: Vec<String> },
77+
78+
/// `//@ !is <path> <value>`
79+
///
80+
/// Check the path isn't the given value.
81+
IsNot { value: String },
82+
83+
/// `//@ count <path> <value>`
84+
///
85+
/// Check the path has the expected number of matches.
86+
CountIs { expected: usize },
87+
88+
/// `//@ set <name> = <path>`
89+
Set { variable: String },
5690
}
5791

5892
impl CommandKind {
59-
fn validate(&self, args: &[String], lineno: usize) -> bool {
60-
// FIXME(adotinthevoid): We should "parse, don't validate" here, so we avoid ad-hoc
61-
// indexing in check_command.
62-
let count = match self {
63-
CommandKind::Has => (1..=2).contains(&args.len()),
64-
CommandKind::IsMany => args.len() >= 2,
65-
CommandKind::Count | CommandKind::Is => 2 == args.len(),
66-
CommandKind::Set => 3 == args.len(),
67-
};
93+
/// Returns both the kind and the path.
94+
///
95+
/// Returns `None` if the command isn't from jsondocck (e.g. from compiletest).
96+
fn parse<'a>(command_name: &str, negated: bool, args: &'a [String]) -> Option<(Self, &'a str)> {
97+
let kind = match (command_name, negated) {
98+
("count", false) => {
99+
assert_eq!(args.len(), 2);
100+
let expected = args[1].parse().expect("invalid number for `count`");
101+
Self::CountIs { expected }
102+
}
68103

69-
if !count {
70-
print_err(&format!("Incorrect number of arguments to `{}`", self), lineno);
71-
return false;
72-
}
104+
("ismany", false) => {
105+
// FIXME: Make this >= 3, and migrate len(values)==1 cases to @is
106+
assert!(args.len() >= 2, "Not enough args to `ismany`");
107+
let values = args[1..].to_owned();
108+
Self::IsMany { values }
109+
}
73110

74-
if let CommandKind::Count = self {
75-
if args[1].parse::<usize>().is_err() {
76-
print_err(
77-
&format!(
78-
"Second argument to `count` must be a valid usize (got `{}`)",
79-
args[1]
80-
),
81-
lineno,
82-
);
83-
return false;
111+
("is", false) => {
112+
assert_eq!(args.len(), 2);
113+
Self::Is { value: args[1].clone() }
114+
}
115+
("is", true) => {
116+
assert_eq!(args.len(), 2);
117+
Self::IsNot { value: args[1].clone() }
84118
}
85-
}
86119

87-
true
88-
}
89-
}
120+
("set", false) => {
121+
assert_eq!(args.len(), 3);
122+
assert_eq!(args[1], "=");
123+
return Some((Self::Set { variable: args[0].clone() }, &args[2]));
124+
}
90125

91-
impl fmt::Display for CommandKind {
92-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93-
let text = match self {
94-
CommandKind::Has => "has",
95-
CommandKind::IsMany => "ismany",
96-
CommandKind::Count => "count",
97-
CommandKind::Is => "is",
98-
CommandKind::Set => "set",
126+
("has", false) => match args {
127+
[_path] => Self::HasPath,
128+
[_path, value] => Self::HasValue { value: value.clone() },
129+
_ => panic!("`//@ has` must have 2 or 3 arguments, but got {args:?}"),
130+
},
131+
("has", true) => {
132+
assert_eq!(args.len(), 1, "args={args:?}");
133+
Self::HasNotPath
134+
}
135+
136+
(_, false) if KNOWN_DIRECTIVE_NAMES.contains(&command_name) => {
137+
return None;
138+
}
139+
_ => {
140+
panic!("Invalid command `//@ {}{command_name}`", if negated { "!" } else { "" })
141+
}
99142
};
100-
write!(f, "{}", text)
143+
144+
Some((kind, &args[0]))
101145
}
102146
}
103147

@@ -125,8 +169,7 @@ fn print_err(msg: &str, lineno: usize) {
125169
// See <https://github.com/rust-lang/rust/issues/125813#issuecomment-2141953780>.
126170
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/../compiletest/src/directive-list.rs"));
127171

128-
/// Get a list of commands from a file. Does the work of ensuring the commands
129-
/// are syntactically valid.
172+
/// Get a list of commands from a file.
130173
fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
131174
let mut commands = Vec::new();
132175
let mut errors = false;
@@ -142,217 +185,102 @@ fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
142185

143186
let negated = cap.name("negated").unwrap().as_str() == "!";
144187

145-
let cmd = match cap.name("cmd").unwrap().as_str() {
146-
"has" => CommandKind::Has,
147-
"count" => CommandKind::Count,
148-
"is" => CommandKind::Is,
149-
"ismany" => CommandKind::IsMany,
150-
"set" => CommandKind::Set,
151-
// FIXME: See the comment above the `include!(...)`.
152-
cmd if KNOWN_DIRECTIVE_NAMES.contains(&cmd) => continue,
153-
cmd => {
154-
print_err(&format!("Unrecognized command name `{cmd}`"), lineno);
155-
errors = true;
156-
continue;
157-
}
158-
};
159-
160-
let args = cap.name("args").map_or(Some(vec![]), |m| shlex::split(m.as_str()));
161-
162-
let args = match args {
188+
let args_str = &cap["args"];
189+
let args = match shlex::split(args_str) {
163190
Some(args) => args,
164191
None => {
165-
print_err(
166-
&format!(
167-
"Invalid arguments to shlex::split: `{}`",
168-
cap.name("args").unwrap().as_str()
169-
),
170-
lineno,
171-
);
192+
print_err(&format!("Invalid arguments to shlex::split: `{args_str}`",), lineno);
172193
errors = true;
173194
continue;
174195
}
175196
};
176197

177-
if !cmd.validate(&args, lineno) {
178-
errors = true;
179-
continue;
198+
if let Some((kind, path)) = CommandKind::parse(&cap["cmd"], negated, &args) {
199+
commands.push(Command { kind, lineno, path: path.to_owned() })
180200
}
181-
182-
commands.push(Command { negated, kind: cmd, args, lineno })
183201
}
184202

185203
if !errors { Ok(commands) } else { Err(()) }
186204
}
187205

188-
/// Performs the actual work of ensuring a command passes. Generally assumes the command
189-
/// is syntactically valid.
190-
fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
191-
// FIXME: Be more granular about why, (e.g. syntax error, count not equal)
192-
let result = match command.kind {
193-
CommandKind::Has => {
194-
match command.args.len() {
195-
// `has <jsonpath>`: Check that `jsonpath` exists.
196-
1 => {
197-
let val = cache.value();
198-
let results = select(val, &command.args[0]).unwrap();
199-
!results.is_empty()
200-
}
201-
// `has <jsonpath> <value>`: Check *any* item matched by `jsonpath` equals `value`.
202-
2 => {
203-
let val = cache.value().clone();
204-
let results = select(&val, &command.args[0]).unwrap();
205-
let pat = string_to_value(&command.args[1], cache);
206-
let has = results.contains(&pat.as_ref());
207-
// Give better error for when `has` check fails.
208-
if !command.negated && !has {
209-
return Err(CkError::FailedCheck(
210-
format!(
211-
"{} matched to {:?} but didn't have {:?}",
212-
&command.args[0],
213-
results,
214-
pat.as_ref()
215-
),
216-
command,
217-
));
218-
} else {
219-
has
220-
}
221-
}
222-
_ => unreachable!(),
206+
/// Performs the actual work of ensuring a command passes.
207+
fn check_command(command: &Command, cache: &mut Cache) -> Result<(), String> {
208+
let matches = cache.select(&command.path);
209+
match &command.kind {
210+
CommandKind::HasPath => {
211+
if matches.is_empty() {
212+
return Err("matched to no values".to_owned());
213+
}
214+
}
215+
CommandKind::HasNotPath => {
216+
if !matches.is_empty() {
217+
return Err(format!("matched to {matches:?}, but wanted no matches"));
218+
}
219+
}
220+
CommandKind::HasValue { value } => {
221+
let want_value = string_to_value(value, cache);
222+
if !matches.contains(&want_value.as_ref()) {
223+
return Err(format!("matched to {matches:?}, which didn't contain {want_value:?}"));
224+
}
225+
}
226+
CommandKind::Is { value } => {
227+
let want_value = string_to_value(value, cache);
228+
let matched = get_one(&matches)?;
229+
if matched != want_value.as_ref() {
230+
return Err(format!("matched to {matched:?} but want {want_value:?}"));
231+
}
232+
}
233+
CommandKind::IsNot { value } => {
234+
let wantnt_value = string_to_value(value, cache);
235+
let matched = get_one(&matches)?;
236+
if matched == wantnt_value.as_ref() {
237+
return Err(format!("got value {wantnt_value:?}, but want anything else"));
223238
}
224239
}
225-
// `ismany <path> <jsonpath> <value...>`
226-
CommandKind::IsMany => {
227-
assert!(!command.negated, "`ismany` may not be negated");
228-
let (query, values) = if let [query, values @ ..] = &command.args[..] {
229-
(query, values)
230-
} else {
231-
unreachable!("Checked in CommandKind::validate")
232-
};
233-
let val = cache.value();
234-
let got_values = select(val, &query).unwrap();
235240

241+
CommandKind::IsMany { values } => {
236242
// Serde json doesn't implement Ord or Hash for Value, so we must
237243
// use a Vec here. While in theory that makes setwize equality
238244
// O(n^2), in practice n will never be large enough to matter.
239245
let expected_values =
240246
values.iter().map(|v| string_to_value(v, cache)).collect::<Vec<_>>();
241-
if expected_values.len() != got_values.len() {
242-
return Err(CkError::FailedCheck(
243-
format!(
244-
"Expected {} values, but `{}` matched to {} values ({:?})",
245-
expected_values.len(),
246-
query,
247-
got_values.len(),
248-
got_values
249-
),
250-
command,
247+
if expected_values.len() != matches.len() {
248+
return Err(format!(
249+
"Expected {} values, but matched to {} values ({:?})",
250+
expected_values.len(),
251+
matches.len(),
252+
matches
251253
));
252254
};
253-
for got_value in got_values {
255+
for got_value in matches {
254256
if !expected_values.iter().any(|exp| &**exp == got_value) {
255-
return Err(CkError::FailedCheck(
256-
format!("`{}` has match {:?}, which was not expected", query, got_value),
257-
command,
258-
));
257+
return Err(format!("has match {got_value:?}, which was not expected",));
259258
}
260259
}
261-
true
262-
}
263-
// `count <jsonpath> <count>`: Check that `jsonpath` matches exactly `count` times.
264-
CommandKind::Count => {
265-
assert_eq!(command.args.len(), 2);
266-
let expected: usize = command.args[1].parse().unwrap();
267-
let val = cache.value();
268-
let results = select(val, &command.args[0]).unwrap();
269-
let eq = results.len() == expected;
270-
if !command.negated && !eq {
271-
return Err(CkError::FailedCheck(
272-
format!(
273-
"`{}` matched to `{:?}` with length {}, but expected length {}",
274-
&command.args[0],
275-
results,
276-
results.len(),
277-
expected
278-
),
279-
command,
280-
));
281-
} else {
282-
eq
283-
}
284260
}
285-
// `has <jsonpath> <value>`: Check` *exactly one* item matched by `jsonpath`, and it equals `value`.
286-
CommandKind::Is => {
287-
assert_eq!(command.args.len(), 2);
288-
let val = cache.value().clone();
289-
let results = select(&val, &command.args[0]).unwrap();
290-
let pat = string_to_value(&command.args[1], cache);
291-
let is = results.len() == 1 && results[0] == pat.as_ref();
292-
if !command.negated && !is {
293-
return Err(CkError::FailedCheck(
294-
format!(
295-
"{} matched to {:?}, but expected {:?}",
296-
&command.args[0],
297-
results,
298-
pat.as_ref()
299-
),
300-
command,
261+
CommandKind::CountIs { expected } => {
262+
if *expected != matches.len() {
263+
return Err(format!(
264+
"matched to `{matches:?}` with length {}, but expected length {expected}",
265+
matches.len(),
301266
));
302-
} else {
303-
is
304267
}
305268
}
306-
// `set <name> = <jsonpath>`
307-
CommandKind::Set => {
308-
assert!(!command.negated, "`set` may not be negated");
309-
assert_eq!(command.args.len(), 3);
310-
assert_eq!(command.args[1], "=", "Expected an `=`");
311-
let val = cache.value().clone();
312-
let results = select(&val, &command.args[2]).unwrap();
313-
assert_eq!(
314-
results.len(),
315-
1,
316-
"Expected 1 match for `{}` (because of `set`): matched to {:?}",
317-
command.args[2],
318-
results
319-
);
320-
match results.len() {
321-
0 => false,
322-
1 => {
323-
let r = cache.variables.insert(command.args[0].clone(), results[0].clone());
324-
assert!(r.is_none(), "Name collision: {} is duplicated", command.args[0]);
325-
true
326-
}
327-
_ => {
328-
panic!(
329-
"Got multiple results in `set` for `{}`: {:?}",
330-
&command.args[2], results,
331-
);
332-
}
333-
}
269+
CommandKind::Set { variable } => {
270+
let value = get_one(&matches)?;
271+
let r = cache.variables.insert(variable.to_owned(), value.clone());
272+
assert!(r.is_none(), "name collision: {variable:?} is duplicated");
334273
}
335-
};
274+
}
336275

337-
if result == command.negated {
338-
if command.negated {
339-
Err(CkError::FailedCheck(
340-
format!("`!{} {}` matched when it shouldn't", command.kind, command.args.join(" ")),
341-
command,
342-
))
343-
} else {
344-
// FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
345-
Err(CkError::FailedCheck(
346-
format!(
347-
"`{} {}` didn't match when it should",
348-
command.kind,
349-
command.args.join(" ")
350-
),
351-
command,
352-
))
353-
}
354-
} else {
355-
Ok(())
276+
Ok(())
277+
}
278+
279+
fn get_one<'a>(matches: &[&'a Value]) -> Result<&'a Value, String> {
280+
match matches {
281+
[] => Err("matched to no values".to_owned()),
282+
[matched] => Ok(matched),
283+
_ => Err(format!("matched to multiple values {matches:?}, but want exactly 1")),
356284
}
357285
}
358286

0 commit comments

Comments
 (0)
This repository has been archived.