Skip to content

Commit f1ebf10

Browse files
blyxyasnahuakang
andcommitted
Add new chapter: "Macro Expansions"
Co-authored-by: Nahua <[email protected]>
1 parent 0049816 commit f1ebf10

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

book/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [Basics](development/basics.md)
1515
- [Adding Lints](development/adding_lints.md)
1616
- [Type Checking](development/type_checking.md)
17+
- [Macro Expansions](development/macro_expansions.md)
1718
- [Common Tools](development/common_tools_writing_lints.md)
1819
- [Infrastructure](development/infrastructure/README.md)
1920
- [Syncing changes between Clippy and rust-lang/rust](development/infrastructure/sync.md)
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Dealing with macros and expansions
2+
3+
Sometimes we might encounter Rust macro expansions while working with Clippy.
4+
While macro expansions are not as dramatic and profound as the expansion
5+
of our universe, they can certainly bring chaos to the orderly world
6+
of code and logic.
7+
8+
The general rule of thumb is that we should ignore code with macro
9+
expansions when working with Clippy because the code can be dynamic
10+
in ways that are difficult or impossible for us to foresee.
11+
12+
## False Positives
13+
14+
What exactly do we mean by _dynamic in ways that are difficult to foresee_?
15+
16+
Macros are [expanded][expansion] in the `EarlyLintPass` level,
17+
so the Abstract Syntax Tree (AST) is generated in place of macros.
18+
This means the code which we work with in Clippy is already desugared.
19+
20+
If we wrote a new lint, there is a possibility that the lint is
21+
triggered in macro-generated code. Since this expanded macro code
22+
is not written by the macro's user but really by the macro's author,
23+
the user cannot and should not be responsible for fixing the issue
24+
that triggers the lint.
25+
26+
Besides, a [Span] in a macro can be changed by the macro author.
27+
Therefore, any lint check related to lines or columns should be
28+
avoided since they might be changed at any time and become unreliable
29+
or incorrect information.
30+
31+
Because of these unforeseeable or unstable behaviors, macro expansion
32+
should often not be regarded as a part of the stable API.
33+
This is also why most lints check if they are inside a macro or not
34+
before emitting suggestions to the end user to avoid false positives.
35+
36+
## How to Work with Macros
37+
38+
Several functions are available for working with macros.
39+
40+
### The `Span.from_expansion` method
41+
42+
We could utilize a `span`'s [`from_expansion`] method, which
43+
detects if the `span` is from a macro expansion / desugaring.
44+
This is a very common first step in a lint:
45+
46+
```rust
47+
if expr.span.from_expansion() {
48+
// We most likely want to ignore it.
49+
return;
50+
}
51+
```
52+
53+
### `Span.ctxt` method
54+
55+
The `span`'s context, given by the method [`ctxt`] and returning [SpanContext],
56+
represents if the span is from a macro expansion and, if it is, which
57+
macro call expanded this span.
58+
59+
Sometimes, it is useful to check if the context of two spans are equal.
60+
For instance, suppose we have the following line of code that would
61+
expand into `1 + 0`:
62+
63+
```rust
64+
// The following code expands to `1 + 0` for both `EarlyLintPass` and `lateLintPass`
65+
1 + mac!()
66+
```
67+
68+
Assuming that we'd collect the `1` expression as a variable `left`
69+
and the `0` expression as a variable `right`, we can simply compare
70+
their contexts. If the context is different, then we most likely
71+
are dealing with a macro expansion and should just ignore it:
72+
73+
```rust
74+
if left.span.ctxt() != right.span.ctxt() {
75+
// The code author most likely cannot modify this expression
76+
return;
77+
}
78+
```
79+
80+
> **Note**: Code that is not from expansion is in the "root" context.
81+
> So any spans whose `from_expansion` returns `false` can be assumed
82+
> to have the same context. Because of this, using `span.from_expansion()`
83+
> is often sufficient.
84+
85+
Going a bit deeper, in a simple expression such as `a == b`,
86+
`a` and `b` have the same context.
87+
However, in a `macro_rules!` with `a == $b`, `$b` is expanded to
88+
an expression that contains a different context from `a`.
89+
90+
Take a look at the following macro `m`:
91+
92+
```rust
93+
macro_rules! m {
94+
($a:expr, $b:expr) => {
95+
if $a.is_some() {
96+
$b;
97+
}
98+
}
99+
}
100+
101+
let x: Option<u32> = Some(42);
102+
m!(x, x.unwrap());
103+
```
104+
105+
If the `m!(x, x.unwrapp());` line is expanded, we would get two desugarized
106+
expressions:
107+
108+
- `x.is_some()` (from the `$a.is_some()` line in the `m` macro)
109+
- `x.unwrap()` (corresponding to `$b` in the `m` macro)
110+
111+
Suppose `x.is_some()` expression's span is associated with the `x_is_some_span` variable
112+
and `x.unwrap()` expression's span is associated with `x_unwrap_span` variable,
113+
we could assume that these two spans do not share the same context:
114+
115+
```rust
116+
// x.is_some() is from inside the macro
117+
// x.unwrap() is from outside the macro
118+
assert_eq!(x_is_some_span.ctxt(), x_unwrap_span.ctxt());
119+
```
120+
121+
### The `in_external_macro` function
122+
123+
`rustc_middle::lint` provides a function ([`in_external_macro`]) that can
124+
detect if the given span is from a macro defined in a foreign crate.
125+
126+
Therefore, if we really want a new lint to work with macro-generated code,
127+
this is the next line of defense to avoid macros not defined inside
128+
the current crate since it is unfair to the user if Clippy lints code
129+
which the user cannot change.
130+
131+
For example, assume we have the following code that is being examined
132+
by Clippy:
133+
134+
```rust
135+
#[macro_use]
136+
extern crate a_foreign_crate_with_macros;
137+
138+
// `foo` macro is defined in `a_foreign_crate_with_macros`
139+
foo!("bar");
140+
```
141+
142+
Also assume that we get the corresponding variable `foo_span` for the
143+
`foo` macro call, we could decide not to lint if `in_external_macro`
144+
results in `true` (note that `cx` can be `EarlyContext` or `LateContext`):
145+
146+
```rust
147+
if in_external_macro(cx.sess(), foo_span) {
148+
// We should ignore macro from a foreign crate.
149+
return;
150+
}
151+
```
152+
153+
[`ctxt`]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html#method.ctxt
154+
[expansion]: https://rustc-dev-guide.rust-lang.org/macro-expansion.html#expansion-and-ast-integration
155+
[`from_expansion`]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html#method.from_expansion
156+
[`in_external_macro`]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_middle/lint/fn.in_external_macro.html
157+
[Span]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html
158+
[SpanContext]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/hygiene/struct.SyntaxContext.html

0 commit comments

Comments
 (0)