Correctly differentiate between sugared and raw doc comments

This commit is contained in:
Guillaume Gomez
2025-11-28 16:08:24 +01:00
parent 04bcd83eb5
commit 148e522112
10 changed files with 117 additions and 59 deletions
+37 -10
View File
@@ -13,7 +13,9 @@
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 @@ fn value_str(&self) -> Option<Symbol> {
}
/// 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 @@ fn value_str(&self) -> Option<Symbol> {
}
}
/// Returns the span in:
///
/// ```text
/// #[attribute = "value"]
/// ^^^^^^^
/// ```
///
/// It returns `None` in any other cases like:
///
/// ```text
/// #[attr("value")]
/// ```
fn value_span(&self) -> Option<Span> {
match &self.args {
AttrArgs::Eq { expr, .. } => Some(expr.span),
AttrArgs::Delimited(_) | AttrArgs::Empty => None,
}
}
pub fn meta(&self, span: Span) -> Option<MetaItem> {
Some(MetaItem {
unsafety: Safety::Default,
@@ -825,7 +852,7 @@ fn is_proc_macro_attr(&self) -> bool {
/// * `/** 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 @@ pub fn is_proc_macro_attr(&self) -> bool {
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)
}
}
+25 -1
View File
@@ -16,7 +16,31 @@
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,
+23 -9
View File
@@ -10,7 +10,7 @@
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 is_punct(tt: &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 @@ fn print_attribute_inline(&mut self, attr: &ast::Attribute, is_inline: bool) ->
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 @@ fn token_kind_to_string_ext(
/* 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 => "<eof>".into(),
}
@@ -474,7 +474,9 @@ fn accept_single_doc_attr<'c, S: Stage>(
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"
);
}
}
}
+4 -4
View File
@@ -2,7 +2,7 @@
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 @@ pub fn parse_attribute_list(
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 @@ pub fn parse_attribute_list(
{
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;
@@ -3,7 +3,7 @@
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,
@@ -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 @@ fn print_attribute(&self, p: &mut Printer) {
Align,
AttrStyle,
CommentKind,
DocFragmentKind,
Transparency,
SanitizerSet,
);
+4 -4
View File
@@ -4,7 +4,7 @@
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 @@ fn is_automatically_derived_attr(&self) -> bool {
}
#[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 @@ pub fn is_proc_macro_attr(&self) -> bool {
}
#[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)
}
}
+16 -26
View File
@@ -10,6 +10,7 @@
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 @@
#[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::<A>::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
+1 -1
View File
@@ -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);