bevy_lint/lints/nursery/
zst_query.rs

1//! Checks for queries that query the data for a zero-sized type.
2//!
3//! # Motivation
4//!
5//! Zero-sized types (ZSTs) are types that have no size because they contain no runtime data. Any
6//! information they may hold is known at compile-time in the form of [constant generics], which do
7//! not need to be queried. As such, ZSTs are better used as query filters instead of query data.
8//!
9//! [constant generics]: https://doc.rust-lang.org/reference/items/generics.html#const-generics
10//!
11//! # Known Issues
12//!
13//! This lint raises false positives on queries like `Has<T>` and `AnyOf<T>` because they are ZSTs,
14//! even though they still retrieve data from the ECS. Please see [#279] for more information.
15//!
16//! [#279]: https://github.com/TheBevyFlock/bevy_cli/issues/279
17//!
18//! # Example
19//!
20//! ```
21//! # use bevy::prelude::*;
22//! #
23//! // This is a zero-sized type, sometimes known as a "marker component".
24//! #[derive(Component)]
25//! struct Player;
26//!
27//! fn move_player(mut query: Query<(&mut Transform, &Player)>) {
28//!     for (transform, _) in query.iter_mut() {
29//!         // ...
30//!     }
31//! }
32//! #
33//! # assert_eq!(std::mem::size_of::<Player>(), 0);
34//! ```
35//!
36//! Use instead:
37//!
38//! ```
39//! # use bevy::prelude::*;
40//! #
41//! #[derive(Component)]
42//! struct Player;
43//!
44//! fn move_player(mut query: Query<&mut Transform, With<Player>>) {
45//!     for transform in query.iter_mut() {
46//!         // ...
47//!     }
48//! }
49//! #
50//! # assert_eq!(std::mem::size_of::<Player>(), 0);
51//! ```
52
53use clippy_utils::{diagnostics::span_lint_and_help, ty::ty_from_hir_ty};
54use rustc_abi::Size;
55use rustc_hir::AmbigArg;
56use rustc_lint::{LateContext, LateLintPass};
57use rustc_middle::ty::{
58    Ty,
59    layout::{LayoutOf, TyAndLayout},
60};
61
62use crate::{
63    declare_bevy_lint, declare_bevy_lint_pass,
64    utils::hir_parse::{detuple, generic_type_at},
65};
66
67declare_bevy_lint! {
68    pub(crate) ZST_QUERY,
69    // This will eventually be a `RESTRICTION` lint, but due to
70    // <https://github.com/TheBevyFlock/bevy_cli/issues/279> it is not yet ready for production.
71    super::Nursery,
72    "queried a zero-sized type",
73}
74
75declare_bevy_lint_pass! {
76    pub(crate) ZstQuery => [ZST_QUERY],
77}
78
79impl<'tcx> LateLintPass<'tcx> for ZstQuery {
80    fn check_ty(&mut self, cx: &LateContext<'tcx>, hir_ty: &'tcx rustc_hir::Ty<'tcx, AmbigArg>) {
81        if hir_ty.span.in_external_macro(cx.tcx.sess.source_map()) {
82            return;
83        }
84        let ty = ty_from_hir_ty(cx, hir_ty.as_unambig_ty());
85
86        let Some(query_kind) = QueryKind::try_from_ty(cx, ty) else {
87            return;
88        };
89
90        let Some(query_data_ty) = generic_type_at(cx, hir_ty.as_unambig_ty(), 2) else {
91            return;
92        };
93
94        for hir_ty in detuple(*query_data_ty) {
95            let ty = ty_from_hir_ty(cx, &hir_ty);
96
97            // We want to make sure we're evaluating `Foo` and not `&Foo`/`&mut Foo`
98            let peeled = ty.peel_refs();
99
100            if !is_zero_sized(cx, peeled).unwrap_or_default() {
101                continue;
102            }
103
104            // TODO: We can also special case `Option<&Foo>`/`Option<&mut Foo>` to
105            //       instead suggest `Has<Foo>`
106            span_lint_and_help(
107                cx,
108                ZST_QUERY,
109                hir_ty.span,
110                ZST_QUERY.desc,
111                None,
112                query_kind.help(peeled),
113            );
114        }
115    }
116}
117
118enum QueryKind {
119    Query,
120}
121
122impl QueryKind {
123    fn try_from_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Self> {
124        if crate::paths::QUERY.matches_ty(cx, ty) {
125            Some(Self::Query)
126        } else {
127            None
128        }
129    }
130
131    fn help(&self, ty: Ty<'_>) -> String {
132        // It should be noted that `With<Foo>` is not always the best filter to suggest.
133        // While it's most often going to be what users want, there's also `Added<Foo>`
134        // and `Changed<Foo>` which might be more appropriate in some cases
135        // (i.e. users are calling `foo.is_added()` or `foo.is_changed()` in the body of
136        // the system).
137        // In the future, we might want to span the usage site of `is_added()`/`is_changed()`
138        // and suggest to use `Added<Foo>`/`Changed<Foo>` instead.
139        match self {
140            Self::Query => format!(
141                // NOTE: This isn't actually true, please see #279 for more info and how this will
142                // be fixed!
143                "zero-sized types do not retrieve any data, consider using a filter instead: `With<{ty}>`"
144            ),
145        }
146    }
147}
148
149/// Checks if a type is zero-sized.
150///
151/// Returns:
152/// - `Some(true)` if the type is most likely a ZST
153/// - `Some(false)` if the type is most likely not a ZST
154/// - `None` if we cannot determine the size (e.g., type is not normalizable)
155fn is_zero_sized<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<bool> {
156    // Note: we don't use `approx_ty_size` from `clippy_utils` here
157    // because it will return `0` as the default value if the type is not
158    // normalizable, which will put us at risk of emitting more false positives.
159    if let Ok(TyAndLayout { layout, .. }) = cx.layout_of(ty) {
160        Some(layout.size() == Size::ZERO)
161    } else {
162        None
163    }
164}