Skip to content

Commit f85331f

Browse files
authored
Explain is_from_proc_macro and proc_macros auxiliary crate in the book (#14398)
This comes up every now and then in PRs, so I think it would be good if we had an explanation in a central place that we can link to. changelog: none
2 parents c07fd48 + 44094e5 commit f85331f

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

book/src/development/macro_expansions.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,85 @@ if foo_span.in_external_macro(cx.sess().source_map()) {
150150
}
151151
```
152152

153+
### The `is_from_proc_macro` function
154+
A common point of confusion is the existence of [`is_from_proc_macro`]
155+
and how it differs from the other [`in_external_macro`]/[`from_expansion`] functions.
156+
157+
While [`in_external_macro`] and [`from_expansion`] both work perfectly fine for detecting expanded code
158+
from *declarative* macros (i.e. `macro_rules!` and macros 2.0),
159+
detecting *proc macro*-generated code is a bit more tricky, as proc macros can (and often do)
160+
freely manipulate the span of returned tokens.
161+
162+
In practice, this often happens through the use of [`quote::quote_spanned!`] with a span from the input tokens.
163+
164+
In those cases, there is no *reliable* way for the compiler (and tools like Clippy)
165+
to distinguish code that comes from such a proc macro from code that the user wrote directly,
166+
and [`in_external_macro`] will return `false`.
167+
168+
This is usually not an issue for the compiler and actually helps proc macro authors create better error messages,
169+
as it allows associating parts of the expansion with parts of the macro input and lets the compiler
170+
point the user to the relevant code in case of a compile error.
171+
172+
However, for Clippy this is inconvenient, because most of the time *we don't* want
173+
to lint proc macro-generated code and this makes it impossible to tell what is and isn't proc macro code.
174+
175+
> NOTE: this is specifically only an issue when a proc macro explicitly sets the span to that of an **input span**.
176+
>
177+
> For example, other common ways of creating `TokenStream`s, such as `"fn foo() {...}".parse::<TokenStream>()`,
178+
> sets each token's span to `Span::call_site()`, which already marks the span as coming from a proc macro
179+
> and the usual span methods have no problem detecting that as a macro span.
180+
181+
As such, Clippy has its own `is_from_proc_macro` function which tries to *approximate*
182+
whether a span comes from a proc macro, by checking whether the source text at the given span
183+
lines up with the given AST node.
184+
185+
This function is typically used in combination with the other mentioned macro span functions,
186+
but is usually called much later into the condition chain as it's a bit heavier than most other conditions,
187+
so that the other cheaper conditions can fail faster. For example, the `borrow_deref_ref` lint:
188+
```rs
189+
impl<'tcx> LateLintPass<'tcx> for BorrowDerefRef {
190+
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &rustc_hir::Expr<'tcx>) {
191+
if let ... = ...
192+
&& ...
193+
&& !e.span.from_expansion()
194+
&& ...
195+
&& ...
196+
&& !is_from_proc_macro(cx, e)
197+
&& ...
198+
{
199+
...
200+
}
201+
}
202+
}
203+
```
204+
205+
### Testing lints with macro expansions
206+
To test that all of these cases are handled correctly in your lint,
207+
we have a helper auxiliary crate that exposes various macros, used by tests like so:
208+
```rust
209+
//@aux-build:proc_macros.rs
210+
211+
extern crate proc_macros;
212+
213+
fn main() {
214+
proc_macros::external!{ code_that_should_trigger_your_lint }
215+
proc_macros::with_span!{ span code_that_should_trigger_your_lint }
216+
}
217+
```
218+
This exercises two cases:
219+
- `proc_macros::external!` is a simple proc macro that echos the input tokens back but with a macro span:
220+
this represents the usual, common case where an external macro expands to code that your lint would trigger,
221+
and is correctly handled by `in_external_macro` and `Span::from_expansion`.
222+
223+
- `proc_macros::with_span!` echos back the input tokens starting from the second token
224+
with the span of the first token: this is where the other functions will fail and `is_from_proc_macro` is needed
225+
226+
153227
[`ctxt`]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html#method.ctxt
154228
[expansion]: https://rustc-dev-guide.rust-lang.org/macro-expansion.html#expansion-and-ast-integration
155229
[`from_expansion`]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html#method.from_expansion
156230
[`in_external_macro`]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html#method.in_external_macro
157231
[Span]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html
158232
[SyntaxContext]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/hygiene/struct.SyntaxContext.html
233+
[`is_from_proc_macro`]: https://doc.rust-lang.org/nightly/nightly-rustc/clippy_utils/fn.is_from_proc_macro.html
234+
[`quote::quote_spanned!`]: https://docs.rs/quote/latest/quote/macro.quote_spanned.html

0 commit comments

Comments
 (0)