Skip to content

Commit 2ae8b30

Browse files
committed
Don't lint unit_arg when expanded from a proc-macro
1 parent 5721ca9 commit 2ae8b30

File tree

7 files changed

+195
-33
lines changed

7 files changed

+195
-33
lines changed

clippy_lints/src/matches/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ mod single_match;
2121
mod try_err;
2222
mod wild_in_or_pats;
2323

24-
use clippy_utils::source::{snippet_opt, span_starts_with, walk_span_to_context};
25-
use clippy_utils::{higher, in_constant, meets_msrv, msrvs};
24+
use clippy_utils::source::{snippet_opt, walk_span_to_context};
25+
use clippy_utils::{higher, in_constant, is_span_match, meets_msrv, msrvs};
2626
use rustc_hir::{Arm, Expr, ExprKind, Local, MatchSource, Pat};
2727
use rustc_lexer::{tokenize, TokenKind};
2828
use rustc_lint::{LateContext, LateLintPass, LintContext};
@@ -949,7 +949,7 @@ impl<'tcx> LateLintPass<'tcx> for Matches {
949949
let from_expansion = expr.span.from_expansion();
950950

951951
if let ExprKind::Match(ex, arms, source) = expr.kind {
952-
if source == MatchSource::Normal && !span_starts_with(cx, expr.span, "match") {
952+
if source == MatchSource::Normal && !is_span_match(cx, expr.span) {
953953
return;
954954
}
955955
if matches!(source, MatchSource::Normal | MatchSource::ForLoopDesugar) {

clippy_lints/src/unit_types/unit_arg.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::is_expr_from_proc_macro;
23
use clippy_utils::source::{indent_of, reindent_multiline, snippet_opt};
34
use if_chain::if_chain;
45
use rustc_errors::Applicability;
@@ -44,7 +45,7 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>) {
4445
}
4546
})
4647
.collect::<Vec<_>>();
47-
if !args_to_recover.is_empty() {
48+
if !args_to_recover.is_empty() && !is_expr_from_proc_macro(cx, expr) {
4849
lint_unit_args(cx, expr, &args_to_recover);
4950
}
5051
},

clippy_utils/src/check_proc_macro.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//! This module handles checking if the span given is from a proc-macro or not.
2+
//!
3+
//! Proc-macros are capable of setting the span of every token they output to a few possible spans.
4+
//! This includes spans we can detect easily as coming from a proc-macro (e.g. the call site
5+
//! or the def site), and spans we can't easily detect as such (e.g. the span of any token
6+
//! passed into the proc macro). This capability means proc-macros are capable of generating code
7+
//! with a span that looks like it was written by the user, but which should not be linted by clippy
8+
//! as it was generated by an external macro.
9+
//!
10+
//! That brings us to this module. The current approach is to determine a small bit of text which
11+
//! must exist at both the start and the end of an item (e.g. an expression or a path) assuming the
12+
//! code was written, and check if the span contains that text. Note this will only work correctly
13+
//! if the span is not from a `macro_rules` based macro.
14+
15+
use rustc_ast::ast::{IntTy, LitIntType, LitKind, StrStyle, UintTy};
16+
use rustc_hir::{
17+
Block, BlockCheckMode, Closure, Destination, Expr, ExprKind, LoopSource, MatchSource, QPath, UnOp, UnsafeSource,
18+
YieldSource,
19+
};
20+
use rustc_lint::{LateContext, LintContext};
21+
use rustc_middle::ty::TyCtxt;
22+
use rustc_session::Session;
23+
use rustc_span::{Span, Symbol};
24+
25+
#[derive(Clone, Copy)]
26+
enum Pat {
27+
Str(&'static str),
28+
Sym(Symbol),
29+
Num,
30+
}
31+
32+
/// Checks if the start and the end of the span's text matches the patterns. This will return false
33+
/// if the span crosses multiple files or if source is not available.
34+
fn span_matches_pat(sess: &Session, span: Span, start_pat: Pat, end_pat: Pat) -> bool {
35+
let pos = sess.source_map().lookup_byte_offset(span.lo());
36+
let Some(ref src) = pos.sf.src else {
37+
return false;
38+
};
39+
let end = span.hi() - pos.sf.start_pos;
40+
src.get(pos.pos.0 as usize..end.0 as usize).map_or(false, |s| {
41+
// Spans can be wrapped in a mixture or parenthesis, whitespace, and trailing commas.
42+
let start_str = s.trim_start_matches(|c: char| c.is_whitespace() || c == '(');
43+
let end_str = s.trim_end_matches(|c: char| c.is_whitespace() || c == ')' || c == ',');
44+
(match start_pat {
45+
Pat::Str(text) => start_str.starts_with(text),
46+
Pat::Sym(sym) => start_str.starts_with(sym.as_str()),
47+
Pat::Num => start_str.as_bytes().first().map_or(false, u8::is_ascii_digit),
48+
} && match end_pat {
49+
Pat::Str(text) => end_str.ends_with(text),
50+
Pat::Sym(sym) => end_str.ends_with(sym.as_str()),
51+
Pat::Num => end_str.as_bytes().last().map_or(false, u8::is_ascii_hexdigit),
52+
})
53+
})
54+
}
55+
56+
/// Get the search patterns to use for the given literal
57+
fn lit_search_pat(lit: &LitKind) -> (Pat, Pat) {
58+
match lit {
59+
LitKind::Str(_, StrStyle::Cooked) => (Pat::Str("\""), Pat::Str("\"")),
60+
LitKind::Str(_, StrStyle::Raw(0)) => (Pat::Str("r"), Pat::Str("\"")),
61+
LitKind::Str(_, StrStyle::Raw(_)) => (Pat::Str("r#"), Pat::Str("#")),
62+
LitKind::ByteStr(_) => (Pat::Str("b\""), Pat::Str("\"")),
63+
LitKind::Byte(_) => (Pat::Str("b'"), Pat::Str("'")),
64+
LitKind::Char(_) => (Pat::Str("'"), Pat::Str("'")),
65+
LitKind::Int(_, LitIntType::Signed(IntTy::Isize)) => (Pat::Num, Pat::Str("isize")),
66+
LitKind::Int(_, LitIntType::Unsigned(UintTy::Usize)) => (Pat::Num, Pat::Str("usize")),
67+
LitKind::Int(..) => (Pat::Num, Pat::Num),
68+
LitKind::Float(..) => (Pat::Num, Pat::Str("")),
69+
LitKind::Bool(true) => (Pat::Str("true"), Pat::Str("true")),
70+
LitKind::Bool(false) => (Pat::Str("false"), Pat::Str("false")),
71+
_ => (Pat::Str(""), Pat::Str("")),
72+
}
73+
}
74+
75+
/// Get the search patterns to use for the given path
76+
fn qpath_search_pat(path: &QPath<'_>) -> (Pat, Pat) {
77+
match path {
78+
QPath::Resolved(ty, path) => {
79+
let start = if ty.is_some() {
80+
Pat::Str("<")
81+
} else {
82+
path.segments
83+
.first()
84+
.map_or(Pat::Str(""), |seg| Pat::Sym(seg.ident.name))
85+
};
86+
let end = path.segments.last().map_or(Pat::Str(""), |seg| {
87+
if seg.args.is_some() {
88+
Pat::Str(">")
89+
} else {
90+
Pat::Sym(seg.ident.name)
91+
}
92+
});
93+
(start, end)
94+
},
95+
QPath::TypeRelative(_, name) => (Pat::Str(""), Pat::Sym(name.ident.name)),
96+
QPath::LangItem(..) => (Pat::Str(""), Pat::Str("")),
97+
}
98+
}
99+
100+
/// Get the search patterns to use for the given expression
101+
fn expr_search_pat(tcx: TyCtxt<'_>, e: &Expr<'_>) -> (Pat, Pat) {
102+
match e.kind {
103+
ExprKind::Box(e) => (Pat::Str("box"), expr_search_pat(tcx, e).1),
104+
ExprKind::ConstBlock(_) => (Pat::Str("const"), Pat::Str("}")),
105+
ExprKind::Tup([]) => (Pat::Str(")"), Pat::Str("(")),
106+
ExprKind::Unary(UnOp::Deref, _) => (Pat::Str("*"), expr_search_pat(tcx, e).1),
107+
ExprKind::Unary(UnOp::Not, _) => (Pat::Str("!"), expr_search_pat(tcx, e).1),
108+
ExprKind::Unary(UnOp::Neg, _) => (Pat::Str("-"), expr_search_pat(tcx, e).1),
109+
ExprKind::Lit(ref lit) => lit_search_pat(&lit.node),
110+
ExprKind::Array(_) | ExprKind::Repeat(..) => (Pat::Str("["), Pat::Str("]")),
111+
ExprKind::Call(e, []) | ExprKind::MethodCall(_, [e], _) => (expr_search_pat(tcx, e).0, Pat::Str("(")),
112+
ExprKind::Call(first, [.., last])
113+
| ExprKind::MethodCall(_, [first, .., last], _)
114+
| ExprKind::Binary(_, first, last)
115+
| ExprKind::Tup([first, .., last])
116+
| ExprKind::Assign(first, last, _)
117+
| ExprKind::AssignOp(_, first, last) => (expr_search_pat(tcx, first).0, expr_search_pat(tcx, last).1),
118+
ExprKind::Tup([e]) | ExprKind::DropTemps(e) => expr_search_pat(tcx, e),
119+
ExprKind::Cast(e, _) | ExprKind::Type(e, _) => (expr_search_pat(tcx, e).0, Pat::Str("")),
120+
ExprKind::Let(let_expr) => (Pat::Str("let"), expr_search_pat(tcx, let_expr.init).1),
121+
ExprKind::If(..) => (Pat::Str("if"), Pat::Str("}")),
122+
ExprKind::Loop(_, Some(_), _, _) | ExprKind::Block(_, Some(_)) => (Pat::Str("'"), Pat::Str("}")),
123+
ExprKind::Loop(_, None, LoopSource::Loop, _) => (Pat::Str("loop"), Pat::Str("}")),
124+
ExprKind::Loop(_, None, LoopSource::While, _) => (Pat::Str("while"), Pat::Str("}")),
125+
ExprKind::Loop(_, None, LoopSource::ForLoop, _) | ExprKind::Match(_, _, MatchSource::ForLoopDesugar) => {
126+
(Pat::Str("for"), Pat::Str("}"))
127+
},
128+
ExprKind::Match(_, _, MatchSource::Normal) => (Pat::Str("match"), Pat::Str("}")),
129+
ExprKind::Match(e, _, MatchSource::TryDesugar) => (expr_search_pat(tcx, e).0, Pat::Str("?")),
130+
ExprKind::Match(e, _, MatchSource::AwaitDesugar) | ExprKind::Yield(e, YieldSource::Await { .. }) => {
131+
(expr_search_pat(tcx, e).0, Pat::Str("await"))
132+
},
133+
ExprKind::Closure(&Closure { body, .. }) => (Pat::Str(""), expr_search_pat(tcx, &tcx.hir().body(body).value).1),
134+
ExprKind::Block(
135+
Block {
136+
rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided),
137+
..
138+
},
139+
None,
140+
) => (Pat::Str("unsafe"), Pat::Str("}")),
141+
ExprKind::Block(_, None) => (Pat::Str("{"), Pat::Str("}")),
142+
ExprKind::Field(e, name) => (expr_search_pat(tcx, e).0, Pat::Sym(name.name)),
143+
ExprKind::Index(e, _) => (expr_search_pat(tcx, e).0, Pat::Str("]")),
144+
ExprKind::Path(ref path) => qpath_search_pat(path),
145+
ExprKind::AddrOf(_, _, e) => (Pat::Str("&"), expr_search_pat(tcx, e).1),
146+
ExprKind::Break(Destination { label: None, .. }, None) => (Pat::Str("break"), Pat::Str("break")),
147+
ExprKind::Break(Destination { label: Some(name), .. }, None) => (Pat::Str("break"), Pat::Sym(name.ident.name)),
148+
ExprKind::Break(_, Some(e)) => (Pat::Str("break"), expr_search_pat(tcx, e).1),
149+
ExprKind::Continue(Destination { label: None, .. }) => (Pat::Str("continue"), Pat::Str("continue")),
150+
ExprKind::Continue(Destination { label: Some(name), .. }) => (Pat::Str("continue"), Pat::Sym(name.ident.name)),
151+
ExprKind::Ret(None) => (Pat::Str("return"), Pat::Str("return")),
152+
ExprKind::Struct(path, _, _) => (qpath_search_pat(path).0, Pat::Str("}")),
153+
ExprKind::Yield(e, YieldSource::Yield) => (Pat::Str("yield"), expr_search_pat(tcx, e).1),
154+
_ => (Pat::Str(""), Pat::Str("")),
155+
}
156+
}
157+
158+
/// Checks if the expression likely came from a proc-macro
159+
pub fn is_expr_from_proc_macro(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
160+
let (start_pat, end_pat) = expr_search_pat(cx.tcx, e);
161+
!span_matches_pat(cx.sess(), e.span, start_pat, end_pat)
162+
}
163+
164+
/// Checks if the span actually refers to a match expression
165+
pub fn is_span_match(cx: &LateContext<'_>, span: Span) -> bool {
166+
span_matches_pat(cx.sess(), span, Pat::Str("match"), Pat::Str("}"))
167+
}

clippy_utils/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub mod sym_helper;
3838

3939
pub mod ast_utils;
4040
pub mod attrs;
41+
mod check_proc_macro;
4142
pub mod comparisons;
4243
pub mod consts;
4344
pub mod diagnostics;
@@ -58,6 +59,7 @@ pub mod usage;
5859
pub mod visitors;
5960

6061
pub use self::attrs::*;
62+
pub use self::check_proc_macro::{is_expr_from_proc_macro, is_span_match};
6163
pub use self::hir_utils::{
6264
both, count_eq, eq_expr_value, hash_expr, hash_stmt, over, HirEqInterExpr, SpanlessEq, SpanlessHash,
6365
};

clippy_utils/src/source.rs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,6 @@ use rustc_span::source_map::SourceMap;
1111
use rustc_span::{BytePos, Pos, Span, SpanData, SyntaxContext};
1212
use std::borrow::Cow;
1313

14-
/// Checks if the span starts with the given text. This will return false if the span crosses
15-
/// multiple files or if source is not available.
16-
///
17-
/// This is used to check for proc macros giving unhelpful spans to things.
18-
pub fn span_starts_with<T: LintContext>(cx: &T, span: Span, text: &str) -> bool {
19-
fn helper(sm: &SourceMap, span: Span, text: &str) -> bool {
20-
let pos = sm.lookup_byte_offset(span.lo());
21-
let Some(ref src) = pos.sf.src else {
22-
return false;
23-
};
24-
let end = span.hi() - pos.sf.start_pos;
25-
src.get(pos.pos.0 as usize..end.0 as usize)
26-
// Expression spans can include wrapping parenthesis. Remove them first.
27-
.map_or(false, |s| s.trim_start_matches('(').starts_with(text))
28-
}
29-
helper(cx.sess().source_map(), span, text)
30-
}
31-
3214
/// Like `snippet_block`, but add braces if the expr is not an `ExprKind::Block`.
3315
/// Also takes an `Option<String>` which can be put inside the braces.
3416
pub fn expr_block<'a, T: LintContext>(

tests/ui/unit_arg.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// aux-build: proc_macro_with_span.rs
2+
13
#![warn(clippy::unit_arg)]
24
#![allow(
35
clippy::no_effect,
@@ -8,9 +10,13 @@
810
clippy::or_fun_call,
911
clippy::needless_question_mark,
1012
clippy::self_named_constructors,
11-
clippy::let_unit_value
13+
clippy::let_unit_value,
14+
clippy::never_loop
1215
)]
1316

17+
extern crate proc_macro_with_span;
18+
19+
use proc_macro_with_span::with_span;
1420
use std::fmt::Debug;
1521

1622
fn foo<T: Debug>(t: T) {
@@ -127,6 +133,10 @@ fn returning_expr() -> Option<()> {
127133

128134
fn taking_multiple_units(a: (), b: ()) {}
129135

136+
fn proc_macro() {
137+
with_span!(span taking_multiple_units(unsafe { (); }, 'x: loop { break 'x (); }));
138+
}
139+
130140
fn main() {
131141
bad();
132142
ok();

tests/ui/unit_arg.stderr

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: passing a unit value to a function
2-
--> $DIR/unit_arg.rs:57:5
2+
--> $DIR/unit_arg.rs:63:5
33
|
44
LL | / foo({
55
LL | | 1;
@@ -20,7 +20,7 @@ LL ~ foo(());
2020
|
2121

2222
error: passing a unit value to a function
23-
--> $DIR/unit_arg.rs:60:5
23+
--> $DIR/unit_arg.rs:66:5
2424
|
2525
LL | foo(foo(1));
2626
| ^^^^^^^^^^^
@@ -32,7 +32,7 @@ LL ~ foo(());
3232
|
3333

3434
error: passing a unit value to a function
35-
--> $DIR/unit_arg.rs:61:5
35+
--> $DIR/unit_arg.rs:67:5
3636
|
3737
LL | / foo({
3838
LL | | foo(1);
@@ -54,7 +54,7 @@ LL ~ foo(());
5454
|
5555

5656
error: passing a unit value to a function
57-
--> $DIR/unit_arg.rs:66:5
57+
--> $DIR/unit_arg.rs:72:5
5858
|
5959
LL | / b.bar({
6060
LL | | 1;
@@ -74,7 +74,7 @@ LL ~ b.bar(());
7474
|
7575

7676
error: passing unit values to a function
77-
--> $DIR/unit_arg.rs:69:5
77+
--> $DIR/unit_arg.rs:75:5
7878
|
7979
LL | taking_multiple_units(foo(0), foo(1));
8080
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -87,7 +87,7 @@ LL ~ taking_multiple_units((), ());
8787
|
8888

8989
error: passing unit values to a function
90-
--> $DIR/unit_arg.rs:70:5
90+
--> $DIR/unit_arg.rs:76:5
9191
|
9292
LL | / taking_multiple_units(foo(0), {
9393
LL | | foo(1);
@@ -110,7 +110,7 @@ LL ~ taking_multiple_units((), ());
110110
|
111111

112112
error: passing unit values to a function
113-
--> $DIR/unit_arg.rs:74:5
113+
--> $DIR/unit_arg.rs:80:5
114114
|
115115
LL | / taking_multiple_units(
116116
LL | | {
@@ -146,7 +146,7 @@ LL ~ );
146146
|
147147

148148
error: passing a unit value to a function
149-
--> $DIR/unit_arg.rs:85:13
149+
--> $DIR/unit_arg.rs:91:13
150150
|
151151
LL | None.or(Some(foo(2)));
152152
| ^^^^^^^^^^^^
@@ -160,7 +160,7 @@ LL ~ });
160160
|
161161

162162
error: passing a unit value to a function
163-
--> $DIR/unit_arg.rs:88:5
163+
--> $DIR/unit_arg.rs:94:5
164164
|
165165
LL | foo(foo(()));
166166
| ^^^^^^^^^^^^
@@ -172,7 +172,7 @@ LL ~ foo(());
172172
|
173173

174174
error: passing a unit value to a function
175-
--> $DIR/unit_arg.rs:125:5
175+
--> $DIR/unit_arg.rs:131:5
176176
|
177177
LL | Some(foo(1))
178178
| ^^^^^^^^^^^^

0 commit comments

Comments
 (0)