Skip to content

Commit 454bd77

Browse files
committed
Make struct_name_repetitions lint on private fields of public structs.
Currently, If a struct is `pub` and its field is private, and `avoid-breaking-exported-api = true` (default), then `struct_field_names` will not lint the field, even though changing the field’s name is not a breaking change. This is because the breaking-exported-api condition was checking the visibility of the struct, not its fields (perhaps because the same code was used for enums). With this change, Clippy will check the field’s effective visibility only. Note: This change is large because some functions were moved into an `impl` to be able to access more configuration. Consider viewing the diff with whitespace ignored.
1 parent d7e20a9 commit 454bd77

File tree

3 files changed

+129
-70
lines changed

3 files changed

+129
-70
lines changed

clippy_lints/src/item_name_repetitions.rs

+95-69
Original file line numberDiff line numberDiff line change
@@ -196,80 +196,100 @@ fn have_no_extra_prefix(prefixes: &[&str]) -> bool {
196196
prefixes.iter().all(|p| p == &"" || p == &"_")
197197
}
198198

199-
fn check_fields(cx: &LateContext<'_>, threshold: u64, item: &Item<'_>, fields: &[FieldDef<'_>]) {
200-
if (fields.len() as u64) < threshold {
201-
return;
202-
}
203-
204-
check_struct_name_repetition(cx, item, fields);
199+
impl ItemNameRepetitions {
200+
/// Lint the names of struct fields against the name of the struct.
201+
fn check_fields(&self, cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) {
202+
if (fields.len() as u64) < self.struct_threshold {
203+
return;
204+
}
205205

206-
// if the SyntaxContext of the identifiers of the fields and struct differ dont lint them.
207-
// this prevents linting in macros in which the location of the field identifier names differ
208-
if !fields.iter().all(|field| item.ident.span.eq_ctxt(field.ident.span)) {
209-
return;
206+
self.check_struct_name_repetition(cx, item, fields);
207+
self.check_common_affix(cx, item, fields);
210208
}
211209

212-
let mut pre: Vec<&str> = match fields.first() {
213-
Some(first_field) => first_field.ident.name.as_str().split('_').collect(),
214-
None => return,
215-
};
216-
let mut post = pre.clone();
217-
post.reverse();
218-
for field in fields {
219-
let field_split: Vec<&str> = field.ident.name.as_str().split('_').collect();
220-
if field_split.len() == 1 {
210+
fn check_common_affix(&self, cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) {
211+
// if the SyntaxContext of the identifiers of the fields and struct differ dont lint them.
212+
// this prevents linting in macros in which the location of the field identifier names differ
213+
if !fields.iter().all(|field| item.ident.span.eq_ctxt(field.ident.span)) {
221214
return;
222215
}
223216

224-
pre = pre
225-
.into_iter()
226-
.zip(field_split.iter())
227-
.take_while(|(a, b)| &a == b)
228-
.map(|e| e.0)
229-
.collect();
230-
post = post
231-
.into_iter()
232-
.zip(field_split.iter().rev())
233-
.take_while(|(a, b)| &a == b)
234-
.map(|e| e.0)
235-
.collect();
236-
}
237-
let prefix = pre.join("_");
238-
post.reverse();
239-
let postfix = match post.last() {
240-
Some(&"") => post.join("_") + "_",
241-
Some(_) | None => post.join("_"),
242-
};
243-
if fields.len() > 1 {
244-
let (what, value) = match (
245-
prefix.is_empty() || prefix.chars().all(|c| c == '_'),
246-
postfix.is_empty(),
247-
) {
248-
(true, true) => return,
249-
(false, _) => ("pre", prefix),
250-
(true, false) => ("post", postfix),
251-
};
252-
if fields.iter().all(|field| is_bool(field.ty)) {
253-
// If all fields are booleans, we don't want to emit this lint.
217+
if self.avoid_breaking_exported_api
218+
&& fields
219+
.iter()
220+
.any(|field| cx.effective_visibilities.is_exported(field.def_id))
221+
{
254222
return;
255223
}
256-
span_lint_and_help(
257-
cx,
258-
STRUCT_FIELD_NAMES,
259-
item.span,
260-
format!("all fields have the same {what}fix: `{value}`"),
261-
None,
262-
format!("remove the {what}fixes"),
263-
);
224+
225+
let mut pre: Vec<&str> = match fields.first() {
226+
Some(first_field) => first_field.ident.name.as_str().split('_').collect(),
227+
None => return,
228+
};
229+
let mut post = pre.clone();
230+
post.reverse();
231+
for field in fields {
232+
let field_split: Vec<&str> = field.ident.name.as_str().split('_').collect();
233+
if field_split.len() == 1 {
234+
return;
235+
}
236+
237+
pre = pre
238+
.into_iter()
239+
.zip(field_split.iter())
240+
.take_while(|(a, b)| &a == b)
241+
.map(|e| e.0)
242+
.collect();
243+
post = post
244+
.into_iter()
245+
.zip(field_split.iter().rev())
246+
.take_while(|(a, b)| &a == b)
247+
.map(|e| e.0)
248+
.collect();
249+
}
250+
let prefix = pre.join("_");
251+
post.reverse();
252+
let postfix = match post.last() {
253+
Some(&"") => post.join("_") + "_",
254+
Some(_) | None => post.join("_"),
255+
};
256+
if fields.len() > 1 {
257+
let (what, value) = match (
258+
prefix.is_empty() || prefix.chars().all(|c| c == '_'),
259+
postfix.is_empty(),
260+
) {
261+
(true, true) => return,
262+
(false, _) => ("pre", prefix),
263+
(true, false) => ("post", postfix),
264+
};
265+
if fields.iter().all(|field| is_bool(field.ty)) {
266+
// If all fields are booleans, we don't want to emit this lint.
267+
return;
268+
}
269+
span_lint_and_help(
270+
cx,
271+
STRUCT_FIELD_NAMES,
272+
item.span,
273+
format!("all fields have the same {what}fix: `{value}`"),
274+
None,
275+
format!("remove the {what}fixes"),
276+
);
277+
}
264278
}
265-
}
266279

267-
fn check_struct_name_repetition(cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) {
268-
let snake_name = to_snake_case(item.ident.name.as_str());
269-
let item_name_words: Vec<&str> = snake_name.split('_').collect();
270-
for field in fields {
271-
if field.ident.span.eq_ctxt(item.ident.span) {
272-
//consider linting only if the field identifier has the same SyntaxContext as the item(struct)
280+
fn check_struct_name_repetition(&self, cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) {
281+
let snake_name = to_snake_case(item.ident.name.as_str());
282+
let item_name_words: Vec<&str> = snake_name.split('_').collect();
283+
for field in fields {
284+
if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(field.def_id) {
285+
continue;
286+
}
287+
288+
if !field.ident.span.eq_ctxt(item.ident.span) {
289+
// consider linting only if the field identifier has the same SyntaxContext as the item(struct)
290+
continue;
291+
}
292+
273293
let field_words: Vec<&str> = field.ident.name.as_str().split('_').collect();
274294
if field_words.len() >= item_name_words.len() {
275295
// if the field name is shorter than the struct name it cannot contain it
@@ -458,17 +478,23 @@ impl LateLintPass<'_> for ItemNameRepetitions {
458478
}
459479
}
460480
}
461-
if !(self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(item.owner_id.def_id))
462-
&& span_is_local(item.span)
463-
{
481+
482+
if span_is_local(item.span) {
464483
match item.kind {
465-
ItemKind::Enum(def, _) => check_variant(cx, self.enum_threshold, &def, item_name, item.span),
484+
ItemKind::Enum(def, _) => {
485+
if !(self.avoid_breaking_exported_api
486+
&& cx.effective_visibilities.is_exported(item.owner_id.def_id))
487+
{
488+
check_variant(cx, self.enum_threshold, &def, item_name, item.span);
489+
}
490+
},
466491
ItemKind::Struct(VariantData::Struct { fields, .. }, _) => {
467-
check_fields(cx, self.struct_threshold, item, fields);
492+
self.check_fields(cx, item, fields);
468493
},
469494
_ => (),
470495
}
471496
}
497+
472498
self.modules.push((item.ident.name, item_camel, item.owner_id));
473499
}
474500
}

tests/ui/struct_fields.rs

+14
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,18 @@ struct Use {
342342
use_baz: bool,
343343
}
344344

345+
// should lint on private fields of public structs (renaming them is not breaking-exported-api)
346+
pub struct PubStructFieldNamedAfterStruct {
347+
pub_struct_field_named_after_struct: bool,
348+
//~^ ERROR: field name starts with the struct's name
349+
other1: bool,
350+
other2: bool,
351+
}
352+
pub struct PubStructFieldPrefix {
353+
//~^ ERROR: all fields have the same prefix: `field`
354+
field_foo: u8,
355+
field_bar: u8,
356+
field_baz: u8,
357+
}
358+
345359
fn main() {}

tests/ui/struct_fields.stderr

+20-1
Original file line numberDiff line numberDiff line change
@@ -281,5 +281,24 @@ error: field name starts with the struct's name
281281
LL | use_baz: bool,
282282
| ^^^^^^^^^^^^^
283283

284-
error: aborting due to 24 previous errors
284+
error: field name starts with the struct's name
285+
--> tests/ui/struct_fields.rs:347:5
286+
|
287+
LL | pub_struct_field_named_after_struct: bool,
288+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
289+
290+
error: all fields have the same prefix: `field`
291+
--> tests/ui/struct_fields.rs:352:1
292+
|
293+
LL | / pub struct PubStructFieldPrefix {
294+
LL | |
295+
LL | | field_foo: u8,
296+
LL | | field_bar: u8,
297+
LL | | field_baz: u8,
298+
LL | | }
299+
| |_^
300+
|
301+
= help: remove the prefixes
302+
303+
error: aborting due to 26 previous errors
285304

0 commit comments

Comments
 (0)