1 // Copyright © SixtyFPS GmbH <[email protected]>
2 // SPDX-License-Identifier: MIT OR Apache-2.0
3
4 /*!
5 Document your crate's feature flags.
6
7 This crates provides a macro that extracts "documentation" comments from Cargo.toml
8
9 To use this crate, add `#![doc = document_features::document_features!()]` in your crate documentation.
10 The `document_features!()` macro reads your `Cargo.toml` file, extracts feature comments and generates
11 a markdown string for your documentation.
12
13 Basic example:
14
15 ```rust
16 //! Normal crate documentation goes here.
17 //!
18 //! ## Feature flags
19 #![doc = document_features::document_features!()]
20
21 // rest of the crate goes here.
22 ```
23
24 ## Documentation format:
25
26 The documentation of your crate features goes into `Cargo.toml`, where they are defined.
27
28 The `document_features!()` macro analyzes the contents of `Cargo.toml`.
29 Similar to Rust's documentation comments `///` and `//!`, the macro understands
30 comments that start with `## ` and `#! `. Note the required trailing space.
31 Lines starting with `###` will not be understood as doc comment.
32
33 `## ` comments are meant to be *above* the feature they document.
34 There can be several `## ` comments, but they must always be followed by a
35 feature name or an optional dependency.
36 There should not be `#! ` comments between the comment and the feature they document.
37
38 `#! ` comments are not associated with a particular feature, and will be printed
39 in where they occur. Use them to group features, for example.
40
41 ## Examples:
42
43 */
44 #![doc = self_test!(/**
45 [package]
46 name = "..."
47 # ...
48
49 [features]
50 default = ["foo"]
51 #! This comments goes on top
52
53 ## The foo feature enables the `foo` functions
54 foo = []
55
56 ## The bar feature enables the bar module
57 bar = []
58
59 #! ### Experimental features
60 #! The following features are experimental
61
62 ## Enable the fusion reactor
63 ##
64 ## ⚠️ Can lead to explosions
65 fusion = []
66
67 [dependencies]
68 document-features = "0.2"
69
70 #! ### Optional dependencies
71
72 ## Enable this feature to implement the trait for the types from the genial crate
73 genial = { version = "0.2", optional = true }
74
75 ## This awesome dependency is specified in its own table
76 [dependencies.awesome]
77 version = "1.3.5"
78 optional = true
79 */
80 =>
81 /**
82 This comments goes on top
83 * **`foo`** *(enabled by default)* — The foo feature enables the `foo` functions
84 * **`bar`** — The bar feature enables the bar module
85
86 #### Experimental features
87 The following features are experimental
88 * **`fusion`** — Enable the fusion reactor
89
90 ⚠️ Can lead to explosions
91
92 #### Optional dependencies
93 * **`genial`** — Enable this feature to implement the trait for the types from the genial crate
94 * **`awesome`** — This awesome dependency is specified in its own table
95 */
96 )]
97 /*!
98
99 ## Customization
100
101 You can customize the formatting of the features in the generated documentation by setting
102 the key **`feature_label=`** to a given format string. This format string must be either
103 a [string literal](https://doc.rust-lang.org/reference/tokens.html#string-literals) or
104 a [raw string literal](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals).
105 Every occurrence of `{feature}` inside the format string will be substituted with the name of the feature.
106
107 For instance, to emulate the HTML formatting used by `rustdoc` one can use the following:
108
109 ```rust
110 #![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
111 ```
112
113 The default formatting is equivalent to:
114
115 ```rust
116 #![doc = document_features::document_features!(feature_label = "**`{feature}`**")]
117 ```
118
119 ## Compatibility
120
121 The minimum Rust version required to use this crate is Rust 1.54 because of the
122 feature to have macro in doc comments. You can make this crate optional and use
123 `#[cfg_attr()]` statements to enable it only when building the documentation:
124 You need to have two levels of `cfg_attr` because Rust < 1.54 doesn't parse the attribute
125 otherwise.
126
127 ```rust,ignore
128 #![cfg_attr(
129 feature = "document-features",
130 cfg_attr(doc, doc = ::document_features::document_features!())
131 )]
132 ```
133
134 In your Cargo.toml, enable this feature while generating the documentation on docs.rs:
135
136 ```toml
137 [dependencies]
138 document-features = { version = "0.2", optional = true }
139
140 [package.metadata.docs.rs]
141 features = ["document-features"]
142 ## Alternative: enable all features so they are all documented
143 ## all-features = true
144 ```
145 */
146
147 #[cfg(not(feature = "default"))]
148 compile_error!(
149 "The feature `default` must be enabled to ensure \
150 forward compatibility with future version of this crate"
151 );
152
153 extern crate proc_macro;
154
155 use proc_macro::{TokenStream, TokenTree};
156 use std::borrow::Cow;
157 use std::collections::HashSet;
158 use std::convert::TryFrom;
159 use std::fmt::Write;
160 use std::path::Path;
161 use std::str::FromStr;
162
error(e: &str) -> TokenStream163 fn error(e: &str) -> TokenStream {
164 TokenStream::from_str(&format!("::core::compile_error!{{\"{}\"}}", e.escape_default())).unwrap()
165 }
166
compile_error(msg: &str, tt: Option<TokenTree>) -> TokenStream167 fn compile_error(msg: &str, tt: Option<TokenTree>) -> TokenStream {
168 let span = tt.as_ref().map_or_else(proc_macro::Span::call_site, TokenTree::span);
169 use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing};
170 use std::iter::FromIterator;
171 TokenStream::from_iter(vec![
172 TokenTree::Ident(Ident::new("compile_error", span)),
173 TokenTree::Punct({
174 let mut punct = Punct::new('!', Spacing::Alone);
175 punct.set_span(span);
176 punct
177 }),
178 TokenTree::Group({
179 let mut group = Group::new(Delimiter::Brace, {
180 TokenStream::from_iter([TokenTree::Literal({
181 let mut string = Literal::string(msg);
182 string.set_span(span);
183 string
184 })])
185 });
186 group.set_span(span);
187 group
188 }),
189 ])
190 }
191
192 #[derive(Default)]
193 struct Args {
194 feature_label: Option<String>,
195 }
196
parse_args(input: TokenStream) -> Result<Args, TokenStream>197 fn parse_args(input: TokenStream) -> Result<Args, TokenStream> {
198 let mut token_trees = input.into_iter().fuse();
199
200 // parse the key, ensuring that it is the identifier `feature_label`
201 match token_trees.next() {
202 None => return Ok(Args::default()),
203 Some(TokenTree::Ident(ident)) if ident.to_string() == "feature_label" => (),
204 tt => return Err(compile_error("expected `feature_label`", tt)),
205 }
206
207 // parse a single equal sign `=`
208 match token_trees.next() {
209 Some(TokenTree::Punct(p)) if p.as_char() == '=' => (),
210 tt => return Err(compile_error("expected `=`", tt)),
211 }
212
213 // parse the value, ensuring that it is a string literal containing the substring `"{feature}"`
214 let feature_label;
215 if let Some(tt) = token_trees.next() {
216 match litrs::StringLit::<String>::try_from(&tt) {
217 Ok(string_lit) if string_lit.value().contains("{feature}") => {
218 feature_label = string_lit.value().to_string()
219 }
220 _ => {
221 return Err(compile_error(
222 "expected a string literal containing the substring \"{feature}\"",
223 Some(tt),
224 ))
225 }
226 }
227 } else {
228 return Err(compile_error(
229 "expected a string literal containing the substring \"{feature}\"",
230 None,
231 ));
232 }
233
234 // ensure there is nothing left after the format string
235 if let tt @ Some(_) = token_trees.next() {
236 return Err(compile_error("unexpected token after the format string", tt));
237 }
238
239 Ok(Args { feature_label: Some(feature_label) })
240 }
241
242 /// Produce a literal string containing documentation extracted from Cargo.toml
243 ///
244 /// See the [crate] documentation for details
245 #[proc_macro]
document_features(tokens: TokenStream) -> TokenStream246 pub fn document_features(tokens: TokenStream) -> TokenStream {
247 parse_args(tokens)
248 .and_then(|args| document_features_impl(&args))
249 .unwrap_or_else(std::convert::identity)
250 }
251
document_features_impl(args: &Args) -> Result<TokenStream, TokenStream>252 fn document_features_impl(args: &Args) -> Result<TokenStream, TokenStream> {
253 let path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
254 let mut cargo_toml = std::fs::read_to_string(Path::new(&path).join("Cargo.toml"))
255 .map_err(|e| error(&format!("Can't open Cargo.toml: {:?}", e)))?;
256
257 if !has_doc_comments(&cargo_toml) {
258 // On crates.io, Cargo.toml is usually "normalized" and stripped of all comments.
259 // The original Cargo.toml has been renamed Cargo.toml.orig
260 if let Ok(orig) = std::fs::read_to_string(Path::new(&path).join("Cargo.toml.orig")) {
261 if has_doc_comments(&orig) {
262 cargo_toml = orig;
263 }
264 }
265 }
266
267 let result = process_toml(&cargo_toml, args).map_err(|e| error(&e))?;
268 Ok(std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&result))).collect())
269 }
270
271 /// Check if the Cargo.toml has comments that looks like doc comments.
has_doc_comments(cargo_toml: &str) -> bool272 fn has_doc_comments(cargo_toml: &str) -> bool {
273 let mut lines = cargo_toml.lines().map(str::trim);
274 while let Some(line) = lines.next() {
275 if line.starts_with("## ") || line.starts_with("#! ") {
276 return true;
277 }
278 let before_coment = line.split_once('#').map_or(line, |(before, _)| before);
279 if line.starts_with("#") {
280 continue;
281 }
282 if let Some((_, mut quote)) = before_coment.split_once("\"\"\"") {
283 loop {
284 // skip slashes.
285 if let Some((_, s)) = quote.split_once('\\') {
286 quote = s.strip_prefix('\\').or_else(|| s.strip_prefix('"')).unwrap_or(s);
287 continue;
288 }
289 // skip quotes.
290 if let Some((_, out_quote)) = quote.split_once("\"\"\"") {
291 let out_quote = out_quote.trim_start_matches('"');
292 let out_quote =
293 out_quote.split_once('#').map_or(out_quote, |(before, _)| before);
294 if let Some((_, q)) = out_quote.split_once("\"\"\"") {
295 quote = q;
296 continue;
297 }
298 break;
299 };
300 match lines.next() {
301 Some(l) => quote = l,
302 None => return false,
303 }
304 }
305 }
306 }
307 false
308 }
309
310 #[test]
test_has_doc_coment()311 fn test_has_doc_coment() {
312 assert!(has_doc_comments("foo\nbar\n## comment\nddd"));
313 assert!(!has_doc_comments("foo\nbar\n#comment\nddd"));
314 assert!(!has_doc_comments(
315 r#"
316 [[package.metadata.release.pre-release-replacements]]
317 exactly = 1 # not a doc comment
318 file = "CHANGELOG.md"
319 replace = """
320 <!-- next-header -->
321 ## [Unreleased] - ReleaseDate
322 """
323 search = "<!-- next-header -->"
324 array = ["""foo""", """
325 bar""", """eee
326 ## not a comment
327 """]
328 "#
329 ));
330 assert!(has_doc_comments(
331 r#"
332 [[package.metadata.release.pre-release-replacements]]
333 exactly = 1 # """
334 file = "CHANGELOG.md"
335 replace = """
336 <!-- next-header -->
337 ## [Unreleased] - ReleaseDate
338 """
339 search = "<!-- next-header -->"
340 array = ["""foo""", """
341 bar""", """eee
342 ## not a comment
343 """]
344 ## This is a comment
345 feature = "45"
346 "#
347 ));
348
349 assert!(!has_doc_comments(
350 r#"
351 [[package.metadata.release.pre-release-replacements]]
352 value = """" string \"""
353 ## within the string
354 \""""
355 another_string = """"" # """
356 ## also within"""
357 "#
358 ));
359
360 assert!(has_doc_comments(
361 r#"
362 [[package.metadata.release.pre-release-replacements]]
363 value = """" string \"""
364 ## within the string
365 \""""
366 another_string = """"" # """
367 ## also within"""
368 ## out of the string
369 foo = bar
370 "#
371 ));
372 }
373
process_toml(cargo_toml: &str, args: &Args) -> Result<String, String>374 fn process_toml(cargo_toml: &str, args: &Args) -> Result<String, String> {
375 // Get all lines between the "[features]" and the next block
376 let mut lines = cargo_toml
377 .lines()
378 .map(str::trim)
379 // and skip empty lines and comments that are not docs comments
380 .filter(|l| {
381 !l.is_empty() && (!l.starts_with('#') || l.starts_with("##") || l.starts_with("#!"))
382 });
383 let mut top_comment = String::new();
384 let mut current_comment = String::new();
385 let mut features = vec![];
386 let mut default_features = HashSet::new();
387 let mut current_table = "";
388 while let Some(line) = lines.next() {
389 if let Some(x) = line.strip_prefix("#!") {
390 if !x.is_empty() && !x.starts_with(' ') {
391 continue; // it's not a doc comment
392 }
393 if !current_comment.is_empty() {
394 return Err("Cannot mix ## and #! comments between features.".into());
395 }
396 if top_comment.is_empty() && !features.is_empty() {
397 top_comment = "\n".into();
398 }
399 writeln!(top_comment, "{}", x).unwrap();
400 } else if let Some(x) = line.strip_prefix("##") {
401 if !x.is_empty() && !x.starts_with(' ') {
402 continue; // it's not a doc comment
403 }
404 writeln!(current_comment, " {}", x).unwrap();
405 } else if let Some(table) = line.strip_prefix('[') {
406 current_table = table
407 .split_once(']')
408 .map(|(t, _)| t.trim())
409 .ok_or_else(|| format!("Parse error while parsing line: {}", line))?;
410 if !current_comment.is_empty() {
411 let dep = current_table
412 .rsplit_once('.')
413 .and_then(|(table, dep)| table.trim().ends_with("dependencies").then(|| dep))
414 .ok_or_else(|| format!("Not a feature: `{}`", line))?;
415 features.push((
416 dep.trim(),
417 std::mem::take(&mut top_comment),
418 std::mem::take(&mut current_comment),
419 ));
420 }
421 } else if let Some((dep, rest)) = line.split_once('=') {
422 let dep = dep.trim().trim_matches('"');
423 let rest = get_balanced(rest, &mut lines)
424 .map_err(|e| format!("Parse error while parsing value {}: {}", dep, e))?;
425 if current_table == "features" && dep == "default" {
426 let defaults = rest
427 .trim()
428 .strip_prefix('[')
429 .and_then(|r| r.strip_suffix(']'))
430 .ok_or_else(|| format!("Parse error while parsing dependency {}", dep))?
431 .split(',')
432 .map(|d| d.trim().trim_matches(|c| c == '"' || c == '\'').trim().to_string())
433 .filter(|d| !d.is_empty());
434 default_features.extend(defaults);
435 }
436 if !current_comment.is_empty() {
437 if current_table.ends_with("dependencies") {
438 if !rest
439 .split_once("optional")
440 .and_then(|(_, r)| r.trim().strip_prefix('='))
441 .map_or(false, |r| r.trim().starts_with("true"))
442 {
443 return Err(format!("Dependency {} is not an optional dependency", dep));
444 }
445 } else if current_table != "features" {
446 return Err(format!(
447 r#"Comment cannot be associated with a feature: "{}""#,
448 current_comment.trim()
449 ));
450 }
451 features.push((
452 dep,
453 std::mem::take(&mut top_comment),
454 std::mem::take(&mut current_comment),
455 ));
456 }
457 }
458 }
459 if !current_comment.is_empty() {
460 return Err("Found comment not associated with a feature".into());
461 }
462 if features.is_empty() {
463 return Ok("*No documented features in Cargo.toml*".into());
464 }
465 let mut result = String::new();
466 for (f, top, comment) in features {
467 let default = if default_features.contains(f) { " *(enabled by default)*" } else { "" };
468 if !comment.trim().is_empty() {
469 if let Some(feature_label) = &args.feature_label {
470 writeln!(
471 result,
472 "{}* {}{} —{}",
473 top,
474 feature_label.replace("{feature}", f),
475 default,
476 comment.trim_end(),
477 )
478 .unwrap();
479 } else {
480 writeln!(result, "{}* **`{}`**{} —{}", top, f, default, comment.trim_end())
481 .unwrap();
482 }
483 } else if let Some(feature_label) = &args.feature_label {
484 writeln!(result, "{}* {}{}", top, feature_label.replace("{feature}", f), default,)
485 .unwrap();
486 } else {
487 writeln!(result, "{}* **`{}`**{}", top, f, default).unwrap();
488 }
489 }
490 result += &top_comment;
491 Ok(result)
492 }
493
get_balanced<'a>( first_line: &'a str, lines: &mut impl Iterator<Item = &'a str>, ) -> Result<Cow<'a, str>, String>494 fn get_balanced<'a>(
495 first_line: &'a str,
496 lines: &mut impl Iterator<Item = &'a str>,
497 ) -> Result<Cow<'a, str>, String> {
498 let mut line = first_line;
499 let mut result = Cow::from("");
500
501 let mut in_quote = false;
502 let mut level = 0;
503 loop {
504 let mut last_slash = false;
505 for (idx, b) in line.as_bytes().iter().enumerate() {
506 if last_slash {
507 last_slash = false
508 } else if in_quote {
509 match b {
510 b'\\' => last_slash = true,
511 b'"' | b'\'' => in_quote = false,
512 _ => (),
513 }
514 } else {
515 match b {
516 b'\\' => last_slash = true,
517 b'"' => in_quote = true,
518 b'{' | b'[' => level += 1,
519 b'}' | b']' if level == 0 => return Err("unbalanced source".into()),
520 b'}' | b']' => level -= 1,
521 b'#' => {
522 line = &line[..idx];
523 break;
524 }
525 _ => (),
526 }
527 }
528 }
529 if result.len() == 0 {
530 result = Cow::from(line);
531 } else {
532 *result.to_mut() += line;
533 }
534 if level == 0 {
535 return Ok(result);
536 }
537 line = if let Some(l) = lines.next() {
538 l
539 } else {
540 return Err("unbalanced source".into());
541 };
542 }
543 }
544
545 #[test]
test_get_balanced()546 fn test_get_balanced() {
547 assert_eq!(
548 get_balanced(
549 "{",
550 &mut IntoIterator::into_iter(["a", "{ abc[], #ignore", " def }", "}", "xxx"])
551 ),
552 Ok("{a{ abc[], def }}".into())
553 );
554 assert_eq!(
555 get_balanced("{ foo = \"{#\" } #ignore", &mut IntoIterator::into_iter(["xxx"])),
556 Ok("{ foo = \"{#\" } ".into())
557 );
558 assert_eq!(
559 get_balanced("]", &mut IntoIterator::into_iter(["["])),
560 Err("unbalanced source".into())
561 );
562 }
563
564 #[cfg(feature = "self-test")]
565 #[proc_macro]
566 #[doc(hidden)]
567 /// Helper macro for the tests. Do not use
self_test_helper(input: TokenStream) -> TokenStream568 pub fn self_test_helper(input: TokenStream) -> TokenStream {
569 let mut code = String::new();
570 for line in (&input).to_string().trim_matches(|c| c == '"' || c == '#').lines() {
571 // Rustdoc removes the lines that starts with `# ` and removes one `#` from lines that starts with # followed by space.
572 // We need to re-add the `#` that was removed by rustdoc to get the original.
573 if line.strip_prefix('#').map_or(false, |x| x.is_empty() || x.starts_with(' ')) {
574 code += "#";
575 }
576 code += line;
577 code += "\n";
578 }
579 process_toml(&code, &Args::default()).map_or_else(
580 |e| error(&e),
581 |r| std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&r))).collect(),
582 )
583 }
584
585 #[cfg(feature = "self-test")]
586 macro_rules! self_test {
587 (#[doc = $toml:literal] => #[doc = $md:literal]) => {
588 concat!(
589 "\n`````rust\n\
590 fn normalize_md(md : &str) -> String {
591 md.lines().skip_while(|l| l.is_empty()).map(|l| l.trim())
592 .collect::<Vec<_>>().join(\"\\n\")
593 }
594 assert_eq!(normalize_md(document_features::self_test_helper!(",
595 stringify!($toml),
596 ")), normalize_md(",
597 stringify!($md),
598 "));\n`````\n\n"
599 )
600 };
601 }
602
603 #[cfg(not(feature = "self-test"))]
604 macro_rules! self_test {
605 (#[doc = $toml:literal] => #[doc = $md:literal]) => {
606 concat!(
607 "This contents in Cargo.toml:\n`````toml",
608 $toml,
609 "\n`````\n Generates the following:\n\
610 <table><tr><th>Preview</th></tr><tr><td>\n\n",
611 $md,
612 "\n</td></tr></table>\n\n \n",
613 )
614 };
615 }
616
617 use self_test;
618
619 // The following struct is inserted only during generation of the documentation in order to exploit doc-tests.
620 // These doc-tests are used to check that invalid arguments to the `document_features!` macro cause a compile time error.
621 // For a more principled way of testing compilation error, maybe investigate <https://docs.rs/trybuild>.
622 //
623 /// ```rust
624 /// #![doc = document_features::document_features!()]
625 /// #![doc = document_features::document_features!(feature_label = "**`{feature}`**")]
626 /// #![doc = document_features::document_features!(feature_label = r"**`{feature}`**")]
627 /// #![doc = document_features::document_features!(feature_label = r#"**`{feature}`**"#)]
628 /// #![doc = document_features::document_features!(feature_label = "<span class=\"stab portability\"><code>{feature}</code></span>")]
629 /// #![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
630 /// ```
631 /// ```compile_fail
632 /// #![doc = document_features::document_features!(feature_label > "<span>{feature}</span>")]
633 /// ```
634 /// ```compile_fail
635 /// #![doc = document_features::document_features!(label = "<span>{feature}</span>")]
636 /// ```
637 /// ```compile_fail
638 /// #![doc = document_features::document_features!(feature_label = "{feat}")]
639 /// ```
640 /// ```compile_fail
641 /// #![doc = document_features::document_features!(feature_label = 3.14)]
642 /// ```
643 /// ```compile_fail
644 /// #![doc = document_features::document_features!(feature_label = )]
645 /// ```
646 /// ```compile_fail
647 /// #![doc = document_features::document_features!(feature_label = "**`{feature}`**" extra)]
648 /// ```
649 #[cfg(doc)]
650 struct FeatureLabelCompilationTest;
651
652 #[cfg(test)]
653 mod tests {
654 use super::{process_toml, Args};
655
656 #[track_caller]
test_error(toml: &str, expected: &str)657 fn test_error(toml: &str, expected: &str) {
658 let err = process_toml(toml, &Args::default()).unwrap_err();
659 assert!(err.contains(expected), "{:?} does not contain {:?}", err, expected)
660 }
661
662 #[test]
only_get_balanced_in_correct_table()663 fn only_get_balanced_in_correct_table() {
664 process_toml(
665 r#"
666
667 [package.metadata.release]
668 pre-release-replacements = [
669 {test=\"\#\# \"},
670 ]
671 [abcd]
672 [features]#xyz
673 #! abc
674 #
675 ###
676 #! def
677 #!
678 ## 123
679 ## 456
680 feat1 = ["plop"]
681 #! ghi
682 no_doc = []
683 ##
684 feat2 = ["momo"]
685 #! klm
686 default = ["feat1", "something_else"]
687 #! end
688 "#,
689 &Args::default(),
690 )
691 .unwrap();
692 }
693
694 #[test]
no_features()695 fn no_features() {
696 let r = process_toml(
697 r#"
698 [features]
699 [dependencies]
700 foo = 4;
701 "#,
702 &Args::default(),
703 )
704 .unwrap();
705 assert_eq!(r, "*No documented features in Cargo.toml*");
706 }
707
708 #[test]
no_features2()709 fn no_features2() {
710 let r = process_toml(
711 r#"
712 [packages]
713 [dependencies]
714 "#,
715 &Args::default(),
716 )
717 .unwrap();
718 assert_eq!(r, "*No documented features in Cargo.toml*");
719 }
720
721 #[test]
parse_error3()722 fn parse_error3() {
723 test_error(
724 r#"
725 [features]
726 ff = []
727 [abcd
728 efgh
729 [dependencies]
730 "#,
731 "Parse error while parsing line: [abcd",
732 );
733 }
734
735 #[test]
parse_error4()736 fn parse_error4() {
737 test_error(
738 r#"
739 [features]
740 ## dd
741 ## ff
742 #! ee
743 ## ff
744 "#,
745 "Cannot mix",
746 );
747 }
748
749 #[test]
parse_error5()750 fn parse_error5() {
751 test_error(
752 r#"
753 [features]
754 ## dd
755 "#,
756 "not associated with a feature",
757 );
758 }
759
760 #[test]
parse_error6()761 fn parse_error6() {
762 test_error(
763 r#"
764 [features]
765 # ff
766 foo = []
767 default = [
768 #ffff
769 # ff
770 "#,
771 "Parse error while parsing value default",
772 );
773 }
774
775 #[test]
parse_error7()776 fn parse_error7() {
777 test_error(
778 r#"
779 [features]
780 # f
781 foo = [ x = { ]
782 bar = []
783 "#,
784 "Parse error while parsing value foo",
785 );
786 }
787
788 #[test]
not_a_feature1()789 fn not_a_feature1() {
790 test_error(
791 r#"
792 ## hallo
793 [features]
794 "#,
795 "Not a feature: `[features]`",
796 );
797 }
798
799 #[test]
not_a_feature2()800 fn not_a_feature2() {
801 test_error(
802 r#"
803 [package]
804 ## hallo
805 foo = []
806 "#,
807 "Comment cannot be associated with a feature: \"hallo\"",
808 );
809 }
810
811 #[test]
non_optional_dep1()812 fn non_optional_dep1() {
813 test_error(
814 r#"
815 [dev-dependencies]
816 ## Not optional
817 foo = { version = "1.2", optional = false }
818 "#,
819 "Dependency foo is not an optional dependency",
820 );
821 }
822
823 #[test]
non_optional_dep2()824 fn non_optional_dep2() {
825 test_error(
826 r#"
827 [dev-dependencies]
828 ## Not optional
829 foo = { version = "1.2" }
830 "#,
831 "Dependency foo is not an optional dependency",
832 );
833 }
834
835 #[test]
basic()836 fn basic() {
837 let toml = r#"
838 [abcd]
839 [features]#xyz
840 #! abc
841 #
842 ###
843 #! def
844 #!
845 ## 123
846 ## 456
847 feat1 = ["plop"]
848 #! ghi
849 no_doc = []
850 ##
851 feat2 = ["momo"]
852 #! klm
853 default = ["feat1", "something_else"]
854 #! end
855 "#;
856 let parsed = process_toml(toml, &Args::default()).unwrap();
857 assert_eq!(
858 parsed,
859 " abc\n def\n\n* **`feat1`** *(enabled by default)* — 123\n 456\n\n ghi\n* **`feat2`**\n\n klm\n end\n"
860 );
861 let parsed = process_toml(
862 toml,
863 &Args {
864 feature_label: Some(
865 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
866 ),
867 },
868 )
869 .unwrap();
870 assert_eq!(
871 parsed,
872 " abc\n def\n\n* <span class=\"stab portability\"><code>feat1</code></span> *(enabled by default)* — 123\n 456\n\n ghi\n* <span class=\"stab portability\"><code>feat2</code></span>\n\n klm\n end\n"
873 );
874 }
875
876 #[test]
dependencies()877 fn dependencies() {
878 let toml = r#"
879 #! top
880 [dev-dependencies] #yo
881 ## dep1
882 dep1 = { version="1.2", optional=true}
883 #! yo
884 dep2 = "1.3"
885 ## dep3
886 [target.'cfg(unix)'.build-dependencies.dep3]
887 version = "42"
888 optional = true
889 "#;
890 let parsed = process_toml(toml, &Args::default()).unwrap();
891 assert_eq!(parsed, " top\n* **`dep1`** — dep1\n\n yo\n* **`dep3`** — dep3\n");
892 let parsed = process_toml(
893 toml,
894 &Args {
895 feature_label: Some(
896 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
897 ),
898 },
899 )
900 .unwrap();
901 assert_eq!(parsed, " top\n* <span class=\"stab portability\"><code>dep1</code></span> — dep1\n\n yo\n* <span class=\"stab portability\"><code>dep3</code></span> — dep3\n");
902 }
903
904 #[test]
multi_lines()905 fn multi_lines() {
906 let toml = r#"
907 [package.metadata.foo]
908 ixyz = [
909 ["array"],
910 [
911 "of",
912 "arrays"
913 ]
914 ]
915 [dev-dependencies]
916 ## dep1
917 dep1 = {
918 version="1.2-}",
919 optional=true
920 }
921 [features]
922 default = [
923 "goo",
924 "\"]",
925 "bar",
926 ]
927 ## foo
928 foo = [
929 "bar"
930 ]
931 ## bar
932 bar = [
933
934 ]
935 "#;
936 let parsed = process_toml(toml, &Args::default()).unwrap();
937 assert_eq!(
938 parsed,
939 "* **`dep1`** — dep1\n* **`foo`** — foo\n* **`bar`** *(enabled by default)* — bar\n"
940 );
941 let parsed = process_toml(
942 toml,
943 &Args {
944 feature_label: Some(
945 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
946 ),
947 },
948 )
949 .unwrap();
950 assert_eq!(
951 parsed,
952 "* <span class=\"stab portability\"><code>dep1</code></span> — dep1\n* <span class=\"stab portability\"><code>foo</code></span> — foo\n* <span class=\"stab portability\"><code>bar</code></span> *(enabled by default)* — bar\n"
953 );
954 }
955
956 #[test]
dots_in_feature()957 fn dots_in_feature() {
958 let toml = r#"
959 [features]
960 ## This is a test
961 "teßt." = []
962 default = ["teßt."]
963 [dependencies]
964 ## A dep
965 "dep" = { version = "123", optional = true }
966 "#;
967 let parsed = process_toml(toml, &Args::default()).unwrap();
968 assert_eq!(
969 parsed,
970 "* **`teßt.`** *(enabled by default)* — This is a test\n* **`dep`** — A dep\n"
971 );
972 let parsed = process_toml(
973 toml,
974 &Args {
975 feature_label: Some(
976 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
977 ),
978 },
979 )
980 .unwrap();
981 assert_eq!(
982 parsed,
983 "* <span class=\"stab portability\"><code>teßt.</code></span> *(enabled by default)* — This is a test\n* <span class=\"stab portability\"><code>dep</code></span> — A dep\n"
984 );
985 }
986 }
987