|
| 1 | +use clippy_utils::diagnostics::span_lint; |
| 2 | +use clippy_utils::fn_has_unsatisfiable_preds; |
| 3 | +use clippy_utils::source::snippet_opt; |
| 4 | +use itertools::Itertools; |
| 5 | +use rustc_const_eval::interpret::Scalar; |
| 6 | +use rustc_hir::def_id::LocalDefId; |
| 7 | +use rustc_hir::{intravisit::FnKind, Body, FnDecl}; |
| 8 | +use rustc_index::IndexVec; |
| 9 | +use rustc_lint::{LateContext, LateLintPass}; |
| 10 | +use rustc_middle::mir::{ |
| 11 | + self, interpret::ConstValue, visit::Visitor, Constant, Location, Mutability, Operand, Place, Rvalue, |
| 12 | +}; |
| 13 | +use rustc_middle::mir::{AggregateKind, PlaceElem, Terminator, TerminatorKind}; |
| 14 | +use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| 15 | +use rustc_span::source_map::Spanned; |
| 16 | +use rustc_span::Span; |
| 17 | + |
| 18 | +declare_clippy_lint! { |
| 19 | + /// ### What it does |
| 20 | + /// Checks for locals that are always assigned the same value. |
| 21 | + /// |
| 22 | + /// ### Why is this bad? |
| 23 | + /// It's almost always a typo. If not, it can be made immutable, or turned into a constant. |
| 24 | + /// |
| 25 | + /// ### Example |
| 26 | + /// ```rust |
| 27 | + /// let mut x = 1; |
| 28 | + /// x = 1; |
| 29 | + /// ``` |
| 30 | + /// Use instead: |
| 31 | + /// ```rust |
| 32 | + /// let x = 1; |
| 33 | + /// ``` |
| 34 | + #[clippy::version = "1.72.0"] |
| 35 | + pub LOCAL_ASSIGNED_SINGLE_VALUE, |
| 36 | + correctness, |
| 37 | + "disallows assigning locals many times with the same value" |
| 38 | +} |
| 39 | +declare_lint_pass!(LocalAssignedSingleValue => [LOCAL_ASSIGNED_SINGLE_VALUE]); |
| 40 | + |
| 41 | +impl LateLintPass<'_> for LocalAssignedSingleValue { |
| 42 | + fn check_fn( |
| 43 | + &mut self, |
| 44 | + cx: &LateContext<'_>, |
| 45 | + _: FnKind<'_>, |
| 46 | + _: &FnDecl<'_>, |
| 47 | + _: &Body<'_>, |
| 48 | + _: Span, |
| 49 | + def_id: LocalDefId, |
| 50 | + ) { |
| 51 | + // Building MIR for `fn`s with unsatisfiable preds results in ICE. |
| 52 | + if fn_has_unsatisfiable_preds(cx, def_id.to_def_id()) { |
| 53 | + return; |
| 54 | + } |
| 55 | + |
| 56 | + let mir = cx.tcx.optimized_mir(def_id.to_def_id()); |
| 57 | + let mut v = V { |
| 58 | + body: mir, |
| 59 | + cx, |
| 60 | + map: mir.local_decls.iter().map(|_| LocalUsageValues::default()).collect(), |
| 61 | + }; |
| 62 | + v.visit_body(mir); |
| 63 | + |
| 64 | + // for (local, i) in v.map.iter_enumerated() { |
| 65 | + // dbg!(local, i); |
| 66 | + // } |
| 67 | + |
| 68 | + for (local, i) in v.map.iter_enumerated() { |
| 69 | + if !i.assigned_non_const_rvalue |
| 70 | + && !i.mut_ref_acquired |
| 71 | + && nested_locals_are_not_from_bad_instr(&v.map, i) |
| 72 | + && assignments_all_same_value(&v.map, i) |
| 73 | + { |
| 74 | + let LocalUsageValues { |
| 75 | + usage, |
| 76 | + mut_ref_acquired: _, |
| 77 | + assigned_non_const_rvalue: _, |
| 78 | + is_from_bad_instr: _, |
| 79 | + } = i; |
| 80 | + |
| 81 | + if let Some(local_decl) = mir.local_decls.get(local) |
| 82 | + && let [dbg_info] = &*mir |
| 83 | + .var_debug_info |
| 84 | + .iter() |
| 85 | + .filter(|info| info.source_info.span == local_decl.source_info.span) |
| 86 | + .collect_vec() |
| 87 | + // Don't handle function arguments. |
| 88 | + && dbg_info.argument_index.is_none() |
| 89 | + // Ignore anything from a procedural macro, or locals we cannot prove aren't |
| 90 | + // temporaries |
| 91 | + && let Some(snippet) = snippet_opt(cx, dbg_info.source_info.span) |
| 92 | + && snippet.ends_with(dbg_info.name.as_str()) |
| 93 | + { |
| 94 | + span_lint( |
| 95 | + cx, |
| 96 | + LOCAL_ASSIGNED_SINGLE_VALUE, |
| 97 | + usage.iter().map(|i| i.span).collect_vec(), |
| 98 | + "local only ever assigned single value", |
| 99 | + ); |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + /* |
| 105 | + for (local, usage) in &v.local_usage { |
| 106 | + if should_lint(&v.local_usage, *local, usage) { |
| 107 | + let LocalUsageValues { |
| 108 | + usage, |
| 109 | + mut_ref_acquired: _, |
| 110 | + assigned_non_const: _, |
| 111 | + } = usage; |
| 112 | +
|
| 113 | + if let Some(local_decl) = mir.local_decls.get(*local) |
| 114 | + && let [dbg_info] = &*mir |
| 115 | + .var_debug_info |
| 116 | + .iter() |
| 117 | + .filter(|info| info.source_info.span == local_decl.source_info.span) |
| 118 | + .collect_vec() |
| 119 | + // Don't handle function arguments. |
| 120 | + && dbg_info.argument_index.is_none() |
| 121 | + // Ignore anything from a procedural macro, or locals we cannot prove aren't |
| 122 | + // temporaries |
| 123 | + && let Some(snippet) = snippet_opt(cx, dbg_info.source_info.span) |
| 124 | + && snippet.ends_with(dbg_info.name.as_str()) |
| 125 | + { |
| 126 | + span_lint( |
| 127 | + cx, |
| 128 | + LOCAL_ASSIGNED_SINGLE_VALUE, |
| 129 | + usage.iter().map(|(span, _)| *span).collect_vec(), |
| 130 | + "local only ever assigned single value", |
| 131 | + ); |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + */ |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +type LocalUsageMap<'tcx> = IndexVec<mir::Local, LocalUsageValues<'tcx>>; |
| 140 | + |
| 141 | +/// Holds the data we have for the usage of a local. |
| 142 | +#[derive(Debug, Default)] |
| 143 | +struct LocalUsageValues<'tcx> { |
| 144 | + /// Where and what this local is assigned. |
| 145 | + usage: Vec<Spanned<LocalUsage<'tcx>>>, |
| 146 | + /// Whether it's mutably borrowed, ever. We should not lint this. |
| 147 | + mut_ref_acquired: bool, |
| 148 | + /// Whether it's assigned a value we cannot prove is constant, ever. We should not lint this. |
| 149 | + assigned_non_const_rvalue: bool, |
| 150 | + /// Whether it's assigned a value that we know cannot be constant. This is differentiated from |
| 151 | + /// `assigned_non_const` since we check this for nested locals. |
| 152 | + /// |
| 153 | + /// This is set to `true` for function calls or binary operations. |
| 154 | + is_from_bad_instr: bool, |
| 155 | +} |
| 156 | + |
| 157 | +#[derive(Debug)] |
| 158 | +enum LocalUsage<'tcx> { |
| 159 | + /// A single `Scalar`, for stuff like `i32` or `bool`. |
| 160 | + Scalar(Scalar), |
| 161 | + /// Any number of `Scalar`s. This is used for handling arrays and tuples |
| 162 | + Scalars(Vec<Scalar>), |
| 163 | + /// When a `Local` depends on the value of another local |
| 164 | + DependsOn(mir::Local), |
| 165 | + /// When a `Local` depends on the value of another local by accessing any of its fields or |
| 166 | + /// indexing |
| 167 | + DependsOnWithProj(mir::Local, &'tcx PlaceElem<'tcx>), |
| 168 | +} |
| 169 | + |
| 170 | +struct V<'a, 'tcx> { |
| 171 | + body: &'a mir::Body<'tcx>, |
| 172 | + cx: &'a LateContext<'tcx>, |
| 173 | + map: LocalUsageMap<'tcx>, |
| 174 | +} |
| 175 | + |
| 176 | +impl<'a, 'tcx> Visitor<'tcx> for V<'a, 'tcx> { |
| 177 | + fn visit_assign(&mut self, place: &Place<'tcx>, rvalue: &Rvalue<'tcx>, loc: Location) { |
| 178 | + let Self { body, cx, map } = self; |
| 179 | + let Some(stmt) = body.stmt_at(loc).left() else { |
| 180 | + return; |
| 181 | + }; |
| 182 | + |
| 183 | + if stmt.source_info.span.from_expansion() { |
| 184 | + return; |
| 185 | + } |
| 186 | + |
| 187 | + // Do not lint if there are any mutable borrows to a local |
| 188 | + if let Rvalue::Ref(_, mir::BorrowKind::Unique | mir::BorrowKind::Mut { .. }, place) |
| 189 | + | Rvalue::AddressOf(Mutability::Mut, place) = rvalue |
| 190 | + { |
| 191 | + map[place.local].mut_ref_acquired = true; |
| 192 | + return; |
| 193 | + } |
| 194 | + |
| 195 | + let usage = &mut map[place.local]; |
| 196 | + |
| 197 | + if let Rvalue::Use(Operand::Constant(constant)) = rvalue |
| 198 | + && let Constant { literal, .. } = **constant |
| 199 | + && let Some(ConstValue::Scalar(scalar)) = literal.try_to_value(cx.tcx) |
| 200 | + { |
| 201 | + usage.usage.push(Spanned { node: LocalUsage::Scalar(scalar), span: stmt.source_info.span }); |
| 202 | + } else if let Rvalue::Use(operand) = rvalue |
| 203 | + && let Some(place) = operand.place() |
| 204 | + { |
| 205 | + if let [base_proj, ..] = place.projection.as_slice() |
| 206 | + // Handle `let [x, y] = [1, 1]` and `let (x, y) = (1, 1)` |
| 207 | + && matches!(base_proj, PlaceElem::Field(..) | PlaceElem::Index(..)) |
| 208 | + { |
| 209 | + usage.usage.push(Spanned { |
| 210 | + node: LocalUsage::DependsOnWithProj(place.local, base_proj), |
| 211 | + span: stmt.source_info.span, |
| 212 | + }); |
| 213 | + } else { |
| 214 | + // It seems sometimes a local's just moved, no projections, so let's make sure we |
| 215 | + // don't set `assigned_non_const` to true for these cases |
| 216 | + usage.usage.push(Spanned { |
| 217 | + node: LocalUsage::DependsOn(place.local), |
| 218 | + span: stmt.source_info.span |
| 219 | + }); |
| 220 | + } |
| 221 | + } |
| 222 | + // Handle creation of tuples/arrays, otherwise the above would be useless |
| 223 | + else if let Rvalue::Aggregate(kind, fields) = rvalue |
| 224 | + // TODO: Handle `Adt`s, if possible. |
| 225 | + && let AggregateKind::Array(..) | AggregateKind::Tuple = **kind |
| 226 | + // TODO: Let's remove this `cloned` call, if possible. |
| 227 | + && let Some(scalars) = extract_scalars(cx, fields.into_iter().cloned()) |
| 228 | + { |
| 229 | + usage.usage.push(Spanned { |
| 230 | + node: LocalUsage::Scalars(scalars), |
| 231 | + span: stmt.source_info.span, |
| 232 | + }) |
| 233 | + } else if let Rvalue::BinaryOp(..) | Rvalue::CheckedBinaryOp(..) = rvalue { |
| 234 | + usage.is_from_bad_instr = true; |
| 235 | + } else { |
| 236 | + // We can also probably handle stuff like `x += 1` here, maybe. But this would be |
| 237 | + // very very complex. Let's keep it simple enough. |
| 238 | + usage.assigned_non_const_rvalue = true; |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + fn visit_terminator(&mut self, terminator: &Terminator<'_>, _: Location) { |
| 243 | + let Self { body: _, cx: _, map } = self; |
| 244 | + |
| 245 | + // TODO: Maybe we can allow const fns, if we can evaluate them of course |
| 246 | + if let TerminatorKind::Call { destination, .. } = terminator.kind { |
| 247 | + map[destination.local].is_from_bad_instr = true; |
| 248 | + } |
| 249 | + } |
| 250 | +} |
| 251 | + |
| 252 | +/// `None` means any one of the `Operand`s is not an `Operand::Constant`. |
| 253 | +fn extract_scalars<'tcx, O>(cx: &LateContext<'tcx>, operands: O) -> Option<Vec<Scalar>> |
| 254 | +where |
| 255 | + O: IntoIterator<Item = Operand<'tcx>>, |
| 256 | +{ |
| 257 | + operands |
| 258 | + .into_iter() |
| 259 | + .map(|operand| -> Option<_> { |
| 260 | + if let Operand::Constant(constant) = operand |
| 261 | + && let Constant { literal, .. } = *constant |
| 262 | + && let ConstValue::Scalar(scalar) = literal.try_to_value(cx.tcx)? |
| 263 | + { |
| 264 | + return Some(scalar); |
| 265 | + } |
| 266 | + |
| 267 | + None |
| 268 | + }) |
| 269 | + .collect::<Option<Vec<_>>>() |
| 270 | +} |
| 271 | + |
| 272 | +fn assignments_all_same_value(map: &LocalUsageMap<'_>, usage: &LocalUsageValues<'_>) -> bool { |
| 273 | + let LocalUsageValues { |
| 274 | + usage, |
| 275 | + mut_ref_acquired: _, |
| 276 | + assigned_non_const_rvalue: _, |
| 277 | + is_from_bad_instr: _, |
| 278 | + } = usage; |
| 279 | + |
| 280 | + if usage.len() <= 1 { |
| 281 | + return false; |
| 282 | + } |
| 283 | + |
| 284 | + // TODO: This code is clone-hell. |
| 285 | + |
| 286 | + let mut all_assignments = vec![]; |
| 287 | + for assignment in usage { |
| 288 | + match &assignment.node { |
| 289 | + LocalUsage::Scalar(scalar) => { |
| 290 | + all_assignments.push(scalar.clone()); |
| 291 | + }, |
| 292 | + // FIXME: This doesn't handle assignment of tuples, arrays or anything else currently. |
| 293 | + LocalUsage::Scalars(_) => {}, |
| 294 | + // FIXME: This doesn't allow nested dependencies, currently. |
| 295 | + // FIXME: This only allows one assignment for dependencies. |
| 296 | + LocalUsage::DependsOn(local) => { |
| 297 | + let [assignment] = &*map[*local].usage else { |
| 298 | + continue; |
| 299 | + }; |
| 300 | + match assignment.node { |
| 301 | + LocalUsage::Scalar(scalar) => all_assignments.push(scalar.clone()), |
| 302 | + LocalUsage::Scalars(_) => {}, |
| 303 | + _ => return false, |
| 304 | + } |
| 305 | + }, |
| 306 | + LocalUsage::DependsOnWithProj(local, base_proj) => { |
| 307 | + let [assignment] = &*map[*local].usage else { |
| 308 | + continue; |
| 309 | + }; |
| 310 | + match base_proj { |
| 311 | + PlaceElem::Field(idx, _) if let LocalUsage::Scalars(scalars) = &assignment.node => { |
| 312 | + all_assignments.push(scalars[idx.as_usize()].clone()); |
| 313 | + }, |
| 314 | + PlaceElem::Index(idx) if let LocalUsage::Scalars(scalars) = &assignment.node => { |
| 315 | + all_assignments.push(scalars[idx.as_usize()].clone()); |
| 316 | + }, |
| 317 | + _ => return false, |
| 318 | + } |
| 319 | + }, |
| 320 | + } |
| 321 | + } |
| 322 | + |
| 323 | + if let [head, tail @ ..] = &*all_assignments && tail.iter().all(|i| i == head) { |
| 324 | + return true; |
| 325 | + } |
| 326 | + |
| 327 | + false |
| 328 | +} |
| 329 | + |
| 330 | +fn nested_locals_are_not_from_bad_instr(map: &LocalUsageMap<'_>, usage: &LocalUsageValues<'_>) -> bool { |
| 331 | + // FIXME: This is a hacky workaround to not have a stack overflow. Instead, we should fix the root |
| 332 | + // cause. |
| 333 | + nested_locals_are_not_from_bad_instr_inner(map, usage, 0) |
| 334 | +} |
| 335 | + |
| 336 | +fn nested_locals_are_not_from_bad_instr_inner( |
| 337 | + map: &LocalUsageMap<'_>, |
| 338 | + usage: &LocalUsageValues<'_>, |
| 339 | + depth: usize, |
| 340 | +) -> bool { |
| 341 | + if depth < 10 && !usage.is_from_bad_instr { |
| 342 | + let mut all_good_instrs = true; |
| 343 | + for assignment in &usage.usage { |
| 344 | + match assignment.node { |
| 345 | + LocalUsage::Scalar(_) | LocalUsage::Scalars(_) => continue, |
| 346 | + LocalUsage::DependsOn(local) | LocalUsage::DependsOnWithProj(local, _) => { |
| 347 | + all_good_instrs &= nested_locals_are_not_from_bad_instr_inner(map, &map[local], depth + 1); |
| 348 | + }, |
| 349 | + } |
| 350 | + } |
| 351 | + return all_good_instrs; |
| 352 | + } |
| 353 | + |
| 354 | + false |
| 355 | +} |
0 commit comments