Skip to content

Commit 1eeee0c

Browse files
committed
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
1 parent ffb1115 commit 1eeee0c

File tree

3 files changed

+136
-54
lines changed

3 files changed

+136
-54
lines changed

nova_lint/src/immediately_bind_scoped.rs

Lines changed: 129 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
use std::ops::ControlFlow;
2+
13
use crate::{is_scoped_ty, method_call};
24
use clippy_utils::{
35
diagnostics::span_lint_and_help,
4-
get_expr_use_or_unification_node, get_parent_expr,
6+
get_enclosing_block, get_parent_expr, path_to_local_id,
57
paths::{PathNS, lookup_path_str},
6-
potential_return_of_enclosing_body,
78
ty::implements_trait,
8-
usage::local_used_after_expr,
9+
visitors::for_each_expr,
910
};
10-
use rustc_hir::{Expr, Node};
11+
12+
use rustc_hir::{Expr, ExprKind, HirId, Node, PatKind, StmtKind};
1113
use rustc_lint::{LateContext, LateLintPass};
1214
use rustc_middle::ty::Ty;
1315

@@ -54,45 +56,144 @@ impl<'tcx> LateLintPass<'tcx> for ImmediatelyBindScoped {
5456
// First we check if we have found a `Scoped<Value>::get` call
5557
if is_scoped_get_method_call(cx, expr) {
5658
// Which is followed by a trait method call to `bind` in which case
57-
// it is all done properly and we can exit out of the lint
59+
// it is all done properly and we can exit out of the lint.
5860
if let Some(parent) = get_parent_expr(cx, expr)
5961
&& is_bindable_bind_method_call(cx, parent)
6062
{
6163
return;
6264
}
6365

64-
// If the `Scoped<Value>::get` call is never used or unified we can
65-
// safely exit out of the rule, otherwise we need to look into how
66-
// it's used.
67-
let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr) else {
68-
return;
69-
};
70-
71-
if !local_used_after_expr(cx, hir_id, expr) {
66+
// Check if the unbound value is used in an argument position of a
67+
// method or function call where binding can be safely skipped.
68+
if is_in_argument_position(cx, expr) {
7269
return;
7370
}
7471

75-
// Now we are onto something! If the expression is returned, used
76-
// after the expression or assigned to a variable we might have
77-
// found an issue.
78-
if let Some((usage, hir_id)) = get_expr_use_or_unification_node(cx.tcx, expr)
79-
&& (potential_return_of_enclosing_body(cx, expr)
80-
|| local_used_after_expr(cx, hir_id, expr)
81-
|| matches!(usage, Node::LetStmt(_)))
72+
// If the expression is assigned to a local variable, we need to
73+
// check that it's next use is binding or as a function argument.
74+
if let Some(local_hir_id) = get_assigned_local(cx, expr)
75+
&& let Some(enclosing_block) = get_enclosing_block(cx, expr.hir_id)
8276
{
83-
span_lint_and_help(
84-
cx,
85-
IMMEDIATELY_BIND_SCOPED,
86-
expr.span,
87-
"the result of `Scoped<Value>::get` should be immediately bound",
88-
None,
89-
"immediately bind the value",
90-
);
77+
let mut found_valid_next_use = false;
78+
79+
// Look for the next use of this local after the current expression.
80+
// We need to traverse the statements in the block to find proper usage
81+
for stmt in enclosing_block
82+
.stmts
83+
.iter()
84+
.skip_while(|s| s.span.lo() < expr.span.hi())
85+
{
86+
// Extract relevant expressions from the statement and check
87+
// it for a use valid of the local variable.
88+
let Some(stmt_expr) = (match &stmt.kind {
89+
StmtKind::Expr(expr) | StmtKind::Semi(expr) => Some(*expr),
90+
StmtKind::Let(local) => local.init,
91+
_ => None,
92+
}) else {
93+
continue;
94+
};
95+
96+
// Check each expression in the current statement for use
97+
// of the value, breaking when found and optionally marking
98+
// it as valid.
99+
if for_each_expr(cx, stmt_expr, |expr_in_stmt| {
100+
if path_to_local_id(expr_in_stmt, local_hir_id) {
101+
if is_valid_use_of_unbound_value(cx, expr_in_stmt, local_hir_id) {
102+
found_valid_next_use = true;
103+
}
104+
105+
return ControlFlow::Break(true);
106+
}
107+
ControlFlow::Continue(())
108+
})
109+
.unwrap_or(false)
110+
{
111+
break;
112+
}
113+
}
114+
115+
if !found_valid_next_use {
116+
span_lint_and_help(
117+
cx,
118+
IMMEDIATELY_BIND_SCOPED,
119+
expr.span,
120+
"the result of `Scoped<Value>::get` should be immediately bound",
121+
None,
122+
"immediately bind the value",
123+
);
124+
}
91125
}
92126
}
93127
}
94128
}
95129

130+
/// Check if an expression is assigned to a local variable and return the local's HirId
131+
fn get_assigned_local(cx: &LateContext<'_>, expr: &Expr) -> Option<HirId> {
132+
let parent_node = cx.tcx.parent_hir_id(expr.hir_id);
133+
134+
if let Node::LetStmt(local) = cx.tcx.hir_node(parent_node)
135+
&& let Some(init) = local.init
136+
&& init.hir_id == expr.hir_id
137+
&& let PatKind::Binding(_, hir_id, _, _) = local.pat.kind
138+
{
139+
Some(hir_id)
140+
} else {
141+
None
142+
}
143+
}
144+
145+
/// Check if a use of an unbound value is valid (binding or function argument)
146+
fn is_valid_use_of_unbound_value(cx: &LateContext<'_>, expr: &Expr, hir_id: HirId) -> bool {
147+
// Check if we're in a method call and if so, check if it's a bind call
148+
if let Some(parent) = get_parent_expr(cx, expr)
149+
&& is_bindable_bind_method_call(cx, parent)
150+
{
151+
return true;
152+
}
153+
154+
// If this is a method call to bind() on our local, it's valid
155+
if is_bindable_bind_method_call(cx, expr) {
156+
return true;
157+
}
158+
159+
// If this is the local being used as a function argument, it's valid
160+
if path_to_local_id(expr, hir_id) && is_in_argument_position(cx, expr) {
161+
return true;
162+
}
163+
164+
false
165+
}
166+
167+
/// Check if an expression is in an argument position where binding can be skipped
168+
fn is_in_argument_position(cx: &LateContext<'_>, expr: &Expr) -> bool {
169+
let mut current_expr = expr;
170+
171+
// Walk up the parent chain to see if we're in a function call argument
172+
while let Some(parent) = get_parent_expr(cx, current_expr) {
173+
match parent.kind {
174+
// If we find a method call where our expression is an argument (not receiver)
175+
ExprKind::MethodCall(_, receiver, args, _) => {
176+
if receiver.hir_id != current_expr.hir_id
177+
&& args.iter().any(|arg| arg.hir_id == current_expr.hir_id)
178+
{
179+
return true;
180+
}
181+
}
182+
// If we find a function call where our expression is an argument
183+
ExprKind::Call(_, args) => {
184+
if args.iter().any(|arg| arg.hir_id == current_expr.hir_id) {
185+
return true;
186+
}
187+
}
188+
// Continue walking up for other expression types
189+
_ => {}
190+
}
191+
current_expr = parent;
192+
}
193+
194+
false
195+
}
196+
96197
fn is_scoped_get_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool {
97198
if let Some((method, recv, _, _, _)) = method_call(expr)
98199
&& method == "get"
@@ -118,18 +219,6 @@ fn is_bindable_bind_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool {
118219
}
119220
}
120221

121-
fn is_bindable_unbind_method_call(cx: &LateContext<'_>, expr: &Expr) -> bool {
122-
if let Some((method, _, _, _, _)) = method_call(expr)
123-
&& method == "unbind"
124-
&& let expr_ty = cx.typeck_results().expr_ty(expr)
125-
&& implements_bindable_trait(cx, &expr_ty)
126-
{
127-
true
128-
} else {
129-
false
130-
}
131-
}
132-
133222
fn implements_bindable_trait<'tcx>(cx: &LateContext<'tcx>, ty: &Ty<'tcx>) -> bool {
134223
lookup_path_str(cx.tcx, PathNS::Type, "nova_vm::engine::context::Bindable")
135224
.first()

nova_lint/ui/immediately_bind_scoped.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@ fn test_scoped_get_can_get_bound_right_after(agent: &Agent, scoped: Scoped<Value
1717
a.bind(gc);
1818
}
1919

20-
fn test_scoped_get_can_get_bound_right_after_but_never_used_again(
20+
fn test_scoped_get_can_get_bound_in_tuple_right_after(
2121
agent: &Agent,
2222
scoped: Scoped<Value>,
2323
gc: NoGcScope,
2424
) {
2525
let a = scoped.get(agent);
26-
let b = a.bind(gc);
27-
a.is_undefined();
26+
(a.bind(gc), ());
2827
}
2928

3029
fn test_scoped_get_can_be_immediately_passed_on(
Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
error: the result of `Scoped<Value>::get` should be immediately bound
2-
--> $DIR/immediately_bind_scoped.rs:25:13
3-
|
4-
LL | let a = scoped.get(agent);
5-
| ^^^^^^^^^^^^^^^^^
6-
|
7-
= help: immediately bind the value
8-
9-
error: the result of `Scoped<Value>::get` should be immediately bound
10-
--> $DIR/immediately_bind_scoped.rs:44:14
2+
--> $DIR/immediately_bind_scoped.rs:43:14
113
|
124
LL | let _a = scoped.get(agent);
135
| ^^^^^^^^^^^^^^^^^
146
|
157
= help: immediately bind the value
8+
= note: `#[deny(immediately_bind_scoped)]` on by default
169

1710
error: the result of `Scoped<Value>::get` should be immediately bound
18-
--> $DIR/immediately_bind_scoped.rs:56:14
11+
--> $DIR/immediately_bind_scoped.rs:55:14
1912
|
2013
LL | let _a = Scoped::new(agent, Value::Undefined, gc).get(agent);
2114
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2215
|
2316
= help: immediately bind the value
2417

25-
error: aborting due to 3 previous errors
18+
error: aborting due to 2 previous errors
19+

0 commit comments

Comments
 (0)