1#![doc = self_test!(=>
83 )]
102#[cfg(not(feature = "default"))]
153compile_error!(
154 "The feature `default` must be enabled to ensure \
155 forward compatibility with future version of this crate"
156);
157
158extern crate proc_macro;
159
160use proc_macro::{TokenStream, TokenTree};
161use std::borrow::Cow;
162use std::collections::HashSet;
163use std::convert::TryFrom;
164use std::fmt::Write;
165use std::path::Path;
166use std::str::FromStr;
167
168fn error(e: &str) -> TokenStream {
169 TokenStream::from_str(&format!("::core::compile_error!{{\"{}\"}}", e.escape_default())).unwrap()
170}
171
172fn compile_error(msg: &str, tt: Option<TokenTree>) -> TokenStream {
173 let span = tt.as_ref().map_or_else(proc_macro::Span::call_site, TokenTree::span);
174 use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing};
175 use std::iter::FromIterator;
176 TokenStream::from_iter(vec![
177 TokenTree::Ident(Ident::new("compile_error", span)),
178 TokenTree::Punct({
179 let mut punct = Punct::new('!', Spacing::Alone);
180 punct.set_span(span);
181 punct
182 }),
183 TokenTree::Group({
184 let mut group = Group::new(Delimiter::Brace, {
185 TokenStream::from_iter([TokenTree::Literal({
186 let mut string = Literal::string(msg);
187 string.set_span(span);
188 string
189 })])
190 });
191 group.set_span(span);
192 group
193 }),
194 ])
195}
196
197#[derive(Default)]
198struct Args {
199 feature_label: Option<String>,
200}
201
202fn parse_args(input: TokenStream) -> Result<Args, TokenStream> {
203 let mut token_trees = input.into_iter().fuse();
204
205 match token_trees.next() {
207 None => return Ok(Args::default()),
208 Some(TokenTree::Ident(ident)) if ident.to_string() == "feature_label" => (),
209 tt => return Err(compile_error("expected `feature_label`", tt)),
210 }
211
212 match token_trees.next() {
214 Some(TokenTree::Punct(p)) if p.as_char() == '=' => (),
215 tt => return Err(compile_error("expected `=`", tt)),
216 }
217
218 let feature_label;
220 if let Some(tt) = token_trees.next() {
221 match litrs::StringLit::<String>::try_from(&tt) {
222 Ok(string_lit) if string_lit.value().contains("{feature}") => {
223 feature_label = string_lit.value().to_string()
224 }
225 _ => {
226 return Err(compile_error(
227 "expected a string literal containing the substring \"{feature}\"",
228 Some(tt),
229 ))
230 }
231 }
232 } else {
233 return Err(compile_error(
234 "expected a string literal containing the substring \"{feature}\"",
235 None,
236 ));
237 }
238
239 if let tt @ Some(_) = token_trees.next() {
241 return Err(compile_error("unexpected token after the format string", tt));
242 }
243
244 Ok(Args { feature_label: Some(feature_label) })
245}
246
247#[proc_macro]
251pub fn document_features(tokens: TokenStream) -> TokenStream {
252 parse_args(tokens)
253 .and_then(|args| document_features_impl(&args))
254 .unwrap_or_else(std::convert::identity)
255}
256
257fn document_features_impl(args: &Args) -> Result<TokenStream, TokenStream> {
258 let path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
259 let mut cargo_toml = std::fs::read_to_string(Path::new(&path).join("Cargo.toml"))
260 .map_err(|e| error(&format!("Can't open Cargo.toml: {:?}", e)))?;
261
262 if !cargo_toml.contains("\n##") && !cargo_toml.contains("\n#!") {
263 if let Ok(orig) = std::fs::read_to_string(Path::new(&path).join("Cargo.toml.orig")) {
266 if orig.contains("##") || orig.contains("#!") {
267 cargo_toml = orig;
268 }
269 }
270 }
271
272 let result = process_toml(&cargo_toml, args).map_err(|e| error(&e))?;
273 Ok(std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&result))).collect())
274}
275
276fn process_toml(cargo_toml: &str, args: &Args) -> Result<String, String> {
277 let mut lines = cargo_toml
279 .lines()
280 .map(str::trim)
281 .filter(|l| {
283 !l.is_empty() && (!l.starts_with("#") || l.starts_with("##") || l.starts_with("#!"))
284 });
285 let mut top_comment = String::new();
286 let mut current_comment = String::new();
287 let mut features = vec![];
288 let mut default_features = HashSet::new();
289 let mut current_table = "";
290 while let Some(line) = lines.next() {
291 if let Some(x) = line.strip_prefix("#!") {
292 if !x.is_empty() && !x.starts_with(" ") {
293 continue; }
295 if !current_comment.is_empty() {
296 return Err("Cannot mix ## and #! comments between features.".into());
297 }
298 writeln!(top_comment, "{}", x).unwrap();
299 } else if let Some(x) = line.strip_prefix("##") {
300 if !x.is_empty() && !x.starts_with(" ") {
301 continue; }
303 writeln!(current_comment, " {}", x).unwrap();
304 } else if let Some(table) = line.strip_prefix("[") {
305 current_table = table
306 .split_once("]")
307 .map(|(t, _)| t.trim())
308 .ok_or_else(|| format!("Parse error while parsing line: {}", line))?;
309 if !current_comment.is_empty() {
310 let dep = current_table
311 .rsplit_once(".")
312 .and_then(|(table, dep)| table.trim().ends_with("dependencies").then(|| dep))
313 .ok_or_else(|| format!("Not a feature: `{}`", line))?;
314 features.push((
315 dep.trim(),
316 std::mem::take(&mut top_comment),
317 std::mem::take(&mut current_comment),
318 ));
319 }
320 } else if let Some((dep, rest)) = line.split_once("=") {
321 let dep = dep.trim().trim_matches('"');
322 let rest = get_balanced(rest, &mut lines)
323 .map_err(|e| format!("Parse error while parsing value {}: {}", dep, e))?;
324 if current_table == "features" && dep == "default" {
325 let defaults = rest
326 .trim()
327 .strip_prefix("[")
328 .and_then(|r| r.strip_suffix("]"))
329 .ok_or_else(|| format!("Parse error while parsing dependency {}", dep))?
330 .split(",")
331 .map(|d| d.trim().trim_matches(|c| c == '"' || c == '\'').trim().to_string())
332 .filter(|d| !d.is_empty());
333 default_features.extend(defaults);
334 }
335 if !current_comment.is_empty() {
336 if current_table.ends_with("dependencies") {
337 if !rest
338 .split_once("optional")
339 .and_then(|(_, r)| r.trim().strip_prefix("="))
340 .map_or(false, |r| r.trim().starts_with("true"))
341 {
342 return Err(format!("Dependency {} is not an optional dependency", dep));
343 }
344 } else if current_table != "features" {
345 return Err(format!(
346 "Comment cannot be associated with a feature:{}",
347 current_comment
348 ));
349 }
350 features.push((
351 dep,
352 std::mem::take(&mut top_comment),
353 std::mem::take(&mut current_comment),
354 ));
355 }
356 }
357 }
358 if !current_comment.is_empty() {
359 return Err("Found comment not associated with a feature".into());
360 }
361 if features.is_empty() {
362 return Err("Could not find documented features in Cargo.toml".into());
363 }
364 let mut result = String::new();
365 for (f, top, comment) in features {
366 let default = if default_features.contains(f) { " *(enabled by default)*" } else { "" };
367 if !comment.trim().is_empty() {
368 if let Some(feature_label) = &args.feature_label {
369 writeln!(
370 result,
371 "{}* {}{} —{}",
372 top,
373 feature_label.replace("{feature}", f),
374 default,
375 comment
376 )
377 .unwrap();
378 } else {
379 writeln!(result, "{}* **`{}`**{} —{}", top, f, default, comment).unwrap();
380 }
381 } else {
382 if let Some(feature_label) = &args.feature_label {
383 writeln!(
384 result,
385 "{}* {}{}\n",
386 top,
387 feature_label.replace("{feature}", f),
388 default,
389 )
390 .unwrap();
391 } else {
392 writeln!(result, "{}* **`{}`**{}\n", top, f, default).unwrap();
393 }
394 }
395 }
396 result += &top_comment;
397 Ok(result)
398}
399
400fn get_balanced<'a>(
401 first_line: &'a str,
402 lines: &mut impl Iterator<Item = &'a str>,
403) -> Result<Cow<'a, str>, String> {
404 let mut line = first_line;
405 let mut result = Cow::from("");
406
407 let mut in_quote = false;
408 let mut level = 0;
409 loop {
410 let mut last_slash = false;
411 for (idx, b) in line.as_bytes().into_iter().enumerate() {
412 if last_slash {
413 last_slash = false
414 } else if in_quote {
415 match b {
416 b'\\' => last_slash = true,
417 b'"' | b'\'' => in_quote = false,
418 _ => (),
419 }
420 } else {
421 match b {
422 b'\\' => last_slash = true,
423 b'"' => in_quote = true,
424 b'{' | b'[' => level += 1,
425 b'}' | b']' if level == 0 => return Err("unbalanced source".into()),
426 b'}' | b']' => level -= 1,
427 b'#' => {
428 line = &line[..idx];
429 break;
430 }
431 _ => (),
432 }
433 }
434 }
435 if result.len() == 0 {
436 result = Cow::from(line);
437 } else {
438 *result.to_mut() += line;
439 }
440 if level == 0 {
441 return Ok(result);
442 }
443 line = if let Some(l) = lines.next() {
444 l
445 } else {
446 return Err("unbalanced source".into());
447 };
448 }
449}
450
451#[test]
452fn test_get_balanced() {
453 assert_eq!(
454 get_balanced(
455 "{",
456 &mut IntoIterator::into_iter(["a", "{ abc[], #ignore", " def }", "}", "xxx"])
457 ),
458 Ok("{a{ abc[], def }}".into())
459 );
460 assert_eq!(
461 get_balanced("{ foo = \"{#\" } #ignore", &mut IntoIterator::into_iter(["xxx"])),
462 Ok("{ foo = \"{#\" } ".into())
463 );
464 assert_eq!(
465 get_balanced("]", &mut IntoIterator::into_iter(["["])),
466 Err("unbalanced source".into())
467 );
468}
469
470#[cfg(feature = "self-test")]
471#[proc_macro]
472#[doc(hidden)]
473pub fn self_test_helper(input: TokenStream) -> TokenStream {
475 process_toml((&input).to_string().trim_matches(|c| c == '"' || c == '#'), &Args::default())
476 .map_or_else(
477 |e| error(&e),
478 |r| {
479 std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&r)))
480 .collect()
481 },
482 )
483}
484
485#[cfg(feature = "self-test")]
486macro_rules! self_test {
487 (#[doc = $toml:literal] => #[doc = $md:literal]) => {
488 concat!(
489 "\n`````rust\n\
490 fn normalize_md(md : &str) -> String {
491 md.lines().skip_while(|l| l.is_empty()).map(|l| l.trim())
492 .collect::<Vec<_>>().join(\"\\n\")
493 }
494 assert_eq!(normalize_md(document_features::self_test_helper!(",
495 stringify!($toml),
496 ")), normalize_md(",
497 stringify!($md),
498 "));\n`````\n\n"
499 )
500 };
501}
502
503#[cfg(not(feature = "self-test"))]
504macro_rules! self_test {
505 (#[doc = $toml:literal] => #[doc = $md:literal]) => {
506 concat!(
507 "This contents in Cargo.toml:\n`````toml",
508 $toml,
509 "\n`````\n Generates the following:\n\
510 <table><tr><th>Preview</th></tr><tr><td>\n\n",
511 $md,
512 "\n</td></tr></table>\n\n \n",
513 )
514 };
515}
516
517#[cfg(doc)]
548struct FeatureLabelCompilationTest;
549
550#[cfg(test)]
551mod tests {
552 use super::{process_toml, Args};
553
554 #[track_caller]
555 fn test_error(toml: &str, expected: &str) {
556 let err = process_toml(toml, &Args::default()).unwrap_err();
557 assert!(err.contains(expected), "{:?} does not contain {:?}", err, expected)
558 }
559
560 #[test]
561 fn only_get_balanced_in_correct_table() {
562 process_toml(
563 r#"
564
565[package.metadata.release]
566pre-release-replacements = [
567 {test=\"\#\# \"},
568]
569[abcd]
570[features]#xyz
571#! abc
572#
573###
574#! def
575#!
576## 123
577## 456
578feat1 = ["plop"]
579#! ghi
580no_doc = []
581##
582feat2 = ["momo"]
583#! klm
584default = ["feat1", "something_else"]
585#! end
586 "#,
587 &Args::default(),
588 )
589 .unwrap();
590 }
591
592 #[test]
593 fn parse_error1() {
594 test_error(
595 r#"
596[features]
597[dependencies]
598foo = 4;
599"#,
600 "Could not find documented features",
601 );
602 }
603
604 #[test]
605 fn parse_error2() {
606 test_error(
607 r#"
608[packages]
609[dependencies]
610"#,
611 "Could not find documented features",
612 );
613 }
614
615 #[test]
616 fn parse_error3() {
617 test_error(
618 r#"
619[features]
620ff = []
621[abcd
622efgh
623[dependencies]
624"#,
625 "Parse error while parsing line: [abcd",
626 );
627 }
628
629 #[test]
630 fn parse_error4() {
631 test_error(
632 r#"
633[features]
634## dd
635## ff
636#! ee
637## ff
638"#,
639 "Cannot mix",
640 );
641 }
642
643 #[test]
644 fn parse_error5() {
645 test_error(
646 r#"
647[features]
648## dd
649"#,
650 "not associated with a feature",
651 );
652 }
653
654 #[test]
655 fn parse_error6() {
656 test_error(
657 r#"
658[features]
659# ff
660foo = []
661default = [
662#ffff
663# ff
664"#,
665 "Parse error while parsing value default",
666 );
667 }
668
669 #[test]
670 fn parse_error7() {
671 test_error(
672 r#"
673[features]
674# f
675foo = [ x = { ]
676bar = []
677"#,
678 "Parse error while parsing value foo",
679 );
680 }
681
682 #[test]
683 fn not_a_feature1() {
684 test_error(
685 r#"
686## hallo
687[features]
688"#,
689 "Not a feature: `[features]`",
690 );
691 }
692
693 #[test]
694 fn not_a_feature2() {
695 test_error(
696 r#"
697[package]
698## hallo
699foo = []
700"#,
701 "Comment cannot be associated with a feature: hallo",
702 );
703 }
704
705 #[test]
706 fn non_optional_dep1() {
707 test_error(
708 r#"
709[dev-dependencies]
710## Not optional
711foo = { version = "1.2", optional = false }
712"#,
713 "Dependency foo is not an optional dependency",
714 );
715 }
716
717 #[test]
718 fn non_optional_dep2() {
719 test_error(
720 r#"
721[dev-dependencies]
722## Not optional
723foo = { version = "1.2" }
724"#,
725 "Dependency foo is not an optional dependency",
726 );
727 }
728
729 #[test]
730 fn basic() {
731 let toml = r#"
732[abcd]
733[features]#xyz
734#! abc
735#
736###
737#! def
738#!
739## 123
740## 456
741feat1 = ["plop"]
742#! ghi
743no_doc = []
744##
745feat2 = ["momo"]
746#! klm
747default = ["feat1", "something_else"]
748#! end
749 "#;
750 let parsed = process_toml(toml, &Args::default()).unwrap();
751 assert_eq!(
752 parsed,
753 " abc\n def\n\n* **`feat1`** *(enabled by default)* — 123\n 456\n\n ghi\n* **`feat2`**\n\n klm\n end\n"
754 );
755 let parsed = process_toml(
756 toml,
757 &Args {
758 feature_label: Some(
759 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
760 ),
761 },
762 )
763 .unwrap();
764 assert_eq!(
765 parsed,
766 " 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"
767 );
768 }
769
770 #[test]
771 fn dependencies() {
772 let toml = r#"
773#! top
774[dev-dependencies] #yo
775## dep1
776dep1 = { version="1.2", optional=true}
777#! yo
778dep2 = "1.3"
779## dep3
780[target.'cfg(unix)'.build-dependencies.dep3]
781version = "42"
782optional = true
783 "#;
784 let parsed = process_toml(toml, &Args::default()).unwrap();
785 assert_eq!(parsed, " top\n* **`dep1`** — dep1\n\n yo\n* **`dep3`** — dep3\n\n");
786 let parsed = process_toml(
787 toml,
788 &Args {
789 feature_label: Some(
790 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
791 ),
792 },
793 )
794 .unwrap();
795 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\n");
796 }
797
798 #[test]
799 fn multi_lines() {
800 let toml = r#"
801[package.metadata.foo]
802ixyz = [
803 ["array"],
804 [
805 "of",
806 "arrays"
807 ]
808]
809[dev-dependencies]
810## dep1
811dep1 = {
812 version="1.2-}",
813 optional=true
814}
815[features]
816default = [
817 "goo",
818 "\"]",
819 "bar",
820]
821## foo
822foo = [
823 "bar"
824]
825## bar
826bar = [
827
828]
829 "#;
830 let parsed = process_toml(toml, &Args::default()).unwrap();
831 assert_eq!(
832 parsed,
833 "* **`dep1`** — dep1\n\n* **`foo`** — foo\n\n* **`bar`** *(enabled by default)* — bar\n\n"
834 );
835 let parsed = process_toml(
836 toml,
837 &Args {
838 feature_label: Some(
839 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
840 ),
841 },
842 )
843 .unwrap();
844 assert_eq!(
845 parsed,
846 "* <span class=\"stab portability\"><code>dep1</code></span> — dep1\n\n* <span class=\"stab portability\"><code>foo</code></span> — foo\n\n* <span class=\"stab portability\"><code>bar</code></span> *(enabled by default)* — bar\n\n"
847 );
848 }
849
850 #[test]
851 fn dots_in_feature() {
852 let toml = r#"
853[features]
854## This is a test
855"teßt." = []
856default = ["teßt."]
857[dependencies]
858## A dep
859"dep" = { version = "123", optional = true }
860 "#;
861 let parsed = process_toml(toml, &Args::default()).unwrap();
862 assert_eq!(
863 parsed,
864 "* **`teßt.`** *(enabled by default)* — This is a test\n\n* **`dep`** — A dep\n\n"
865 );
866 let parsed = process_toml(
867 toml,
868 &Args {
869 feature_label: Some(
870 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
871 ),
872 },
873 )
874 .unwrap();
875 assert_eq!(
876 parsed,
877 "* <span class=\"stab portability\"><code>teßt.</code></span> *(enabled by default)* — This is a test\n\n* <span class=\"stab portability\"><code>dep</code></span> — A dep\n\n"
878 );
879 }
880}