Skip to content

Commit d073638

Browse files
committed
Lint on unfulfilled lint expectation (RFC 2383)
1 parent a56f4b4 commit d073638

File tree

5 files changed

+430
-0
lines changed

5 files changed

+430
-0
lines changed

compiler/rustc_lint/src/expect.rs

+381
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
use crate::builtin;
2+
use crate::context::{CheckLintNameResult, LintStore};
3+
use crate::late::unerased_lint_store;
4+
use crate::levels::{try_parse_reason_metadata, ParseLintReasonResult};
5+
use rustc_ast as ast;
6+
use rustc_ast::unwrap_or;
7+
use rustc_ast_pretty::pprust;
8+
use rustc_hir as hir;
9+
use rustc_hir::intravisit;
10+
use rustc_middle::hir::map::Map;
11+
use rustc_middle::ty::query::Providers;
12+
use rustc_middle::ty::TyCtxt;
13+
use rustc_session::lint::{Level, LintId};
14+
use rustc_session::Session;
15+
use rustc_span::symbol::{sym, Symbol};
16+
use rustc_span::{MultiSpan, Span};
17+
18+
fn check_expect_lint(tcx: TyCtxt<'_>, _: ()) -> () {
19+
if !tcx.sess.features_untracked().enabled(sym::lint_reasons) {
20+
return;
21+
}
22+
23+
let store = unerased_lint_store(tcx);
24+
let krate = tcx.hir().krate();
25+
26+
let mut checker = LintExpectationChecker::new(tcx, tcx.sess, store);
27+
checker.check_crate(krate);
28+
}
29+
30+
/// This is used by the expectation check to define in which scope expectations
31+
/// count towards fulfilling the expectation.
32+
#[derive(Debug, Clone, Copy)]
33+
enum CheckScope {
34+
/// The scope it limited to a `Span` only lint emissions within this span
35+
/// can fulfill the expectation.
36+
Span(Span),
37+
/// All emissions in this crate can fulfill this emission. This is used for
38+
/// crate expectation attributes.
39+
CreateWide,
40+
}
41+
42+
impl CheckScope {
43+
fn includes_span(&self, emission: &MultiSpan) -> bool {
44+
match self {
45+
CheckScope::Span(scope_span) => {
46+
emission.primary_spans().iter().any(|span| scope_span.contains(*span))
47+
}
48+
CheckScope::CreateWide => true,
49+
}
50+
}
51+
}
52+
53+
#[derive(Debug, Clone)]
54+
struct LintIdEmission {
55+
lint_id: LintId,
56+
span: MultiSpan,
57+
}
58+
59+
impl LintIdEmission {
60+
fn new(lint_id: LintId, span: MultiSpan) -> Self {
61+
Self { lint_id, span }
62+
}
63+
}
64+
65+
#[derive(Debug, Clone)]
66+
struct LintExpectation {
67+
lints: Vec<LintId>,
68+
reason: Option<Symbol>,
69+
attr_span: Span,
70+
}
71+
72+
impl LintExpectation {
73+
fn new(lints: Vec<LintId>, reason: Option<Symbol>, attr_span: Span) -> Self {
74+
Self { lints, reason, attr_span }
75+
}
76+
}
77+
78+
struct LintExpectationChecker<'a, 'tcx> {
79+
tcx: TyCtxt<'tcx>,
80+
sess: &'a Session,
81+
store: &'a LintStore,
82+
emitted_lints: Vec<LintIdEmission>,
83+
crate_attrs: &'tcx [ast::Attribute],
84+
}
85+
86+
impl<'a, 'tcx> LintExpectationChecker<'a, 'tcx> {
87+
fn new(tcx: TyCtxt<'tcx>, sess: &'a Session, store: &'a LintStore) -> Self {
88+
let mut expect_lint_emissions = tcx.sess.diagnostic().steal_expect_lint_emissions();
89+
let crate_attrs = tcx.hir().attrs(hir::CRATE_HIR_ID);
90+
let emitted_lints = expect_lint_emissions
91+
.drain(..)
92+
.filter_map(|emission| {
93+
if let CheckLintNameResult::Ok(&[id]) =
94+
store.check_lint_name(sess, &emission.lint_name, None, crate_attrs)
95+
{
96+
Some(LintIdEmission::new(id, emission.lint_span))
97+
} else {
98+
None
99+
}
100+
})
101+
.collect();
102+
103+
Self { tcx, sess, store, emitted_lints, crate_attrs }
104+
}
105+
106+
fn check_item_with_attrs<F>(&mut self, id: hir::HirId, scope: Span, f: F)
107+
where
108+
F: FnOnce(&mut Self),
109+
{
110+
let mut expectations = self.collect_expectations(id);
111+
112+
f(self);
113+
114+
for expect in expectations.drain(..) {
115+
self.check_expectation(expect, CheckScope::Span(scope), id);
116+
}
117+
}
118+
119+
fn check_crate(&mut self, krate: &'tcx hir::Crate<'tcx>) {
120+
let mut expectations = self.collect_expectations(hir::CRATE_HIR_ID);
121+
122+
intravisit::walk_crate(self, krate);
123+
124+
for expect in expectations.drain(..) {
125+
self.check_expectation(expect, CheckScope::CreateWide, hir::CRATE_HIR_ID);
126+
}
127+
}
128+
129+
fn collect_expectations(&self, id: hir::HirId) -> Vec<LintExpectation> {
130+
let mut result = Vec::new();
131+
132+
for attr in self.tcx.hir().attrs(id) {
133+
// We only care about expectations
134+
if attr.name_or_empty() != sym::expect {
135+
continue;
136+
}
137+
138+
self.sess.mark_attr_used(attr);
139+
140+
let mut metas = unwrap_or!(attr.meta_item_list(), continue);
141+
if metas.is_empty() {
142+
// FIXME (#55112): issue unused-attributes lint for `#[level()]`
143+
continue;
144+
}
145+
146+
// Before processing the lint names, look for a reason (RFC 2383)
147+
// at the end.
148+
let tail_li = &metas[metas.len() - 1];
149+
let reason = match try_parse_reason_metadata(tail_li, self.sess) {
150+
ParseLintReasonResult::Ok(reason) => {
151+
metas.pop().unwrap();
152+
Some(reason)
153+
}
154+
ParseLintReasonResult::MalformedReason => {
155+
metas.pop().unwrap();
156+
None
157+
}
158+
ParseLintReasonResult::NotFound => None,
159+
};
160+
161+
// This will simply collect the lints specified in the expect attribute.
162+
// Error handling about unknown renamed and weird lints is done by the
163+
// `LintLevelMapBuilder`
164+
let mut lints: Vec<LintId> = Default::default();
165+
for li in metas {
166+
let mut meta_item = match li {
167+
ast::NestedMetaItem::MetaItem(meta_item) if meta_item.is_word() => meta_item,
168+
_ => continue,
169+
};
170+
171+
// Extracting the tool
172+
let tool_name = if meta_item.path.segments.len() > 1 {
173+
Some(meta_item.path.segments.remove(0).ident.name)
174+
} else {
175+
None
176+
};
177+
178+
// Checking the lint name
179+
let name = pprust::path_to_string(&meta_item.path);
180+
match &self.store.check_lint_name(self.sess, &name, tool_name, self.crate_attrs) {
181+
CheckLintNameResult::Ok(ids) => {
182+
lints.extend_from_slice(ids);
183+
}
184+
CheckLintNameResult::Tool(result) => {
185+
match *result {
186+
Ok(ids) => {
187+
lints.extend_from_slice(ids);
188+
}
189+
Err((_, _)) => {
190+
// The lint could not be found, this can happen if the
191+
// lint doesn't exist in the tool or if the Tool is not
192+
// enabled. In either case we don't want to add it to the
193+
// lints as it can not be emitted during this compiler run
194+
// and the expectation could therefor also not be fulfilled.
195+
continue;
196+
}
197+
}
198+
}
199+
CheckLintNameResult::Warning(_, Some(new_name)) => {
200+
// The lint has been renamed. The `LintLevelMapBuilder` then
201+
// registers the level for the new name. This means that the
202+
// expectation of a renamed lint should also be fulfilled by
203+
// the new name of the lint.
204+
205+
// NOTE: `new_name` already includes the tool name, so we don't have to add it again.
206+
if let CheckLintNameResult::Ok(ids) =
207+
self.store.check_lint_name(self.sess, &new_name, None, self.crate_attrs)
208+
{
209+
lints.extend_from_slice(ids);
210+
}
211+
}
212+
CheckLintNameResult::Warning(_, _)
213+
| CheckLintNameResult::NoLint(_)
214+
| CheckLintNameResult::NoTool => {
215+
// The `LintLevelMapBuilder` will issue a message about this.
216+
continue;
217+
}
218+
}
219+
}
220+
221+
if !lints.is_empty() {
222+
result.push(LintExpectation::new(lints, reason, attr.span));
223+
}
224+
}
225+
226+
result
227+
}
228+
229+
fn check_expectation(
230+
&mut self,
231+
expectation: LintExpectation,
232+
scope: CheckScope,
233+
id: hir::HirId,
234+
) {
235+
let mut fulfilled = false;
236+
let mut index = 0;
237+
while index < self.emitted_lints.len() {
238+
let lint_emission = &self.emitted_lints[index];
239+
let lint = &lint_emission.lint_id;
240+
241+
if expectation.lints.contains(lint) && scope.includes_span(&lint_emission.span) {
242+
drop(self.emitted_lints.swap_remove(index));
243+
fulfilled = true;
244+
245+
// The index is not increase here as the entry in the
246+
// index has been changed.
247+
continue;
248+
}
249+
index += 1;
250+
}
251+
252+
if !fulfilled {
253+
self.emit_unfulfilled_expectation_lint(&expectation, expectation.attr_span, id);
254+
}
255+
}
256+
257+
fn emit_unfulfilled_expectation_lint(
258+
&mut self,
259+
expectation: &LintExpectation,
260+
span: Span,
261+
id: hir::HirId,
262+
) {
263+
let parent_id = self.tcx.hir().get_parent_node(id);
264+
let level = self.tcx.lint_level_at_node(builtin::UNFULFILLED_LINT_EXPECTATION, parent_id).0;
265+
if level == Level::Expect {
266+
// This diagnostic is actually expected. It has to be added manually to
267+
// `self.emitted_lints` because we only collect expected diagnostics at
268+
// the start. It would therefore not be included in the backlog.
269+
let expect_lint_name = builtin::UNFULFILLED_LINT_EXPECTATION.name.to_ascii_lowercase();
270+
if let CheckLintNameResult::Ok(&[expect_lint_id]) =
271+
self.store.check_lint_name(self.sess, &expect_lint_name, None, self.crate_attrs)
272+
{
273+
self.emitted_lints.push(LintIdEmission::new(expect_lint_id, span.into()));
274+
} else {
275+
unreachable!(
276+
"the `unfulfilled_lint_expectation` lint should be registered when this code is executed"
277+
);
278+
}
279+
280+
// The diagnostic will still be emitted as usual to make sure that it's
281+
// stored in cache.
282+
}
283+
284+
self.tcx.struct_span_lint_hir(
285+
builtin::UNFULFILLED_LINT_EXPECTATION,
286+
parent_id,
287+
span,
288+
|diag| {
289+
let mut diag = diag.build("this lint expectation is unfulfilled");
290+
if let Some(rationale) = expectation.reason {
291+
diag.note(&rationale.as_str());
292+
}
293+
diag.emit();
294+
},
295+
);
296+
}
297+
}
298+
299+
impl<'tcx> intravisit::Visitor<'tcx> for LintExpectationChecker<'_, 'tcx> {
300+
type Map = Map<'tcx>;
301+
302+
fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap<Self::Map> {
303+
intravisit::NestedVisitorMap::All(self.tcx.hir())
304+
}
305+
306+
fn visit_param(&mut self, param: &'tcx hir::Param<'tcx>) {
307+
self.check_item_with_attrs(param.hir_id, param.span, |builder| {
308+
intravisit::walk_param(builder, param);
309+
});
310+
}
311+
312+
fn visit_item(&mut self, it: &'tcx hir::Item<'tcx>) {
313+
self.check_item_with_attrs(it.hir_id(), it.span, |builder| {
314+
intravisit::walk_item(builder, it);
315+
});
316+
}
317+
318+
fn visit_foreign_item(&mut self, it: &'tcx hir::ForeignItem<'tcx>) {
319+
self.check_item_with_attrs(it.hir_id(), it.span, |builder| {
320+
intravisit::walk_foreign_item(builder, it);
321+
})
322+
}
323+
324+
fn visit_stmt(&mut self, e: &'tcx hir::Stmt<'tcx>) {
325+
// We will call `with_lint_attrs` when we walk
326+
// the `StmtKind`. The outer statement itself doesn't
327+
// define the lint levels.
328+
intravisit::walk_stmt(self, e);
329+
}
330+
331+
fn visit_expr(&mut self, e: &'tcx hir::Expr<'tcx>) {
332+
self.check_item_with_attrs(e.hir_id, e.span, |builder| {
333+
intravisit::walk_expr(builder, e);
334+
})
335+
}
336+
337+
fn visit_field_def(&mut self, s: &'tcx hir::FieldDef<'tcx>) {
338+
self.check_item_with_attrs(s.hir_id, s.span, |builder| {
339+
intravisit::walk_field_def(builder, s);
340+
})
341+
}
342+
343+
fn visit_variant(
344+
&mut self,
345+
v: &'tcx hir::Variant<'tcx>,
346+
g: &'tcx hir::Generics<'tcx>,
347+
item_id: hir::HirId,
348+
) {
349+
self.check_item_with_attrs(v.id, v.span, |builder| {
350+
intravisit::walk_variant(builder, v, g, item_id);
351+
})
352+
}
353+
354+
fn visit_local(&mut self, l: &'tcx hir::Local<'tcx>) {
355+
self.check_item_with_attrs(l.hir_id, l.span, |builder| {
356+
intravisit::walk_local(builder, l);
357+
})
358+
}
359+
360+
fn visit_arm(&mut self, a: &'tcx hir::Arm<'tcx>) {
361+
self.check_item_with_attrs(a.hir_id, a.span, |builder| {
362+
intravisit::walk_arm(builder, a);
363+
})
364+
}
365+
366+
fn visit_trait_item(&mut self, trait_item: &'tcx hir::TraitItem<'tcx>) {
367+
self.check_item_with_attrs(trait_item.hir_id(), trait_item.span, |builder| {
368+
intravisit::walk_trait_item(builder, trait_item);
369+
});
370+
}
371+
372+
fn visit_impl_item(&mut self, impl_item: &'tcx hir::ImplItem<'tcx>) {
373+
self.check_item_with_attrs(impl_item.hir_id(), impl_item.span, |builder| {
374+
intravisit::walk_impl_item(builder, impl_item);
375+
});
376+
}
377+
}
378+
379+
pub fn provide(providers: &mut Providers) {
380+
providers.check_expect_lint = check_expect_lint;
381+
}

compiler/rustc_lint/src/late.rs

+9
Original file line numberDiff line numberDiff line change
@@ -512,4 +512,13 @@ pub fn check_crate<'tcx, T: LateLintPass<'tcx>>(
512512
});
513513
},
514514
);
515+
516+
// This check has to be run after all lints are done processing for this crate
517+
//
518+
// This could most likely be optimized by checking the lint expectations on a module
519+
// level instead, as rustc runs the analysis in `lint_mod` on this level. However this
520+
// would require that the tracked lint emissions can be linked to a specific module in
521+
// the diagnostic emission. This is not possible to my knowledge and would also not coder
522+
// the lints that are validated on the crate level.
523+
tcx.sess.time("check_expect_lint", || tcx.check_expect_lint(()));
515524
}

0 commit comments

Comments
 (0)