about summary refs log tree commit diff
diff options
context:
space:
mode:
authorYuki Okushi <jtitor@2k36.org>2022-06-24 16:43:41 +0900
committerGitHub <noreply@github.com>2022-06-24 16:43:41 +0900
commit97f4d7bd2c1b001d7b9d8b238f866de0cb328b05 (patch)
tree5d49138356fca3df1820d004dff2c2a8b0d4ca53
parentfc96600bf6a52f92aeeee60a92a161a82b61c0ef (diff)
parentbeb2f364cc85b4408da1d043f875d159003558e4 (diff)
Rollup merge of #91264 - GuillaumeGomez:macro-jump-to-def, r=jsha
Add macro support in jump to definition feature

Fixes #91174.

To do so, I check if the span comes from an expansion, and if so, I infer the original macro `DefId` or `Span` depending if it's a defined in the current crate or not.

There is one limitation due to macro expansion though:

```rust
macro_rules! yolo { () => {}}

fn foo() {
    yolo!();
}
```

In `foo`, `yolo!` won't be linked because after expansion, it is replaced by nothing (which seems logical). So I can't get an item from the `Visitor` from which I could tell if its `Span` comes from an expansion.

I added a test for this specific limitation alongside others.

Demo: https://rustdoc.crud.net/imperio/macro-jump-to-def/src/foo/check-source-code-urls-to-def-std.rs.html

As for the empty macro issue that cannot create a jump to definition, you can see it [here](https://rustdoc.crud.net/imperio/macro-jump-to-def/src/foo/check-source-code-urls-to-def-std.rs.html#35).

r? ```@jyn514```
-rw-r--r--compiler/rustc_metadata/src/creader.rs4
-rw-r--r--src/librustdoc/html/format.rs80
-rw-r--r--src/librustdoc/html/highlight.rs115
-rw-r--r--src/librustdoc/html/render/span_map.rs83
-rw-r--r--src/librustdoc/html/sources.rs2
-rw-r--r--src/test/rustdoc/check-source-code-urls-to-def-std.rs25
6 files changed, 258 insertions, 51 deletions
diff --git a/compiler/rustc_metadata/src/creader.rs b/compiler/rustc_metadata/src/creader.rs
index 947d563ae3c..555db5846ed 100644
--- a/compiler/rustc_metadata/src/creader.rs
+++ b/compiler/rustc_metadata/src/creader.rs
@@ -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()
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index 5baa53d5554..056eda089c1 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -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);
             }
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs
index 480728b1797..d2ef89078bf 100644
--- a/src/librustdoc/html/highlight.rs
+++ b/src/librustdoc/html/highlight.rs
@@ -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,8 +85,8 @@ 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
@@ -94,22 +94,27 @@ 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)]
diff --git a/src/librustdoc/html/render/span_map.rs b/src/librustdoc/html/render/span_map.rs
index 86961dc3bf1..34d590fb244 100644
--- a/src/librustdoc/html/render/span_map.rs
+++ b/src/librustdoc/html/render/span_map.rs
@@ -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);
     }
 }
diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs
index 1971d08e5be..d0fd637ba88 100644
--- a/src/librustdoc/html/sources.rs
+++ b/src/librustdoc/html/sources.rs
@@ -297,7 +297,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,
     );
 }
diff --git a/src/test/rustdoc/check-source-code-urls-to-def-std.rs b/src/test/rustdoc/check-source-code-urls-to-def-std.rs
index b129ceb5b73..3396b234a77 100644
--- a/src/test/rustdoc/check-source-code-urls-to-def-std.rs
+++ b/src/test/rustdoc/check-source-code-urls-to-def-std.rs
@@ -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);
+}