bevy_lint/lints/pedantic/
borrowed_reborrowable.rs

1//! Checks for function parameters that take a mutable reference to a re-borrowable type.
2//!
3//! This lint checks for the following re-borrowable types:
4//!
5//! - `Commands`
6//! - `Deferred`
7//! - `DeferredWorld`
8//! - `EntityCommands`
9//! - `EntityMut`
10//! - `FilteredEntityMut`
11//! - `Mut`
12//! - `MutUntyped`
13//! - `NonSendMut`
14//! - `PtrMut`
15//! - `Query`
16//! - `ResMut`
17//!
18//! Though a type may be re-borrowable, there are circumstances where it cannot be easily
19//! reborrowed. (Please see the [Examples](#example).) In these cases, no warning will be emitted.
20//!
21//! # Motivation
22//!
23//! Several Bevy types look like they are owned, when in reality they contain an `&mut` reference
24//! to the data owned by the ECS. `Commands` and `Query` are examples of such types that _pretend_
25//! to own data for better user ergonomics.
26//!
27//! This can be an issue when a user writes a function that takes a mutable reference to one of
28//! these types, not realizing that it itself is _already_ a reference. These mutable references
29//! can almost always be readily converted back to an owned instance of the type, which is a cheap
30//! operation that avoids nested references.
31//!
32//! # Known Issues
33//!
34//! This lint does not currently support the [`Fn`] traits or function pointers. This means the
35//! following types will not be caught by the lint:
36//!
37//! - `impl FnOnce(&mut Commands)`
38//! - `Box<dyn FnMut(&mut Commands)>`
39//! - `fn(&mut Commands)`
40//!
41//! For more information, please see [#174].
42//!
43//! [#174]: https://github.com/TheBevyFlock/bevy_cli/issues/174
44//!
45//! # Example
46//!
47//! ```
48//! # use bevy::prelude::*;
49//! #
50//! fn system(mut commands: Commands) {
51//!     helper_function(&mut commands);
52//! }
53//!
54//! // This takes `&mut Commands`, but it doesn't need to!
55//! fn helper_function(commands: &mut Commands) {
56//!     // ...
57//! }
58//! #
59//! # bevy::ecs::system::assert_is_system(system);
60//! ```
61//!
62//! Use instead:
63//!
64//! ```
65//! # use bevy::prelude::*;
66//! #
67//! fn system(mut commands: Commands) {
68//!     // Convert `&mut Commands` to `Commands`.
69//!     helper_function(commands.reborrow());
70//! }
71//!
72//! fn helper_function(mut commands: Commands) {
73//!     // ...
74//! }
75//! #
76//! # bevy::ecs::system::assert_is_system(system);
77//! ```
78//!
79//! A type cannot be easily reborrowed when a function returns a reference with the same lifetime
80//! as the borrowed type. The lint knows about this case, however, and will not emit any warning if
81//! it knows the type cannot be re-borrowed:
82//!
83//! ```
84//! # use bevy::{prelude::*, ecs::system::EntityCommands};
85//! #
86//! fn system(mut commands: Commands) {
87//!     let entity_commands = helper_function(&mut commands);
88//!     // ...
89//! }
90//!
91//! // Note how this function returns a reference with the same lifetime as `Commands`.
92//! fn helper_function<'a>(commands: &'a mut Commands) -> EntityCommands<'a> {
93//!     commands.spawn_empty()
94//! }
95//! #
96//! # bevy::ecs::system::assert_is_system(system);
97//! ```
98
99use std::ops::ControlFlow;
100
101use clippy_utils::{
102    diagnostics::span_lint_and_sugg,
103    paths::PathLookup,
104    source::{snippet, snippet_opt},
105};
106use rustc_errors::Applicability;
107use rustc_hir::{Body, FnDecl, MutTy, Mutability, PatKind, intravisit::FnKind};
108use rustc_lint::{LateContext, LateLintPass};
109use rustc_middle::ty::{Ty, TyKind, TypeVisitable, TypeVisitor};
110use rustc_span::{Span, def_id::LocalDefId, kw};
111use rustc_type_ir::Interner;
112
113use crate::{declare_bevy_lint, declare_bevy_lint_pass};
114
115declare_bevy_lint! {
116    pub(crate) BORROWED_REBORROWABLE,
117    super::Pedantic,
118    "function parameter takes a mutable reference to a re-borrowable type",
119}
120
121declare_bevy_lint_pass! {
122    pub(crate) BorrowedReborrowable => [BORROWED_REBORROWABLE],
123}
124
125impl<'tcx> LateLintPass<'tcx> for BorrowedReborrowable {
126    fn check_fn(
127        &mut self,
128        cx: &LateContext<'tcx>,
129        kind: FnKind<'tcx>,
130        decl: &'tcx FnDecl<'tcx>,
131        body: &'tcx Body<'tcx>,
132        fn_span: Span,
133        def_id: LocalDefId,
134    ) {
135        // If the function originates from an external macro, skip this lint
136        if fn_span.in_external_macro(cx.tcx.sess.source_map()) {
137            return;
138        }
139
140        let fn_sig = match kind {
141            FnKind::Closure => cx.tcx.closure_user_provided_sig(def_id).value,
142            // We use `instantiate_identity` to discharge the binder since we don't
143            // mind using placeholders for any bound arguments
144            _ => cx.tcx.fn_sig(def_id).instantiate_identity(),
145        };
146
147        // A list of argument types, used in the actual lint check.
148        let arg_types = fn_sig.inputs().skip_binder();
149        // A list of argument parameters, used to find the span of arguments.
150        let arg_params = body.params;
151
152        debug_assert_eq!(
153            arg_types.len(),
154            arg_params.len(),
155            "there must be the same number of argument types and parameters"
156        );
157
158        for (arg_index, arg_ty) in arg_types.iter().enumerate() {
159            let TyKind::Ref(region, ty, Mutability::Mut) = arg_ty.kind() else {
160                // We only care about `&mut` parameters
161                continue;
162            };
163
164            // If the argument is named `self`, skip it. Without this check the lint would be
165            // emitted for `&mut self` if `self` was reborrowable, which isn't wanted! That would
166            // just be annoying for engine developers trying to add useful methods to reborrowable
167            // types.
168            if let PatKind::Binding(_, _, ident, _) = body.params[arg_index].pat.kind
169                && ident.name == kw::SelfLower
170            {
171                continue;
172            }
173
174            let Some(reborrowable) = Reborrowable::try_from_ty(cx, *ty) else {
175                // The type is not one of our known re-borrowable types
176                continue;
177            };
178
179            let is_output_bound_to_arg = fn_sig
180                .output()
181                .visit_with(&mut ContainsRegion(*region))
182                .is_break();
183
184            if is_output_bound_to_arg {
185                // We don't want to suggest re-borrowing if the return type's
186                // lifetime is bound to the argument's reference.
187                // This is because it's impossible to convert something like:
188                // `for<'a> (&'a mut Commands<'_, '_>) -> EntityCommands<'a>`
189                // to something like:
190                // `for<'a> (Commands<'_, '_>) -> EntityCommands<'a>`
191                // without getting: `error[E0515]: cannot return value referencing function
192                // parameter `commands` ``
193                continue;
194            }
195
196            // This tries to get the user-written form of `T` given the HIR representation for `&T`
197            // / `&mut T`. If we cannot for whatever reason, we fallback to using
198            // `Ty::to_string()` to get the fully-qualified form of `T`.
199            //
200            // For example, given a function signature like `fn(&mut Commands)`, we try to get the
201            // snippet for just `Commands` but default to `bevy::prelude::Commands<'_, '_>` if we
202            // cannot.
203            let ty_snippet = match decl.inputs[arg_index].kind {
204                // The `Ty` should be a `Ref`, since we proved that above.
205                rustc_hir::TyKind::Ref(_, MutTy { ty: inner_ty, .. }) => {
206                    // Get the snippet for the inner type.
207                    snippet_opt(cx, inner_ty.span)
208                }
209                // If it's not a `Ref` for whatever reason, fallback to our default value.
210                _ => None,
211            }
212            // We previously peeled the `&mut` reference, so `ty` is just the underlying `T`.
213            .unwrap_or_else(|| ty.to_string());
214
215            span_lint_and_sugg(
216                cx,
217                BORROWED_REBORROWABLE,
218                // The span contains both the argument name and type.
219                arg_params[arg_index].span,
220                reborrowable.message(),
221                reborrowable.help(),
222                reborrowable.suggest(cx, arg_params[arg_index].pat.span, ty_snippet),
223                // Not machine-applicable since the function body may need to
224                // also be updated to account for the removed ref
225                Applicability::MaybeIncorrect,
226            );
227        }
228    }
229}
230
231#[derive(Debug, Copy, Clone)]
232enum Reborrowable {
233    Commands,
234    Deferred,
235    DeferredWorld,
236    EntityCommands,
237    EntityMut,
238    FilteredEntityMut,
239    Mut,
240    MutUntyped,
241    NonSendMut,
242    PtrMut,
243    Query,
244    ResMut,
245}
246
247impl Reborrowable {
248    fn try_from_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Self> {
249        use crate::paths::*;
250
251        static PATH_MAP: &[(&PathLookup, Reborrowable)] = &[
252            (&COMMANDS, Reborrowable::Commands),
253            (&DEFERRED, Reborrowable::Deferred),
254            (&DEFERRED_WORLD, Reborrowable::DeferredWorld),
255            (&ENTITY_COMMANDS, Reborrowable::EntityCommands),
256            (&ENTITY_MUT, Reborrowable::EntityMut),
257            (&FILTERED_ENTITY_MUT, Reborrowable::FilteredEntityMut),
258            (&MUT, Reborrowable::Mut),
259            (&MUT_UNTYPED, Reborrowable::MutUntyped),
260            (&NON_SEND_MUT, Reborrowable::NonSendMut),
261            (&PTR_MUT, Reborrowable::PtrMut),
262            (&QUERY, Reborrowable::Query),
263            (&RES_MUT, Reborrowable::ResMut),
264        ];
265
266        for &(path, reborrowable) in PATH_MAP {
267            if path.matches_ty(cx, ty) {
268                return Some(reborrowable);
269            }
270        }
271
272        None
273    }
274
275    fn message(&self) -> String {
276        let name = self.name();
277        format!("parameter takes `&mut {name}` instead of a re-borrowed `{name}`",)
278    }
279
280    fn name(&self) -> &'static str {
281        match self {
282            Self::Commands => "Commands",
283            Self::Deferred => "Deferred",
284            Self::DeferredWorld => "DeferredWorld",
285            Self::EntityCommands => "EntityCommands",
286            Self::EntityMut => "EntityMut",
287            Self::FilteredEntityMut => "FilteredEntityMut",
288            Self::Mut => "Mut",
289            Self::MutUntyped => "MutUntyped",
290            Self::NonSendMut => "NonSendMut",
291            Self::PtrMut => "PtrMut",
292            Self::Query => "Query",
293            Self::ResMut => "ResMut",
294        }
295    }
296
297    fn help(&self) -> String {
298        let name = self.name();
299        format!("use `{name}` instead")
300    }
301
302    fn suggest(&self, cx: &LateContext, name: Span, ty: String) -> String {
303        let name = snippet(cx, name, "_");
304        format!("mut {name}: {ty}")
305    }
306}
307
308/// [`TypeVisitor`] for checking if the given region is contained in the type.
309struct ContainsRegion<I: Interner>(pub I::Region);
310
311impl<I: Interner> TypeVisitor<I> for ContainsRegion<I> {
312    type Result = ControlFlow<()>;
313
314    fn visit_region(&mut self, r: I::Region) -> Self::Result {
315        if self.0 == r {
316            ControlFlow::Break(())
317        } else {
318            ControlFlow::Continue(())
319        }
320    }
321}