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::{Region, Ty, TyCtxt, TyKind, TypeVisitable, TypeVisitor};
110use rustc_span::{Span, def_id::LocalDefId, kw};
111
112use crate::{declare_bevy_lint, declare_bevy_lint_pass};
113
114declare_bevy_lint! {
115    pub(crate) BORROWED_REBORROWABLE,
116    super::Pedantic,
117    "function parameter takes a mutable reference to a re-borrowable type",
118}
119
120declare_bevy_lint_pass! {
121    pub(crate) BorrowedReborrowable => [BORROWED_REBORROWABLE],
122}
123
124impl<'tcx> LateLintPass<'tcx> for BorrowedReborrowable {
125    fn check_fn(
126        &mut self,
127        cx: &LateContext<'tcx>,
128        kind: FnKind<'tcx>,
129        decl: &'tcx FnDecl<'tcx>,
130        body: &'tcx Body<'tcx>,
131        fn_span: Span,
132        def_id: LocalDefId,
133    ) {
134        // If the function originates from an external macro, skip this lint
135        if fn_span.in_external_macro(cx.tcx.sess.source_map()) {
136            return;
137        }
138
139        let fn_sig = match kind {
140            FnKind::Closure => cx.tcx.closure_user_provided_sig(def_id).value,
141            // We use `instantiate_identity` to discharge the binder since we don't
142            // mind using placeholders for any bound arguments
143            _ => cx.tcx.fn_sig(def_id).instantiate_identity(),
144        };
145
146        // A list of argument types, used in the actual lint check.
147        let arg_types = fn_sig.inputs().skip_binder();
148        // A list of argument parameters, used to find the span of arguments.
149        let arg_params = body.params;
150
151        debug_assert_eq!(
152            arg_types.len(),
153            arg_params.len(),
154            "there must be the same number of argument types and parameters"
155        );
156
157        for (arg_index, arg_ty) in arg_types.iter().enumerate() {
158            let TyKind::Ref(region, ty, Mutability::Mut) = arg_ty.kind() else {
159                // We only care about `&mut` parameters
160                continue;
161            };
162
163            // If the argument is named `self`, skip it. Without this check the lint would be
164            // emitted for `&mut self` if `self` was reborrowable, which isn't wanted! That would
165            // just be annoying for engine developers trying to add useful methods to reborrowable
166            // types.
167            if let PatKind::Binding(_, _, ident, _) = body.params[arg_index].pat.kind
168                && ident.name == kw::SelfLower
169            {
170                continue;
171            }
172
173            let Some(reborrowable) = Reborrowable::try_from_ty(cx, *ty) else {
174                // The type is not one of our known re-borrowable types
175                continue;
176            };
177
178            let is_output_bound_to_arg = fn_sig
179                .output()
180                .visit_with(&mut ContainsRegion(*region))
181                .is_break();
182
183            if is_output_bound_to_arg {
184                // We don't want to suggest re-borrowing if the return type's
185                // lifetime is bound to the argument's reference.
186                // This is because it's impossible to convert something like:
187                // `for<'a> (&'a mut Commands<'_, '_>) -> EntityCommands<'a>`
188                // to something like:
189                // `for<'a> (Commands<'_, '_>) -> EntityCommands<'a>`
190                // without getting: `error[E0515]: cannot return value referencing function
191                // parameter `commands` ``
192                continue;
193            }
194
195            // This tries to get the user-written form of `T` given the HIR representation for `&T`
196            // / `&mut T`. If we cannot for whatever reason, we fallback to using
197            // `Ty::to_string()` to get the fully-qualified form of `T`.
198            //
199            // For example, given a function signature like `fn(&mut Commands)`, we try to get the
200            // snippet for just `Commands` but default to `bevy::prelude::Commands<'_, '_>` if we
201            // cannot.
202            let ty_snippet = match decl.inputs[arg_index].kind {
203                // The `Ty` should be a `Ref`, since we proved that above.
204                rustc_hir::TyKind::Ref(_, MutTy { ty: inner_ty, .. }) => {
205                    // Get the snippet for the inner type.
206                    snippet_opt(cx, inner_ty.span)
207                }
208                // If it's not a `Ref` for whatever reason, fallback to our default value.
209                _ => None,
210            }
211            // We previously peeled the `&mut` reference, so `ty` is just the underlying `T`.
212            .unwrap_or_else(|| ty.to_string());
213
214            span_lint_and_sugg(
215                cx,
216                BORROWED_REBORROWABLE,
217                // The span contains both the argument name and type.
218                arg_params[arg_index].span,
219                reborrowable.message(),
220                reborrowable.help(),
221                reborrowable.suggest(cx, arg_params[arg_index].pat.span, ty_snippet),
222                // Not machine-applicable since the function body may need to
223                // also be updated to account for the removed ref
224                Applicability::MaybeIncorrect,
225            );
226        }
227    }
228}
229
230#[derive(Debug, Copy, Clone)]
231enum Reborrowable {
232    Commands,
233    Deferred,
234    DeferredWorld,
235    EntityCommands,
236    EntityMut,
237    FilteredEntityMut,
238    Mut,
239    MutUntyped,
240    NonSendMut,
241    PtrMut,
242    Query,
243    ResMut,
244}
245
246impl Reborrowable {
247    fn try_from_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Self> {
248        use crate::paths::*;
249
250        static PATH_MAP: &[(&PathLookup, Reborrowable)] = &[
251            (&COMMANDS, Reborrowable::Commands),
252            (&DEFERRED, Reborrowable::Deferred),
253            (&DEFERRED_WORLD, Reborrowable::DeferredWorld),
254            (&ENTITY_COMMANDS, Reborrowable::EntityCommands),
255            (&ENTITY_MUT, Reborrowable::EntityMut),
256            (&FILTERED_ENTITY_MUT, Reborrowable::FilteredEntityMut),
257            (&MUT, Reborrowable::Mut),
258            (&MUT_UNTYPED, Reborrowable::MutUntyped),
259            (&NON_SEND_MUT, Reborrowable::NonSendMut),
260            (&PTR_MUT, Reborrowable::PtrMut),
261            (&QUERY, Reborrowable::Query),
262            (&RES_MUT, Reborrowable::ResMut),
263        ];
264
265        for &(path, reborrowable) in PATH_MAP {
266            if path.matches_ty(cx, ty) {
267                return Some(reborrowable);
268            }
269        }
270
271        None
272    }
273
274    fn message(&self) -> String {
275        let name = self.name();
276        format!("parameter takes `&mut {name}` instead of a re-borrowed `{name}`",)
277    }
278
279    fn name(&self) -> &'static str {
280        match self {
281            Self::Commands => "Commands",
282            Self::Deferred => "Deferred",
283            Self::DeferredWorld => "DeferredWorld",
284            Self::EntityCommands => "EntityCommands",
285            Self::EntityMut => "EntityMut",
286            Self::FilteredEntityMut => "FilteredEntityMut",
287            Self::Mut => "Mut",
288            Self::MutUntyped => "MutUntyped",
289            Self::NonSendMut => "NonSendMut",
290            Self::PtrMut => "PtrMut",
291            Self::Query => "Query",
292            Self::ResMut => "ResMut",
293        }
294    }
295
296    fn help(&self) -> String {
297        let name = self.name();
298        format!("use `{name}` instead")
299    }
300
301    fn suggest(&self, cx: &LateContext, name: Span, ty: String) -> String {
302        let name = snippet(cx, name, "_");
303        format!("mut {name}: {ty}")
304    }
305}
306
307/// [`TypeVisitor`] for checking if the given region is contained in the type.
308struct ContainsRegion<'tcx>(pub Region<'tcx>);
309
310impl<'tcx> TypeVisitor<TyCtxt<'tcx>> for ContainsRegion<'tcx> {
311    type Result = ControlFlow<()>;
312
313    fn visit_region(&mut self, r: Region<'tcx>) -> Self::Result {
314        if self.0 == r {
315            ControlFlow::Break(())
316        } else {
317            ControlFlow::Continue(())
318        }
319    }
320}