fix(embedded)!: Remove doc comment manifest support

We added code fence support in ba869d36ed
(September), so I think this was enough of a transition period and there
is little interest in going back to this.
This commit is contained in:
Ed Page 2024-01-02 16:07:34 -06:00
parent 42d2c028bd
commit 7e5dd711b5
3 changed files with 8 additions and 688 deletions

2
Cargo.lock generated
View file

@ -289,7 +289,6 @@ dependencies = [
"os_info",
"pasetors",
"pathdiff",
"pulldown-cmark",
"rand",
"regex",
"rusqlite",
@ -304,7 +303,6 @@ dependencies = [
"shell-escape",
"snapbox",
"supports-hyperlinks",
"syn 2.0.46",
"tar",
"tempfile",
"time",

View file

@ -91,7 +91,6 @@ sha2 = "0.10.8"
shell-escape = "0.1.5"
supports-hyperlinks = "2.1.0"
snapbox = { version = "0.4.15", features = ["diff", "path"] }
syn = { version = "2.0.46", features = ["extra-traits", "full"] }
tar = { version = "0.4.40", default-features = false }
tempfile = "3.9.0"
thiserror = "1.0.56"
@ -179,7 +178,6 @@ opener.workspace = true
os_info.workspace = true
pasetors.workspace = true
pathdiff.workspace = true
pulldown-cmark.workspace = true
rand.workspace = true
regex.workspace = true
rusqlite.workspace = true
@ -192,7 +190,6 @@ serde_json = { workspace = true, features = ["raw_value"] }
sha1.workspace = true
shell-escape.workspace = true
supports-hyperlinks.workspace = true
syn.workspace = true
tar.workspace = true
tempfile.workspace = true
time.workspace = true

View file

@ -66,17 +66,8 @@ pub(super) fn expand_manifest(
let manifest = toml::to_string_pretty(&manifest)?;
Ok(manifest)
} else {
// Legacy doc-comment support; here only for transitional purposes
let comment = extract_comment(content)?.unwrap_or_default();
let manifest = match extract_manifest(&comment)? {
Some(manifest) => Some(manifest),
None => {
tracing::trace!("failed to extract manifest");
None
}
}
.unwrap_or_default();
let manifest = expand_manifest_(&manifest, path, config)
let frontmatter = "";
let manifest = expand_manifest_(frontmatter, path, config)
.with_context(|| format!("failed to parse manifest at {}", path.display()))?;
let manifest = toml::to_string_pretty(&manifest)?;
Ok(manifest)
@ -268,323 +259,6 @@ fn split_source(input: &str) -> CargoResult<Source<'_>> {
Ok(source)
}
/// Locates a "code block manifest" in Rust source.
fn extract_comment(input: &str) -> CargoResult<Option<String>> {
let mut doc_fragments = Vec::new();
let file = syn::parse_file(input)?;
// HACK: `syn` doesn't tell us what kind of comment was used, so infer it from how many
// attributes were used
let kind = if 1 < file
.attrs
.iter()
.filter(|attr| attr.meta.path().is_ident("doc"))
.count()
{
CommentKind::Line
} else {
CommentKind::Block
};
for attr in &file.attrs {
if attr.meta.path().is_ident("doc") {
doc_fragments.push(DocFragment::new(attr, kind)?);
}
}
if doc_fragments.is_empty() {
return Ok(None);
}
unindent_doc_fragments(&mut doc_fragments);
let mut doc_comment = String::new();
for frag in &doc_fragments {
add_doc_fragment(&mut doc_comment, frag);
}
Ok(Some(doc_comment))
}
/// A `#[doc]`
#[derive(Clone, Debug)]
struct DocFragment {
/// The attribute value
doc: String,
/// Indentation used within `doc
indent: usize,
}
impl DocFragment {
fn new(attr: &syn::Attribute, kind: CommentKind) -> CargoResult<Self> {
let syn::Meta::NameValue(nv) = &attr.meta else {
anyhow::bail!("unsupported attr meta for {:?}", attr.meta.path())
};
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) = &nv.value
else {
anyhow::bail!("only string literals are supported")
};
Ok(Self {
doc: beautify_doc_string(lit.value(), kind),
indent: 0,
})
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum CommentKind {
Line,
Block,
}
/// Makes a doc string more presentable to users.
/// Used by rustdoc and perhaps other tools, but not by rustc.
///
/// See `rustc_ast/util/comments.rs`
fn beautify_doc_string(data: String, kind: CommentKind) -> String {
fn get_vertical_trim(lines: &[&str]) -> Option<(usize, usize)> {
let mut i = 0;
let mut j = lines.len();
// first line of all-stars should be omitted
if !lines.is_empty() && lines[0].chars().all(|c| c == '*') {
i += 1;
}
// like the first, a last line of all stars should be omitted
if j > i && !lines[j - 1].is_empty() && lines[j - 1].chars().all(|c| c == '*') {
j -= 1;
}
if i != 0 || j != lines.len() {
Some((i, j))
} else {
None
}
}
fn get_horizontal_trim(lines: &[&str], kind: CommentKind) -> Option<String> {
let mut i = usize::MAX;
let mut first = true;
// In case we have doc comments like `/**` or `/*!`, we want to remove stars if they are
// present. However, we first need to strip the empty lines so they don't get in the middle
// when we try to compute the "horizontal trim".
let lines = match kind {
CommentKind::Block => {
// Whatever happens, we skip the first line.
let mut i = lines
.get(0)
.map(|l| {
if l.trim_start().starts_with('*') {
0
} else {
1
}
})
.unwrap_or(0);
let mut j = lines.len();
while i < j && lines[i].trim().is_empty() {
i += 1;
}
while j > i && lines[j - 1].trim().is_empty() {
j -= 1;
}
&lines[i..j]
}
CommentKind::Line => lines,
};
for line in lines {
for (j, c) in line.chars().enumerate() {
if j > i || !"* \t".contains(c) {
return None;
}
if c == '*' {
if first {
i = j;
first = false;
} else if i != j {
return None;
}
break;
}
}
if i >= line.len() {
return None;
}
}
if lines.is_empty() {
None
} else {
Some(lines[0][..i].into())
}
}
let data_s = data.as_str();
if data_s.contains('\n') {
let mut lines = data_s.lines().collect::<Vec<&str>>();
let mut changes = false;
let lines = if let Some((i, j)) = get_vertical_trim(&lines) {
changes = true;
// remove whitespace-only lines from the start/end of lines
&mut lines[i..j]
} else {
&mut lines
};
if let Some(horizontal) = get_horizontal_trim(lines, kind) {
changes = true;
// remove a "[ \t]*\*" block from each line, if possible
for line in lines.iter_mut() {
if let Some(tmp) = line.strip_prefix(&horizontal) {
*line = tmp;
if kind == CommentKind::Block
&& (*line == "*" || line.starts_with("* ") || line.starts_with("**"))
{
*line = &line[1..];
}
}
}
}
if changes {
return lines.join("\n");
}
}
data
}
/// Removes excess indentation on comments in order for the Markdown
/// to be parsed correctly. This is necessary because the convention for
/// writing documentation is to provide a space between the /// or //! marker
/// and the doc text, but Markdown is whitespace-sensitive. For example,
/// a block of text with four-space indentation is parsed as a code block,
/// so if we didn't unindent comments, these list items
///
/// /// A list:
/// ///
/// /// - Foo
/// /// - Bar
///
/// would be parsed as if they were in a code block, which is likely not what the user intended.
///
/// See also `rustc_resolve/rustdoc.rs`
fn unindent_doc_fragments(docs: &mut [DocFragment]) {
// HACK: We can't tell the difference between `#[doc]` and doc-comments, so we can't specialize
// the indentation like rustodc does
let add = 0;
// `min_indent` is used to know how much whitespaces from the start of each lines must be
// removed. Example:
//
// ```
// /// hello!
// #[doc = "another"]
// ```
//
// In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
// 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
// (5 - 1) whitespaces.
let Some(min_indent) = docs
.iter()
.map(|fragment| {
fragment
.doc
.as_str()
.lines()
.fold(usize::MAX, |min_indent, line| {
if line.chars().all(|c| c.is_whitespace()) {
min_indent
} else {
// Compare against either space or tab, ignoring whether they are
// mixed or not.
let whitespace =
line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
min_indent.min(whitespace)
}
})
})
.min()
else {
return;
};
for fragment in docs {
if fragment.doc.is_empty() {
continue;
}
let min_indent = if min_indent > 0 {
min_indent - add
} else {
min_indent
};
fragment.indent = min_indent;
}
}
/// The goal of this function is to apply the `DocFragment` transformation that is required when
/// transforming into the final Markdown, which is applying the computed indent to each line in
/// each doc fragment (a `DocFragment` can contain multiple lines in case of `#[doc = ""]`).
///
/// Note: remove the trailing newline where appropriate
///
/// See also `rustc_resolve/rustdoc.rs`
fn add_doc_fragment(out: &mut String, frag: &DocFragment) {
let s = frag.doc.as_str();
let mut iter = s.lines();
if s.is_empty() {
out.push('\n');
return;
}
while let Some(line) = iter.next() {
if line.chars().any(|c| !c.is_whitespace()) {
assert!(line.len() >= frag.indent);
out.push_str(&line[frag.indent..]);
} else {
out.push_str(line);
}
out.push('\n');
}
}
/// Extracts the first `Cargo` fenced code block from a chunk of Markdown.
fn extract_manifest(comment: &str) -> CargoResult<Option<String>> {
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
// To match librustdoc/html/markdown.rs, opts.
let exts = Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES;
let md = Parser::new_ext(comment, exts);
let mut inside = false;
let mut output = None;
for item in md {
match item {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info)))
if info.to_lowercase() == "cargo" =>
{
if output.is_some() {
anyhow::bail!("multiple `cargo` manifests present")
} else {
output = Some(String::new());
}
inside = true;
}
Event::Text(ref text) if inside => {
let s = output.get_or_insert(String::new());
s.push_str(text);
}
Event::End(Tag::CodeBlock(_)) if inside => {
inside = false;
}
_ => (),
}
}
Ok(output)
}
#[cfg(test)]
mod test_expand {
use super::*;
@ -627,10 +301,10 @@ strip = true
#[test]
fn test_dependencies() {
snapbox::assert_eq(
snapbox::assert_matches(
r#"[[bin]]
name = "test-"
path = "/home/me/test.rs"
path = [..]
[dependencies]
time = "0.1.25"
@ -649,361 +323,12 @@ strip = true
[workspace]
"#,
si!(r#"
//! ```cargo
//! [dependencies]
//! time="0.1.25"
//! ```
si!(r#"```cargo
[dependencies]
time="0.1.25"
```
fn main() {}
"#),
);
}
}
#[cfg(test)]
mod test_comment {
use super::*;
macro_rules! ec {
($s:expr) => {
extract_comment($s)
.unwrap_or_else(|err| panic!("{}", err))
.unwrap()
};
}
#[test]
fn test_no_comment() {
assert_eq!(
None,
extract_comment(
r#"
fn main () {
}
"#,
)
.unwrap()
);
}
#[test]
fn test_no_comment_she_bang() {
assert_eq!(
None,
extract_comment(
r#"#!/usr/bin/env cargo-eval
fn main () {
}
"#,
)
.unwrap()
);
}
#[test]
fn test_comment() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r#"//! Here is a manifest:
//!
//! ```cargo
//! [dependencies]
//! time = "*"
//! ```
fn main() {}
"#),
);
}
#[test]
fn test_comment_shebang() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r#"#!/usr/bin/env cargo-eval
//! Here is a manifest:
//!
//! ```cargo
//! [dependencies]
//! time = "*"
//! ```
fn main() {}
"#),
);
}
#[test]
fn test_multiline_comment() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r#"/*!
Here is a manifest:
```cargo
[dependencies]
time = "*"
```
*/
fn main() {
}
"#),
);
}
#[test]
fn test_multiline_comment_shebang() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r#"#!/usr/bin/env cargo-eval
/*!
Here is a manifest:
```cargo
[dependencies]
time = "*"
```
*/
fn main() {
}
"#),
);
}
#[test]
fn test_multiline_block_comment() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r#"/*!
* Here is a manifest:
*
* ```cargo
* [dependencies]
* time = "*"
* ```
*/
fn main() {}
"#),
);
}
#[test]
fn test_multiline_block_comment_shebang() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r#"#!/usr/bin/env cargo-eval
/*!
* Here is a manifest:
*
* ```cargo
* [dependencies]
* time = "*"
* ```
*/
fn main() {}
"#),
);
}
#[test]
fn test_adjacent_comments() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r#"#!/usr/bin/env cargo-eval
// I am a normal comment
//! Here is a manifest:
//!
//! ```cargo
//! [dependencies]
//! time = "*"
//! ```
fn main () {
}
"#),
);
}
#[test]
fn test_doc_attrib() {
snapbox::assert_eq(
r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#,
ec!(r###"#!/usr/bin/env cargo-eval
#![doc = r#"Here is a manifest:
```cargo
[dependencies]
time = "*"
```
"#]
fn main () {
}
"###),
);
}
}
#[cfg(test)]
mod test_manifest {
use super::*;
macro_rules! smm {
($c:expr) => {
extract_manifest($c)
};
}
#[test]
fn test_no_code_fence() {
assert_eq!(
smm!(
r#"There is no manifest in this comment.
"#
)
.unwrap(),
None
);
}
#[test]
fn test_no_cargo_code_fence() {
assert_eq!(
smm!(
r#"There is no manifest in this comment.
```
This is not a manifest.
```
```rust
println!("Nor is this.");
```
Or this.
"#
)
.unwrap(),
None
);
}
#[test]
fn test_cargo_code_fence() {
assert_eq!(
smm!(
r#"This is a manifest:
```cargo
dependencies = { time = "*" }
```
"#
)
.unwrap(),
Some(
r#"dependencies = { time = "*" }
"#
.into()
)
);
}
#[test]
fn test_mixed_code_fence() {
assert_eq!(
smm!(
r#"This is *not* a manifest:
```
He's lying, I'm *totally* a manifest!
```
This *is*:
```cargo
dependencies = { time = "*" }
```
"#
)
.unwrap(),
Some(
r#"dependencies = { time = "*" }
"#
.into()
)
);
}
#[test]
fn test_two_cargo_code_fence() {
assert!(smm!(
r#"This is a manifest:
```cargo
dependencies = { time = "*" }
```
So is this, but it doesn't count:
```cargo
dependencies = { explode = true }
```
"#
)
.is_err());
}
}