From 40a355a42d4a9444dc753c04c6608dade2f06a23 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:13:27 +0200 Subject: Adding upstream version 125.0.1. Signed-off-by: Daniel Baumann --- third_party/rust/document-features/lib.rs | 877 ++++++++++++++++++++++++++++++ 1 file changed, 877 insertions(+) create mode 100644 third_party/rust/document-features/lib.rs (limited to 'third_party/rust/document-features/lib.rs') diff --git a/third_party/rust/document-features/lib.rs b/third_party/rust/document-features/lib.rs new file mode 100644 index 0000000000..b30ebe7f42 --- /dev/null +++ b/third_party/rust/document-features/lib.rs @@ -0,0 +1,877 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT OR Apache-2.0 + +/*! +Document your crate's feature flags. + +This crates provides a macro that extracts "documentation" comments from Cargo.toml + +To use this crate, add `#![doc = document_features::document_features!()]` in your crate documentation. +The `document_features!()` macro reads your `Cargo.toml` file, extracts feature comments and generates +a markdown string for your documentation. + +Basic example: + +```rust +//! Normal crate documentation goes here. +//! +//! ## Feature flags +#![doc = document_features::document_features!()] + +// rest of the crate goes here. +``` + +## Documentation format: + +The documentation of your crate features goes into `Cargo.toml`, where they are defined. + +The `document_features!()` macro analyzes the contents of `Cargo.toml`. +Similar to Rust's documentation comments `///` and `//!`, the macro understands +comments that start with `## ` and `#! `. Note the required trailing space. +Lines starting with `###` will not be understood as doc comment. + +`## ` comments are meant to be *above* the feature they document. +There can be several `## ` comments, but they must always be followed by a +feature name or an optional dependency. +There should not be `#! ` comments between the comment and the feature they document. + +`#! ` comments are not associated with a particular feature, and will be printed +in where they occur. Use them to group features, for example. + +## Examples: + +*/ +// Note: because rustdoc escapes the first `#` of a line starting with `#`, +// these docs comments have one more `#` , +#![doc = self_test!(/** +[package] +name = "..." +## ... + +[features] +default = ["foo"] +##! This comments goes on top + +### The foo feature enables the `foo` functions +foo = [] + +### The bar feature enables the bar module +bar = [] + +##! ### Experimental features +##! The following features are experimental + +### Enable the fusion reactor +### +### ⚠️ Can lead to explosions +fusion = [] + +[dependencies] +document-features = "0.2" + +##! ### Optional dependencies + +### Enable this feature to implement the trait for the types from the genial crate +genial = { version = "0.2", optional = true } + +### This awesome dependency is specified in its own table +[dependencies.awesome] +version = "1.3.5" +optional = true +*/ +=> + /** +This comments goes on top +* **`foo`** *(enabled by default)* — The foo feature enables the `foo` functions +* **`bar`** — The bar feature enables the bar module + +#### Experimental features +The following features are experimental +* **`fusion`** — Enable the fusion reactor + + ⚠️ Can lead to explosions + +#### Optional dependencies +* **`genial`** — Enable this feature to implement the trait for the types from the genial crate +* **`awesome`** — This awesome dependency is specified in its own table +*/ +)] +/*! + +## Customization + +You can customize the formatting of the features in the generated documentation by setting +the key **`feature_label=`** to a given format string. This format string must be either +a [string literal](https://doc.rust-lang.org/reference/tokens.html#string-literals) or +a [raw string literal](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals). +Every occurrence of `{feature}` inside the format string will be substituted with the name of the feature. + +For instance, to emulate the HTML formatting used by `rustdoc` one can use the following: + +```rust +#![doc = document_features::document_features!(feature_label = r#"{feature}"#)] +``` + +The default formatting is equivalent to: + +```rust +#![doc = document_features::document_features!(feature_label = "**`{feature}`**")] +``` + +## Compatibility + +The minimum Rust version required to use this crate is Rust 1.54 because of the +feature to have macro in doc comments. You can make this crate optional and use +`#[cfg_attr()]` statements to enable it only when building the documentation: +You need to have two levels of `cfg_attr` because Rust < 1.54 doesn't parse the attribute +otherwise. + +```rust,ignore +#![cfg_attr( + feature = "document-features", + cfg_attr(doc, doc = ::document_features::document_features!()) +)] +``` + +In your Cargo.toml, enable this feature while generating the documentation on docs.rs: + +```toml +[dependencies] +document-features = { version = "0.2", optional = true } + +[package.metadata.docs.rs] +features = ["document-features"] +## Alternative: enable all features so they are all documented +## all-features = true +``` + */ + +#[cfg(not(feature = "default"))] +compile_error!( + "The feature `default` must be enabled to ensure \ + forward compatibility with future version of this crate" +); + +extern crate proc_macro; + +use proc_macro::{TokenStream, TokenTree}; +use std::borrow::Cow; +use std::collections::HashSet; +use std::convert::TryFrom; +use std::fmt::Write; +use std::path::Path; +use std::str::FromStr; + +fn error(e: &str) -> TokenStream { + TokenStream::from_str(&format!("::core::compile_error!{{\"{}\"}}", e.escape_default())).unwrap() +} + +fn compile_error(msg: &str, tt: Option) -> TokenStream { + let span = tt.as_ref().map_or_else(proc_macro::Span::call_site, TokenTree::span); + use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing}; + use std::iter::FromIterator; + TokenStream::from_iter(vec![ + TokenTree::Ident(Ident::new("compile_error", span)), + TokenTree::Punct({ + let mut punct = Punct::new('!', Spacing::Alone); + punct.set_span(span); + punct + }), + TokenTree::Group({ + let mut group = Group::new(Delimiter::Brace, { + TokenStream::from_iter([TokenTree::Literal({ + let mut string = Literal::string(msg); + string.set_span(span); + string + })]) + }); + group.set_span(span); + group + }), + ]) +} + +#[derive(Default)] +struct Args { + feature_label: Option, +} + +fn parse_args(input: TokenStream) -> Result { + let mut token_trees = input.into_iter().fuse(); + + // parse the key, ensuring that it is the identifier `feature_label` + match token_trees.next() { + None => return Ok(Args::default()), + Some(TokenTree::Ident(ident)) if ident.to_string() == "feature_label" => (), + tt => return Err(compile_error("expected `feature_label`", tt)), + } + + // parse a single equal sign `=` + match token_trees.next() { + Some(TokenTree::Punct(p)) if p.as_char() == '=' => (), + tt => return Err(compile_error("expected `=`", tt)), + } + + // parse the value, ensuring that it is a string literal containing the substring `"{feature}"` + let feature_label; + if let Some(tt) = token_trees.next() { + match litrs::StringLit::::try_from(&tt) { + Ok(string_lit) if string_lit.value().contains("{feature}") => { + feature_label = string_lit.value().to_string() + } + _ => { + return Err(compile_error( + "expected a string literal containing the substring \"{feature}\"", + Some(tt), + )) + } + } + } else { + return Err(compile_error( + "expected a string literal containing the substring \"{feature}\"", + None, + )); + } + + // ensure there is nothing left after the format string + if let tt @ Some(_) = token_trees.next() { + return Err(compile_error("unexpected token after the format string", tt)); + } + + Ok(Args { feature_label: Some(feature_label) }) +} + +/// Produce a literal string containing documentation extracted from Cargo.toml +/// +/// See the [crate] documentation for details +#[proc_macro] +pub fn document_features(tokens: TokenStream) -> TokenStream { + parse_args(tokens) + .and_then(|args| document_features_impl(&args)) + .unwrap_or_else(std::convert::identity) +} + +fn document_features_impl(args: &Args) -> Result { + let path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut cargo_toml = std::fs::read_to_string(Path::new(&path).join("Cargo.toml")) + .map_err(|e| error(&format!("Can't open Cargo.toml: {:?}", e)))?; + + if !cargo_toml.contains("\n##") && !cargo_toml.contains("\n#!") { + // On crates.io, Cargo.toml is usually "normalized" and stripped of all comments. + // The original Cargo.toml has been renamed Cargo.toml.orig + if let Ok(orig) = std::fs::read_to_string(Path::new(&path).join("Cargo.toml.orig")) { + if orig.contains("##") || orig.contains("#!") { + cargo_toml = orig; + } + } + } + + let result = process_toml(&cargo_toml, args).map_err(|e| error(&e))?; + Ok(std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&result))).collect()) +} + +fn process_toml(cargo_toml: &str, args: &Args) -> Result { + // Get all lines between the "[features]" and the next block + let mut lines = cargo_toml + .lines() + .map(str::trim) + // and skip empty lines and comments that are not docs comments + .filter(|l| { + !l.is_empty() && (!l.starts_with('#') || l.starts_with("##") || l.starts_with("#!")) + }); + let mut top_comment = String::new(); + let mut current_comment = String::new(); + let mut features = vec![]; + let mut default_features = HashSet::new(); + let mut current_table = ""; + while let Some(line) = lines.next() { + if let Some(x) = line.strip_prefix("#!") { + if !x.is_empty() && !x.starts_with(' ') { + continue; // it's not a doc comment + } + if !current_comment.is_empty() { + return Err("Cannot mix ## and #! comments between features.".into()); + } + if top_comment.is_empty() && !features.is_empty() { + top_comment = "\n".into(); + } + writeln!(top_comment, "{}", x).unwrap(); + } else if let Some(x) = line.strip_prefix("##") { + if !x.is_empty() && !x.starts_with(' ') { + continue; // it's not a doc comment + } + writeln!(current_comment, " {}", x).unwrap(); + } else if let Some(table) = line.strip_prefix('[') { + current_table = table + .split_once(']') + .map(|(t, _)| t.trim()) + .ok_or_else(|| format!("Parse error while parsing line: {}", line))?; + if !current_comment.is_empty() { + let dep = current_table + .rsplit_once('.') + .and_then(|(table, dep)| table.trim().ends_with("dependencies").then(|| dep)) + .ok_or_else(|| format!("Not a feature: `{}`", line))?; + features.push(( + dep.trim(), + std::mem::take(&mut top_comment), + std::mem::take(&mut current_comment), + )); + } + } else if let Some((dep, rest)) = line.split_once('=') { + let dep = dep.trim().trim_matches('"'); + let rest = get_balanced(rest, &mut lines) + .map_err(|e| format!("Parse error while parsing value {}: {}", dep, e))?; + if current_table == "features" && dep == "default" { + let defaults = rest + .trim() + .strip_prefix('[') + .and_then(|r| r.strip_suffix(']')) + .ok_or_else(|| format!("Parse error while parsing dependency {}", dep))? + .split(',') + .map(|d| d.trim().trim_matches(|c| c == '"' || c == '\'').trim().to_string()) + .filter(|d| !d.is_empty()); + default_features.extend(defaults); + } + if !current_comment.is_empty() { + if current_table.ends_with("dependencies") { + if !rest + .split_once("optional") + .and_then(|(_, r)| r.trim().strip_prefix('=')) + .map_or(false, |r| r.trim().starts_with("true")) + { + return Err(format!("Dependency {} is not an optional dependency", dep)); + } + } else if current_table != "features" { + return Err(format!( + r#"Comment cannot be associated with a feature: "{}""#, + current_comment.trim() + )); + } + features.push(( + dep, + std::mem::take(&mut top_comment), + std::mem::take(&mut current_comment), + )); + } + } + } + if !current_comment.is_empty() { + return Err("Found comment not associated with a feature".into()); + } + if features.is_empty() { + return Ok("*No documented features in Cargo.toml*".into()); + } + let mut result = String::new(); + for (f, top, comment) in features { + let default = if default_features.contains(f) { " *(enabled by default)*" } else { "" }; + if !comment.trim().is_empty() { + if let Some(feature_label) = &args.feature_label { + writeln!( + result, + "{}* {}{} —{}", + top, + feature_label.replace("{feature}", f), + default, + comment.trim_end(), + ) + .unwrap(); + } else { + writeln!(result, "{}* **`{}`**{} —{}", top, f, default, comment.trim_end()) + .unwrap(); + } + } else if let Some(feature_label) = &args.feature_label { + writeln!(result, "{}* {}{}", top, feature_label.replace("{feature}", f), default,) + .unwrap(); + } else { + writeln!(result, "{}* **`{}`**{}", top, f, default).unwrap(); + } + } + result += &top_comment; + Ok(result) +} + +fn get_balanced<'a>( + first_line: &'a str, + lines: &mut impl Iterator, +) -> Result, String> { + let mut line = first_line; + let mut result = Cow::from(""); + + let mut in_quote = false; + let mut level = 0; + loop { + let mut last_slash = false; + for (idx, b) in line.as_bytes().iter().enumerate() { + if last_slash { + last_slash = false + } else if in_quote { + match b { + b'\\' => last_slash = true, + b'"' | b'\'' => in_quote = false, + _ => (), + } + } else { + match b { + b'\\' => last_slash = true, + b'"' => in_quote = true, + b'{' | b'[' => level += 1, + b'}' | b']' if level == 0 => return Err("unbalanced source".into()), + b'}' | b']' => level -= 1, + b'#' => { + line = &line[..idx]; + break; + } + _ => (), + } + } + } + if result.len() == 0 { + result = Cow::from(line); + } else { + *result.to_mut() += line; + } + if level == 0 { + return Ok(result); + } + line = if let Some(l) = lines.next() { + l + } else { + return Err("unbalanced source".into()); + }; + } +} + +#[test] +fn test_get_balanced() { + assert_eq!( + get_balanced( + "{", + &mut IntoIterator::into_iter(["a", "{ abc[], #ignore", " def }", "}", "xxx"]) + ), + Ok("{a{ abc[], def }}".into()) + ); + assert_eq!( + get_balanced("{ foo = \"{#\" } #ignore", &mut IntoIterator::into_iter(["xxx"])), + Ok("{ foo = \"{#\" } ".into()) + ); + assert_eq!( + get_balanced("]", &mut IntoIterator::into_iter(["["])), + Err("unbalanced source".into()) + ); +} + +#[cfg(feature = "self-test")] +#[proc_macro] +#[doc(hidden)] +/// Helper macro for the tests. Do not use +pub fn self_test_helper(input: TokenStream) -> TokenStream { + process_toml((&input).to_string().trim_matches(|c| c == '"' || c == '#'), &Args::default()) + .map_or_else( + |e| error(&e), + |r| { + std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&r))) + .collect() + }, + ) +} + +#[cfg(feature = "self-test")] +macro_rules! self_test { + (#[doc = $toml:literal] => #[doc = $md:literal]) => { + concat!( + "\n`````rust\n\ + fn normalize_md(md : &str) -> String { + md.lines().skip_while(|l| l.is_empty()).map(|l| l.trim()) + .collect::>().join(\"\\n\") + } + assert_eq!(normalize_md(document_features::self_test_helper!(", + stringify!($toml), + ")), normalize_md(", + stringify!($md), + "));\n`````\n\n" + ) + }; +} + +#[cfg(not(feature = "self-test"))] +macro_rules! self_test { + (#[doc = $toml:literal] => #[doc = $md:literal]) => { + concat!( + "This contents in Cargo.toml:\n`````toml", + $toml, + "\n`````\n Generates the following:\n\ +
Preview
\n\n", + $md, + "\n
\n\n \n", + ) + }; +} + +// The following struct is inserted only during generation of the documentation in order to exploit doc-tests. +// These doc-tests are used to check that invalid arguments to the `document_features!` macro cause a compile time error. +// For a more principled way of testing compilation error, maybe investigate . +// +/// ```rust +/// #![doc = document_features::document_features!()] +/// #![doc = document_features::document_features!(feature_label = "**`{feature}`**")] +/// #![doc = document_features::document_features!(feature_label = r"**`{feature}`**")] +/// #![doc = document_features::document_features!(feature_label = r#"**`{feature}`**"#)] +/// #![doc = document_features::document_features!(feature_label = "{feature}")] +/// #![doc = document_features::document_features!(feature_label = r#"{feature}"#)] +/// ``` +/// ```compile_fail +/// #![doc = document_features::document_features!(feature_label > "{feature}")] +/// ``` +/// ```compile_fail +/// #![doc = document_features::document_features!(label = "{feature}")] +/// ``` +/// ```compile_fail +/// #![doc = document_features::document_features!(feature_label = "{feat}")] +/// ``` +/// ```compile_fail +/// #![doc = document_features::document_features!(feature_label = 3.14)] +/// ``` +/// ```compile_fail +/// #![doc = document_features::document_features!(feature_label = )] +/// ``` +/// ```compile_fail +/// #![doc = document_features::document_features!(feature_label = "**`{feature}`**" extra)] +/// ``` +#[cfg(doc)] +struct FeatureLabelCompilationTest; + +#[cfg(test)] +mod tests { + use super::{process_toml, Args}; + + #[track_caller] + fn test_error(toml: &str, expected: &str) { + let err = process_toml(toml, &Args::default()).unwrap_err(); + assert!(err.contains(expected), "{:?} does not contain {:?}", err, expected) + } + + #[test] + fn only_get_balanced_in_correct_table() { + process_toml( + r#" + +[package.metadata.release] +pre-release-replacements = [ + {test=\"\#\# \"}, +] +[abcd] +[features]#xyz +#! abc +# +### +#! def +#! +## 123 +## 456 +feat1 = ["plop"] +#! ghi +no_doc = [] +## +feat2 = ["momo"] +#! klm +default = ["feat1", "something_else"] +#! end + "#, + &Args::default(), + ) + .unwrap(); + } + + #[test] + fn no_features() { + let r = process_toml( + r#" +[features] +[dependencies] +foo = 4; +"#, + &Args::default(), + ) + .unwrap(); + assert_eq!(r, "*No documented features in Cargo.toml*"); + } + + #[test] + fn no_features2() { + let r = process_toml( + r#" +[packages] +[dependencies] +"#, + &Args::default(), + ) + .unwrap(); + assert_eq!(r, "*No documented features in Cargo.toml*"); + } + + #[test] + fn parse_error3() { + test_error( + r#" +[features] +ff = [] +[abcd +efgh +[dependencies] +"#, + "Parse error while parsing line: [abcd", + ); + } + + #[test] + fn parse_error4() { + test_error( + r#" +[features] +## dd +## ff +#! ee +## ff +"#, + "Cannot mix", + ); + } + + #[test] + fn parse_error5() { + test_error( + r#" +[features] +## dd +"#, + "not associated with a feature", + ); + } + + #[test] + fn parse_error6() { + test_error( + r#" +[features] +# ff +foo = [] +default = [ +#ffff +# ff +"#, + "Parse error while parsing value default", + ); + } + + #[test] + fn parse_error7() { + test_error( + r#" +[features] +# f +foo = [ x = { ] +bar = [] +"#, + "Parse error while parsing value foo", + ); + } + + #[test] + fn not_a_feature1() { + test_error( + r#" +## hallo +[features] +"#, + "Not a feature: `[features]`", + ); + } + + #[test] + fn not_a_feature2() { + test_error( + r#" +[package] +## hallo +foo = [] +"#, + "Comment cannot be associated with a feature: \"hallo\"", + ); + } + + #[test] + fn non_optional_dep1() { + test_error( + r#" +[dev-dependencies] +## Not optional +foo = { version = "1.2", optional = false } +"#, + "Dependency foo is not an optional dependency", + ); + } + + #[test] + fn non_optional_dep2() { + test_error( + r#" +[dev-dependencies] +## Not optional +foo = { version = "1.2" } +"#, + "Dependency foo is not an optional dependency", + ); + } + + #[test] + fn basic() { + let toml = r#" +[abcd] +[features]#xyz +#! abc +# +### +#! def +#! +## 123 +## 456 +feat1 = ["plop"] +#! ghi +no_doc = [] +## +feat2 = ["momo"] +#! klm +default = ["feat1", "something_else"] +#! end + "#; + let parsed = process_toml(toml, &Args::default()).unwrap(); + assert_eq!( + parsed, + " abc\n def\n\n* **`feat1`** *(enabled by default)* — 123\n 456\n\n ghi\n* **`feat2`**\n\n klm\n end\n" + ); + let parsed = process_toml( + toml, + &Args { + feature_label: Some( + "{feature}".into(), + ), + }, + ) + .unwrap(); + assert_eq!( + parsed, + " abc\n def\n\n* feat1 *(enabled by default)* — 123\n 456\n\n ghi\n* feat2\n\n klm\n end\n" + ); + } + + #[test] + fn dependencies() { + let toml = r#" +#! top +[dev-dependencies] #yo +## dep1 +dep1 = { version="1.2", optional=true} +#! yo +dep2 = "1.3" +## dep3 +[target.'cfg(unix)'.build-dependencies.dep3] +version = "42" +optional = true + "#; + let parsed = process_toml(toml, &Args::default()).unwrap(); + assert_eq!(parsed, " top\n* **`dep1`** — dep1\n\n yo\n* **`dep3`** — dep3\n"); + let parsed = process_toml( + toml, + &Args { + feature_label: Some( + "{feature}".into(), + ), + }, + ) + .unwrap(); + assert_eq!(parsed, " top\n* dep1 — dep1\n\n yo\n* dep3 — dep3\n"); + } + + #[test] + fn multi_lines() { + let toml = r#" +[package.metadata.foo] +ixyz = [ + ["array"], + [ + "of", + "arrays" + ] +] +[dev-dependencies] +## dep1 +dep1 = { + version="1.2-}", + optional=true +} +[features] +default = [ + "goo", + "\"]", + "bar", +] +## foo +foo = [ + "bar" +] +## bar +bar = [ + +] + "#; + let parsed = process_toml(toml, &Args::default()).unwrap(); + assert_eq!( + parsed, + "* **`dep1`** — dep1\n* **`foo`** — foo\n* **`bar`** *(enabled by default)* — bar\n" + ); + let parsed = process_toml( + toml, + &Args { + feature_label: Some( + "{feature}".into(), + ), + }, + ) + .unwrap(); + assert_eq!( + parsed, + "* dep1 — dep1\n* foo — foo\n* bar *(enabled by default)* — bar\n" + ); + } + + #[test] + fn dots_in_feature() { + let toml = r#" +[features] +## This is a test +"teßt." = [] +default = ["teßt."] +[dependencies] +## A dep +"dep" = { version = "123", optional = true } + "#; + let parsed = process_toml(toml, &Args::default()).unwrap(); + assert_eq!( + parsed, + "* **`teßt.`** *(enabled by default)* — This is a test\n* **`dep`** — A dep\n" + ); + let parsed = process_toml( + toml, + &Args { + feature_label: Some( + "{feature}".into(), + ), + }, + ) + .unwrap(); + assert_eq!( + parsed, + "* teßt. *(enabled by default)* — This is a test\n* dep — A dep\n" + ); + } +} -- cgit v1.2.3