#![feature(rustc_private)]
#![warn(unused_extern_crates)]
dylint_linting::dylint_library!();
extern crate rustc_ast;
extern crate rustc_errors;
extern crate rustc_hir;
extern crate rustc_lint;
extern crate rustc_middle;
extern crate rustc_session;
extern crate rustc_span;
mod diag;
mod lints;
mod utils;
use lints::{combinators, exprs, fn_sigs, items, sql};
use rustc_lint::{LateContext, LateLintPass, LintStore};
use rustc_session::{Session, declare_lint, declare_lint_pass};
declare_lint! {
pub NO_ASYNC_FN_IN_EFFECT_CRATE,
Warn,
"use `fn -> Effect<A, E, R>` + `from_async` instead of `async fn`"
}
declare_lint! {
pub NO_RESULT_RETURN_IN_PUB_FN,
Warn,
"public functions must return `Effect<A, E, R>` rather than `Result<T, E>`"
}
declare_lint! {
pub EFFECT_FN_REQUIRES_WHERE_CLAUSE,
Warn,
"Effect-returning functions with generic parameters must have a `where` clause"
}
declare_lint! {
pub EFFECT_ERROR_BOUND_MUST_USE_FROM,
Warn,
"`E` in Effect-returning functions must include `E: From<ConcreteError>` bounds"
}
declare_lint! {
pub NO_CONCRETE_CONTEXT_IN_PUB_API,
Warn,
"public APIs must not expose a 0.6 `Context<Cons<…>>` as `R`; spell `R` with `caps!(Key, …)`"
}
declare_lint! {
pub NO_CONCRETE_ENV_IN_PUB_API,
Warn,
"public APIs must not expose a 0.6 `Env` as `R`; spell `R` with `caps!(Key, …)`"
}
declare_lint! {
pub PREFER_NEEDS_X_OVER_RAW_GET_BOUNDS,
Warn,
"use `caps!(Key, …)` / `R: Selector<Key, I>` instead of 0.6 `Get<Key, Here, Target = T>` bounds"
}
declare_lint! {
pub NO_EFFECT_SUFFIX_ON_GRAPH_BUILDERS,
Warn,
"Effect-returning functions must not have the `_effect` suffix"
}
declare_lint! {
pub BLOCKING_SUFFIX_RESERVED_FOR_RUNNERS,
Warn,
"`*_blocking` names must call `block_on` internally; not return `Effect`"
}
declare_lint! {
pub MULTI_STEP_MUST_USE_EFFECT_MACRO,
Warn,
"chains of 2+ `.flat_map` calls should use `effect!({ ~… })` do-notation"
}
declare_lint! {
pub NO_AWAIT_OUTSIDE_FROM_ASYNC,
Warn,
"`.await` must only appear inside `async` blocks passed to `from_async`"
}
declare_lint! {
pub RUN_BLOCKING_ONLY_AT_PROGRAM_EDGE,
Warn,
"`block_on`/`try_block_on` must only be called from binary entry points"
}
declare_lint! {
pub TESTS_MUST_USE_RUN_TEST,
Allow,
"time-dependent test effects should run on a `TestExecutor` (virtual time), not `block_on`"
}
declare_lint! {
pub NO_TOKIO_SPAWN,
Warn,
"use `.fork()` / `Runtime::fork` instead of `tokio::spawn`"
}
declare_lint! {
pub NO_THREAD_SPAWN,
Warn,
"use `.fork()` / `FiberHandle` instead of `std::thread::spawn`"
}
declare_lint! {
pub NO_STATIC_SERVICE_GLOBALS,
Warn,
"service types must not live in `static` globals; list them in `R` (`caps!(Key, …)`) and provide them with a `Layer`"
}
declare_lint! {
pub NO_THREAD_LOCAL_FOR_REQUEST_CONTEXT,
Warn,
"use `FiberRef` for fiber-scoped state instead of `thread_local!`"
}
declare_lint! {
pub PROVIDE_AT_EDGE_NOT_IN_LIBRARY,
Warn,
"library functions must not call `.provide(…)`; provide at the program edge"
}
declare_lint! {
pub NO_NESTED_RESULT_IN_PUB_API,
Warn,
"avoid `Result<Result<_,_>,_>` in public APIs; use `Or<E1,E2>` or flat errors"
}
declare_lint! {
pub NO_ERROR_COLLAPSE_TO_STRING,
Warn,
"avoid `Err(format!(…))` / `Err(e.to_string())`; use typed error variants"
}
declare_lint! {
pub NO_UNWRAP_INSIDE_EFFECT,
Warn,
"avoid `.unwrap()` / `.expect()` in Effect-returning functions; use typed propagation"
}
declare_lint! {
pub NO_PANIC_INSIDE_EFFECT,
Warn,
"avoid `panic!` / `unreachable!` in Effect-returning functions; use `fail(…)`"
}
declare_lint! {
pub FORK_MUST_BE_JOINED,
Warn,
"every `.fork()` result must be bound and `.join()`-ed or `.interrupt()`-ed"
}
declare_lint! {
pub NO_MUTEX_FOR_FIBER_COORDINATION,
Warn,
"use `SynchronizedRef` or `TRef`/`Stm` (`id_effect_fp`) instead of `Arc<Mutex<…>>` across fibers"
}
declare_lint! {
pub NO_RC_REFCELL_IN_EFFECT_CODE,
Warn,
"`Rc<RefCell<…>>` is unsafe across fibers; use `Ref` / `SynchronizedRef`"
}
declare_lint! {
pub NO_THREAD_SLEEP_IN_DOMAIN,
Warn,
"use `Schedule`-based combinators instead of `std::thread::sleep`"
}
declare_lint! {
pub NO_SYSTEM_TIME_IN_DOMAIN,
Warn,
"read time from the executor clock (`UtcDateTime::now()`) instead of `SystemTime::now()` etc."
}
declare_lint! {
pub TEST_MUST_NOT_USE_THREAD_SLEEP,
Warn,
"advance a `TestExecutor`'s virtual clock in tests instead of `std::thread::sleep`"
}
declare_lint! {
pub RUN_TEST_RESULT_MUST_BE_MATCHED,
Warn,
"`exit.into_result().unwrap()` – use `assert!(matches!(exit, Exit::Success(_)))` instead"
}
declare_lint! {
pub NO_PRINTLN_IN_EFFECT_GRAPH,
Warn,
"use structured spans / tracing hooks instead of `println!` in Effect code"
}
declare_lint! {
pub SCHEMA_PARSING_AT_IO_BOUNDARY,
Warn,
"use `Unknown` + `Schema` pipeline instead of bare `serde_json::from_value(…).unwrap()`"
}
declare_lint! {
pub EFFECT_MAP_IDENTITY,
Warn,
"`.map(|x| x)` / `.map_err(|e| e)` on an `Effect` does nothing"
}
declare_lint! {
pub EFFECT_FLAT_MAP_SUCCEED,
Warn,
"`.flat_map(|x| succeed(…))` on an `Effect` is `.map(|x| …)`"
}
declare_lint! {
pub EFFECT_FLAT_MAP_THEN,
Warn,
"`.flat_map(move |_| next)` on an `Effect` is `.then(next)`"
}
declare_lint! {
pub RAW_SQL_NON_LITERAL,
Warn,
"SQL passed to a `SqlClient` / `SqlTransaction` `&str` method is not a literal or a `const`"
}
declare_lint_pass!(EffectRsPass => [
NO_ASYNC_FN_IN_EFFECT_CRATE,
NO_RESULT_RETURN_IN_PUB_FN,
EFFECT_FN_REQUIRES_WHERE_CLAUSE,
EFFECT_ERROR_BOUND_MUST_USE_FROM,
NO_CONCRETE_CONTEXT_IN_PUB_API,
NO_CONCRETE_ENV_IN_PUB_API,
PREFER_NEEDS_X_OVER_RAW_GET_BOUNDS,
NO_EFFECT_SUFFIX_ON_GRAPH_BUILDERS,
BLOCKING_SUFFIX_RESERVED_FOR_RUNNERS,
MULTI_STEP_MUST_USE_EFFECT_MACRO,
NO_AWAIT_OUTSIDE_FROM_ASYNC,
RUN_BLOCKING_ONLY_AT_PROGRAM_EDGE,
TESTS_MUST_USE_RUN_TEST,
NO_TOKIO_SPAWN,
NO_THREAD_SPAWN,
NO_STATIC_SERVICE_GLOBALS,
NO_THREAD_LOCAL_FOR_REQUEST_CONTEXT,
PROVIDE_AT_EDGE_NOT_IN_LIBRARY,
NO_NESTED_RESULT_IN_PUB_API,
NO_ERROR_COLLAPSE_TO_STRING,
NO_UNWRAP_INSIDE_EFFECT,
NO_PANIC_INSIDE_EFFECT,
FORK_MUST_BE_JOINED,
NO_MUTEX_FOR_FIBER_COORDINATION,
NO_RC_REFCELL_IN_EFFECT_CODE,
NO_THREAD_SLEEP_IN_DOMAIN,
NO_SYSTEM_TIME_IN_DOMAIN,
TEST_MUST_NOT_USE_THREAD_SLEEP,
RUN_TEST_RESULT_MUST_BE_MATCHED,
NO_PRINTLN_IN_EFFECT_GRAPH,
SCHEMA_PARSING_AT_IO_BOUNDARY,
EFFECT_MAP_IDENTITY,
EFFECT_FLAT_MAP_SUCCEED,
EFFECT_FLAT_MAP_THEN,
RAW_SQL_NON_LITERAL,
]);
impl<'tcx> LateLintPass<'tcx> for EffectRsPass {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
kind: rustc_hir::intravisit::FnKind<'tcx>,
decl: &'tcx rustc_hir::FnDecl<'tcx>,
_body: &'tcx rustc_hir::Body<'tcx>,
span: rustc_span::Span,
def_id: rustc_hir::def_id::LocalDefId,
) {
let is_pub = cx.tcx.visibility(def_id).is_public();
let in_test = utils::in_test_context(cx, def_id);
fn_sigs::check_no_async_fn(cx, kind, span, def_id);
fn_sigs::check_no_result_return_pub(cx, kind, decl, is_pub, span, def_id);
items::check_no_nested_result_fn(cx, decl, is_pub, span);
if !in_test {
let generics = extract_generics(kind);
if let Some(generics) = generics {
fn_sigs::check_effect_fn_where_clause(cx, decl, generics, span);
fn_sigs::check_effect_error_from_bound(cx, decl, generics, span);
fn_sigs::check_no_concrete_context(cx, decl, is_pub, span);
fn_sigs::check_prefer_needs_x(cx, decl, generics, span);
}
}
fn_sigs::check_no_effect_suffix(cx, kind, decl, span);
fn_sigs::check_blocking_suffix_misuse(cx, kind, decl, span);
}
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx rustc_hir::Expr<'tcx>) {
let in_effect_fn = current_fn_returns_effect(cx, expr);
let def_id = cx.tcx.hir_get_parent_item(expr.hir_id).def_id;
fn_sigs::check_multi_step_flat_map(cx, expr);
exprs::check_no_await_outside_from_async(cx, expr);
exprs::check_run_blocking_at_edge(cx, expr, def_id);
exprs::check_tests_use_run_test(cx, expr, def_id);
exprs::check_no_tokio_spawn(cx, expr);
exprs::check_no_thread_spawn(cx, expr);
exprs::check_provide_at_edge(cx, expr, def_id);
exprs::check_no_error_collapse_to_string(cx, expr);
exprs::check_no_unwrap_in_effect(cx, expr, in_effect_fn);
exprs::check_no_panic_in_effect(cx, expr, in_effect_fn);
exprs::check_no_thread_sleep(cx, expr, def_id);
exprs::check_no_system_time_now(cx, expr);
exprs::check_run_test_result_matched(cx, expr);
exprs::check_no_println_in_effect(cx, expr, in_effect_fn);
exprs::check_schema_parsing_at_boundary(cx, expr);
combinators::check_map_identity(cx, expr);
combinators::check_flat_map_succeed(cx, expr);
combinators::check_flat_map_then(cx, expr);
sql::check_raw_sql_non_literal(cx, expr);
}
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx rustc_hir::Stmt<'tcx>) {
exprs::check_fork_result_discarded(cx, stmt);
}
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx rustc_hir::Item<'tcx>) {
items::check_no_static_service_globals(cx, item);
items::check_no_thread_local(cx, item);
items::check_no_nested_result_type(cx, item);
}
fn check_ty(
&mut self,
cx: &LateContext<'tcx>,
ty: &'tcx rustc_hir::Ty<'tcx, rustc_hir::AmbigArg>,
) {
exprs::check_no_mutex_for_fiber_coordination(cx, ty);
exprs::check_no_rc_refcell(cx, ty);
}
}
#[unsafe(no_mangle)]
pub fn register_lints(_sess: &Session, store: &mut LintStore) {
store.register_lints(&[
NO_ASYNC_FN_IN_EFFECT_CRATE,
NO_RESULT_RETURN_IN_PUB_FN,
EFFECT_FN_REQUIRES_WHERE_CLAUSE,
EFFECT_ERROR_BOUND_MUST_USE_FROM,
NO_CONCRETE_CONTEXT_IN_PUB_API,
NO_CONCRETE_ENV_IN_PUB_API,
PREFER_NEEDS_X_OVER_RAW_GET_BOUNDS,
NO_EFFECT_SUFFIX_ON_GRAPH_BUILDERS,
BLOCKING_SUFFIX_RESERVED_FOR_RUNNERS,
MULTI_STEP_MUST_USE_EFFECT_MACRO,
NO_AWAIT_OUTSIDE_FROM_ASYNC,
RUN_BLOCKING_ONLY_AT_PROGRAM_EDGE,
TESTS_MUST_USE_RUN_TEST,
NO_TOKIO_SPAWN,
NO_THREAD_SPAWN,
NO_STATIC_SERVICE_GLOBALS,
NO_THREAD_LOCAL_FOR_REQUEST_CONTEXT,
PROVIDE_AT_EDGE_NOT_IN_LIBRARY,
NO_NESTED_RESULT_IN_PUB_API,
NO_ERROR_COLLAPSE_TO_STRING,
NO_UNWRAP_INSIDE_EFFECT,
NO_PANIC_INSIDE_EFFECT,
FORK_MUST_BE_JOINED,
NO_MUTEX_FOR_FIBER_COORDINATION,
NO_RC_REFCELL_IN_EFFECT_CODE,
NO_THREAD_SLEEP_IN_DOMAIN,
NO_SYSTEM_TIME_IN_DOMAIN,
TEST_MUST_NOT_USE_THREAD_SLEEP,
RUN_TEST_RESULT_MUST_BE_MATCHED,
NO_PRINTLN_IN_EFFECT_GRAPH,
SCHEMA_PARSING_AT_IO_BOUNDARY,
EFFECT_MAP_IDENTITY,
EFFECT_FLAT_MAP_SUCCEED,
EFFECT_FLAT_MAP_THEN,
RAW_SQL_NON_LITERAL,
]);
store.register_late_lint_pass(Box::new(|_| Box::new(EffectRsPass)));
}
#[test]
fn ui() {
dylint_testing::ui::Test::src_base(
env!("CARGO_PKG_NAME"),
concat!(env!("CARGO_MANIFEST_DIR"), "/ui"),
)
.rustc_flags(["--edition=2024"])
.run();
}
#[test]
fn ui_test_context() {
dylint_testing::ui::Test::src_base(
env!("CARGO_PKG_NAME"),
concat!(env!("CARGO_MANIFEST_DIR"), "/ui_test"),
)
.rustc_flags(["--edition=2024", "--test"])
.run();
}
fn extract_generics<'tcx>(
kind: rustc_hir::intravisit::FnKind<'tcx>,
) -> Option<&'tcx rustc_hir::Generics<'tcx>> {
match kind {
rustc_hir::intravisit::FnKind::ItemFn(_, generics, _) => Some(generics),
rustc_hir::intravisit::FnKind::Method(..) | rustc_hir::intravisit::FnKind::Closure => None,
}
}
fn current_fn_returns_effect<'tcx>(cx: &LateContext<'tcx>, expr: &rustc_hir::Expr<'tcx>) -> bool {
let mut hir_id = expr.hir_id;
loop {
hir_id = match cx.tcx.hir_parent_id_iter(hir_id).next() {
Some(p) => p,
None => return false,
};
match cx.tcx.hir_node(hir_id) {
rustc_hir::Node::Item(item) => {
if let rustc_hir::ItemKind::Fn { sig, .. } = &item.kind {
return utils::returns_effect(sig.decl);
}
return false;
}
rustc_hir::Node::TraitItem(ti) => {
if let rustc_hir::TraitItemKind::Fn(sig, _) = &ti.kind {
return utils::returns_effect(sig.decl);
}
return false;
}
rustc_hir::Node::ImplItem(ii) => {
if let rustc_hir::ImplItemKind::Fn(sig, _) = &ii.kind {
return utils::returns_effect(sig.decl);
}
return false;
}
rustc_hir::Node::Expr(e) => {
if let rustc_hir::ExprKind::Closure(cl) = &e.kind {
return utils::returns_effect(cl.fn_decl);
}
}
_ => {}
}
}
}