|
| 1 | +use clippy_utils::diagnostics::span_lint_and_then; |
| 2 | +use clippy_utils::path_to_local; |
| 3 | +use clippy_utils::source::snippet_opt; |
| 4 | +use clippy_utils::visitors::{expr_visitor, is_local_used}; |
| 5 | +use rustc_errors::Applicability; |
| 6 | +use rustc_hir::intravisit::Visitor; |
| 7 | +use rustc_hir::{Block, Expr, ExprKind, HirId, Local, LocalSource, MatchSource, Node, Pat, PatKind, Stmt, StmtKind}; |
| 8 | +use rustc_lint::{LateContext, LateLintPass}; |
| 9 | +use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| 10 | +use rustc_span::Span; |
| 11 | + |
| 12 | +declare_clippy_lint! { |
| 13 | + /// ### What it does |
| 14 | + /// Checks for late initializations that can be replaced with a regular |
| 15 | + /// declaration |
| 16 | + /// |
| 17 | + /// ### Why is this bad? |
| 18 | + /// Assigning in the declaration is less repetitive |
| 19 | + /// |
| 20 | + /// ### Example |
| 21 | + /// ```rust |
| 22 | + /// let a; |
| 23 | + /// a = 1; |
| 24 | + /// |
| 25 | + /// let b; |
| 26 | + /// match 3 { |
| 27 | + /// 0 => b = "zero", |
| 28 | + /// 1 => b = "one", |
| 29 | + /// _ => b = "many", |
| 30 | + /// } |
| 31 | + /// |
| 32 | + /// let c; |
| 33 | + /// if true { |
| 34 | + /// c = 1; |
| 35 | + /// } else { |
| 36 | + /// c = -1; |
| 37 | + /// } |
| 38 | + /// ``` |
| 39 | + /// Use instead: |
| 40 | + /// ```rust |
| 41 | + /// let a = 1; |
| 42 | + /// |
| 43 | + /// let b = match 3 { |
| 44 | + /// 0 => "zero", |
| 45 | + /// 1 => "one", |
| 46 | + /// _ => "many", |
| 47 | + /// }; |
| 48 | + /// |
| 49 | + /// let c = if true { |
| 50 | + /// 1 |
| 51 | + /// } else { |
| 52 | + /// -1 |
| 53 | + /// }; |
| 54 | + /// ``` |
| 55 | + #[clippy::version = "1.58.0"] |
| 56 | + pub NEEDLESS_LATE_INIT, |
| 57 | + style, |
| 58 | + "late initializations that can be replaced with a regular declaration" |
| 59 | +} |
| 60 | +declare_lint_pass!(NeedlessLateInit => [NEEDLESS_LATE_INIT]); |
| 61 | + |
| 62 | +fn contains_assign_expr<'tcx>(cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) -> bool { |
| 63 | + let mut seen = false; |
| 64 | + expr_visitor(cx, |expr| { |
| 65 | + if let ExprKind::Assign(..) = expr.kind { |
| 66 | + seen = true; |
| 67 | + } |
| 68 | + |
| 69 | + !seen |
| 70 | + }) |
| 71 | + .visit_stmt(stmt); |
| 72 | + |
| 73 | + seen |
| 74 | +} |
| 75 | + |
| 76 | +#[derive(Debug)] |
| 77 | +struct LocalAssign { |
| 78 | + lhs_id: HirId, |
| 79 | + lhs_span: Span, |
| 80 | + rhs_span: Span, |
| 81 | + span: Span, |
| 82 | +} |
| 83 | + |
| 84 | +impl LocalAssign { |
| 85 | + fn from_expr(expr: &Expr<'_>, span: Span) -> Option<Self> { |
| 86 | + if let ExprKind::Assign(lhs, rhs, _) = expr.kind { |
| 87 | + if lhs.span.from_expansion() { |
| 88 | + return None; |
| 89 | + } |
| 90 | + |
| 91 | + Some(Self { |
| 92 | + lhs_id: path_to_local(lhs)?, |
| 93 | + lhs_span: lhs.span, |
| 94 | + rhs_span: rhs.span.source_callsite(), |
| 95 | + span, |
| 96 | + }) |
| 97 | + } else { |
| 98 | + None |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + fn new<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, binding_id: HirId) -> Option<LocalAssign> { |
| 103 | + let assign = match expr.kind { |
| 104 | + ExprKind::Block(Block { expr: Some(expr), .. }, _) => Self::from_expr(expr, expr.span), |
| 105 | + ExprKind::Block(block, _) => { |
| 106 | + if_chain! { |
| 107 | + if let Some((last, other_stmts)) = block.stmts.split_last(); |
| 108 | + if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = last.kind; |
| 109 | + |
| 110 | + let assign = Self::from_expr(expr, last.span)?; |
| 111 | + |
| 112 | + // avoid visiting if not needed |
| 113 | + if assign.lhs_id == binding_id; |
| 114 | + if other_stmts.iter().all(|stmt| !contains_assign_expr(cx, stmt)); |
| 115 | + |
| 116 | + then { |
| 117 | + Some(assign) |
| 118 | + } else { |
| 119 | + None |
| 120 | + } |
| 121 | + } |
| 122 | + }, |
| 123 | + ExprKind::Assign(..) => Self::from_expr(expr, expr.span), |
| 124 | + _ => None, |
| 125 | + }?; |
| 126 | + |
| 127 | + if assign.lhs_id == binding_id { |
| 128 | + Some(assign) |
| 129 | + } else { |
| 130 | + None |
| 131 | + } |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +fn assignment_suggestions<'tcx>( |
| 136 | + cx: &LateContext<'tcx>, |
| 137 | + binding_id: HirId, |
| 138 | + exprs: impl IntoIterator<Item = &'tcx Expr<'tcx>>, |
| 139 | +) -> Option<(Applicability, Vec<(Span, String)>)> { |
| 140 | + let mut assignments = Vec::new(); |
| 141 | + |
| 142 | + for expr in exprs { |
| 143 | + let ty = cx.typeck_results().expr_ty(expr); |
| 144 | + |
| 145 | + if ty.is_never() { |
| 146 | + continue; |
| 147 | + } |
| 148 | + if !ty.is_unit() { |
| 149 | + return None; |
| 150 | + } |
| 151 | + |
| 152 | + let assign = LocalAssign::new(cx, expr, binding_id)?; |
| 153 | + |
| 154 | + assignments.push(assign); |
| 155 | + } |
| 156 | + |
| 157 | + let suggestions = assignments |
| 158 | + .into_iter() |
| 159 | + .map(|assignment| Some((assignment.span, snippet_opt(cx, assignment.rhs_span)?))) |
| 160 | + .collect::<Option<Vec<(Span, String)>>>()?; |
| 161 | + |
| 162 | + let applicability = if suggestions.len() > 1 { |
| 163 | + // multiple suggestions don't work with rustfix in multipart_suggest |
| 164 | + // https://github.com/rust-lang/rustfix/issues/141 |
| 165 | + Applicability::Unspecified |
| 166 | + } else { |
| 167 | + Applicability::MachineApplicable |
| 168 | + }; |
| 169 | + Some((applicability, suggestions)) |
| 170 | +} |
| 171 | + |
| 172 | +struct Usage<'tcx> { |
| 173 | + stmt: &'tcx Stmt<'tcx>, |
| 174 | + expr: &'tcx Expr<'tcx>, |
| 175 | + needs_semi: bool, |
| 176 | +} |
| 177 | + |
| 178 | +fn first_usage<'tcx>( |
| 179 | + cx: &LateContext<'tcx>, |
| 180 | + binding_id: HirId, |
| 181 | + local_stmt_id: HirId, |
| 182 | + block: &'tcx Block<'tcx>, |
| 183 | +) -> Option<Usage<'tcx>> { |
| 184 | + block |
| 185 | + .stmts |
| 186 | + .iter() |
| 187 | + .skip_while(|stmt| stmt.hir_id != local_stmt_id) |
| 188 | + .skip(1) |
| 189 | + .find(|&stmt| is_local_used(cx, stmt, binding_id)) |
| 190 | + .and_then(|stmt| match stmt.kind { |
| 191 | + StmtKind::Expr(expr) => Some(Usage { |
| 192 | + stmt, |
| 193 | + expr, |
| 194 | + needs_semi: true, |
| 195 | + }), |
| 196 | + StmtKind::Semi(expr) => Some(Usage { |
| 197 | + stmt, |
| 198 | + expr, |
| 199 | + needs_semi: false, |
| 200 | + }), |
| 201 | + _ => None, |
| 202 | + }) |
| 203 | +} |
| 204 | + |
| 205 | +fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> { |
| 206 | + let span = local.span.with_hi(match local.ty { |
| 207 | + // let <pat>: <ty>; |
| 208 | + // ~~~~~~~~~~~~~~~ |
| 209 | + Some(ty) => ty.span.hi(), |
| 210 | + // let <pat>; |
| 211 | + // ~~~~~~~~~ |
| 212 | + None => local.pat.span.hi(), |
| 213 | + }); |
| 214 | + |
| 215 | + snippet_opt(cx, span) |
| 216 | +} |
| 217 | + |
| 218 | +fn check<'tcx>( |
| 219 | + cx: &LateContext<'tcx>, |
| 220 | + local: &'tcx Local<'tcx>, |
| 221 | + local_stmt: &'tcx Stmt<'tcx>, |
| 222 | + block: &'tcx Block<'tcx>, |
| 223 | + binding_id: HirId, |
| 224 | +) -> Option<()> { |
| 225 | + let usage = first_usage(cx, binding_id, local_stmt.hir_id, block)?; |
| 226 | + let binding_name = cx.tcx.hir().opt_name(binding_id)?; |
| 227 | + let let_snippet = local_snippet_without_semicolon(cx, local)?; |
| 228 | + |
| 229 | + match usage.expr.kind { |
| 230 | + ExprKind::Assign(..) => { |
| 231 | + let assign = LocalAssign::new(cx, usage.expr, binding_id)?; |
| 232 | + |
| 233 | + span_lint_and_then( |
| 234 | + cx, |
| 235 | + NEEDLESS_LATE_INIT, |
| 236 | + local_stmt.span, |
| 237 | + "unneeded late initalization", |
| 238 | + |diag| { |
| 239 | + diag.tool_only_span_suggestion( |
| 240 | + local_stmt.span, |
| 241 | + "remove the local", |
| 242 | + String::new(), |
| 243 | + Applicability::MachineApplicable, |
| 244 | + ); |
| 245 | + |
| 246 | + diag.span_suggestion( |
| 247 | + assign.lhs_span, |
| 248 | + &format!("declare `{}` here", binding_name), |
| 249 | + let_snippet, |
| 250 | + Applicability::MachineApplicable, |
| 251 | + ); |
| 252 | + }, |
| 253 | + ); |
| 254 | + }, |
| 255 | + ExprKind::If(_, then_expr, Some(else_expr)) => { |
| 256 | + let (applicability, suggestions) = assignment_suggestions(cx, binding_id, [then_expr, else_expr])?; |
| 257 | + |
| 258 | + span_lint_and_then( |
| 259 | + cx, |
| 260 | + NEEDLESS_LATE_INIT, |
| 261 | + local_stmt.span, |
| 262 | + "unneeded late initalization", |
| 263 | + |diag| { |
| 264 | + diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); |
| 265 | + |
| 266 | + diag.span_suggestion_verbose( |
| 267 | + usage.stmt.span.shrink_to_lo(), |
| 268 | + &format!("declare `{}` here", binding_name), |
| 269 | + format!("{} = ", let_snippet), |
| 270 | + applicability, |
| 271 | + ); |
| 272 | + |
| 273 | + diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability); |
| 274 | + |
| 275 | + if usage.needs_semi { |
| 276 | + diag.span_suggestion( |
| 277 | + usage.stmt.span.shrink_to_hi(), |
| 278 | + "add a semicolon after the if expression", |
| 279 | + ";".to_string(), |
| 280 | + applicability, |
| 281 | + ); |
| 282 | + } |
| 283 | + }, |
| 284 | + ); |
| 285 | + }, |
| 286 | + ExprKind::Match(_, arms, MatchSource::Normal) => { |
| 287 | + let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?; |
| 288 | + |
| 289 | + span_lint_and_then( |
| 290 | + cx, |
| 291 | + NEEDLESS_LATE_INIT, |
| 292 | + local_stmt.span, |
| 293 | + "unneeded late initalization", |
| 294 | + |diag| { |
| 295 | + diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); |
| 296 | + |
| 297 | + diag.span_suggestion_verbose( |
| 298 | + usage.stmt.span.shrink_to_lo(), |
| 299 | + &format!("declare `{}` here", binding_name), |
| 300 | + format!("{} = ", let_snippet), |
| 301 | + applicability, |
| 302 | + ); |
| 303 | + |
| 304 | + diag.multipart_suggestion("remove the assignments from the match arms", suggestions, applicability); |
| 305 | + |
| 306 | + if usage.needs_semi { |
| 307 | + diag.span_suggestion( |
| 308 | + usage.stmt.span.shrink_to_hi(), |
| 309 | + "add a semicolon after the match expression", |
| 310 | + ";".to_string(), |
| 311 | + applicability, |
| 312 | + ); |
| 313 | + } |
| 314 | + }, |
| 315 | + ); |
| 316 | + }, |
| 317 | + _ => {}, |
| 318 | + }; |
| 319 | + |
| 320 | + Some(()) |
| 321 | +} |
| 322 | + |
| 323 | +impl LateLintPass<'tcx> for NeedlessLateInit { |
| 324 | + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { |
| 325 | + let mut parents = cx.tcx.hir().parent_iter(local.hir_id); |
| 326 | + |
| 327 | + if_chain! { |
| 328 | + if let Local { |
| 329 | + init: None, |
| 330 | + pat: &Pat { |
| 331 | + kind: PatKind::Binding(_, binding_id, _, None), |
| 332 | + .. |
| 333 | + }, |
| 334 | + source: LocalSource::Normal, |
| 335 | + .. |
| 336 | + } = local; |
| 337 | + if let Some((_, Node::Stmt(local_stmt))) = parents.next(); |
| 338 | + if let Some((_, Node::Block(block))) = parents.next(); |
| 339 | + |
| 340 | + then { |
| 341 | + check(cx, local, local_stmt, block, binding_id); |
| 342 | + } |
| 343 | + } |
| 344 | + } |
| 345 | +} |
0 commit comments