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    if crate::paths::MESSAGES.matches_ty(cx, ty) {
123        let mut applicability = Applicability::MachineApplicable;
124
125        let message_ty_snippet = extract_ty_message_snippet(ty, &mut applicability);
126        let args_snippet = snippet(cx, span_args(method_call.args), "");
127        let generics_snippet = generic_args_snippet(cx, method_call.method_path);
128
129        if method_call.is_fully_qualified {
130            let receiver_snippet = snippet(cx, method_call.receiver.span, "");
131            span_lint_and_sugg(
132                cx,
133                INSERT_MESSAGE_RESOURCE,
134                method_call.span,
135                format!(
136                    "called `App::insert_resource{generics_snippet}({receiver_snippet}, {args_snippet})` instead of `App::add_message::<{message_ty_snippet}>({receiver_snippet})`"
137                ),
138                HELP_MESSAGE,
139                format!("App::add_message::<{message_ty_snippet}>({receiver_snippet})"),
140                applicability,
141            );
142        } else {
143            span_lint_and_sugg(
144                cx,
145                INSERT_MESSAGE_RESOURCE,
146                method_call.span,
147                format!(
148                    "called `App::insert_resource{generics_snippet}({args_snippet})` instead of `App::add_message::<{message_ty_snippet}>()`"
149                ),
150                HELP_MESSAGE,
151                format!("add_message::<{message_ty_snippet}>()"),
152                applicability,
153            );
154        }
155    }
156}
157
158/// Creates a string representation of type `T` for [`Ty`] `Messages<T>`.
159///
160/// This takes a mutable applicability reference, and will set it to
161/// [`Applicability::HasPlaceholders`] if the type cannot be stringified.
162fn extract_ty_message_snippet<'tcx>(
163    messages_ty: Ty<'tcx>,
164    applicability: &mut Applicability,
165) -> Cow<'tcx, str> {
166    const DEFAULT: Cow<str> = Cow::Borrowed("T");
167
168    let TyKind::Adt(_, messages_arguments) = messages_ty.kind() else {
169        if let Applicability::MachineApplicable = applicability {
170            *applicability = Applicability::HasPlaceholders;
171        }
172
173        return DEFAULT;
174    };
175
176    let Some(message_snippet) = messages_arguments.iter().next() else {
177        if let Applicability::MachineApplicable = applicability {
178            *applicability = Applicability::HasPlaceholders;
179        }
180
181        return DEFAULT;
182    };
183
184    format!("{message_snippet:?}").into()
185}
186
187/// Checks if `App::init_resource()` inserts an `Messages<T>`, and emits a diagnostic if so.
188fn check_init_resource<'tcx>(cx: &LateContext<'tcx>, method_call: &MethodCall<'tcx>) {
189    if let Some(&GenericArgs {
190        // `App::init_resource()` has one generic type argument: T.
191        args: &[GenericArg::Type(resource_hir_ty)],
192        ..
193    }) = method_call.method_path.args
194    {
195        // Lower `rustc_hir::Ty` to `ty::Ty`, so we can inspect type information. For more
196        // information, see <https://rustc-dev-guide.rust-lang.org/ty.html#rustc_hirty-vs-tyty>.
197        let resource_ty = ty_from_hir_ty(cx, resource_hir_ty.as_unambig_ty());
198
199        // If the resource type is `Messages<T>`, emit the lint.
200        if crate::paths::MESSAGES.matches_ty(cx, resource_ty) {
201            let mut applicability = Applicability::MachineApplicable;
202
203            let message_ty_snippet = extract_hir_message_snippet(
204                cx,
205                resource_hir_ty.as_unambig_ty(),
206                &mut applicability,
207            );
208
209            let args_snippet = snippet(cx, span_args(method_call.args), "");
210            let generics_snippet = generic_args_snippet(cx, method_call.method_path);
211
212            if method_call.is_fully_qualified {
213                let receiver_snippet = snippet(cx, method_call.receiver.span, "");
214                span_lint_and_sugg(
215                    cx,
216                    INSERT_MESSAGE_RESOURCE,
217                    method_call.span,
218                    format!(
219                        "called `App::init_resource{generics_snippet}({receiver_snippet})` instead of `App::add_message::<{message_ty_snippet}>({receiver_snippet})`"
220                    ),
221                    HELP_MESSAGE,
222                    format!("App::add_message::<{message_ty_snippet}>({receiver_snippet})"),
223                    applicability,
224                );
225            } else {
226                span_lint_and_sugg(
227                    cx,
228                    INSERT_MESSAGE_RESOURCE,
229                    method_call.span,
230                    format!(
231                        "called `App::init_resource{generics_snippet}({args_snippet})` instead of `App::add_message::<{message_ty_snippet}>()`"
232                    ),
233                    HELP_MESSAGE,
234                    format!("add_message::<{message_ty_snippet}>()"),
235                    applicability,
236                );
237            }
238        }
239    }
240}
241
242/// Tries to extract the snippet `MyMessage` from the [`rustc_hir::Ty`] representing
243/// `Messages<MyMessage>`.
244///
245/// Note that this works on a best-effort basis, and will return `"T"` if the type cannot be
246/// extracted. If so, it will mutate the passed applicability to [`Applicability::HasPlaceholders`],
247/// similar to [`snippet_with_applicability()`].
248fn extract_hir_message_snippet<'tcx>(
249    cx: &LateContext<'tcx>,
250    messages_hir_ty: &rustc_hir::Ty<'tcx>,
251    applicability: &mut Applicability,
252) -> Cow<'static, str> {
253    const DEFAULT: Cow<str> = Cow::Borrowed("T");
254
255    // This is some crazy pattern matching. Let me walk you through it:
256    let message_span = match messages_hir_ty.kind {
257        // There are multiple kinds of HIR types, but we're looking for a path to a type
258        // definition. This path is likely `Messages`, and contains the generic argument that we're
259        // searching for.
260        rustc_hir::TyKind::Path(QPath::Resolved(
261            _,
262            &Path {
263                // There can be multiple segments in a path, such as if it were
264                // `bevy::prelude::Messages`, but in this case we just care about the last:
265                // `Messages`.
266                segments:
267                    &[
268                        ..,
269                        PathSegment {
270                            // Find the arguments to `Messages<T>`, extracting `T`.
271                            args:
272                                Some(&GenericArgs {
273                                    args: &[GenericArg::Type(ty)],
274                                    ..
275                                }),
276                            ..
277                        },
278                    ],
279                ..
280            },
281        )) => {
282            // We now have the HIR type `T` for `Messages<T>`, let's return its span.
283            ty.span
284        }
285        // Something in the above pattern matching went wrong, likely due to an edge case. For
286        // this, we set the applicability to `HasPlaceholders` and return the default snippet.
287        _ => {
288            if let Applicability::MachineApplicable = applicability {
289                *applicability = Applicability::HasPlaceholders;
290            }
291
292            return DEFAULT;
293        }
294    };
295
296    // We now have the span to the message type, so let's try to extract it into a string.
297    snippet_with_applicability(cx, message_span, &DEFAULT, applicability)
298}