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}