bevy_lint/lints/style/unconventional_naming.rs
1//! Checks for types that implement certain Bevy traits but do not follow that trait's naming
2//! convention.
3//!
4//! This lint currently enforces the following conventions:
5//!
6//! |Trait|Convention|
7//! |-|-|
8//! |`Plugin`|Name ends in "Plugin"|
9//! |`SystemSet`|Name ends in "Systems"|
10//!
11//! # Motivation
12//!
13//! Bevy provides several traits, such as `Plugin` and `SystemSet`, that designate the primary
14//! purpose of a type. It is common for these types to follow certain naming conventions that
15//! *signal* how it should be used. This lint helps enforce these conventions to ensure consistency
16//! across the Bevy engine and ecosystem.
17//!
18//! # Example
19//!
20//! ```
21//! # use bevy::prelude::*;
22//! #
23//! struct Physics;
24//!
25//! impl Plugin for Physics {
26//! # fn build(&self, app: &mut App) {}
27//! #
28//! // ...
29//! }
30//!
31//! #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
32//! struct MyAudio;
33//! ```
34//!
35//! Use instead:
36//!
37//! ```
38//! # use bevy::prelude::*;
39//! #
40//! struct PhysicsPlugin;
41//!
42//! impl Plugin for PhysicsPlugin {
43//! # fn build(&self, app: &mut App) {}
44//! #
45//! // ...
46//! }
47//!
48//! #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
49//! struct MyAudioSystems;
50//! ```
51
52use clippy_utils::{diagnostics::span_lint_hir_and_then, path_res};
53use rustc_errors::Applicability;
54use rustc_hir::{HirId, Impl, Item, ItemKind, OwnerId};
55use rustc_lint::{LateContext, LateLintPass};
56use rustc_span::symbol::Ident;
57
58use crate::{declare_bevy_lint, declare_bevy_lint_pass, utils::hir_parse::impls_trait};
59
60declare_bevy_lint! {
61 pub(crate) UNCONVENTIONAL_NAMING,
62 super::Style,
63 "unconventional type name for a `Plugin` or `SystemSet`",
64}
65
66declare_bevy_lint_pass! {
67 pub(crate) UnconventionalNaming => [UNCONVENTIONAL_NAMING],
68}
69
70impl<'tcx> LateLintPass<'tcx> for UnconventionalNaming {
71 fn check_item(&mut self, cx: &LateContext<'tcx>, item: &Item<'tcx>) {
72 // Find `impl` items...
73 if let ItemKind::Impl(impl_) = item.kind
74 && let Some(conventional_name_impl) = TraitConvention::try_from_impl(cx, impl_)
75 {
76 // Try to resolve where this type was originally defined. This will result in a `DefId`
77 // pointing to the original `struct Foo` definition, or `impl <T>` if it's a generic
78 // parameter.
79 let Some(struct_def_id) = path_res(cx, impl_.self_ty).opt_def_id() else {
80 return;
81 };
82
83 // If this type is a generic parameter, exit. Their names, such as `T`, cannot be
84 // referenced by others.
85 if impl_
86 .generics
87 .params
88 .iter()
89 .any(|param| param.def_id.to_def_id() == struct_def_id)
90 {
91 return;
92 }
93
94 // Find the original name and span of the type.
95 let Some(Ident {
96 name: struct_name,
97 span: struct_span,
98 }) = cx.tcx.opt_item_ident(struct_def_id)
99 else {
100 return;
101 };
102
103 // skip lint if the struct was defined in an external macro
104 if struct_span.in_external_macro(cx.tcx.sess.source_map()) {
105 return;
106 }
107
108 // If the type's name matches the given convention
109 if conventional_name_impl.matches_conventional_name(struct_name.as_str()) {
110 return;
111 }
112
113 // Convert the `DefId` of the structure to a `LocalDefId`. If it cannot be converted
114 // then the struct is from an external crate, in which case this lint should not be
115 // emitted. (The user cannot easily rename that struct if they didn't define it.)
116 let Some(struct_local_def_id) = struct_def_id.as_local() else {
117 return;
118 };
119
120 // Convert struct `LocalDefId` to an `HirId` so that we can emit the lint for the
121 // correct HIR node.
122 let struct_hir_id: HirId = OwnerId {
123 def_id: struct_local_def_id,
124 }
125 .into();
126
127 span_lint_hir_and_then(
128 cx,
129 UNCONVENTIONAL_NAMING,
130 struct_hir_id,
131 struct_span,
132 conventional_name_impl.lint_description(),
133 |diag| {
134 diag.note(format!(
135 "structures that implement `{}` should end in \"{}\"",
136 conventional_name_impl.name(),
137 conventional_name_impl.suffix()
138 ));
139
140 diag.span_suggestion(
141 struct_span,
142 format!("rename `{}`", struct_name.as_str()),
143 conventional_name_impl.name_suggestion(struct_name.as_str()),
144 Applicability::MaybeIncorrect,
145 );
146
147 diag.span_note(
148 item.span,
149 format!("`{}` implemented here", conventional_name_impl.name()),
150 );
151 },
152 );
153 }
154 }
155}
156
157/// Collections of bevy traits where types that implement this trait should follow a specific naming
158/// convention
159enum TraitConvention {
160 SystemSet,
161 Plugin,
162}
163
164impl TraitConvention {
165 /// check if this `impl` block implements a Bevy trait that should follow a naming pattern
166 fn try_from_impl(cx: &LateContext, impl_: &Impl) -> Option<Self> {
167 if impls_trait(cx, impl_, &crate::paths::SYSTEM_SET) {
168 Some(TraitConvention::SystemSet)
169 } else if impls_trait(cx, impl_, &crate::paths::PLUGIN) {
170 Some(TraitConvention::Plugin)
171 } else {
172 None
173 }
174 }
175
176 fn name(&self) -> &'static str {
177 match self {
178 TraitConvention::SystemSet => "SystemSet",
179 TraitConvention::Plugin => "Plugin",
180 }
181 }
182
183 /// Returns the suffix that should be used when implementing this trait
184 fn suffix(&self) -> &'static str {
185 match self {
186 TraitConvention::SystemSet => "Systems",
187 TraitConvention::Plugin => "Plugin",
188 }
189 }
190
191 fn lint_description(&self) -> String {
192 format!("unconventional type name for a `{}`", self.name())
193 }
194
195 /// Test if the Structure name matches the naming convention
196 fn matches_conventional_name(&self, struct_name: &str) -> bool {
197 struct_name.ends_with(self.suffix())
198 }
199
200 /// Suggest a name for the Structure that matches the naming pattern
201 fn name_suggestion(&self, struct_name: &str) -> String {
202 match self {
203 TraitConvention::SystemSet => {
204 // There are several competing naming standards. These are a few that we specially
205 // check for.
206 const INCORRECT_SUFFIXES: [&str; 3] = ["System", "Set", "Steps"];
207
208 // If the name ends in one of the other suffixes, strip it out and replace it with
209 // "Systems". If a struct was originally named `FooSet`, this suggests `FooSystems`
210 // instead of `FooSetSystems`.
211 for incorrect_suffix in INCORRECT_SUFFIXES {
212 if let Some(stripped_name) = struct_name.strip_suffix(incorrect_suffix) {
213 return format!("{stripped_name}{}", self.suffix());
214 }
215 }
216
217 // If none of the special cases are matched, simply append the suffix.
218 format!("{struct_name}{}", self.suffix())
219 }
220 TraitConvention::Plugin => {
221 // If the name is prefixed with "Plugin", remove it and add it to the end.
222 if let Some(stripped_name) = struct_name.strip_prefix("Plugin") {
223 return format!("{stripped_name}{}", self.suffix());
224 }
225
226 // If "Plugins" is plural instead of singular, remove the "s" to make it singular.
227 if let Some(stripped_name) = struct_name.strip_suffix("Plugins") {
228 return format!("{stripped_name}{}", self.suffix());
229 }
230
231 // If none of the special cases are matched, simply append the suffix.
232 format!("{struct_name}{}", self.suffix())
233 }
234 }
235 }
236}