Skip to content

Commit 607ab6b

Browse files
committed
add SuggestedPath config type;
1 parent 66c1371 commit 607ab6b

11 files changed

+224
-159
lines changed

clippy_config/src/conf.rs

+35-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::msrvs::Msrv;
2-
use crate::types::{DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename};
2+
use crate::types::{
3+
DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename, SuggestedPath,
4+
};
35
use crate::ClippyConfiguration;
46
use rustc_data_structures::fx::FxHashSet;
57
use rustc_errors::Applicability;
@@ -39,6 +41,31 @@ const DEFAULT_DOC_VALID_IDENTS: &[&str] = &[
3941
];
4042
const DEFAULT_DISALLOWED_NAMES: &[&str] = &["foo", "baz", "quux"];
4143
const DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS: &[&str] = &["i", "j", "x", "y", "z", "w", "n"];
44+
const DEFAULT_BLOCKING_OP_PATHS: &[&str] = &[
45+
"std::thread::sleep",
46+
// Filesystem functions
47+
"std::fs::try_exists",
48+
"std::fs::canonicalize",
49+
"std::fs::copy",
50+
"std::fs::create_dir",
51+
"std::fs::create_dir_all",
52+
"std::fs::hard_link",
53+
"std::fs::metadata",
54+
"std::fs::read",
55+
"std::fs::read_dir",
56+
"std::fs::read_link",
57+
"std::fs::read_to_string",
58+
"std::fs::remove_dir",
59+
"std::fs::remove_dir_all",
60+
"std::fs::remove_file",
61+
"std::fs::rename",
62+
"std::fs::set_permissions",
63+
"std::fs::symlink_metadata",
64+
"std::fs::write",
65+
// IO functions
66+
"std::io::copy",
67+
"std::io::read_to_string",
68+
];
4269

4370
/// Conf with parse errors
4471
#[derive(Default)]
@@ -591,24 +618,22 @@ define_Conf! {
591618
(allowed_wildcard_imports: FxHashSet<String> = FxHashSet::default()),
592619
/// Lint: UNNECESSARY_BLOCKING_OPS.
593620
///
594-
/// List of additional blocking function paths to check.
621+
/// List of additional blocking function paths to check, with optional suggestions for each path.
595622
///
596623
/// #### Example
597624
///
598625
/// ```toml
599-
/// blocking-ops = ["my_crate::some_blocking_fn"]
626+
/// blocking-ops = [ "my_crate::blocking_foo" ]
600627
/// ```
601-
(blocking_ops: Vec<String> = <_>::default()),
602-
/// Lint: UNNECESSARY_BLOCKING_OPS.
603628
///
604-
/// List of additional blocking function paths to check, with replacement suggestion function paths.
605-
///
606-
/// #### Example
629+
/// Or, if you are sure that some functions can be replaced by a suggested one:
607630
///
608631
/// ```toml
609-
/// blocking-ops-with-suggestions = [["my_crate::some_blocking_fn" , "my_crate::use_this_instead"]]
632+
/// blocking-ops = [
633+
/// { path = "my_crate::blocking_foo", suggestion = "my_crate::async::async_foo" }
634+
/// ]
610635
/// ```
611-
(blocking_ops_with_suggestions: Vec<[String; 2]> = <_>::default()),
636+
(blocking_ops: Vec<SuggestedPath> = DEFAULT_BLOCKING_OP_PATHS.iter().map(SuggestedPath::from_path_str).collect()),
612637
}
613638

614639
/// Search for the configuration file.

clippy_config/src/types.rs

+28
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,33 @@ impl DisallowedPath {
3232
}
3333
}
3434

35+
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
36+
#[serde(untagged)]
37+
pub enum SuggestedPath {
38+
Simple(String),
39+
WithSuggestion { path: String, suggestion: Option<String> },
40+
}
41+
42+
impl SuggestedPath {
43+
pub fn path(&self) -> &str {
44+
let (Self::Simple(path) | Self::WithSuggestion { path, .. }) = self;
45+
46+
path
47+
}
48+
49+
pub fn from_path_str<S: ToString>(path: &S) -> Self {
50+
Self::Simple(path.to_string())
51+
}
52+
53+
pub fn suggestion(&self) -> Option<&str> {
54+
if let Self::WithSuggestion { suggestion, .. } = self {
55+
suggestion.as_deref()
56+
} else {
57+
None
58+
}
59+
}
60+
}
61+
3562
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
3663
pub enum MatchLintBehaviour {
3764
AllTypes,
@@ -125,6 +152,7 @@ unimplemented_serialize! {
125152
DisallowedPath,
126153
Rename,
127154
MacroMatcher,
155+
SuggestedPath,
128156
}
129157

130158
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]

clippy_lints/src/lib.rs

-2
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
547547
avoid_breaking_exported_api,
548548
ref await_holding_invalid_types,
549549
ref blocking_ops,
550-
ref blocking_ops_with_suggestions,
551550
cargo_ignore_publish,
552551
cognitive_complexity_threshold,
553552
ref disallowed_macros,
@@ -1137,7 +1136,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
11371136
store.register_late_pass(move |_| {
11381137
Box::new(unnecessary_blocking_ops::UnnecessaryBlockingOps::new(
11391138
blocking_ops.clone(),
1140-
blocking_ops_with_suggestions.clone(),
11411139
))
11421140
});
11431141
// add lints here, do not remove this comment, it's used in `new_lint`
+87-99
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
use std::ops::ControlFlow;
2-
1+
use clippy_config::types::SuggestedPath;
32
use clippy_utils::diagnostics::span_lint_and_then;
43
use clippy_utils::source::snippet_with_applicability;
5-
use clippy_utils::visitors::for_each_expr_with_closures;
64
use clippy_utils::{def_path_def_ids, fn_def_id, is_lint_allowed};
75
use rustc_data_structures::fx::FxHashMap;
8-
use rustc_errors::{Applicability, Diagnostic};
6+
use rustc_errors::{Applicability, Diag};
97
use rustc_hir::def_id::DefId;
10-
use rustc_hir::hir_id::CRATE_HIR_ID;
11-
use rustc_hir::{Body, Expr, ExprKind, GeneratorKind, HirIdSet};
12-
use rustc_lint::{LateContext, LateLintPass};
13-
use rustc_session::{declare_tool_lint, impl_lint_pass};
8+
use rustc_hir::{
9+
Body, BodyId, Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, Expr, ExprKind, ImplItem, ImplItemKind,
10+
Item, ItemKind, Node, TraitItem, TraitItemKind,
11+
};
12+
use rustc_lint::{LateContext, LateLintPass, LintContext};
13+
use rustc_middle::lint::in_external_macro;
14+
use rustc_session::impl_lint_pass;
1415
use rustc_span::Span;
1516

1617
declare_clippy_lint! {
@@ -43,131 +44,118 @@ declare_clippy_lint! {
4344
/// ```
4445
#[clippy::version = "1.74.0"]
4546
pub UNNECESSARY_BLOCKING_OPS,
46-
nursery,
47+
pedantic,
4748
"blocking operations in an async context"
4849
}
4950

5051
pub(crate) struct UnnecessaryBlockingOps {
51-
blocking_ops: Vec<String>,
52-
blocking_ops_with_suggs: Vec<[String; 2]>,
53-
/// Map of resolved funtion def_id with suggestion string after checking crate
52+
blocking_ops: Vec<SuggestedPath>,
53+
/// Map of resolved funtion `def_id` with suggestion string after checking crate
5454
id_with_suggs: FxHashMap<DefId, Option<String>>,
55-
/// Keep track of visited block ids to skip checking the same bodies in `check_body` calls
56-
visited_block: HirIdSet,
55+
/// Tracking whether a body is async after entering it.
56+
body_asyncness: Vec<bool>,
5757
}
5858

5959
impl UnnecessaryBlockingOps {
60-
pub(crate) fn new(blocking_ops: Vec<String>, blocking_ops_with_suggs: Vec<[String; 2]>) -> Self {
60+
pub(crate) fn new(blocking_ops: Vec<SuggestedPath>) -> Self {
6161
Self {
6262
blocking_ops,
63-
blocking_ops_with_suggs,
6463
id_with_suggs: FxHashMap::default(),
65-
visited_block: HirIdSet::default(),
64+
body_asyncness: vec![],
6665
}
6766
}
6867
}
6968

7069
impl_lint_pass!(UnnecessaryBlockingOps => [UNNECESSARY_BLOCKING_OPS]);
7170

72-
static HARD_CODED_BLOCKING_OPS: [&[&str]; 21] = [
73-
&["std", "thread", "sleep"],
74-
// Filesystem functions
75-
&["std", "fs", "try_exists"],
76-
&["std", "fs", "canonicalize"],
77-
&["std", "fs", "copy"],
78-
&["std", "fs", "create_dir"],
79-
&["std", "fs", "create_dir_all"],
80-
&["std", "fs", "hard_link"],
81-
&["std", "fs", "metadata"],
82-
&["std", "fs", "read"],
83-
&["std", "fs", "read_dir"],
84-
&["std", "fs", "read_link"],
85-
&["std", "fs", "read_to_string"],
86-
&["std", "fs", "remove_dir"],
87-
&["std", "fs", "remove_dir_all"],
88-
&["std", "fs", "remove_file"],
89-
&["std", "fs", "rename"],
90-
&["std", "fs", "set_permissions"],
91-
&["std", "fs", "symlink_metadata"],
92-
&["std", "fs", "write"],
93-
// IO functions
94-
&["std", "io", "copy"],
95-
&["std", "io", "read_to_string"],
96-
];
97-
9871
impl<'tcx> LateLintPass<'tcx> for UnnecessaryBlockingOps {
9972
fn check_crate(&mut self, cx: &LateContext<'tcx>) {
100-
// Avoids processing and storing a long list of paths if this lint was allowed entirely
101-
if is_lint_allowed(cx, UNNECESSARY_BLOCKING_OPS, CRATE_HIR_ID) {
102-
return;
103-
}
104-
105-
let full_fn_list = HARD_CODED_BLOCKING_OPS
106-
.into_iter()
107-
.map(|p| (p.to_vec(), None))
108-
// Chain configured functions without suggestions
109-
.chain(
110-
self.blocking_ops
111-
.iter()
112-
.map(|p| (p.split("::").collect::<Vec<_>>(), None)),
113-
)
114-
// Chain configured functions with suggestions
115-
.chain(
116-
self.blocking_ops_with_suggs
117-
.iter()
118-
.map(|[p, s]| (p.split("::").collect::<Vec<_>>(), Some(s.as_str()))),
119-
);
120-
for (path, maybe_sugg_str) in full_fn_list {
73+
let full_fn_list = self.blocking_ops.iter().map(|p| (p.path(), p.suggestion()));
74+
for (path_str, maybe_sugg_str) in full_fn_list {
75+
let path: Vec<&str> = path_str.split("::").collect();
12176
for did in def_path_def_ids(cx, &path) {
12277
self.id_with_suggs.insert(did, maybe_sugg_str.map(ToOwned::to_owned));
12378
}
12479
}
12580
}
12681

12782
fn check_body(&mut self, cx: &LateContext<'tcx>, body: &'tcx Body<'tcx>) {
128-
if is_lint_allowed(cx, UNNECESSARY_BLOCKING_OPS, body.value.hir_id)
129-
|| self.visited_block.contains(&body.value.hir_id)
130-
{
83+
if is_lint_allowed(cx, UNNECESSARY_BLOCKING_OPS, body.value.hir_id) {
13184
return;
13285
}
133-
if let Some(GeneratorKind::Async(_)) = body.generator_kind() {
134-
for_each_expr_with_closures(cx, body, |ex| {
135-
match ex.kind {
136-
ExprKind::Block(block, _) => {
137-
self.visited_block.insert(block.hir_id);
138-
}
139-
ExprKind::Call(call, _)
140-
if let Some(call_did) = fn_def_id(cx, ex) &&
141-
let Some(maybe_sugg) = self.id_with_suggs.get(&call_did) => {
142-
span_lint_and_then(
143-
cx,
144-
UNNECESSARY_BLOCKING_OPS,
145-
call.span,
146-
"blocking function call detected in an async body",
147-
|diag| {
148-
if let Some(sugg_fn_path) = maybe_sugg {
149-
make_suggestion(diag, cx, ex, call.span, sugg_fn_path);
150-
}
151-
}
152-
);
86+
self.body_asyncness.push(in_async_body(cx, body.id()));
87+
}
88+
89+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
90+
if !in_external_macro(cx.sess(), expr.span)
91+
&& matches!(self.body_asyncness.last(), Some(true))
92+
&& let ExprKind::Call(call, _) = &expr.kind
93+
&& let Some(call_did) = fn_def_id(cx, expr)
94+
&& let Some(maybe_sugg) = self.id_with_suggs.get(&call_did)
95+
{
96+
span_lint_and_then(
97+
cx,
98+
UNNECESSARY_BLOCKING_OPS,
99+
call.span,
100+
"blocking function call detected in an async body",
101+
|diag| {
102+
if let Some(sugg_fn_path) = maybe_sugg {
103+
make_suggestion(diag, cx, expr, call.span, sugg_fn_path);
153104
}
154-
_ => {}
155-
}
156-
ControlFlow::<()>::Continue(())
157-
});
105+
},
106+
);
158107
}
159108
}
109+
110+
fn check_body_post(&mut self, _: &LateContext<'tcx>, _: &'tcx Body<'tcx>) {
111+
self.body_asyncness.pop();
112+
}
160113
}
161114

162-
fn make_suggestion(diag: &mut Diagnostic, cx: &LateContext<'_>, expr: &Expr<'_>, fn_span: Span, sugg_fn_path: &str) {
163-
let mut applicability = Applicability::Unspecified;
115+
fn make_suggestion(diag: &mut Diag<'_, ()>, cx: &LateContext<'_>, expr: &Expr<'_>, fn_span: Span, sugg_fn_path: &str) {
116+
// Suggestion should only be offered when user specified it in the configuration file,
117+
// so we only assume it can be fixed here if only the path could be found.
118+
let mut applicability = if def_path_def_ids(cx, &sugg_fn_path.split("::").collect::<Vec<_>>())
119+
.next()
120+
.is_some()
121+
{
122+
Applicability::MaybeIncorrect
123+
} else {
124+
Applicability::Unspecified
125+
};
126+
164127
let args_span = expr.span.with_lo(fn_span.hi());
165128
let args_snippet = snippet_with_applicability(cx, args_span, "..", &mut applicability);
166129
let suggestion = format!("{sugg_fn_path}{args_snippet}.await");
167-
diag.span_suggestion(
168-
expr.span,
169-
"try using its async counterpart",
170-
suggestion,
171-
Applicability::Unspecified,
172-
);
130+
diag.span_suggestion(expr.span, "try using its async counterpart", suggestion, applicability);
131+
}
132+
133+
/// Check whether a body is from an async function/closure.
134+
fn in_async_body(cx: &LateContext<'_>, body_id: BodyId) -> bool {
135+
let parent_node = cx.tcx.parent_hir_node(body_id.hir_id);
136+
match parent_node {
137+
Node::Expr(expr) => matches!(
138+
expr.kind,
139+
ExprKind::Closure(Closure {
140+
kind: ClosureKind::Coroutine(CoroutineKind::Desugared(
141+
CoroutineDesugaring::Async | CoroutineDesugaring::AsyncGen,
142+
_
143+
)),
144+
..
145+
})
146+
),
147+
Node::Item(Item {
148+
kind: ItemKind::Fn(fn_sig, ..),
149+
..
150+
})
151+
| Node::ImplItem(ImplItem {
152+
kind: ImplItemKind::Fn(fn_sig, _),
153+
..
154+
})
155+
| Node::TraitItem(TraitItem {
156+
kind: TraitItemKind::Fn(fn_sig, _),
157+
..
158+
}) => fn_sig.header.is_async(),
159+
_ => false,
160+
}
173161
}

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
2323
avoid-breaking-exported-api
2424
await-holding-invalid-types
2525
blacklisted-names
26+
blocking-ops
2627
cargo-ignore-publish
2728
check-private-items
2829
cognitive-complexity-threshold
@@ -102,6 +103,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
102103
avoid-breaking-exported-api
103104
await-holding-invalid-types
104105
blacklisted-names
106+
blocking-ops
105107
cargo-ignore-publish
106108
check-private-items
107109
cognitive-complexity-threshold
@@ -181,6 +183,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
181183
avoid-breaking-exported-api
182184
await-holding-invalid-types
183185
blacklisted-names
186+
blocking-ops
184187
cargo-ignore-publish
185188
check-private-items
186189
cognitive-complexity-threshold

0 commit comments

Comments
 (0)