From 3f819653f93072d55e56a02b0bfa5daf230f87f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Tue, 8 Jul 2025 21:04:13 +0200 Subject: [PATCH 1/9] fix(linter): Update dylint and related dependencies --- nova_lint/Cargo.toml | 6 +++--- nova_lint/src/utils.rs | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/nova_lint/Cargo.toml b/nova_lint/Cargo.toml index 37ac5c607..8826497c1 100644 --- a/nova_lint/Cargo.toml +++ b/nova_lint/Cargo.toml @@ -25,11 +25,11 @@ name = "gc_scope_is_only_passed_by_value" path = "ui/gc_scope_is_only_passed_by_value.rs" [dependencies] -clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "ff4a26d442bead94a4c96fb1de967374bc4fbd8e" } -dylint_linting = { version = "4.0.0", features = ["constituent"] } +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "0450db33a5d8587f7c1d4b6d233dac963605766b" } +dylint_linting = { version = "4.1.0", features = ["constituent"] } [dev-dependencies] -dylint_testing = "4.0.0" +dylint_testing = "4.1.0" nova_vm = { path = "../nova_vm" } [package.metadata.rust-analyzer] diff --git a/nova_lint/src/utils.rs b/nova_lint/src/utils.rs index b0546c9ef..073f64a0b 100644 --- a/nova_lint/src/utils.rs +++ b/nova_lint/src/utils.rs @@ -1,6 +1,23 @@ -use clippy_utils::match_def_path; +use rustc_hir::def_id::DefId; use rustc_lint::LateContext; use rustc_middle::ty::{Ty, TyKind}; +use rustc_span::symbol::Symbol; + +// Copyright (c) 2014-2025 The Rust Project Developers +// +// Originally copied from `dylint` which in turn copied it from `clippy_utils`: +// - https://github.com/trailofbits/dylint/blob/a2dd5c60d53d66fc791fa8184bed27a4cb142e74/internal/src/match_def_path.rs +// - https://github.com/rust-lang/rust-clippy/blob/f62f26965817f2573c2649288faa489a03ed1665/clippy_utils/src/lib.rs +// It was removed from `clippy_utils` by the following PR: +// - https://github.com/rust-lang/rust-clippy/pull/14705 +/// Checks if the given `DefId` matches the path. +pub fn match_def_path(cx: &LateContext<'_>, did: DefId, syms: &[&str]) -> bool { + // We should probably move to Symbols in Clippy as well rather than interning every time. + let path = cx.get_def_path(did); + syms.iter() + .map(|x| Symbol::intern(x)) + .eq(path.iter().copied()) +} pub fn is_param_ty(ty: &Ty) -> bool { matches!(ty.kind(), TyKind::Param(_)) From a236999db4690e335b05f87e1def707481177e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Tue, 8 Jul 2025 21:05:03 +0200 Subject: [PATCH 2/9] ci: Enable dylint again --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 303c5532a..0dd832923 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,6 @@ jobs: with: shared-key: warm - name: Install Dylint - if: false uses: taiki-e/install-action@v2 with: tool: cargo-dylint,dylint-link @@ -50,11 +49,9 @@ jobs: cargo +stable clippy --all-targets -- -D warnings cargo +nightly-2025-05-14 clippy --all-targets --all-features -- -D warnings - name: Dylint tests - if: false working-directory: nova_lint run: cargo test - name: Dylint - if: false run: cargo dylint --all build: From af89977875011329cb84877f75d34a1a91f76849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Thu, 18 Sep 2025 20:48:35 +0200 Subject: [PATCH 3/9] fix(linter): Update cargo config for linter --- nova_lint/.cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova_lint/.cargo/config.toml b/nova_lint/.cargo/config.toml index d58627620..93dceb699 100644 --- a/nova_lint/.cargo/config.toml +++ b/nova_lint/.cargo/config.toml @@ -1,2 +1,2 @@ [target.'cfg(all())'] -rustflags = ["-C", "linker=dylint-link"] +linker = "dylint-link" From 72708097020cc191c5830435c67d4f8e72a5919a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Fri, 19 Sep 2025 00:36:07 +0200 Subject: [PATCH 4/9] feat(linter): Implement `immediately_bind_scoped` rule --- nova_lint/Cargo.toml | 6 +- nova_lint/rust-toolchain.toml | 2 +- nova_lint/src/immediately_bind_scoped.rs | 103 ++++++++++++++++++++ nova_lint/src/lib.rs | 2 + nova_lint/src/utils.rs | 58 ++++++++++- nova_lint/ui/immediately_bind_scoped.rs | 63 ++++++++++++ nova_lint/ui/immediately_bind_scoped.stderr | 19 ++++ 7 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 nova_lint/src/immediately_bind_scoped.rs create mode 100644 nova_lint/ui/immediately_bind_scoped.rs create mode 100644 nova_lint/ui/immediately_bind_scoped.stderr diff --git a/nova_lint/Cargo.toml b/nova_lint/Cargo.toml index 8826497c1..0ef542acf 100644 --- a/nova_lint/Cargo.toml +++ b/nova_lint/Cargo.toml @@ -24,8 +24,12 @@ path = "ui/gc_scope_comes_last.rs" name = "gc_scope_is_only_passed_by_value" path = "ui/gc_scope_is_only_passed_by_value.rs" +[[example]] +name = "immediately_bind_scoped" +path = "ui/immediately_bind_scoped.rs" + [dependencies] -clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "0450db33a5d8587f7c1d4b6d233dac963605766b" } +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "334fb906aef13d20050987b13448f37391bb97a2" } dylint_linting = { version = "4.1.0", features = ["constituent"] } [dev-dependencies] diff --git a/nova_lint/rust-toolchain.toml b/nova_lint/rust-toolchain.toml index 0f53877b3..07038b722 100644 --- a/nova_lint/rust-toolchain.toml +++ b/nova_lint/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2025-05-14" +channel = "nightly-2025-08-07" components = ["llvm-tools-preview", "rustc-dev"] diff --git a/nova_lint/src/immediately_bind_scoped.rs b/nova_lint/src/immediately_bind_scoped.rs new file mode 100644 index 000000000..48a70b8a9 --- /dev/null +++ b/nova_lint/src/immediately_bind_scoped.rs @@ -0,0 +1,103 @@ +use clippy_utils::paths::{PathLookup, PathNS, lookup_path_str}; +use clippy_utils::sym::Symbol; +use clippy_utils::ty::implements_trait; +use clippy_utils::usage::local_used_after_expr; +use clippy_utils::{diagnostics::span_lint_and_help, is_self}; +use clippy_utils::{ + get_expr_use_or_unification_node, get_parent_expr, is_expr_final_block_expr, is_trait_method, + path_def_id, peel_blocks, potential_return_of_enclosing_body, +}; +use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind}; +use rustc_hir::{Expr, ExprKind, LetStmt, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_span::symbol::Symbol; + +use crate::{is_scoped_ty, method_call}; + +dylint_linting::declare_late_lint! { + /// ### What it does + /// + /// Makes sure that the user immediately binds `Scoped::get` results. + /// + /// ### Why is this bad? + /// + /// TODO: Write an explanation of why this is bad. + /// + /// ### Example + /// + /// ``` + /// let a = scoped_a.get(agent); + /// ``` + /// + /// Use instead: + /// + /// ``` + /// let a = scoped_a.get(agent).bind(gc.nogc()); + /// ``` + /// + /// Which ensures that no odd bugs occur. + /// + /// ### Exception: If the result is immediately used without assigning to a + /// variable, binding can be skipped. + /// + /// ``` + /// scoped_a.get(agent).internal_delete(agent, scoped_b.get(agent), gc.reborrow()); + /// ``` + /// + /// Here it is perfectly okay to skip the binding for both `scoped_a` and + /// `scoped_b` as the borrow checker would force you to again unbind both + /// `Value`s immediately. + pub IMMEDIATELY_BIND_SCOPED, + Deny, + "the result of `Scoped::get` should be immediately bound" +} + +impl<'tcx> LateLintPass<'tcx> for ImmediatelyBindScoped { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + // First we check if we have found a `Scoped::get` call + if let Some((method, recv, _, _, _)) = method_call(expr) + && method == "get" + && let typeck_results = cx.typeck_results() + && let recv_ty = typeck_results.expr_ty(recv) + && is_scoped_ty(cx, &recv_ty) + { + // Which is followed by a trait method call to `bind` in which case + // it is all done properly and we can exit out of the lint + if let Some(parent) = get_parent_expr(cx, expr) + && let Some((parent_method, _, _, _, _)) = method_call(parent) + && parent_method == "bind" + && let parent_ty = typeck_results.expr_ty(parent) + && let Some(&trait_def_id) = + lookup_path_str(cx.tcx, PathNS::Type, "nova_vm::engine::context::Bindable") + .first() + && implements_trait(cx, parent_ty, trait_def_id, &[]) + { + return; + } + + // Now we are onto something! If the expression is returned, used + // after the expression or assigned to a variable we might have + // found an issue. + if let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr) + && (potential_return_of_enclosing_body(cx, expr) + || local_used_after_expr(cx, hir_id, expr) + || matches!( + usage, + Node::LetStmt(LetStmt { + init: Some(hir_id), + .. + }) + )) + { + span_lint_and_help( + cx, + IMMEDIATELY_BIND_SCOPED, + expr.span, + "the result of `Scoped::get` should be immediately bound", + None, + "immediately bind the value", + ); + } + } + } +} diff --git a/nova_lint/src/lib.rs b/nova_lint/src/lib.rs index 6d40617c3..864448189 100644 --- a/nova_lint/src/lib.rs +++ b/nova_lint/src/lib.rs @@ -25,6 +25,7 @@ extern crate rustc_trait_selection; mod agent_comes_first; mod gc_scope_comes_last; mod gc_scope_is_only_passed_by_value; +mod immediately_bind_scoped; mod utils; pub(crate) use utils::*; @@ -34,6 +35,7 @@ pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint agent_comes_first::register_lints(sess, lint_store); gc_scope_comes_last::register_lints(sess, lint_store); gc_scope_is_only_passed_by_value::register_lints(sess, lint_store); + immediately_bind_scoped::register_lints(sess, lint_store); } #[test] diff --git a/nova_lint/src/utils.rs b/nova_lint/src/utils.rs index 073f64a0b..4200c8078 100644 --- a/nova_lint/src/utils.rs +++ b/nova_lint/src/utils.rs @@ -1,6 +1,7 @@ -use rustc_hir::def_id::DefId; +use rustc_hir::{Expr, ExprKind, def_id::DefId}; use rustc_lint::LateContext; use rustc_middle::ty::{Ty, TyKind}; +use rustc_span::Span; use rustc_span::symbol::Symbol; // Copyright (c) 2014-2025 The Rust Project Developers @@ -19,6 +20,25 @@ pub fn match_def_path(cx: &LateContext<'_>, did: DefId, syms: &[&str]) -> bool { .eq(path.iter().copied()) } +// Copyright (c) 2014-2025 The Rust Project Developers +// +// Originally copied from `dylint` which in turn copied it from `clippy_lints`: +// - https://github.com/trailofbits/dylint/blob/d1be1c42f363ca11f8ebce0ff0797ecbbcc3680b/examples/restriction/collapsible_unwrap/src/lib.rs#L180 +// - https://github.com/rust-lang/rust-clippy/blob/3f015a363020d3811e1f028c9ce4b0705c728289/clippy_lints/src/methods/mod.rs#L3293-L3304 +/// Extracts a method call name, args, and `Span` of the method name. +pub fn method_call<'tcx>( + recv: &'tcx Expr<'tcx>, +) -> Option<(&'tcx str, &'tcx Expr<'tcx>, &'tcx [Expr<'tcx>], Span, Span)> { + if let ExprKind::MethodCall(path, receiver, args, call_span) = recv.kind + && !args.iter().any(|e| e.span.from_expansion()) + && !receiver.span.from_expansion() + { + let name = path.ident.name.as_str(); + return Some((name, receiver, args, path.ident.span, call_span)); + } + None +} + pub fn is_param_ty(ty: &Ty) -> bool { matches!(ty.kind(), TyKind::Param(_)) } @@ -53,3 +73,39 @@ pub fn is_no_gc_scope_ty(cx: &LateContext<'_>, ty: &Ty) -> bool { _ => false, } } + +pub fn is_scoped_ty(cx: &LateContext<'_>, ty: &Ty) -> bool { + match ty.kind() { + TyKind::Adt(def, _) => match_def_path( + cx, + def.did(), + &["nova_vm", "engine", "rootable", "scoped", "Scoped"], + ), + _ => false, + } +} + +// Checks if a given expression is assigned to a variable. +// +// Copyright (c) 2014-2025 The Rust Project Developers +// +// Copied and modified from `clippy_utils`: +// - https://github.com/rust-lang/rust-clippy/blob/8a5dc7c1713a7eb9af28bf9f53dc6b61da7aad90/clippy_utils/src/lib.rs#L1369-L1388 +// pub fn is_inside_let(tcx: TyCtxt<'_>, expr: &Expr<'_>) -> bool { +// let mut child_id = expr.hir_id; +// for (parent_id, node) in tcx.hir_parent_iter(child_id) { +// if let Node::LetStmt(LetStmt { +// init: Some(init), +// els: Some(els), +// .. +// }) = node +// && (init.hir_id == child_id || els.hir_id == child_id) +// { +// return true; +// } + +// child_id = parent_id; +// } + +// false +// } diff --git a/nova_lint/ui/immediately_bind_scoped.rs b/nova_lint/ui/immediately_bind_scoped.rs new file mode 100644 index 000000000..fce71c9b3 --- /dev/null +++ b/nova_lint/ui/immediately_bind_scoped.rs @@ -0,0 +1,63 @@ +#![allow(dead_code, unused_variables, clippy::disallowed_names)] + +use nova_vm::{ + ecmascript::{execution::Agent, types::Value}, + engine::{ + Scoped, + context::{Bindable, NoGcScope}, + }, +}; + +fn test_scoped_get_is_immediately_bound(agent: &Agent, scoped: Scoped, gc: NoGcScope) { + let _a = scoped.get(agent).bind(gc); +} + +// TODO: These are valid patterns, which are found in certain parts of the +// codebase so should ideally be implemented. +// fn test_scoped_get_can_get_bound_right_after(agent: &Agent, scoped: Scoped, gc: NoGcScope) { +// let a = scoped.get(agent); +// a.bind(gc); +// } +// +// fn test_scoped_get_can_get_bound_right_after_and_never_used_again( +// agent: &Agent, +// scoped: Scoped, +// gc: NoGcScope, +// ) { +// let a = scoped.get(agent); +// let b = a.bind(gc); +// a; +// } +// +// fn test_scoped_get_can_be_immediately_passed_on( +// agent: &Agent, +// scoped: Scoped, +// gc: NoGcScope, +// ) { +// let a = scoped.get(agent); +// test_consumes_unbound_value(value); +// } +// +// fn test_consumes_unbound_value(value: Value) { +// unimplemented!() +// } + +fn test_scoped_get_is_not_immediately_bound(agent: &Agent, scoped: Scoped) { + let _a = scoped.get(agent); +} + +fn test_scoped_get_doesnt_need_to_be_bound_if_not_assigned(agent: &Agent, scoped: Scoped) { + scoped.get(agent); +} + +fn test_improbable_but_technically_bad_situation( + agent: &Agent, + scoped: Scoped, + gc: NoGcScope, +) { + let _a = Scoped::new(agent, Value::Undefined, gc).get(agent); +} + +fn main() { + unimplemented!() +} diff --git a/nova_lint/ui/immediately_bind_scoped.stderr b/nova_lint/ui/immediately_bind_scoped.stderr new file mode 100644 index 000000000..53ff97d5c --- /dev/null +++ b/nova_lint/ui/immediately_bind_scoped.stderr @@ -0,0 +1,19 @@ +error: the result of `Scoped::get` should be immediately bound + --> $DIR/immediately_bind_scoped.rs:16:14 + | +LL | let _a = scoped.get(agent); + | ^^^^^^^^^^^^^^^^^ + | + = help: immediately bind the value + = note: `#[deny(immediately_bind_scoped)]` on by default + +error: the result of `Scoped::get` should be immediately bound + --> $DIR/immediately_bind_scoped.rs:24:14 + | +LL | let _a = Scoped::new(agent, Value::Undefined, gc).get(agent); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: immediately bind the value + +error: aborting due to 2 previous errors + From 43772b6900fd8b0dc1067143100ce63abbbdee72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Fri, 19 Sep 2025 00:52:50 +0200 Subject: [PATCH 5/9] fix: Test expectations --- nova_lint/ui/immediately_bind_scoped.stderr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova_lint/ui/immediately_bind_scoped.stderr b/nova_lint/ui/immediately_bind_scoped.stderr index 53ff97d5c..feeb517dd 100644 --- a/nova_lint/ui/immediately_bind_scoped.stderr +++ b/nova_lint/ui/immediately_bind_scoped.stderr @@ -1,5 +1,5 @@ error: the result of `Scoped::get` should be immediately bound - --> $DIR/immediately_bind_scoped.rs:16:14 + --> $DIR/immediately_bind_scoped.rs:46:14 | LL | let _a = scoped.get(agent); | ^^^^^^^^^^^^^^^^^ @@ -8,7 +8,7 @@ LL | let _a = scoped.get(agent); = note: `#[deny(immediately_bind_scoped)]` on by default error: the result of `Scoped::get` should be immediately bound - --> $DIR/immediately_bind_scoped.rs:24:14 + --> $DIR/immediately_bind_scoped.rs:58:14 | LL | let _a = Scoped::new(agent, Value::Undefined, gc).get(agent); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 800fa5fe1f5a95f8097cd82f60547b81a9cb852c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Fri, 19 Sep 2025 00:59:44 +0200 Subject: [PATCH 6/9] fix: Clippy --- nova_lint/src/immediately_bind_scoped.rs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/nova_lint/src/immediately_bind_scoped.rs b/nova_lint/src/immediately_bind_scoped.rs index 48a70b8a9..1b99043e4 100644 --- a/nova_lint/src/immediately_bind_scoped.rs +++ b/nova_lint/src/immediately_bind_scoped.rs @@ -1,16 +1,12 @@ -use clippy_utils::paths::{PathLookup, PathNS, lookup_path_str}; -use clippy_utils::sym::Symbol; +use clippy_utils::paths::{PathNS, lookup_path_str}; use clippy_utils::ty::implements_trait; use clippy_utils::usage::local_used_after_expr; -use clippy_utils::{diagnostics::span_lint_and_help, is_self}; +use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::{ - get_expr_use_or_unification_node, get_parent_expr, is_expr_final_block_expr, is_trait_method, - path_def_id, peel_blocks, potential_return_of_enclosing_body, + get_expr_use_or_unification_node, get_parent_expr, potential_return_of_enclosing_body, }; -use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind}; -use rustc_hir::{Expr, ExprKind, LetStmt, Node}; +use rustc_hir::{Expr, Node}; use rustc_lint::{LateContext, LateLintPass}; -use rustc_span::symbol::Symbol; use crate::{is_scoped_ty, method_call}; @@ -81,13 +77,7 @@ impl<'tcx> LateLintPass<'tcx> for ImmediatelyBindScoped { if let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr) && (potential_return_of_enclosing_body(cx, expr) || local_used_after_expr(cx, hir_id, expr) - || matches!( - usage, - Node::LetStmt(LetStmt { - init: Some(hir_id), - .. - }) - )) + || matches!(usage, Node::LetStmt(_))) { span_lint_and_help( cx, From bcbefb4618322e04a75e184c92c831e011dae79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Fri, 19 Sep 2025 11:35:14 +0200 Subject: [PATCH 7/9] chore: Clean up, organize, think, and start work on the edge-cases --- nova_lint/src/agent_comes_first.rs | 2 +- nova_lint/src/gc_scope_comes_last.rs | 2 +- .../src/gc_scope_is_only_passed_by_value.rs | 2 +- nova_lint/src/immediately_bind_scoped.rs | 84 ++++++++++++++----- nova_lint/src/utils.rs | 28 +------ nova_lint/ui/immediately_bind_scoped.rs | 56 ++++++------- 6 files changed, 95 insertions(+), 79 deletions(-) diff --git a/nova_lint/src/agent_comes_first.rs b/nova_lint/src/agent_comes_first.rs index c930389ce..5f39d8ecc 100644 --- a/nova_lint/src/agent_comes_first.rs +++ b/nova_lint/src/agent_comes_first.rs @@ -1,5 +1,5 @@ use clippy_utils::{diagnostics::span_lint_and_help, is_self}; -use rustc_hir::{def_id::LocalDefId, intravisit::FnKind, Body, FnDecl}; +use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_span::Span; diff --git a/nova_lint/src/gc_scope_comes_last.rs b/nova_lint/src/gc_scope_comes_last.rs index ba422a23c..85f799eed 100644 --- a/nova_lint/src/gc_scope_comes_last.rs +++ b/nova_lint/src/gc_scope_comes_last.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_help; -use rustc_hir::{def_id::LocalDefId, intravisit::FnKind, Body, FnDecl}; +use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_span::Span; diff --git a/nova_lint/src/gc_scope_is_only_passed_by_value.rs b/nova_lint/src/gc_scope_is_only_passed_by_value.rs index fb59027bd..7078c020e 100644 --- a/nova_lint/src/gc_scope_is_only_passed_by_value.rs +++ b/nova_lint/src/gc_scope_is_only_passed_by_value.rs @@ -1,5 +1,5 @@ use clippy_utils::{diagnostics::span_lint_and_help, is_self}; -use rustc_hir::{def_id::LocalDefId, intravisit::FnKind, Body, FnDecl}; +use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::TyKind; use rustc_span::Span; diff --git a/nova_lint/src/immediately_bind_scoped.rs b/nova_lint/src/immediately_bind_scoped.rs index 1b99043e4..b37485bd2 100644 --- a/nova_lint/src/immediately_bind_scoped.rs +++ b/nova_lint/src/immediately_bind_scoped.rs @@ -1,14 +1,15 @@ -use clippy_utils::paths::{PathNS, lookup_path_str}; -use clippy_utils::ty::implements_trait; -use clippy_utils::usage::local_used_after_expr; -use clippy_utils::diagnostics::span_lint_and_help; +use crate::{is_scoped_ty, method_call}; use clippy_utils::{ - get_expr_use_or_unification_node, get_parent_expr, potential_return_of_enclosing_body, + diagnostics::span_lint_and_help, + get_expr_use_or_unification_node, get_parent_expr, + paths::{PathNS, lookup_path_str}, + potential_return_of_enclosing_body, + ty::implements_trait, + usage::local_used_after_expr, }; use rustc_hir::{Expr, Node}; use rustc_lint::{LateContext, LateLintPass}; - -use crate::{is_scoped_ty, method_call}; +use rustc_middle::ty::Ty; dylint_linting::declare_late_lint! { /// ### What it does @@ -51,26 +52,26 @@ dylint_linting::declare_late_lint! { impl<'tcx> LateLintPass<'tcx> for ImmediatelyBindScoped { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { // First we check if we have found a `Scoped::get` call - if let Some((method, recv, _, _, _)) = method_call(expr) - && method == "get" - && let typeck_results = cx.typeck_results() - && let recv_ty = typeck_results.expr_ty(recv) - && is_scoped_ty(cx, &recv_ty) - { + if is_scoped_get_method_call(cx, expr) { // Which is followed by a trait method call to `bind` in which case // it is all done properly and we can exit out of the lint if let Some(parent) = get_parent_expr(cx, expr) - && let Some((parent_method, _, _, _, _)) = method_call(parent) - && parent_method == "bind" - && let parent_ty = typeck_results.expr_ty(parent) - && let Some(&trait_def_id) = - lookup_path_str(cx.tcx, PathNS::Type, "nova_vm::engine::context::Bindable") - .first() - && implements_trait(cx, parent_ty, trait_def_id, &[]) + && is_bindable_bind_method_call(cx, parent) { return; } + // If the `Scoped::get` call is never used or unified we can + // safely exit out of the rule, otherwise we need to look into how + // it's used. + let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr) else { + return; + }; + + if !local_used_after_expr(cx, hir_id, expr) { + return; + } + // Now we are onto something! If the expression is returned, used // after the expression or assigned to a variable we might have // found an issue. @@ -91,3 +92,46 @@ impl<'tcx> LateLintPass<'tcx> for ImmediatelyBindScoped { } } } + +fn is_scoped_get_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool { + if let Some((method, recv, _, _, _)) = method_call(expr) + && method == "get" + && let typeck_results = cx.typeck_results() + && let recv_ty = typeck_results.expr_ty(recv) + && is_scoped_ty(cx, &recv_ty) + { + true + } else { + false + } +} + +fn is_bindable_bind_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool { + if let Some((method, _, _, _, _)) = method_call(expr) + && method == "bind" + && let expr_ty = cx.typeck_results().expr_ty(expr) + && implements_bindable_trait(cx, &expr_ty) + { + true + } else { + false + } +} + +fn is_bindable_unbind_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool { + if let Some((method, _, _, _, _)) = method_call(expr) + && method == "unbind" + && let expr_ty = cx.typeck_results().expr_ty(expr) + && implements_bindable_trait(cx, &expr_ty) + { + true + } else { + false + } +} + +fn implements_bindable_trait<'tcx>(cx: &LateContext<'tcx>, ty: &Ty<'tcx>) -> bool { + lookup_path_str(cx.tcx, PathNS::Type, "nova_vm::engine::context::Bindable") + .first() + .is_some_and(|&trait_def_id| implements_trait(cx, *ty, trait_def_id, &[])) +} diff --git a/nova_lint/src/utils.rs b/nova_lint/src/utils.rs index 4200c8078..b52548082 100644 --- a/nova_lint/src/utils.rs +++ b/nova_lint/src/utils.rs @@ -1,8 +1,7 @@ use rustc_hir::{Expr, ExprKind, def_id::DefId}; use rustc_lint::LateContext; use rustc_middle::ty::{Ty, TyKind}; -use rustc_span::Span; -use rustc_span::symbol::Symbol; +use rustc_span::{Span, symbol::Symbol}; // Copyright (c) 2014-2025 The Rust Project Developers // @@ -84,28 +83,3 @@ pub fn is_scoped_ty(cx: &LateContext<'_>, ty: &Ty) -> bool { _ => false, } } - -// Checks if a given expression is assigned to a variable. -// -// Copyright (c) 2014-2025 The Rust Project Developers -// -// Copied and modified from `clippy_utils`: -// - https://github.com/rust-lang/rust-clippy/blob/8a5dc7c1713a7eb9af28bf9f53dc6b61da7aad90/clippy_utils/src/lib.rs#L1369-L1388 -// pub fn is_inside_let(tcx: TyCtxt<'_>, expr: &Expr<'_>) -> bool { -// let mut child_id = expr.hir_id; -// for (parent_id, node) in tcx.hir_parent_iter(child_id) { -// if let Node::LetStmt(LetStmt { -// init: Some(init), -// els: Some(els), -// .. -// }) = node -// && (init.hir_id == child_id || els.hir_id == child_id) -// { -// return true; -// } - -// child_id = parent_id; -// } - -// false -// } diff --git a/nova_lint/ui/immediately_bind_scoped.rs b/nova_lint/ui/immediately_bind_scoped.rs index fce71c9b3..73a05d2f6 100644 --- a/nova_lint/ui/immediately_bind_scoped.rs +++ b/nova_lint/ui/immediately_bind_scoped.rs @@ -12,35 +12,33 @@ fn test_scoped_get_is_immediately_bound(agent: &Agent, scoped: Scoped, gc let _a = scoped.get(agent).bind(gc); } -// TODO: These are valid patterns, which are found in certain parts of the -// codebase so should ideally be implemented. -// fn test_scoped_get_can_get_bound_right_after(agent: &Agent, scoped: Scoped, gc: NoGcScope) { -// let a = scoped.get(agent); -// a.bind(gc); -// } -// -// fn test_scoped_get_can_get_bound_right_after_and_never_used_again( -// agent: &Agent, -// scoped: Scoped, -// gc: NoGcScope, -// ) { -// let a = scoped.get(agent); -// let b = a.bind(gc); -// a; -// } -// -// fn test_scoped_get_can_be_immediately_passed_on( -// agent: &Agent, -// scoped: Scoped, -// gc: NoGcScope, -// ) { -// let a = scoped.get(agent); -// test_consumes_unbound_value(value); -// } -// -// fn test_consumes_unbound_value(value: Value) { -// unimplemented!() -// } +fn test_scoped_get_can_get_bound_right_after(agent: &Agent, scoped: Scoped, gc: NoGcScope) { + let a = scoped.get(agent); + a.bind(gc); +} + +fn test_scoped_get_can_get_bound_right_after_but_never_used_again( + agent: &Agent, + scoped: Scoped, + gc: NoGcScope, +) { + let a = scoped.get(agent); + let b = a.bind(gc); + a.is_undefined(); +} + +fn test_scoped_get_can_be_immediately_passed_on( + agent: &Agent, + scoped: Scoped, + gc: NoGcScope, +) { + let a = scoped.get(agent); + test_consumes_unbound_value(a.unbind()); +} + +fn test_consumes_unbound_value(value: Value) { + unimplemented!() +} fn test_scoped_get_is_not_immediately_bound(agent: &Agent, scoped: Scoped) { let _a = scoped.get(agent); From eb9159e46f082a46f8149b4ff6e0e08cd54d5220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Fri, 19 Sep 2025 11:41:08 +0200 Subject: [PATCH 8/9] fix: Update test expectations --- nova_lint/ui/immediately_bind_scoped.rs | 2 +- nova_lint/ui/immediately_bind_scoped.stderr | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/nova_lint/ui/immediately_bind_scoped.rs b/nova_lint/ui/immediately_bind_scoped.rs index 73a05d2f6..66027a63c 100644 --- a/nova_lint/ui/immediately_bind_scoped.rs +++ b/nova_lint/ui/immediately_bind_scoped.rs @@ -33,7 +33,7 @@ fn test_scoped_get_can_be_immediately_passed_on( gc: NoGcScope, ) { let a = scoped.get(agent); - test_consumes_unbound_value(a.unbind()); + test_consumes_unbound_value(a); } fn test_consumes_unbound_value(value: Value) { diff --git a/nova_lint/ui/immediately_bind_scoped.stderr b/nova_lint/ui/immediately_bind_scoped.stderr index feeb517dd..91b65f9df 100644 --- a/nova_lint/ui/immediately_bind_scoped.stderr +++ b/nova_lint/ui/immediately_bind_scoped.stderr @@ -1,19 +1,25 @@ error: the result of `Scoped::get` should be immediately bound - --> $DIR/immediately_bind_scoped.rs:46:14 + --> $DIR/immediately_bind_scoped.rs:25:13 + | +LL | let a = scoped.get(agent); + | ^^^^^^^^^^^^^^^^^ + | + = help: immediately bind the value + +error: the result of `Scoped::get` should be immediately bound + --> $DIR/immediately_bind_scoped.rs:44:14 | LL | let _a = scoped.get(agent); | ^^^^^^^^^^^^^^^^^ | = help: immediately bind the value - = note: `#[deny(immediately_bind_scoped)]` on by default error: the result of `Scoped::get` should be immediately bound - --> $DIR/immediately_bind_scoped.rs:58:14 + --> $DIR/immediately_bind_scoped.rs:56:14 | LL | let _a = Scoped::new(agent, Value::Undefined, gc).get(agent); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: immediately bind the value -error: aborting due to 2 previous errors - +error: aborting due to 3 previous errors From 9cb11348ae31a0a9e583c061a60581e95c781c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Sun, 21 Sep 2025 00:07:11 +0200 Subject: [PATCH 9/9] fix(linter): Make sure the immediate next use of the scoped variable is either as a binding or as a parameter to a method call --- nova_lint/src/immediately_bind_scoped.rs | 169 +++++++++++++++----- nova_lint/ui/immediately_bind_scoped.rs | 5 +- nova_lint/ui/immediately_bind_scoped.stderr | 16 +- 3 files changed, 136 insertions(+), 54 deletions(-) diff --git a/nova_lint/src/immediately_bind_scoped.rs b/nova_lint/src/immediately_bind_scoped.rs index b37485bd2..010f14ad9 100644 --- a/nova_lint/src/immediately_bind_scoped.rs +++ b/nova_lint/src/immediately_bind_scoped.rs @@ -1,13 +1,15 @@ +use std::ops::ControlFlow; + use crate::{is_scoped_ty, method_call}; use clippy_utils::{ diagnostics::span_lint_and_help, - get_expr_use_or_unification_node, get_parent_expr, + get_enclosing_block, get_parent_expr, path_to_local_id, paths::{PathNS, lookup_path_str}, - potential_return_of_enclosing_body, ty::implements_trait, - usage::local_used_after_expr, + visitors::for_each_expr, }; -use rustc_hir::{Expr, Node}; + +use rustc_hir::{Expr, ExprKind, HirId, Node, PatKind, StmtKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::Ty; @@ -54,45 +56,144 @@ impl<'tcx> LateLintPass<'tcx> for ImmediatelyBindScoped { // First we check if we have found a `Scoped::get` call if is_scoped_get_method_call(cx, expr) { // Which is followed by a trait method call to `bind` in which case - // it is all done properly and we can exit out of the lint + // it is all done properly and we can exit out of the lint. if let Some(parent) = get_parent_expr(cx, expr) && is_bindable_bind_method_call(cx, parent) { return; } - // If the `Scoped::get` call is never used or unified we can - // safely exit out of the rule, otherwise we need to look into how - // it's used. - let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr) else { - return; - }; - - if !local_used_after_expr(cx, hir_id, expr) { + // Check if the unbound value is used in an argument position of a + // method or function call where binding can be safely skipped. + if is_in_argument_position(cx, expr) { return; } - // Now we are onto something! If the expression is returned, used - // after the expression or assigned to a variable we might have - // found an issue. - if let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr) - && (potential_return_of_enclosing_body(cx, expr) - || local_used_after_expr(cx, hir_id, expr) - || matches!(usage, Node::LetStmt(_))) + // If the expression is assigned to a local variable, we need to + // check that it's next use is binding or as a function argument. + if let Some(local_hir_id) = get_assigned_local(cx, expr) + && let Some(enclosing_block) = get_enclosing_block(cx, expr.hir_id) { - span_lint_and_help( - cx, - IMMEDIATELY_BIND_SCOPED, - expr.span, - "the result of `Scoped::get` should be immediately bound", - None, - "immediately bind the value", - ); + let mut found_valid_next_use = false; + + // Look for the next use of this local after the current expression. + // We need to traverse the statements in the block to find proper usage + for stmt in enclosing_block + .stmts + .iter() + .skip_while(|s| s.span.lo() < expr.span.hi()) + { + // Extract relevant expressions from the statement and check + // it for a use valid of the local variable. + let Some(stmt_expr) = (match &stmt.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => Some(*expr), + StmtKind::Let(local) => local.init, + _ => None, + }) else { + continue; + }; + + // Check each expression in the current statement for use + // of the value, breaking when found and optionally marking + // it as valid. + if for_each_expr(cx, stmt_expr, |expr_in_stmt| { + if path_to_local_id(expr_in_stmt, local_hir_id) { + if is_valid_use_of_unbound_value(cx, expr_in_stmt, local_hir_id) { + found_valid_next_use = true; + } + + return ControlFlow::Break(true); + } + ControlFlow::Continue(()) + }) + .unwrap_or(false) + { + break; + } + } + + if !found_valid_next_use { + span_lint_and_help( + cx, + IMMEDIATELY_BIND_SCOPED, + expr.span, + "the result of `Scoped::get` should be immediately bound", + None, + "immediately bind the value", + ); + } } } } } +/// Check if an expression is assigned to a local variable and return the local's HirId +fn get_assigned_local(cx: &LateContext<'_>, expr: &Expr) -> Option { + let parent_node = cx.tcx.parent_hir_id(expr.hir_id); + + if let Node::LetStmt(local) = cx.tcx.hir_node(parent_node) + && let Some(init) = local.init + && init.hir_id == expr.hir_id + && let PatKind::Binding(_, hir_id, _, _) = local.pat.kind + { + Some(hir_id) + } else { + None + } +} + +/// Check if a use of an unbound value is valid (binding or function argument) +fn is_valid_use_of_unbound_value(cx: &LateContext<'_>, expr: &Expr, hir_id: HirId) -> bool { + // Check if we're in a method call and if so, check if it's a bind call + if let Some(parent) = get_parent_expr(cx, expr) + && is_bindable_bind_method_call(cx, parent) + { + return true; + } + + // If this is a method call to bind() on our local, it's valid + if is_bindable_bind_method_call(cx, expr) { + return true; + } + + // If this is the local being used as a function argument, it's valid + if path_to_local_id(expr, hir_id) && is_in_argument_position(cx, expr) { + return true; + } + + false +} + +/// Check if an expression is in an argument position where binding can be skipped +fn is_in_argument_position(cx: &LateContext<'_>, expr: &Expr) -> bool { + let mut current_expr = expr; + + // Walk up the parent chain to see if we're in a function call argument + while let Some(parent) = get_parent_expr(cx, current_expr) { + match parent.kind { + // If we find a method call where our expression is an argument (not receiver) + ExprKind::MethodCall(_, receiver, args, _) => { + if receiver.hir_id != current_expr.hir_id + && args.iter().any(|arg| arg.hir_id == current_expr.hir_id) + { + return true; + } + } + // If we find a function call where our expression is an argument + ExprKind::Call(_, args) => { + if args.iter().any(|arg| arg.hir_id == current_expr.hir_id) { + return true; + } + } + // Continue walking up for other expression types + _ => {} + } + current_expr = parent; + } + + false +} + fn is_scoped_get_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool { if let Some((method, recv, _, _, _)) = method_call(expr) && method == "get" @@ -118,18 +219,6 @@ fn is_bindable_bind_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool { } } -fn is_bindable_unbind_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool { - if let Some((method, _, _, _, _)) = method_call(expr) - && method == "unbind" - && let expr_ty = cx.typeck_results().expr_ty(expr) - && implements_bindable_trait(cx, &expr_ty) - { - true - } else { - false - } -} - fn implements_bindable_trait<'tcx>(cx: &LateContext<'tcx>, ty: &Ty<'tcx>) -> bool { lookup_path_str(cx.tcx, PathNS::Type, "nova_vm::engine::context::Bindable") .first() diff --git a/nova_lint/ui/immediately_bind_scoped.rs b/nova_lint/ui/immediately_bind_scoped.rs index 66027a63c..372991f24 100644 --- a/nova_lint/ui/immediately_bind_scoped.rs +++ b/nova_lint/ui/immediately_bind_scoped.rs @@ -17,14 +17,13 @@ fn test_scoped_get_can_get_bound_right_after(agent: &Agent, scoped: Scoped, gc: NoGcScope, ) { let a = scoped.get(agent); - let b = a.bind(gc); - a.is_undefined(); + (a.bind(gc), ()); } fn test_scoped_get_can_be_immediately_passed_on( diff --git a/nova_lint/ui/immediately_bind_scoped.stderr b/nova_lint/ui/immediately_bind_scoped.stderr index 91b65f9df..f38b2e398 100644 --- a/nova_lint/ui/immediately_bind_scoped.stderr +++ b/nova_lint/ui/immediately_bind_scoped.stderr @@ -1,25 +1,19 @@ error: the result of `Scoped::get` should be immediately bound - --> $DIR/immediately_bind_scoped.rs:25:13 - | -LL | let a = scoped.get(agent); - | ^^^^^^^^^^^^^^^^^ - | - = help: immediately bind the value - -error: the result of `Scoped::get` should be immediately bound - --> $DIR/immediately_bind_scoped.rs:44:14 + --> $DIR/immediately_bind_scoped.rs:43:14 | LL | let _a = scoped.get(agent); | ^^^^^^^^^^^^^^^^^ | = help: immediately bind the value + = note: `#[deny(immediately_bind_scoped)]` on by default error: the result of `Scoped::get` should be immediately bound - --> $DIR/immediately_bind_scoped.rs:56:14 + --> $DIR/immediately_bind_scoped.rs:55:14 | LL | let _a = Scoped::new(agent, Value::Undefined, gc).get(agent); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: immediately bind the value -error: aborting due to 3 previous errors +error: aborting due to 2 previous errors +