From 8f266f172a5fa7307549ad90af59c01fbf0d8a69 Mon Sep 17 00:00:00 2001 From: oxalica Date: Thu, 3 Oct 2024 09:33:06 -0400 Subject: [PATCH] Keep indentation in doc comments and simplify 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. --- utoipa-gen/CHANGELOG.md | 1 + utoipa-gen/src/doc_comment.rs | 86 +++++++++++++++------------------ utoipa-gen/tests/path_derive.rs | 9 ++-- 3 files changed, 45 insertions(+), 51 deletions(-) diff --git a/utoipa-gen/CHANGELOG.md b/utoipa-gen/CHANGELOG.md index 48ef6359..c725cf63 100644 --- a/utoipa-gen/CHANGELOG.md +++ b/utoipa-gen/CHANGELOG.md @@ -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) diff --git a/utoipa-gen/src/doc_comment.rs b/utoipa-gen/src/doc_comment.rs index 9ec682f0..1105136b 100644 --- a/utoipa-gen/src/doc_comment.rs +++ b/utoipa-gen/src/doc_comment.rs @@ -1,6 +1,3 @@ -use std::ops::Deref; - -use proc_macro2::Ident; use syn::{Attribute, Expr, Lit, Meta}; const DOC_ATTRIBUTE_TYPE: &str = "doc"; @@ -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>(attributes: I) -> Vec { - attributes - .into_iter() - .filter_map(Self::parse_doc_comment) - .collect() - } - - fn parse_doc_comment(attribute: &Attribute) -> Option { - 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::>(); + // 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; - 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") } } diff --git a/utoipa-gen/tests/path_derive.rs b/utoipa-gen/tests/path_derive.rs index e0cb58fb..16822638 100644 --- a/utoipa-gen/tests/path_derive.rs +++ b/utoipa-gen/tests/path_derive.rs @@ -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] } @@ -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"