rustc_mir_transform/
lint_tail_expr_drop_order.rs

1use std::cell::RefCell;
2use std::collections::hash_map;
3use std::rc::Rc;
4
5use rustc_data_structures::fx::{FxHashMap, FxHashSet};
6use rustc_data_structures::unord::{UnordMap, UnordSet};
7use rustc_errors::Subdiagnostic;
8use rustc_hir::CRATE_HIR_ID;
9use rustc_hir::def_id::{DefId, LocalDefId};
10use rustc_index::bit_set::MixedBitSet;
11use rustc_index::{IndexSlice, IndexVec};
12use rustc_macros::{LintDiagnostic, Subdiagnostic};
13use rustc_middle::bug;
14use rustc_middle::mir::{
15    self, BasicBlock, Body, ClearCrossCrate, Local, Location, Place, StatementKind, TerminatorKind,
16    dump_mir,
17};
18use rustc_middle::ty::{self, Ty, TyCtxt};
19use rustc_mir_dataflow::impls::MaybeInitializedPlaces;
20use rustc_mir_dataflow::move_paths::{LookupResult, MoveData, MovePathIndex};
21use rustc_mir_dataflow::{Analysis, MaybeReachable, ResultsCursor};
22use rustc_session::lint::builtin::TAIL_EXPR_DROP_ORDER;
23use rustc_session::lint::{self};
24use rustc_span::{DUMMY_SP, Span, Symbol};
25use rustc_type_ir::data_structures::IndexMap;
26use smallvec::{SmallVec, smallvec};
27use tracing::{debug, instrument};
28
29fn place_has_common_prefix<'tcx>(left: &Place<'tcx>, right: &Place<'tcx>) -> bool {
30    left.local == right.local
31        && left.projection.iter().zip(right.projection).all(|(left, right)| left == right)
32}
33
34/// Cache entry of `drop` at a `BasicBlock`
35#[derive(Debug, Clone, Copy)]
36enum MovePathIndexAtBlock {
37    /// We know nothing yet
38    Unknown,
39    /// We know that the `drop` here has no effect
40    None,
41    /// We know that the `drop` here will invoke a destructor
42    Some(MovePathIndex),
43}
44
45struct DropsReachable<'a, 'mir, 'tcx> {
46    body: &'a Body<'tcx>,
47    place: &'a Place<'tcx>,
48    drop_span: &'a mut Option<Span>,
49    move_data: &'a MoveData<'tcx>,
50    maybe_init: &'a mut ResultsCursor<'mir, 'tcx, MaybeInitializedPlaces<'mir, 'tcx>>,
51    block_drop_value_info: &'a mut IndexSlice<BasicBlock, MovePathIndexAtBlock>,
52    collected_drops: &'a mut MixedBitSet<MovePathIndex>,
53    visited: FxHashMap<BasicBlock, Rc<RefCell<MixedBitSet<MovePathIndex>>>>,
54}
55
56impl<'a, 'mir, 'tcx> DropsReachable<'a, 'mir, 'tcx> {
57    fn visit(&mut self, block: BasicBlock) {
58        let move_set_size = self.move_data.move_paths.len();
59        let make_new_path_set = || Rc::new(RefCell::new(MixedBitSet::new_empty(move_set_size)));
60
61        let data = &self.body.basic_blocks[block];
62        let Some(terminator) = &data.terminator else { return };
63        // Given that we observe these dropped locals here at `block` so far, we will try to update
64        // the successor blocks. An occupied entry at `block` in `self.visited` signals that we
65        // have visited `block` before.
66        let dropped_local_here =
67            Rc::clone(self.visited.entry(block).or_insert_with(make_new_path_set));
68        // We could have invoked reverse lookup for a `MovePathIndex` every time, but unfortunately
69        // it is expensive. Let's cache them in `self.block_drop_value_info`.
70        match self.block_drop_value_info[block] {
71            MovePathIndexAtBlock::Some(dropped) => {
72                dropped_local_here.borrow_mut().insert(dropped);
73            }
74            MovePathIndexAtBlock::Unknown => {
75                if let TerminatorKind::Drop { place, .. } = &terminator.kind
76                    && let LookupResult::Exact(idx) | LookupResult::Parent(Some(idx)) =
77                        self.move_data.rev_lookup.find(place.as_ref())
78                {
79                    // Since we are working with MIRs at a very early stage, observing a `drop`
80                    // terminator is not indicative enough that the drop will definitely happen.
81                    // That is decided in the drop elaboration pass instead. Therefore, we need to
82                    // consult with the maybe-initialization information.
83                    self.maybe_init.seek_before_primary_effect(Location {
84                        block,
85                        statement_index: data.statements.len(),
86                    });
87
88                    // Check if the drop of `place` under inspection is really in effect. This is
89                    // true only when `place` may have been initialized along a control flow path
90                    // from a BID to the drop program point today. In other words, this is where
91                    // the drop of `place` will happen in the future instead.
92                    if let MaybeReachable::Reachable(maybe_init) = self.maybe_init.get()
93                        && maybe_init.contains(idx)
94                    {
95                        // We also cache the drop information, so that we do not need to check on
96                        // data-flow cursor again.
97                        self.block_drop_value_info[block] = MovePathIndexAtBlock::Some(idx);
98                        dropped_local_here.borrow_mut().insert(idx);
99                    } else {
100                        self.block_drop_value_info[block] = MovePathIndexAtBlock::None;
101                    }
102                }
103            }
104            MovePathIndexAtBlock::None => {}
105        }
106
107        for succ in terminator.successors() {
108            let target = &self.body.basic_blocks[succ];
109            if target.is_cleanup {
110                continue;
111            }
112
113            // As long as we are passing through a new block, or new dropped places to propagate,
114            // we will proceed with `succ`
115            let dropped_local_there = match self.visited.entry(succ) {
116                hash_map::Entry::Occupied(occupied_entry) => {
117                    if succ == block
118                        || !occupied_entry.get().borrow_mut().union(&*dropped_local_here.borrow())
119                    {
120                        // `succ` has been visited but no new drops observed so far,
121                        // so we can bail on `succ` until new drop information arrives
122                        continue;
123                    }
124                    Rc::clone(occupied_entry.get())
125                }
126                hash_map::Entry::Vacant(vacant_entry) => Rc::clone(
127                    vacant_entry.insert(Rc::new(RefCell::new(dropped_local_here.borrow().clone()))),
128                ),
129            };
130            if let Some(terminator) = &target.terminator
131                && let TerminatorKind::Drop {
132                    place: dropped_place,
133                    target: _,
134                    unwind: _,
135                    replace: _,
136                } = &terminator.kind
137                && place_has_common_prefix(dropped_place, self.place)
138            {
139                // We have now reached the current drop of the `place`.
140                // Let's check the observed dropped places in.
141                self.collected_drops.union(&*dropped_local_there.borrow());
142                if self.drop_span.is_none() {
143                    // FIXME(@dingxiangfei2009): it turns out that `self.body.source_scopes` are
144                    // still a bit wonky. There is a high chance that this span still points to a
145                    // block rather than a statement semicolon.
146                    *self.drop_span = Some(terminator.source_info.span);
147                }
148                // Now we have discovered a simple control flow path from a future drop point
149                // to the current drop point.
150                // We will not continue from there.
151            } else {
152                self.visit(succ)
153            }
154        }
155    }
156}
157
158/// An additional filter to exclude well-known types from the ecosystem
159/// because their drops are trivial.
160/// This returns additional types to check if the drops are delegated to those.
161/// A typical example is `hashbrown::HashMap<K, V>`, whose drop is delegated to `K` and `V`.
162fn true_significant_drop_ty<'tcx>(
163    tcx: TyCtxt<'tcx>,
164    ty: Ty<'tcx>,
165) -> Option<SmallVec<[Ty<'tcx>; 2]>> {
166    if let ty::Adt(def, args) = ty.kind() {
167        let mut did = def.did();
168        let mut name_rev = vec![];
169        loop {
170            let key = tcx.def_key(did);
171
172            match key.disambiguated_data.data {
173                rustc_hir::definitions::DefPathData::CrateRoot => {
174                    name_rev.push(tcx.crate_name(did.krate))
175                }
176                rustc_hir::definitions::DefPathData::TypeNs(symbol) => name_rev.push(symbol),
177                _ => return None,
178            }
179            if let Some(parent) = key.parent {
180                did = DefId { krate: did.krate, index: parent };
181            } else {
182                break;
183            }
184        }
185        let name_str: Vec<_> = name_rev.iter().rev().map(|x| x.as_str()).collect();
186        debug!(?name_str);
187        match name_str[..] {
188            // These are the types from Rust core ecosystem
189            ["sym" | "proc_macro2", ..]
190            | ["core" | "std", "task", "LocalWaker" | "Waker"]
191            | ["core" | "std", "task", "wake", "LocalWaker" | "Waker"] => Some(smallvec![]),
192            // These are important types from Rust ecosystem
193            ["tracing", "instrument", "Instrumented"] | ["bytes", "Bytes"] => Some(smallvec![]),
194            ["hashbrown", "raw", "RawTable" | "RawIntoIter"] => {
195                if let [ty, ..] = &***args
196                    && let Some(ty) = ty.as_type()
197                {
198                    Some(smallvec![ty])
199                } else {
200                    None
201                }
202            }
203            ["hashbrown", "raw", "RawDrain"] => {
204                if let [_, ty, ..] = &***args
205                    && let Some(ty) = ty.as_type()
206                {
207                    Some(smallvec![ty])
208                } else {
209                    None
210                }
211            }
212            _ => None,
213        }
214    } else {
215        None
216    }
217}
218
219/// Returns the list of types with a "potentially sigificant" that may be dropped
220/// by dropping a value of type `ty`.
221#[instrument(level = "debug", skip(tcx, typing_env))]
222fn extract_component_raw<'tcx>(
223    tcx: TyCtxt<'tcx>,
224    typing_env: ty::TypingEnv<'tcx>,
225    ty: Ty<'tcx>,
226    ty_seen: &mut UnordSet<Ty<'tcx>>,
227) -> SmallVec<[Ty<'tcx>; 4]> {
228    // Droppiness does not depend on regions, so let us erase them.
229    let ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty);
230
231    let tys = tcx.list_significant_drop_tys(typing_env.as_query_input(ty));
232    debug!(?ty, "components");
233    let mut out_tys = smallvec![];
234    for ty in tys {
235        if let Some(tys) = true_significant_drop_ty(tcx, ty) {
236            // Some types can be further opened up because the drop is simply delegated
237            for ty in tys {
238                if ty_seen.insert(ty) {
239                    out_tys.extend(extract_component_raw(tcx, typing_env, ty, ty_seen));
240                }
241            }
242        } else {
243            if ty_seen.insert(ty) {
244                out_tys.push(ty);
245            }
246        }
247    }
248    out_tys
249}
250
251#[instrument(level = "debug", skip(tcx, typing_env))]
252fn extract_component_with_significant_dtor<'tcx>(
253    tcx: TyCtxt<'tcx>,
254    typing_env: ty::TypingEnv<'tcx>,
255    ty: Ty<'tcx>,
256) -> SmallVec<[Ty<'tcx>; 4]> {
257    let mut tys = extract_component_raw(tcx, typing_env, ty, &mut Default::default());
258    let mut deduplicate = FxHashSet::default();
259    tys.retain(|oty| deduplicate.insert(*oty));
260    tys.into_iter().collect()
261}
262
263/// Extract the span of the custom destructor of a type
264/// especially the span of the `impl Drop` header or its entire block
265/// when we are working with current local crate.
266#[instrument(level = "debug", skip(tcx))]
267fn ty_dtor_span<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option<Span> {
268    match ty.kind() {
269        ty::Bool
270        | ty::Char
271        | ty::Int(_)
272        | ty::Uint(_)
273        | ty::Float(_)
274        | ty::Error(_)
275        | ty::Str
276        | ty::Never
277        | ty::RawPtr(_, _)
278        | ty::Ref(_, _, _)
279        | ty::FnPtr(_, _)
280        | ty::Tuple(_)
281        | ty::Dynamic(_, _, _)
282        | ty::Alias(_, _)
283        | ty::Bound(_, _)
284        | ty::Pat(_, _)
285        | ty::Placeholder(_)
286        | ty::Infer(_)
287        | ty::Slice(_)
288        | ty::Array(_, _)
289        | ty::UnsafeBinder(_) => None,
290
291        ty::Adt(adt_def, _) => {
292            let did = adt_def.did();
293            let try_local_did_span = |did: DefId| {
294                if let Some(local) = did.as_local() {
295                    tcx.source_span(local)
296                } else {
297                    tcx.def_span(did)
298                }
299            };
300            let dtor = if let Some(dtor) = tcx.adt_destructor(did) {
301                dtor.did
302            } else if let Some(dtor) = tcx.adt_async_destructor(did) {
303                dtor.future
304            } else {
305                return Some(try_local_did_span(did));
306            };
307            let def_key = tcx.def_key(dtor);
308            let Some(parent_index) = def_key.parent else { return Some(try_local_did_span(dtor)) };
309            let parent_did = DefId { index: parent_index, krate: dtor.krate };
310            Some(try_local_did_span(parent_did))
311        }
312        ty::Coroutine(did, _)
313        | ty::CoroutineWitness(did, _)
314        | ty::CoroutineClosure(did, _)
315        | ty::Closure(did, _)
316        | ty::FnDef(did, _)
317        | ty::Foreign(did) => Some(tcx.def_span(did)),
318        ty::Param(_) => None,
319    }
320}
321
322/// Check if a moved place at `idx` is a part of a BID.
323/// The use of this check is that we will consider drops on these
324/// as a drop of the overall BID and, thus, we can exclude it from the diagnosis.
325fn place_descendent_of_bids<'tcx>(
326    mut idx: MovePathIndex,
327    move_data: &MoveData<'tcx>,
328    bids: &UnordSet<&Place<'tcx>>,
329) -> bool {
330    loop {
331        let path = &move_data.move_paths[idx];
332        if bids.contains(&path.place) {
333            return true;
334        }
335        if let Some(parent) = path.parent {
336            idx = parent;
337        } else {
338            return false;
339        }
340    }
341}
342
343/// The core of the lint `tail-expr-drop-order`
344pub(crate) fn run_lint<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId, body: &Body<'tcx>) {
345    if matches!(tcx.def_kind(def_id), rustc_hir::def::DefKind::SyntheticCoroutineBody) {
346        // A synthetic coroutine has no HIR body and it is enough to just analyse the original body
347        return;
348    }
349    if body.span.edition().at_least_rust_2024()
350        || tcx.lints_that_dont_need_to_run(()).contains(&lint::LintId::of(TAIL_EXPR_DROP_ORDER))
351    {
352        return;
353    }
354
355    // FIXME(typing_env): This should be able to reveal the opaques local to the
356    // body using the typeck results.
357    let typing_env = ty::TypingEnv::non_body_analysis(tcx, def_id);
358
359    // ## About BIDs in blocks ##
360    // Track the set of blocks that contain a backwards-incompatible drop (BID)
361    // and, for each block, the vector of locations.
362    //
363    // We group them per-block because they tend to scheduled in the same drop ladder block.
364    let mut bid_per_block = IndexMap::default();
365    let mut bid_places = UnordSet::new();
366
367    let mut ty_dropped_components = UnordMap::default();
368    for (block, data) in body.basic_blocks.iter_enumerated() {
369        for (statement_index, stmt) in data.statements.iter().enumerate() {
370            if let StatementKind::BackwardIncompatibleDropHint { place, reason: _ } = &stmt.kind {
371                let ty = place.ty(body, tcx).ty;
372                if ty_dropped_components
373                    .entry(ty)
374                    .or_insert_with(|| extract_component_with_significant_dtor(tcx, typing_env, ty))
375                    .is_empty()
376                {
377                    continue;
378                }
379                bid_per_block
380                    .entry(block)
381                    .or_insert(vec![])
382                    .push((Location { block, statement_index }, &**place));
383                bid_places.insert(&**place);
384            }
385        }
386    }
387    if bid_per_block.is_empty() {
388        return;
389    }
390
391    dump_mir(tcx, false, "lint_tail_expr_drop_order", &0 as _, body, |_, _| Ok(()));
392    let locals_with_user_names = collect_user_names(body);
393    let is_closure_like = tcx.is_closure_like(def_id.to_def_id());
394
395    // Compute the "maybe initialized" information for this body.
396    // When we encounter a DROP of some place P we only care
397    // about the drop if `P` may be initialized.
398    let move_data = MoveData::gather_moves(body, tcx, |_| true);
399    let maybe_init = MaybeInitializedPlaces::new(tcx, body, &move_data);
400    let mut maybe_init = maybe_init.iterate_to_fixpoint(tcx, body, None).into_results_cursor(body);
401    let mut block_drop_value_info =
402        IndexVec::from_elem_n(MovePathIndexAtBlock::Unknown, body.basic_blocks.len());
403    for (&block, candidates) in &bid_per_block {
404        // We will collect drops on locals on paths between BID points to their actual drop locations
405        // into `all_locals_dropped`.
406        let mut all_locals_dropped = MixedBitSet::new_empty(move_data.move_paths.len());
407        let mut drop_span = None;
408        for &(_, place) in candidates.iter() {
409            let mut collected_drops = MixedBitSet::new_empty(move_data.move_paths.len());
410            // ## On detecting change in relative drop order ##
411            // Iterate through each BID-containing block `block`.
412            // If the place `P` targeted by the BID is "maybe initialized",
413            // then search forward to find the actual `DROP(P)` point.
414            // Everything dropped between the BID and the actual drop point
415            // is something whose relative drop order will change.
416            DropsReachable {
417                body,
418                place,
419                drop_span: &mut drop_span,
420                move_data: &move_data,
421                maybe_init: &mut maybe_init,
422                block_drop_value_info: &mut block_drop_value_info,
423                collected_drops: &mut collected_drops,
424                visited: Default::default(),
425            }
426            .visit(block);
427            // Compute the set `all_locals_dropped` of local variables that are dropped
428            // after the BID point but before the current drop point.
429            //
430            // These are the variables whose drop impls will be reordered with respect
431            // to `place`.
432            all_locals_dropped.union(&collected_drops);
433        }
434
435        // We shall now exclude some local bindings for the following cases.
436        {
437            let mut to_exclude = MixedBitSet::new_empty(all_locals_dropped.domain_size());
438            // We will now do subtraction from the candidate dropped locals, because of the
439            // following reasons.
440            for path_idx in all_locals_dropped.iter() {
441                let move_path = &move_data.move_paths[path_idx];
442                let dropped_local = move_path.place.local;
443                // a) A return value _0 will eventually be used
444                // Example:
445                // fn f() -> Droppy {
446                //     let _x = Droppy;
447                //     Droppy
448                // }
449                // _0 holds the literal `Droppy` and rightfully `_x` has to be dropped first
450                if dropped_local == Local::ZERO {
451                    debug!(?dropped_local, "skip return value");
452                    to_exclude.insert(path_idx);
453                    continue;
454                }
455                // b) If we are analysing a closure, the captures are still dropped last.
456                // This is part of the closure capture lifetime contract.
457                // They are similar to the return value _0 with respect to lifetime rules.
458                if is_closure_like && matches!(dropped_local, ty::CAPTURE_STRUCT_LOCAL) {
459                    debug!(?dropped_local, "skip closure captures");
460                    to_exclude.insert(path_idx);
461                    continue;
462                }
463                // c) Sometimes we collect places that are projections into the BID locals,
464                // so they are considered dropped now.
465                // Example:
466                // struct NotVeryDroppy(Droppy);
467                // impl Drop for Droppy {..}
468                // fn f() -> NotVeryDroppy {
469                //    let x = NotVeryDroppy(droppy());
470                //    {
471                //        let y: Droppy = x.0;
472                //        NotVeryDroppy(y)
473                //    }
474                // }
475                // `y` takes `x.0`, which invalidates `x` as a complete `NotVeryDroppy`
476                // so there is no point in linting against `x` any more.
477                if place_descendent_of_bids(path_idx, &move_data, &bid_places) {
478                    debug!(?dropped_local, "skip descendent of bids");
479                    to_exclude.insert(path_idx);
480                    continue;
481                }
482                let observer_ty = move_path.place.ty(body, tcx).ty;
483                // d) The collected local has no custom destructor that passes our ecosystem filter.
484                if ty_dropped_components
485                    .entry(observer_ty)
486                    .or_insert_with(|| {
487                        extract_component_with_significant_dtor(tcx, typing_env, observer_ty)
488                    })
489                    .is_empty()
490                {
491                    debug!(?dropped_local, "skip non-droppy types");
492                    to_exclude.insert(path_idx);
493                    continue;
494                }
495            }
496            // Suppose that all BIDs point into the same local,
497            // we can remove the this local from the observed drops,
498            // so that we can focus our diagnosis more on the others.
499            if candidates.iter().all(|&(_, place)| candidates[0].1.local == place.local) {
500                for path_idx in all_locals_dropped.iter() {
501                    if move_data.move_paths[path_idx].place.local == candidates[0].1.local {
502                        to_exclude.insert(path_idx);
503                    }
504                }
505            }
506            all_locals_dropped.subtract(&to_exclude);
507        }
508        if all_locals_dropped.is_empty() {
509            // No drop effect is observable, so let us move on.
510            continue;
511        }
512
513        // ## The final work to assemble the diagnosis ##
514        // First collect or generate fresh names for local variable bindings and temporary values.
515        let local_names = assign_observables_names(
516            all_locals_dropped
517                .iter()
518                .map(|path_idx| move_data.move_paths[path_idx].place.local)
519                .chain(candidates.iter().map(|(_, place)| place.local)),
520            &locals_with_user_names,
521        );
522
523        let mut lint_root = None;
524        let mut local_labels = vec![];
525        // We now collect the types with custom destructors.
526        for &(_, place) in candidates {
527            let linted_local_decl = &body.local_decls[place.local];
528            let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
529                bug!("a name should have been assigned")
530            };
531            let name = name.as_str();
532
533            if lint_root.is_none()
534                && let ClearCrossCrate::Set(data) =
535                    &body.source_scopes[linted_local_decl.source_info.scope].local_data
536            {
537                lint_root = Some(data.lint_root);
538            }
539
540            // Collect spans of the custom destructors.
541            let mut seen_dyn = false;
542            let destructors = ty_dropped_components
543                .get(&linted_local_decl.ty)
544                .unwrap()
545                .iter()
546                .filter_map(|&ty| {
547                    if let Some(span) = ty_dtor_span(tcx, ty) {
548                        Some(DestructorLabel { span, name, dtor_kind: "concrete" })
549                    } else if matches!(ty.kind(), ty::Dynamic(..)) {
550                        if seen_dyn {
551                            None
552                        } else {
553                            seen_dyn = true;
554                            Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
555                        }
556                    } else {
557                        None
558                    }
559                })
560                .collect();
561            local_labels.push(LocalLabel {
562                span: linted_local_decl.source_info.span,
563                destructors,
564                name,
565                is_generated_name,
566                is_dropped_first_edition_2024: true,
567            });
568        }
569
570        // Similarly, custom destructors of the observed drops.
571        for path_idx in all_locals_dropped.iter() {
572            let place = &move_data.move_paths[path_idx].place;
573            // We are not using the type of the local because the drop may be partial.
574            let observer_ty = place.ty(body, tcx).ty;
575
576            let observer_local_decl = &body.local_decls[place.local];
577            let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
578                bug!("a name should have been assigned")
579            };
580            let name = name.as_str();
581
582            let mut seen_dyn = false;
583            let destructors = extract_component_with_significant_dtor(tcx, typing_env, observer_ty)
584                .into_iter()
585                .filter_map(|ty| {
586                    if let Some(span) = ty_dtor_span(tcx, ty) {
587                        Some(DestructorLabel { span, name, dtor_kind: "concrete" })
588                    } else if matches!(ty.kind(), ty::Dynamic(..)) {
589                        if seen_dyn {
590                            None
591                        } else {
592                            seen_dyn = true;
593                            Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
594                        }
595                    } else {
596                        None
597                    }
598                })
599                .collect();
600            local_labels.push(LocalLabel {
601                span: observer_local_decl.source_info.span,
602                destructors,
603                name,
604                is_generated_name,
605                is_dropped_first_edition_2024: false,
606            });
607        }
608
609        let span = local_labels[0].span;
610        tcx.emit_node_span_lint(
611            lint::builtin::TAIL_EXPR_DROP_ORDER,
612            lint_root.unwrap_or(CRATE_HIR_ID),
613            span,
614            TailExprDropOrderLint { local_labels, drop_span, _epilogue: () },
615        );
616    }
617}
618
619/// Extract binding names if available for diagnosis
620fn collect_user_names(body: &Body<'_>) -> IndexMap<Local, Symbol> {
621    let mut names = IndexMap::default();
622    for var_debug_info in &body.var_debug_info {
623        if let mir::VarDebugInfoContents::Place(place) = &var_debug_info.value
624            && let Some(local) = place.local_or_deref_local()
625        {
626            names.entry(local).or_insert(var_debug_info.name);
627        }
628    }
629    names
630}
631
632/// Assign names for anonymous or temporary values for diagnosis
633fn assign_observables_names(
634    locals: impl IntoIterator<Item = Local>,
635    user_names: &IndexMap<Local, Symbol>,
636) -> IndexMap<Local, (String, bool)> {
637    let mut names = IndexMap::default();
638    let mut assigned_names = FxHashSet::default();
639    let mut idx = 0u64;
640    let mut fresh_name = || {
641        idx += 1;
642        (format!("#{idx}"), true)
643    };
644    for local in locals {
645        let name = if let Some(name) = user_names.get(&local) {
646            let name = name.as_str();
647            if assigned_names.contains(name) { fresh_name() } else { (name.to_owned(), false) }
648        } else {
649            fresh_name()
650        };
651        assigned_names.insert(name.0.clone());
652        names.insert(local, name);
653    }
654    names
655}
656
657#[derive(LintDiagnostic)]
658#[diag(mir_transform_tail_expr_drop_order)]
659struct TailExprDropOrderLint<'a> {
660    #[subdiagnostic]
661    local_labels: Vec<LocalLabel<'a>>,
662    #[label(mir_transform_drop_location)]
663    drop_span: Option<Span>,
664    #[note(mir_transform_note_epilogue)]
665    _epilogue: (),
666}
667
668struct LocalLabel<'a> {
669    span: Span,
670    name: &'a str,
671    is_generated_name: bool,
672    is_dropped_first_edition_2024: bool,
673    destructors: Vec<DestructorLabel<'a>>,
674}
675
676/// A custom `Subdiagnostic` implementation so that the notes are delivered in a specific order
677impl Subdiagnostic for LocalLabel<'_> {
678    fn add_to_diag_with<
679        G: rustc_errors::EmissionGuarantee,
680        F: rustc_errors::SubdiagMessageOp<G>,
681    >(
682        self,
683        diag: &mut rustc_errors::Diag<'_, G>,
684        f: &F,
685    ) {
686        diag.arg("name", self.name);
687        diag.arg("is_generated_name", self.is_generated_name);
688        diag.arg("is_dropped_first_edition_2024", self.is_dropped_first_edition_2024);
689        let msg = f(diag, crate::fluent_generated::mir_transform_tail_expr_local.into());
690        diag.span_label(self.span, msg);
691        for dtor in self.destructors {
692            dtor.add_to_diag_with(diag, f);
693        }
694        let msg = f(diag, crate::fluent_generated::mir_transform_label_local_epilogue);
695        diag.span_label(self.span, msg);
696    }
697}
698
699#[derive(Subdiagnostic)]
700#[note(mir_transform_tail_expr_dtor)]
701struct DestructorLabel<'a> {
702    #[primary_span]
703    span: Span,
704    dtor_kind: &'static str,
705    name: &'a str,
706}