Skip to content

Commit

Permalink
Keep indentation in doc comments and simplify (#1082)
Browse files Browse the repository at this point in the history
Leading spaces is significant for Markdown and we should not blindlessly
trim them, which may break Markdown lists or code.

Here we strip the minimal indentation of all non-empty doc comment
lines, so we can both keep Markdown semantics and avoid unnecessary
leading spaces immediately after the `///` token.
  • Loading branch information
oxalica authored Oct 4, 2024
1 parent 0a83458 commit 33e64c7
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 51 deletions.
1 change: 1 addition & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

### Fixed

* Fix doc comment trimming to keep relative indentation. (https://github.com/juhaku/utoipa/pull/1082)
* Fix generic aliases (https://github.com/juhaku/utoipa/pull/1083)
* Fix nest path config struct name (https://github.com/juhaku/utoipa/pull/1081)
* Fix `as` attribute path format (https://github.com/juhaku/utoipa/pull/1080)
Expand Down
86 changes: 39 additions & 47 deletions utoipa-gen/src/doc_comment.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
use std::ops::Deref;

use proc_macro2::Ident;
use syn::{Attribute, Expr, Lit, Meta};

const DOC_ATTRIBUTE_TYPE: &str = "doc";
Expand All @@ -13,57 +10,52 @@ impl CommentAttributes {
/// Creates new [`CommentAttributes`] instance from [`Attribute`] slice filtering out all
/// other attributes which are not `doc` comments
pub(crate) fn from_attributes(attributes: &[Attribute]) -> Self {
Self(Self::as_string_vec(
attributes.iter().filter(Self::is_doc_attribute),
))
}

fn is_doc_attribute(attribute: &&Attribute) -> bool {
match Self::get_attribute_ident(attribute) {
Some(attribute) => attribute == DOC_ATTRIBUTE_TYPE,
None => false,
}
}

fn get_attribute_ident(attribute: &Attribute) -> Option<&Ident> {
attribute.path().get_ident()
}

fn as_string_vec<'a, I: Iterator<Item = &'a Attribute>>(attributes: I) -> Vec<String> {
attributes
.into_iter()
.filter_map(Self::parse_doc_comment)
.collect()
}

fn parse_doc_comment(attribute: &Attribute) -> Option<String> {
match &attribute.meta {
Meta::NameValue(name_value) => {
if let Expr::Lit(ref doc_comment) = name_value.value {
if let Lit::Str(ref comment) = doc_comment.lit {
Some(comment.value().trim().to_string())
} else {
None
let mut docs = attributes
.iter()
.filter_map(|attr| {
if !matches!(attr.path().get_ident(), Some(ident) if ident == DOC_ATTRIBUTE_TYPE) {
return None;
}
// ignore `#[doc(hidden)]` and similar tags.
if let Meta::NameValue(name_value) = &attr.meta {
if let Expr::Lit(ref doc_comment) = name_value.value {
if let Lit::Str(ref doc) = doc_comment.lit {
let mut doc = doc.value();
// NB. Only trim trailing whitespaces. Leading whitespaces are handled
// below.
doc.truncate(doc.trim_end().len());
return Some(doc);
}
}
} else {
None
}
None
})
.collect::<Vec<_>>();
// Calculate the minimum indentation of all non-empty lines and strip them.
// This can get rid of typical single space after doc comment start `///`, but not messing
// up indentation of markdown list or code.
let min_indent = docs
.iter()
.filter(|s| !s.is_empty())
// Only recognize ASCII space, not unicode multi-bytes ones.
// `str::trim_ascii_start` requires 1.80 which is greater than our MSRV yet.
.map(|s| s.len() - s.trim_start_matches(' ').len())
.min()
.unwrap_or(0);
for line in &mut docs {
if !line.is_empty() {
line.drain(..min_indent);
}
// ignore `#[doc(hidden)]` and similar tags.
_ => None,
}
Self(docs)
}

/// Returns found `doc comments` as formatted `String` joining them all with `\n` _(new line)_.
pub(crate) fn as_formatted_string(&self) -> String {
self.join("\n")
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

impl Deref for CommentAttributes {
type Target = Vec<String>;

fn deref(&self) -> &Self::Target {
&self.0
/// Returns found `doc comments` as formatted `String` joining them all with `\n` _(new line)_.
pub(crate) fn as_formatted_string(&self) -> String {
self.0.join("\n")
}
}
9 changes: 5 additions & 4 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,10 @@ test_api_fn! {
/// Additional info in long description
///
/// With more info on separate lines
/// containing text.
///
/// Yeah
/// containing markdown:
/// - A
/// Indented.
/// - B
#[deprecated]
}

Expand All @@ -164,7 +165,7 @@ fn derive_path_with_all_info_success() {
common::assert_json_array_len(operation.pointer("/parameters").unwrap(), 1);
assert_value! {operation=>
"deprecated" = r#"true"#, "Api fn deprecated status"
"description" = r#""Additional info in long description\n\nWith more info on separate lines\ncontaining text.\n\nYeah""#, "Api fn description"
"description" = r#""Additional info in long description\n\nWith more info on separate lines\ncontaining markdown:\n- A\n Indented.\n- B""#, "Api fn description"
"summary" = r#""This is test operation long multiline\nsummary. That need to be correctly split.""#, "Api fn summary"
"operationId" = r#""foo_bar_id""#, "Api fn operation_id"
"tags.[0]" = r#""custom_tag""#, "Api fn tag"
Expand Down

0 comments on commit 33e64c7

Please sign in to comment.