Skip to content

Commit b5b0d15

Browse files
committed
Implement a lint for replacable Command::new
1 parent 5873cb9 commit b5b0d15

File tree

12 files changed

+216
-11
lines changed

12 files changed

+216
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6075,6 +6075,7 @@ Released 2018-09-13
60756075
[`unnecessary_safety_comment`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_safety_comment
60766076
[`unnecessary_safety_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_safety_doc
60776077
[`unnecessary_self_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_self_imports
6078+
[`unnecessary_shell_command`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_shell_command
60786079
[`unnecessary_sort_by`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_sort_by
60796080
[`unnecessary_struct_initialization`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_struct_initialization
60806081
[`unnecessary_to_owned`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_to_owned
@@ -6221,6 +6222,7 @@ Released 2018-09-13
62216222
[`trivial-copy-size-limit`]: https://doc.rust-lang.org/clippy/lint_configuration.html#trivial-copy-size-limit
62226223
[`type-complexity-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#type-complexity-threshold
62236224
[`unnecessary-box-size`]: https://doc.rust-lang.org/clippy/lint_configuration.html#unnecessary-box-size
6225+
[`unnecessary-commands`]: https://doc.rust-lang.org/clippy/lint_configuration.html#unnecessary-commands
62246226
[`unreadable-literal-lint-fractions`]: https://doc.rust-lang.org/clippy/lint_configuration.html#unreadable-literal-lint-fractions
62256227
[`upper-case-acronyms-aggressive`]: https://doc.rust-lang.org/clippy/lint_configuration.html#upper-case-acronyms-aggressive
62266228
[`vec-box-size-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#vec-box-size-threshold

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
77

8-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
8+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
99

1010
Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
1111
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.

book/src/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
A collection of lints to catch common mistakes and improve your
77
[Rust](https://github.com/rust-lang/rust) code.
88

9-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
1010

1111
Lints are divided into categories, each with a default [lint
1212
level](https://doc.rust-lang.org/rustc/lints/levels.html). You can choose how

book/src/lint_configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,16 @@ The byte size a `T` in `Box<T>` can have, below which it triggers the `clippy::u
893893
* [`unnecessary_box_returns`](https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_box_returns)
894894

895895

896+
## `unnecessary-commands`
897+
The list of shell commands to recommend replacing.
898+
899+
**Default Value:** `{ curl = "use `std::net`, `ureq`, or `reqwest` instead", jq = "use `serde_json`, or another native json parser instead", ls = "use `std::fs::read_dir` instead", sed = "use `regex`, or the methods on `str` instead", wget = "use `std::net`, `ureq`, or `reqwest` instead" }`
900+
901+
---
902+
**Affected lints:**
903+
* [`unnecessary_shell_commands`](https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_shell_commands)
904+
905+
896906
## `unreadable-literal-lint-fractions`
897907
Should the fraction of a decimal be linted to include separators.
898908

clippy_config/src/conf.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use rustc_span::edit_distance::edit_distance;
77
use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
88
use serde::de::{IgnoredAny, IntoDeserializer, MapAccess, Visitor};
99
use serde::{Deserialize, Deserializer, Serialize};
10+
use std::borrow::Cow;
11+
use std::collections::BTreeMap;
1012
use std::fmt::{Debug, Display, Formatter};
1113
use std::ops::Range;
1214
use std::path::PathBuf;
@@ -648,6 +650,15 @@ define_Conf! {
648650
/// The byte size a `T` in `Box<T>` can have, below which it triggers the `clippy::unnecessary_box` lint
649651
#[lints(unnecessary_box_returns)]
650652
unnecessary_box_size: u64 = 128,
653+
/// The list of shell commands to recommend replacing.
654+
#[lints(unnecessary_shell_commands)]
655+
unnecessary_commands: BTreeMap<Box<str>, Option<Cow<'static, str>>> = [
656+
["ls", "use `std::fs::read_dir` instead"],
657+
["curl", "use `std::net`, `ureq`, or `reqwest` instead"],
658+
["wget", "use `std::net`, `ureq`, or `reqwest` instead"],
659+
["sed", "use `regex`, or the methods on `str` instead"],
660+
["jq", "use `serde_json`, or another native json parser instead"],
661+
].into_iter().map(|[k, v]| (Box::<str>::from(k), Some(Cow::Borrowed(v)))).collect(),
651662
/// Should the fraction of a decimal be linted to include separators.
652663
#[lints(unreadable_literal)]
653664
unreadable_literal_lint_fractions: bool = true,

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
744744
crate::unnecessary_map_on_constructor::UNNECESSARY_MAP_ON_CONSTRUCTOR_INFO,
745745
crate::unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS_INFO,
746746
crate::unnecessary_self_imports::UNNECESSARY_SELF_IMPORTS_INFO,
747+
crate::unnecessary_shell_command::UNNECESSARY_SHELL_COMMAND_INFO,
747748
crate::unnecessary_struct_initialization::UNNECESSARY_STRUCT_INITIALIZATION_INFO,
748749
crate::unnecessary_wraps::UNNECESSARY_WRAPS_INFO,
749750
crate::unnested_or_patterns::UNNESTED_OR_PATTERNS_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ mod unnecessary_literal_bound;
367367
mod unnecessary_map_on_constructor;
368368
mod unnecessary_owned_empty_strings;
369369
mod unnecessary_self_imports;
370+
mod unnecessary_shell_command;
370371
mod unnecessary_struct_initialization;
371372
mod unnecessary_wraps;
372373
mod unnested_or_patterns;
@@ -949,5 +950,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
949950
store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
950951
store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
951952
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
953+
store.register_late_pass(move |_| Box::new(unnecessary_shell_command::UnnecessaryShellCommand::new(conf)));
952954
// add lints here, do not remove this comment, it's used in `new_lint`
953955
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::borrow::Cow;
2+
use std::collections::BTreeMap;
3+
4+
use clippy_config::Conf;
5+
use clippy_utils::consts::{ConstEvalCtxt, Constant};
6+
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
7+
use clippy_utils::path_def_id;
8+
use clippy_utils::ty::get_inherent_method;
9+
use rustc_hir::def_id::DefId;
10+
use rustc_hir::{Expr, ExprKind};
11+
use rustc_lint::{LateContext, LateLintPass};
12+
use rustc_session::impl_lint_pass;
13+
use rustc_span::sym;
14+
15+
declare_clippy_lint! {
16+
/// ### What it does
17+
///
18+
/// Detects calls to `std::process::Command::new` that can easily be replaced with Rust code.
19+
///
20+
/// ### Why is this bad?
21+
///
22+
/// "Shelling out" is slow, non-portable, and generally unnecessary (especially to these programs).
23+
///
24+
/// ### Example
25+
/// ```no_run
26+
/// use std::io;
27+
/// use std::process::Command;
28+
///
29+
/// fn list_files() -> io::Result<Vec<String>> {
30+
/// let output = Command::new("ls").output()?;
31+
/// if !output.status.success() {
32+
/// return Err(io::Error::new(
33+
/// io::ErrorKind::Other,
34+
/// String::from_utf8_lossy(&output.stderr)
35+
/// ));
36+
/// }
37+
///
38+
/// let stdout = std::str::from_utf8(&output.stdout).expect("should be UTF-8 output");
39+
/// Ok(stdout.split_whitespace().map(String::from).collect())
40+
/// }
41+
/// // example code where clippy issues a warning
42+
/// ```
43+
/// Use instead:
44+
/// ```no_run
45+
/// fn list_files() -> std::io::Result<Vec<String>> {
46+
/// let mut buf = Vec::new();
47+
/// for entry_res in std::fs::read_dir(".")? {
48+
/// let path = entry_res?.path();
49+
/// let os_name = path.into_os_string();
50+
/// buf.push(os_name.into_string().expect("should be UTF-8 paths"))
51+
/// }
52+
///
53+
/// Ok(buf)
54+
/// }
55+
/// ```
56+
#[clippy::version = "1.84.0"]
57+
pub UNNECESSARY_SHELL_COMMAND,
58+
pedantic,
59+
"using the simple shell utilities instead of Rust code"
60+
}
61+
62+
pub struct UnnecessaryShellCommand {
63+
std_process_command_new: Option<DefId>,
64+
unnecessary_commands: &'static BTreeMap<Box<str>, Option<Cow<'static, str>>>,
65+
}
66+
67+
impl UnnecessaryShellCommand {
68+
pub fn new(conf: &'static Conf) -> Self {
69+
Self {
70+
std_process_command_new: None,
71+
unnecessary_commands: &conf.unnecessary_commands,
72+
}
73+
}
74+
}
75+
76+
impl_lint_pass!(UnnecessaryShellCommand => [UNNECESSARY_SHELL_COMMAND]);
77+
78+
impl LateLintPass<'_> for UnnecessaryShellCommand {
79+
fn check_crate(&mut self, cx: &LateContext<'_>) {
80+
if let Some(command_did) = cx.tcx.get_diagnostic_item(sym::Command)
81+
&& let Some(fn_item) = get_inherent_method(cx, command_did, sym::new)
82+
{
83+
self.std_process_command_new = Some(fn_item.def_id);
84+
}
85+
}
86+
87+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
88+
let Some(std_process_command_new) = self.std_process_command_new else {
89+
return;
90+
};
91+
92+
if let ExprKind::Call(func, [command]) = expr.kind
93+
&& path_def_id(cx, func) == Some(std_process_command_new)
94+
&& let Some(Constant::Str(command_lit)) = ConstEvalCtxt::new(cx).eval(command)
95+
&& let command_lit = command_lit.strip_suffix(".exe").unwrap_or(&command_lit)
96+
&& let Some(help) = self.unnecessary_commands.get(command_lit)
97+
{
98+
let lint = UNNECESSARY_SHELL_COMMAND;
99+
let msg = "unnecessarily shelling out for trivial operation";
100+
if let Some(help) = help.as_deref() {
101+
span_lint_and_help(cx, lint, command.span, msg, None, help);
102+
} else {
103+
span_lint(cx, lint, command.span, msg);
104+
}
105+
}
106+
}
107+
}

clippy_utils/src/ty.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,22 +1323,29 @@ pub fn deref_chain<'cx, 'tcx>(cx: &'cx LateContext<'tcx>, ty: Ty<'tcx>) -> impl
13231323

13241324
/// Checks if a Ty<'_> has some inherent method Symbol.
13251325
///
1326-
/// This does not look for impls in the type's `Deref::Target` type.
1327-
/// If you need this, you should wrap this call in `clippy_utils::ty::deref_chain().any(...)`.
1326+
/// This is a helper for [`get_inherent_method`].
13281327
pub fn get_adt_inherent_method<'a>(cx: &'a LateContext<'_>, ty: Ty<'_>, method_name: Symbol) -> Option<&'a AssocItem> {
13291328
if let Some(ty_did) = ty.ty_adt_def().map(AdtDef::did) {
1330-
cx.tcx.inherent_impls(ty_did).iter().find_map(|&did| {
1331-
cx.tcx
1332-
.associated_items(did)
1333-
.filter_by_name_unhygienic(method_name)
1334-
.next()
1335-
.filter(|item| item.kind == AssocKind::Fn)
1336-
})
1329+
get_inherent_method(cx, ty_did, method_name)
13371330
} else {
13381331
None
13391332
}
13401333
}
13411334

1335+
/// Checks if the [`DefId`] of a Ty has some inherent method Symbol.
1336+
///
1337+
/// This does not look for impls in the type's `Deref::Target` type.
1338+
/// If you need this, you should wrap this call in `clippy_utils::ty::deref_chain().any(...)`.
1339+
pub fn get_inherent_method<'a>(cx: &'a LateContext<'_>, ty_did: DefId, method_name: Symbol) -> Option<&'a AssocItem> {
1340+
cx.tcx.inherent_impls(ty_did).iter().find_map(|&did| {
1341+
cx.tcx
1342+
.associated_items(did)
1343+
.filter_by_name_unhygienic(method_name)
1344+
.next()
1345+
.filter(|item| item.kind == AssocKind::Fn)
1346+
})
1347+
}
1348+
13421349
/// Get's the type of a field by name.
13431350
pub fn get_field_by_name<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, name: Symbol) -> Option<Ty<'tcx>> {
13441351
match *ty.kind() {

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
7171
trivial-copy-size-limit
7272
type-complexity-threshold
7373
unnecessary-box-size
74+
unnecessary-commands
7475
unreadable-literal-lint-fractions
7576
upper-case-acronyms-aggressive
7677
vec-box-size-threshold
@@ -155,6 +156,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
155156
trivial-copy-size-limit
156157
type-complexity-threshold
157158
unnecessary-box-size
159+
unnecessary-commands
158160
unreadable-literal-lint-fractions
159161
upper-case-acronyms-aggressive
160162
vec-box-size-threshold
@@ -239,6 +241,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
239241
trivial-copy-size-limit
240242
type-complexity-threshold
241243
unnecessary-box-size
244+
unnecessary-commands
242245
unreadable-literal-lint-fractions
243246
upper-case-acronyms-aggressive
244247
vec-box-size-threshold

0 commit comments

Comments
 (0)