Skip to content

Commit 2c9fb22

Browse files
committed
jsondoclint: Extract Command/CommandKind into its own file
1 parent 6f69710 commit 2c9fb22

File tree

2 files changed

+233
-222
lines changed

2 files changed

+233
-222
lines changed

src/tools/jsondocck/src/directive.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
use std::borrow::Cow;
2+
use serde_json::Value;
3+
use crate::cache::Cache;
4+
5+
#[derive(Debug)]
6+
pub struct Command {
7+
pub kind: CommandKind,
8+
pub path: String,
9+
pub lineno: usize,
10+
}
11+
12+
#[derive(Debug)]
13+
pub enum CommandKind {
14+
/// `//@ has <path>`
15+
///
16+
/// Checks the path exists.
17+
HasPath,
18+
19+
/// `//@ has <path> <value>`
20+
///
21+
/// Check one thing at the path is equal to the value.
22+
HasValue { value: String },
23+
24+
/// `//@ !has <path>`
25+
///
26+
/// Checks the path doesn't exist.
27+
HasNotPath,
28+
29+
/// `//@ !has <path> <value>`
30+
///
31+
/// Checks the path exists, but doesn't have the given value.
32+
HasNotValue { value: String },
33+
34+
/// `//@ is <path> <value>`
35+
///
36+
/// Check the path is the given value.
37+
Is { value: String },
38+
39+
/// `//@ is <path> <value> <value>...`
40+
///
41+
/// Check that the path matches to exactly every given value.
42+
IsMany { values: Vec<String> },
43+
44+
/// `//@ !is <path> <value>`
45+
///
46+
/// Check the path isn't the given value.
47+
IsNot { value: String },
48+
49+
/// `//@ count <path> <value>`
50+
///
51+
/// Check the path has the expected number of matches.
52+
CountIs { expected: usize },
53+
54+
/// `//@ set <name> = <path>`
55+
Set { variable: String },
56+
}
57+
58+
impl CommandKind {
59+
/// Returns both the kind and the path.
60+
///
61+
/// Returns `None` if the command isn't from jsondocck (e.g. from compiletest).
62+
pub fn parse<'a>(
63+
command_name: &str,
64+
negated: bool,
65+
args: &'a [String],
66+
) -> Option<(Self, &'a str)> {
67+
let kind = match (command_name, negated) {
68+
("count", false) => {
69+
assert_eq!(args.len(), 2);
70+
let expected = args[1].parse().expect("invalid number for `count`");
71+
Self::CountIs { expected }
72+
}
73+
74+
("ismany", false) => {
75+
// FIXME: Make this >= 3, and migrate len(values)==1 cases to @is
76+
assert!(args.len() >= 2, "Not enough args to `ismany`");
77+
let values = args[1..].to_owned();
78+
Self::IsMany { values }
79+
}
80+
81+
("is", false) => {
82+
assert_eq!(args.len(), 2);
83+
Self::Is { value: args[1].clone() }
84+
}
85+
("is", true) => {
86+
assert_eq!(args.len(), 2);
87+
Self::IsNot { value: args[1].clone() }
88+
}
89+
90+
("set", false) => {
91+
assert_eq!(args.len(), 3);
92+
assert_eq!(args[1], "=");
93+
return Some((Self::Set { variable: args[0].clone() }, &args[2]));
94+
}
95+
96+
("has", false) => match args {
97+
[_path] => Self::HasPath,
98+
[_path, value] => Self::HasValue { value: value.clone() },
99+
_ => panic!("`//@ has` must have 2 or 3 arguments, but got {args:?}"),
100+
},
101+
("has", true) => match args {
102+
[_path] => Self::HasNotPath,
103+
[_path, value] => Self::HasNotValue { value: value.clone() },
104+
_ => panic!("`//@ !has` must have 2 or 3 arguments, but got {args:?}"),
105+
},
106+
107+
(_, false) if KNOWN_DIRECTIVE_NAMES.contains(&command_name) => {
108+
return None;
109+
}
110+
_ => {
111+
panic!("Invalid command `//@ {}{command_name}`", if negated { "!" } else { "" })
112+
}
113+
};
114+
115+
Some((kind, &args[0]))
116+
}
117+
}
118+
119+
impl Command {
120+
/// Performs the actual work of ensuring a command passes.
121+
pub fn check(&self, cache: &mut Cache) -> Result<(), String> {
122+
let matches = cache.select(&self.path);
123+
match &self.kind {
124+
CommandKind::HasPath => {
125+
if matches.is_empty() {
126+
return Err("matched to no values".to_owned());
127+
}
128+
}
129+
CommandKind::HasNotPath => {
130+
if !matches.is_empty() {
131+
return Err(format!("matched to {matches:?}, but wanted no matches"));
132+
}
133+
}
134+
CommandKind::HasValue { value } => {
135+
let want_value = string_to_value(value, cache);
136+
if !matches.contains(&want_value.as_ref()) {
137+
return Err(format!(
138+
"matched to {matches:?}, which didn't contain {want_value:?}"
139+
));
140+
}
141+
}
142+
CommandKind::HasNotValue { value } => {
143+
let wantnt_value = string_to_value(value, cache);
144+
if matches.contains(&wantnt_value.as_ref()) {
145+
return Err(format!(
146+
"matched to {matches:?}, which contains unwanted {wantnt_value:?}"
147+
));
148+
} else if matches.is_empty() {
149+
return Err(format!(
150+
"got no matches, but expected some matched (not containing {wantnt_value:?}"
151+
));
152+
}
153+
}
154+
155+
CommandKind::Is { value } => {
156+
let want_value = string_to_value(value, cache);
157+
let matched = get_one(&matches)?;
158+
if matched != want_value.as_ref() {
159+
return Err(format!("matched to {matched:?} but want {want_value:?}"));
160+
}
161+
}
162+
CommandKind::IsNot { value } => {
163+
let wantnt_value = string_to_value(value, cache);
164+
let matched = get_one(&matches)?;
165+
if matched == wantnt_value.as_ref() {
166+
return Err(format!("got value {wantnt_value:?}, but want anything else"));
167+
}
168+
}
169+
170+
CommandKind::IsMany { values } => {
171+
// Serde json doesn't implement Ord or Hash for Value, so we must
172+
// use a Vec here. While in theory that makes setwize equality
173+
// O(n^2), in practice n will never be large enough to matter.
174+
let expected_values =
175+
values.iter().map(|v| string_to_value(v, cache)).collect::<Vec<_>>();
176+
if expected_values.len() != matches.len() {
177+
return Err(format!(
178+
"Expected {} values, but matched to {} values ({:?})",
179+
expected_values.len(),
180+
matches.len(),
181+
matches
182+
));
183+
};
184+
for got_value in matches {
185+
if !expected_values.iter().any(|exp| &**exp == got_value) {
186+
return Err(format!("has match {got_value:?}, which was not expected",));
187+
}
188+
}
189+
}
190+
CommandKind::CountIs { expected } => {
191+
if *expected != matches.len() {
192+
return Err(format!(
193+
"matched to `{matches:?}` with length {}, but expected length {expected}",
194+
matches.len(),
195+
));
196+
}
197+
}
198+
CommandKind::Set { variable } => {
199+
let value = get_one(&matches)?;
200+
let r = cache.variables.insert(variable.to_owned(), value.clone());
201+
assert!(r.is_none(), "name collision: {variable:?} is duplicated");
202+
}
203+
}
204+
205+
Ok(())
206+
}
207+
}
208+
209+
fn get_one<'a>(matches: &[&'a Value]) -> Result<&'a Value, String> {
210+
match matches {
211+
[] => Err("matched to no values".to_owned()),
212+
[matched] => Ok(matched),
213+
_ => Err(format!("matched to multiple values {matches:?}, but want exactly 1")),
214+
}
215+
}
216+
217+
// FIXME: This setup is temporary until we figure out how to improve this situation.
218+
// See <https://github.com/rust-lang/rust/issues/125813#issuecomment-2141953780>.
219+
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/../compiletest/src/directive-list.rs"));
220+
221+
fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
222+
if s.starts_with("$") {
223+
Cow::Borrowed(&cache.variables.get(&s[1..]).unwrap_or_else(|| {
224+
// FIXME(adotinthevoid): Show line number
225+
panic!("No variable: `{}`. Current state: `{:?}`", &s[1..], cache.variables)
226+
}))
227+
} else {
228+
Cow::Owned(serde_json::from_str(s).expect(&format!("Cannot convert `{}` to json", s)))
229+
}
230+
}

0 commit comments

Comments
 (0)