Skip to content

Commit

Permalink
Auto merge of #9295 - Guilherme-Vasconcelos:manual-empty-string-creat…
Browse files Browse the repository at this point in the history
…ion, r=dswij

Add `manual_empty_string_creations` lint

Closes #2972

- [x] Followed [lint naming conventions][lint_naming]
- [x] Added passing UI tests (including committed `.stderr` file)
- [x] `cargo test` passes locally
- [x] Executed `cargo dev update_lints`
- [x] Added lint documentation
- [x] Run `cargo dev fmt`

changelog: [`manual_empty_string_creations`]: Add lint for empty String not being created with `String::new()`
  • Loading branch information
bors committed Aug 19, 2022
2 parents eeaaba3 + 1bf8841 commit 868dba9
Show file tree
Hide file tree
Showing 32 changed files with 371 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3830,6 +3830,7 @@ Released 2018-09-13
[`manual_assert`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_assert
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
[`manual_bits`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits
[`manual_empty_string_creations`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_empty_string_creations
[`manual_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_filter_map
[`manual_find`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_find
[`manual_find_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_find_map
Expand Down
2 changes: 1 addition & 1 deletion clippy_dev/src/new_lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ fn to_camel_case(name: &str) -> String {
name.split('_')
.map(|s| {
if s.is_empty() {
String::from("")
String::new()
} else {
[&s[0..1].to_uppercase(), &s[1..]].concat()
}
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
LintId::of(main_recursion::MAIN_RECURSION),
LintId::of(manual_async_fn::MANUAL_ASYNC_FN),
LintId::of(manual_bits::MANUAL_BITS),
LintId::of(manual_empty_string_creations::MANUAL_EMPTY_STRING_CREATIONS),
LintId::of(manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE),
LintId::of(manual_rem_euclid::MANUAL_REM_EUCLID),
LintId::of(manual_retain::MANUAL_RETAIN),
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ store.register_lints(&[
manual_assert::MANUAL_ASSERT,
manual_async_fn::MANUAL_ASYNC_FN,
manual_bits::MANUAL_BITS,
manual_empty_string_creations::MANUAL_EMPTY_STRING_CREATIONS,
manual_instant_elapsed::MANUAL_INSTANT_ELAPSED,
manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE,
manual_ok_or::MANUAL_OK_OR,
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ store.register_group(true, "clippy::style", Some("clippy_style"), vec![
LintId::of(main_recursion::MAIN_RECURSION),
LintId::of(manual_async_fn::MANUAL_ASYNC_FN),
LintId::of(manual_bits::MANUAL_BITS),
LintId::of(manual_empty_string_creations::MANUAL_EMPTY_STRING_CREATIONS),
LintId::of(manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE),
LintId::of(map_clone::MAP_CLONE),
LintId::of(match_result_ok::MATCH_RESULT_OK),
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ mod main_recursion;
mod manual_assert;
mod manual_async_fn;
mod manual_bits;
mod manual_empty_string_creations;
mod manual_instant_elapsed;
mod manual_non_exhaustive;
mod manual_ok_or;
Expand Down Expand Up @@ -933,6 +934,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
store.register_late_pass(|| Box::new(std_instead_of_core::StdReexports::default()));
store.register_late_pass(|| Box::new(manual_instant_elapsed::ManualInstantElapsed));
store.register_late_pass(|| Box::new(partialeq_to_none::PartialeqToNone));
store.register_late_pass(|| Box::new(manual_empty_string_creations::ManualEmptyStringCreations));
// add lints here, do not remove this comment, it's used in `new_lint`
}

Expand Down
2 changes: 1 addition & 1 deletion clippy_lints/src/manual_async_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ fn suggested_ret(cx: &LateContext<'_>, output: &Ty<'_>) -> Option<(&'static str,
match output.kind {
TyKind::Tup(tys) if tys.is_empty() => {
let sugg = "remove the return type";
Some((sugg, "".into()))
Some((sugg, String::new()))
},
_ => {
let sugg = "return the output of the future directly";
Expand Down
141 changes: 141 additions & 0 deletions clippy_lints/src/manual_empty_string_creations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use rustc_ast::LitKind;
use rustc_errors::Applicability::MachineApplicable;
use rustc_hir::{Expr, ExprKind, PathSegment, QPath, TyKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::{sym, symbol, Span};

declare_clippy_lint! {
/// ### What it does
///
/// Checks for usage of `""` to create a `String`, such as `"".to_string()`, `"".to_owned()`,
/// `String::from("")` and others.
///
/// ### Why is this bad?
///
/// Different ways of creating an empty string makes your code less standardized, which can
/// be confusing.
///
/// ### Example
/// ```rust
/// let a = "".to_string();
/// let b: String = "".into();
/// ```
/// Use instead:
/// ```rust
/// let a = String::new();
/// let b = String::new();
/// ```
#[clippy::version = "1.65.0"]
pub MANUAL_EMPTY_STRING_CREATIONS,
style,
"empty String is being created manually"
}
declare_lint_pass!(ManualEmptyStringCreations => [MANUAL_EMPTY_STRING_CREATIONS]);

impl LateLintPass<'_> for ManualEmptyStringCreations {
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
if expr.span.from_expansion() {
return;
}

let ty = cx.typeck_results().expr_ty(expr);
match ty.kind() {
ty::Adt(adt_def, _) if adt_def.is_struct() => {
if !cx.tcx.is_diagnostic_item(sym::String, adt_def.did()) {
return;
}
},
_ => return,
}

match expr.kind {
ExprKind::Call(func, args) => {
parse_call(cx, expr.span, func, args);
},
ExprKind::MethodCall(path_segment, args, _) => {
parse_method_call(cx, expr.span, path_segment, args);
},
_ => (),
}
}
}

/// Checks if an expression's kind corresponds to an empty &str.
fn is_expr_kind_empty_str(expr_kind: &ExprKind<'_>) -> bool {
if let ExprKind::Lit(lit) = expr_kind &&
let LitKind::Str(value, _) = lit.node &&
value == symbol::kw::Empty
{
return true;
}

false
}

/// Emits the `MANUAL_EMPTY_STRING_CREATION` warning and suggests the appropriate fix.
fn warn_then_suggest(cx: &LateContext<'_>, span: Span) {
span_lint_and_sugg(
cx,
MANUAL_EMPTY_STRING_CREATIONS,
span,
"empty String is being created manually",
"consider using",
"String::new()".into(),
MachineApplicable,
);
}

/// Tries to parse an expression as a method call, emiting the warning if necessary.
fn parse_method_call(cx: &LateContext<'_>, span: Span, path_segment: &PathSegment<'_>, args: &[Expr<'_>]) {
if args.is_empty() {
// When parsing TryFrom::try_from(...).expect(...), we will have more than 1 arg.
return;
}

let ident = path_segment.ident.as_str();
let method_arg_kind = &args[0].kind;
if ["to_string", "to_owned", "into"].contains(&ident) && is_expr_kind_empty_str(method_arg_kind) {
warn_then_suggest(cx, span);
} else if let ExprKind::Call(func, args) = method_arg_kind {
// If our first argument is a function call itself, it could be an `unwrap`-like function.
// E.g. String::try_from("hello").unwrap(), TryFrom::try_from("").expect("hello"), etc.
parse_call(cx, span, func, args);
}
}

/// Tries to parse an expression as a function call, emiting the warning if necessary.
fn parse_call(cx: &LateContext<'_>, span: Span, func: &Expr<'_>, args: &[Expr<'_>]) {
if args.len() != 1 {
return;
}

let arg_kind = &args[0].kind;
if let ExprKind::Path(qpath) = &func.kind {
if let QPath::TypeRelative(_, _) = qpath {
// String::from(...) or String::try_from(...)
if let QPath::TypeRelative(ty, path_seg) = qpath &&
[sym::from, sym::try_from].contains(&path_seg.ident.name) &&
let TyKind::Path(qpath) = &ty.kind &&
let QPath::Resolved(_, path) = qpath &&
let [path_seg] = path.segments &&
path_seg.ident.name == sym::String &&
is_expr_kind_empty_str(arg_kind)
{
warn_then_suggest(cx, span);
}
} else if let QPath::Resolved(_, path) = qpath {
// From::from(...) or TryFrom::try_from(...)
if let [path_seg1, path_seg2] = path.segments &&
is_expr_kind_empty_str(arg_kind) && (
(path_seg1.ident.name == sym::From && path_seg2.ident.name == sym::from) ||
(path_seg1.ident.name == sym::TryFrom && path_seg2.ident.name == sym::try_from)
)
{
warn_then_suggest(cx, span);
}
}
}
}
2 changes: 1 addition & 1 deletion clippy_lints/src/methods/option_map_unwrap_or.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub(super) fn check<'tcx>(
map_span,
String::from(if unwrap_snippet_none { "and_then" } else { "map_or" }),
),
(expr.span.with_lo(unwrap_recv.span.hi()), String::from("")),
(expr.span.with_lo(unwrap_recv.span.hi()), String::new()),
];

if !unwrap_snippet_none {
Expand Down
2 changes: 1 addition & 1 deletion clippy_lints/src/misc_early/unneeded_wildcard_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fn span_lint(cx: &EarlyContext<'_>, span: Span, only_one: bool) {
"these patterns are unneeded as the `..` pattern can match those elements"
},
if only_one { "remove it" } else { "remove them" },
"".to_string(),
String::new(),
Applicability::MachineApplicable,
);
}
2 changes: 1 addition & 1 deletion clippy_lints/src/unnecessary_wraps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl<'tcx> LateLintPass<'tcx> for UnnecessaryWraps {
(
ret_expr.span,
if inner_type.is_unit() {
"".to_string()
String::new()
} else {
snippet(cx, arg.span.source_callsite(), "..").to_string()
}
Expand Down
2 changes: 1 addition & 1 deletion clippy_utils/src/msrvs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ msrv_aliases! {
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
1,28,0 { FROM_BOOL }
1,26,0 { RANGE_INCLUSIVE, STRING_RETAIN }
1,24,0 { IS_ASCII_DIGIT }
1,18,0 { HASH_MAP_RETAIN, HASH_SET_RETAIN }
1,17,0 { FIELD_INIT_SHORTHAND, STATIC_IN_CONST, EXPECT_ERR }
1,16,0 { STR_REPEAT }
1,24,0 { IS_ASCII_DIGIT }
}
10 changes: 5 additions & 5 deletions tests/ui/case_sensitive_file_extension_comparisons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,31 @@ fn is_rust_file(filename: &str) -> bool {

fn main() {
// std::string::String and &str should trigger the lint failure with .ext12
let _ = String::from("").ends_with(".ext12");
let _ = String::new().ends_with(".ext12");
let _ = "str".ends_with(".ext12");

// The test struct should not trigger the lint failure with .ext12
TestStruct {}.ends_with(".ext12");

// std::string::String and &str should trigger the lint failure with .EXT12
let _ = String::from("").ends_with(".EXT12");
let _ = String::new().ends_with(".EXT12");
let _ = "str".ends_with(".EXT12");

// The test struct should not trigger the lint failure with .EXT12
TestStruct {}.ends_with(".EXT12");

// Should not trigger the lint failure with .eXT12
let _ = String::from("").ends_with(".eXT12");
let _ = String::new().ends_with(".eXT12");
let _ = "str".ends_with(".eXT12");
TestStruct {}.ends_with(".eXT12");

// Should not trigger the lint failure with .EXT123 (too long)
let _ = String::from("").ends_with(".EXT123");
let _ = String::new().ends_with(".EXT123");
let _ = "str".ends_with(".EXT123");
TestStruct {}.ends_with(".EXT123");

// Shouldn't fail if it doesn't start with a dot
let _ = String::from("").ends_with("a.ext");
let _ = String::new().ends_with("a.ext");
let _ = "str".ends_with("a.extA");
TestStruct {}.ends_with("a.ext");
}
12 changes: 6 additions & 6 deletions tests/ui/case_sensitive_file_extension_comparisons.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ LL | filename.ends_with(".rs")
= help: consider using a case-insensitive comparison instead

error: case-sensitive file extension comparison
--> $DIR/case_sensitive_file_extension_comparisons.rs:17:30
--> $DIR/case_sensitive_file_extension_comparisons.rs:17:27
|
LL | let _ = String::from("").ends_with(".ext12");
| ^^^^^^^^^^^^^^^^^^^
LL | let _ = String::new().ends_with(".ext12");
| ^^^^^^^^^^^^^^^^^^^
|
= help: consider using a case-insensitive comparison instead

Expand All @@ -24,10 +24,10 @@ LL | let _ = "str".ends_with(".ext12");
= help: consider using a case-insensitive comparison instead

error: case-sensitive file extension comparison
--> $DIR/case_sensitive_file_extension_comparisons.rs:24:30
--> $DIR/case_sensitive_file_extension_comparisons.rs:24:27
|
LL | let _ = String::from("").ends_with(".EXT12");
| ^^^^^^^^^^^^^^^^^^^
LL | let _ = String::new().ends_with(".EXT12");
| ^^^^^^^^^^^^^^^^^^^
|
= help: consider using a case-insensitive comparison instead

Expand Down
2 changes: 1 addition & 1 deletion tests/ui/format.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() {
format!("foo {}", "bar");
format!("{} bar", "foo");

let arg: String = "".to_owned();
let arg = String::new();
arg.to_string();
format!("{:?}", arg); // Don't warn about debug.
format!("{:8}", arg);
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn main() {
format!("foo {}", "bar");
format!("{} bar", "foo");

let arg: String = "".to_owned();
let arg = String::new();
format!("{}", arg);
format!("{:?}", arg); // Don't warn about debug.
format!("{:8}", arg);
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/identity_op.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fn main() {
&x;
x;

let mut a = A("".into());
let mut a = A(String::new());
let b = a << 0; // no error: non-integer

1 * Meter; // no error: non-integer
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/identity_op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fn main() {
&x >> 0;
x >> &0;

let mut a = A("".into());
let mut a = A(String::new());
let b = a << 0; // no error: non-integer

1 * Meter; // no error: non-integer
Expand Down
Loading

0 comments on commit 868dba9

Please sign in to comment.