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

Add macro support in jump to definition feature #91264

Merged
merged 6 commits into from
Jun 24, 2022
Merged
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
4 changes: 4 additions & 0 deletions compiler/rustc_metadata/src/creader.rs
Original file line number Diff line number Diff line change
@@ -133,6 +133,10 @@ impl CStore {
CrateNum::new(self.metas.len() - 1)
}

pub fn has_crate_data(&self, cnum: CrateNum) -> bool {
self.metas[cnum].is_some()
}

pub(crate) fn get_crate_data(&self, cnum: CrateNum) -> CrateMetadataRef<'_> {
let cdata = self.metas[cnum]
.as_ref()
80 changes: 79 additions & 1 deletion src/librustdoc/html/format.rs
Original file line number Diff line number Diff line change
@@ -8,14 +8,16 @@
use std::borrow::Cow;
use std::cell::Cell;
use std::fmt;
use std::iter;
use std::iter::{self, once};

use rustc_ast as ast;
use rustc_attr::{ConstStability, StabilityLevel};
use rustc_data_structures::captures::Captures;
use rustc_data_structures::fx::FxHashSet;
use rustc_hir as hir;
use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefId;
use rustc_metadata::creader::{CStore, LoadedMacro};
use rustc_middle::ty;
use rustc_middle::ty::DefIdTree;
use rustc_middle::ty::TyCtxt;
@@ -519,6 +521,7 @@ impl clean::GenericArgs {
}

// Possible errors when computing href link source for a `DefId`
#[derive(PartialEq, Eq)]
pub(crate) enum HrefError {
/// This item is known to rustdoc, but from a crate that does not have documentation generated.
///
@@ -556,6 +559,79 @@ pub(crate) fn join_with_double_colon(syms: &[Symbol]) -> String {
s
}

/// This function is to get the external macro path because they are not in the cache used in
/// `href_with_root_path`.
fn generate_macro_def_id_path(
def_id: DefId,
cx: &Context<'_>,
root_path: Option<&str>,
) -> Result<(String, ItemType, Vec<Symbol>), HrefError> {
let tcx = cx.shared.tcx;
let crate_name = tcx.crate_name(def_id.krate).to_string();
let cache = cx.cache();

let fqp: Vec<Symbol> = tcx
.def_path(def_id)
.data
.into_iter()
.filter_map(|elem| {
// extern blocks (and a few others things) have an empty name.
match elem.data.get_opt_name() {
Some(s) if !s.is_empty() => Some(s),
_ => None,
}
})
.collect();
let relative = fqp.iter().map(|elem| elem.to_string());
let cstore = CStore::from_tcx(tcx);
// We need this to prevent a `panic` when this function is used from intra doc links...
if !cstore.has_crate_data(def_id.krate) {
debug!("No data for crate {}", crate_name);
return Err(HrefError::NotInExternalCache);
}
// Check to see if it is a macro 2.0 or built-in macro.
// More information in <https://rust-lang.github.io/rfcs/1584-macros.html>.
let is_macro_2 = match cstore.load_macro_untracked(def_id, tcx.sess) {
LoadedMacro::MacroDef(def, _) => {
// If `ast_def.macro_rules` is `true`, then it's not a macro 2.0.
matches!(&def.kind, ast::ItemKind::MacroDef(ast_def) if !ast_def.macro_rules)
}
_ => false,
};

let mut path = if is_macro_2 {
once(crate_name.clone()).chain(relative).collect()
} else {
vec![crate_name.clone(), relative.last().unwrap()]
};
if path.len() < 2 {
// The minimum we can have is the crate name followed by the macro name. If shorter, then
// it means that that `relative` was empty, which is an error.
debug!("macro path cannot be empty!");
return Err(HrefError::NotInExternalCache);
}

if let Some(last) = path.last_mut() {
*last = format!("macro.{}.html", last);
}

let url = match cache.extern_locations[&def_id.krate] {
ExternalLocation::Remote(ref s) => {
// `ExternalLocation::Remote` always end with a `/`.
format!("{}{}", s, path.join("/"))
}
ExternalLocation::Local => {
// `root_path` always end with a `/`.
format!("{}{}/{}", root_path.unwrap_or(""), crate_name, path.join("/"))
}
ExternalLocation::Unknown => {
debug!("crate {} not in cache when linkifying macros", crate_name);
return Err(HrefError::NotInExternalCache);
}
};
Ok((url, ItemType::Macro, fqp))
}

pub(crate) fn href_with_root_path(
did: DefId,
cx: &Context<'_>,
@@ -611,6 +687,8 @@ pub(crate) fn href_with_root_path(
ExternalLocation::Unknown => return Err(HrefError::DocumentationNotBuilt),
},
)
} else if matches!(def_kind, DefKind::Macro(_)) {
return generate_macro_def_id_path(did, cx, root_path);
} else {
return Err(HrefError::NotInExternalCache);
}
115 changes: 83 additions & 32 deletions src/librustdoc/html/highlight.rs
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ use super::format::{self, Buffer};
use super::render::LinkFromSrc;

/// This type is needed in case we want to render links on items to allow to go to their definition.
pub(crate) struct ContextInfo<'a, 'b, 'c> {
pub(crate) struct HrefContext<'a, 'b, 'c> {
pub(crate) context: &'a Context<'b>,
/// This span contains the current file we're going through.
pub(crate) file_span: Span,
@@ -44,7 +44,7 @@ pub(crate) fn render_with_highlighting(
tooltip: Option<(Option<Edition>, &str)>,
edition: Edition,
extra_content: Option<Buffer>,
context_info: Option<ContextInfo<'_, '_, '_>>,
href_context: Option<HrefContext<'_, '_, '_>>,
decoration_info: Option<DecorationInfo>,
) {
debug!("highlighting: ================\n{}\n==============", src);
@@ -62,7 +62,7 @@ pub(crate) fn render_with_highlighting(
}

write_header(out, class, extra_content);
write_code(out, src, edition, context_info, decoration_info);
write_code(out, src, edition, href_context, decoration_info);
write_footer(out, playground_button);
}

@@ -85,31 +85,36 @@ fn write_header(out: &mut Buffer, class: Option<&str>, extra_content: Option<Buf
///
/// Some explanations on the last arguments:
///
/// In case we are rendering a code block and not a source code file, `context_info` will be `None`.
/// To put it more simply: if `context_info` is `None`, the code won't try to generate links to an
/// In case we are rendering a code block and not a source code file, `href_context` will be `None`.
/// To put it more simply: if `href_context` is `None`, the code won't try to generate links to an
/// item definition.
///
/// More explanations about spans and how we use them here are provided in the
fn write_code(
out: &mut Buffer,
src: &str,
edition: Edition,
context_info: Option<ContextInfo<'_, '_, '_>>,
href_context: Option<HrefContext<'_, '_, '_>>,
decoration_info: Option<DecorationInfo>,
) {
// This replace allows to fix how the code source with DOS backline characters is displayed.
let src = src.replace("\r\n", "\n");
let mut closing_tags: Vec<&'static str> = Vec::new();
Classifier::new(
&src,
edition,
context_info.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
decoration_info,
)
.highlight(&mut |highlight| {
match highlight {
Highlight::Token { text, class } => string(out, Escape(text), class, &context_info),
Highlight::EnterSpan { class } => enter_span(out, class),
Highlight::ExitSpan => exit_span(out),
Highlight::Token { text, class } => string(out, Escape(text), class, &href_context),
Highlight::EnterSpan { class } => {
closing_tags.push(enter_span(out, class, &href_context))
}
Highlight::ExitSpan => {
exit_span(out, closing_tags.pop().expect("ExitSpan without EnterSpan"))
}
};
});
}
@@ -129,7 +134,7 @@ enum Class {
RefKeyWord,
Self_(Span),
Op,
Macro,
Macro(Span),
MacroNonTerminal,
String,
Number,
@@ -153,7 +158,7 @@ impl Class {
Class::RefKeyWord => "kw-2",
Class::Self_(_) => "self",
Class::Op => "op",
Class::Macro => "macro",
Class::Macro(_) => "macro",
Class::MacroNonTerminal => "macro-nonterminal",
Class::String => "string",
Class::Number => "number",
@@ -171,8 +176,22 @@ impl Class {
/// a "span" (a tuple representing `(lo, hi)` equivalent of `Span`).
fn get_span(self) -> Option<Span> {
match self {
Self::Ident(sp) | Self::Self_(sp) => Some(sp),
_ => None,
Self::Ident(sp) | Self::Self_(sp) | Self::Macro(sp) => Some(sp),
Self::Comment
| Self::DocComment
| Self::Attribute
| Self::KeyWord
| Self::RefKeyWord
| Self::Op
| Self::MacroNonTerminal
| Self::String
| Self::Number
| Self::Bool
| Self::Lifetime
| Self::PreludeTy
| Self::PreludeVal
| Self::QuestionMark
| Self::Decoration(_) => None,
}
}
}
@@ -611,7 +630,7 @@ impl<'a> Classifier<'a> {
},
TokenKind::Ident | TokenKind::RawIdent if lookahead == Some(TokenKind::Bang) => {
self.in_macro = true;
sink(Highlight::EnterSpan { class: Class::Macro });
sink(Highlight::EnterSpan { class: Class::Macro(self.new_span(before, text)) });
sink(Highlight::Token { text, class: None });
return;
}
@@ -658,13 +677,20 @@ impl<'a> Classifier<'a> {

/// Called when we start processing a span of text that should be highlighted.
/// The `Class` argument specifies how it should be highlighted.
fn enter_span(out: &mut Buffer, klass: Class) {
write!(out, "<span class=\"{}\">", klass.as_html());
fn enter_span(
out: &mut Buffer,
klass: Class,
href_context: &Option<HrefContext<'_, '_, '_>>,
) -> &'static str {
string_without_closing_tag(out, "", Some(klass), href_context).expect(
"internal error: enter_span was called with Some(klass) but did not return a \
closing HTML tag",
)
}

/// Called at the end of a span of highlighted text.
fn exit_span(out: &mut Buffer) {
out.write_str("</span>");
fn exit_span(out: &mut Buffer, closing_tag: &str) {
out.write_str(closing_tag);
}

/// Called for a span of text. If the text should be highlighted differently
@@ -687,15 +713,39 @@ fn string<T: Display>(
out: &mut Buffer,
text: T,
klass: Option<Class>,
context_info: &Option<ContextInfo<'_, '_, '_>>,
href_context: &Option<HrefContext<'_, '_, '_>>,
) {
if let Some(closing_tag) = string_without_closing_tag(out, text, klass, href_context) {
out.write_str(closing_tag);
}
}

/// This function writes `text` into `out` with some modifications depending on `klass`:
///
/// * If `klass` is `None`, `text` is written into `out` with no modification.
/// * If `klass` is `Some` but `klass.get_span()` is `None`, it writes the text wrapped in a
/// `<span>` with the provided `klass`.
/// * If `klass` is `Some` and has a [`rustc_span::Span`], it then tries to generate a link (`<a>`
/// element) by retrieving the link information from the `span_correspondance_map` that was filled
/// in `span_map.rs::collect_spans_and_sources`. If it cannot retrieve the information, then it's
/// the same as the second point (`klass` is `Some` but doesn't have a [`rustc_span::Span`]).
fn string_without_closing_tag<T: Display>(
out: &mut Buffer,
text: T,
klass: Option<Class>,
href_context: &Option<HrefContext<'_, '_, '_>>,
) -> Option<&'static str> {
let Some(klass) = klass
else { return write!(out, "{}", text) };
else {
write!(out, "{}", text);
return None;
};
let Some(def_span) = klass.get_span()
else {
write!(out, "<span class=\"{}\">{}</span>", klass.as_html(), text);
return;
write!(out, "<span class=\"{}\">{}", klass.as_html(), text);
return Some("</span>");
};

let mut text_s = text.to_string();
if text_s.contains("::") {
text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
@@ -715,10 +765,10 @@ fn string<T: Display>(
path
});
}
if let Some(context_info) = context_info {
if let Some(href_context) = href_context {
if let Some(href) =
context_info.context.shared.span_correspondance_map.get(&def_span).and_then(|href| {
let context = context_info.context;
href_context.context.shared.span_correspondance_map.get(&def_span).and_then(|href| {
let context = href_context.context;
// FIXME: later on, it'd be nice to provide two links (if possible) for all items:
// one to the documentation page and one to the source definition.
// FIXME: currently, external items only generate a link to their documentation,
@@ -727,27 +777,28 @@ fn string<T: Display>(
match href {
LinkFromSrc::Local(span) => context
.href_from_span(*span, true)
.map(|s| format!("{}{}", context_info.root_path, s)),
.map(|s| format!("{}{}", href_context.root_path, s)),
LinkFromSrc::External(def_id) => {
format::href_with_root_path(*def_id, context, Some(context_info.root_path))
format::href_with_root_path(*def_id, context, Some(href_context.root_path))
.ok()
.map(|(url, _, _)| url)
}
LinkFromSrc::Primitive(prim) => format::href_with_root_path(
PrimitiveType::primitive_locations(context.tcx())[prim],
context,
Some(context_info.root_path),
Some(href_context.root_path),
)
.ok()
.map(|(url, _, _)| url),
}
})
{
write!(out, "<a class=\"{}\" href=\"{}\">{}</a>", klass.as_html(), href, text_s);
return;
write!(out, "<a class=\"{}\" href=\"{}\">{}", klass.as_html(), href, text_s);
return Some("</a>");
}
}
write!(out, "<span class=\"{}\">{}</span>", klass.as_html(), text_s);
write!(out, "<span class=\"{}\">{}", klass.as_html(), text_s);
Some("</span>")
}

#[cfg(test)]
83 changes: 66 additions & 17 deletions src/librustdoc/html/render/span_map.rs
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@ use rustc_hir::intravisit::{self, Visitor};
use rustc_hir::{ExprKind, HirId, Mod, Node};
use rustc_middle::hir::nested_filter;
use rustc_middle::ty::TyCtxt;
use rustc_span::Span;
use rustc_span::hygiene::MacroKind;
use rustc_span::{BytePos, ExpnKind, Span};

use std::path::{Path, PathBuf};

@@ -63,33 +64,72 @@ struct SpanMapVisitor<'tcx> {

impl<'tcx> SpanMapVisitor<'tcx> {
/// This function is where we handle `hir::Path` elements and add them into the "span map".
fn handle_path(&mut self, path: &rustc_hir::Path<'_>, path_span: Option<Span>) {
fn handle_path(&mut self, path: &rustc_hir::Path<'_>) {
let info = match path.res {
// FIXME: For now, we only handle `DefKind` if it's not `DefKind::TyParam` or
// `DefKind::Macro`. Would be nice to support them too alongside the other `DefKind`
// FIXME: For now, we handle `DefKind` if it's not a `DefKind::TyParam`.
// Would be nice to support them too alongside the other `DefKind`
// (such as primitive types!).
Res::Def(kind, def_id) if kind != DefKind::TyParam => {
if matches!(kind, DefKind::Macro(_)) {
return;
}
Some(def_id)
}
Res::Def(kind, def_id) if kind != DefKind::TyParam => Some(def_id),
Res::Local(_) => None,
Res::PrimTy(p) => {
// FIXME: Doesn't handle "path-like" primitives like arrays or tuples.
let span = path_span.unwrap_or(path.span);
self.matches.insert(span, LinkFromSrc::Primitive(PrimitiveType::from(p)));
self.matches.insert(path.span, LinkFromSrc::Primitive(PrimitiveType::from(p)));
return;
}
Res::Err => return,
_ => return,
};
if let Some(span) = self.tcx.hir().res_span(path.res) {
self.matches
.insert(path_span.unwrap_or(path.span), LinkFromSrc::Local(clean::Span::new(span)));
self.matches.insert(path.span, LinkFromSrc::Local(clean::Span::new(span)));
} else if let Some(def_id) = info {
self.matches.insert(path_span.unwrap_or(path.span), LinkFromSrc::External(def_id));
self.matches.insert(path.span, LinkFromSrc::External(def_id));
}
}

/// Adds the macro call into the span map. Returns `true` if the `span` was inside a macro
/// expansion, whether or not it was added to the span map.
///
/// The idea for the macro support is to check if the current `Span` comes from expansion. If
/// so, we loop until we find the macro definition by using `outer_expn_data` in a loop.
/// Finally, we get the information about the macro itself (`span` if "local", `DefId`
/// otherwise) and store it inside the span map.
fn handle_macro(&mut self, span: Span) -> bool {
if !span.from_expansion() {
return false;
}
// So if the `span` comes from a macro expansion, we need to get the original
// macro's `DefId`.
let mut data = span.ctxt().outer_expn_data();
let mut call_site = data.call_site;
// Macros can expand to code containing macros, which will in turn be expanded, etc.
// So the idea here is to "go up" until we're back to code that was generated from
// macro expansion so that we can get the `DefId` of the original macro that was at the
// origin of this expansion.
while call_site.from_expansion() {
data = call_site.ctxt().outer_expn_data();
call_site = data.call_site;
}

let macro_name = match data.kind {
ExpnKind::Macro(MacroKind::Bang, macro_name) => macro_name,
// Even though we don't handle this kind of macro, this `data` still comes from
// expansion so we return `true` so we don't go any deeper in this code.
_ => return true,
};
let link_from_src = match data.macro_def_id {
Some(macro_def_id) if macro_def_id.is_local() => {
LinkFromSrc::Local(clean::Span::new(data.def_site))
}
Some(macro_def_id) => LinkFromSrc::External(macro_def_id),
None => return true,
};
let new_span = data.call_site;
let macro_name = macro_name.as_str();
// The "call_site" includes the whole macro with its "arguments". We only want
// the macro name.
let new_span = new_span.with_hi(new_span.lo() + BytePos(macro_name.len() as u32));
self.matches.insert(new_span, link_from_src);
true
}
}

@@ -101,7 +141,10 @@ impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
}

fn visit_path(&mut self, path: &'tcx rustc_hir::Path<'tcx>, _id: HirId) {
self.handle_path(path, None);
if self.handle_macro(path.span) {
return;
}
self.handle_path(path);
intravisit::walk_path(self, path);
}

@@ -143,12 +186,18 @@ impl<'tcx> Visitor<'tcx> for SpanMapVisitor<'tcx> {
);
}
}
} else if self.handle_macro(expr.span) {
// We don't want to go deeper into the macro.
return;
}
intravisit::walk_expr(self, expr);
}

fn visit_use(&mut self, path: &'tcx rustc_hir::Path<'tcx>, id: HirId) {
self.handle_path(path, None);
if self.handle_macro(path.span) {
return;
}
self.handle_path(path);
intravisit::walk_use(self, path, id);
}
}
2 changes: 1 addition & 1 deletion src/librustdoc/html/sources.rs
Original file line number Diff line number Diff line change
@@ -299,7 +299,7 @@ pub(crate) fn print_src(
None,
edition,
Some(line_numbers),
Some(highlight::ContextInfo { context, file_span, root_path }),
Some(highlight::HrefContext { context, file_span, root_path }),
decoration_info,
);
}
25 changes: 25 additions & 0 deletions src/test/rustdoc/check-source-code-urls-to-def-std.rs
Original file line number Diff line number Diff line change
@@ -15,3 +15,28 @@ pub fn foo(a: u32, b: &str, c: String) {
let y: bool = true;
babar();
}

macro_rules! yolo { () => {}}

fn bar(a: i32) {}

macro_rules! bar {
($a:ident) => { bar($a) }
}

macro_rules! data {
($x:expr) => { $x * 2 }
}

pub fn another_foo() {
// This is known limitation: if the macro doesn't generate anything, the visitor
// can't find any item or anything that could tell us that it comes from expansion.
// @!has - '//a[@href="../../src/foo/check-source-code-urls-to-def-std.rs.html#19"]' 'yolo!'
yolo!();
// @has - '//a[@href="{{channel}}/std/macro.eprintln.html"]' 'eprintln!'
eprintln!();
// @has - '//a[@href="../../src/foo/check-source-code-urls-to-def-std.rs.html#27-29"]' 'data!'
let x = data!(4);
// @has - '//a[@href="../../src/foo/check-source-code-urls-to-def-std.rs.html#23-25"]' 'bar!'
bar!(x);
}