mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
Adds a dylint library under tooling/lints that flags Zed-specific anti-patterns: * shared_string_from_str_literal, * async_block_without_await, * entity_update_in_render, * notify_in_render, * owned_string_into_shared, * len_in_loop_condition, and * blocking_io_on_foreground. Includes UI tests, a single-lint helper, and workspace.metadata.dylint registration so cargo dylint --all discovers it. The library pins its own nightly toolchain (kept out of the main workspace) and tracks dylint 6. Release Notes: - N/A Self-Review Checklist: - [ ] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [ ] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A or Added/Fixed/Improved ...
171 lines
5.5 KiB
Rust
171 lines
5.5 KiB
Rust
use clippy_utils::diagnostics::span_lint;
|
|
use rustc_ast::ast::LitKind;
|
|
use rustc_hir::def::{DefKind, Res};
|
|
use rustc_hir::{Expr, ExprKind};
|
|
use rustc_lint::{LateContext, LateLintPass};
|
|
use rustc_middle::ty::Ty;
|
|
|
|
rustc_session::declare_lint! {
|
|
/// ### What it does
|
|
///
|
|
/// Flags expressions that build an owned `String` from a string literal
|
|
/// and then immediately convert it with `.into()` into one of the
|
|
/// refcounted/shared string types: `gpui::SharedString`, `Arc<str>`,
|
|
/// `Rc<str>`, or `Cow<'_, str>`.
|
|
///
|
|
/// The flagged shapes are:
|
|
///
|
|
/// ```ignore
|
|
/// let label: SharedString = String::from("foo").into();
|
|
/// let key: Arc<str> = "foo".to_string().into();
|
|
/// let value: Rc<str> = "foo".to_owned().into();
|
|
/// ```
|
|
///
|
|
/// ### Why is this bad?
|
|
///
|
|
/// Two heap allocations and two copies of the literal happen where one is
|
|
/// enough: `String::from` (or `to_string`/`to_owned`) allocates a `String`
|
|
/// and copies the bytes; the `.into()` conversion into the refcounted
|
|
/// destination then allocates an `Arc<str>`-like buffer and copies the
|
|
/// bytes a second time. For string literals the destination can be built
|
|
/// directly from `'static` data with no allocation at all.
|
|
pub OWNED_STRING_INTO_SHARED,
|
|
Warn,
|
|
"an owned `String` is built from a string literal only to be converted into a refcounted string"
|
|
}
|
|
|
|
pub(crate) struct OwnedStringIntoShared;
|
|
|
|
rustc_session::impl_lint_pass!(OwnedStringIntoShared => [OWNED_STRING_INTO_SHARED]);
|
|
|
|
impl<'tcx> LateLintPass<'tcx> for OwnedStringIntoShared {
|
|
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
|
|
if expr.span.from_expansion() {
|
|
return;
|
|
}
|
|
|
|
let ExprKind::MethodCall(segment, receiver, [], _) = &expr.kind else {
|
|
return;
|
|
};
|
|
if segment.ident.name.as_str() != "into" {
|
|
return;
|
|
}
|
|
|
|
let dest_ty = cx.typeck_results().expr_ty(expr);
|
|
if !is_refcounted_string_destination(cx, dest_ty) {
|
|
return;
|
|
}
|
|
|
|
// The receiver must produce an owned `String`. Confirming this rules
|
|
// out custom `into` impls on unrelated types that just happen to look
|
|
// similar.
|
|
let receiver_ty = cx.typeck_results().expr_ty(receiver);
|
|
if !is_std_string(cx, receiver_ty) {
|
|
return;
|
|
}
|
|
|
|
if !is_owned_string_built_from_literal(cx, receiver) {
|
|
return;
|
|
}
|
|
|
|
span_lint(
|
|
cx,
|
|
OWNED_STRING_INTO_SHARED,
|
|
expr.span,
|
|
"this allocates an owned `String` from a string literal only to convert it into a refcounted string",
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Returns `true` when `ty` is one of the refcounted/shared string types this
|
|
/// lint targets: `gpui::SharedString`, `Arc<str>`, `Rc<str>`, or
|
|
/// `Cow<'_, str>`.
|
|
fn is_refcounted_string_destination<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
|
|
let Some(adt) = ty.ty_adt_def() else {
|
|
return false;
|
|
};
|
|
let did = adt.did();
|
|
|
|
if cx.tcx.crate_name(did.krate).as_str() == "gpui_shared_string"
|
|
&& cx.tcx.item_name(did).as_str() == "SharedString"
|
|
{
|
|
return true;
|
|
}
|
|
|
|
let path = cx.tcx.def_path_str(did);
|
|
let is_str_wrapper = matches!(
|
|
path.as_str(),
|
|
"alloc::sync::Arc"
|
|
| "std::sync::Arc"
|
|
| "alloc::rc::Rc"
|
|
| "std::rc::Rc"
|
|
| "alloc::borrow::Cow"
|
|
| "std::borrow::Cow"
|
|
);
|
|
if !is_str_wrapper {
|
|
return false;
|
|
}
|
|
|
|
let rustc_middle::ty::TyKind::Adt(_, args) = ty.kind() else {
|
|
return false;
|
|
};
|
|
args.iter()
|
|
.find_map(|arg| arg.as_type())
|
|
.is_some_and(|inner| inner.is_str())
|
|
}
|
|
|
|
/// Returns `true` when `ty` is `alloc::string::String`.
|
|
fn is_std_string(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
|
|
let Some(adt) = ty.ty_adt_def() else {
|
|
return false;
|
|
};
|
|
let path = cx.tcx.def_path_str(adt.did());
|
|
path == "alloc::string::String" || path == "std::string::String"
|
|
}
|
|
|
|
/// Returns `true` if `expr` matches one of:
|
|
///
|
|
/// * `String::from(<string literal>)`
|
|
/// * `<string literal>.to_string()`
|
|
/// * `<string literal>.to_owned()`
|
|
fn is_owned_string_built_from_literal<'tcx>(
|
|
cx: &LateContext<'tcx>,
|
|
expr: &'tcx Expr<'tcx>,
|
|
) -> bool {
|
|
match &expr.kind {
|
|
ExprKind::Call(func, [arg]) => {
|
|
let ExprKind::Path(qpath) = &func.kind else {
|
|
return false;
|
|
};
|
|
let Res::Def(DefKind::Fn | DefKind::AssocFn, def_id) = cx.qpath_res(qpath, func.hir_id)
|
|
else {
|
|
return false;
|
|
};
|
|
if cx.tcx.item_name(def_id).as_str() != "from" {
|
|
return false;
|
|
}
|
|
is_string_literal(arg)
|
|
}
|
|
ExprKind::MethodCall(segment, receiver, [], _) => {
|
|
let name = segment.ident.name.as_str();
|
|
if name != "to_string" && name != "to_owned" {
|
|
return false;
|
|
}
|
|
is_string_literal(receiver)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if `expr` is a string-literal expression, optionally wrapped
|
|
/// in a single layer of reference (`&"lit"`).
|
|
fn is_string_literal<'tcx>(expr: &'tcx Expr<'tcx>) -> bool {
|
|
let inner = match &expr.kind {
|
|
ExprKind::AddrOf(_, _, inner) => *inner,
|
|
_ => expr,
|
|
};
|
|
matches!(
|
|
&inner.kind,
|
|
ExprKind::Lit(lit) if matches!(lit.node, LitKind::Str(..))
|
|
)
|
|
}
|