Skip to content

Commit f90251e

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

File tree

10 files changed

+194
-137
lines changed

10 files changed

+194
-137
lines changed

clippy_config/src/conf.rs

+38-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,25 @@ 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.\
622+
/// Note: Because this configuration supports different syntax (simple path or path with suggestion),
623+
/// to prevent suggestion conflicts between default and user specified values,
624+
/// adding a value here will override the default configuration.
595625
///
596626
/// #### Example
597627
///
598628
/// ```toml
599-
/// blocking-ops = ["my_crate::some_blocking_fn"]
629+
/// blocking-ops = [ "my_crate::blocking_foo" ]
600630
/// ```
601-
(blocking_ops: Vec<String> = <_>::default()),
602-
/// Lint: UNNECESSARY_BLOCKING_OPS.
603631
///
604-
/// List of additional blocking function paths to check, with replacement suggestion function paths.
605-
///
606-
/// #### Example
632+
/// Or, if you want these functions to be auto replaced by a suggested one:
607633
///
608634
/// ```toml
609-
/// blocking-ops-with-suggestions = [["my_crate::some_blocking_fn" , "my_crate::use_this_instead"]]
635+
/// blocking-ops = [
636+
/// { path = "my_crate::blocking_foo", suggestion = "my_crate::async::async_foo" }
637+
/// ]
610638
/// ```
611-
(blocking_ops_with_suggestions: Vec<[String; 2]> = <_>::default()),
639+
(blocking_ops: Vec<SuggestedPath> = DEFAULT_BLOCKING_OP_PATHS.iter().map(SuggestedPath::from_path_str).collect()),
612640
}
613641

614642
/// 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`
+85-98
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
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};
8+
use rustc_hir::{
9+
Body, BodyId, Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, Expr, ExprKind, ImplItem, ImplItemKind,
10+
Item, ItemKind, Node, TraitItem, TraitItemKind,
11+
};
1212
use rustc_lint::{LateContext, LateLintPass};
13-
use rustc_session::{declare_tool_lint, impl_lint_pass};
13+
use rustc_session::impl_lint_pass;
1414
use rustc_span::Span;
1515

1616
declare_clippy_lint! {
@@ -43,131 +43,118 @@ declare_clippy_lint! {
4343
/// ```
4444
#[clippy::version = "1.74.0"]
4545
pub UNNECESSARY_BLOCKING_OPS,
46-
nursery,
46+
pedantic,
4747
"blocking operations in an async context"
4848
}
4949

5050
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
51+
blocking_ops: Vec<SuggestedPath>,
52+
/// Map of resolved funtion `def_id` with suggestion string after checking crate
5453
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,
54+
/// Tracking whether a body is async after entering it.
55+
body_asyncness: Vec<bool>,
5756
}
5857

5958
impl UnnecessaryBlockingOps {
60-
pub(crate) fn new(blocking_ops: Vec<String>, blocking_ops_with_suggs: Vec<[String; 2]>) -> Self {
59+
pub(crate) fn new(blocking_ops: Vec<SuggestedPath>) -> Self {
6160
Self {
6261
blocking_ops,
63-
blocking_ops_with_suggs,
6462
id_with_suggs: FxHashMap::default(),
65-
visited_block: HirIdSet::default(),
63+
body_asyncness: vec![],
6664
}
6765
}
6866
}
6967

7068
impl_lint_pass!(UnnecessaryBlockingOps => [UNNECESSARY_BLOCKING_OPS]);
7169

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-
9870
impl<'tcx> LateLintPass<'tcx> for UnnecessaryBlockingOps {
9971
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 {
72+
let full_fn_list = self.blocking_ops.iter().map(|p| (p.path(), p.suggestion()));
73+
for (path_str, maybe_sugg_str) in full_fn_list {
74+
let path: Vec<&str> = path_str.split("::").collect();
12175
for did in def_path_def_ids(cx, &path) {
12276
self.id_with_suggs.insert(did, maybe_sugg_str.map(ToOwned::to_owned));
12377
}
12478
}
12579
}
12680

12781
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-
{
82+
if is_lint_allowed(cx, UNNECESSARY_BLOCKING_OPS, body.value.hir_id) {
13183
return;
13284
}
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-
);
85+
self.body_asyncness.push(in_async_body(cx, body.id()));
86+
}
87+
88+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
89+
if matches!(self.body_asyncness.last(), Some(true))
90+
&& let ExprKind::Call(call, _) = &expr.kind
91+
&& let Some(call_did) = fn_def_id(cx, expr)
92+
&& let Some(maybe_sugg) = self.id_with_suggs.get(&call_did)
93+
{
94+
span_lint_and_then(
95+
cx,
96+
UNNECESSARY_BLOCKING_OPS,
97+
call.span,
98+
"blocking function call detected in an async body",
99+
|diag| {
100+
if let Some(sugg_fn_path) = maybe_sugg {
101+
make_suggestion(diag, cx, expr, call.span, sugg_fn_path);
153102
}
154-
_ => {}
155-
}
156-
ControlFlow::<()>::Continue(())
157-
});
103+
},
104+
);
158105
}
159106
}
107+
108+
fn check_body_post(&mut self, _: &LateContext<'tcx>, _: &'tcx Body<'tcx>) {
109+
self.body_asyncness.pop();
110+
}
160111
}
161112

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

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)