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&nbsp;\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