Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add borrowed_reborrowable lint #164

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
238 changes: 238 additions & 0 deletions bevy_lint/src/lints/borrowed_reborrowable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
//! Checks for function parameters that take a mutable reference to a
//! re-borrwable type.
//!
//! # Motivation
//!
//! Mutable references to re-borrwable types can almost always be readily
//! converted back to an owned instance of itself, albeit with an appropriately
//! shorter lifetime.
//!
//! The only time this isn't true is when the function returns referenced data
//! that is bound to the mutable reference of the re-borrowable type.
//!
//! # Known Issues
//!
//! This lint does not currently support the `Fn` traits or function pointers.
//!
//! This means the following types will not be caught by the lint:
//!
//! - `impl FnOnce(&mut Commands)`
//! - `Box<dyn FnMut(&mut Commands)>`
//! - `fn(&mut Commands)`
//!
//! # Example
//!
//! ```
//! # use bevy::prelude::*;
//! fn system(mut commands: Commands) {
//! helper_function(&mut commands);
//! }
//!
//! fn helper_function(commands: &mut Commands) {
//! // ...
//! }
//! ```
//!
//! Use instead:
//!
//! ```
//! # use bevy::prelude::*;
//! fn system(mut commands: Commands) {
//! helper_function(commands.reborrow());
//! }
//!
//! fn helper_function(mut commands: Commands) {
//! // ...
//! }
//! ```

use std::ops::ControlFlow;

use crate::declare_bevy_lint;
use clippy_utils::{diagnostics::span_lint_and_sugg, ty::match_type};
use rustc_errors::Applicability;
use rustc_hir::{intravisit::FnKind, Body, FnDecl, Mutability};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{Interner, Ty, TyKind, TypeVisitable, TypeVisitor};
use rustc_session::declare_lint_pass;
use rustc_span::{
def_id::LocalDefId,
symbol::{kw, Ident},
Span,
};

declare_bevy_lint! {
pub BORROWED_REBORROWABLE,
PEDANTIC,
"parameter takes a mutable reference to a re-borrowable type",
}

declare_lint_pass! {
BorrowedReborrowable => [BORROWED_REBORROWABLE.lint]
}

impl<'tcx> LateLintPass<'tcx> for BorrowedReborrowable {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
kind: FnKind<'tcx>,
decl: &'tcx FnDecl<'tcx>,
_: &'tcx Body<'tcx>,
_: Span,
def_id: LocalDefId,
) {
// We use `instantiate_identity` to discharge the binder since we don't
// mind using placeholders for any bound arguments
let fn_sig = match kind {
FnKind::Closure => cx.tcx.closure_user_provided_sig(def_id).value,
_ => cx.tcx.fn_sig(def_id).instantiate_identity(),
};

let arg_names = cx.tcx.fn_arg_names(def_id);

let args = fn_sig.inputs().skip_binder();

for (arg_index, arg_ty) in args.iter().enumerate() {
let TyKind::Ref(region, ty, Mutability::Mut) = arg_ty.kind() else {
// We only care about `&mut` parameters
continue;
};

let arg_ident = arg_names[arg_index];
if arg_ident.name == kw::SelfLower {
// Skip `&mut self` parameters
continue;
}

let peeled_ty = ty.peel_refs();

let Some(reborrowable) = Reborrowable::try_from_ty(cx, peeled_ty) else {
// The type is not one of our known re-borrowable types
continue;
};

let is_output_bound_to_arg = fn_sig
.output()
.visit_with(&mut ContainsRegion(*region))
.is_break();

if is_output_bound_to_arg {
// We don't want to suggest re-borrowing if the return type's
// lifetime is bound to the argument's reference.
// This is because it's impossible to convert something like:
// `for<'a> (&'a mut Commands<'_, '_>) -> EntityCommands<'a>`
// to something like:
// `for<'a> (Commands<'_, '_>) -> EntityCommands<'a>`
// without getting: `error[E0515]: cannot return value referencing function
// parameter `commands` ``
continue;
}

let arg_ident = arg_names[arg_index];
let span = decl.inputs[arg_index].span.to(arg_ident.span);

span_lint_and_sugg(
cx,
BORROWED_REBORROWABLE.lint,
span,
reborrowable.message(),
reborrowable.help(),
reborrowable.suggest(arg_ident, peeled_ty.to_string()),
// Not machine-applicable since the function body may need to
// also be updated to account for the removed ref
Applicability::MaybeIncorrect,
);
}
}
}

#[derive(Debug, Copy, Clone)]
enum Reborrowable {
Commands,
Deferred,
DeferredWorld,
EntityCommands,
EntityMut,
FilteredEntityMut,
Mut,
MutUntyped,
NonSendMut,
PtrMut,
Query,
ResMut,
}

impl Reborrowable {
fn try_from_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Self> {
use crate::paths::*;

const PATH_MAP: &[(&[&str], Reborrowable)] = &[
(&COMMANDS, Reborrowable::Commands),
(&DEFERRED, Reborrowable::Deferred),
(&DEFERRED_WORLD, Reborrowable::DeferredWorld),
(&ENTITY_COMMANDS, Reborrowable::EntityCommands),
(&ENTITY_MUT, Reborrowable::EntityMut),
(&FILTERED_ENTITY_MUT, Reborrowable::FilteredEntityMut),
(&MUT, Reborrowable::Mut),
(&MUT_UNTYPED, Reborrowable::MutUntyped),
(&NON_SEND_MUT, Reborrowable::NonSendMut),
(&PTR_MUT, Reborrowable::PtrMut),
(&QUERY, Reborrowable::Query),
(&RES_MUT, Reborrowable::ResMut),
];

for &(path, reborrowable) in PATH_MAP {
if match_type(cx, ty, path) {
return Some(reborrowable);
}
}

None
}

fn message(&self) -> String {
let name = self.name();
format!("parameter takes `&mut {name}` instead of a re-borrowed `{name}`",)
}

fn name(&self) -> &'static str {
match self {
Self::Commands => "Commands",
Self::Deferred => "Deferred",
Self::DeferredWorld => "DeferredWorld",
Self::EntityCommands => "EntityCommands",
Self::EntityMut => "EntityMut",
Self::FilteredEntityMut => "FilteredEntityMut",
Self::Mut => "Mut",
Self::MutUntyped => "MutUntyped",
Self::NonSendMut => "NonSendMut",
Self::PtrMut => "PtrMut",
Self::Query => "Query",
Self::ResMut => "ResMut",
}
}

fn help(&self) -> String {
let name = self.name();
format!("use `{name}` instead")
}

fn suggest(&self, ident: Ident, ty: String) -> String {
format!("mut {ident}: {ty}")
}
}

/// [`TypeVisitor`] for checking if the given region is contained in the type.
struct ContainsRegion<I: Interner>(pub I::Region);

impl<I: Interner> TypeVisitor<I> for ContainsRegion<I> {
type Result = ControlFlow<()>;

fn visit_region(&mut self, r: I::Region) -> Self::Result {
if self.0 == r {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}
}
3 changes: 3 additions & 0 deletions bevy_lint/src/lints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
use crate::lint::BevyLint;
use rustc_lint::{Lint, LintStore};

pub mod borrowed_reborrowable;
pub mod insert_event_resource;
pub mod main_return_without_appexit;
pub mod missing_reflect;
pub mod panicking_methods;
pub mod plugin_not_ending_in_plugin;

pub(crate) static LINTS: &[&BevyLint] = &[
borrowed_reborrowable::BORROWED_REBORROWABLE,
insert_event_resource::INSERT_EVENT_RESOURCE,
main_return_without_appexit::MAIN_RETURN_WITHOUT_APPEXIT,
panicking_methods::PANICKING_QUERY_METHODS,
Expand All @@ -26,6 +28,7 @@ pub(crate) fn register_lints(store: &mut LintStore) {
}

pub(crate) fn register_passes(store: &mut LintStore) {
store.register_late_pass(|_| Box::new(borrowed_reborrowable::BorrowedReborrowable));
store.register_late_pass(|_| Box::new(insert_event_resource::InsertEventResource));
store.register_late_pass(|_| Box::new(main_return_without_appexit::MainReturnWithoutAppExit));
store.register_late_pass(|_| Box::new(missing_reflect::MissingReflect));
Expand Down
11 changes: 11 additions & 0 deletions bevy_lint/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@
//! [`match_def_path()`](clippy_utils::match_def_path).

pub const APP: [&str; 3] = ["bevy_app", "app", "App"];
pub const COMMANDS: [&str; 4] = ["bevy_ecs", "system", "commands", "Commands"];
pub const COMPONENT: [&str; 3] = ["bevy_ecs", "component", "Component"];
pub const DEFERRED: [&str; 4] = ["bevy_ecs", "system", "system_param", "Deferred"];
pub const DEFERRED_WORLD: [&str; 4] = ["bevy_ecs", "world", "deferred_world", "DeferredWorld"];
pub const ENTITY_COMMANDS: [&str; 4] = ["bevy_ecs", "system", "commands", "EntityCommands"];
pub const ENTITY_MUT: [&str; 4] = ["bevy_ecs", "world", "entity_ref", "EntityMut"];
// Note that this moves to `bevy_ecs::event::base::Event` in 0.15.
pub const EVENT: [&str; 3] = ["bevy_ecs", "event", "Event"];
pub const EVENTS: [&str; 3] = ["bevy_ecs", "event", "Events"];
pub const FILTERED_ENTITY_MUT: [&str; 4] = ["bevy_ecs", "world", "entity_ref", "FilteredEntityMut"];
pub const MUT: [&str; 3] = ["bevy_ecs", "change_detection", "Mut"];
pub const MUT_UNTYPED: [&str; 3] = ["bevy_ecs", "change_detection", "MutUntyped"];
pub const NON_SEND_MUT: [&str; 3] = ["bevy_ecs", "change_detection", "NonSendMut"];
pub const PLUGIN: [&str; 3] = ["bevy_app", "plugin", "Plugin"];
pub const PTR_MUT: [&str; 2] = ["bevy_ptr", "PtrMut"];
pub const QUERY: [&str; 4] = ["bevy_ecs", "system", "query", "Query"];
pub const QUERY_STATE: [&str; 4] = ["bevy_ecs", "query", "state", "QueryState"];
pub const REFLECT: [&str; 3] = ["bevy_reflect", "reflect", "Reflect"];
pub const RES_MUT: [&str; 3] = ["bevy_ecs", "change_detection", "ResMut"];
pub const RESOURCE: [&str; 4] = ["bevy_ecs", "system", "system_param", "Resource"];
pub const WORLD: [&str; 3] = ["bevy_ecs", "world", "World"];
26 changes: 26 additions & 0 deletions bevy_lint/tests/ui/borrowed_reborrowable/bug_174.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! This test tracks the bug reported in [#174]. When this starts failing, the bug has been fixed.
//!
//! [#174]: https://github.com/TheBevyFlock/bevy_cli/issues/174

//@check-pass

#![feature(register_tool)]
#![register_tool(bevy)]
#![deny(bevy::borrowed_reborrowable)]

use bevy::prelude::*;

fn main() {
let mut world = World::new();

closure_wrapper(world.commands(), |commands| commands.spawn_empty().id());
closure_wrapper2(world.commands(), |commands| commands.spawn_empty().id());
}

fn closure_wrapper(mut commands: Commands, f: impl FnOnce(&mut Commands) -> Entity) {
f(&mut commands);
}

fn closure_wrapper2(mut commands: Commands, f: fn(&mut Commands) -> Entity) {
f(&mut commands);
}
20 changes: 20 additions & 0 deletions bevy_lint/tests/ui/borrowed_reborrowable/closures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//! This tests the `borrowed_reborrowable` lint, specifically when triggered on closure types.

#![feature(register_tool)]
#![register_tool(bevy)]
#![deny(bevy::borrowed_reborrowable)]

use bevy::prelude::*;

fn main() {
let mut world = World::new();
let mut commands = world.commands();

//~| HELP: use `Commands` instead
//~v ERROR: parameter takes `&mut Commands` instead of a re-borrowed `Commands`
let closure = |commands: &mut Commands| {
commands.spawn_empty();
};

closure(&mut commands);
}
14 changes: 14 additions & 0 deletions bevy_lint/tests/ui/borrowed_reborrowable/closures.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
error: parameter takes `&mut Commands` instead of a re-borrowed `Commands`
--> tests/ui/borrowed_reborrowable/closures.rs:15:20
|
15 | let closure = |commands: &mut Commands| {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: use `Commands` instead: `mut commands: bevy::prelude::Commands<'_, '_>`
|
note: the lint level is defined here
--> tests/ui/borrowed_reborrowable/closures.rs:5:9
|
5 | #![deny(bevy::borrowed_reborrowable)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 1 previous error

48 changes: 48 additions & 0 deletions bevy_lint/tests/ui/borrowed_reborrowable/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! This tests the `borrowed_reborrowable` lint, specifically when triggered on the `Commands` type.

#![feature(register_tool)]
#![register_tool(bevy)]
#![deny(bevy::borrowed_reborrowable)]

use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;

// OK: Lint does not apply to immutable references
fn immutable_reference(_commands: &Commands) {
// ...
}

//~| HELP: use `Commands` instead
//~v ERROR: parameter takes `&mut Commands` instead of a re-borrowed `Commands`
fn mutable_reference(commands: &mut Commands) {
commands.spawn_empty();
}

//~| HELP: use `Commands` instead
//~v ERROR: parameter takes `&mut Commands` instead of a re-borrowed `Commands`
fn mutable_reference_return<'a>(_commands: &'a mut Commands) -> usize {
123
}

// OK: Lint does not apply when return type relies on reference lifetime
fn mutable_reference_bounded_return<'a>(commands: &'a mut Commands) -> EntityCommands<'a> {
commands.spawn_empty()
}

// OK: Lint does not apply when return type relies on reference lifetime
fn mutable_reference_bounded_return_complex<'a>(
commands: &'a mut Commands,
) -> Vec<(usize, EntityCommands<'a>)> {
vec![(1, commands.spawn_empty())]
}

fn main() {
let mut world = World::new();
let mut commands = world.commands();

immutable_reference(&commands);
mutable_reference(&mut commands);
_ = mutable_reference_return(&mut commands);
_ = mutable_reference_bounded_return(&mut commands);
_ = mutable_reference_bounded_return_complex(&mut commands);
}
Loading
Loading