Skip to content

Commit 4572c12

Browse files
committed
feat(linter): Implement immediately_bind_scoped rule
1 parent ab81608 commit 4572c12

7 files changed

Lines changed: 250 additions & 3 deletions

File tree

nova_lint/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ path = "ui/gc_scope_comes_last.rs"
2424
name = "gc_scope_is_only_passed_by_value"
2525
path = "ui/gc_scope_is_only_passed_by_value.rs"
2626

27+
[[example]]
28+
name = "immediately_bind_scoped"
29+
path = "ui/immediately_bind_scoped.rs"
30+
2731
[dependencies]
28-
clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "0450db33a5d8587f7c1d4b6d233dac963605766b" }
32+
clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "334fb906aef13d20050987b13448f37391bb97a2" }
2933
dylint_linting = { version = "4.1.0", features = ["constituent"] }
3034

3135
[dev-dependencies]

nova_lint/rust-toolchain.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[toolchain]
2-
channel = "nightly-2025-05-14"
2+
channel = "nightly-2025-08-07"
33
components = ["llvm-tools-preview", "rustc-dev"]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use clippy_utils::paths::{PathLookup, PathNS, lookup_path_str};
2+
use clippy_utils::sym::Symbol;
3+
use clippy_utils::ty::implements_trait;
4+
use clippy_utils::usage::local_used_after_expr;
5+
use clippy_utils::{diagnostics::span_lint_and_help, is_self};
6+
use clippy_utils::{
7+
get_expr_use_or_unification_node, get_parent_expr, is_expr_final_block_expr, is_trait_method,
8+
path_def_id, peel_blocks, potential_return_of_enclosing_body,
9+
};
10+
use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind};
11+
use rustc_hir::{Expr, ExprKind, LetStmt, Node};
12+
use rustc_lint::{LateContext, LateLintPass};
13+
use rustc_span::symbol::Symbol;
14+
15+
use crate::{is_scoped_ty, method_call};
16+
17+
dylint_linting::declare_late_lint! {
18+
/// ### What it does
19+
///
20+
/// Makes sure that the user immediately binds `Scoped<Value>::get` results.
21+
///
22+
/// ### Why is this bad?
23+
///
24+
/// TODO: Write an explanation of why this is bad.
25+
///
26+
/// ### Example
27+
///
28+
/// ```
29+
/// let a = scoped_a.get(agent);
30+
/// ```
31+
///
32+
/// Use instead:
33+
///
34+
/// ```
35+
/// let a = scoped_a.get(agent).bind(gc.nogc());
36+
/// ```
37+
///
38+
/// Which ensures that no odd bugs occur.
39+
///
40+
/// ### Exception: If the result is immediately used without assigning to a
41+
/// variable, binding can be skipped.
42+
///
43+
/// ```
44+
/// scoped_a.get(agent).internal_delete(agent, scoped_b.get(agent), gc.reborrow());
45+
/// ```
46+
///
47+
/// Here it is perfectly okay to skip the binding for both `scoped_a` and
48+
/// `scoped_b` as the borrow checker would force you to again unbind both
49+
/// `Value`s immediately.
50+
pub IMMEDIATELY_BIND_SCOPED,
51+
Deny,
52+
"the result of `Scoped<Value>::get` should be immediately bound"
53+
}
54+
55+
impl<'tcx> LateLintPass<'tcx> for ImmediatelyBindScoped {
56+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
57+
// First we check if we have found a `Scoped<Value>::get` call
58+
if let Some((method, recv, _, _, _)) = method_call(expr)
59+
&& method == "get"
60+
&& let typeck_results = cx.typeck_results()
61+
&& let recv_ty = typeck_results.expr_ty(recv)
62+
&& is_scoped_ty(cx, &recv_ty)
63+
{
64+
// Which is followed by a trait method call to `bind` in which case
65+
// it is all done properly and we can exit out of the lint
66+
if let Some(parent) = get_parent_expr(cx, expr)
67+
&& let Some((parent_method, _, _, _, _)) = method_call(parent)
68+
&& parent_method == "bind"
69+
&& let parent_ty = typeck_results.expr_ty(parent)
70+
&& let Some(&trait_def_id) =
71+
lookup_path_str(cx.tcx, PathNS::Type, "nova_vm::engine::context::Bindable")
72+
.first()
73+
&& implements_trait(cx, parent_ty, trait_def_id, &[])
74+
{
75+
return;
76+
}
77+
78+
// Now we are onto something! If the expression is returned, used
79+
// after the expression or assigned to a variable we might have
80+
// found an issue.
81+
if let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr)
82+
&& (potential_return_of_enclosing_body(cx, expr)
83+
|| local_used_after_expr(cx, hir_id, expr)
84+
|| matches!(
85+
usage,
86+
Node::LetStmt(LetStmt {
87+
init: Some(hir_id),
88+
..
89+
})
90+
))
91+
{
92+
span_lint_and_help(
93+
cx,
94+
IMMEDIATELY_BIND_SCOPED,
95+
expr.span,
96+
"the result of `Scoped<Value>::get` should be immediately bound",
97+
None,
98+
"immediately bind the value",
99+
);
100+
}
101+
}
102+
}
103+
}

nova_lint/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ extern crate rustc_trait_selection;
2525
mod agent_comes_first;
2626
mod gc_scope_comes_last;
2727
mod gc_scope_is_only_passed_by_value;
28+
mod immediately_bind_scoped;
2829
mod utils;
2930

3031
pub(crate) use utils::*;
@@ -34,6 +35,7 @@ pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint
3435
agent_comes_first::register_lints(sess, lint_store);
3536
gc_scope_comes_last::register_lints(sess, lint_store);
3637
gc_scope_is_only_passed_by_value::register_lints(sess, lint_store);
38+
immediately_bind_scoped::register_lints(sess, lint_store);
3739
}
3840

3941
#[test]

nova_lint/src/utils.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use rustc_hir::def_id::DefId;
1+
use rustc_hir::{Expr, ExprKind, def_id::DefId};
22
use rustc_lint::LateContext;
33
use rustc_middle::ty::{Ty, TyKind};
4+
use rustc_span::Span;
45
use rustc_span::symbol::Symbol;
56

67
// Copyright (c) 2014-2025 The Rust Project Developers
@@ -19,6 +20,25 @@ pub fn match_def_path(cx: &LateContext<'_>, did: DefId, syms: &[&str]) -> bool {
1920
.eq(path.iter().copied())
2021
}
2122

23+
// Copyright (c) 2014-2025 The Rust Project Developers
24+
//
25+
// Originally copied from `dylint` which in turn copied it from `clippy_lints`:
26+
// - https://github.com/trailofbits/dylint/blob/d1be1c42f363ca11f8ebce0ff0797ecbbcc3680b/examples/restriction/collapsible_unwrap/src/lib.rs#L180
27+
// - https://github.com/rust-lang/rust-clippy/blob/3f015a363020d3811e1f028c9ce4b0705c728289/clippy_lints/src/methods/mod.rs#L3293-L3304
28+
/// Extracts a method call name, args, and `Span` of the method name.
29+
pub fn method_call<'tcx>(
30+
recv: &'tcx Expr<'tcx>,
31+
) -> Option<(&'tcx str, &'tcx Expr<'tcx>, &'tcx [Expr<'tcx>], Span, Span)> {
32+
if let ExprKind::MethodCall(path, receiver, args, call_span) = recv.kind
33+
&& !args.iter().any(|e| e.span.from_expansion())
34+
&& !receiver.span.from_expansion()
35+
{
36+
let name = path.ident.name.as_str();
37+
return Some((name, receiver, args, path.ident.span, call_span));
38+
}
39+
None
40+
}
41+
2242
pub fn is_param_ty(ty: &Ty) -> bool {
2343
matches!(ty.kind(), TyKind::Param(_))
2444
}
@@ -53,3 +73,39 @@ pub fn is_no_gc_scope_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
5373
_ => false,
5474
}
5575
}
76+
77+
pub fn is_scoped_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
78+
match ty.kind() {
79+
TyKind::Adt(def, _) => match_def_path(
80+
cx,
81+
def.did(),
82+
&["nova_vm", "engine", "rootable", "scoped", "Scoped"],
83+
),
84+
_ => false,
85+
}
86+
}
87+
88+
// Checks if a given expression is assigned to a variable.
89+
//
90+
// Copyright (c) 2014-2025 The Rust Project Developers
91+
//
92+
// Copied and modified from `clippy_utils`:
93+
// - https://github.com/rust-lang/rust-clippy/blob/8a5dc7c1713a7eb9af28bf9f53dc6b61da7aad90/clippy_utils/src/lib.rs#L1369-L1388
94+
// pub fn is_inside_let(tcx: TyCtxt<'_>, expr: &Expr<'_>) -> bool {
95+
// let mut child_id = expr.hir_id;
96+
// for (parent_id, node) in tcx.hir_parent_iter(child_id) {
97+
// if let Node::LetStmt(LetStmt {
98+
// init: Some(init),
99+
// els: Some(els),
100+
// ..
101+
// }) = node
102+
// && (init.hir_id == child_id || els.hir_id == child_id)
103+
// {
104+
// return true;
105+
// }
106+
107+
// child_id = parent_id;
108+
// }
109+
110+
// false
111+
// }
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#![allow(dead_code, unused_variables, clippy::disallowed_names)]
2+
3+
use nova_vm::{
4+
ecmascript::{execution::Agent, types::Value},
5+
engine::{
6+
Scoped,
7+
context::{Bindable, NoGcScope},
8+
},
9+
};
10+
11+
fn test_scoped_get_is_immediately_bound(agent: &Agent, scoped: Scoped<Value>, gc: NoGcScope) {
12+
let _a = scoped.get(agent).bind(gc);
13+
}
14+
15+
// TODO: These are valid patterns, which are found in certain parts of the
16+
// codebase so should ideally be implemented.
17+
// fn test_scoped_get_can_get_bound_right_after(agent: &Agent, scoped: Scoped<Value>, gc: NoGcScope) {
18+
// let a = scoped.get(agent);
19+
// a.bind(gc);
20+
// }
21+
//
22+
// fn test_scoped_get_can_get_bound_right_after_and_never_used_again(
23+
// agent: &Agent,
24+
// scoped: Scoped<Value>,
25+
// gc: NoGcScope,
26+
// ) {
27+
// let a = scoped.get(agent);
28+
// let b = a.bind(gc);
29+
// a;
30+
// }
31+
//
32+
// fn test_scoped_get_can_be_immediately_passed_on(
33+
// agent: &Agent,
34+
// scoped: Scoped<Value>,
35+
// gc: NoGcScope,
36+
// ) {
37+
// let a = scoped.get(agent);
38+
// test_consumes_unbound_value(value);
39+
// }
40+
//
41+
// fn test_consumes_unbound_value(value: Value) {
42+
// unimplemented!()
43+
// }
44+
45+
fn test_scoped_get_is_not_immediately_bound(agent: &Agent, scoped: Scoped<Value>) {
46+
let _a = scoped.get(agent);
47+
}
48+
49+
fn test_scoped_get_doesnt_need_to_be_bound_if_not_assigned(agent: &Agent, scoped: Scoped<Value>) {
50+
scoped.get(agent);
51+
}
52+
53+
fn test_improbable_but_technically_bad_situation(
54+
agent: &Agent,
55+
scoped: Scoped<Value>,
56+
gc: NoGcScope,
57+
) {
58+
let _a = Scoped::new(agent, Value::Undefined, gc).get(agent);
59+
}
60+
61+
fn main() {
62+
unimplemented!()
63+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
error: the result of `Scoped<Value>::get` should be immediately bound
2+
--> $DIR/immediately_bind_scoped.rs:16:14
3+
|
4+
LL | let _a = scoped.get(agent);
5+
| ^^^^^^^^^^^^^^^^^
6+
|
7+
= help: immediately bind the value
8+
= note: `#[deny(immediately_bind_scoped)]` on by default
9+
10+
error: the result of `Scoped<Value>::get` should be immediately bound
11+
--> $DIR/immediately_bind_scoped.rs:24:14
12+
|
13+
LL | let _a = Scoped::new(agent, Value::Undefined, gc).get(agent);
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15+
|
16+
= help: immediately bind the value
17+
18+
error: aborting due to 2 previous errors
19+

0 commit comments

Comments
 (0)