aboutsummaryrefslogtreecommitdiff
path: root/crates/cfg/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/cfg/src')
-rw-r--r--crates/cfg/src/cfg_expr.rs113
-rw-r--r--crates/cfg/src/dnf.rs320
-rw-r--r--crates/cfg/src/lib.rs130
-rw-r--r--crates/cfg/src/tests.rs193
4 files changed, 685 insertions, 71 deletions
diff --git a/crates/cfg/src/cfg_expr.rs b/crates/cfg/src/cfg_expr.rs
index 336fe25bc..42327f1e1 100644
--- a/crates/cfg/src/cfg_expr.rs
+++ b/crates/cfg/src/cfg_expr.rs
@@ -2,30 +2,77 @@
2//! 2//!
3//! See: https://doc.rust-lang.org/reference/conditional-compilation.html#conditional-compilation 3//! See: https://doc.rust-lang.org/reference/conditional-compilation.html#conditional-compilation
4 4
5use std::slice::Iter as SliceIter; 5use std::{fmt, slice::Iter as SliceIter};
6 6
7use tt::SmolStr; 7use tt::SmolStr;
8 8
9/// A simple configuration value passed in from the outside.
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
11pub enum CfgAtom {
12 /// eg. `#[cfg(test)]`
13 Flag(SmolStr),
14 /// eg. `#[cfg(target_os = "linux")]`
15 ///
16 /// Note that a key can have multiple values that are all considered "active" at the same time.
17 /// For example, `#[cfg(target_feature = "sse")]` and `#[cfg(target_feature = "sse2")]`.
18 KeyValue { key: SmolStr, value: SmolStr },
19}
20
21impl CfgAtom {
22 /// Returns `true` when the atom comes from the target specification.
23 ///
24 /// If this returns `true`, then changing this atom requires changing the compilation target. If
25 /// it returns `false`, the atom might come from a build script or the build system.
26 pub fn is_target_defined(&self) -> bool {
27 match self {
28 CfgAtom::Flag(flag) => matches!(&**flag, "unix" | "windows"),
29 CfgAtom::KeyValue { key, value: _ } => matches!(
30 &**key,
31 "target_arch"
32 | "target_os"
33 | "target_env"
34 | "target_family"
35 | "target_endian"
36 | "target_pointer_width"
37 | "target_vendor" // NOTE: `target_feature` is left out since it can be configured via `-Ctarget-feature`
38 ),
39 }
40 }
41}
42
43impl fmt::Display for CfgAtom {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 CfgAtom::Flag(name) => write!(f, "{}", name),
47 CfgAtom::KeyValue { key, value } => write!(f, "{} = {:?}", key, value),
48 }
49 }
50}
51
9#[derive(Debug, Clone, PartialEq, Eq)] 52#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum CfgExpr { 53pub enum CfgExpr {
11 Invalid, 54 Invalid,
12 Atom(SmolStr), 55 Atom(CfgAtom),
13 KeyValue { key: SmolStr, value: SmolStr },
14 All(Vec<CfgExpr>), 56 All(Vec<CfgExpr>),
15 Any(Vec<CfgExpr>), 57 Any(Vec<CfgExpr>),
16 Not(Box<CfgExpr>), 58 Not(Box<CfgExpr>),
17} 59}
18 60
61impl From<CfgAtom> for CfgExpr {
62 fn from(atom: CfgAtom) -> Self {
63 CfgExpr::Atom(atom)
64 }
65}
66
19impl CfgExpr { 67impl CfgExpr {
20 pub fn parse(tt: &tt::Subtree) -> CfgExpr { 68 pub fn parse(tt: &tt::Subtree) -> CfgExpr {
21 next_cfg_expr(&mut tt.token_trees.iter()).unwrap_or(CfgExpr::Invalid) 69 next_cfg_expr(&mut tt.token_trees.iter()).unwrap_or(CfgExpr::Invalid)
22 } 70 }
23 /// Fold the cfg by querying all basic `Atom` and `KeyValue` predicates. 71 /// Fold the cfg by querying all basic `Atom` and `KeyValue` predicates.
24 pub fn fold(&self, query: &dyn Fn(&SmolStr, Option<&SmolStr>) -> bool) -> Option<bool> { 72 pub fn fold(&self, query: &dyn Fn(&CfgAtom) -> bool) -> Option<bool> {
25 match self { 73 match self {
26 CfgExpr::Invalid => None, 74 CfgExpr::Invalid => None,
27 CfgExpr::Atom(name) => Some(query(name, None)), 75 CfgExpr::Atom(atom) => Some(query(atom)),
28 CfgExpr::KeyValue { key, value } => Some(query(key, Some(value))),
29 CfgExpr::All(preds) => { 76 CfgExpr::All(preds) => {
30 preds.iter().try_fold(true, |s, pred| Some(s && pred.fold(query)?)) 77 preds.iter().try_fold(true, |s, pred| Some(s && pred.fold(query)?))
31 } 78 }
@@ -54,7 +101,7 @@ fn next_cfg_expr(it: &mut SliceIter<tt::TokenTree>) -> Option<CfgExpr> {
54 // FIXME: escape? raw string? 101 // FIXME: escape? raw string?
55 let value = 102 let value =
56 SmolStr::new(literal.text.trim_start_matches('"').trim_end_matches('"')); 103 SmolStr::new(literal.text.trim_start_matches('"').trim_end_matches('"'));
57 CfgExpr::KeyValue { key: name, value } 104 CfgAtom::KeyValue { key: name, value }.into()
58 } 105 }
59 _ => return Some(CfgExpr::Invalid), 106 _ => return Some(CfgExpr::Invalid),
60 } 107 }
@@ -70,7 +117,7 @@ fn next_cfg_expr(it: &mut SliceIter<tt::TokenTree>) -> Option<CfgExpr> {
70 _ => CfgExpr::Invalid, 117 _ => CfgExpr::Invalid,
71 } 118 }
72 } 119 }
73 _ => CfgExpr::Atom(name), 120 _ => CfgAtom::Flag(name).into(),
74 }; 121 };
75 122
76 // Eat comma separator 123 // Eat comma separator
@@ -81,53 +128,3 @@ fn next_cfg_expr(it: &mut SliceIter<tt::TokenTree>) -> Option<CfgExpr> {
81 } 128 }
82 Some(ret) 129 Some(ret)
83} 130}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 use mbe::ast_to_token_tree;
90 use syntax::ast::{self, AstNode};
91
92 fn assert_parse_result(input: &str, expected: CfgExpr) {
93 let (tt, _) = {
94 let source_file = ast::SourceFile::parse(input).ok().unwrap();
95 let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap();
96 ast_to_token_tree(&tt).unwrap()
97 };
98 let cfg = CfgExpr::parse(&tt);
99 assert_eq!(cfg, expected);
100 }
101
102 #[test]
103 fn test_cfg_expr_parser() {
104 assert_parse_result("#![cfg(foo)]", CfgExpr::Atom("foo".into()));
105 assert_parse_result("#![cfg(foo,)]", CfgExpr::Atom("foo".into()));
106 assert_parse_result(
107 "#![cfg(not(foo))]",
108 CfgExpr::Not(Box::new(CfgExpr::Atom("foo".into()))),
109 );
110 assert_parse_result("#![cfg(foo(bar))]", CfgExpr::Invalid);
111
112 // Only take the first
113 assert_parse_result(r#"#![cfg(foo, bar = "baz")]"#, CfgExpr::Atom("foo".into()));
114
115 assert_parse_result(
116 r#"#![cfg(all(foo, bar = "baz"))]"#,
117 CfgExpr::All(vec![
118 CfgExpr::Atom("foo".into()),
119 CfgExpr::KeyValue { key: "bar".into(), value: "baz".into() },
120 ]),
121 );
122
123 assert_parse_result(
124 r#"#![cfg(any(not(), all(), , bar = "baz",))]"#,
125 CfgExpr::Any(vec![
126 CfgExpr::Not(Box::new(CfgExpr::Invalid)),
127 CfgExpr::All(vec![]),
128 CfgExpr::Invalid,
129 CfgExpr::KeyValue { key: "bar".into(), value: "baz".into() },
130 ]),
131 );
132 }
133}
diff --git a/crates/cfg/src/dnf.rs b/crates/cfg/src/dnf.rs
new file mode 100644
index 000000000..580c9a9a2
--- /dev/null
+++ b/crates/cfg/src/dnf.rs
@@ -0,0 +1,320 @@
1//! Disjunctive Normal Form construction.
2//!
3//! Algorithm from <https://www.cs.drexel.edu/~jjohnson/2015-16/fall/CS270/Lectures/3/dnf.pdf>,
4//! which would have been much easier to read if it used pattern matching. It's also missing the
5//! entire "distribute ANDs over ORs" part, which is not trivial. Oh well.
6//!
7//! This is currently both messy and inefficient. Feel free to improve, there are unit tests.
8
9use std::fmt;
10
11use rustc_hash::FxHashSet;
12
13use crate::{CfgAtom, CfgDiff, CfgExpr, CfgOptions, InactiveReason};
14
15/// A `#[cfg]` directive in Disjunctive Normal Form (DNF).
16pub struct DnfExpr {
17 conjunctions: Vec<Conjunction>,
18}
19
20struct Conjunction {
21 literals: Vec<Literal>,
22}
23
24struct Literal {
25 negate: bool,
26 var: Option<CfgAtom>, // None = Invalid
27}
28
29impl DnfExpr {
30 pub fn new(expr: CfgExpr) -> Self {
31 let builder = Builder { expr: DnfExpr { conjunctions: Vec::new() } };
32
33 builder.lower(expr.clone())
34 }
35
36 /// Computes a list of present or absent atoms in `opts` that cause this expression to evaluate
37 /// to `false`.
38 ///
39 /// Note that flipping a subset of these atoms might be sufficient to make the whole expression
40 /// evaluate to `true`. For that, see `compute_enable_hints`.
41 ///
42 /// Returns `None` when `self` is already true, or contains errors.
43 pub fn why_inactive(&self, opts: &CfgOptions) -> Option<InactiveReason> {
44 let mut res = InactiveReason { enabled: Vec::new(), disabled: Vec::new() };
45
46 for conj in &self.conjunctions {
47 let mut conj_is_true = true;
48 for lit in &conj.literals {
49 let atom = lit.var.as_ref()?;
50 let enabled = opts.enabled.contains(atom);
51 if lit.negate == enabled {
52 // Literal is false, but needs to be true for this conjunction.
53 conj_is_true = false;
54
55 if enabled {
56 res.enabled.push(atom.clone());
57 } else {
58 res.disabled.push(atom.clone());
59 }
60 }
61 }
62
63 if conj_is_true {
64 // This expression is not actually inactive.
65 return None;
66 }
67 }
68
69 res.enabled.sort_unstable();
70 res.enabled.dedup();
71 res.disabled.sort_unstable();
72 res.disabled.dedup();
73 Some(res)
74 }
75
76 /// Returns `CfgDiff` objects that would enable this directive if applied to `opts`.
77 pub fn compute_enable_hints<'a>(
78 &'a self,
79 opts: &'a CfgOptions,
80 ) -> impl Iterator<Item = CfgDiff> + 'a {
81 // A cfg is enabled if any of `self.conjunctions` evaluate to `true`.
82
83 self.conjunctions.iter().filter_map(move |conj| {
84 let mut enable = FxHashSet::default();
85 let mut disable = FxHashSet::default();
86 for lit in &conj.literals {
87 let atom = lit.var.as_ref()?;
88 let enabled = opts.enabled.contains(atom);
89 if lit.negate && enabled {
90 disable.insert(atom.clone());
91 }
92 if !lit.negate && !enabled {
93 enable.insert(atom.clone());
94 }
95 }
96
97 // Check that this actually makes `conj` true.
98 for lit in &conj.literals {
99 let atom = lit.var.as_ref()?;
100 let enabled = enable.contains(atom)
101 || (opts.enabled.contains(atom) && !disable.contains(atom));
102 if enabled == lit.negate {
103 return None;
104 }
105 }
106
107 if enable.is_empty() && disable.is_empty() {
108 return None;
109 }
110
111 let mut diff = CfgDiff {
112 enable: enable.into_iter().collect(),
113 disable: disable.into_iter().collect(),
114 };
115
116 // Undo the FxHashMap randomization for consistent output.
117 diff.enable.sort_unstable();
118 diff.disable.sort_unstable();
119
120 Some(diff)
121 })
122 }
123}
124
125impl fmt::Display for DnfExpr {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 if self.conjunctions.len() != 1 {
128 write!(f, "any(")?;
129 }
130 for (i, conj) in self.conjunctions.iter().enumerate() {
131 if i != 0 {
132 f.write_str(", ")?;
133 }
134
135 write!(f, "{}", conj)?;
136 }
137 if self.conjunctions.len() != 1 {
138 write!(f, ")")?;
139 }
140
141 Ok(())
142 }
143}
144
145impl Conjunction {
146 fn new(parts: Vec<CfgExpr>) -> Self {
147 let mut literals = Vec::new();
148 for part in parts {
149 match part {
150 CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::Not(_) => {
151 literals.push(Literal::new(part));
152 }
153 CfgExpr::All(conj) => {
154 // Flatten.
155 literals.extend(Conjunction::new(conj).literals);
156 }
157 CfgExpr::Any(_) => unreachable!("disjunction in conjunction"),
158 }
159 }
160
161 Self { literals }
162 }
163}
164
165impl fmt::Display for Conjunction {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 if self.literals.len() != 1 {
168 write!(f, "all(")?;
169 }
170 for (i, lit) in self.literals.iter().enumerate() {
171 if i != 0 {
172 f.write_str(", ")?;
173 }
174
175 write!(f, "{}", lit)?;
176 }
177 if self.literals.len() != 1 {
178 write!(f, ")")?;
179 }
180
181 Ok(())
182 }
183}
184
185impl Literal {
186 fn new(expr: CfgExpr) -> Self {
187 match expr {
188 CfgExpr::Invalid => Self { negate: false, var: None },
189 CfgExpr::Atom(atom) => Self { negate: false, var: Some(atom) },
190 CfgExpr::Not(expr) => match *expr {
191 CfgExpr::Invalid => Self { negate: true, var: None },
192 CfgExpr::Atom(atom) => Self { negate: true, var: Some(atom) },
193 _ => unreachable!("non-atom {:?}", expr),
194 },
195 CfgExpr::Any(_) | CfgExpr::All(_) => unreachable!("non-literal {:?}", expr),
196 }
197 }
198}
199
200impl fmt::Display for Literal {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 if self.negate {
203 write!(f, "not(")?;
204 }
205
206 match &self.var {
207 Some(var) => write!(f, "{}", var)?,
208 None => f.write_str("<invalid>")?,
209 }
210
211 if self.negate {
212 write!(f, ")")?;
213 }
214
215 Ok(())
216 }
217}
218
219struct Builder {
220 expr: DnfExpr,
221}
222
223impl Builder {
224 fn lower(mut self, expr: CfgExpr) -> DnfExpr {
225 let expr = make_nnf(expr);
226 let expr = make_dnf(expr);
227
228 match expr {
229 CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::Not(_) => {
230 self.expr.conjunctions.push(Conjunction::new(vec![expr]));
231 }
232 CfgExpr::All(conj) => {
233 self.expr.conjunctions.push(Conjunction::new(conj));
234 }
235 CfgExpr::Any(mut disj) => {
236 disj.reverse();
237 while let Some(conj) = disj.pop() {
238 match conj {
239 CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::All(_) | CfgExpr::Not(_) => {
240 self.expr.conjunctions.push(Conjunction::new(vec![conj]));
241 }
242 CfgExpr::Any(inner_disj) => {
243 // Flatten.
244 disj.extend(inner_disj.into_iter().rev());
245 }
246 }
247 }
248 }
249 }
250
251 self.expr
252 }
253}
254
255fn make_dnf(expr: CfgExpr) -> CfgExpr {
256 match expr {
257 CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::Not(_) => expr,
258 CfgExpr::Any(e) => CfgExpr::Any(e.into_iter().map(|expr| make_dnf(expr)).collect()),
259 CfgExpr::All(e) => {
260 let e = e.into_iter().map(|expr| make_nnf(expr)).collect::<Vec<_>>();
261
262 CfgExpr::Any(distribute_conj(&e))
263 }
264 }
265}
266
267/// Turns a conjunction of expressions into a disjunction of expressions.
268fn distribute_conj(conj: &[CfgExpr]) -> Vec<CfgExpr> {
269 fn go(out: &mut Vec<CfgExpr>, with: &mut Vec<CfgExpr>, rest: &[CfgExpr]) {
270 match rest {
271 [head, tail @ ..] => match head {
272 CfgExpr::Any(disj) => {
273 for part in disj {
274 with.push(part.clone());
275 go(out, with, tail);
276 with.pop();
277 }
278 }
279 _ => {
280 with.push(head.clone());
281 go(out, with, tail);
282 with.pop();
283 }
284 },
285 _ => {
286 // Turn accumulated parts into a new conjunction.
287 out.push(CfgExpr::All(with.clone()));
288 }
289 }
290 }
291
292 let mut out = Vec::new();
293 let mut with = Vec::new();
294
295 go(&mut out, &mut with, conj);
296
297 out
298}
299
300fn make_nnf(expr: CfgExpr) -> CfgExpr {
301 match expr {
302 CfgExpr::Invalid | CfgExpr::Atom(_) => expr,
303 CfgExpr::Any(expr) => CfgExpr::Any(expr.into_iter().map(|expr| make_nnf(expr)).collect()),
304 CfgExpr::All(expr) => CfgExpr::All(expr.into_iter().map(|expr| make_nnf(expr)).collect()),
305 CfgExpr::Not(operand) => match *operand {
306 CfgExpr::Invalid | CfgExpr::Atom(_) => CfgExpr::Not(operand.clone()), // Original negated expr
307 CfgExpr::Not(expr) => {
308 // Remove double negation.
309 make_nnf(*expr)
310 }
311 // Convert negated conjunction/disjunction using DeMorgan's Law.
312 CfgExpr::Any(inner) => CfgExpr::All(
313 inner.into_iter().map(|expr| make_nnf(CfgExpr::Not(Box::new(expr)))).collect(),
314 ),
315 CfgExpr::All(inner) => CfgExpr::Any(
316 inner.into_iter().map(|expr| make_nnf(CfgExpr::Not(Box::new(expr)))).collect(),
317 ),
318 },
319 }
320}
diff --git a/crates/cfg/src/lib.rs b/crates/cfg/src/lib.rs
index a9d50e698..d0e08cf5f 100644
--- a/crates/cfg/src/lib.rs
+++ b/crates/cfg/src/lib.rs
@@ -1,11 +1,17 @@
1//! cfg defines conditional compiling options, `cfg` attibute parser and evaluator 1//! cfg defines conditional compiling options, `cfg` attibute parser and evaluator
2 2
3mod cfg_expr; 3mod cfg_expr;
4mod dnf;
5#[cfg(test)]
6mod tests;
7
8use std::fmt;
4 9
5use rustc_hash::FxHashSet; 10use rustc_hash::FxHashSet;
6use tt::SmolStr; 11use tt::SmolStr;
7 12
8pub use cfg_expr::CfgExpr; 13pub use cfg_expr::{CfgAtom, CfgExpr};
14pub use dnf::DnfExpr;
9 15
10/// Configuration options used for conditional compilition on items with `cfg` attributes. 16/// Configuration options used for conditional compilition on items with `cfg` attributes.
11/// We have two kind of options in different namespaces: atomic options like `unix`, and 17/// We have two kind of options in different namespaces: atomic options like `unix`, and
@@ -19,33 +25,131 @@ pub use cfg_expr::CfgExpr;
19/// See: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options 25/// See: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options
20#[derive(Debug, Clone, PartialEq, Eq, Default)] 26#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct CfgOptions { 27pub struct CfgOptions {
22 atoms: FxHashSet<SmolStr>, 28 enabled: FxHashSet<CfgAtom>,
23 key_values: FxHashSet<(SmolStr, SmolStr)>,
24} 29}
25 30
26impl CfgOptions { 31impl CfgOptions {
27 pub fn check(&self, cfg: &CfgExpr) -> Option<bool> { 32 pub fn check(&self, cfg: &CfgExpr) -> Option<bool> {
28 cfg.fold(&|key, value| match value { 33 cfg.fold(&|atom| self.enabled.contains(atom))
29 None => self.atoms.contains(key),
30 Some(value) => self.key_values.contains(&(key.clone(), value.clone())),
31 })
32 } 34 }
33 35
34 pub fn insert_atom(&mut self, key: SmolStr) { 36 pub fn insert_atom(&mut self, key: SmolStr) {
35 self.atoms.insert(key); 37 self.enabled.insert(CfgAtom::Flag(key));
36 } 38 }
37 39
38 pub fn insert_key_value(&mut self, key: SmolStr, value: SmolStr) { 40 pub fn insert_key_value(&mut self, key: SmolStr, value: SmolStr) {
39 self.key_values.insert((key, value)); 41 self.enabled.insert(CfgAtom::KeyValue { key, value });
40 } 42 }
41 43
42 pub fn append(&mut self, other: &CfgOptions) { 44 pub fn append(&mut self, other: &CfgOptions) {
43 for atom in &other.atoms { 45 for atom in &other.enabled {
44 self.atoms.insert(atom.clone()); 46 self.enabled.insert(atom.clone());
47 }
48 }
49
50 pub fn apply_diff(&mut self, diff: CfgDiff) {
51 for atom in diff.enable {
52 self.enabled.insert(atom);
45 } 53 }
46 54
47 for (key, value) in &other.key_values { 55 for atom in diff.disable {
48 self.key_values.insert((key.clone(), value.clone())); 56 self.enabled.remove(&atom);
57 }
58 }
59}
60
61pub struct CfgDiff {
62 // Invariants: No duplicates, no atom that's both in `enable` and `disable`.
63 enable: Vec<CfgAtom>,
64 disable: Vec<CfgAtom>,
65}
66
67impl CfgDiff {
68 /// Returns the total number of atoms changed by this diff.
69 pub fn len(&self) -> usize {
70 self.enable.len() + self.disable.len()
71 }
72}
73
74impl fmt::Display for CfgDiff {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 if !self.enable.is_empty() {
77 f.write_str("enable ")?;
78 for (i, atom) in self.enable.iter().enumerate() {
79 let sep = match i {
80 0 => "",
81 _ if i == self.enable.len() - 1 => " and ",
82 _ => ", ",
83 };
84 f.write_str(sep)?;
85
86 write!(f, "{}", atom)?;
87 }
88
89 if !self.disable.is_empty() {
90 f.write_str("; ")?;
91 }
49 } 92 }
93
94 if !self.disable.is_empty() {
95 f.write_str("disable ")?;
96 for (i, atom) in self.disable.iter().enumerate() {
97 let sep = match i {
98 0 => "",
99 _ if i == self.enable.len() - 1 => " and ",
100 _ => ", ",
101 };
102 f.write_str(sep)?;
103
104 write!(f, "{}", atom)?;
105 }
106 }
107
108 Ok(())
109 }
110}
111
112pub struct InactiveReason {
113 enabled: Vec<CfgAtom>,
114 disabled: Vec<CfgAtom>,
115}
116
117impl fmt::Display for InactiveReason {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 if !self.enabled.is_empty() {
120 for (i, atom) in self.enabled.iter().enumerate() {
121 let sep = match i {
122 0 => "",
123 _ if i == self.enabled.len() - 1 => " and ",
124 _ => ", ",
125 };
126 f.write_str(sep)?;
127
128 write!(f, "{}", atom)?;
129 }
130 let is_are = if self.enabled.len() == 1 { "is" } else { "are" };
131 write!(f, " {} enabled", is_are)?;
132
133 if !self.disabled.is_empty() {
134 f.write_str(" and ")?;
135 }
136 }
137
138 if !self.disabled.is_empty() {
139 for (i, atom) in self.disabled.iter().enumerate() {
140 let sep = match i {
141 0 => "",
142 _ if i == self.disabled.len() - 1 => " and ",
143 _ => ", ",
144 };
145 f.write_str(sep)?;
146
147 write!(f, "{}", atom)?;
148 }
149 let is_are = if self.disabled.len() == 1 { "is" } else { "are" };
150 write!(f, " {} disabled", is_are)?;
151 }
152
153 Ok(())
50 } 154 }
51} 155}
diff --git a/crates/cfg/src/tests.rs b/crates/cfg/src/tests.rs
new file mode 100644
index 000000000..bd0f9ec48
--- /dev/null
+++ b/crates/cfg/src/tests.rs
@@ -0,0 +1,193 @@
1use expect_test::{expect, Expect};
2use mbe::ast_to_token_tree;
3use syntax::{ast, AstNode};
4
5use crate::{CfgAtom, CfgExpr, CfgOptions, DnfExpr};
6
7fn assert_parse_result(input: &str, expected: CfgExpr) {
8 let (tt, _) = {
9 let source_file = ast::SourceFile::parse(input).ok().unwrap();
10 let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap();
11 ast_to_token_tree(&tt).unwrap()
12 };
13 let cfg = CfgExpr::parse(&tt);
14 assert_eq!(cfg, expected);
15}
16
17fn check_dnf(input: &str, expect: Expect) {
18 let (tt, _) = {
19 let source_file = ast::SourceFile::parse(input).ok().unwrap();
20 let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap();
21 ast_to_token_tree(&tt).unwrap()
22 };
23 let cfg = CfgExpr::parse(&tt);
24 let actual = format!("#![cfg({})]", DnfExpr::new(cfg));
25 expect.assert_eq(&actual);
26}
27
28fn check_why_inactive(input: &str, opts: &CfgOptions, expect: Expect) {
29 let (tt, _) = {
30 let source_file = ast::SourceFile::parse(input).ok().unwrap();
31 let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap();
32 ast_to_token_tree(&tt).unwrap()
33 };
34 let cfg = CfgExpr::parse(&tt);
35 let dnf = DnfExpr::new(cfg);
36 let why_inactive = dnf.why_inactive(opts).unwrap().to_string();
37 expect.assert_eq(&why_inactive);
38}
39
40#[track_caller]
41fn check_enable_hints(input: &str, opts: &CfgOptions, expected_hints: &[&str]) {
42 let (tt, _) = {
43 let source_file = ast::SourceFile::parse(input).ok().unwrap();
44 let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap();
45 ast_to_token_tree(&tt).unwrap()
46 };
47 let cfg = CfgExpr::parse(&tt);
48 let dnf = DnfExpr::new(cfg);
49 let hints = dnf.compute_enable_hints(opts).map(|diff| diff.to_string()).collect::<Vec<_>>();
50 assert_eq!(hints, expected_hints);
51}
52
53#[test]
54fn test_cfg_expr_parser() {
55 assert_parse_result("#![cfg(foo)]", CfgAtom::Flag("foo".into()).into());
56 assert_parse_result("#![cfg(foo,)]", CfgAtom::Flag("foo".into()).into());
57 assert_parse_result(
58 "#![cfg(not(foo))]",
59 CfgExpr::Not(Box::new(CfgAtom::Flag("foo".into()).into())),
60 );
61 assert_parse_result("#![cfg(foo(bar))]", CfgExpr::Invalid);
62
63 // Only take the first
64 assert_parse_result(r#"#![cfg(foo, bar = "baz")]"#, CfgAtom::Flag("foo".into()).into());
65
66 assert_parse_result(
67 r#"#![cfg(all(foo, bar = "baz"))]"#,
68 CfgExpr::All(vec![
69 CfgAtom::Flag("foo".into()).into(),
70 CfgAtom::KeyValue { key: "bar".into(), value: "baz".into() }.into(),
71 ]),
72 );
73
74 assert_parse_result(
75 r#"#![cfg(any(not(), all(), , bar = "baz",))]"#,
76 CfgExpr::Any(vec![
77 CfgExpr::Not(Box::new(CfgExpr::Invalid)),
78 CfgExpr::All(vec![]),
79 CfgExpr::Invalid,
80 CfgAtom::KeyValue { key: "bar".into(), value: "baz".into() }.into(),
81 ]),
82 );
83}
84
85#[test]
86fn smoke() {
87 check_dnf("#![cfg(test)]", expect![[r#"#![cfg(test)]"#]]);
88 check_dnf("#![cfg(not(test))]", expect![[r#"#![cfg(not(test))]"#]]);
89 check_dnf("#![cfg(not(not(test)))]", expect![[r#"#![cfg(test)]"#]]);
90
91 check_dnf("#![cfg(all(a, b))]", expect![[r#"#![cfg(all(a, b))]"#]]);
92 check_dnf("#![cfg(any(a, b))]", expect![[r#"#![cfg(any(a, b))]"#]]);
93
94 check_dnf("#![cfg(not(a))]", expect![[r#"#![cfg(not(a))]"#]]);
95}
96
97#[test]
98fn distribute() {
99 check_dnf("#![cfg(all(any(a, b), c))]", expect![[r#"#![cfg(any(all(a, c), all(b, c)))]"#]]);
100 check_dnf("#![cfg(all(c, any(a, b)))]", expect![[r#"#![cfg(any(all(c, a), all(c, b)))]"#]]);
101 check_dnf(
102 "#![cfg(all(any(a, b), any(c, d)))]",
103 expect![[r#"#![cfg(any(all(a, c), all(a, d), all(b, c), all(b, d)))]"#]],
104 );
105
106 check_dnf(
107 "#![cfg(all(any(a, b, c), any(d, e, f), g))]",
108 expect![[
109 r#"#![cfg(any(all(a, d, g), all(a, e, g), all(a, f, g), all(b, d, g), all(b, e, g), all(b, f, g), all(c, d, g), all(c, e, g), all(c, f, g)))]"#
110 ]],
111 );
112}
113
114#[test]
115fn demorgan() {
116 check_dnf("#![cfg(not(all(a, b)))]", expect![[r#"#![cfg(any(not(a), not(b)))]"#]]);
117 check_dnf("#![cfg(not(any(a, b)))]", expect![[r#"#![cfg(all(not(a), not(b)))]"#]]);
118
119 check_dnf("#![cfg(not(all(not(a), b)))]", expect![[r#"#![cfg(any(a, not(b)))]"#]]);
120 check_dnf("#![cfg(not(any(a, not(b))))]", expect![[r#"#![cfg(all(not(a), b))]"#]]);
121}
122
123#[test]
124fn nested() {
125 check_dnf("#![cfg(all(any(a), not(all(any(b)))))]", expect![[r#"#![cfg(all(a, not(b)))]"#]]);
126
127 check_dnf("#![cfg(any(any(a, b)))]", expect![[r#"#![cfg(any(a, b))]"#]]);
128 check_dnf("#![cfg(not(any(any(a, b))))]", expect![[r#"#![cfg(all(not(a), not(b)))]"#]]);
129 check_dnf("#![cfg(all(all(a, b)))]", expect![[r#"#![cfg(all(a, b))]"#]]);
130 check_dnf("#![cfg(not(all(all(a, b))))]", expect![[r#"#![cfg(any(not(a), not(b)))]"#]]);
131}
132
133#[test]
134fn hints() {
135 let mut opts = CfgOptions::default();
136
137 check_enable_hints("#![cfg(test)]", &opts, &["enable test"]);
138 check_enable_hints("#![cfg(not(test))]", &opts, &[]);
139
140 check_enable_hints("#![cfg(any(a, b))]", &opts, &["enable a", "enable b"]);
141 check_enable_hints("#![cfg(any(b, a))]", &opts, &["enable b", "enable a"]);
142
143 check_enable_hints("#![cfg(all(a, b))]", &opts, &["enable a and b"]);
144
145 opts.insert_atom("test".into());
146
147 check_enable_hints("#![cfg(test)]", &opts, &[]);
148 check_enable_hints("#![cfg(not(test))]", &opts, &["disable test"]);
149}
150
151/// Tests that we don't suggest hints for cfgs that express an inconsistent formula.
152#[test]
153fn hints_impossible() {
154 let mut opts = CfgOptions::default();
155
156 check_enable_hints("#![cfg(all(test, not(test)))]", &opts, &[]);
157
158 opts.insert_atom("test".into());
159
160 check_enable_hints("#![cfg(all(test, not(test)))]", &opts, &[]);
161}
162
163#[test]
164fn why_inactive() {
165 let mut opts = CfgOptions::default();
166 opts.insert_atom("test".into());
167 opts.insert_atom("test2".into());
168
169 check_why_inactive("#![cfg(a)]", &opts, expect![["a is disabled"]]);
170 check_why_inactive("#![cfg(not(test))]", &opts, expect![["test is enabled"]]);
171
172 check_why_inactive(
173 "#![cfg(all(not(test), not(test2)))]",
174 &opts,
175 expect![["test and test2 are enabled"]],
176 );
177 check_why_inactive("#![cfg(all(a, b))]", &opts, expect![["a and b are disabled"]]);
178 check_why_inactive(
179 "#![cfg(all(not(test), a))]",
180 &opts,
181 expect![["test is enabled and a is disabled"]],
182 );
183 check_why_inactive(
184 "#![cfg(all(not(test), test2, a))]",
185 &opts,
186 expect![["test is enabled and a is disabled"]],
187 );
188 check_why_inactive(
189 "#![cfg(all(not(test), not(test2), a))]",
190 &opts,
191 expect![["test and test2 are enabled and a is disabled"]],
192 );
193}