summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_lints/src/manual_string_new.rs
blob: 6acfb2ae3471c3b3a4f3840b609d490af95e0cfe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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_STRING_NEW,
    pedantic,
    "empty String is being created manually"
}
declare_lint_pass!(ManualStringNew => [MANUAL_STRING_NEW]);

impl LateLintPass<'_> for ManualStringNew {
    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, receiver, ..) => {
                parse_method_call(cx, expr.span, path_segment, receiver);
            },
            _ => (),
        }
    }
}

/// 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
}

fn warn_then_suggest(cx: &LateContext<'_>, span: Span) {
    span_lint_and_sugg(
        cx,
        MANUAL_STRING_NEW,
        span,
        "empty String is being created manually",
        "consider using",
        "String::new()".into(),
        MachineApplicable,
    );
}

/// Tries to parse an expression as a method call, emitting the warning if necessary.
fn parse_method_call(cx: &LateContext<'_>, span: Span, path_segment: &PathSegment<'_>, receiver: &Expr<'_>) {
    let ident = path_segment.ident.as_str();
    let method_arg_kind = &receiver.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, emitting 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);
            }
        }
    }
}