bevy_lint/lints/suspicious/
insert_message_resource.rs

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