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}