Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep indentation in doc comments and simplify #1082

Merged
merged 1 commit into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading