bevy_lint/lints/
zst_query.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Checks for queries that query for a zero-sized type.
//!
//! # Motivation
//!
//! Zero-sized types (ZSTs) are types that have no size as a result of containing no runtime data.
//! In Bevy, such types are often used as marker components and are best used as filters.
//!
//! # Example
//!
//! ```
//! # use bevy::prelude::*;
//!
//! #[derive(Component)]
//! struct Player;
//!
//! fn move_player(mut query: Query<(&mut Transform, &Player)>) {
//!     // ...
//! }
//! ```
//!
//! Use instead:
//!
//! ```
//! # use bevy::prelude::*;
//!
//! #[derive(Component)]
//! struct Player;
//!
//! fn move_player(query: Query<&mut Transform, With<Player>>) {
//!     // ...
//! }
//! ```

use crate::{
    declare_bevy_lint,
    utils::hir_parse::{detuple, generic_type_at},
};
use clippy_utils::{
    diagnostics::span_lint_and_help,
    ty::{is_normalizable, match_type},
};
use rustc_abi::Size;
use rustc_hir_analysis::collect::ItemCtxt;
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{
    layout::{LayoutOf, TyAndLayout},
    Ty,
};
use rustc_session::declare_lint_pass;

declare_bevy_lint! {
    pub ZST_QUERY,
    RESTRICTION,
    "query for a zero-sized type"
}

declare_lint_pass! {
    ZstQuery => [ZST_QUERY.lint]
}

impl<'tcx> LateLintPass<'tcx> for ZstQuery {
    fn check_ty(&mut self, cx: &LateContext<'tcx>, hir_ty: &'tcx rustc_hir::Ty<'tcx>) {
        let item_cx = ItemCtxt::new(cx.tcx, hir_ty.hir_id.owner.def_id);
        let ty = item_cx.lower_ty(hir_ty);

        let Some(query_kind) = QueryKind::try_from_ty(cx, ty) else {
            return;
        };

        let Some(query_data_ty) = generic_type_at(cx, hir_ty, 2) else {
            return;
        };

        for hir_ty in detuple(*query_data_ty) {
            let ty = item_cx.lower_ty(&hir_ty);

            // We want to make sure we're evaluating `Foo` and not `&Foo`/`&mut Foo`
            let peeled = ty.peel_refs();

            if !is_zero_sized(cx, peeled).unwrap_or_default() {
                continue;
            }

            // TODO: We can also special case `Option<&Foo>`/`Option<&mut Foo>` to
            //       instead suggest `Has<Foo>`
            span_lint_and_help(
                cx,
                ZST_QUERY.lint,
                hir_ty.span,
                ZST_QUERY.lint.desc,
                None,
                query_kind.help(&peeled),
            );
        }
    }
}

enum QueryKind {
    Query,
}

impl QueryKind {
    fn try_from_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Self> {
        if match_type(cx, ty, &crate::paths::QUERY) {
            Some(Self::Query)
        } else {
            None
        }
    }

    fn help(&self, ty: &Ty<'_>) -> String {
        // It should be noted that `With<Foo>` is not always the best filter to suggest.
        // While it's most often going to be what users want, there's also `Added<Foo>`
        // and `Changed<Foo>` which might be more appropriate in some cases
        // (i.e. users are calling `foo.is_added()` or `foo.is_changed()` in the body of
        // the system).
        // In the future, we might want to span the usage site of `is_added()`/`is_changed()`
        // and suggest to use `Added<Foo>`/`Changed<Foo>` instead.
        match self {
            Self::Query => format!("consider using a filter instead: `With<{ty}>`"),
        }
    }
}

/// Checks if a type is zero-sized.
///
/// Returns:
/// - `Some(true)` if the type is most likely a ZST
/// - `Some(false)` if the type is most likely not a ZST
/// - `None` if we cannot determine the size (e.g., type is not normalizable)
fn is_zero_sized<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<bool> {
    // `cx.layout_of()` panics if the type is not normalizable.
    if !is_normalizable(cx, cx.param_env, ty) {
        return None;
    }

    // Note: we don't use `approx_ty_size` from `clippy_utils` here
    // because it will return `0` as the default value if the type is not
    // normalizable, which will put us at risk of emitting more false positives.
    if let Ok(TyAndLayout { layout, .. }) = cx.layout_of(ty) {
        Some(layout.size() == Size::ZERO)
    } else {
        None
    }
}