Skip to content

Commit bcd76c3

Browse files
authored
empty_enum_variants_with_brackets: Do not lint reachable enums and enum variants used as functions in the same crate (#12971)
Fixes #12551 changelog: [`empty_enum_variants_with_brackets`]: Do not lint reachable enums or enums which are used as functions within the same crate. r? @xFrednet
2 parents e294f94 + fa9254f commit bcd76c3

5 files changed

+385
-68
lines changed

clippy_lints/src/empty_with_brackets.rs

+178-55
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::source::snippet_opt;
3-
use rustc_ast::ast::{Item, ItemKind, Variant, VariantData};
1+
use clippy_utils::attrs::span_contains_cfg;
2+
use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then};
3+
use rustc_data_structures::fx::FxIndexMap;
44
use rustc_errors::Applicability;
5-
use rustc_lexer::TokenKind;
6-
use rustc_lint::{EarlyContext, EarlyLintPass};
7-
use rustc_session::declare_lint_pass;
5+
use rustc_hir::def::CtorOf;
6+
use rustc_hir::def::DefKind::Ctor;
7+
use rustc_hir::def::Res::Def;
8+
use rustc_hir::def_id::LocalDefId;
9+
use rustc_hir::{Expr, ExprKind, Item, ItemKind, Node, Path, QPath, Variant, VariantData};
10+
use rustc_lint::{LateContext, LateLintPass};
11+
use rustc_middle::ty::TyCtxt;
12+
use rustc_session::impl_lint_pass;
813
use rustc_span::Span;
914

1015
declare_clippy_lint! {
@@ -70,10 +75,23 @@ declare_clippy_lint! {
7075
"finds enum variants with empty brackets"
7176
}
7277

73-
declare_lint_pass!(EmptyWithBrackets => [EMPTY_STRUCTS_WITH_BRACKETS, EMPTY_ENUM_VARIANTS_WITH_BRACKETS]);
78+
#[derive(Debug)]
79+
enum Usage {
80+
Unused { redundant_use_sites: Vec<Span> },
81+
Used,
82+
NoDefinition { redundant_use_sites: Vec<Span> },
83+
}
84+
85+
#[derive(Default)]
86+
pub struct EmptyWithBrackets {
87+
// Value holds `Usage::Used` if the empty tuple variant was used as a function
88+
empty_tuple_enum_variants: FxIndexMap<LocalDefId, Usage>,
89+
}
90+
91+
impl_lint_pass!(EmptyWithBrackets => [EMPTY_STRUCTS_WITH_BRACKETS, EMPTY_ENUM_VARIANTS_WITH_BRACKETS]);
7492

75-
impl EarlyLintPass for EmptyWithBrackets {
76-
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
93+
impl LateLintPass<'_> for EmptyWithBrackets {
94+
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
7795
if let ItemKind::Struct(ident, var_data, _) = &item.kind
7896
&& has_brackets(var_data)
7997
&& let span_after_ident = item.span.with_lo(ident.span.hi())
@@ -96,70 +114,175 @@ impl EarlyLintPass for EmptyWithBrackets {
96114
}
97115
}
98116

99-
fn check_variant(&mut self, cx: &EarlyContext<'_>, variant: &Variant) {
117+
fn check_variant(&mut self, cx: &LateContext<'_>, variant: &Variant<'_>) {
118+
// the span of the parentheses/braces
100119
let span_after_ident = variant.span.with_lo(variant.ident.span.hi());
101120

102-
if has_brackets(&variant.data) && has_no_fields(cx, &variant.data, span_after_ident) {
103-
span_lint_and_then(
121+
if has_no_fields(cx, &variant.data, span_after_ident) {
122+
match variant.data {
123+
VariantData::Struct { .. } => {
124+
// Empty struct variants can be linted immediately
125+
span_lint_and_then(
126+
cx,
127+
EMPTY_ENUM_VARIANTS_WITH_BRACKETS,
128+
span_after_ident,
129+
"enum variant has empty brackets",
130+
|diagnostic| {
131+
diagnostic.span_suggestion_hidden(
132+
span_after_ident,
133+
"remove the brackets",
134+
"",
135+
Applicability::MaybeIncorrect,
136+
);
137+
},
138+
);
139+
},
140+
VariantData::Tuple(.., local_def_id) => {
141+
// Don't lint reachable tuple enums
142+
if cx.effective_visibilities.is_reachable(variant.def_id) {
143+
return;
144+
}
145+
if let Some(entry) = self.empty_tuple_enum_variants.get_mut(&local_def_id) {
146+
// empty_tuple_enum_variants contains Usage::NoDefinition if the variant was called before the
147+
// definition was encountered. Now that there's a definition, convert it
148+
// to Usage::Unused.
149+
if let Usage::NoDefinition { redundant_use_sites } = entry {
150+
*entry = Usage::Unused {
151+
redundant_use_sites: redundant_use_sites.clone(),
152+
};
153+
}
154+
} else {
155+
self.empty_tuple_enum_variants.insert(
156+
local_def_id,
157+
Usage::Unused {
158+
redundant_use_sites: vec![],
159+
},
160+
);
161+
}
162+
},
163+
VariantData::Unit(..) => {},
164+
}
165+
}
166+
}
167+
168+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
169+
if let Some(def_id) = check_expr_for_enum_as_function(expr) {
170+
if let Some(parentheses_span) = call_parentheses_span(cx.tcx, expr) {
171+
// Do not count expressions from macro expansion as a redundant use site.
172+
if expr.span.from_expansion() {
173+
return;
174+
}
175+
match self.empty_tuple_enum_variants.get_mut(&def_id) {
176+
Some(
177+
&mut (Usage::Unused {
178+
ref mut redundant_use_sites,
179+
}
180+
| Usage::NoDefinition {
181+
ref mut redundant_use_sites,
182+
}),
183+
) => {
184+
redundant_use_sites.push(parentheses_span);
185+
},
186+
None => {
187+
// The variant isn't in the IndexMap which means its definition wasn't encountered yet.
188+
self.empty_tuple_enum_variants.insert(
189+
def_id,
190+
Usage::NoDefinition {
191+
redundant_use_sites: vec![parentheses_span],
192+
},
193+
);
194+
},
195+
_ => {},
196+
}
197+
} else {
198+
// The parentheses are not redundant.
199+
self.empty_tuple_enum_variants.insert(def_id, Usage::Used);
200+
}
201+
}
202+
}
203+
204+
fn check_crate_post(&mut self, cx: &LateContext<'_>) {
205+
for (local_def_id, usage) in &self.empty_tuple_enum_variants {
206+
// Ignore all variants with Usage::Used or Usage::NoDefinition
207+
let Usage::Unused { redundant_use_sites } = usage else {
208+
continue;
209+
};
210+
// Attempt to fetch the Variant from LocalDefId.
211+
let Node::Variant(variant) = cx.tcx.hir_node(
212+
cx.tcx
213+
.local_def_id_to_hir_id(cx.tcx.parent(local_def_id.to_def_id()).expect_local()),
214+
) else {
215+
continue;
216+
};
217+
// Span of the parentheses in variant definition
218+
let span = variant.span.with_lo(variant.ident.span.hi());
219+
span_lint_hir_and_then(
104220
cx,
105221
EMPTY_ENUM_VARIANTS_WITH_BRACKETS,
106-
span_after_ident,
222+
variant.hir_id,
223+
span,
107224
"enum variant has empty brackets",
108225
|diagnostic| {
109-
diagnostic.span_suggestion_hidden(
110-
span_after_ident,
111-
"remove the brackets",
112-
"",
113-
Applicability::MaybeIncorrect,
114-
);
226+
if redundant_use_sites.is_empty() {
227+
// If there's no redundant use sites, the definition is the only place to modify.
228+
diagnostic.span_suggestion_hidden(
229+
span,
230+
"remove the brackets",
231+
"",
232+
Applicability::MaybeIncorrect,
233+
);
234+
} else {
235+
let mut parentheses_spans: Vec<_> =
236+
redundant_use_sites.iter().map(|span| (*span, String::new())).collect();
237+
parentheses_spans.push((span, String::new()));
238+
diagnostic.multipart_suggestion(
239+
"remove the brackets",
240+
parentheses_spans,
241+
Applicability::MaybeIncorrect,
242+
);
243+
}
115244
},
116245
);
117246
}
118247
}
119248
}
120249

121-
fn has_no_ident_token(braces_span_str: &str) -> bool {
122-
!rustc_lexer::tokenize(braces_span_str).any(|t| t.kind == TokenKind::Ident)
250+
fn has_brackets(var_data: &VariantData<'_>) -> bool {
251+
!matches!(var_data, VariantData::Unit(..))
123252
}
124253

125-
fn has_brackets(var_data: &VariantData) -> bool {
126-
!matches!(var_data, VariantData::Unit(_))
127-
}
128-
129-
fn has_no_fields(cx: &EarlyContext<'_>, var_data: &VariantData, braces_span: Span) -> bool {
130-
if !var_data.fields().is_empty() {
131-
return false;
132-
}
133-
254+
fn has_no_fields(cx: &LateContext<'_>, var_data: &VariantData<'_>, braces_span: Span) -> bool {
255+
var_data.fields().is_empty() &&
134256
// there might still be field declarations hidden from the AST
135257
// (conditionally compiled code using #[cfg(..)])
136-
137-
let Some(braces_span_str) = snippet_opt(cx, braces_span) else {
138-
return false;
139-
};
140-
141-
has_no_ident_token(braces_span_str.as_ref())
258+
!span_contains_cfg(cx, braces_span)
142259
}
143260

144-
#[cfg(test)]
145-
mod unit_test {
146-
use super::*;
147-
148-
#[test]
149-
fn test_has_no_ident_token() {
150-
let input = "{ field: u8 }";
151-
assert!(!has_no_ident_token(input));
152-
153-
let input = "(u8, String);";
154-
assert!(!has_no_ident_token(input));
155-
156-
let input = " {
157-
// test = 5
158-
}
159-
";
160-
assert!(has_no_ident_token(input));
261+
// If expression HIR ID and callee HIR ID are same, returns the span of the parentheses, else,
262+
// returns None.
263+
fn call_parentheses_span(tcx: TyCtxt<'_>, expr: &Expr<'_>) -> Option<Span> {
264+
if let Node::Expr(parent) = tcx.parent_hir_node(expr.hir_id)
265+
&& let ExprKind::Call(callee, ..) = parent.kind
266+
&& callee.hir_id == expr.hir_id
267+
{
268+
Some(parent.span.with_lo(expr.span.hi()))
269+
} else {
270+
None
271+
}
272+
}
161273

162-
let input = " ();";
163-
assert!(has_no_ident_token(input));
274+
// Returns the LocalDefId of the variant being called as a function if it exists.
275+
fn check_expr_for_enum_as_function(expr: &Expr<'_>) -> Option<LocalDefId> {
276+
if let ExprKind::Path(QPath::Resolved(
277+
_,
278+
Path {
279+
res: Def(Ctor(CtorOf::Variant, _), def_id),
280+
..
281+
},
282+
)) = expr.kind
283+
{
284+
def_id.as_local()
285+
} else {
286+
None
164287
}
165288
}

clippy_lints/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
858858
store.register_late_pass(move |_| Box::new(write::Write::new(conf, format_args.clone())));
859859
store.register_late_pass(move |_| Box::new(cargo::Cargo::new(conf)));
860860
store.register_early_pass(|| Box::new(crate_in_macro_def::CrateInMacroDef));
861-
store.register_early_pass(|| Box::new(empty_with_brackets::EmptyWithBrackets));
861+
store.register_late_pass(|_| Box::new(empty_with_brackets::EmptyWithBrackets::default()));
862862
store.register_late_pass(|_| Box::new(unnecessary_owned_empty_strings::UnnecessaryOwnedEmptyStrings));
863863
store.register_early_pass(|| Box::new(pub_use::PubUse));
864864
store.register_late_pass(|_| Box::new(format_push_string::FormatPushString));

tests/ui/empty_enum_variants_with_brackets.fixed

+76-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ pub enum PublicTestEnum {
66
NonEmptyParentheses(i32, i32), // No error
77
EmptyBraces,
88
//~^ empty_enum_variants_with_brackets
9-
EmptyParentheses,
10-
//~^ empty_enum_variants_with_brackets
9+
EmptyParentheses(), // No error as enum is pub
1110
}
1211

1312
enum TestEnum {
@@ -20,6 +19,67 @@ enum TestEnum {
2019
AnotherEnum, // No error
2120
}
2221

22+
mod issue12551 {
23+
enum EvenOdd {
24+
// Used as functions -> no error
25+
Even(),
26+
Odd(),
27+
// Not used as a function
28+
Unknown,
29+
//~^ empty_enum_variants_with_brackets
30+
}
31+
32+
fn even_odd(x: i32) -> EvenOdd {
33+
(x % 2 == 0).then(EvenOdd::Even).unwrap_or_else(EvenOdd::Odd)
34+
}
35+
36+
fn natural_number(x: i32) -> NaturalOrNot {
37+
(x > 0)
38+
.then(NaturalOrNot::Natural)
39+
.unwrap_or_else(NaturalOrNot::NotNatural)
40+
}
41+
42+
enum NaturalOrNot {
43+
// Used as functions -> no error
44+
Natural(),
45+
NotNatural(),
46+
// Not used as a function
47+
Unknown,
48+
//~^ empty_enum_variants_with_brackets
49+
}
50+
51+
enum RedundantParenthesesFunctionCall {
52+
// Used as a function call but with redundant parentheses
53+
Parentheses,
54+
//~^ empty_enum_variants_with_brackets
55+
// Not used as a function
56+
NoParentheses,
57+
}
58+
59+
#[allow(clippy::no_effect)]
60+
fn redundant_parentheses_function_call() {
61+
// The parentheses in the below line are redundant.
62+
RedundantParenthesesFunctionCall::Parentheses;
63+
RedundantParenthesesFunctionCall::NoParentheses;
64+
}
65+
66+
// Same test as above but with usage of the enum occurring before the definition.
67+
#[allow(clippy::no_effect)]
68+
fn redundant_parentheses_function_call_2() {
69+
// The parentheses in the below line are redundant.
70+
RedundantParenthesesFunctionCall2::Parentheses;
71+
RedundantParenthesesFunctionCall2::NoParentheses;
72+
}
73+
74+
enum RedundantParenthesesFunctionCall2 {
75+
// Used as a function call but with redundant parentheses
76+
Parentheses,
77+
//~^ empty_enum_variants_with_brackets
78+
// Not used as a function
79+
NoParentheses,
80+
}
81+
}
82+
2383
enum TestEnumWithFeatures {
2484
NonEmptyBraces {
2585
#[cfg(feature = "thisisneverenabled")]
@@ -28,4 +88,18 @@ enum TestEnumWithFeatures {
2888
NonEmptyParentheses(#[cfg(feature = "thisisneverenabled")] i32), // No error
2989
}
3090

91+
#[derive(Clone)]
92+
enum Foo {
93+
Variant1(i32),
94+
Variant2,
95+
Variant3, //~ ERROR: enum variant has empty brackets
96+
}
97+
98+
#[derive(Clone)]
99+
pub enum PubFoo {
100+
Variant1(i32),
101+
Variant2,
102+
Variant3(),
103+
}
104+
31105
fn main() {}

0 commit comments

Comments
 (0)