bevy_lint/lints/restriction/
panicking_methods.rs

1//! Checks for use of panicking methods of `World` when a non-panicking
2//! alternative exists.
3//!
4//! For instance, this will lint against `World::entity()`, recommending that `World::get_entity()`
5//! should be used instead.
6//!
7//! # Motivation
8//!
9//! Panicking is the nuclear option of error handling in Rust: it is meant for cases where recovery
10//! is near-impossible. As such, panicking is usually undesirable in long-running applications
11//! and games like what Bevy is used for. This lint aims to prevent unwanted crashes in these
12//! applications by forcing developers to handle the `Option` or `Result` in their code.
13//!
14//! # Example
15//!
16//! ```
17//! # use bevy::prelude::*;
18//! #
19//! #[derive(Resource)]
20//! struct MyResource;
21//!
22//! fn panicking_world(world: &mut World) {
23//!     let resource = world.resource::<MyResource>();
24//!     // ...
25//! }
26//! #
27//! # bevy::ecs::system::assert_is_system(panicking_world);
28//! ```
29//!
30//! Use instead:
31//!
32//! ```
33//! # use bevy::prelude::*;
34//! #
35//!
36//! #[derive(Resource)]
37//! struct MyResource;
38//!
39//! fn graceful_world(world: &mut World) {
40//!     let Some(resource) = world.get_resource::<MyResource>() else {
41//!         // Resource may not exist.
42//!         return;
43//!     };
44//!
45//!     // ...
46//! }
47//! #
48//! # bevy::ecs::system::assert_is_system(graceful_world);
49//! ```
50
51use clippy_utils::{
52    diagnostics::span_lint_and_help,
53    source::{snippet, snippet_opt},
54};
55use rustc_hir::Expr;
56use rustc_lint::{LateContext, LateLintPass};
57use rustc_middle::ty::Ty;
58use rustc_span::Symbol;
59
60use crate::{
61    declare_bevy_lint, declare_bevy_lint_pass,
62    utils::{
63        hir_parse::{generic_args_snippet, span_args},
64        method_call::MethodCall,
65    },
66};
67
68declare_bevy_lint! {
69    pub(crate) PANICKING_METHODS,
70    super::Restriction,
71    "called a method that can panic when a non-panicking alternative exists",
72}
73
74declare_bevy_lint_pass! {
75    pub(crate) PanickingMethods => [PANICKING_METHODS],
76}
77
78impl<'tcx> LateLintPass<'tcx> for PanickingMethods {
79    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
80        // skip expressions that originate from external macros
81        if expr.span.in_external_macro(cx.tcx.sess.source_map()) {
82            return;
83        }
84
85        // Check if `expr` is a method call
86        if let Some(MethodCall {
87            span,
88            method_path,
89            args,
90            receiver,
91            is_fully_qulified,
92        }) = MethodCall::try_from(cx, expr)
93        {
94            // get the `Ty` of the receiver, this can either be:
95            //
96            // for fully qualified method calls the first argument is `Self` and represents the
97            // `Ty` we are looking for:
98            //
99            // Query::single(&foo, args);
100            //              ^^^^^
101            // for *not* fully qualified method calls:
102            //
103            // foo.single();
104            // ^^^^^
105            //
106            // We peel all references to that `Foo`, `&Foo`, `&&Foo`, etc.
107            let src_ty = cx.typeck_results().expr_ty_adjusted(receiver).peel_refs();
108
109            // Check if `src_ty` is a type that has panicking methods (e.g. `Query`), else exit.
110            let Some(panicking_type) = PanickingType::try_from_ty(cx, src_ty) else {
111                return;
112            };
113
114            // Get a list of methods that panic and their alternatives for the specific query
115            // variant.
116            let panicking_alternatives = panicking_type.alternatives();
117
118            // Here we check if the method name matches one of methods in `panicking_alternatives`.
119            // If it does match, we store the recommended alternative for reference in diagnostics
120            // later. If nothing matches, we exit the entire function.
121            let alternative = 'block: {
122                for (panicking_method, alternative_method) in panicking_alternatives {
123                    // If performance is an issue in the future, this could be cached.
124                    let key = Symbol::intern(panicking_method);
125
126                    if method_path.ident.name == key {
127                        // It is one of the panicking methods. Write down the alternative and
128                        // stop searching.
129                        break 'block *alternative_method;
130                    }
131                }
132
133                // If we reach this point, the method is not one we're searching for. In this
134                // case, we exit.
135                return;
136            };
137
138            // By this point, we've verified that `src` is a panicking type and the method is
139            // one that panics with a viable alternative. Let's emit the lint.
140            let (src_snippet, generics_snippet, args_snippet) = if is_fully_qulified {
141                // When the method was a fully qualified method call, the beginning of the snippet
142                // is just the `PanickingType`.
143                let mut src_snippet = panicking_type.name().to_string();
144                src_snippet.push_str("::");
145
146                // Try to find the generic arguments of the method, if any exist. This can
147                // either evaluate to `""` or `"::<A, B, C>"`.
148                let generics_snippet = generic_args_snippet(cx, method_path);
149
150                // The first argument to a fully qualified method call is the receiver (`Self`) and
151                // is not part of the `args`
152                let receiver_snippet = snippet(cx, receiver.span, "");
153
154                // Try to find the string representation of the arguments to our panicking
155                // method. See `span_args()` for more details on how this is
156                // done.
157                let args_snippet = snippet(cx, span_args(args), "");
158                // If there are no args, just return the `receiver` as the only argument
159                if args_snippet.is_empty() {
160                    (src_snippet, generics_snippet, receiver_snippet)
161                } else {
162                    // If there are arguments in the method call, add them after the `receiver` and
163                    // add the `,` as delimiter
164                    (
165                        src_snippet,
166                        generics_snippet,
167                        format!("{receiver_snippet}, {args_snippet}").into(),
168                    )
169                }
170            }
171            // The method was not a fully qualified call
172            else {
173                // Try to find the string representation of `src`. This usually returns
174                // `my_query` without the trailing `.`, so we manually
175                // append it. When the snippet cannot be found, we default
176                // to the qualified `Type::` form.
177                let src_snippet = snippet_opt(cx, receiver.span).map_or_else(
178                    || format!("{}::", panicking_type.name()),
179                    |mut s| {
180                        s.push('.');
181                        s
182                    },
183                );
184                // Try to find the generic arguments of the method, if any exist. This can
185                // either evaluate to `""` or `"::<A, B, C>"`.
186                let generics_snippet = generic_args_snippet(cx, method_path);
187
188                // Try to find the string representation of the arguments to our panicking
189                // method. See `span_args()` for more details on how this is
190                // done.
191                let args_snippet = snippet(cx, span_args(args), "");
192                (src_snippet, generics_snippet, args_snippet)
193            };
194
195            span_lint_and_help(
196                cx,
197                PANICKING_METHODS,
198                span,
199                format!(
200                    "called a `{}` method that can panic when a non-panicking alternative exists",
201                    panicking_type.name()
202                ),
203                None,
204                // This usually ends up looking like: `query.get_many([e1, e2])`.
205                format!(
206                    "use `{src_snippet}{alternative}{generics_snippet}({args_snippet})` and handle the `Option` or `Result`"
207                ),
208            );
209        }
210    }
211}
212
213enum PanickingType {
214    Query,
215    World,
216}
217
218impl PanickingType {
219    /// Returns the corresponding variant for the given [`Ty`], if it is supported by this lint.
220    fn try_from_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Self> {
221        if crate::paths::QUERY.matches_ty(cx, ty) {
222            Some(Self::Query)
223        } else if crate::paths::WORLD.matches_ty(cx, ty) {
224            Some(Self::World)
225        } else {
226            None
227        }
228    }
229
230    /// Returns a list of panicking methods for each of the supported types.
231    ///
232    /// Each item in the returned [`slice`] is of the format
233    /// `(panicking_method, alternative_method)`.
234    fn alternatives(&self) -> &'static [(&'static str, &'static str)] {
235        match self {
236            Self::Query => &[("many", "get_many"), ("many_mut", "get_many_mut")],
237            Self::World => &[
238                ("entity", "get_entity"),
239                ("entity_mut", "get_entity_mut"),
240                ("resource", "get_resource"),
241                ("resource_mut", "get_resource_mut"),
242                ("resource_ref", "get_resource_ref"),
243                ("non_send_resource", "get_non_send_resource"),
244                ("non_send_resource_mut", "get_non_send_resource_mut"),
245                ("run_schedule", "try_run_schedule"),
246                ("schedule_scope", "try_schedule_scope"),
247            ],
248        }
249    }
250
251    /// Returns the name of the type this variant represents.
252    fn name(&self) -> &'static str {
253        match self {
254            Self::Query => "Query",
255            Self::World => "World",
256        }
257    }
258}