bevy_lint/lints/suspicious/unit_in_bundle.rs
1//! Checks for `Bundle`s that contain the unit [`()`](unit) as a component.
2//!
3//! Specifically, this lint checks for when you pass a `Bundle` to a function or method, such as
4//! `Commands::spawn()`. If the bundle contains a unit, the lint will emit a warning.
5//!
6//! # Motivation
7//!
8//! It is possible to create bundles with a unit `()` component, since unit implements `Bundle`.
9//! Unit is not a `Component`, however, and will be ignored instead of added to the entity. Often,
10//! inserting a unit is unintentional and is a sign that the author intended to do something else.
11//!
12//! # Example
13//!
14//! ```
15//! # use bevy::prelude::*;
16//! # use std::f32::consts::PI;
17//! #
18//! fn spawn(mut commands: Commands) {
19//! commands.spawn(());
20//!
21//! commands.spawn((
22//! Name::new("Decal"),
23//! // This is likely a mistake! `Transform::rotate_z()` returns a unit `()`, not a
24//! // `Transform`! As such, no `Transform` will be inserted into the entity.
25//! Transform::from_translation(Vec3::new(0.75, 0.0, 0.0))
26//! .rotate_z(PI / 4.0),
27//! ));
28//! }
29//! #
30//! # bevy::ecs::system::assert_is_system(spawn);
31//! ```
32//!
33//! Use instead:
34//!
35//! ```
36//! # use bevy::prelude::*;
37//! # use std::f32::consts::PI;
38//! #
39//! fn spawn(mut commands: Commands) {
40//! // `Commands::spawn_empty()` is preferred if you do not need to add any components.
41//! commands.spawn_empty();
42//!
43//! commands.spawn((
44//! Name::new("Decal"),
45//! // `Transform::with_rotation()` returns a `Transform`, which was the intended behavior.
46//! Transform::from_translation(Vec3::new(0.75, 0.0, 0.0))
47//! .with_rotation(Quat::from_rotation_z(PI / 4.0)),
48//! ));
49//! }
50//! #
51//! # bevy::ecs::system::assert_is_system(spawn);
52//! ```
53
54use clippy_utils::{diagnostics::span_lint_hir_and_then, fn_def_id, paths::PathLookup};
55use rustc_errors::Applicability;
56use rustc_hir::{Expr, ExprKind, PathSegment, def_id::DefId};
57use rustc_lint::{LateContext, LateLintPass};
58use rustc_middle::ty::{self, Ty};
59use rustc_span::Symbol;
60#[allow(
61 rustc::direct_use_of_rustc_type_ir,
62 reason = "needed to correctly find `Bundle` trait bounds"
63)]
64use rustc_type_ir::PredicatePolarity;
65
66use crate::{
67 declare_bevy_lint, declare_bevy_lint_pass, paths, span_assert, span_assert_eq, sym,
68 utils::method_call::MethodCall,
69};
70
71declare_bevy_lint! {
72 pub(crate) UNIT_IN_BUNDLE,
73 super::Suspicious,
74 "created a `Bundle` containing a unit `()`",
75}
76
77declare_bevy_lint_pass! {
78 pub(crate) UnitInBundle => [UNIT_IN_BUNDLE],
79}
80
81impl<'tcx> LateLintPass<'tcx> for UnitInBundle {
82 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
83 if expr.span.in_external_macro(cx.tcx.sess.source_map()) {
84 return;
85 }
86
87 let (fn_id, fn_args, fn_arg_types) = if let Some(MethodCall {
88 method_path,
89 receiver,
90 args,
91 span,
92 ..
93 }) = MethodCall::try_from(cx, expr)
94 {
95 // There are a few methods named `spawn()` that can be substituted for `spawn_empty()`.
96 // This checks for those special cases and emits a machine-applicable suggestion when
97 // possible.
98 if let Some(bundle_expr) = can_be_spawn_empty(cx, method_path, receiver, args) {
99 span_lint_hir_and_then(
100 cx,
101 UNIT_IN_BUNDLE,
102 bundle_expr.hir_id,
103 bundle_expr.span,
104 UNIT_IN_BUNDLE.desc,
105 |diag| {
106 diag.note("units `()` are not `Component`s and will be skipped")
107 .span_suggestion(
108 span,
109 "`spawn_empty()` is more efficient",
110 "spawn_empty()",
111 Applicability::MachineApplicable,
112 );
113 },
114 );
115
116 return;
117 }
118
119 let Some(fn_id) = fn_def_id(cx, expr) else {
120 // This will be `None` if the function is a local closure. Since closures
121 // cannot have generic parameters, they cannot take bundles as an input, so we
122 // can skip them.
123 return;
124 };
125
126 // The first argument is `&self` because it's a method. We skip it because `&self`
127 // won't be in `args`, making the two slices two different lengths.
128 let fn_arg_types = &fn_arg_types(cx, fn_id)[1..];
129
130 (fn_id, args, fn_arg_types)
131 } else if let ExprKind::Call(_, fn_args) = expr.kind {
132 let Some(fn_id) = fn_def_id(cx, expr) else {
133 // This will be `None` if the function is a local closure. Since closures
134 // cannot have generic parameters, they cannot take bundles as an input, so we
135 // can skip them.
136 return;
137 };
138
139 let fn_arg_types = fn_arg_types(cx, fn_id);
140
141 (fn_id, fn_args, fn_arg_types)
142 } else {
143 return;
144 };
145
146 span_assert_eq!(expr.span, fn_args.len(), fn_arg_types.len());
147
148 let typeck_results = cx.typeck_results();
149
150 for bundle_expr in filter_bundle_args(cx, fn_id, fn_args, fn_arg_types) {
151 let bundle_ty = typeck_results.expr_ty(bundle_expr);
152
153 for tuple_path in find_units_in_tuple(bundle_ty) {
154 let unit_expr = tuple_path.into_expr(bundle_expr);
155
156 span_lint_hir_and_then(
157 cx,
158 UNIT_IN_BUNDLE,
159 unit_expr.hir_id,
160 unit_expr.span,
161 UNIT_IN_BUNDLE.desc,
162 |diag| {
163 diag.note("units `()` are not `Component`s and will be skipped");
164 },
165 );
166 }
167 }
168 }
169}
170
171/// Returns the arguments of a method call that are intended to be `Bundle`s.
172///
173/// `fn_id` should be the definition of the function itself, and `args` should be the arguments
174/// passed to the function.
175fn filter_bundle_args<'tcx>(
176 cx: &LateContext<'tcx>,
177 fn_id: DefId,
178 fn_args: &'tcx [Expr<'tcx>],
179 fn_arg_types: &[Ty<'tcx>],
180) -> impl Iterator<Item = &'tcx Expr<'tcx>> {
181 let bundle_bounded_generics: Vec<Ty<'_>> = bundle_bounded_generics(cx, fn_id);
182
183 // Only yield arguments whose types are generic parameters that require the `Bundle` trait.
184 fn_arg_types
185 .iter()
186 .enumerate()
187 .filter(move |(_, arg)| bundle_bounded_generics.contains(arg))
188 .map(|(i, _)| &fn_args[i])
189}
190
191/// Returns a list of types corresponding to the inputs of a function.
192///
193/// Notably, the returned types are not instantiated. Generic parameters will be preserved and not
194/// filled in with actual types.
195///
196/// # Example
197///
198/// Running this function on the [`DefId`] of `foo()` will return `[usize, bool]`, while `bar()`
199/// will return `[T, usize]`.
200///
201/// ```
202/// # use bevy::ecs::bundle::Bundle;
203/// #
204/// fn foo(a: usize, b: bool) {}
205/// fn bar<T: Bundle>(bundle: T, size: usize) {}
206/// ```
207fn fn_arg_types<'tcx>(cx: &LateContext<'tcx>, fn_id: DefId) -> &'tcx [Ty<'tcx>] {
208 cx.tcx
209 .fn_sig(fn_id)
210 .instantiate_identity()
211 .inputs()
212 .skip_binder()
213}
214
215/// Returns a list of a generic parameters of a function that must implement `Bundle`.
216///
217/// Each returned [`Ty`] is guaranteed to be a generic parameter ([`ty::TyKind::Param`]) or a
218/// projection ([`ty::TyKind::Alias`] containing [`ty::AliasTyKind::Projection`]).
219///
220/// # Example
221///
222/// If run on the following function, this function would return `A` and `C` because they both
223/// implement `Bundle`.
224///
225/// ```
226/// # use bevy::ecs::bundle::Bundle;
227/// #
228/// fn my_function<A: Bundle, B: Clone, C: Bundle + Clone>(_: A, _: B, _: C) {
229/// // ...
230/// }
231/// ```
232///
233/// Additionally, this supports projection types as well. If run on the following function, this
234/// function would return `I::Item` because it implements `Bundle`:
235///
236/// ```
237/// # use bevy::ecs::bundle::Bundle;
238/// #
239/// fn my_function<I>(bundle: I::Item)
240/// where
241/// I: IntoIterator,
242/// I::Item: Bundle,
243/// {
244/// // ...
245/// }
246/// ```
247fn bundle_bounded_generics<'tcx>(cx: &LateContext<'tcx>, fn_id: DefId) -> Vec<Ty<'tcx>> {
248 let mut bundle_bounded_generics = Vec::new();
249
250 // Fetch the parameter environment for the function, which contains all generic trait bounds.
251 // (Such as the `T: Bundle` that we're looking for!) See
252 // <https://rustc-dev-guide.rust-lang.org/typing_parameter_envs.html> for more information.
253 let param_env = cx.tcx.param_env(fn_id);
254
255 for clause in param_env.caller_bounds() {
256 // We only want trait predicates, filtering out lifetimes and constants.
257 if let Some(trait_predicate) = clause.as_trait_clause()
258 // The `Bundle` trait doesn't require any bound vars, so we dispel the binder.
259 && let Some(trait_predicate) = trait_predicate.no_bound_vars()
260 && let ty::TraitPredicate {
261 trait_ref,
262 // Negative trait bounds, which are unstable, allow matching all types _except_
263 // those with a specific trait. We don't want that, however, so we only match
264 // positive trait bounds.
265 polarity: PredicatePolarity::Positive,
266 } = trait_predicate
267 // Only match `T: Bundle` predicates.
268 && paths::BUNDLE.matches(cx, trait_ref.def_id)
269 {
270 let self_ty = trait_ref.self_ty();
271
272 span_assert!(
273 cx.tcx.def_span(fn_id),
274 matches!(
275 self_ty.kind(),
276 // It must either be a generic parameter `B`, or a projection
277 // `B::AssociatedType`.
278 ty::TyKind::Param(_)
279 | ty::TyKind::Alias(ty::AliasTy {
280 kind: ty::AliasTyKind::Projection { .. },
281 ..
282 })
283 ),
284 "type {self_ty} from trait bound {trait_ref} was expected to be a type parameter, but instead was a {self_ty_kind:?}",
285 self_ty_kind = self_ty.kind(),
286 );
287
288 // At this point, we've confirmed the predicate is `T: Bundle`! Add it to the list to
289 // be returned. :)
290 bundle_bounded_generics.push(trait_ref.self_ty());
291 }
292 }
293
294 bundle_bounded_generics
295}
296
297/// Represents the path to an item within a nested tuple.
298///
299/// # Example
300///
301/// Each number within the [`TuplePath`] represents an index into the tuple. An empty path
302/// represents the root tuple, while a path of `TuplePath([0])` represents the first item within
303/// that tuple.
304///
305/// ```ignore
306/// // TuplePath([])
307/// (
308/// // TuplePath([0])
309/// Name::new("Foo"),
310/// // TuplePath([1])
311/// (
312/// // TuplePath([1, 0])
313/// (),
314/// // TuplePath([1, 1])
315/// Transform::default(),
316/// // TuplePath([1, 2])
317/// Visibility::Hidden,
318/// ),
319/// // TuplePath([2])
320/// (),
321/// )
322/// ```
323#[derive(Clone, Debug)]
324#[repr(transparent)]
325struct TuplePath(Vec<usize>);
326
327impl TuplePath {
328 /// Creates an empty [`TuplePath`].
329 fn new() -> Self {
330 Self(Vec::new())
331 }
332
333 /// Pushes an index to the end of the path.
334 fn push(&mut self, i: usize) {
335 self.0.push(i);
336 }
337
338 /// Pops the last index in the path.
339 fn pop(&mut self) -> Option<usize> {
340 self.0.pop()
341 }
342
343 /// Finds the [`Expr`] of the item represented by this path given the root tuple.
344 ///
345 /// In the event the path is invalid in some way (such as if an expected tuple is not found),
346 /// this will return the expression closest to the target.
347 fn into_expr<'tcx>(self, root_tuple: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
348 let mut tuple = root_tuple;
349
350 for i in self.0 {
351 let ExprKind::Tup(items) = tuple.kind else {
352 // If the path is invalid in some way, return the expression nearest to the target.
353 // This is usually the case when the bundle is created outside of
354 // `Commands::spawn()`, such as with `commands.spawn(my_helper())` instead of the
355 // expected `commands.spawn((Foo, Bar, ()))`.
356 return tuple;
357 };
358
359 tuple = &items[i];
360 }
361
362 tuple
363 }
364}
365
366/// Returns the [`TuplePath`]s to all unit types within a tuple type.
367///
368/// # Example
369///
370/// Given a type:
371///
372/// ```ignore
373/// type MyBundle = (
374/// Name,
375/// (
376/// (),
377/// Transform,
378/// Visibility,
379/// ),
380/// (),
381/// );
382/// ```
383///
384/// This function would return:
385///
386/// ```ignore
387/// [
388/// TuplePath([1, 0]),
389/// TuplePath([2]),
390/// ]
391/// ```
392///
393/// See [`TuplePath`]'s documentation for more information.
394fn find_units_in_tuple(ty: Ty<'_>) -> Vec<TuplePath> {
395 fn inner(ty: Ty<'_>, current_path: &mut TuplePath, unit_paths: &mut Vec<TuplePath>) {
396 if let ty::TyKind::Tuple(types) = ty.kind() {
397 if types.is_empty() {
398 unit_paths.push(current_path.clone());
399 return;
400 }
401
402 for (i, ty) in types.into_iter().enumerate() {
403 current_path.push(i);
404 inner(ty, current_path, unit_paths);
405 current_path.pop();
406 }
407 }
408 }
409
410 let mut current_path = TuplePath::new();
411 let mut unit_paths = Vec::new();
412
413 inner(ty, &mut current_path, &mut unit_paths);
414
415 unit_paths
416}
417
418/// Returns [`Some`] if the method can be replaced with `spawn_empty()`.
419///
420/// The returned [`Expr`] is that of the unit `()` in the bundle argument.
421fn can_be_spawn_empty<'tcx>(
422 cx: &LateContext<'tcx>,
423 method_path: &'tcx PathSegment<'tcx>,
424 receiver: &'tcx Expr<'tcx>,
425 args: &'tcx [Expr<'tcx>],
426) -> Option<&'tcx Expr<'tcx>> {
427 // A list of all methods that can be replaced with `spawn_empty()`. The format is `(receiver
428 // type, method name, bundle arg index)`.
429 static CAN_SPAWN_EMPTY: &[(&PathLookup, Symbol, usize)] = &[
430 (&paths::COMMANDS, sym::spawn, 0),
431 (&paths::WORLD, sym::spawn, 0),
432 (&paths::RELATED_SPAWNER, sym::spawn, 0),
433 (&paths::RELATED_SPAWNER_COMMANDS, sym::spawn, 0),
434 ];
435
436 let typeck_results = cx.typeck_results();
437
438 // Find the adjusted receiver type (e.g. `World` from `Box<World>`), removing any references to
439 // find the underlying type.
440 let receiver_ty = typeck_results.expr_ty_adjusted(receiver).peel_refs();
441
442 for (path, method, index) in CAN_SPAWN_EMPTY {
443 if path.matches_ty(cx, receiver_ty)
444 && method_path.ident.name == *method
445 && let Some(bundle_expr) = args.get(*index)
446 && typeck_results.expr_ty(bundle_expr).is_unit()
447 {
448 return Some(bundle_expr);
449 }
450 }
451
452 None
453}