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