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}