bevy_lint/lints/suspicious/
insert_event_resource.rs

1//! Checks for the `Events<T>` resource being manually inserted with `App::init_resource()` or
2//! `App::insert_resource()` instead of with `App::add_event()`.
3//!
4//! # Motivation
5//!
6//! Unless you have intentionally and knowingly initialized the `Events<T>` resource in this way,
7//! events and their resources should be initialized with `App::add_event()` because it
8//! automatically handles dropping old events. Just adding `Events<T>` makes no such guarantee, and
9//! will likely result in a memory leak.
10//!
11//! For more information, please see the documentation on [`App::add_event()`] and [`Events<T>`].
12//!
13//! [`Events<T>`]: https://dev-docs.bevyengine.org/bevy/ecs/event/struct.Events.html
14//! [`App::add_event()`]: https://docs.rs/bevy/latest/bevy/app/struct.App.html#method.add_event
15//!
16//! # Example
17//!
18//! ```
19//! # use bevy::prelude::*;
20//! #
21//! #[derive(Event)]
22//! struct MyEvent;
23//!
24//! fn plugin(app: &mut App) {
25//!     app.init_resource::<Events<MyEvent>>();
26//! }
27//! ```
28//!
29//! Use instead:
30//!
31//! ```
32//! # use bevy::prelude::*;
33//! #
34//! #[derive(Event)]
35//! struct MyEvent;
36//!
37//! fn plugin(app: &mut App) {
38//!     app.add_event::<MyEvent>();
39//! }
40//! ```
41
42use std::borrow::Cow;
43
44use clippy_utils::{
45    diagnostics::span_lint_and_sugg,
46    source::{snippet, snippet_with_applicability},
47    ty::ty_from_hir_ty,
48};
49use rustc_errors::Applicability;
50use rustc_hir::{Expr, GenericArg, GenericArgs, Path, PathSegment, QPath};
51use rustc_lint::{LateContext, LateLintPass};
52use rustc_middle::ty::{Ty, TyKind};
53
54use crate::{
55    declare_bevy_lint, declare_bevy_lint_pass, sym,
56    utils::hir_parse::{MethodCall, generic_args_snippet, span_args},
57};
58
59declare_bevy_lint! {
60    pub(crate) INSERT_EVENT_RESOURCE,
61    super::Suspicious,
62    "called `App::insert_resource(Events<T>)` or `App::init_resource::<Events<T>>()` instead of `App::add_event::<T>()`",
63}
64
65declare_bevy_lint_pass! {
66    pub(crate) InsertEventResource => [INSERT_EVENT_RESOURCE],
67}
68
69const HELP_MESSAGE: &str = "inserting an `Events` resource does not fully setup that event";
70
71impl<'tcx> LateLintPass<'tcx> for InsertEventResource {
72    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
73        // skip expressions that originate from external macros
74        if expr.span.in_external_macro(cx.tcx.sess.source_map()) {
75            return;
76        }
77
78        // Find a method call.
79        if let Some(method_call) = MethodCall::try_from(cx, expr) {
80            // Get the type for `src` in `src.method()`. We peel all references because the type
81            // could either be `App` or `&mut App`.
82            let src_ty = cx
83                .typeck_results()
84                .expr_ty_adjusted(method_call.receiver)
85                .peel_refs();
86
87            // If `src` is not a Bevy `App`, exit.
88            if !crate::paths::APP.matches_ty(cx, src_ty) {
89                return;
90            }
91
92            // If the method is `App::insert_resource()` or `App::init_resource()`, check it with
93            // its corresponding function.
94            match method_call.method_path.ident.name {
95                symbol if symbol == sym::insert_resource => {
96                    check_insert_resource(cx, &method_call);
97                }
98                symbol if symbol == sym::init_resource => {
99                    check_init_resource(cx, &method_call);
100                }
101                _ => {}
102            }
103        }
104    }
105}
106
107/// Checks if `App::insert_resource()` inserts an `Events<T>`, and emits a diagnostic if so.
108fn check_insert_resource(cx: &LateContext<'_>, method_call: &MethodCall) {
109    // Extract the argument if there is only 1 (which there should be!), else exit.
110    let [arg] = method_call.args else {
111        return;
112    };
113
114    // Find the type of `arg` in `App::insert_resource(&mut self, arg)`.
115    let ty = cx.typeck_results().expr_ty_adjusted(arg);
116
117    // If `arg` is `Events<T>`, emit the lint.
118    if crate::paths::EVENTS.matches_ty(cx, ty) {
119        let mut applicability = Applicability::MachineApplicable;
120
121        let event_ty_snippet = extract_ty_event_snippet(ty, &mut applicability);
122        let args_snippet = snippet(cx, span_args(method_call.args), "");
123        let generics_snippet = generic_args_snippet(cx, method_call.method_path);
124
125        if method_call.is_fully_qulified {
126            let receiver_snippet = snippet(cx, method_call.receiver.span, "");
127            span_lint_and_sugg(
128                cx,
129                INSERT_EVENT_RESOURCE,
130                method_call.span,
131                format!(
132                    "called `App::insert_resource{generics_snippet}({receiver_snippet}, {args_snippet})` instead of `App::add_event::<{event_ty_snippet}>({receiver_snippet})`"
133                ),
134                HELP_MESSAGE,
135                format!("App::add_event::<{event_ty_snippet}>({receiver_snippet})"),
136                applicability,
137            );
138        } else {
139            span_lint_and_sugg(
140                cx,
141                INSERT_EVENT_RESOURCE,
142                method_call.span,
143                format!(
144                    "called `App::insert_resource{generics_snippet}({args_snippet})` instead of `App::add_event::<{event_ty_snippet}>()`"
145                ),
146                HELP_MESSAGE,
147                format!("add_event::<{event_ty_snippet}>()"),
148                applicability,
149            );
150        }
151    }
152}
153
154/// Creates a string representation of type `T` for [`Ty`] `Events<T>`.
155///
156/// This takes a mutable applicability reference, and will set it to
157/// [`Applicability::HasPlaceholders`] if the type cannot be stringified.
158fn extract_ty_event_snippet<'tcx>(
159    events_ty: Ty<'tcx>,
160    applicability: &mut Applicability,
161) -> Cow<'tcx, str> {
162    const DEFAULT: Cow<str> = Cow::Borrowed("T");
163
164    let TyKind::Adt(_, events_arguments) = events_ty.kind() else {
165        if let Applicability::MachineApplicable = applicability {
166            *applicability = Applicability::HasPlaceholders;
167        }
168
169        return DEFAULT;
170    };
171
172    let Some(event_snippet) = events_arguments.iter().next() else {
173        if let Applicability::MachineApplicable = applicability {
174            *applicability = Applicability::HasPlaceholders;
175        }
176
177        return DEFAULT;
178    };
179
180    format!("{event_snippet:?}").into()
181}
182
183/// Checks if `App::init_resource()` inserts an `Events<T>`, and emits a diagnostic if so.
184fn check_init_resource<'tcx>(cx: &LateContext<'tcx>, method_call: &MethodCall<'tcx>) {
185    if let Some(&GenericArgs {
186        // `App::init_resource()` has one generic type argument: T.
187        args: &[GenericArg::Type(resource_hir_ty)],
188        ..
189    }) = method_call.method_path.args
190    {
191        // Lower `rustc_hir::Ty` to `ty::Ty`, so we can inspect type information. For more
192        // information, see <https://rustc-dev-guide.rust-lang.org/ty.html#rustc_hirty-vs-tyty>.
193        let resource_ty = ty_from_hir_ty(cx, resource_hir_ty.as_unambig_ty());
194
195        // If the resource type is `Events<T>`, emit the lint.
196        if crate::paths::EVENTS.matches_ty(cx, resource_ty) {
197            let mut applicability = Applicability::MachineApplicable;
198
199            let event_ty_snippet =
200                extract_hir_event_snippet(cx, resource_hir_ty.as_unambig_ty(), &mut applicability);
201
202            let args_snippet = snippet(cx, span_args(method_call.args), "");
203            let generics_snippet = generic_args_snippet(cx, method_call.method_path);
204
205            if method_call.is_fully_qulified {
206                let receiver_snippet = snippet(cx, method_call.receiver.span, "");
207                span_lint_and_sugg(
208                    cx,
209                    INSERT_EVENT_RESOURCE,
210                    method_call.span,
211                    format!(
212                        "called `App::init_resource{generics_snippet}({receiver_snippet})` instead of `App::add_event::<{event_ty_snippet}>({receiver_snippet})`"
213                    ),
214                    HELP_MESSAGE,
215                    format!("App::add_event::<{event_ty_snippet}>({receiver_snippet})"),
216                    applicability,
217                );
218            } else {
219                span_lint_and_sugg(
220                    cx,
221                    INSERT_EVENT_RESOURCE,
222                    method_call.span,
223                    format!(
224                        "called `App::init_resource{generics_snippet}({args_snippet})` instead of `App::add_event::<{event_ty_snippet}>()`"
225                    ),
226                    HELP_MESSAGE,
227                    format!("add_event::<{event_ty_snippet}>()"),
228                    applicability,
229                );
230            }
231        }
232    }
233}
234
235/// Tries to extract the snippet `MyEvent` from the [`rustc_hir::Ty`] representing
236/// `Events<MyEvent>`.
237///
238/// Note that this works on a best-effort basis, and will return `"T"` if the type cannot be
239/// extracted. If so, it will mutate the passed applicability to [`Applicability::HasPlaceholders`],
240/// similar to [`snippet_with_applicability()`].
241fn extract_hir_event_snippet<'tcx>(
242    cx: &LateContext<'tcx>,
243    events_hir_ty: &rustc_hir::Ty<'tcx>,
244    applicability: &mut Applicability,
245) -> Cow<'static, str> {
246    const DEFAULT: Cow<str> = Cow::Borrowed("T");
247
248    // This is some crazy pattern matching. Let me walk you through it:
249    let event_span = match events_hir_ty.kind {
250        // There are multiple kinds of HIR types, but we're looking for a path to a type
251        // definition. This path is likely `Events`, and contains the generic argument that we're
252        // searching for.
253        rustc_hir::TyKind::Path(QPath::Resolved(
254            _,
255            &Path {
256                // There can be multiple segments in a path, such as if it were
257                // `bevy::prelude::Events`, but in this case we just care about the last: `Events`.
258                segments:
259                    &[
260                        ..,
261                        PathSegment {
262                            // Find the arguments to `Events<T>`, extracting `T`.
263                            args:
264                                Some(&GenericArgs {
265                                    args: &[GenericArg::Type(ty)],
266                                    ..
267                                }),
268                            ..
269                        },
270                    ],
271                ..
272            },
273        )) => {
274            // We now have the HIR type `T` for `Events<T>`, let's return its span.
275            ty.span
276        }
277        // Something in the above pattern matching went wrong, likely due to an edge case. For
278        // this, we set the applicability to `HasPlaceholders` and return the default snippet.
279        _ => {
280            if let Applicability::MachineApplicable = applicability {
281                *applicability = Applicability::HasPlaceholders;
282            }
283
284            return DEFAULT;
285        }
286    };
287
288    // We now have the span to the event type, so let's try to extract it into a string.
289    snippet_with_applicability(cx, event_span, &DEFAULT, applicability)
290}