Skip to content
/ rust Public
forked from rust-lang/rust

Commit 1e836d1

Browse files
committed
Auto merge of rust-lang#114710 - Urgau:fix-expect-dead_code-114557, r=cjgillot
Respect `#[expect]` the same way `#[allow]` is with the `dead_code` lint This PR makes the `#[expect]` attribute being respected in the same way the `#[allow]` attribute is with the `dead_code` lint. The fix is much more involved than I would have liked (and it's not because I didn't tried!), because the implementation took advantage of the fact that firing a lint in a allow context is a nop (for the user, as the lint is suppressed) to not fire-it at all. And will it's fine for `#[allow]`, it definitively isn't for `#[expect]`, as the presence and absence of the lint is significant. So a big part of the PR is just adding the context information of whenever an item is on the worklist because of an `[allow]`/`#[expect]` or not. Fixes rust-lang#114557
2 parents f1b8548 + d801a2f commit 1e836d1

6 files changed

+163
-33
lines changed

Diff for: compiler/rustc_passes/src/dead.rs

+93-33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use hir::def_id::{LocalDefIdMap, LocalDefIdSet};
66
use itertools::Itertools;
7+
use rustc_data_structures::unord::UnordSet;
78
use rustc_errors::MultiSpan;
89
use rustc_hir as hir;
910
use rustc_hir::def::{CtorOf, DefKind, Res};
@@ -42,8 +43,16 @@ fn should_explore(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
4243
)
4344
}
4445

46+
/// Determine if a work from the worklist is coming from the a `#[allow]`
47+
/// or a `#[expect]` of `dead_code`
48+
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
49+
enum ComesFromAllowExpect {
50+
Yes,
51+
No,
52+
}
53+
4554
struct MarkSymbolVisitor<'tcx> {
46-
worklist: Vec<LocalDefId>,
55+
worklist: Vec<(LocalDefId, ComesFromAllowExpect)>,
4756
tcx: TyCtxt<'tcx>,
4857
maybe_typeck_results: Option<&'tcx ty::TypeckResults<'tcx>>,
4958
live_symbols: LocalDefIdSet,
@@ -72,7 +81,7 @@ impl<'tcx> MarkSymbolVisitor<'tcx> {
7281
fn check_def_id(&mut self, def_id: DefId) {
7382
if let Some(def_id) = def_id.as_local() {
7483
if should_explore(self.tcx, def_id) || self.struct_constructors.contains_key(&def_id) {
75-
self.worklist.push(def_id);
84+
self.worklist.push((def_id, ComesFromAllowExpect::No));
7685
}
7786
self.live_symbols.insert(def_id);
7887
}
@@ -269,12 +278,14 @@ impl<'tcx> MarkSymbolVisitor<'tcx> {
269278
}
270279

271280
fn mark_live_symbols(&mut self) {
272-
let mut scanned = LocalDefIdSet::default();
273-
while let Some(id) = self.worklist.pop() {
274-
if !scanned.insert(id) {
281+
let mut scanned = UnordSet::default();
282+
while let Some(work) = self.worklist.pop() {
283+
if !scanned.insert(work) {
275284
continue;
276285
}
277286

287+
let (id, comes_from_allow_expect) = work;
288+
278289
// Avoid accessing the HIR for the synthesized associated type generated for RPITITs.
279290
if self.tcx.is_impl_trait_in_trait(id.to_def_id()) {
280291
self.live_symbols.insert(id);
@@ -286,7 +297,30 @@ impl<'tcx> MarkSymbolVisitor<'tcx> {
286297
let id = self.struct_constructors.get(&id).copied().unwrap_or(id);
287298

288299
if let Some(node) = self.tcx.hir().find_by_def_id(id) {
289-
self.live_symbols.insert(id);
300+
// When using `#[allow]` or `#[expect]` of `dead_code`, we do a QOL improvement
301+
// by declaring fn calls, statics, ... within said items as live, as well as
302+
// the item itself, although technically this is not the case.
303+
//
304+
// This means that the lint for said items will never be fired.
305+
//
306+
// This doesn't make any difference for the item declared with `#[allow]`, as
307+
// the lint firing will be a nop, as it will be silenced by the `#[allow]` of
308+
// the item.
309+
//
310+
// However, for `#[expect]`, the presence or absence of the lint is relevant,
311+
// so we don't add it to the list of live symbols when it comes from a
312+
// `#[expect]`. This means that we will correctly report an item as live or not
313+
// for the `#[expect]` case.
314+
//
315+
// Note that an item can and will be duplicated on the worklist with different
316+
// `ComesFromAllowExpect`, particulary if it was added from the
317+
// `effective_visibilities` query or from the `#[allow]`/`#[expect]` checks,
318+
// this "duplication" is essential as otherwise a function with `#[expect]`
319+
// called from a `pub fn` may be falsely reported as not live, falsely
320+
// triggering the `unfulfilled_lint_expectations` lint.
321+
if comes_from_allow_expect != ComesFromAllowExpect::Yes {
322+
self.live_symbols.insert(id);
323+
}
290324
self.visit_node(node);
291325
}
292326
}
@@ -513,16 +547,20 @@ impl<'tcx> Visitor<'tcx> for MarkSymbolVisitor<'tcx> {
513547
}
514548
}
515549

516-
fn has_allow_dead_code_or_lang_attr(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
550+
fn has_allow_dead_code_or_lang_attr(
551+
tcx: TyCtxt<'_>,
552+
def_id: LocalDefId,
553+
) -> Option<ComesFromAllowExpect> {
517554
fn has_lang_attr(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
518555
tcx.has_attr(def_id, sym::lang)
519556
// Stable attribute for #[lang = "panic_impl"]
520557
|| tcx.has_attr(def_id, sym::panic_handler)
521558
}
522559

523-
fn has_allow_dead_code(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
560+
fn has_allow_expect_dead_code(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
524561
let hir_id = tcx.hir().local_def_id_to_hir_id(def_id);
525-
tcx.lint_level_at_node(lint::builtin::DEAD_CODE, hir_id).0 == lint::Allow
562+
let lint_level = tcx.lint_level_at_node(lint::builtin::DEAD_CODE, hir_id).0;
563+
matches!(lint_level, lint::Allow | lint::Expect(_))
526564
}
527565

528566
fn has_used_like_attr(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
@@ -537,9 +575,13 @@ fn has_allow_dead_code_or_lang_attr(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool
537575
}
538576
}
539577

540-
has_allow_dead_code(tcx, def_id)
541-
|| has_used_like_attr(tcx, def_id)
542-
|| has_lang_attr(tcx, def_id)
578+
if has_allow_expect_dead_code(tcx, def_id) {
579+
Some(ComesFromAllowExpect::Yes)
580+
} else if has_used_like_attr(tcx, def_id) || has_lang_attr(tcx, def_id) {
581+
Some(ComesFromAllowExpect::No)
582+
} else {
583+
None
584+
}
543585
}
544586

545587
// These check_* functions seeds items that
@@ -557,21 +599,23 @@ fn has_allow_dead_code_or_lang_attr(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool
557599
// * Implementations of traits and trait methods
558600
fn check_item<'tcx>(
559601
tcx: TyCtxt<'tcx>,
560-
worklist: &mut Vec<LocalDefId>,
602+
worklist: &mut Vec<(LocalDefId, ComesFromAllowExpect)>,
561603
struct_constructors: &mut LocalDefIdMap<LocalDefId>,
562604
id: hir::ItemId,
563605
) {
564606
let allow_dead_code = has_allow_dead_code_or_lang_attr(tcx, id.owner_id.def_id);
565-
if allow_dead_code {
566-
worklist.push(id.owner_id.def_id);
607+
if let Some(comes_from_allow) = allow_dead_code {
608+
worklist.push((id.owner_id.def_id, comes_from_allow));
567609
}
568610

569611
match tcx.def_kind(id.owner_id) {
570612
DefKind::Enum => {
571613
let item = tcx.hir().item(id);
572614
if let hir::ItemKind::Enum(ref enum_def, _) = item.kind {
573-
if allow_dead_code {
574-
worklist.extend(enum_def.variants.iter().map(|variant| variant.def_id));
615+
if let Some(comes_from_allow) = allow_dead_code {
616+
worklist.extend(
617+
enum_def.variants.iter().map(|variant| (variant.def_id, comes_from_allow)),
618+
);
575619
}
576620

577621
for variant in enum_def.variants {
@@ -583,7 +627,7 @@ fn check_item<'tcx>(
583627
}
584628
DefKind::Impl { of_trait } => {
585629
if of_trait {
586-
worklist.push(id.owner_id.def_id);
630+
worklist.push((id.owner_id.def_id, ComesFromAllowExpect::No));
587631
}
588632

589633
// get DefIds from another query
@@ -594,8 +638,10 @@ fn check_item<'tcx>(
594638

595639
// And we access the Map here to get HirId from LocalDefId
596640
for id in local_def_ids {
597-
if of_trait || has_allow_dead_code_or_lang_attr(tcx, id) {
598-
worklist.push(id);
641+
if of_trait {
642+
worklist.push((id, ComesFromAllowExpect::No));
643+
} else if let Some(comes_from_allow) = has_allow_dead_code_or_lang_attr(tcx, id) {
644+
worklist.push((id, comes_from_allow));
599645
}
600646
}
601647
}
@@ -609,43 +655,59 @@ fn check_item<'tcx>(
609655
}
610656
DefKind::GlobalAsm => {
611657
// global_asm! is always live.
612-
worklist.push(id.owner_id.def_id);
658+
worklist.push((id.owner_id.def_id, ComesFromAllowExpect::No));
613659
}
614660
_ => {}
615661
}
616662
}
617663

618-
fn check_trait_item(tcx: TyCtxt<'_>, worklist: &mut Vec<LocalDefId>, id: hir::TraitItemId) {
664+
fn check_trait_item(
665+
tcx: TyCtxt<'_>,
666+
worklist: &mut Vec<(LocalDefId, ComesFromAllowExpect)>,
667+
id: hir::TraitItemId,
668+
) {
619669
use hir::TraitItemKind::{Const, Fn};
620670
if matches!(tcx.def_kind(id.owner_id), DefKind::AssocConst | DefKind::AssocFn) {
621671
let trait_item = tcx.hir().trait_item(id);
622672
if matches!(trait_item.kind, Const(_, Some(_)) | Fn(_, hir::TraitFn::Provided(_)))
623-
&& has_allow_dead_code_or_lang_attr(tcx, trait_item.owner_id.def_id)
673+
&& let Some(comes_from_allow) = has_allow_dead_code_or_lang_attr(tcx, trait_item.owner_id.def_id)
624674
{
625-
worklist.push(trait_item.owner_id.def_id);
675+
worklist.push((trait_item.owner_id.def_id, comes_from_allow));
626676
}
627677
}
628678
}
629679

630-
fn check_foreign_item(tcx: TyCtxt<'_>, worklist: &mut Vec<LocalDefId>, id: hir::ForeignItemId) {
680+
fn check_foreign_item(
681+
tcx: TyCtxt<'_>,
682+
worklist: &mut Vec<(LocalDefId, ComesFromAllowExpect)>,
683+
id: hir::ForeignItemId,
684+
) {
631685
if matches!(tcx.def_kind(id.owner_id), DefKind::Static(_) | DefKind::Fn)
632-
&& has_allow_dead_code_or_lang_attr(tcx, id.owner_id.def_id)
686+
&& let Some(comes_from_allow) = has_allow_dead_code_or_lang_attr(tcx, id.owner_id.def_id)
633687
{
634-
worklist.push(id.owner_id.def_id);
688+
worklist.push((id.owner_id.def_id, comes_from_allow));
635689
}
636690
}
637691

638-
fn create_and_seed_worklist(tcx: TyCtxt<'_>) -> (Vec<LocalDefId>, LocalDefIdMap<LocalDefId>) {
692+
fn create_and_seed_worklist(
693+
tcx: TyCtxt<'_>,
694+
) -> (Vec<(LocalDefId, ComesFromAllowExpect)>, LocalDefIdMap<LocalDefId>) {
639695
let effective_visibilities = &tcx.effective_visibilities(());
640696
// see `MarkSymbolVisitor::struct_constructors`
641697
let mut struct_constructors = Default::default();
642698
let mut worklist = effective_visibilities
643699
.iter()
644700
.filter_map(|(&id, effective_vis)| {
645-
effective_vis.is_public_at_level(Level::Reachable).then_some(id)
701+
effective_vis
702+
.is_public_at_level(Level::Reachable)
703+
.then_some(id)
704+
.map(|id| (id, ComesFromAllowExpect::No))
646705
})
647706
// Seed entry point
648-
.chain(tcx.entry_fn(()).and_then(|(def_id, _)| def_id.as_local()))
707+
.chain(
708+
tcx.entry_fn(())
709+
.and_then(|(def_id, _)| def_id.as_local().map(|id| (id, ComesFromAllowExpect::No))),
710+
)
649711
.collect::<Vec<_>>();
650712

651713
let crate_items = tcx.hir_crate_items(());
@@ -878,9 +940,7 @@ impl<'tcx> DeadVisitor<'tcx> {
878940
return true;
879941
};
880942

881-
self.live_symbols.contains(&def_id)
882-
|| has_allow_dead_code_or_lang_attr(self.tcx, def_id)
883-
|| name.as_str().starts_with('_')
943+
self.live_symbols.contains(&def_id) || name.as_str().starts_with('_')
884944
}
885945
}
886946

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// check-pass
2+
3+
// this test checks that the `dead_code` lint is *NOT* being emited
4+
// for `foo` as `foo` is being used by `main`, and so the `#[expect]`
5+
// is unfulfilled
6+
//
7+
// it also checks that the `dead_code` lint is also *NOT* emited
8+
// for `bar` as it's suppresed by the `#[expect]` on `bar`
9+
10+
#![feature(lint_reasons)]
11+
#![warn(dead_code)] // to override compiletest
12+
13+
fn bar() {}
14+
15+
#[expect(dead_code)]
16+
//~^ WARN this lint expectation is unfulfilled
17+
fn foo() { bar() }
18+
19+
fn main() { foo() }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
warning: this lint expectation is unfulfilled
2+
--> $DIR/allow-or-expect-dead_code-114557-2.rs:15:10
3+
|
4+
LL | #[expect(dead_code)]
5+
| ^^^^^^^^^
6+
|
7+
= note: `#[warn(unfulfilled_lint_expectations)]` on by default
8+
9+
warning: 1 warning emitted
10+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// check-pass
2+
3+
// this test makes sure that the `unfulfilled_lint_expectations` lint
4+
// is being emited for `foo` as foo is not dead code, it's pub
5+
6+
#![feature(lint_reasons)]
7+
#![warn(dead_code)] // to override compiletest
8+
9+
#[expect(dead_code)]
10+
//~^ WARN this lint expectation is unfulfilled
11+
pub fn foo() {}
12+
13+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
warning: this lint expectation is unfulfilled
2+
--> $DIR/allow-or-expect-dead_code-114557-3.rs:9:10
3+
|
4+
LL | #[expect(dead_code)]
5+
| ^^^^^^^^^
6+
|
7+
= note: `#[warn(unfulfilled_lint_expectations)]` on by default
8+
9+
warning: 1 warning emitted
10+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// check-pass
2+
// revisions: allow expect
3+
4+
// this test checks that no matter if we put #[allow(dead_code)]
5+
// or #[expect(dead_code)], no warning is being emited
6+
7+
#![feature(lint_reasons)]
8+
#![warn(dead_code)] // to override compiletest
9+
10+
fn f() {}
11+
12+
#[cfg_attr(allow, allow(dead_code))]
13+
#[cfg_attr(expect, expect(dead_code))]
14+
fn g() {
15+
f();
16+
}
17+
18+
fn main() {}

0 commit comments

Comments
 (0)