Skip to content

Commit b92330b

Browse files
committed
fix nonstandard_macro_braces: suggest trailing semicolon when needed
1 parent 544e24a commit b92330b

File tree

4 files changed

+87
-28
lines changed

4 files changed

+87
-28
lines changed

clippy_lints/src/nonstandard_macro_braces.rs

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ declare_clippy_lint! {
1616
/// Checks that common macros are used with consistent bracing.
1717
///
1818
/// ### Why is this bad?
19-
/// This is mostly a consistency lint although using () or []
20-
/// doesn't give you a semicolon in item position, which can be unexpected.
19+
/// Having non-conventional braces on well-stablished macros can be confusing
20+
/// when debugging, and they bring incosistencies with the rest of the ecosystem.
2121
///
2222
/// ### Example
2323
/// ```no_run
@@ -33,8 +33,12 @@ declare_clippy_lint! {
3333
"check consistent use of braces in macro"
3434
}
3535

36-
/// The (callsite span, (open brace, close brace), source snippet)
37-
type MacroInfo = (Span, (char, char), SourceText);
36+
struct MacroInfo {
37+
callsite_span: Span,
38+
callsite_snippet: SourceText,
39+
old_open_brace: char,
40+
braces: (char, char),
41+
}
3842

3943
pub struct MacroBraces {
4044
macro_braces: FxHashMap<String, (char, char)>,
@@ -54,30 +58,58 @@ impl_lint_pass!(MacroBraces => [NONSTANDARD_MACRO_BRACES]);
5458

5559
impl EarlyLintPass for MacroBraces {
5660
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
57-
if let Some((span, braces, snip)) = is_offending_macro(cx, item.span, self) {
58-
emit_help(cx, &snip, braces, span);
59-
self.done.insert(span);
61+
if let Some(MacroInfo {
62+
callsite_span,
63+
callsite_snippet,
64+
braces,
65+
..
66+
}) = is_offending_macro(cx, item.span, self)
67+
{
68+
emit_help(cx, &callsite_snippet, braces, callsite_span, false);
69+
self.done.insert(callsite_span);
6070
}
6171
}
6272

6373
fn check_stmt(&mut self, cx: &EarlyContext<'_>, stmt: &ast::Stmt) {
64-
if let Some((span, braces, snip)) = is_offending_macro(cx, stmt.span, self) {
65-
emit_help(cx, &snip, braces, span);
66-
self.done.insert(span);
74+
if let Some(MacroInfo {
75+
callsite_span,
76+
callsite_snippet,
77+
braces,
78+
old_open_brace,
79+
}) = is_offending_macro(cx, stmt.span, self)
80+
{
81+
// if we turn `macro!{}` into `macro!()`/`macro![]`, we'll no longer get the implicit
82+
// trailing semicolon, see #9913
83+
// NOTE: `stmt.kind != StmtKind::MacCall` because `EarlyLintPass` happens after macro expansion
84+
let add_semi = matches!(stmt.kind, ast::StmtKind::Expr(..)) && old_open_brace == '{';
85+
emit_help(cx, &callsite_snippet, braces, callsite_span, add_semi);
86+
self.done.insert(callsite_span);
6787
}
6888
}
6989

7090
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
71-
if let Some((span, braces, snip)) = is_offending_macro(cx, expr.span, self) {
72-
emit_help(cx, &snip, braces, span);
73-
self.done.insert(span);
91+
if let Some(MacroInfo {
92+
callsite_span,
93+
callsite_snippet,
94+
braces,
95+
..
96+
}) = is_offending_macro(cx, expr.span, self)
97+
{
98+
emit_help(cx, &callsite_snippet, braces, callsite_span, false);
99+
self.done.insert(callsite_span);
74100
}
75101
}
76102

77103
fn check_ty(&mut self, cx: &EarlyContext<'_>, ty: &ast::Ty) {
78-
if let Some((span, braces, snip)) = is_offending_macro(cx, ty.span, self) {
79-
emit_help(cx, &snip, braces, span);
80-
self.done.insert(span);
104+
if let Some(MacroInfo {
105+
callsite_span,
106+
braces,
107+
callsite_snippet,
108+
..
109+
}) = is_offending_macro(cx, ty.span, self)
110+
{
111+
emit_help(cx, &callsite_snippet, braces, callsite_span, false);
112+
self.done.insert(callsite_span);
81113
}
82114
}
83115
}
@@ -90,39 +122,44 @@ fn is_offending_macro(cx: &EarlyContext<'_>, span: Span, mac_braces: &MacroBrace
90122
.last()
91123
.is_some_and(|e| e.macro_def_id.is_some_and(DefId::is_local))
92124
};
93-
let span_call_site = span.ctxt().outer_expn_data().call_site;
125+
let callsite_span = span.ctxt().outer_expn_data().call_site;
94126
if let ExpnKind::Macro(MacroKind::Bang, mac_name) = span.ctxt().outer_expn_data().kind
95127
&& let name = mac_name.as_str()
96128
&& let Some(&braces) = mac_braces.macro_braces.get(name)
97-
&& let Some(snip) = span_call_site.get_source_text(cx)
129+
&& let Some(snip) = callsite_span.get_source_text(cx)
98130
// we must check only invocation sites
99131
// https://github.com/rust-lang/rust-clippy/issues/7422
100-
&& snip.starts_with(&format!("{name}!"))
132+
&& let Some(macro_args_str) = snip.strip_prefix(name).and_then(|snip| snip.strip_prefix('!'))
133+
&& let Some(old_open_brace @ ('{' | '(' | '[')) = macro_args_str.trim_start().chars().next()
134+
&& old_open_brace != braces.0
101135
&& unnested_or_local()
102-
// make formatting consistent
103-
&& let c = snip.replace(' ', "")
104-
&& !c.starts_with(&format!("{name}!{}", braces.0))
105-
&& !mac_braces.done.contains(&span_call_site)
136+
&& !mac_braces.done.contains(&callsite_span)
106137
{
107-
Some((span_call_site, braces, snip))
138+
Some(MacroInfo {
139+
callsite_span,
140+
callsite_snippet: snip,
141+
old_open_brace,
142+
braces,
143+
})
108144
} else {
109145
None
110146
}
111147
}
112148

113-
fn emit_help(cx: &EarlyContext<'_>, snip: &str, (open, close): (char, char), span: Span) {
149+
fn emit_help(cx: &EarlyContext<'_>, snip: &str, (open, close): (char, char), span: Span, add_semi: bool) {
150+
let semi = if add_semi { ";" } else { "" };
114151
if let Some((macro_name, macro_args_str)) = snip.split_once('!') {
115152
let mut macro_args = macro_args_str.trim().to_string();
116153
// now remove the wrong braces
117-
macro_args.remove(0);
118154
macro_args.pop();
155+
macro_args.remove(0);
119156
span_lint_and_sugg(
120157
cx,
121158
NONSTANDARD_MACRO_BRACES,
122159
span,
123160
format!("use of irregular braces for `{macro_name}!` macro"),
124161
"consider writing",
125-
format!("{macro_name}!{open}{macro_args}{close}"),
162+
format!("{macro_name}!{open}{macro_args}{close}{semi}"),
126163
Applicability::MachineApplicable,
127164
);
128165
}

tests/ui-toml/nonstandard_macro_braces/conf_nonstandard_macro_braces.fixed

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,11 @@ fn main() {
6767

6868
printlnfoo!["test if printlnfoo is triggered by println"];
6969
}
70+
71+
#[rustfmt::skip]
72+
#[expect(clippy::no_effect)]
73+
fn issue9913() {
74+
println!("hello world");
75+
[0]; // separate statement, not indexing into the result of println.
76+
//~^^ nonstandard_macro_braces
77+
}

tests/ui-toml/nonstandard_macro_braces/conf_nonstandard_macro_braces.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,11 @@ fn main() {
6767

6868
printlnfoo!["test if printlnfoo is triggered by println"];
6969
}
70+
71+
#[rustfmt::skip]
72+
#[expect(clippy::no_effect)]
73+
fn issue9913() {
74+
println! {"hello world"}
75+
[0]; // separate statement, not indexing into the result of println.
76+
//~^^ nonstandard_macro_braces
77+
}

tests/ui-toml/nonstandard_macro_braces/conf_nonstandard_macro_braces.stderr

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,11 @@ error: use of irregular braces for `eprint!` macro
5454
LL | eprint!("test if user config overrides defaults");
5555
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `eprint!["test if user config overrides defaults"]`
5656

57-
error: aborting due to 8 previous errors
57+
error: use of irregular braces for `println!` macro
58+
--> tests/ui-toml/nonstandard_macro_braces/conf_nonstandard_macro_braces.rs:74:5
59+
|
60+
LL | println! {"hello world"}
61+
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `println!("hello world");`
62+
63+
error: aborting due to 9 previous errors
5864

0 commit comments

Comments
 (0)