1use std::cell::RefCell;
2use std::collections::hash_map;
3use std::rc::Rc;
4
5use itertools::Itertools as _;
6use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap};
7use rustc_data_structures::unord::{UnordMap, UnordSet};
8use rustc_errors::Subdiagnostic;
9use rustc_hir::CRATE_HIR_ID;
10use rustc_hir::def_id::LocalDefId;
11use rustc_index::bit_set::MixedBitSet;
12use rustc_index::{IndexSlice, IndexVec};
13use rustc_macros::{LintDiagnostic, Subdiagnostic};
14use rustc_middle::bug;
15use rustc_middle::mir::{
16 self, BasicBlock, Body, ClearCrossCrate, Local, Location, MirDumper, Place, StatementKind,
17 TerminatorKind,
18};
19use rustc_middle::ty::significant_drop_order::{
20 extract_component_with_significant_dtor, ty_dtor_span,
21};
22use rustc_middle::ty::{self, TyCtxt};
23use rustc_mir_dataflow::impls::MaybeInitializedPlaces;
24use rustc_mir_dataflow::move_paths::{LookupResult, MoveData, MovePathIndex};
25use rustc_mir_dataflow::{Analysis, MaybeReachable, ResultsCursor};
26use rustc_session::lint::builtin::TAIL_EXPR_DROP_ORDER;
27use rustc_session::lint::{self};
28use rustc_span::{DUMMY_SP, Span, Symbol};
29use tracing::debug;
30
31fn place_has_common_prefix<'tcx>(left: &Place<'tcx>, right: &Place<'tcx>) -> bool {
32 left.local == right.local
33 && left.projection.iter().zip(right.projection).all(|(left, right)| left == right)
34}
35
36#[derive(Debug, Clone, Copy)]
38enum MovePathIndexAtBlock {
39 Unknown,
41 None,
43 Some(MovePathIndex),
45}
46
47struct DropsReachable<'a, 'mir, 'tcx> {
48 body: &'a Body<'tcx>,
49 place: &'a Place<'tcx>,
50 drop_span: &'a mut Option<Span>,
51 move_data: &'a MoveData<'tcx>,
52 maybe_init: &'a mut ResultsCursor<'mir, 'tcx, MaybeInitializedPlaces<'mir, 'tcx>>,
53 block_drop_value_info: &'a mut IndexSlice<BasicBlock, MovePathIndexAtBlock>,
54 collected_drops: &'a mut MixedBitSet<MovePathIndex>,
55 visited: FxHashMap<BasicBlock, Rc<RefCell<MixedBitSet<MovePathIndex>>>>,
56}
57
58impl<'a, 'mir, 'tcx> DropsReachable<'a, 'mir, 'tcx> {
59 fn visit(&mut self, block: BasicBlock) {
60 let move_set_size = self.move_data.move_paths.len();
61 let make_new_path_set = || Rc::new(RefCell::new(MixedBitSet::new_empty(move_set_size)));
62
63 let data = &self.body.basic_blocks[block];
64 let Some(terminator) = &data.terminator else { return };
65 let dropped_local_here =
69 Rc::clone(self.visited.entry(block).or_insert_with(make_new_path_set));
70 match self.block_drop_value_info[block] {
73 MovePathIndexAtBlock::Some(dropped) => {
74 dropped_local_here.borrow_mut().insert(dropped);
75 }
76 MovePathIndexAtBlock::Unknown => {
77 if let TerminatorKind::Drop { place, .. } = &terminator.kind
78 && let LookupResult::Exact(idx) | LookupResult::Parent(Some(idx)) =
79 self.move_data.rev_lookup.find(place.as_ref())
80 {
81 self.maybe_init.seek_before_primary_effect(Location {
86 block,
87 statement_index: data.statements.len(),
88 });
89
90 if let MaybeReachable::Reachable(maybe_init) = self.maybe_init.get()
95 && maybe_init.contains(idx)
96 {
97 self.block_drop_value_info[block] = MovePathIndexAtBlock::Some(idx);
100 dropped_local_here.borrow_mut().insert(idx);
101 } else {
102 self.block_drop_value_info[block] = MovePathIndexAtBlock::None;
103 }
104 }
105 }
106 MovePathIndexAtBlock::None => {}
107 }
108
109 for succ in terminator.successors() {
110 let target = &self.body.basic_blocks[succ];
111 if target.is_cleanup {
112 continue;
113 }
114
115 let dropped_local_there = match self.visited.entry(succ) {
118 hash_map::Entry::Occupied(occupied_entry) => {
119 if succ == block
120 || !occupied_entry.get().borrow_mut().union(&*dropped_local_here.borrow())
121 {
122 continue;
125 }
126 Rc::clone(occupied_entry.get())
127 }
128 hash_map::Entry::Vacant(vacant_entry) => Rc::clone(
129 vacant_entry.insert(Rc::new(RefCell::new(dropped_local_here.borrow().clone()))),
130 ),
131 };
132 if let Some(terminator) = &target.terminator
133 && let TerminatorKind::Drop {
134 place: dropped_place,
135 target: _,
136 unwind: _,
137 replace: _,
138 drop: _,
139 async_fut: _,
140 } = &terminator.kind
141 && place_has_common_prefix(dropped_place, self.place)
142 {
143 self.collected_drops.union(&*dropped_local_there.borrow());
146 if self.drop_span.is_none() {
147 *self.drop_span = Some(terminator.source_info.span);
151 }
152 } else {
156 self.visit(succ)
157 }
158 }
159 }
160}
161
162fn place_descendent_of_bids<'tcx>(
166 mut idx: MovePathIndex,
167 move_data: &MoveData<'tcx>,
168 bids: &UnordSet<&Place<'tcx>>,
169) -> bool {
170 loop {
171 let path = &move_data.move_paths[idx];
172 if bids.contains(&path.place) {
173 return true;
174 }
175 if let Some(parent) = path.parent {
176 idx = parent;
177 } else {
178 return false;
179 }
180 }
181}
182
183pub(crate) fn run_lint<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId, body: &Body<'tcx>) {
185 if matches!(tcx.def_kind(def_id), rustc_hir::def::DefKind::SyntheticCoroutineBody) {
186 return;
188 }
189 if body.span.edition().at_least_rust_2024()
190 || tcx.lints_that_dont_need_to_run(()).contains(&lint::LintId::of(TAIL_EXPR_DROP_ORDER))
191 {
192 return;
193 }
194
195 let typing_env = ty::TypingEnv::non_body_analysis(tcx, def_id);
198
199 let mut bid_per_block = FxIndexMap::default();
205 let mut bid_places = UnordSet::new();
206
207 let mut ty_dropped_components = UnordMap::default();
208 for (block, data) in body.basic_blocks.iter_enumerated() {
209 for (statement_index, stmt) in data.statements.iter().enumerate() {
210 if let StatementKind::BackwardIncompatibleDropHint { place, reason: _ } = &stmt.kind {
211 let ty = place.ty(body, tcx).ty;
212 if ty_dropped_components
213 .entry(ty)
214 .or_insert_with(|| extract_component_with_significant_dtor(tcx, typing_env, ty))
215 .is_empty()
216 {
217 continue;
218 }
219 bid_per_block
220 .entry(block)
221 .or_insert(vec![])
222 .push((Location { block, statement_index }, &**place));
223 bid_places.insert(&**place);
224 }
225 }
226 }
227 if bid_per_block.is_empty() {
228 return;
229 }
230
231 if let Some(dumper) = MirDumper::new(tcx, "lint_tail_expr_drop_order", body) {
232 dumper.dump_mir(body);
233 }
234
235 let locals_with_user_names = collect_user_names(body);
236 let is_closure_like = tcx.is_closure_like(def_id.to_def_id());
237
238 let move_data = MoveData::gather_moves(body, tcx, |_| true);
242 let mut maybe_init = MaybeInitializedPlaces::new(tcx, body, &move_data)
243 .iterate_to_fixpoint(tcx, body, None)
244 .into_results_cursor(body);
245 let mut block_drop_value_info =
246 IndexVec::from_elem_n(MovePathIndexAtBlock::Unknown, body.basic_blocks.len());
247 for (&block, candidates) in &bid_per_block {
248 let mut all_locals_dropped = MixedBitSet::new_empty(move_data.move_paths.len());
251 let mut drop_span = None;
252 for &(_, place) in candidates.iter() {
253 let mut collected_drops = MixedBitSet::new_empty(move_data.move_paths.len());
254 DropsReachable {
261 body,
262 place,
263 drop_span: &mut drop_span,
264 move_data: &move_data,
265 maybe_init: &mut maybe_init,
266 block_drop_value_info: &mut block_drop_value_info,
267 collected_drops: &mut collected_drops,
268 visited: Default::default(),
269 }
270 .visit(block);
271 all_locals_dropped.union(&collected_drops);
277 }
278
279 {
281 let mut to_exclude = MixedBitSet::new_empty(all_locals_dropped.domain_size());
282 for path_idx in all_locals_dropped.iter() {
285 let move_path = &move_data.move_paths[path_idx];
286 let dropped_local = move_path.place.local;
287 if dropped_local == Local::ZERO {
295 debug!(?dropped_local, "skip return value");
296 to_exclude.insert(path_idx);
297 continue;
298 }
299 if is_closure_like && matches!(dropped_local, ty::CAPTURE_STRUCT_LOCAL) {
303 debug!(?dropped_local, "skip closure captures");
304 to_exclude.insert(path_idx);
305 continue;
306 }
307 if place_descendent_of_bids(path_idx, &move_data, &bid_places) {
322 debug!(?dropped_local, "skip descendent of bids");
323 to_exclude.insert(path_idx);
324 continue;
325 }
326 let observer_ty = move_path.place.ty(body, tcx).ty;
327 if ty_dropped_components
329 .entry(observer_ty)
330 .or_insert_with(|| {
331 extract_component_with_significant_dtor(tcx, typing_env, observer_ty)
332 })
333 .is_empty()
334 {
335 debug!(?dropped_local, "skip non-droppy types");
336 to_exclude.insert(path_idx);
337 continue;
338 }
339 }
340 if let Ok(local) = candidates.iter().map(|&(_, place)| place.local).all_equal_value() {
344 for path_idx in all_locals_dropped.iter() {
345 if move_data.move_paths[path_idx].place.local == local {
346 to_exclude.insert(path_idx);
347 }
348 }
349 }
350 all_locals_dropped.subtract(&to_exclude);
351 }
352 if all_locals_dropped.is_empty() {
353 continue;
355 }
356
357 let local_names = assign_observables_names(
360 all_locals_dropped
361 .iter()
362 .map(|path_idx| move_data.move_paths[path_idx].place.local)
363 .chain(candidates.iter().map(|(_, place)| place.local)),
364 &locals_with_user_names,
365 );
366
367 let mut lint_root = None;
368 let mut local_labels = vec![];
369 for &(_, place) in candidates {
371 let linted_local_decl = &body.local_decls[place.local];
372 let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
373 bug!("a name should have been assigned")
374 };
375 let name = name.as_str();
376
377 if lint_root.is_none()
378 && let ClearCrossCrate::Set(data) =
379 &body.source_scopes[linted_local_decl.source_info.scope].local_data
380 {
381 lint_root = Some(data.lint_root);
382 }
383
384 let mut seen_dyn = false;
386 let destructors = ty_dropped_components
387 .get(&linted_local_decl.ty)
388 .unwrap()
389 .iter()
390 .filter_map(|&ty| {
391 if let Some(span) = ty_dtor_span(tcx, ty) {
392 Some(DestructorLabel { span, name, dtor_kind: "concrete" })
393 } else if matches!(ty.kind(), ty::Dynamic(..)) {
394 if seen_dyn {
395 None
396 } else {
397 seen_dyn = true;
398 Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
399 }
400 } else {
401 None
402 }
403 })
404 .collect();
405 local_labels.push(LocalLabel {
406 span: linted_local_decl.source_info.span,
407 destructors,
408 name,
409 is_generated_name,
410 is_dropped_first_edition_2024: true,
411 });
412 }
413
414 for path_idx in all_locals_dropped.iter() {
416 let place = &move_data.move_paths[path_idx].place;
417 let observer_ty = place.ty(body, tcx).ty;
419
420 let observer_local_decl = &body.local_decls[place.local];
421 let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
422 bug!("a name should have been assigned")
423 };
424 let name = name.as_str();
425
426 let mut seen_dyn = false;
427 let destructors = extract_component_with_significant_dtor(tcx, typing_env, observer_ty)
428 .into_iter()
429 .filter_map(|ty| {
430 if let Some(span) = ty_dtor_span(tcx, ty) {
431 Some(DestructorLabel { span, name, dtor_kind: "concrete" })
432 } else if matches!(ty.kind(), ty::Dynamic(..)) {
433 if seen_dyn {
434 None
435 } else {
436 seen_dyn = true;
437 Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
438 }
439 } else {
440 None
441 }
442 })
443 .collect();
444 local_labels.push(LocalLabel {
445 span: observer_local_decl.source_info.span,
446 destructors,
447 name,
448 is_generated_name,
449 is_dropped_first_edition_2024: false,
450 });
451 }
452
453 let span = local_labels[0].span;
454 tcx.emit_node_span_lint(
455 lint::builtin::TAIL_EXPR_DROP_ORDER,
456 lint_root.unwrap_or(CRATE_HIR_ID),
457 span,
458 TailExprDropOrderLint { local_labels, drop_span, _epilogue: () },
459 );
460 }
461}
462
463fn collect_user_names(body: &Body<'_>) -> FxIndexMap<Local, Symbol> {
465 let mut names = FxIndexMap::default();
466 for var_debug_info in &body.var_debug_info {
467 if let mir::VarDebugInfoContents::Place(place) = &var_debug_info.value
468 && let Some(local) = place.local_or_deref_local()
469 {
470 names.entry(local).or_insert(var_debug_info.name);
471 }
472 }
473 names
474}
475
476fn assign_observables_names(
478 locals: impl IntoIterator<Item = Local>,
479 user_names: &FxIndexMap<Local, Symbol>,
480) -> FxIndexMap<Local, (String, bool)> {
481 let mut names = FxIndexMap::default();
482 let mut assigned_names = FxHashSet::default();
483 let mut idx = 0u64;
484 let mut fresh_name = || {
485 idx += 1;
486 (format!("#{idx}"), true)
487 };
488 for local in locals {
489 let name = if let Some(name) = user_names.get(&local) {
490 let name = name.as_str();
491 if assigned_names.contains(name) { fresh_name() } else { (name.to_owned(), false) }
492 } else {
493 fresh_name()
494 };
495 assigned_names.insert(name.0.clone());
496 names.insert(local, name);
497 }
498 names
499}
500
501#[derive(LintDiagnostic)]
502#[diag(mir_transform_tail_expr_drop_order)]
503struct TailExprDropOrderLint<'a> {
504 #[subdiagnostic]
505 local_labels: Vec<LocalLabel<'a>>,
506 #[label(mir_transform_drop_location)]
507 drop_span: Option<Span>,
508 #[note(mir_transform_note_epilogue)]
509 _epilogue: (),
510}
511
512struct LocalLabel<'a> {
513 span: Span,
514 name: &'a str,
515 is_generated_name: bool,
516 is_dropped_first_edition_2024: bool,
517 destructors: Vec<DestructorLabel<'a>>,
518}
519
520impl Subdiagnostic for LocalLabel<'_> {
522 fn add_to_diag<G: rustc_errors::EmissionGuarantee>(self, diag: &mut rustc_errors::Diag<'_, G>) {
523 diag.remove_arg("name");
525 diag.arg("name", self.name);
526 diag.remove_arg("is_generated_name");
527 diag.arg("is_generated_name", self.is_generated_name);
528 diag.remove_arg("is_dropped_first_edition_2024");
529 diag.arg("is_dropped_first_edition_2024", self.is_dropped_first_edition_2024);
530 let msg = diag.eagerly_translate(crate::fluent_generated::mir_transform_tail_expr_local);
531 diag.span_label(self.span, msg);
532 for dtor in self.destructors {
533 dtor.add_to_diag(diag);
534 }
535 let msg =
536 diag.eagerly_translate(crate::fluent_generated::mir_transform_label_local_epilogue);
537 diag.span_label(self.span, msg);
538 }
539}
540
541#[derive(Subdiagnostic)]
542#[note(mir_transform_tail_expr_dtor)]
543struct DestructorLabel<'a> {
544 #[primary_span]
545 span: Span,
546 dtor_kind: &'static str,
547 name: &'a str,
548}