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 ...
247 lines
8.1 KiB
Rust
247 lines
8.1 KiB
Rust
use clippy_utils::diagnostics::span_lint;
|
|
use rustc_hir::def::Res;
|
|
use rustc_hir::{Expr, ExprKind, HirId, Node};
|
|
use rustc_lint::{LateContext, LateLintPass};
|
|
use rustc_middle::ty::Ty;
|
|
|
|
use crate::render_helpers::is_directly_in_render_method;
|
|
|
|
rustc_session::declare_lint! {
|
|
/// ### What it does
|
|
///
|
|
/// Flags calls to known blocking IO functions from the standard library
|
|
/// (`std::fs`, `std::thread::sleep`, `std::process::Command`, `std::net`)
|
|
/// when they appear inside a function that receives a synchronous GPUI
|
|
/// context parameter (`&App`, `&mut App`, `&Context<T>`,
|
|
/// `&mut Context<T>`, `&mut Window`) or directly inside a
|
|
/// `Render::render` / `RenderOnce::render` method.
|
|
///
|
|
/// ### Why is this bad?
|
|
///
|
|
/// In GPUI, code that receives a synchronous context type runs on the
|
|
/// foreground (UI) thread. A blocking IO call on this thread freezes the
|
|
/// application until the syscall returns.
|
|
pub BLOCKING_IO_ON_FOREGROUND,
|
|
Warn,
|
|
"blocking IO call on the GPUI foreground thread"
|
|
}
|
|
|
|
pub(crate) struct BlockingIoOnForeground;
|
|
|
|
rustc_session::impl_lint_pass!(BlockingIoOnForeground => [BLOCKING_IO_ON_FOREGROUND]);
|
|
|
|
const BLOCKING_FN_PATHS: &[&str] = &[
|
|
// std::fs free functions
|
|
"std::fs::read",
|
|
"std::fs::read_to_string",
|
|
"std::fs::write",
|
|
"std::fs::read_dir",
|
|
"std::fs::read_link",
|
|
"std::fs::metadata",
|
|
"std::fs::symlink_metadata",
|
|
"std::fs::set_permissions",
|
|
"std::fs::canonicalize",
|
|
"std::fs::create_dir",
|
|
"std::fs::create_dir_all",
|
|
"std::fs::remove_file",
|
|
"std::fs::remove_dir",
|
|
"std::fs::remove_dir_all",
|
|
"std::fs::copy",
|
|
"std::fs::rename",
|
|
"std::fs::hard_link",
|
|
// std::fs::File associated functions
|
|
"std::fs::File::open",
|
|
"std::fs::File::create",
|
|
"std::fs::File::create_new",
|
|
// std::thread
|
|
"std::thread::sleep",
|
|
// std::path::Path methods (resolved via method call def_id)
|
|
"std::path::Path::metadata",
|
|
"std::path::Path::symlink_metadata",
|
|
"std::path::Path::read_link",
|
|
"std::path::Path::read_dir",
|
|
"std::path::Path::exists",
|
|
"std::path::Path::try_exists",
|
|
"std::path::Path::is_file",
|
|
"std::path::Path::is_dir",
|
|
"std::path::Path::is_symlink",
|
|
"std::path::Path::canonicalize",
|
|
// std::net associated functions
|
|
"std::net::TcpStream::connect",
|
|
"std::net::TcpStream::connect_timeout",
|
|
"std::net::TcpListener::bind",
|
|
"std::net::UdpSocket::bind",
|
|
];
|
|
|
|
const BLOCKING_METHODS: &[(&str, &str)] = &[
|
|
// std::process
|
|
("Command", "output"),
|
|
("Command", "status"),
|
|
("Command", "spawn"),
|
|
("Child", "wait"),
|
|
("Child", "wait_with_output"),
|
|
// std::fs::File instance methods
|
|
("File", "sync_all"),
|
|
("File", "sync_data"),
|
|
("File", "set_len"),
|
|
("File", "metadata"),
|
|
("File", "try_clone"),
|
|
("File", "set_permissions"),
|
|
// std::net — TCP
|
|
("TcpStream", "connect"),
|
|
("TcpStream", "peek"),
|
|
("TcpListener", "bind"),
|
|
("TcpListener", "accept"),
|
|
("TcpListener", "incoming"),
|
|
// std::net — UDP
|
|
("UdpSocket", "send"),
|
|
("UdpSocket", "send_to"),
|
|
("UdpSocket", "recv"),
|
|
("UdpSocket", "recv_from"),
|
|
("UdpSocket", "peek"),
|
|
("UdpSocket", "peek_from"),
|
|
// std::sync
|
|
("Mutex", "lock"),
|
|
("RwLock", "read"),
|
|
("RwLock", "write"),
|
|
("Condvar", "wait"),
|
|
("Condvar", "wait_timeout"),
|
|
("Condvar", "wait_while"),
|
|
("Barrier", "wait"),
|
|
// std::sync::mpsc
|
|
("Receiver", "recv"),
|
|
("Receiver", "recv_timeout"),
|
|
("SyncSender", "send"),
|
|
];
|
|
|
|
fn is_blocking_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
|
|
match &expr.kind {
|
|
ExprKind::Call(callee, _) => {
|
|
if let ExprKind::Path(qpath) = &callee.kind {
|
|
if let Res::Def(_, def_id) = cx.qpath_res(qpath, callee.hir_id) {
|
|
let path = cx.tcx.def_path_str(def_id);
|
|
return BLOCKING_FN_PATHS.iter().any(|blocked| path == *blocked);
|
|
}
|
|
}
|
|
false
|
|
}
|
|
ExprKind::MethodCall(segment, receiver, _args, _span) => {
|
|
if let Some(def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) {
|
|
let path = cx.tcx.def_path_str(def_id);
|
|
if BLOCKING_FN_PATHS.iter().any(|blocked| path == *blocked) {
|
|
return true;
|
|
}
|
|
}
|
|
let method_name = segment.ident.name.as_str();
|
|
if !BLOCKING_METHODS
|
|
.iter()
|
|
.any(|(_, name)| *name == method_name)
|
|
{
|
|
return false;
|
|
}
|
|
let receiver_ty = cx.typeck_results().expr_ty(receiver).peel_refs();
|
|
if let Some(adt) = receiver_ty.ty_adt_def() {
|
|
let type_name = cx.tcx.item_name(adt.did());
|
|
return BLOCKING_METHODS
|
|
.iter()
|
|
.any(|(ty, name)| *name == method_name && type_name.as_str() == *ty);
|
|
}
|
|
false
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if `ty` (after peeling references) is a synchronous GPUI
|
|
/// foreground type: `App`, `Context`, or `Window`.
|
|
fn is_gpui_foreground_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
|
|
let peeled = ty.peel_refs();
|
|
let Some(adt) = peeled.ty_adt_def() else {
|
|
return false;
|
|
};
|
|
let did = adt.did();
|
|
let crate_name = cx.tcx.crate_name(did.krate);
|
|
if crate_name.as_str() != "gpui" {
|
|
return false;
|
|
}
|
|
let name = cx.tcx.item_name(did);
|
|
matches!(name.as_str(), "App" | "Context" | "Window")
|
|
}
|
|
|
|
/// Walks up the HIR parent chain from `hir_id` to find the enclosing
|
|
/// function. Returns `true` if that function has a parameter whose type is a
|
|
/// synchronous GPUI context type. Returns `false` if a closure boundary is
|
|
/// crossed first (the closure might run on a background thread).
|
|
fn is_in_foreground_fn(cx: &LateContext<'_>, hir_id: HirId) -> bool {
|
|
for (_parent_id, node) in cx.tcx.hir_parent_iter(hir_id) {
|
|
match node {
|
|
Node::Expr(expr) if matches!(expr.kind, ExprKind::Closure(_)) => {
|
|
return false;
|
|
}
|
|
Node::Item(item) => {
|
|
if let rustc_hir::ItemKind::Fn { .. } = &item.kind {
|
|
let owner_id = item.owner_id.def_id;
|
|
return owner_has_foreground_param(cx, owner_id);
|
|
}
|
|
return false;
|
|
}
|
|
Node::ImplItem(impl_item) => {
|
|
if let rustc_hir::ImplItemKind::Fn(_, _) = &impl_item.kind {
|
|
let owner_id = impl_item.owner_id.def_id;
|
|
return owner_has_foreground_param(cx, owner_id);
|
|
}
|
|
return false;
|
|
}
|
|
Node::TraitItem(trait_item) => {
|
|
if let rustc_hir::TraitItemKind::Fn(_, _) = &trait_item.kind {
|
|
let owner_id = trait_item.owner_id.def_id;
|
|
return owner_has_foreground_param(cx, owner_id);
|
|
}
|
|
return false;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Checks whether the function identified by `local_def_id` has any parameter
|
|
/// whose type is a synchronous GPUI foreground type.
|
|
fn owner_has_foreground_param(
|
|
cx: &LateContext<'_>,
|
|
local_def_id: rustc_hir::def_id::LocalDefId,
|
|
) -> bool {
|
|
let def_id = local_def_id.to_def_id();
|
|
let sig = cx.tcx.fn_sig(def_id).instantiate_identity();
|
|
sig.inputs()
|
|
.skip_binder()
|
|
.iter()
|
|
.any(|ty| is_gpui_foreground_type(cx, *ty))
|
|
}
|
|
|
|
impl<'tcx> LateLintPass<'tcx> for BlockingIoOnForeground {
|
|
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
|
|
if expr.span.from_expansion() {
|
|
return;
|
|
}
|
|
|
|
if !is_blocking_call(cx, expr) {
|
|
return;
|
|
}
|
|
|
|
let in_render = is_directly_in_render_method(cx, expr.hir_id);
|
|
let in_foreground_fn = is_in_foreground_fn(cx, expr.hir_id);
|
|
|
|
if !in_render && !in_foreground_fn {
|
|
return;
|
|
}
|
|
|
|
span_lint(
|
|
cx,
|
|
BLOCKING_IO_ON_FOREGROUND,
|
|
expr.span,
|
|
"blocking IO call on the GPUI foreground thread",
|
|
);
|
|
}
|
|
}
|