Unnamed repository; edit this file 'description' to name the repository.
feat: support macro expansion in `#[doc = ...]` attributes
so1ve 8 weeks ago
parent 9b4098c · commit 0eebfac
-rw-r--r--crates/hir-def/src/attrs.rs379
-rw-r--r--crates/ide/src/hover/tests.rs172
2 files changed, 493 insertions, 58 deletions
diff --git a/crates/hir-def/src/attrs.rs b/crates/hir-def/src/attrs.rs
index e3e1aac709..f91c82d729 100644
--- a/crates/hir-def/src/attrs.rs
+++ b/crates/hir-def/src/attrs.rs
@@ -29,8 +29,10 @@ use base_db::Crate;
use cfg::{CfgExpr, CfgOptions};
use either::Either;
use hir_expand::{
- HirFileId, InFile, Lookup,
+ AstId, ExpandTo, HirFileId, InFile, Lookup,
attrs::{Meta, expand_cfg_attr, expand_cfg_attr_with_doc_comments},
+ mod_path::ModPath,
+ span_map::SpanMap,
};
use intern::Symbol;
use itertools::Itertools;
@@ -38,6 +40,7 @@ use la_arena::ArenaMap;
use rustc_abi::ReprOptions;
use rustc_hash::FxHashSet;
use smallvec::SmallVec;
+use span::AstIdMap;
use syntax::{
AstNode, AstToken, NodeOrToken, SmolStr, SourceFile, SyntaxNode, SyntaxToken, T,
ast::{self, AttrDocCommentIter, HasAttrs, IsString, TokenTreeChildren},
@@ -49,7 +52,9 @@ use crate::{
LocalFieldId, MacroId, ModuleId, TypeOrConstParamId, VariantId,
db::DefDatabase,
hir::generics::{GenericParams, LocalLifetimeParamId, LocalTypeOrConstParamId},
- nameres::ModuleOrigin,
+ macro_call_as_call_id,
+ nameres::{MacroSubNs, ModuleOrigin, crate_def_map},
+ resolver::{HasResolver, Resolver},
src::{HasChildSource, HasSource},
};
@@ -398,6 +403,28 @@ fn attrs_source(
(owner, None, None, krate)
}
+fn resolver_for_attr_def_id(db: &dyn DefDatabase, owner: AttrDefId) -> Resolver<'_> {
+ match owner {
+ AttrDefId::ModuleId(id) => id.resolver(db),
+ AttrDefId::AdtId(AdtId::StructId(id)) => id.resolver(db),
+ AttrDefId::AdtId(AdtId::UnionId(id)) => id.resolver(db),
+ AttrDefId::AdtId(AdtId::EnumId(id)) => id.resolver(db),
+ AttrDefId::FunctionId(id) => id.resolver(db),
+ AttrDefId::EnumVariantId(id) => id.resolver(db),
+ AttrDefId::StaticId(id) => id.resolver(db),
+ AttrDefId::ConstId(id) => id.resolver(db),
+ AttrDefId::TraitId(id) => id.resolver(db),
+ AttrDefId::TypeAliasId(id) => id.resolver(db),
+ AttrDefId::MacroId(MacroId::Macro2Id(id)) => id.resolver(db),
+ AttrDefId::MacroId(MacroId::MacroRulesId(id)) => id.resolver(db),
+ AttrDefId::MacroId(MacroId::ProcMacroId(id)) => id.resolver(db),
+ AttrDefId::ImplId(id) => id.resolver(db),
+ AttrDefId::ExternBlockId(id) => id.resolver(db),
+ AttrDefId::ExternCrateId(id) => id.resolver(db),
+ AttrDefId::UseId(id) => id.resolver(db),
+ }
+}
+
fn collect_attrs<BreakValue>(
db: &dyn DefDatabase,
owner: AttrDefId,
@@ -479,8 +506,9 @@ pub struct RustcLayoutScalarValidRange {
struct DocsSourceMapLine {
/// The offset in [`Docs::docs`].
string_offset: TextSize,
- /// The offset in the AST of the text.
- ast_offset: TextSize,
+ /// The offset in the AST of the text. `None` for macro-expanded doc strings
+ /// where we cannot provide a faithful source mapping.
+ ast_offset: Option<TextSize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -569,12 +597,14 @@ impl Docs {
source_map.partition_point(|line| line.string_offset <= string_range.start()) - 1;
let after_range = &source_map[after_range..];
let line = after_range.first()?;
+ // Unmapped lines (from macro-expanded docs) cannot be mapped back to AST.
+ let ast_offset = line.ast_offset?;
if after_range.get(1).is_some_and(|next_line| next_line.string_offset < string_range.end())
{
// The range is combined from two lines - cannot map it back.
return None;
}
- let ast_range = string_range - line.string_offset + line.ast_offset;
+ let ast_range = string_range - line.string_offset + ast_offset;
let is_inner = if inner_docs_start
.is_some_and(|inner_docs_start| string_range.start() >= inner_docs_start)
{
@@ -638,7 +668,7 @@ impl Docs {
for line in doc.split('\n') {
self.docs_source_map.push(DocsSourceMapLine {
string_offset: TextSize::of(&self.docs),
- ast_offset: offset_in_ast,
+ ast_offset: Some(offset_in_ast),
});
offset_in_ast += TextSize::of(line) + TextSize::of("\n");
@@ -652,6 +682,21 @@ impl Docs {
}
}
+ fn extend_with_unmapped_doc_str(&mut self, doc: &str, indent: &mut usize) {
+ for line in doc.split('\n') {
+ self.docs_source_map.push(DocsSourceMapLine {
+ string_offset: TextSize::of(&self.docs),
+ ast_offset: None,
+ });
+ let line = line.trim_end();
+ if let Some(line_indent) = line.chars().position(|ch| !ch.is_whitespace()) {
+ *indent = std::cmp::min(*indent, line_indent);
+ }
+ self.docs.push_str(line);
+ self.docs.push('\n');
+ }
+ }
+
fn remove_indent(&mut self, indent: usize, start_source_map_index: usize) {
/// In case of panics, we want to avoid corrupted UTF-8 in `self.docs`, so we clear it.
struct Guard<'a>(&'a mut Docs);
@@ -721,7 +766,9 @@ impl Docs {
// line should not get shifted (in general, the shift for the string offset is by the
// number of lines until the current one, excluding the current one).
line_source.string_offset -= accumulated_offset;
- line_source.ast_offset += indent_size;
+ if let Some(ref mut ast_offset) = line_source.ast_offset {
+ *ast_offset += indent_size;
+ }
accumulated_offset += indent_size;
}
@@ -757,6 +804,20 @@ pub struct DeriveInfo {
pub helpers: Box<[Symbol]>,
}
+struct DocMacroExpander<'db> {
+ db: &'db dyn DefDatabase,
+ krate: Crate,
+ recursion_depth: usize,
+ recursion_limit: usize,
+}
+
+struct DocExprSourceCtx<'db> {
+ resolver: Resolver<'db>,
+ file_id: HirFileId,
+ ast_id_map: &'db AstIdMap,
+ span_map: SpanMap,
+}
+
fn extract_doc_aliases(result: &mut Vec<Symbol>, attr: Meta) -> ControlFlow<Infallible> {
if let Meta::TokenTree { path, tt } = attr
&& path.is1("doc")
@@ -785,7 +846,125 @@ fn extract_cfgs(result: &mut Vec<CfgExpr>, attr: Meta) -> ControlFlow<Infallible
ControlFlow::Continue(())
}
-fn extract_docs<'a>(
+fn expand_doc_expr_via_macro_pipeline<'db>(
+ expander: &mut DocMacroExpander<'db>,
+ source_ctx: &DocExprSourceCtx<'db>,
+ expr: ast::Expr,
+) -> Option<String> {
+ match expr {
+ ast::Expr::Literal(literal) => match literal.kind() {
+ ast::LiteralKind::String(string) => string.value().ok().map(Into::into),
+ _ => None,
+ },
+ ast::Expr::MacroExpr(macro_expr) => {
+ let macro_call = macro_expr.macro_call()?;
+ let (expr, new_source_ctx) = expand_doc_macro_call(expander, source_ctx, macro_call)?;
+ // After expansion, the expr lives in the expansion file; use its source context.
+ expand_doc_expr_via_macro_pipeline(expander, &new_source_ctx, expr)
+ }
+ _ => None,
+ }
+}
+
+fn expand_doc_macro_call<'db>(
+ expander: &mut DocMacroExpander<'db>,
+ source_ctx: &DocExprSourceCtx<'db>,
+ macro_call: ast::MacroCall,
+) -> Option<(ast::Expr, DocExprSourceCtx<'db>)> {
+ if expander.recursion_depth >= expander.recursion_limit {
+ return None;
+ }
+
+ let path = macro_call.path()?;
+ let mod_path = ModPath::from_src(expander.db, path, &mut |range| {
+ source_ctx.span_map.span_for_range(range).ctx
+ })?;
+ let call_site = source_ctx.span_map.span_for_range(macro_call.syntax().text_range());
+ let ast_id = AstId::new(source_ctx.file_id, source_ctx.ast_id_map.ast_id(&macro_call));
+ let call_id = macro_call_as_call_id(
+ expander.db,
+ ast_id,
+ &mod_path,
+ call_site.ctx,
+ ExpandTo::Expr,
+ expander.krate,
+ |path| {
+ source_ctx.resolver.resolve_path_as_macro_def(expander.db, path, Some(MacroSubNs::Bang))
+ },
+ &mut |_, _| (),
+ )
+ .ok()?
+ .value?;
+
+ expander.recursion_depth += 1;
+ let parse = expander.db.parse_macro_expansion(call_id).value.0;
+ let expr = parse.cast::<ast::Expr>().map(|parse| parse.tree())?;
+ expander.recursion_depth -= 1;
+
+ // Build a new source context for the expansion file so that any further
+ // recursive expansion (e.g. a user macro expanding to `concat!(...)`)
+ // correctly resolves AstIds and spans in the expansion.
+ let expansion_file_id: HirFileId = call_id.into();
+ let new_source_ctx = DocExprSourceCtx {
+ resolver: source_ctx.resolver.clone(),
+ file_id: expansion_file_id,
+ ast_id_map: expander.db.ast_id_map(expansion_file_id),
+ span_map: expander.db.span_map(expansion_file_id),
+ };
+ Some((expr, new_source_ctx))
+}
+
+fn extend_with_attrs<'a, 'db>(
+ result: &mut Docs,
+ node: &SyntaxNode,
+ expect_inner_attrs: bool,
+ indent: &mut usize,
+ get_cfg_options: &dyn Fn() -> &'a CfgOptions,
+ cfg_options: &mut Option<&'a CfgOptions>,
+ mut expander: Option<&mut DocMacroExpander<'db>>,
+ source_ctx: Option<&DocExprSourceCtx<'db>>,
+) {
+ expand_cfg_attr_with_doc_comments::<_, Infallible>(
+ AttrDocCommentIter::from_syntax_node(node).filter(|attr| match attr {
+ Either::Left(attr) => attr.kind().is_inner() == expect_inner_attrs,
+ Either::Right(comment) => comment
+ .kind()
+ .doc
+ .is_some_and(|kind| (kind == ast::CommentPlacement::Inner) == expect_inner_attrs),
+ }),
+ || *cfg_options.get_or_insert_with(get_cfg_options),
+ |attr| {
+ match attr {
+ Either::Right(doc_comment) => result.extend_with_doc_comment(doc_comment, indent),
+ Either::Left((attr, _, _, top_attr)) => match attr {
+ Meta::NamedKeyValue { name: Some(name), value: Some(value), .. }
+ if name.text() == "doc" =>
+ {
+ result.extend_with_doc_attr(value, indent);
+ }
+ Meta::NamedKeyValue { name: Some(name), value: None, .. }
+ if name.text() == "doc" =>
+ {
+ if let (Some(expander), Some(source_ctx)) =
+ (expander.as_deref_mut(), source_ctx)
+ && let Some(expr) = top_attr.expr()
+ && let Some(expanded) =
+ expand_doc_expr_via_macro_pipeline(expander, source_ctx, expr)
+ {
+ result.extend_with_unmapped_doc_str(&expanded, indent);
+ }
+ }
+ _ => {}
+ },
+ }
+ ControlFlow::Continue(())
+ },
+ );
+}
+
+fn extract_docs<'a, 'db>(
+ mut expander: Option<&mut DocMacroExpander<'db>>,
+ resolver: Option<&Resolver<'db>>,
get_cfg_options: &dyn Fn() -> &'a CfgOptions,
source: InFile<ast::AnyHasAttrs>,
outer_mod_decl: Option<InFile<ast::Module>>,
@@ -802,49 +981,69 @@ fn extract_docs<'a>(
};
let mut cfg_options = None;
- let mut extend_with_attrs =
- |result: &mut Docs, node: &SyntaxNode, expect_inner_attrs, indent: &mut usize| {
- expand_cfg_attr_with_doc_comments::<_, Infallible>(
- AttrDocCommentIter::from_syntax_node(node).filter(|attr| match attr {
- Either::Left(attr) => attr.kind().is_inner() == expect_inner_attrs,
- Either::Right(comment) => comment.kind().doc.is_some_and(|kind| {
- (kind == ast::CommentPlacement::Inner) == expect_inner_attrs
- }),
- }),
- || cfg_options.get_or_insert_with(get_cfg_options),
- |attr| {
- match attr {
- Either::Right(doc_comment) => {
- result.extend_with_doc_comment(doc_comment, indent)
- }
- Either::Left((attr, _, _, _)) => match attr {
- // FIXME: Handle macros: `#[doc = concat!("foo", "bar")]`.
- Meta::NamedKeyValue {
- name: Some(name), value: Some(value), ..
- } if name.text() == "doc" => {
- result.extend_with_doc_attr(value, indent);
- }
- _ => {}
- },
- }
- ControlFlow::Continue(())
- },
- );
- };
if let Some(outer_mod_decl) = outer_mod_decl {
let mut indent = usize::MAX;
- extend_with_attrs(&mut result, outer_mod_decl.value.syntax(), false, &mut indent);
+ let outer_source_ctx =
+ if let (Some(expander), Some(resolver)) = (expander.as_deref(), resolver) {
+ Some(DocExprSourceCtx {
+ resolver: resolver.clone(),
+ file_id: outer_mod_decl.file_id,
+ ast_id_map: expander.db.ast_id_map(outer_mod_decl.file_id),
+ span_map: expander.db.span_map(outer_mod_decl.file_id),
+ })
+ } else {
+ None
+ };
+ extend_with_attrs(
+ &mut result,
+ outer_mod_decl.value.syntax(),
+ false,
+ &mut indent,
+ get_cfg_options,
+ &mut cfg_options,
+ expander.as_deref_mut(),
+ outer_source_ctx.as_ref(),
+ );
result.remove_indent(indent, 0);
result.outline_mod = Some((outer_mod_decl.file_id, result.docs_source_map.len()));
}
let inline_source_map_start = result.docs_source_map.len();
let mut indent = usize::MAX;
- extend_with_attrs(&mut result, source.value.syntax(), false, &mut indent);
+ let inline_source_ctx =
+ if let (Some(expander), Some(resolver)) = (expander.as_deref(), resolver) {
+ Some(DocExprSourceCtx {
+ resolver: resolver.clone(),
+ file_id: source.file_id,
+ ast_id_map: expander.db.ast_id_map(source.file_id),
+ span_map: expander.db.span_map(source.file_id),
+ })
+ } else {
+ None
+ };
+ extend_with_attrs(
+ &mut result,
+ source.value.syntax(),
+ false,
+ &mut indent,
+ get_cfg_options,
+ &mut cfg_options,
+ expander.as_deref_mut(),
+ inline_source_ctx.as_ref(),
+ );
if let Some(inner_attrs_node) = &inner_attrs_node {
result.inline_inner_docs_start = Some(TextSize::of(&result.docs));
- extend_with_attrs(&mut result, inner_attrs_node, true, &mut indent);
+ extend_with_attrs(
+ &mut result,
+ inner_attrs_node,
+ true,
+ &mut indent,
+ get_cfg_options,
+ &mut cfg_options,
+ expander.as_deref_mut(),
+ inline_source_ctx.as_ref(),
+ );
}
result.remove_indent(indent, inline_source_map_start);
@@ -1292,10 +1491,25 @@ impl AttrFlags {
pub fn docs(db: &dyn DefDatabase, owner: AttrDefId) -> Option<Box<Docs>> {
let (source, outer_mod_decl, _extra_crate_attrs, krate) = attrs_source(db, owner);
let inner_attrs_node = source.value.inner_attributes_node();
+ let resolver = resolver_for_attr_def_id(db, owner);
+ let def_map = crate_def_map(db, krate);
+ let recursion_limit = if cfg!(test) {
+ std::cmp::min(32, def_map.recursion_limit() as usize)
+ } else {
+ def_map.recursion_limit() as usize
+ };
+ let mut expander = DocMacroExpander { db, krate, recursion_depth: 0, recursion_limit };
// Note: we don't have to pass down `_extra_crate_attrs` here, since `extract_docs`
// does not handle crate-level attributes related to docs.
// See: https://doc.rust-lang.org/rustdoc/write-documentation/the-doc-attribute.html#at-the-crate-level
- extract_docs(&|| krate.cfg_options(db), source, outer_mod_decl, inner_attrs_node)
+ extract_docs(
+ Some(&mut expander),
+ Some(&resolver),
+ &|| krate.cfg_options(db),
+ source,
+ outer_mod_decl,
+ inner_attrs_node,
+ )
}
#[inline]
@@ -1308,8 +1522,25 @@ impl AttrFlags {
db: &dyn DefDatabase,
variant: VariantId,
) -> ArenaMap<LocalFieldId, Option<Box<Docs>>> {
+ let krate = variant.module(db).krate(db);
+ let resolver = variant.resolver(db);
+ let def_map = crate_def_map(db, krate);
+ let recursion_limit = if cfg!(test) {
+ std::cmp::min(32, def_map.recursion_limit() as usize)
+ } else {
+ def_map.recursion_limit() as usize
+ };
collect_field_attrs(db, variant, |cfg_options, field| {
- extract_docs(&|| cfg_options, field, None, None)
+ let mut expander =
+ DocMacroExpander { db, krate, recursion_depth: 0, recursion_limit };
+ extract_docs(
+ Some(&mut expander),
+ Some(&resolver),
+ &|| cfg_options,
+ field,
+ None,
+ None,
+ )
})
}
}
@@ -1580,19 +1811,27 @@ mod tests {
[
DocsSourceMapLine {
string_offset: 0,
- ast_offset: 123,
+ ast_offset: Some(
+ 123,
+ ),
},
DocsSourceMapLine {
string_offset: 5,
- ast_offset: 128,
+ ast_offset: Some(
+ 128,
+ ),
},
DocsSourceMapLine {
string_offset: 15,
- ast_offset: 261,
+ ast_offset: Some(
+ 261,
+ ),
},
DocsSourceMapLine {
string_offset: 20,
- ast_offset: 267,
+ ast_offset: Some(
+ 267,
+ ),
},
]
"#]]
@@ -1607,19 +1846,27 @@ mod tests {
[
DocsSourceMapLine {
string_offset: 0,
- ast_offset: 124,
+ ast_offset: Some(
+ 124,
+ ),
},
DocsSourceMapLine {
string_offset: 4,
- ast_offset: 129,
+ ast_offset: Some(
+ 129,
+ ),
},
DocsSourceMapLine {
string_offset: 13,
- ast_offset: 262,
+ ast_offset: Some(
+ 262,
+ ),
},
DocsSourceMapLine {
string_offset: 17,
- ast_offset: 268,
+ ast_offset: Some(
+ 268,
+ ),
},
]
"#]]
@@ -1632,35 +1879,51 @@ mod tests {
[
DocsSourceMapLine {
string_offset: 0,
- ast_offset: 124,
+ ast_offset: Some(
+ 124,
+ ),
},
DocsSourceMapLine {
string_offset: 4,
- ast_offset: 129,
+ ast_offset: Some(
+ 129,
+ ),
},
DocsSourceMapLine {
string_offset: 13,
- ast_offset: 262,
+ ast_offset: Some(
+ 262,
+ ),
},
DocsSourceMapLine {
string_offset: 17,
- ast_offset: 268,
+ ast_offset: Some(
+ 268,
+ ),
},
DocsSourceMapLine {
string_offset: 21,
- ast_offset: 124,
+ ast_offset: Some(
+ 124,
+ ),
},
DocsSourceMapLine {
string_offset: 25,
- ast_offset: 129,
+ ast_offset: Some(
+ 129,
+ ),
},
DocsSourceMapLine {
string_offset: 34,
- ast_offset: 262,
+ ast_offset: Some(
+ 262,
+ ),
},
DocsSourceMapLine {
string_offset: 38,
- ast_offset: 268,
+ ast_offset: Some(
+ 268,
+ ),
},
]
"#]]
diff --git a/crates/ide/src/hover/tests.rs b/crates/ide/src/hover/tests.rs
index 7a758cd4c1..e7a8b140f8 100644
--- a/crates/ide/src/hover/tests.rs
+++ b/crates/ide/src/hover/tests.rs
@@ -11404,3 +11404,175 @@ pub trait MyTrait {
"#]],
);
}
+
+#[test]
+fn test_hover_doc_attr_macro_generated_method() {
+ check(
+ r#"
+#[rustc_builtin_macro]
+macro_rules! concat {}
+
+macro_rules! bar {
+ () => {
+ struct Bar;
+ impl Bar {
+ #[doc = concat!("Do", " the foo")]
+ fn foo(&self) {}
+ }
+ }
+}
+
+bar!();
+
+fn foo() { let bar = Bar; bar.fo$0o(); }
+"#,
+ expect![[r#"
+ *foo*
+
+ ```rust
+ ra_test_fixture::Bar
+ ```
+
+ ```rust
+ fn foo(&self)
+ ```
+
+ ---
+
+ Do the foo
+ "#]],
+ );
+}
+
+#[test]
+fn test_hover_doc_attr_concat_macro() {
+ check(
+ r#"
+#[rustc_builtin_macro]
+macro_rules! concat {}
+
+#[doc = concat!("Hello", " ", "World")]
+struct Ba$0r;
+"#,
+ expect![[r#"
+ *Bar*
+
+ ```rust
+ ra_test_fixture
+ ```
+
+ ```rust
+ struct Bar
+ ```
+
+ ---
+
+ size = 0, align = 1, no Drop
+
+ ---
+
+ Hello World
+ "#]],
+ );
+}
+
+#[test]
+fn test_hover_doc_attr_user_macro_returning_string() {
+ check(
+ r#"
+macro_rules! doc_str {
+ () => { "Documentation from macro" };
+}
+
+#[doc = doc_str!()]
+struct Ba$0r;
+"#,
+ expect![[r#"
+ *Bar*
+
+ ```rust
+ ra_test_fixture
+ ```
+
+ ```rust
+ struct Bar
+ ```
+
+ ---
+
+ size = 0, align = 1, no Drop
+
+ ---
+
+ Documentation from macro
+ "#]],
+ );
+}
+
+#[test]
+fn test_hover_doc_attr_mixed_literal_and_macro() {
+ check(
+ r#"
+#[rustc_builtin_macro]
+macro_rules! concat {}
+
+/// First line
+#[doc = concat!("Second", " line")]
+struct Ba$0r;
+"#,
+ expect![[r#"
+ *Bar*
+
+ ```rust
+ ra_test_fixture
+ ```
+
+ ```rust
+ struct Bar
+ ```
+
+ ---
+
+ size = 0, align = 1, no Drop
+
+ ---
+
+ First line
+ Second line
+ "#]],
+ );
+}
+
+#[test]
+fn test_hover_doc_attr_field_with_macro() {
+ check(
+ r#"
+#[rustc_builtin_macro]
+macro_rules! concat {}
+
+struct Bar {
+ #[doc = concat!("field", " docs")]
+ ba$0z: i32,
+}
+"#,
+ expect![[r#"
+ *baz*
+
+ ```rust
+ ra_test_fixture::Bar
+ ```
+
+ ```rust
+ baz: i32
+ ```
+
+ ---
+
+ size = 4, align = 4, offset = 0, no Drop
+
+ ---
+
+ field docs
+ "#]],
+ );
+}