Skip to content

Commit 72a1d45

Browse files
feat(linter): Rule which warns when you could've used NoGcScope (#913)
1 parent 81d3387 commit 72a1d45

18 files changed

+449
-34
lines changed

nova_cli/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// This Source Code Form is subject to the terms of the Mozilla Public
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
#![allow(unknown_lints, can_use_no_gc_scope)]
5+
46
mod helper;
57
mod theme;
68

nova_lint/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ crate-type = ["cdylib"]
1616
name = "agent_comes_first"
1717
path = "ui/agent_comes_first.rs"
1818

19+
[[example]]
20+
name = "can_use_no_gc_scope"
21+
path = "ui/can_use_no_gc_scope.rs"
22+
1923
[[example]]
2024
name = "gc_scope_comes_last"
2125
path = "ui/gc_scope_comes_last.rs"

nova_lint/src/agent_comes_first.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clippy_utils::{diagnostics::span_lint_and_help, is_self};
2-
use rustc_hir::{def_id::LocalDefId, intravisit::FnKind, Body, FnDecl};
2+
use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind};
33
use rustc_lint::{LateContext, LateLintPass};
44
use rustc_span::Span;
55

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use std::ops::ControlFlow;
2+
3+
use clippy_utils::{
4+
diagnostics::span_lint_and_sugg, fn_def_id, is_trait_impl_item, source::HasSession,
5+
visitors::for_each_expr,
6+
};
7+
use rustc_errors::Applicability;
8+
use rustc_hir::{Body, Expr, ExprKind, FnDecl, def::Res, def_id::LocalDefId, intravisit::FnKind};
9+
use rustc_hir_analysis::lower_ty;
10+
use rustc_lint::{LateContext, LateLintPass};
11+
use rustc_span::{BytePos, Span};
12+
13+
use crate::{could_be_builtin_method_def, is_gc_scope_ty, is_no_gc_method, is_trait_item};
14+
15+
dylint_linting::declare_late_lint! {
16+
/// ### What it does
17+
///
18+
/// Checks that a function which only needs `NoGcScope` uses it instead of
19+
/// `GcScope`.
20+
///
21+
/// ### Why is this bad?
22+
///
23+
/// You usually should use `NoGcScope` instead of `GcScope` if you don't
24+
/// need the latter. The reason this is bad is that it forces the caller
25+
/// to scope any heap references held past the call site unnecessarily.
26+
///
27+
/// ### Example
28+
///
29+
/// ```rust
30+
/// fn foo(gc: GcScope<'_, '_>) {}
31+
/// ```
32+
///
33+
/// Use instead:
34+
///
35+
/// ```rust
36+
/// fn foo(gc: NoGcScope<'_, '_>) {}
37+
/// ```
38+
pub CAN_USE_NO_GC_SCOPE,
39+
Warn,
40+
"you should use `NoGcScope` instead of `GcScope` if you don't need the latter"
41+
}
42+
43+
impl<'tcx> LateLintPass<'tcx> for CanUseNoGcScope {
44+
fn check_fn(
45+
&mut self,
46+
cx: &LateContext<'tcx>,
47+
kind: FnKind<'tcx>,
48+
_: &'tcx FnDecl<'tcx>,
49+
body: &'tcx Body<'tcx>,
50+
span: Span,
51+
_: LocalDefId,
52+
) {
53+
if span.from_expansion() {
54+
return;
55+
}
56+
57+
// Skip closures
58+
if matches!(kind, FnKind::Closure) {
59+
return;
60+
}
61+
62+
// Skip trait definitions and methods
63+
let res = {
64+
let body_id = cx.tcx.hir_body_owner(body.id());
65+
is_trait_impl_item(cx, body_id) || is_trait_item(cx, body_id)
66+
};
67+
if res {
68+
return;
69+
}
70+
71+
// Skip builtin methods
72+
if could_be_builtin_method_def(cx, kind) {
73+
return;
74+
}
75+
76+
// Skip `GcScope` methods
77+
if let FnKind::Method(_, sig) = kind
78+
&& let Some(maybe_self) = sig.decl.inputs.first()
79+
&& is_gc_scope_ty(cx, &lower_ty(cx.tcx, maybe_self))
80+
{
81+
return;
82+
}
83+
84+
let typeck = cx.typeck_results();
85+
86+
let Some(gc_scope) = body.params.iter().find(|param| {
87+
let ty = typeck.pat_ty(param.pat);
88+
is_gc_scope_ty(cx, &ty)
89+
}) else {
90+
// Either the function already takes `NoGcScope` or it doesn't take any
91+
// at all in which case we don't need to lint.
92+
return;
93+
};
94+
95+
if for_each_expr(cx, body.value, |expr| {
96+
// Checks if the expression is function or method call
97+
if let Some(did) = fn_def_id(cx, expr)
98+
// If we encountered either a `nogc` och `into_nogc` method call
99+
// we skip them because they don't count as needing a `GcScope`.
100+
&& !is_no_gc_method(cx, did)
101+
{
102+
// Check if the function actually uses `GcScope` in its signature
103+
let sig = cx.tcx.fn_sig(did).instantiate_identity().skip_binder();
104+
if sig.inputs().iter().any(|input| is_gc_scope_ty(cx, input)) {
105+
return ControlFlow::Break(());
106+
}
107+
}
108+
109+
// Calls to closures and other functions may also use `GcScope`,
110+
// we need to check those as well.
111+
if let ExprKind::Call(
112+
Expr {
113+
kind: ExprKind::Path(qpath),
114+
hir_id: path_hir_id,
115+
..
116+
},
117+
args,
118+
) = expr.kind
119+
&& let Res::Local(_) = typeck.qpath_res(qpath, *path_hir_id)
120+
&& args.iter().any(|arg| {
121+
let ty = typeck.expr_ty(arg);
122+
is_gc_scope_ty(cx, &ty)
123+
})
124+
{
125+
return ControlFlow::Break(());
126+
}
127+
128+
ControlFlow::Continue(())
129+
})
130+
.is_none()
131+
{
132+
// We didn't find any calls in the body that would require a `GcScope`
133+
// so we can suggest using `NoGcScope` instead.
134+
let ty_span = cx
135+
.sess()
136+
.source_map()
137+
.span_until_char(gc_scope.ty_span, '<');
138+
// Trim the start of the span to just before the `GcScope` type name
139+
let ty_span = ty_span.with_lo(ty_span.hi() - BytePos(7));
140+
span_lint_and_sugg(
141+
cx,
142+
CAN_USE_NO_GC_SCOPE,
143+
ty_span,
144+
"you can use `NoGcScope` instead of `GcScope` here",
145+
"replace with",
146+
"NoGcScope".to_owned(),
147+
Applicability::MaybeIncorrect,
148+
);
149+
}
150+
}
151+
}

nova_lint/src/gc_scope_comes_last.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clippy_utils::diagnostics::span_lint_and_help;
2-
use rustc_hir::{def_id::LocalDefId, intravisit::FnKind, Body, FnDecl};
2+
use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind};
33
use rustc_lint::{LateContext, LateLintPass};
44
use rustc_span::Span;
55

nova_lint/src/gc_scope_is_only_passed_by_value.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clippy_utils::{diagnostics::span_lint_and_help, is_self};
2-
use rustc_hir::{def_id::LocalDefId, intravisit::FnKind, Body, FnDecl};
2+
use rustc_hir::{Body, FnDecl, def_id::LocalDefId, intravisit::FnKind};
33
use rustc_lint::{LateContext, LateLintPass};
44
use rustc_middle::ty::TyKind;
55
use rustc_span::Span;

nova_lint/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ extern crate rustc_ast_pretty;
99
extern crate rustc_data_structures;
1010
extern crate rustc_errors;
1111
extern crate rustc_hir;
12+
extern crate rustc_hir_analysis;
1213
extern crate rustc_hir_pretty;
14+
extern crate rustc_hir_typeck;
1315
extern crate rustc_index;
1416
extern crate rustc_infer;
1517
extern crate rustc_lexer;
@@ -23,6 +25,7 @@ extern crate rustc_target;
2325
extern crate rustc_trait_selection;
2426

2527
mod agent_comes_first;
28+
mod can_use_no_gc_scope;
2629
mod gc_scope_comes_last;
2730
mod gc_scope_is_only_passed_by_value;
2831
mod no_it_performs_the_following;
@@ -35,6 +38,7 @@ pub(crate) use utils::*;
3538
#[unsafe(no_mangle)]
3639
pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) {
3740
agent_comes_first::register_lints(sess, lint_store);
41+
can_use_no_gc_scope::register_lints(sess, lint_store);
3842
gc_scope_comes_last::register_lints(sess, lint_store);
3943
gc_scope_is_only_passed_by_value::register_lints(sess, lint_store);
4044
no_it_performs_the_following::register_lints(sess, lint_store);

nova_lint/src/utils.rs

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use rustc_hir::def_id::DefId;
1+
use clippy_utils::peel_hir_ty_options;
2+
use rustc_hir::{FnSig, HirId, ItemKind, Node, def_id::DefId, intravisit::FnKind};
3+
use rustc_hir_analysis::lower_ty;
24
use rustc_lint::LateContext;
35
use rustc_middle::ty::{Ty, TyKind};
46
use rustc_span::symbol::Symbol;
@@ -19,6 +21,23 @@ pub fn match_def_path(cx: &LateContext<'_>, did: DefId, syms: &[&str]) -> bool {
1921
.eq(path.iter().copied())
2022
}
2123

24+
pub fn match_def_paths(cx: &LateContext<'_>, did: DefId, syms: &[&[&str]]) -> bool {
25+
let path = cx.get_def_path(did);
26+
syms.iter().any(|syms| {
27+
syms.iter()
28+
.map(|x| Symbol::intern(x))
29+
.eq(path.iter().copied())
30+
})
31+
}
32+
33+
pub fn is_trait_item(cx: &LateContext<'_>, hir_id: HirId) -> bool {
34+
if let Node::Item(item) = cx.tcx.parent_hir_node(hir_id) {
35+
matches!(item.kind, ItemKind::Trait(..))
36+
} else {
37+
false
38+
}
39+
}
40+
2241
pub fn is_param_ty(ty: &Ty) -> bool {
2342
matches!(ty.kind(), TyKind::Param(_))
2443
}
@@ -35,16 +54,27 @@ pub fn is_agent_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
3554
}
3655

3756
pub fn is_gc_scope_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
38-
match ty.kind() {
57+
match ty.peel_refs().kind() {
3958
TyKind::Adt(def, _) => {
4059
match_def_path(cx, def.did(), &["nova_vm", "engine", "context", "GcScope"])
4160
}
4261
_ => false,
4362
}
4463
}
4564

65+
pub fn is_no_gc_method(cx: &LateContext<'_>, did: DefId) -> bool {
66+
match_def_paths(
67+
cx,
68+
did,
69+
&[
70+
&["nova_vm", "engine", "context", "GcScope", "nogc"],
71+
&["nova_vm", "engine", "context", "GcScope", "into_nogc"],
72+
],
73+
)
74+
}
75+
4676
pub fn is_no_gc_scope_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
47-
match ty.kind() {
77+
match ty.peel_refs().kind() {
4878
TyKind::Adt(def, _) => match_def_path(
4979
cx,
5080
def.did(),
@@ -53,3 +83,85 @@ pub fn is_no_gc_scope_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
5383
_ => false,
5484
}
5585
}
86+
87+
pub fn is_value_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
88+
match ty.peel_refs().kind() {
89+
TyKind::Adt(def, _) => match_def_path(
90+
cx,
91+
def.did(),
92+
&[
93+
"nova_vm",
94+
"ecmascript",
95+
"types",
96+
"language",
97+
"value",
98+
"Value",
99+
],
100+
),
101+
_ => false,
102+
}
103+
}
104+
105+
pub fn is_object_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
106+
match ty.peel_refs().kind() {
107+
TyKind::Adt(def, _) => match_def_path(
108+
cx,
109+
def.did(),
110+
&[
111+
"nova_vm",
112+
"ecmascript",
113+
"types",
114+
"language",
115+
"object",
116+
"Object",
117+
],
118+
),
119+
_ => false,
120+
}
121+
}
122+
123+
pub fn is_arguments_list_ty(cx: &LateContext<'_>, ty: &Ty) -> bool {
124+
match ty.peel_refs().kind() {
125+
TyKind::Adt(def, _) => match_def_path(
126+
cx,
127+
def.did(),
128+
&[
129+
"nova_vm",
130+
"ecmascript",
131+
"builtins",
132+
"builtin_function",
133+
"ArgumentsList",
134+
],
135+
),
136+
_ => false,
137+
}
138+
}
139+
140+
pub fn could_be_builtin_method_sig<'tcx>(cx: &LateContext<'tcx>, sig: &FnSig<'tcx>) -> bool {
141+
sig.decl.inputs.len() == 4
142+
&& is_agent_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[0]))
143+
&& is_value_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[1]))
144+
&& is_arguments_list_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[2]))
145+
&& is_gc_scope_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[3]))
146+
}
147+
148+
pub fn could_be_builtin_constructor_sig<'tcx>(cx: &LateContext<'tcx>, sig: &FnSig<'tcx>) -> bool {
149+
sig.decl.inputs.len() == 5
150+
&& is_agent_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[0]))
151+
&& is_value_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[1]))
152+
&& is_arguments_list_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[2]))
153+
&& is_object_ty(
154+
cx,
155+
&lower_ty(cx.tcx, peel_hir_ty_options(cx, &sig.decl.inputs[3])),
156+
)
157+
&& is_gc_scope_ty(cx, &lower_ty(cx.tcx, &sig.decl.inputs[4]))
158+
}
159+
160+
pub fn could_be_builtin_method_def<'tcx>(cx: &LateContext<'tcx>, kind: FnKind<'tcx>) -> bool {
161+
match kind {
162+
FnKind::Method(_, sig) => {
163+
could_be_builtin_method_sig(cx, sig) || could_be_builtin_constructor_sig(cx, sig)
164+
}
165+
_ => false,
166+
}
167+
}

0 commit comments

Comments
 (0)