|
| 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