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, res::MaybeQPath};
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(ref 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 struct_def_id = match impl_.self_ty.opt_qpath() {
80                Some((qpath, hir_id)) => cx.qpath_res(qpath, hir_id).def_id(),
81                None => return,
82            };
83
84            // If this type is a generic parameter, exit. Their names, such as `T`, cannot be
85            // referenced by others.
86            if impl_
87                .generics
88                .params
89                .iter()
90                .any(|param| param.def_id.to_def_id() == struct_def_id)
91            {
92                return;
93            }
94
95            // Find the original name and span of the type.
96            let Some(Ident {
97                name: struct_name,
98                span: struct_span,
99            }) = cx.tcx.opt_item_ident(struct_def_id)
100            else {
101                return;
102            };
103
104            // skip lint if the struct was defined in an external macro
105            if struct_span.in_external_macro(cx.tcx.sess.source_map()) {
106                return;
107            }
108
109            // If the type's name matches the given convention
110            if conventional_name_impl.matches_conventional_name(struct_name.as_str()) {
111                return;
112            }
113
114            // Convert the `DefId` of the structure to a `LocalDefId`. If it cannot be converted
115            // then the struct is from an external crate, in which case this lint should not be
116            // emitted. (The user cannot easily rename that struct if they didn't define it.)
117            let Some(struct_local_def_id) = struct_def_id.as_local() else {
118                return;
119            };
120
121            // Convert struct `LocalDefId` to an `HirId` so that we can emit the lint for the
122            // correct HIR node.
123            let struct_hir_id: HirId = OwnerId {
124                def_id: struct_local_def_id,
125            }
126            .into();
127
128            span_lint_hir_and_then(
129                cx,
130                UNCONVENTIONAL_NAMING,
131                struct_hir_id,
132                struct_span,
133                conventional_name_impl.lint_description(),
134                |diag| {
135                    diag.note(format!(
136                        "structures that implement `{}` should end in \"{}\"",
137                        conventional_name_impl.name(),
138                        conventional_name_impl.suffix()
139                    ));
140
141                    diag.span_suggestion(
142                        struct_span,
143                        format!("rename `{}`", struct_name.as_str()),
144                        conventional_name_impl.name_suggestion(struct_name.as_str()),
145                        Applicability::MaybeIncorrect,
146                    );
147
148                    diag.span_note(
149                        item.span,
150                        format!("`{}` implemented here", conventional_name_impl.name()),
151                    );
152                },
153            );
154        }
155    }
156}
157
158/// Collections of bevy traits where types that implement this trait should follow a specific naming
159/// convention
160enum TraitConvention {
161    SystemSet,
162    Plugin,
163}
164
165impl TraitConvention {
166    /// check if this `impl` block implements a Bevy trait that should follow a naming pattern
167    fn try_from_impl(cx: &LateContext, impl_: &Impl) -> Option<Self> {
168        if impls_trait(cx, impl_, &crate::paths::SYSTEM_SET) {
169            Some(TraitConvention::SystemSet)
170        } else if impls_trait(cx, impl_, &crate::paths::PLUGIN) {
171            Some(TraitConvention::Plugin)
172        } else {
173            None
174        }
175    }
176
177    fn name(&self) -> &'static str {
178        match self {
179            TraitConvention::SystemSet => "SystemSet",
180            TraitConvention::Plugin => "Plugin",
181        }
182    }
183
184    /// Returns the suffix that should be used when implementing this trait
185    fn suffix(&self) -> &'static str {
186        match self {
187            TraitConvention::SystemSet => "Systems",
188            TraitConvention::Plugin => "Plugin",
189        }
190    }
191
192    fn lint_description(&self) -> String {
193        format!("unconventional type name for a `{}`", self.name())
194    }
195
196    /// Test if the Structure name matches the naming convention
197    fn matches_conventional_name(&self, struct_name: &str) -> bool {
198        struct_name.ends_with(self.suffix())
199    }
200
201    /// Suggest a name for the Structure that matches the naming pattern
202    fn name_suggestion(&self, struct_name: &str) -> String {
203        match self {
204            TraitConvention::SystemSet => {
205                // There are several competing naming standards. These are a few that we specially
206                // check for.
207                const INCORRECT_SUFFIXES: [&str; 3] = ["System", "Set", "Steps"];
208
209                // If the name ends in one of the other suffixes, strip it out and replace it with
210                // "Systems". If a struct was originally named `FooSet`, this suggests `FooSystems`
211                // instead of `FooSetSystems`.
212                for incorrect_suffix in INCORRECT_SUFFIXES {
213                    if let Some(stripped_name) = struct_name.strip_suffix(incorrect_suffix) {
214                        return format!("{stripped_name}{}", self.suffix());
215                    }
216                }
217
218                // If none of the special cases are matched, simply append the suffix.
219                format!("{struct_name}{}", self.suffix())
220            }
221            TraitConvention::Plugin => {
222                // If the name is prefixed with "Plugin", remove it and add it to the end.
223                if let Some(stripped_name) = struct_name.strip_prefix("Plugin") {
224                    return format!("{stripped_name}{}", self.suffix());
225                }
226
227                // If "Plugins" is plural instead of singular, remove the "s" to make it singular.
228                if let Some(stripped_name) = struct_name.strip_suffix("Plugins") {
229                    return format!("{stripped_name}{}", self.suffix());
230                }
231
232                // If none of the special cases are matched, simply append the suffix.
233                format!("{struct_name}{}", self.suffix())
234            }
235        }
236    }
237}