Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add manual_empty_string_creations lint #9295

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3828,6 +3828,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