diff --git a/bevy_lint/src/lints/style/bevy_platform_alternative_exists.rs b/bevy_lint/src/lints/style/bevy_platform_alternative_exists.rs new file mode 100644 index 00000000..4bc2d74c --- /dev/null +++ b/bevy_lint/src/lints/style/bevy_platform_alternative_exists.rs @@ -0,0 +1,132 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet, ty::ty_from_hir_ty}; +use rustc_errors::Applicability; +use rustc_hir::{QPath, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; + +use crate::{declare_bevy_lint, declare_bevy_lint_pass}; + +declare_bevy_lint! { + pub(crate) BEVY_PLATFORM_ALTERNATIVE_EXISTS, + super::Style, + "Used type from the `std` that has an existing alternative from `bevy_platform`", +} + +declare_bevy_lint_pass! { + pub(crate) BevyPlatformAlternativeExists => [BEVY_PLATFORM_ALTERNATIVE_EXISTS], +} + +impl<'tcx> LateLintPass<'tcx> for BevyPlatformAlternativeExists { + fn check_ty( + &mut self, + cx: &LateContext<'tcx>, + hir_ty: &'tcx rustc_hir::Ty<'tcx, rustc_hir::AmbigArg>, + ) { + if hir_ty.span.in_external_macro(cx.tcx.sess.source_map()) { + return; + } + + let as_unambig_ty = hir_ty.as_unambig_ty(); + + // lower the [`hir::Ty`] to a [`rustc_middle::ty::Ty`] + let ty = ty_from_hir_ty(cx, as_unambig_ty); + + // Get the path to the type definition. + let TyKind::Path(QPath::Resolved(_, path)) = &as_unambig_ty.kind else { + return; + }; + + // if for the given `ty` an alternative from `bevy_platform` exists. + if let Some(bevy_platform_alternative) = BevyPlatformType::try_from_ty(cx, ty) + // Only emit a lint if the first segment of this path is `std` thus the type originates + // from the standart library. This prevents linting for `bevy::platform` types that are just a reexport of the `std`. + && path.segments.first().is_some_and(|segment| segment.ident.name.as_str().starts_with("std")) + { + span_lint_and_sugg( + cx, + BEVY_PLATFORM_ALTERNATIVE_EXISTS, + hir_ty.span, + BEVY_PLATFORM_ALTERNATIVE_EXISTS.desc, + format!( + "the type `{}` can be replaced with the `no_std` compatible type {}", + snippet(cx.tcx.sess, hir_ty.span, ""), + bevy_platform_alternative.full_path() + ), + bevy_platform_alternative.full_path().to_string(), + Applicability::MachineApplicable, + ); + } + } +} + +/// Creates an enum containing all the types form `bevy_platform` as variants. +/// +/// # Example +/// +/// ```ignore +/// declare_bevy_platform_types! { +/// // The variant name => [`PathLookup`] that matches the equivalent type in the std. +/// CustomType => CUSTOMTYPE, +/// // Optional the module path can be passed too, default is `bevy::platform::`. +/// // If an additional module path is added, it will result in: `bevy::platform::custom::thread::CustomType`. +/// CustomType("custom::thread") => BARRIER, +/// ``` +macro_rules! declare_bevy_platform_types { + ( + $( + $variant:ident $(($mod_path:expr))? => $path:ident + ) + ,+$(,)? + ) => { + #[derive(Copy, Clone)] + pub enum BevyPlatformType { + $( + $variant, + )+ + } + + impl BevyPlatformType{ + /// Try to create a [`BevyPlatformType`] from the given [`Ty`]. + pub fn try_from_ty(cx: &LateContext<'_>, ty: rustc_middle::ty::Ty<'_>) -> Option { + use crate::paths::bevy_platform_types::*; + $( + if $path.matches_ty(cx, ty) { + Some(Self::$variant) + } else + )+ + { + None + } + } + + ///Returns a string identifying this [`BevyPlatformType`]. This string is suitable for user output. + pub const fn full_path(&self) -> &'static str { + match self { + $(Self::$variant => concat!("bevy::platform", $("::", $mod_path,)? "::" , stringify!($variant)),)+ + } + } + } + }; +} + +declare_bevy_platform_types! { + Barrier("sync") => BARRIER, + BarrierWaitResult("sync") => BARRIERWAITRESULT, + HashMap("collections") => HASHMAP, + HashSet("collections") => HASHSET, + Instant("time") => INSTANT, + LazyLock("sync") => LAZYLOCK, + LockResult("sync") => LOCKRESULT, + Mutex("sync") => MUTEX, + MutexGuard("sync") => MUTEXGUARD, + Once("sync")=> ONCE, + OnceLock("sync") => ONCELOCK, + OnceState("sync") => ONCESTATE, + PoisonError("sync") => POISONERROR, + RwLock("sync") => RWLOCK, + RwLockReadGuard("sync") => RWLOCKREADGUARD, + RwLockWriteGuard("sync") => RWLOCKWRITEGUARD, + SyncCell("sync") => SYNCCELL, + SyncUnsafeCell("cell") => SYNCUNSAFECELL, + TryLockError("sync") => TRYLOCKERROR, + TryLockResult("sync") => TRYLOCKRESULT, +} diff --git a/bevy_lint/src/lints/style/mod.rs b/bevy_lint/src/lints/style/mod.rs index f7a9fc6b..904522ce 100644 --- a/bevy_lint/src/lints/style/mod.rs +++ b/bevy_lint/src/lints/style/mod.rs @@ -8,6 +8,7 @@ use rustc_lint::{Level, Lint, LintStore}; use crate::lint::LintGroup; +pub mod bevy_platform_alternative_exists; pub mod unconventional_naming; pub(crate) struct Style; @@ -15,9 +16,15 @@ pub(crate) struct Style; impl LintGroup for Style { const NAME: &str = "bevy::style"; const LEVEL: Level = Level::Warn; - const LINTS: &[&Lint] = &[unconventional_naming::UNCONVENTIONAL_NAMING]; + const LINTS: &[&Lint] = &[ + bevy_platform_alternative_exists::BEVY_PLATFORM_ALTERNATIVE_EXISTS, + unconventional_naming::UNCONVENTIONAL_NAMING, + ]; fn register_passes(store: &mut LintStore) { + store.register_late_pass(|_| { + Box::new(bevy_platform_alternative_exists::BevyPlatformAlternativeExists) + }); store.register_late_pass(|_| Box::new(unconventional_naming::UnconventionalNaming)); } } diff --git a/bevy_lint/src/paths.rs b/bevy_lint/src/paths.rs index 9ed21c38..b816f42c 100644 --- a/bevy_lint/src/paths.rs +++ b/bevy_lint/src/paths.rs @@ -86,3 +86,32 @@ pub static UPDATE: PathLookup = type_path!(bevy_app::main_schedule::Update); pub static WITH: PathLookup = type_path!(bevy_ecs::query::filter::With); /// pub static WORLD: PathLookup = type_path!(bevy_ecs::world::World); + +// All the paths that represent the `bevy_platform` types. +// Keep the following list alphabetically sorted :) in neovim, use `:sort/\vpub static \w+/ ri` +pub mod bevy_platform_types { + use clippy_utils::paths::{PathLookup, PathNS}; + + use crate::sym; + + pub static BARRIER: PathLookup = type_path!(std::sync::Barrier); + pub static BARRIERWAITRESULT: PathLookup = type_path!(std::sync::BarrierWaitResult); + pub static HASHMAP: PathLookup = type_path!(std::collections::HashMap); + pub static HASHSET: PathLookup = type_path!(std::collections::HashSet); + pub static INSTANT: PathLookup = type_path!(std::time::Instant); + pub static LAZYLOCK: PathLookup = type_path!(std::sync::LazyLock); + pub static LOCKRESULT: PathLookup = type_path!(std::sync::LockResult); + pub static MUTEX: PathLookup = type_path!(std::sync::Mutex); + pub static MUTEXGUARD: PathLookup = type_path!(std::sync::MutexGuard); + pub static ONCE: PathLookup = type_path!(std::sync::Once); + pub static ONCELOCK: PathLookup = type_path!(std::sync::OnceLock); + pub static ONCESTATE: PathLookup = type_path!(std::sync::OnceState); + pub static POISONERROR: PathLookup = type_path!(std::sync::PoisonError); + pub static RWLOCK: PathLookup = type_path!(std::sync::RwLock); + pub static RWLOCKREADGUARD: PathLookup = type_path!(std::sync::RwLockReadGuard); + pub static RWLOCKWRITEGUARD: PathLookup = type_path!(std::sync::RwLockWriteGuard); + pub static SYNCCELL: PathLookup = type_path!(std::sync::Exclusive); + pub static SYNCUNSAFECELL: PathLookup = type_path!(std::cell::SyncUnsafeCell); + pub static TRYLOCKERROR: PathLookup = type_path!(std::sync::TryLockError); + pub static TRYLOCKRESULT: PathLookup = type_path!(std::sync::TryLockResult); +} diff --git a/bevy_lint/src/sym.rs b/bevy_lint/src/sym.rs index bd88efb5..79552e1e 100644 --- a/bevy_lint/src/sym.rs +++ b/bevy_lint/src/sym.rs @@ -57,7 +57,10 @@ use clippy_utils::sym::EXTRA_SYMBOLS as CLIPPY_SYMBOLS; /// These are symbols that we use but are already interned by either the compiler or Clippy. pub use clippy_utils::sym::filter; -pub use rustc_span::sym::{bevy_ecs, bundle, message, plugin, reflect}; +pub use rustc_span::sym::{ + HashMap, HashSet, Instant, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard, + SyncUnsafeCell, bevy_ecs, bundle, message, plugin, reflect, std, sync, +}; use rustc_span::{Symbol, symbol::PREDEFINED_SYMBOLS_COUNT}; /// The starting offset used for the first Bevy-specific symbol. @@ -114,26 +117,30 @@ macro_rules! declare_bevy_symbols { // Before adding a new symbol here, check that it doesn't exist yet in `rustc_span::sym` or // `clippy_utils::sym`. Having duplicate symbols will cause the compiler to ICE! Also please keep -// this list alphabetically sorted :) +// this list alphabetically sorted :) (use `:sort i` in nvim) declare_bevy_symbols! { add_systems, app, App, + Barrier, + BarrierWaitResult, + bevy, bevy_app, bevy_camera, bevy_ptr, bevy_reflect, - bevy, Bundle, camera, Camera, + cell, change_detection, + collections, commands, Commands, component, Component, - deferred_world, Deferred, + deferred_world, DeferredWorld, entity_ref, EntityCommands, @@ -141,11 +148,13 @@ declare_bevy_symbols! { event, Event, Events, + Exclusive, FilteredEntityMut, FixedUpdate, init_resource, insert_resource, iter_current_update_messages, + LazyLock, main_schedule, Message, Messages, @@ -169,13 +178,21 @@ declare_bevy_symbols! { schedule, set, spawn, - system_param, system, + system_param, SystemSet, + time, Update, With, world, World, + Once, + OnceLock, + OnceState, + LockResult, + PoisonError, + TryLockError, + TryLockResult, } /// Returns a list of strings that should be supplied to diff --git a/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.fixed b/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.fixed new file mode 100644 index 00000000..a21c8430 --- /dev/null +++ b/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.fixed @@ -0,0 +1,28 @@ +#![feature(register_tool)] +#![register_tool(bevy)] +#![deny(bevy::bevy_platform_alternative_exists)] + +#[allow(dead_code)] +struct Player { + attack_cd: bevy::platform::time::Instant, + //~^ ERROR: Used type from the `std` that has an existing alternative from `bevy_platform` + //~| HELP: the type `std::time::Instant` can be replaced with the `no_std` compatible + // type bevy::platform::time::Instant +} + +fn main() { + let mut hash_map = bevy::platform::collections::HashMap::new(); + //~^ ERROR: Used type from the `std` that has an existing alternative from `bevy_platform` + //~| HELP: the type `std::collections::HashMap` can be replaced with the `no_std` compatible + // type bevy::platform::collections::HashMap + hash_map.insert("foo", "bar"); + + let _time = bevy::platform::time::Instant::now(); + //~^ ERROR: Used type from the `std` that has an existing alternative from `bevy_platform` + //~| HELP: the type `std::time::Instant` can be replaced with the `no_std` compatible + // type bevy::platform::time::Instant + + // This should be fine even tho it will result in a `std::time::Instant` after full path + // resolution. + let _bevy_time = bevy::platform::time::Instant::now(); +} diff --git a/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.rs b/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.rs new file mode 100644 index 00000000..fa7948b0 --- /dev/null +++ b/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.rs @@ -0,0 +1,30 @@ +#![feature(register_tool)] +#![register_tool(bevy)] +#![deny(bevy::bevy_platform_alternative_exists)] + +use std::time; + +#[allow(dead_code)] +struct Player { + attack_cd: time::Instant, + //~^ ERROR: Used type from the `std` that has an existing alternative from `bevy_platform` + //~| HELP: the type `std::time::Instant` can be replaced with the `no_std` compatible + // type bevy::platform::time::Instant +} + +fn main() { + let mut hash_map = std::collections::HashMap::new(); + //~^ ERROR: Used type from the `std` that has an existing alternative from `bevy_platform` + //~| HELP: the type `std::collections::HashMap` can be replaced with the `no_std` compatible + // type bevy::platform::collections::HashMap + hash_map.insert("foo", "bar"); + + let _time = std::time::Instant::now(); + //~^ ERROR: Used type from the `std` that has an existing alternative from `bevy_platform` + //~| HELP: the type `std::time::Instant` can be replaced with the `no_std` compatible + // type bevy::platform::time::Instant + + // This should be fine even tho it will result in a `std::time::Instant` after full path + // resolution. + let _bevy_time = bevy::platform::time::Instant::now(); +} diff --git a/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.stderr b/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.stderr new file mode 100644 index 00000000..6555bc81 --- /dev/null +++ b/bevy_lint/tests/ui/bevy_platform_alternative_exists/main.stderr @@ -0,0 +1,31 @@ +error: Used type from the `std` that has an existing alternative from `bevy_platform` + --> tests/ui/bevy_platform_alternative_exists/main.rs:16:24 + | +16 | let mut hash_map = std::collections::HashMap::new(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | +note: the lint level is defined here + --> tests/ui/bevy_platform_alternative_exists/main.rs:3:9 + | + 3 | #![deny(bevy::bevy_platform_alternative_exists)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: the type `std::collections::HashMap` can be replaced with the `no_std` compatible type bevy::platform::collections::HashMap + | +16 - let mut hash_map = std::collections::HashMap::new(); +16 + let mut hash_map = bevy::platform::collections::HashMap::new(); + | + +error: Used type from the `std` that has an existing alternative from `bevy_platform` + --> tests/ui/bevy_platform_alternative_exists/main.rs:22:17 + | +22 | let _time = std::time::Instant::now(); + | ^^^^^^^^^^^^^^^^^^ + | +help: the type `std::time::Instant` can be replaced with the `no_std` compatible type bevy::platform::time::Instant + | +22 - let _time = std::time::Instant::now(); +22 + let _time = bevy::platform::time::Instant::now(); + | + +error: aborting due to 2 previous errors +