Skip to content

Commit 231bf45

Browse files
authored
Make struct_field_names check private fields of public structs. (#14076)
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. changelog: [`struct_field_names`]: also check private fields of public structs
2 parents 06e7590 + 8c73b76 commit 231bf45

File tree

3 files changed

+200
-123
lines changed

3 files changed

+200
-123
lines changed

clippy_lints/src/item_name_repetitions.rs

Lines changed: 153 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use rustc_data_structures::fx::FxHashSet;
88
use rustc_hir::{EnumDef, FieldDef, Item, ItemKind, OwnerId, Variant, VariantData};
99
use rustc_lint::{LateContext, LateLintPass};
1010
use rustc_session::impl_lint_pass;
11-
use rustc_span::Span;
1211
use rustc_span::symbol::Symbol;
1312

1413
declare_clippy_lint! {
@@ -196,80 +195,169 @@ fn have_no_extra_prefix(prefixes: &[&str]) -> bool {
196195
prefixes.iter().all(|p| p == &"" || p == &"_")
197196
}
198197

199-
fn check_fields(cx: &LateContext<'_>, threshold: u64, item: &Item<'_>, fields: &[FieldDef<'_>]) {
200-
if (fields.len() as u64) < threshold {
201-
return;
202-
}
198+
impl ItemNameRepetitions {
199+
/// Lint the names of enum variants against the name of the enum.
200+
fn check_variants(&self, cx: &LateContext<'_>, item: &Item<'_>, def: &EnumDef<'_>) {
201+
if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(item.owner_id.def_id) {
202+
return;
203+
}
204+
205+
if (def.variants.len() as u64) < self.enum_threshold {
206+
return;
207+
}
203208

204-
check_struct_name_repetition(cx, item, fields);
209+
let item_name = item.ident.name.as_str();
210+
for var in def.variants {
211+
check_enum_start(cx, item_name, var);
212+
check_enum_end(cx, item_name, var);
213+
}
205214

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;
215+
Self::check_enum_common_affix(cx, item, def);
210216
}
211217

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 {
218+
/// Lint the names of struct fields against the name of the struct.
219+
fn check_fields(&self, cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) {
220+
if (fields.len() as u64) < self.struct_threshold {
221221
return;
222222
}
223223

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();
224+
self.check_struct_name_repetition(cx, item, fields);
225+
self.check_struct_common_affix(cx, item, fields);
236226
}
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),
227+
228+
fn check_enum_common_affix(cx: &LateContext<'_>, item: &Item<'_>, def: &EnumDef<'_>) {
229+
let first = match def.variants.first() {
230+
Some(variant) => variant.ident.name.as_str(),
231+
None => return,
251232
};
252-
if fields.iter().all(|field| is_bool(field.ty)) {
253-
// If all fields are booleans, we don't want to emit this lint.
254-
return;
233+
let mut pre = camel_case_split(first);
234+
let mut post = pre.clone();
235+
post.reverse();
236+
for var in def.variants {
237+
let name = var.ident.name.as_str();
238+
239+
let variant_split = camel_case_split(name);
240+
if variant_split.len() == 1 {
241+
return;
242+
}
243+
244+
pre = pre
245+
.iter()
246+
.zip(variant_split.iter())
247+
.take_while(|(a, b)| a == b)
248+
.map(|e| *e.0)
249+
.collect();
250+
post = post
251+
.iter()
252+
.zip(variant_split.iter().rev())
253+
.take_while(|(a, b)| a == b)
254+
.map(|e| *e.0)
255+
.collect();
255256
}
257+
let (what, value) = match (have_no_extra_prefix(&pre), post.is_empty()) {
258+
(true, true) => return,
259+
(false, _) => ("pre", pre.join("")),
260+
(true, false) => {
261+
post.reverse();
262+
("post", post.join(""))
263+
},
264+
};
256265
span_lint_and_help(
257266
cx,
258-
STRUCT_FIELD_NAMES,
267+
ENUM_VARIANT_NAMES,
259268
item.span,
260-
format!("all fields have the same {what}fix: `{value}`"),
269+
format!("all variants have the same {what}fix: `{value}`"),
261270
None,
262-
format!("remove the {what}fixes"),
271+
format!(
272+
"remove the {what}fixes and use full paths to \
273+
the variants instead of glob imports"
274+
),
263275
);
264276
}
265-
}
266277

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)
278+
fn check_struct_common_affix(&self, cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) {
279+
// if the SyntaxContext of the identifiers of the fields and struct differ dont lint them.
280+
// this prevents linting in macros in which the location of the field identifier names differ
281+
if !fields.iter().all(|field| item.ident.span.eq_ctxt(field.ident.span)) {
282+
return;
283+
}
284+
285+
if self.avoid_breaking_exported_api
286+
&& fields
287+
.iter()
288+
.any(|field| cx.effective_visibilities.is_exported(field.def_id))
289+
{
290+
return;
291+
}
292+
293+
let mut pre: Vec<&str> = match fields.first() {
294+
Some(first_field) => first_field.ident.name.as_str().split('_').collect(),
295+
None => return,
296+
};
297+
let mut post = pre.clone();
298+
post.reverse();
299+
for field in fields {
300+
let field_split: Vec<&str> = field.ident.name.as_str().split('_').collect();
301+
if field_split.len() == 1 {
302+
return;
303+
}
304+
305+
pre = pre
306+
.into_iter()
307+
.zip(field_split.iter())
308+
.take_while(|(a, b)| &a == b)
309+
.map(|e| e.0)
310+
.collect();
311+
post = post
312+
.into_iter()
313+
.zip(field_split.iter().rev())
314+
.take_while(|(a, b)| &a == b)
315+
.map(|e| e.0)
316+
.collect();
317+
}
318+
let prefix = pre.join("_");
319+
post.reverse();
320+
let postfix = match post.last() {
321+
Some(&"") => post.join("_") + "_",
322+
Some(_) | None => post.join("_"),
323+
};
324+
if fields.len() > 1 {
325+
let (what, value) = match (
326+
prefix.is_empty() || prefix.chars().all(|c| c == '_'),
327+
postfix.is_empty(),
328+
) {
329+
(true, true) => return,
330+
(false, _) => ("pre", prefix),
331+
(true, false) => ("post", postfix),
332+
};
333+
if fields.iter().all(|field| is_bool(field.ty)) {
334+
// If all fields are booleans, we don't want to emit this lint.
335+
return;
336+
}
337+
span_lint_and_help(
338+
cx,
339+
STRUCT_FIELD_NAMES,
340+
item.span,
341+
format!("all fields have the same {what}fix: `{value}`"),
342+
None,
343+
format!("remove the {what}fixes"),
344+
);
345+
}
346+
}
347+
348+
fn check_struct_name_repetition(&self, cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) {
349+
let snake_name = to_snake_case(item.ident.name.as_str());
350+
let item_name_words: Vec<&str> = snake_name.split('_').collect();
351+
for field in fields {
352+
if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(field.def_id) {
353+
continue;
354+
}
355+
356+
if !field.ident.span.eq_ctxt(item.ident.span) {
357+
// consider linting only if the field identifier has the same SyntaxContext as the item(struct)
358+
continue;
359+
}
360+
273361
let field_words: Vec<&str> = field.ident.name.as_str().split('_').collect();
274362
if field_words.len() >= item_name_words.len() {
275363
// if the field name is shorter than the struct name it cannot contain it
@@ -337,65 +425,6 @@ fn check_enum_end(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>)
337425
}
338426
}
339427

340-
fn check_variant(cx: &LateContext<'_>, threshold: u64, def: &EnumDef<'_>, item_name: &str, span: Span) {
341-
if (def.variants.len() as u64) < threshold {
342-
return;
343-
}
344-
345-
for var in def.variants {
346-
check_enum_start(cx, item_name, var);
347-
check_enum_end(cx, item_name, var);
348-
}
349-
350-
let first = match def.variants.first() {
351-
Some(variant) => variant.ident.name.as_str(),
352-
None => return,
353-
};
354-
let mut pre = camel_case_split(first);
355-
let mut post = pre.clone();
356-
post.reverse();
357-
for var in def.variants {
358-
let name = var.ident.name.as_str();
359-
360-
let variant_split = camel_case_split(name);
361-
if variant_split.len() == 1 {
362-
return;
363-
}
364-
365-
pre = pre
366-
.iter()
367-
.zip(variant_split.iter())
368-
.take_while(|(a, b)| a == b)
369-
.map(|e| *e.0)
370-
.collect();
371-
post = post
372-
.iter()
373-
.zip(variant_split.iter().rev())
374-
.take_while(|(a, b)| a == b)
375-
.map(|e| *e.0)
376-
.collect();
377-
}
378-
let (what, value) = match (have_no_extra_prefix(&pre), post.is_empty()) {
379-
(true, true) => return,
380-
(false, _) => ("pre", pre.join("")),
381-
(true, false) => {
382-
post.reverse();
383-
("post", post.join(""))
384-
},
385-
};
386-
span_lint_and_help(
387-
cx,
388-
ENUM_VARIANT_NAMES,
389-
span,
390-
format!("all variants have the same {what}fix: `{value}`"),
391-
None,
392-
format!(
393-
"remove the {what}fixes and use full paths to \
394-
the variants instead of glob imports"
395-
),
396-
);
397-
}
398-
399428
impl LateLintPass<'_> for ItemNameRepetitions {
400429
fn check_item_post(&mut self, _cx: &LateContext<'_>, _item: &Item<'_>) {
401430
let last = self.modules.pop();
@@ -458,17 +487,19 @@ impl LateLintPass<'_> for ItemNameRepetitions {
458487
}
459488
}
460489
}
461-
if !(self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(item.owner_id.def_id))
462-
&& span_is_local(item.span)
463-
{
490+
491+
if span_is_local(item.span) {
464492
match item.kind {
465-
ItemKind::Enum(def, _) => check_variant(cx, self.enum_threshold, &def, item_name, item.span),
493+
ItemKind::Enum(def, _) => {
494+
self.check_variants(cx, item, &def);
495+
},
466496
ItemKind::Struct(VariantData::Struct { fields, .. }, _) => {
467-
check_fields(cx, self.struct_threshold, item, fields);
497+
self.check_fields(cx, item, fields);
468498
},
469499
_ => (),
470500
}
471501
}
502+
472503
self.modules.push((item.ident.name, item_camel, item.owner_id));
473504
}
474505
}

tests/ui/struct_fields.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,31 @@ struct Use {
344344
//~^ struct_field_names
345345
}
346346

347+
// should lint on private fields of public structs (renaming them is not breaking-exported-api)
348+
pub struct PubStructFieldNamedAfterStruct {
349+
pub_struct_field_named_after_struct: bool,
350+
//~^ ERROR: field name starts with the struct's name
351+
other1: bool,
352+
other2: bool,
353+
}
354+
pub struct PubStructFieldPrefix {
355+
//~^ ERROR: all fields have the same prefix: `field`
356+
field_foo: u8,
357+
field_bar: u8,
358+
field_baz: u8,
359+
}
360+
// ...but should not lint on structs with public fields.
361+
pub struct PubStructPubAndPrivateFields {
362+
/// One could argue that this field should be linted, but currently, any public field stops all
363+
/// checking.
364+
pub_struct_pub_and_private_fields_1: bool,
365+
pub pub_struct_pub_and_private_fields_2: bool,
366+
}
367+
// nor on common prefixes if one of the involved fields is public
368+
pub struct PubStructPubAndPrivateFieldPrefix {
369+
pub field_foo: u8,
370+
field_bar: u8,
371+
field_baz: u8,
372+
}
373+
347374
fn main() {}

tests/ui/struct_fields.stderr

Lines changed: 20 additions & 1 deletion
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:349: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:354: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)