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}