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