From 148e522112d65d23c8615e22da65a64e319674bb Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 28 Nov 2025 16:08:24 +0100 Subject: [PATCH] Correctly differentiate between sugared and raw doc comments --- compiler/rustc_ast/src/attr/mod.rs | 47 +++++++++++++++---- compiler/rustc_ast/src/token.rs | 26 +++++++++- compiler/rustc_ast_pretty/src/pprust/state.rs | 32 +++++++++---- .../rustc_attr_parsing/src/attributes/doc.rs | 4 +- compiler/rustc_attr_parsing/src/interface.rs | 8 ++-- .../rustc_hir/src/attrs/data_structures.rs | 4 +- .../rustc_hir/src/attrs/pretty_printing.rs | 3 +- compiler/rustc_hir/src/hir.rs | 8 ++-- compiler/rustc_resolve/src/rustdoc.rs | 42 +++++++---------- src/librustdoc/clean/types.rs | 2 +- 10 files changed, 117 insertions(+), 59 deletions(-) diff --git a/compiler/rustc_ast/src/attr/mod.rs b/compiler/rustc_ast/src/attr/mod.rs index d54d900128bd..94e7462d26df 100644 --- a/compiler/rustc_ast/src/attr/mod.rs +++ b/compiler/rustc_ast/src/attr/mod.rs @@ -13,7 +13,9 @@ use crate::ast::{ Expr, ExprKind, LitKind, MetaItem, MetaItemInner, MetaItemKind, MetaItemLit, NormalAttr, Path, PathSegment, Safety, }; -use crate::token::{self, CommentKind, Delimiter, InvisibleOrigin, MetaVarKind, Token}; +use crate::token::{ + self, CommentKind, Delimiter, DocFragmentKind, InvisibleOrigin, MetaVarKind, Token, +}; use crate::tokenstream::{ DelimSpan, LazyAttrTokenStream, Spacing, TokenStream, TokenStreamIter, TokenTree, }; @@ -179,15 +181,21 @@ impl AttributeExt for Attribute { } /// Returns the documentation and its kind if this is a doc comment or a sugared doc comment. - /// * `///doc` returns `Some(("doc", CommentKind::Line))`. - /// * `/** doc */` returns `Some(("doc", CommentKind::Block))`. - /// * `#[doc = "doc"]` returns `Some(("doc", CommentKind::Line))`. + /// * `///doc` returns `Some(("doc", DocFragmentKind::Sugared(CommentKind::Line)))`. + /// * `/** doc */` returns `Some(("doc", DocFragmentKind::Sugared(CommentKind::Block)))`. + /// * `#[doc = "doc"]` returns `Some(("doc", DocFragmentKind::Raw))`. /// * `#[doc(...)]` returns `None`. - fn doc_str_and_comment_kind(&self) -> Option<(Symbol, CommentKind)> { + fn doc_str_and_fragment_kind(&self) -> Option<(Symbol, DocFragmentKind)> { match &self.kind { - AttrKind::DocComment(kind, data) => Some((*data, *kind)), + AttrKind::DocComment(kind, data) => Some((*data, DocFragmentKind::Sugared(*kind))), AttrKind::Normal(normal) if normal.item.path == sym::doc => { - normal.item.value_str().map(|s| (s, CommentKind::Line)) + if let Some(value) = normal.item.value_str() + && let Some(value_span) = normal.item.value_span() + { + Some((value, DocFragmentKind::Raw(value_span))) + } else { + None + } } _ => None, } @@ -305,6 +313,25 @@ impl AttrItem { } } + /// Returns the span in: + /// + /// ```text + /// #[attribute = "value"] + /// ^^^^^^^ + /// ``` + /// + /// It returns `None` in any other cases like: + /// + /// ```text + /// #[attr("value")] + /// ``` + fn value_span(&self) -> Option { + match &self.args { + AttrArgs::Eq { expr, .. } => Some(expr.span), + AttrArgs::Delimited(_) | AttrArgs::Empty => None, + } + } + pub fn meta(&self, span: Span) -> Option { Some(MetaItem { unsafety: Safety::Default, @@ -825,7 +852,7 @@ pub trait AttributeExt: Debug { /// * `/** doc */` returns `Some(("doc", CommentKind::Block))`. /// * `#[doc = "doc"]` returns `Some(("doc", CommentKind::Line))`. /// * `#[doc(...)]` returns `None`. - fn doc_str_and_comment_kind(&self) -> Option<(Symbol, CommentKind)>; + fn doc_str_and_fragment_kind(&self) -> Option<(Symbol, DocFragmentKind)>; /// Returns outer or inner if this is a doc attribute or a sugared doc /// comment, otherwise None. @@ -910,7 +937,7 @@ impl Attribute { AttributeExt::is_proc_macro_attr(self) } - pub fn doc_str_and_comment_kind(&self) -> Option<(Symbol, CommentKind)> { - AttributeExt::doc_str_and_comment_kind(self) + pub fn doc_str_and_fragment_kind(&self) -> Option<(Symbol, DocFragmentKind)> { + AttributeExt::doc_str_and_fragment_kind(self) } } diff --git a/compiler/rustc_ast/src/token.rs b/compiler/rustc_ast/src/token.rs index e1231312a2af..accf4d181632 100644 --- a/compiler/rustc_ast/src/token.rs +++ b/compiler/rustc_ast/src/token.rs @@ -16,7 +16,31 @@ use rustc_span::{Ident, Symbol}; use crate::ast; use crate::util::case::Case; -#[derive(Clone, Copy, PartialEq, Encodable, Decodable, Debug, HashStable_Generic)] +/// Represents the kind of doc comment it is, ie `///` or `#[doc = ""]`. +#[derive(Clone, Copy, PartialEq, Eq, Encodable, Decodable, Debug, HashStable_Generic)] +pub enum DocFragmentKind { + /// A sugared doc comment: `///` or `//!` or `/**` or `/*!`. + Sugared(CommentKind), + /// A "raw" doc comment: `#[doc = ""]`. The `Span` represents the string literal. + Raw(Span), +} + +impl DocFragmentKind { + pub fn is_sugared(self) -> bool { + matches!(self, Self::Sugared(_)) + } + + /// If it is `Sugared`, it will return its associated `CommentKind`, otherwise it will return + /// `CommentKind::Line`. + pub fn comment_kind(self) -> CommentKind { + match self { + Self::Sugared(kind) => kind, + Self::Raw(_) => CommentKind::Line, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Encodable, Decodable, Debug, HashStable_Generic)] pub enum CommentKind { Line, Block, diff --git a/compiler/rustc_ast_pretty/src/pprust/state.rs b/compiler/rustc_ast_pretty/src/pprust/state.rs index 3dce0498efbf..35e47fed9f7a 100644 --- a/compiler/rustc_ast_pretty/src/pprust/state.rs +++ b/compiler/rustc_ast_pretty/src/pprust/state.rs @@ -10,7 +10,7 @@ use std::borrow::Cow; use std::sync::Arc; use rustc_ast::attr::AttrIdGenerator; -use rustc_ast::token::{self, CommentKind, Delimiter, Token, TokenKind}; +use rustc_ast::token::{self, CommentKind, Delimiter, DocFragmentKind, Token, TokenKind}; use rustc_ast::tokenstream::{Spacing, TokenStream, TokenTree}; use rustc_ast::util::classify; use rustc_ast::util::comments::{Comment, CommentStyle}; @@ -381,15 +381,24 @@ fn space_between(tt1: &TokenTree, tt2: &TokenTree) -> bool { } pub fn doc_comment_to_string( - comment_kind: CommentKind, + fragment_kind: DocFragmentKind, attr_style: ast::AttrStyle, data: Symbol, ) -> String { - match (comment_kind, attr_style) { - (CommentKind::Line, ast::AttrStyle::Outer) => format!("///{data}"), - (CommentKind::Line, ast::AttrStyle::Inner) => format!("//!{data}"), - (CommentKind::Block, ast::AttrStyle::Outer) => format!("/**{data}*/"), - (CommentKind::Block, ast::AttrStyle::Inner) => format!("/*!{data}*/"), + match fragment_kind { + DocFragmentKind::Sugared(comment_kind) => match (comment_kind, attr_style) { + (CommentKind::Line, ast::AttrStyle::Outer) => format!("///{data}"), + (CommentKind::Line, ast::AttrStyle::Inner) => format!("//!{data}"), + (CommentKind::Block, ast::AttrStyle::Outer) => format!("/**{data}*/"), + (CommentKind::Block, ast::AttrStyle::Inner) => format!("/*!{data}*/"), + }, + DocFragmentKind::Raw(_) => { + format!( + "#{}[doc = {:?}]", + if attr_style == ast::AttrStyle::Inner { "!" } else { "" }, + data.to_string(), + ) + } } } @@ -665,7 +674,11 @@ pub trait PrintState<'a>: std::ops::Deref + std::ops::Dere self.word("]"); } ast::AttrKind::DocComment(comment_kind, data) => { - self.word(doc_comment_to_string(*comment_kind, attr.style, *data)); + self.word(doc_comment_to_string( + DocFragmentKind::Sugared(*comment_kind), + attr.style, + *data, + )); self.hardbreak() } } @@ -1029,7 +1042,8 @@ pub trait PrintState<'a>: std::ops::Deref + std::ops::Dere /* Other */ token::DocComment(comment_kind, attr_style, data) => { - doc_comment_to_string(comment_kind, attr_style, data).into() + doc_comment_to_string(DocFragmentKind::Sugared(comment_kind), attr_style, data) + .into() } token::Eof => "".into(), } diff --git a/compiler/rustc_attr_parsing/src/attributes/doc.rs b/compiler/rustc_attr_parsing/src/attributes/doc.rs index fbe1ac63ab9c..1a7d8ec93f70 100644 --- a/compiler/rustc_attr_parsing/src/attributes/doc.rs +++ b/compiler/rustc_attr_parsing/src/attributes/doc.rs @@ -474,7 +474,9 @@ impl DocParser { if nv.value_as_str().is_none() { cx.expected_string_literal(nv.value_span, Some(nv.value_as_lit())); } else { - unreachable!("Should have been handled at the same time as sugar-syntaxed doc comments"); + unreachable!( + "Should have been handled at the same time as sugar-syntaxed doc comments" + ); } } } diff --git a/compiler/rustc_attr_parsing/src/interface.rs b/compiler/rustc_attr_parsing/src/interface.rs index feb7bbcb6234..b1538b447da0 100644 --- a/compiler/rustc_attr_parsing/src/interface.rs +++ b/compiler/rustc_attr_parsing/src/interface.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use rustc_ast as ast; use rustc_ast::{AttrStyle, NodeId, Safety}; -use rustc_ast::token::CommentKind; +use rustc_ast::token::DocFragmentKind; use rustc_errors::DiagCtxtHandle; use rustc_feature::{AttributeTemplate, Features}; use rustc_hir::attrs::AttributeKind; @@ -295,7 +295,7 @@ impl<'sess, S: Stage> AttributeParser<'sess, S> { attributes.push(Attribute::Parsed(AttributeKind::DocComment { style: attr.style, - kind: *comment_kind, + kind: DocFragmentKind::Sugared(*comment_kind), span: lower_span(attr.span), comment: *symbol, })) @@ -350,8 +350,8 @@ impl<'sess, S: Stage> AttributeParser<'sess, S> { { attributes.push(Attribute::Parsed(AttributeKind::DocComment { style: attr.style, - kind: CommentKind::Block, - span: nv.value_span, + kind: DocFragmentKind::Raw(nv.value_span), + span: attr.span, comment, })); continue; diff --git a/compiler/rustc_hir/src/attrs/data_structures.rs b/compiler/rustc_hir/src/attrs/data_structures.rs index 39008914f9ef..87a4cf7823f5 100644 --- a/compiler/rustc_hir/src/attrs/data_structures.rs +++ b/compiler/rustc_hir/src/attrs/data_structures.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; pub use ReprAttr::*; use rustc_abi::Align; -use rustc_ast::token::CommentKind; +use rustc_ast::token::DocFragmentKind; use rustc_ast::{AttrStyle, ast}; use rustc_data_structures::fx::FxIndexMap; use rustc_error_messages::{DiagArgValue, IntoDiagArg}; @@ -648,7 +648,7 @@ pub enum AttributeKind { /// Represents specifically [`#[doc = "..."]`](https://doc.rust-lang.org/stable/rustdoc/write-documentation/the-doc-attribute.html). /// i.e. doc comments. - DocComment { style: AttrStyle, kind: CommentKind, span: Span, comment: Symbol }, + DocComment { style: AttrStyle, kind: DocFragmentKind, span: Span, comment: Symbol }, /// Represents `#[rustc_dummy]`. Dummy, diff --git a/compiler/rustc_hir/src/attrs/pretty_printing.rs b/compiler/rustc_hir/src/attrs/pretty_printing.rs index 75886fb08a2e..29df586ed296 100644 --- a/compiler/rustc_hir/src/attrs/pretty_printing.rs +++ b/compiler/rustc_hir/src/attrs/pretty_printing.rs @@ -1,7 +1,7 @@ use std::num::NonZero; use rustc_abi::Align; -use rustc_ast::token::CommentKind; +use rustc_ast::token::{CommentKind, DocFragmentKind}; use rustc_ast::{AttrStyle, IntTy, UintTy}; use rustc_ast_pretty::pp::Printer; use rustc_data_structures::fx::FxIndexMap; @@ -167,6 +167,7 @@ print_debug!( Align, AttrStyle, CommentKind, + DocFragmentKind, Transparency, SanitizerSet, ); diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index e6a0f430b63a..afa1a33fe769 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -4,7 +4,7 @@ use std::fmt; use rustc_abi::ExternAbi; use rustc_ast::attr::AttributeExt; -use rustc_ast::token::CommentKind; +use rustc_ast::token::DocFragmentKind; use rustc_ast::util::parser::ExprPrecedence; use rustc_ast::{ self as ast, FloatTy, InlineAsmOptions, InlineAsmTemplatePiece, IntTy, Label, LitIntType, @@ -1385,7 +1385,7 @@ impl AttributeExt for Attribute { } #[inline] - fn doc_str_and_comment_kind(&self) -> Option<(Symbol, CommentKind)> { + fn doc_str_and_fragment_kind(&self) -> Option<(Symbol, DocFragmentKind)> { match &self { Attribute::Parsed(AttributeKind::DocComment { kind, comment, .. }) => { Some((*comment, *kind)) @@ -1503,8 +1503,8 @@ impl Attribute { } #[inline] - pub fn doc_str_and_comment_kind(&self) -> Option<(Symbol, CommentKind)> { - AttributeExt::doc_str_and_comment_kind(self) + pub fn doc_str_and_fragment_kind(&self) -> Option<(Symbol, DocFragmentKind)> { + AttributeExt::doc_str_and_fragment_kind(self) } } diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 3c0a89b7c8a7..0ac8e68ad968 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -10,6 +10,7 @@ use pulldown_cmark::{ use rustc_ast as ast; use rustc_ast::attr::AttributeExt; use rustc_ast::join_path_syms; +use rustc_ast::token::DocFragmentKind; use rustc_ast::util::comments::beautify_doc_string; use rustc_data_structures::fx::FxIndexMap; use rustc_data_structures::unord::UnordSet; @@ -23,14 +24,6 @@ use tracing::{debug, trace}; #[cfg(test)] mod tests; -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum DocFragmentKind { - /// A doc fragment created from a `///` or `//!` doc comment. - SugaredDoc, - /// A doc fragment created from a "raw" `#[doc=""]` attribute. - RawDoc, -} - /// A portion of documentation, extracted from a `#[doc]` attribute. /// /// Each variant contains the line number within the complete doc-comment where the fragment @@ -125,7 +118,7 @@ pub fn unindent_doc_fragments(docs: &mut [DocFragment]) { // // In this case, you want "hello! another" and not "hello! another". let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind) - && docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc) + && docs.iter().any(|d| d.kind.is_sugared()) { // In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to // "decide" how much the minimum indent will be. @@ -155,8 +148,7 @@ pub fn unindent_doc_fragments(docs: &mut [DocFragment]) { // Compare against either space or tab, ignoring whether they are // mixed or not. let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count(); - whitespace - + (if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add }) + whitespace + (if fragment.kind.is_sugared() { 0 } else { add }) }) .min() .unwrap_or(usize::MAX) @@ -171,7 +163,7 @@ pub fn unindent_doc_fragments(docs: &mut [DocFragment]) { continue; } - let indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 { + let indent = if !fragment.kind.is_sugared() && min_indent > 0 { min_indent - add } else { min_indent @@ -214,19 +206,17 @@ pub fn attrs_to_doc_fragments<'a, A: AttributeExt + Clone + 'a>( let mut doc_fragments = Vec::with_capacity(size_hint); let mut other_attrs = ThinVec::::with_capacity(if doc_only { 0 } else { size_hint }); for (attr, item_id) in attrs { - if let Some((doc_str, comment_kind)) = attr.doc_str_and_comment_kind() { - let doc = beautify_doc_string(doc_str, comment_kind); - let (span, kind, from_expansion) = if let Some(span) = attr.is_doc_comment() { - (span, DocFragmentKind::SugaredDoc, span.from_expansion()) - } else { - let attr_span = attr.span(); - let (span, from_expansion) = match attr.value_span() { - Some(sp) => (sp.with_ctxt(attr_span.ctxt()), sp.from_expansion()), - None => (attr_span, attr_span.from_expansion()), - }; - (span, DocFragmentKind::RawDoc, from_expansion) + if let Some((doc_str, fragment_kind)) = attr.doc_str_and_fragment_kind() { + let doc = beautify_doc_string(doc_str, fragment_kind.comment_kind()); + let attr_span = attr.span(); + let (span, from_expansion) = match fragment_kind { + DocFragmentKind::Sugared(_) => (attr_span, attr_span.from_expansion()), + DocFragmentKind::Raw(value_span) => { + (value_span.with_ctxt(attr_span.ctxt()), value_span.from_expansion()) + } }; - let fragment = DocFragment { span, doc, kind, item_id, indent: 0, from_expansion }; + let fragment = + DocFragment { span, doc, kind: fragment_kind, item_id, indent: 0, from_expansion }; doc_fragments.push(fragment); } else if !doc_only { other_attrs.push(attr.clone()); @@ -571,7 +561,7 @@ pub fn source_span_for_markdown_range_inner( use rustc_span::BytePos; if let &[fragment] = &fragments - && fragment.kind == DocFragmentKind::RawDoc + && !fragment.kind.is_sugared() && let Ok(snippet) = map.span_to_snippet(fragment.span) && snippet.trim_end() == markdown.trim_end() && let Ok(md_range_lo) = u32::try_from(md_range.start) @@ -589,7 +579,7 @@ pub fn source_span_for_markdown_range_inner( )); } - let is_all_sugared_doc = fragments.iter().all(|frag| frag.kind == DocFragmentKind::SugaredDoc); + let is_all_sugared_doc = fragments.iter().all(|frag| frag.kind.is_sugared()); if !is_all_sugared_doc { // This case ignores the markdown outside of the range so that it can diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 12dcb198bd21..40191d5c98e0 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -2403,7 +2403,7 @@ mod size_asserts { use super::*; // tidy-alphabetical-start static_assert_size!(Crate, 16); // frequently moved by-value - static_assert_size!(DocFragment, 32); + static_assert_size!(DocFragment, 48); static_assert_size!(GenericArg, 32); static_assert_size!(GenericArgs, 24); static_assert_size!(GenericParamDef, 40);