Skip to content
104 changes: 4 additions & 100 deletions bevy_lint/src/lints/restriction/missing_reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,16 @@

use clippy_utils::{
diagnostics::span_lint_hir_and_then,
paths::PathLookup,
sugg::DiagExt,
ty::{implements_trait, ty_from_hir_ty},
};
use rustc_errors::Applicability;
use rustc_hir::{HirId, Item, ItemKind, Node, OwnerId, QPath, TyKind, def::DefKind};
use rustc_hir::ItemKind;
use rustc_lint::{LateContext, LateLintPass};
use rustc_span::Span;

use crate::{declare_bevy_lint, declare_bevy_lint_pass, span_unreachable};
use crate::{
declare_bevy_lint, declare_bevy_lint_pass, span_unreachable, utils::traits::TraitType,
};

declare_bevy_lint! {
pub(crate) MISSING_REFLECT,
Expand Down Expand Up @@ -201,99 +201,3 @@ impl<'tcx> LateLintPass<'tcx> for MissingReflect {
}
}
}

/// Represents a type that implements a specific trait.
#[derive(Debug)]
struct TraitType {
/// The [`HirId`] pointing to the type item declaration.
hir_id: HirId,
/// The span where the type was declared.
item_span: Span,
/// The span where the trait was implemented.
impl_span: Span,
}

impl TraitType {
fn from_local_crate<'tcx, 'a>(
cx: &'a LateContext<'tcx>,
trait_path: &'a PathLookup,
) -> impl Iterator<Item = Self> + use<'tcx, 'a> {
// Find the `DefId` of the trait. There may be multiple if there are multiple versions of
// the same crate.
let trait_def_ids = trait_path
.get(cx)
.iter()
.filter(|&def_id| cx.tcx.def_kind(def_id) == DefKind::Trait);

// Find a map of all trait `impl` items within the current crate. The key is the `DefId` of
// the trait, and the value is a `Vec<LocalDefId>` for all `impl` items.
let all_trait_impls = cx.tcx.all_local_trait_impls(());

// Find all `impl` items for the specific trait.
let trait_impls = trait_def_ids
.filter_map(|def_id| all_trait_impls.get(def_id))
.flatten()
.copied();

// Map the `DefId`s of `impl` items to `TraitType`s. Sometimes this conversion can fail, so
// we use `filter_map()` to skip errors.
trait_impls.filter_map(move |local_def_id| {
// Retrieve the node of the `impl` item from its `DefId`.
let node = cx.tcx.hir_node_by_def_id(local_def_id);

// Verify that it's an `impl` item and not something else.
let Node::Item(Item {
kind: ItemKind::Impl(impl_),
span: impl_span,
..
}) = node
else {
return None;
};

// Find where `T` in `impl T` was originally defined, after peeling away all references
// `&`. This was adapted from `clippy_utils::path_res()` in order to avoid passing
// `LateContext` to this function.
let def_id = match impl_.self_ty.peel_refs().kind {
TyKind::Path(QPath::Resolved(_, path)) => path.res.opt_def_id()?,
_ => return None,
};

// Tries to convert the `DefId` to a `LocalDefId`, exiting early if it cannot be done.
// This will only work if `T` in `impl T` is defined within the same crate.
//
// In most cases this will succeed due to Rust's orphan rule, but it notably fails
// within `bevy_reflect` itself, since that crate implements `Reflect` for `std` types
// such as `String`.
let local_def_id = def_id.as_local()?;

// Find the `HirId` from the `LocalDefId`. This is like a `DefId`, but with further
// constraints on what it can represent.
let hir_id = OwnerId {
def_id: local_def_id,
}
.into();

// Find the span where the type was declared. This is guaranteed to be an item, so we
// can safely call `expect_item()` without it panicking.
let item_span = cx.tcx.hir_node(hir_id).expect_item().span;

Some(TraitType {
hir_id,
item_span,
impl_span: *impl_span,
})
})
}
}

/// A custom equality implementation that just checks the [`HirId`] of the [`TraitType`], and skips
/// the other values.
///
/// [`TraitType`]s with equal [`HirId`]s are guaranteed to be equal in all other fields, so this
/// takes advantage of that fact.
impl PartialEq for TraitType {
fn eq(&self, other: &Self) -> bool {
self.hir_id == other.hir_id
}
}
144 changes: 144 additions & 0 deletions bevy_lint/src/lints/restriction/missing_trait_impls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use clippy_utils::{diagnostics::span_lint_hir_and_then, sugg::DiagExt, ty::implements_trait};
use rustc_errors::Applicability;
use rustc_hir::{ItemKind, def_id::DefId};
use rustc_lint::{LateContext, LateLintPass, Lint};

use crate::{declare_bevy_lint, declare_bevy_lint_pass, utils::traits::TraitType};

declare_bevy_lint! {
pub(crate) MISSING_DEFAULT,
super::Restriction,
"defined a unit component without a `Default` implementation",
}

declare_bevy_lint! {
pub(crate) MISSING_CLONE,
super::Restriction,
"defined a unit component without a `Clone` implementation",
}

declare_bevy_lint! {
pub(crate) MISSING_COPY,
super::Restriction,
"defined a unit component without a `Copy` implementation",
}

declare_bevy_lint_pass! {
pub(crate) MissingTraitImpls => [MISSING_DEFAULT,MISSING_CLONE,MISSING_COPY],
}

impl<'tcx> LateLintPass<'tcx> for MissingTraitImpls {
fn check_crate(&mut self, cx: &LateContext<'tcx>) {
// Finds all types that implement `Component` in this crate.
let components: Vec<TraitType> =
TraitType::from_local_crate(cx, &crate::paths::COMPONENT).collect();

for component in components {
// Skip if a types originates from a foreign crate's macro
if component
.item_span
.in_external_macro(cx.tcx.sess.source_map())
{
continue;
}

let def_id = component.hir_id.owner.to_def_id();

// Skip binder as we are not interested in the generics
let ty = cx.tcx.type_of(def_id).skip_binder();

for trait_to_implement in Trait::all() {
// get the def_id of the trait that should be implement for unit structures.
if let Some(trait_def_id) = trait_to_implement.get_def_id(cx)
&& !implements_trait(cx, ty, trait_def_id, &[])
{
// Find the `Item` definition of the Component missing the trait derive.
let missing_trait_imp_item = cx
.tcx
.hir_expect_item(component.hir_id.expect_owner().def_id);

let fields = match missing_trait_imp_item.kind {
ItemKind::Struct(_, _, data) => data.fields().to_vec(),
// If this item is not a struct, continue
_ => continue,
};

// Check if the struct is a unit struct (contains no fields)
if !fields.is_empty() {
continue;
}

span_lint_hir_and_then(
cx,
trait_to_implement.lint(),
// This tells `rustc` where to search for `#[allow(...)]` attributes.
component.hir_id,
component.item_span,
format!(
"defined a unit struct without a `{}` implementation",
trait_to_implement.name()
),
|diag| {
diag.span_note(component.impl_span, "`Component` implemented here")
.suggest_item_with_attr(
cx,
component.item_span,
format!(
"`{}` can be automatically derived",
trait_to_implement.name()
)
.as_str(),
format!("#[derive({})]", trait_to_implement.name()).as_str(),
// This lint is MachineApplicable, since we only lint structs
// that do not have any
// fields. This suggestion
// may result in two consecutive
// `#[derive(...)]` attributes, but `rustfmt` merges them
// afterwards.
Applicability::MachineApplicable,
);
},
);
}
}
}
}
}

enum Trait {
Copy,
Clone,
Default,
}

impl Trait {
const fn all() -> [Trait; 3] {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not want to create our own proc macro for this / use an extra crate but it's rather sad that there is no built-in way to do this.

use Trait::*;

[Copy, Clone, Default]
}

const fn name(&self) -> &'static str {
match self {
Trait::Copy => "Copy",
Trait::Clone => "Clone",
Trait::Default => "Default",
}
}

const fn lint(&self) -> &'static Lint {
match self {
Trait::Copy => MISSING_COPY,
Trait::Clone => MISSING_CLONE,
Trait::Default => MISSING_DEFAULT,
}
}

fn get_def_id(&self, cx: &LateContext) -> std::option::Option<DefId> {
match self {
Trait::Copy => cx.tcx.get_diagnostic_item(rustc_span::symbol::sym::Copy),
Trait::Clone => cx.tcx.get_diagnostic_item(rustc_span::symbol::sym::Clone),
Trait::Default => cx.tcx.get_diagnostic_item(rustc_span::symbol::sym::Default),
}
}
}
5 changes: 5 additions & 0 deletions bevy_lint/src/lints/restriction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use rustc_lint::{Level, Lint, LintStore};
use crate::lint::LintGroup;

pub mod missing_reflect;
pub mod missing_trait_impls;
pub mod panicking_methods;
pub mod schedule;

Expand All @@ -21,13 +22,17 @@ impl LintGroup for Restriction {
const LEVEL: Level = Level::Allow;
const LINTS: &[&Lint] = &[
missing_reflect::MISSING_REFLECT,
missing_trait_impls::MISSING_CLONE,
missing_trait_impls::MISSING_COPY,
missing_trait_impls::MISSING_DEFAULT,
panicking_methods::PANICKING_METHODS,
schedule::FIXED_UPDATE_SCHEDULE,
schedule::UPDATE_SCHEDULE,
];

fn register_passes(store: &mut LintStore) {
store.register_late_pass(|_| Box::new(missing_reflect::MissingReflect));
store.register_late_pass(|_| Box::new(missing_trait_impls::MissingTraitImpls));
store.register_late_pass(|_| Box::new(panicking_methods::PanickingMethods));
store.register_late_pass(|_| Box::new(schedule::Schedule));
}
Expand Down
1 change: 1 addition & 0 deletions bevy_lint/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod cargo;
pub mod hir_parse;
pub mod method_call;
mod panic;
pub mod traits;
100 changes: 100 additions & 0 deletions bevy_lint/src/utils/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use clippy_utils::paths::PathLookup;
use rustc_hir::{HirId, Item, ItemKind, Node, OwnerId, QPath, TyKind, def::DefKind};
use rustc_lint::LateContext;
use rustc_span::Span;

/// Represents a type that implements a specific trait.
#[derive(Debug)]
pub struct TraitType {
/// The [`HirId`] pointing to the type item declaration.
pub hir_id: HirId,
/// The span where the type was declared.
pub item_span: Span,
/// The span where the trait was implemented.
pub impl_span: Span,
}

impl TraitType {
pub fn from_local_crate<'tcx, 'a>(
cx: &'a LateContext<'tcx>,
trait_path: &'a PathLookup,
) -> impl Iterator<Item = Self> + use<'tcx, 'a> {
// Find the `DefId` of the trait. There may be multiple if there are multiple versions of
// the same crate.
let trait_def_ids = trait_path
.get(cx)
.iter()
.filter(|&def_id| cx.tcx.def_kind(def_id) == DefKind::Trait);

// Find a map of all trait `impl` items within the current crate. The key is the `DefId` of
// the trait, and the value is a `Vec<LocalDefId>` for all `impl` items.
let all_trait_impls = cx.tcx.all_local_trait_impls(());

// Find all `impl` items for the specific trait.
let trait_impls = trait_def_ids
.filter_map(|def_id| all_trait_impls.get(def_id))
.flatten()
.copied();

// Map the `DefId`s of `impl` items to `TraitType`s. Sometimes this conversion can fail, so
// we use `filter_map()` to skip errors.
trait_impls.filter_map(move |local_def_id| {
// Retrieve the node of the `impl` item from its `DefId`.
let node = cx.tcx.hir_node_by_def_id(local_def_id);

// Verify that it's an `impl` item and not something else.
let Node::Item(Item {
kind: ItemKind::Impl(impl_),
span: impl_span,
..
}) = node
else {
return None;
};

// Find where `T` in `impl T` was originally defined, after peeling away all references
// `&`. This was adapted from `clippy_utils::path_res()` in order to avoid passing
// `LateContext` to this function.
let def_id = match impl_.self_ty.peel_refs().kind {
TyKind::Path(QPath::Resolved(_, path)) => path.res.opt_def_id()?,
_ => return None,
};

// Tries to convert the `DefId` to a `LocalDefId`, exiting early if it cannot be done.
// This will only work if `T` in `impl T` is defined within the same crate.
//
// In most cases this will succeed due to Rust's orphan rule, but it notably fails
// within `bevy_reflect` itself, since that crate implements `Reflect` for `std` types
// such as `String`.
let local_def_id = def_id.as_local()?;

// Find the `HirId` from the `LocalDefId`. This is like a `DefId`, but with further
// constraints on what it can represent.
let hir_id = OwnerId {
def_id: local_def_id,
}
.into();

// Find the span where the type was declared. This is guaranteed to be an item, so we
// can safely call `expect_item()` without it panicking.
let item_span = cx.tcx.hir_node(hir_id).expect_item().span;

Some(TraitType {
hir_id,
item_span,
impl_span: *impl_span,
})
})
}
}

/// A custom equality implementation that just checks the [`HirId`] of the [`TraitType`], and skips
/// the other values.
///
/// [`TraitType`]s with equal [`HirId`]s are guaranteed to be equal in all other fields, so this
/// takes advantage of that fact.
impl PartialEq for TraitType {
fn eq(&self, other: &Self) -> bool {
self.hir_id == other.hir_id
}
}
Loading