Repository navigation
New lint: Suggests using Cell<T: Copy> instead of RefCell<T: Copy>
#17522
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
Open
qdot3
wants to merge
16
commits into
rust-lang:master
Choose a base branch
from
qdot3:refcell_cell
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
9f357db
add tests
qdot3 732e11c
impl logic
qdot3 4b1a6a7
improve let-stmt pattern
qdot3 9803d77
add tests
qdot3 68e6d3e
write doc
qdot3 71c45a2
configure lint
qdot3 a8cc347
fix `*.fixed` file
qdot3 2fd2ac5
fix example
qdot3 f943fcd
fix simple ctor pattern
qdot3 453d1d4
suppress the lint for large `Copy` types
qdot3 4af35b8
Lint `&RefCell` usage except in return types
qdot3 734b20c
simple usage analysis: exclude local variables that is used
qdot3 424b7f1
config lint
qdot3 95c7b73
refactor
qdot3 d9f03d5
handle unwrap error
qdot3 e6d57f5
fix conf
qdot3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,329 @@ | ||
| use std::ops::ControlFlow::{Break, Continue}; | ||
|
|
||
| use clippy_config::Conf; | ||
| use clippy_utils::diagnostics::span_lint_and_sugg; | ||
| use clippy_utils::res::{MaybeDef as _, MaybeQPath as _, MaybeResPath as _}; | ||
| use clippy_utils::source::{snippet_with_applicability, snippet_with_context}; | ||
| use clippy_utils::ty::{approx_ty_size, is_copy, ty_from_hir_ty}; | ||
| use clippy_utils::visitors::for_each_local_use_after_expr; | ||
| use clippy_utils::{ExprUseNode, expr_use_sites, is_trait_impl_item, qpath_generic_tys, sym}; | ||
| use rustc_errors::Applicability; | ||
| use rustc_hir::def_id::LocalDefId; | ||
| use rustc_hir::intravisit::FnKind; | ||
| use rustc_hir::{ | ||
| self, Body, Expr, ExprKind, FieldDef, FnDecl, FnRetTy, GenericArg, ImplItem, ImplItemKind, Item, ItemKind, Pat, | ||
| QPath, Stmt, StmtKind, TraitItem, TraitItemKind, Ty, TyKind, | ||
| }; | ||
| use rustc_lint::{LateContext, LateLintPass}; | ||
| use rustc_middle::ty; | ||
| use rustc_session::impl_lint_pass; | ||
| use rustc_span::Span; | ||
| use rustc_span::sym::RefCell; | ||
|
|
||
| declare_clippy_lint! { | ||
| /// ### What it does | ||
| /// This lint suggests using `Cell` for `Copy` types instead of `RefCell`. | ||
| /// | ||
| /// ### Why is this bad? | ||
| /// `RefCell` avoids cloning at the cost of additional memory usage and | ||
| /// instructions, which isn't worth it for **small** `Copy` types. | ||
| /// | ||
| /// ### Known problems | ||
| /// `RefCell` might be useful for **large** `Copy` types. | ||
| /// See <https://manishearth.github.io/blog/2015/05/27/wrapper-types-in-rust-choosing-your-guarantees/#cell-types> for details. | ||
| /// | ||
| /// ### Example | ||
| /// ```no_run | ||
| /// use std::cell::RefCell; | ||
| /// | ||
| /// struct Counter(RefCell<i32>); | ||
| /// | ||
| /// let _ = RefCell::new(1); | ||
| /// let _ = RefCell::<i32>::default(); | ||
| /// ``` | ||
| /// Use instead: | ||
| /// ```no_run | ||
| /// use std::cell::Cell; | ||
| /// | ||
| /// struct Counter(Cell<i32>); | ||
| /// | ||
| /// let _ = Cell::new(1); | ||
| /// let _ = Cell::<i32>::default(); | ||
| /// ``` | ||
| #[clippy::version = "1.99.0"] | ||
| pub REFCELL_CELL, | ||
| perf, | ||
| "using a `RefCell` for a `Copy` type" | ||
| } | ||
|
|
||
| impl_lint_pass!(RefcellCell => [REFCELL_CELL]); | ||
|
|
||
| pub struct RefcellCell { | ||
| max_cheap_copy_size: u64, | ||
| } | ||
|
|
||
| impl RefcellCell { | ||
| pub fn new(conf: &'static Conf) -> Self { | ||
| Self { | ||
| max_cheap_copy_size: conf.max_cheap_copy_size, | ||
| } | ||
| } | ||
|
|
||
| fn emit_refcell_copy_def<'tcx>(&self, cx: &LateContext<'tcx>, hir_ty: &'tcx Ty<'tcx>, no_ref: bool) { | ||
| match hir_ty.kind { | ||
| TyKind::Array(hir_ty, _) | TyKind::Slice(hir_ty) => { | ||
| self.emit_refcell_copy_def(cx, hir_ty, no_ref); | ||
| }, | ||
| TyKind::Tup(hir_tys) => { | ||
| for hir_ty in hir_tys { | ||
| self.emit_refcell_copy_def(cx, hir_ty, no_ref); | ||
| } | ||
| }, | ||
| TyKind::Ref(_, mut_ty) if !no_ref => { | ||
| self.emit_refcell_copy_def(cx, mut_ty.ty, no_ref); | ||
| }, | ||
| TyKind::Path(qpath) | ||
| if let (None, Some(path)) = qpath.opt_res_path() | ||
| // RefCell<T> | ||
| && let [segment] = path.segments | ||
| && segment.res.is_diag_item(&cx.tcx, RefCell) | ||
| && let Some(args) = segment.args | ||
| && let [GenericArg::Type(hir_ty)] = args.args | ||
| // `T` is copyable and small | ||
| && { | ||
| let ty = ty_from_hir_ty(cx, hir_ty.as_unambig_ty()); | ||
| is_copy(cx, ty) && approx_ty_size(cx, ty) <= self.max_cheap_copy_size | ||
| } | ||
| // Be conservative about macro | ||
| && !hir_ty.span.from_expansion() => | ||
| { | ||
| let mut app = Applicability::MaybeIncorrect; | ||
| let sugg = { | ||
| let hir_ty = snippet_with_applicability(cx, hir_ty.span, "_", &mut app); | ||
| format!("Cell<{hir_ty}>") | ||
| }; | ||
| span_lint_and_sugg( | ||
| cx, | ||
| REFCELL_CELL, | ||
| path.span, | ||
| "using a `RefCell` for a `Copy` type", | ||
| "try", | ||
| sugg, | ||
| app, | ||
| ); | ||
| }, | ||
| _ => (), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<'tcx> LateLintPass<'tcx> for RefcellCell { | ||
| fn check_field_def(&mut self, cx: &LateContext<'tcx>, def: &'tcx FieldDef<'tcx>) { | ||
| if !def.span.from_expansion() { | ||
| self.emit_refcell_copy_def(cx, def.ty, false); | ||
| } | ||
| } | ||
|
|
||
| fn check_fn( | ||
| &mut self, | ||
| cx: &LateContext<'tcx>, | ||
| _: FnKind<'tcx>, | ||
| decl: &'tcx FnDecl<'tcx>, | ||
| _: &'tcx Body<'tcx>, | ||
| span: Span, | ||
| def_id: LocalDefId, | ||
| ) { | ||
| if !span.from_expansion() && !is_trait_impl_item(cx, cx.tcx.local_def_id_to_hir_id(def_id)) { | ||
| for hir_ty in decl.inputs { | ||
| self.emit_refcell_copy_def(cx, hir_ty, false); | ||
| } | ||
| if let FnRetTy::Return(hir_ty) = decl.output { | ||
| self.emit_refcell_copy_def(cx, hir_ty, true); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'tcx>) { | ||
| if let ImplItemKind::Type(hir_ty) = item.kind | ||
| && !item.span.from_expansion() | ||
| { | ||
| self.emit_refcell_copy_def(cx, hir_ty, false); | ||
| } | ||
| } | ||
|
|
||
| fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { | ||
| if let ItemKind::TyAlias(_, _, hir_ty) = item.kind | ||
| && !item.span.from_expansion() | ||
| { | ||
| self.emit_refcell_copy_def(cx, hir_ty, false); | ||
| } | ||
| } | ||
|
|
||
| fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'tcx>) { | ||
| if let TraitItemKind::Fn(sig, _) = item.kind | ||
| && !item.span.from_expansion() | ||
| { | ||
| for hir_ty in sig.decl.inputs { | ||
| self.emit_refcell_copy_def(cx, hir_ty, false); | ||
| } | ||
| if let FnRetTy::Return(hir_ty) = sig.decl.output { | ||
| self.emit_refcell_copy_def(cx, hir_ty, true); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) { | ||
| if let StmtKind::Let(let_stmt) = stmt.kind | ||
| && !stmt.span.from_expansion() | ||
| && let Some(init) = let_stmt.init | ||
| && !callee_requires_refcell(cx, let_stmt.pat, init) | ||
| // RefCell<T> | ||
| && let Some(ty) = cx.typeck_results().expr_ty_opt(init) | ||
| && let ty::Adt(def, args) = ty.kind() | ||
| && def.is_diag_item(&cx.tcx, RefCell) | ||
| // `T` is a small `Copy` type | ||
| && is_copy(cx, args.type_at(0)) | ||
| && approx_ty_size(cx, args.type_at(0)) <= self.max_cheap_copy_size | ||
| // `init` is a (simple) constructor of `RefCell` | ||
| && let ExprKind::Call(maybe_qpath, ctor_args) = init.kind | ||
| && let Some(kind) = CtorKind::new(cx, maybe_qpath, ctor_args.len()) | ||
| // Be conservative about macro | ||
| && ctor_args.iter().all(|e| !e.span.from_expansion()) | ||
| { | ||
| // Suggestion can be a false positive because trait bounds are not considered. | ||
| let mut app = Applicability::MaybeIncorrect; | ||
|
|
||
| let init_str = { | ||
| let arg = ctor_args | ||
| .first() | ||
| .map_or_default(|arg| snippet_with_context(cx, arg.span, stmt.span.ctxt(), "..", &mut app).0); | ||
| match kind { | ||
| CtorKind::New => format!("Cell::new({arg})"), | ||
| CtorKind::From => format!("Cell::from({arg})"), | ||
| CtorKind::Default => "Cell::default()".to_string(), | ||
| } | ||
| }; | ||
|
|
||
| let (span, sugg) = if let Some(annotation) = let_stmt.ty | ||
| && let TyKind::Path(qpath) = annotation.kind | ||
| && let Some(inner_ty) = qpath_generic_tys(&qpath).next() | ||
| { | ||
| let sugg = { | ||
| let inner_ty = snippet_with_context(cx, inner_ty.span, stmt.span.ctxt(), "_", &mut app).0; | ||
|
|
||
| format!(/* let pat: */ "Cell<{inner_ty}> = {init_str};") | ||
| }; | ||
|
|
||
| (annotation.span.to(init.span), sugg) | ||
| } else { | ||
| let sugg = if let ExprKind::Path(qpath) = maybe_qpath.kind | ||
| // <..>::method -> <..> | ||
| && let Some(ty) = match qpath { | ||
| QPath::Resolved(ty, _) => ty, | ||
| QPath::TypeRelative(ty, _) => Some(ty), | ||
| } | ||
| // (..::)RefCell::<T> -> T | ||
| && let TyKind::Path(qpath) = ty.kind | ||
| && let Some(ty) = qpath_generic_tys(&qpath).next() | ||
| { | ||
| let ty = snippet_with_context(cx, ty.span, stmt.span.ctxt(), "_", &mut app).0; | ||
| init_str.replacen("::", &format!("::<{ty}>::"), 1) | ||
| } else { | ||
| init_str | ||
| }; | ||
|
|
||
| (init.span, sugg) | ||
| }; | ||
|
|
||
| span_lint_and_sugg( | ||
| cx, | ||
| REFCELL_CELL, | ||
| span, | ||
| "using a `RefCell` for a `Copy` type", | ||
| "try", | ||
| sugg, | ||
| app, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| enum CtorKind { | ||
| New, | ||
| From, | ||
| Default, | ||
| } | ||
|
|
||
| impl CtorKind { | ||
| fn new<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, num_args: usize) -> Option<Self> { | ||
| if let Some(def_id) = expr.res(cx).opt_def_id() | ||
| && def_id.is_diag_item(&cx.tcx, sym::from_fn) | ||
| && num_args == 1 | ||
| { | ||
| Some(Self::From) | ||
| } else if let Some(def_id) = expr.res(cx).opt_def_id() | ||
| && def_id.is_diag_item(&cx.tcx, sym::default_fn) | ||
| && num_args == 0 | ||
| { | ||
| Some(Self::Default) | ||
| } else if let ExprKind::Path(QPath::TypeRelative(hir_ty, segment)) = expr.kind | ||
| && segment.ident.name == sym::new | ||
| && hir_ty.res(cx).is_diag_item(&cx.tcx, RefCell) | ||
| && num_args == 1 | ||
| { | ||
| Some(Self::New) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Checks whether the given local variable must be a `RefCell`. | ||
| /// | ||
| /// This performs only simple analysis because a full analysis would be too expensive. | ||
| /// Trait bounds and indirect uses are not considered. | ||
| fn callee_requires_refcell<'tcx>(cx: &LateContext<'tcx>, pat: &'tcx Pat<'tcx>, init: &'tcx Expr<'tcx>) -> bool { | ||
| for_each_local_use_after_expr(cx, pat.hir_id, init.hir_id, |expr| { | ||
| let Some(use_site) = expr_use_sites(cx.tcx, cx.typeck_results(), expr.span.ctxt(), expr) | ||
| // Ignore `&`s because we want to know whether `expr` is passed to a function. | ||
| .find(|use_site| !matches!(use_site.use_node(cx), ExprUseNode::AddrOf(..))) | ||
| else { | ||
| return Continue(()); | ||
| }; | ||
|
|
||
| let (def_id, i) = match use_site.use_node(cx) { | ||
| ExprUseNode::FnArg(path, i) if let Some(def_id) = path.res(cx).opt_def_id() => (def_id, i), | ||
| ExprUseNode::MethodArg(hir_id, _, i) | ||
| if let Some(def_id) = cx.typeck_results().type_dependent_def_id(hir_id) => | ||
| { | ||
| (def_id, i) | ||
| }, | ||
| ExprUseNode::Callee => return Break(()), | ||
| _ => return Continue(()), | ||
| }; | ||
|
|
||
| // Does callee require `RefCell`? | ||
| let Some(input_ty) = cx | ||
| .tcx | ||
| .fn_sig(def_id) | ||
| // Inspect the type as declared by the callee, without normalization. | ||
| .instantiate_identity() | ||
| .skip_norm_wip() | ||
| .skip_binder() | ||
| .inputs() | ||
| .get(i) | ||
| else { | ||
| // FIXME: This arm seems unreachable but is required to avoid a panic from | ||
| // an `Option::unwrap` inside `FnSig::input` when linting some crates on Linux. | ||
| // Why? | ||
| return Break(()); | ||
| }; | ||
|
|
||
| if input_ty.peel_refs().is_diag_item(&cx.tcx, RefCell) { | ||
| Break(()) | ||
| } else { | ||
| Continue(()) | ||
| } | ||
| }) | ||
| .is_break() | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| max-cheap-copy-size = 64 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are some
Known problemsregarding trait bounds as noted in the tests, can we document them here?Also, we need to note that while on avarage this is a good idea, there might be better options..
https://manishearth.github.io/blog/2015/05/27/wrapper-types-in-rust-choosing-your-guarantees/
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I documented it and introduced new configuration named
max_cheap_copy_sizeto suppress the lint for largeCopytypes. The default value is16, witch is the size ofu/i128.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did you come up/reason that this should be 16? (It might be right, but I'd like to know how you came up with it) 🤔
Can you add that either as a comment or into the PR description since otherwise it is not very clear.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, I chose it simply because it's the minimum size required to represent all primitive integer types.
But the word size is better, since it usually fits in a cache line. And since this is aIt doesn't seem bad, considering typical cache line sizes.perflint, performance should take priority over avoiding extra configuration.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you post the benchmarking code?
Cache line size might be fairly different on some of the arch that are supported...
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood. I'll do it. Since this is my first time running a benchmark on mahines othre than my own, it may take me a while.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to do full benchmarkign on all platforms, just at least one and if you need help with other platforms, people are avaliable for pining on zulip 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the wait. I did benchmarking on my machine. Before trying other machines on GitHub Actions, could you review my benchmarking approach?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the link seems to be dead. where is the benchmarking setup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, here.