diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs index 2253007ce3027..5b74cba27cf74 100644 --- a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs +++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs @@ -73,6 +73,7 @@ fn annotation_type_for_level(level: Level) -> AnnotationType { // FIXME(#59346): Not sure how to map these two levels Level::Cancelled | Level::FailureNote => AnnotationType::Error, Level::Allow => panic!("Should not call with Allow"), + Level::Expect => panic!("Should not call with Expect"), } } diff --git a/compiler/rustc_errors/src/diagnostic.rs b/compiler/rustc_errors/src/diagnostic.rs index 45661ac15623f..2aa97c24261c9 100644 --- a/compiler/rustc_errors/src/diagnostic.rs +++ b/compiler/rustc_errors/src/diagnostic.rs @@ -98,7 +98,12 @@ impl Diagnostic { match self.level { Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => true, - Level::Warning | Level::Note | Level::Help | Level::Cancelled | Level::Allow => false, + Level::Warning + | Level::Note + | Level::Help + | Level::Cancelled + | Level::Allow + | Level::Expect => false, } } diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs index 993a7c2c162c6..4c4a0e9edc741 100644 --- a/compiler/rustc_errors/src/lib.rs +++ b/compiler/rustc_errors/src/lib.rs @@ -30,10 +30,15 @@ use rustc_span::source_map::SourceMap; use rustc_span::{Loc, MultiSpan, Span}; use std::borrow::Cow; +use std::clone::Clone; +use std::convert::TryFrom; use std::hash::{Hash, Hasher}; +use std::iter::Iterator; +use std::mem::take; use std::num::NonZeroUsize; use std::panic; use std::path::Path; +use std::result::Result::Ok; use std::{error, fmt}; use termcolor::{Color, ColorSpec}; @@ -332,6 +337,10 @@ struct HandlerInner { /// twice. emitted_diagnostics: FxHashSet, + /// This contains all lint emission information from this compiling session + /// with the emission level `Expect`. + expected_lint_emissions: Vec, + /// Stashed diagnostics emitted in one stage of the compiler that may be /// stolen by other stages (e.g. to improve them and add more information). /// The stashed diagnostics count towards the total error count. @@ -454,6 +463,7 @@ impl Handler { taught_diagnostics: Default::default(), emitted_diagnostic_codes: Default::default(), emitted_diagnostics: Default::default(), + expected_lint_emissions: Vec::new(), stashed_diagnostics: Default::default(), future_breakage_diagnostics: Vec::new(), }), @@ -589,6 +599,11 @@ impl Handler { DiagnosticBuilder::new(self, Level::Allow, msg) } + /// Construct a builder at the `Expect` level with the `msg`. + pub fn struct_expect(&self, msg: &str) -> DiagnosticBuilder<'_> { + DiagnosticBuilder::new(self, Level::Expect, msg) + } + /// Construct a builder at the `Error` level at the given `span` and with the `msg`. pub fn struct_span_err(&self, span: impl Into, msg: &str) -> DiagnosticBuilder<'_> { let mut result = self.struct_err(msg); @@ -800,6 +815,14 @@ impl Handler { pub fn delay_as_bug(&self, diagnostic: Diagnostic) { self.inner.borrow_mut().delay_as_bug(diagnostic) } + + /// This method takes all lint emission information that have been issued from + /// by `HandlerInner` in this session. This will steal the collection from the + /// internal handler and should therefore only be used to check for expected + /// lints. (RFC 2383) + pub fn steal_expect_lint_emissions(&self) -> Vec { + take(&mut self.inner.borrow_mut().expected_lint_emissions) + } } impl HandlerInner { @@ -840,6 +863,11 @@ impl HandlerInner { if diagnostic.level == Allow { return; + } else if diagnostic.level == Level::Expect { + if let Ok(emission) = LintEmission::try_from(diagnostic) { + self.expected_lint_emissions.push(emission); + } + return; } if let Some(ref code) = diagnostic.code { @@ -1090,6 +1118,70 @@ impl DelayedDiagnostic { } } +/// Used to track all emitted lints to later evaluate if expected lints have been +/// emitted. +#[must_use] +#[derive(Clone, Debug, PartialEq, Hash)] +pub struct LintEmission { + /// The lint name that was emitted. + pub lint_name: String, + + /// The spans of the emission. + /// + /// FIXME: We should define which span is taken from the diagnostic, this simply takes all spans + // until that is defined (xFrednet 2021-06-03) + pub lint_span: MultiSpan, +} + +impl TryFrom<&Diagnostic> for LintEmission { + type Error = (); + + fn try_from(diagnostic: &Diagnostic) -> Result { + if let Some(DiagnosticId::Lint { name, .. }) = &diagnostic.code { + Ok(LintEmission { lint_name: name.clone(), lint_span: extract_all_spans(diagnostic) }) + } else { + Err(()) + } + } +} + +fn extract_all_spans(source: &Diagnostic) -> MultiSpan { + let mut result = Vec::new(); + + result.append(&mut extract_spans_from_multispan(&source.span)); + + // Some lints only have a suggestion span. Example: unused_variables + for sugg in &source.suggestions { + for substitution in &sugg.substitutions { + for part in &substitution.parts { + result.push(part.span); + } + } + } + + // Some lints only have `SubDiagnostic`s. Example: const_item_mutation + for sub in &source.children { + result.append(&mut extract_spans_from_multispan(&sub.span)); + } + + MultiSpan::from_spans(result) +} + +fn extract_spans_from_multispan(source: &MultiSpan) -> Vec { + let mut result: Vec = source.primary_spans().into(); + + // Some lints only have span_lints for notes. Example: clashing_extern_declarations + result.extend( + source + .span_labels() + .iter() + .filter(|span| span.is_primary) + .map(|span_label| span_label.span), + ); + + result +} + #[derive(Copy, PartialEq, Clone, Hash, Debug, Encodable, Decodable)] pub enum Level { Bug, @@ -1101,6 +1193,7 @@ pub enum Level { Cancelled, FailureNote, Allow, + Expect, } impl fmt::Display for Level { @@ -1126,7 +1219,7 @@ impl Level { spec.set_fg(Some(Color::Cyan)).set_intense(true); } FailureNote => {} - Allow | Cancelled => unreachable!(), + Allow | Expect | Cancelled => unreachable!(), } spec } @@ -1141,6 +1234,7 @@ impl Level { FailureNote => "failure-note", Cancelled => panic!("Shouldn't call on cancelled error"), Allow => panic!("Shouldn't call on allowed error"), + Expect => panic!("Shouldn't call on expected error"), } } diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs index 36d035cdfd3ae..6c3d54f4c5fbc 100644 --- a/compiler/rustc_feature/src/builtin_attrs.rs +++ b/compiler/rustc_feature/src/builtin_attrs.rs @@ -204,6 +204,10 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[ // Lints: ungated!(warn, Normal, template!(List: r#"lint1, lint2, ..., /*opt*/ reason = "...""#)), ungated!(allow, Normal, template!(List: r#"lint1, lint2, ..., /*opt*/ reason = "...""#)), + gated!( + expect, Normal, template!(List: r#"lint1, lint2, ..., /*opt*/ reason = "...""#), lint_reasons, + experimental!(expect) + ), ungated!(forbid, Normal, template!(List: r#"lint1, lint2, ..., /*opt*/ reason = "...""#)), ungated!(deny, Normal, template!(List: r#"lint1, lint2, ..., /*opt*/ reason = "...""#)), ungated!(must_use, AssumedUsed, template!(Word, NameValueStr: "reason")), diff --git a/compiler/rustc_lint/src/context.rs b/compiler/rustc_lint/src/context.rs index ad8a41a56cc16..e28b26e1348bb 100644 --- a/compiler/rustc_lint/src/context.rs +++ b/compiler/rustc_lint/src/context.rs @@ -125,6 +125,7 @@ struct LintGroup { depr: Option, } +#[derive(Debug)] pub enum CheckLintNameResult<'a> { Ok(&'a [LintId]), /// Lint doesn't exist. Potentially contains a suggestion for a correct lint name. @@ -373,6 +374,9 @@ impl LintStore { Level::ForceWarn => "--force-warn", Level::Deny => "-D", Level::Forbid => "-F", + Level::Expect => { + unreachable!("lints with the level of `expect` should not run this code"); + } }, lint_name ); diff --git a/compiler/rustc_lint/src/expect.rs b/compiler/rustc_lint/src/expect.rs new file mode 100644 index 0000000000000..c2c202b114f27 --- /dev/null +++ b/compiler/rustc_lint/src/expect.rs @@ -0,0 +1,376 @@ +use crate::builtin; +use crate::context::{CheckLintNameResult, LintStore}; +use crate::late::unerased_lint_store; +use crate::levels::{try_parse_reason_metadata, ParseLintReasonResult}; +use rustc_ast as ast; +use rustc_ast::unwrap_or; +use rustc_ast_pretty::pprust; +use rustc_hir as hir; +use rustc_hir::intravisit; +use rustc_middle::hir::map::Map; +use rustc_middle::ty::query::Providers; +use rustc_middle::ty::TyCtxt; +use rustc_session::lint::{Level, LintId}; +use rustc_session::Session; +use rustc_span::symbol::{sym, Symbol}; +use rustc_span::{MultiSpan, Span}; + +fn check_expect_lint(tcx: TyCtxt<'_>, _: ()) -> () { + if !tcx.sess.features_untracked().enabled(sym::lint_reasons) { + return; + } + + let store = unerased_lint_store(tcx); + let krate = tcx.hir().krate(); + + let mut checker = LintExpectationChecker::new(tcx, tcx.sess, store); + checker.check_crate(krate); +} + +/// This is used by the expectation check to define in which scope expectations +/// count towards fulfilling the expectation. +#[derive(Debug, Clone, Copy)] +enum CheckScope { + /// The scope it limited to a `Span` only lint emissions within this span + /// can fulfill the expectation. + Span(Span), + /// All emissions in this crate can fulfill this emission. This is used for + /// crate expectation attributes. + CreateWide, +} + +impl CheckScope { + fn includes_span(&self, emission: &MultiSpan) -> bool { + match self { + CheckScope::Span(scope_span) => { + emission.primary_spans().iter().any(|span| scope_span.contains(*span)) + } + CheckScope::CreateWide => true, + } + } +} + +#[derive(Debug, Clone)] +struct LintIdEmission { + lint_id: LintId, + span: MultiSpan, +} + +impl LintIdEmission { + fn new(lint_id: LintId, span: MultiSpan) -> Self { + Self { lint_id, span } + } +} + +#[derive(Debug, Clone)] +struct LintExpectation { + lints: Vec, + reason: Option, + attr_span: Span, +} + +impl LintExpectation { + fn new(lints: Vec, reason: Option, attr_span: Span) -> Self { + Self { lints, reason, attr_span } + } +} + +struct LintExpectationChecker<'a, 'tcx> { + tcx: TyCtxt<'tcx>, + sess: &'a Session, + store: &'a LintStore, + emitted_lints: Vec, + crate_attrs: &'tcx [ast::Attribute], +} + +impl<'a, 'tcx> LintExpectationChecker<'a, 'tcx> { + fn new(tcx: TyCtxt<'tcx>, sess: &'a Session, store: &'a LintStore) -> Self { + let mut expect_lint_emissions = tcx.sess.diagnostic().steal_expect_lint_emissions(); + let crate_attrs = tcx.hir().attrs(hir::CRATE_HIR_ID); + let emitted_lints = expect_lint_emissions + .drain(..) + .filter_map(|emission| { + if let CheckLintNameResult::Ok(&[id]) = + store.check_lint_name(sess, &emission.lint_name, None, crate_attrs) + { + Some(LintIdEmission::new(id, emission.lint_span)) + } else { + None + } + }) + .collect(); + + Self { tcx, sess, store, emitted_lints, crate_attrs } + } + + fn check_item_with_attrs(&mut self, id: hir::HirId, scope: Span, f: F) + where + F: FnOnce(&mut Self), + { + let mut expectations = self.collect_expectations(id); + + f(self); + + for expect in expectations.drain(..) { + self.check_expectation(expect, CheckScope::Span(scope), id); + } + } + + fn check_crate(&mut self, krate: &'tcx hir::Crate<'tcx>) { + let mut expectations = self.collect_expectations(hir::CRATE_HIR_ID); + + intravisit::walk_crate(self, krate); + + for expect in expectations.drain(..) { + self.check_expectation(expect, CheckScope::CreateWide, hir::CRATE_HIR_ID); + } + } + + fn collect_expectations(&self, id: hir::HirId) -> Vec { + let mut result = Vec::new(); + + for attr in self.tcx.hir().attrs(id) { + // We only care about expectations + if attr.name_or_empty() != sym::expect { + continue; + } + + self.sess.mark_attr_used(attr); + + let mut metas = unwrap_or!(attr.meta_item_list(), continue); + if metas.is_empty() { + // FIXME (#55112): issue unused-attributes lint for `#[level()]` + continue; + } + + // Before processing the lint names, look for a reason (RFC 2383) + // at the end. + let tail_li = &metas[metas.len() - 1]; + let reason = match try_parse_reason_metadata(tail_li, self.sess) { + ParseLintReasonResult::Ok(reason) => { + metas.pop().unwrap(); + Some(reason) + } + ParseLintReasonResult::MalformedReason => { + metas.pop().unwrap(); + None + } + ParseLintReasonResult::NotFound => None, + }; + + // This will simply collect the lints specified in the expect attribute. + // Error handling about unknown renamed and weird lints is done by the + // `LintLevelMapBuilder` + let mut lints: Vec = Default::default(); + for li in metas { + let mut meta_item = match li { + ast::NestedMetaItem::MetaItem(meta_item) if meta_item.is_word() => meta_item, + _ => continue, + }; + + // Extracting the tool + let tool_name = if meta_item.path.segments.len() > 1 { + Some(meta_item.path.segments.remove(0).ident.name) + } else { + None + }; + + // Checking the lint name + let name = pprust::path_to_string(&meta_item.path); + match &self.store.check_lint_name(self.sess, &name, tool_name, self.crate_attrs) { + CheckLintNameResult::Ok(ids) => { + lints.extend_from_slice(ids); + } + CheckLintNameResult::Tool(result) => { + match *result { + Ok(ids) => { + lints.extend_from_slice(ids); + } + Err((_, _)) => { + // The lint could not be found, this can happen if the + // lint doesn't exist in the tool or if the Tool is not + // enabled. In either case we don't want to add it to the + // lints as it can not be emitted during this compiler run + // and the expectation could therefor also not be fulfilled. + continue; + } + } + } + CheckLintNameResult::Warning(_, Some(new_name)) => { + // The lint has been renamed. The `LintLevelMapBuilder` then + // registers the level for the new name. This means that the + // expectation of a renamed lint should also be fulfilled by + // the new name of the lint. + + // NOTE: `new_name` already includes the tool name, so we don't have to add it again. + if let CheckLintNameResult::Ok(ids) = + self.store.check_lint_name(self.sess, &new_name, None, self.crate_attrs) + { + lints.extend_from_slice(ids); + } + } + CheckLintNameResult::Warning(_, _) + | CheckLintNameResult::NoLint(_) + | CheckLintNameResult::NoTool => { + // The `LintLevelMapBuilder` will issue a message about this. + continue; + } + } + } + + if !lints.is_empty() { + result.push(LintExpectation::new(lints, reason, attr.span)); + } + } + + result + } + + fn check_expectation( + &mut self, + expectation: LintExpectation, + scope: CheckScope, + id: hir::HirId, + ) { + let mut fulfilled = false; + let mut index = 0; + while index < self.emitted_lints.len() { + let lint_emission = &self.emitted_lints[index]; + let lint = &lint_emission.lint_id; + + if expectation.lints.contains(lint) && scope.includes_span(&lint_emission.span) { + drop(self.emitted_lints.swap_remove(index)); + fulfilled = true; + + // The index is not increase here as the entry in the + // index has been changed. + continue; + } + index += 1; + } + + if !fulfilled { + self.emit_unfulfilled_expectation_lint(&expectation, expectation.attr_span, id); + } + } + + fn emit_unfulfilled_expectation_lint( + &mut self, + expectation: &LintExpectation, + span: Span, + id: hir::HirId, + ) { + let parent_id = self.tcx.hir().get_parent_node(id); + let level = + self.tcx.lint_level_at_node(builtin::UNFULFILLED_LINT_EXPECTATIONS, parent_id).0; + if level == Level::Expect { + // This diagnostic is actually expected. It has to be added manually to + // `self.emitted_lints` because we only collect expected diagnostics at + // the start. It would therefore not be included in the backlog. + self.emitted_lints.push(LintIdEmission::new( + LintId::of(builtin::UNFULFILLED_LINT_EXPECTATIONS), + span.into(), + )); + + // The diagnostic will still be emitted as usual to make sure that it's + // stored in cache. + } + + self.tcx.struct_span_lint_hir( + builtin::UNFULFILLED_LINT_EXPECTATIONS, + parent_id, + span, + |diag| { + let mut diag = diag.build("this lint expectation is unfulfilled"); + if let Some(rationale) = expectation.reason { + diag.note(&rationale.as_str()); + } + diag.emit(); + }, + ); + } +} + +impl<'tcx> intravisit::Visitor<'tcx> for LintExpectationChecker<'_, 'tcx> { + type Map = Map<'tcx>; + + fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap { + intravisit::NestedVisitorMap::All(self.tcx.hir()) + } + + fn visit_param(&mut self, param: &'tcx hir::Param<'tcx>) { + self.check_item_with_attrs(param.hir_id, param.span, |builder| { + intravisit::walk_param(builder, param); + }); + } + + fn visit_item(&mut self, it: &'tcx hir::Item<'tcx>) { + self.check_item_with_attrs(it.hir_id(), it.span, |builder| { + intravisit::walk_item(builder, it); + }); + } + + fn visit_foreign_item(&mut self, it: &'tcx hir::ForeignItem<'tcx>) { + self.check_item_with_attrs(it.hir_id(), it.span, |builder| { + intravisit::walk_foreign_item(builder, it); + }) + } + + fn visit_stmt(&mut self, e: &'tcx hir::Stmt<'tcx>) { + // We will call `with_lint_attrs` when we walk + // the `StmtKind`. The outer statement itself doesn't + // define the lint levels. + intravisit::walk_stmt(self, e); + } + + fn visit_expr(&mut self, e: &'tcx hir::Expr<'tcx>) { + self.check_item_with_attrs(e.hir_id, e.span, |builder| { + intravisit::walk_expr(builder, e); + }) + } + + fn visit_field_def(&mut self, s: &'tcx hir::FieldDef<'tcx>) { + self.check_item_with_attrs(s.hir_id, s.span, |builder| { + intravisit::walk_field_def(builder, s); + }) + } + + fn visit_variant( + &mut self, + v: &'tcx hir::Variant<'tcx>, + g: &'tcx hir::Generics<'tcx>, + item_id: hir::HirId, + ) { + self.check_item_with_attrs(v.id, v.span, |builder| { + intravisit::walk_variant(builder, v, g, item_id); + }) + } + + fn visit_local(&mut self, l: &'tcx hir::Local<'tcx>) { + self.check_item_with_attrs(l.hir_id, l.span, |builder| { + intravisit::walk_local(builder, l); + }) + } + + fn visit_arm(&mut self, a: &'tcx hir::Arm<'tcx>) { + self.check_item_with_attrs(a.hir_id, a.span, |builder| { + intravisit::walk_arm(builder, a); + }) + } + + fn visit_trait_item(&mut self, trait_item: &'tcx hir::TraitItem<'tcx>) { + self.check_item_with_attrs(trait_item.hir_id(), trait_item.span, |builder| { + intravisit::walk_trait_item(builder, trait_item); + }); + } + + fn visit_impl_item(&mut self, impl_item: &'tcx hir::ImplItem<'tcx>) { + self.check_item_with_attrs(impl_item.hir_id(), impl_item.span, |builder| { + intravisit::walk_impl_item(builder, impl_item); + }); + } +} + +pub fn provide(providers: &mut Providers) { + providers.check_expect_lint = check_expect_lint; +} diff --git a/compiler/rustc_lint/src/late.rs b/compiler/rustc_lint/src/late.rs index 052efa851f7cf..215eff3baef03 100644 --- a/compiler/rustc_lint/src/late.rs +++ b/compiler/rustc_lint/src/late.rs @@ -512,4 +512,13 @@ pub fn check_crate<'tcx, T: LateLintPass<'tcx>>( }); }, ); + + // This check has to be run after all lints are done processing for this crate + // + // This could most likely be optimized by checking the lint expectations on a module + // level instead, as rustc runs the analysis in `lint_mod` on this level. However this + // would require that the tracked lint emissions can be linked to a specific module in + // the diagnostic emission. This is not possible to my knowledge and would also not coder + // the lints that are validated on the crate level. + tcx.sess.time("check_expect_lint", || tcx.check_expect_lint(())); } diff --git a/compiler/rustc_lint/src/levels.rs b/compiler/rustc_lint/src/levels.rs index 4f223afcc27a0..e2985e955e5db 100644 --- a/compiler/rustc_lint/src/levels.rs +++ b/compiler/rustc_lint/src/levels.rs @@ -2,6 +2,7 @@ use crate::context::{CheckLintNameResult, LintStore}; use crate::late::unerased_lint_store; use rustc_ast as ast; use rustc_ast::unwrap_or; +use rustc_ast::NestedMetaItem; use rustc_ast_pretty::pprust; use rustc_data_structures::fx::FxHashMap; use rustc_errors::{struct_span_err, Applicability, DiagnosticBuilder}; @@ -247,44 +248,18 @@ impl<'s> LintLevelsBuilder<'s> { // Before processing the lint names, look for a reason (RFC 2383) // at the end. - let mut reason = None; let tail_li = &metas[metas.len() - 1]; - if let Some(item) = tail_li.meta_item() { - match item.kind { - ast::MetaItemKind::Word => {} // actual lint names handled later - ast::MetaItemKind::NameValue(ref name_value) => { - if item.path == sym::reason { - // FIXME (#55112): issue unused-attributes lint if we thereby - // don't have any lint names (`#[level(reason = "foo")]`) - if let ast::LitKind::Str(rationale, _) = name_value.kind { - if !self.sess.features_untracked().lint_reasons { - feature_err( - &self.sess.parse_sess, - sym::lint_reasons, - item.span, - "lint reasons are experimental", - ) - .emit(); - } - reason = Some(rationale); - } else { - bad_attr(name_value.span) - .span_label(name_value.span, "reason must be a string literal") - .emit(); - } - // found reason, reslice meta list to exclude it - metas.pop().unwrap(); - } else { - bad_attr(item.span) - .span_label(item.span, "bad attribute argument") - .emit(); - } - } - ast::MetaItemKind::List(_) => { - bad_attr(item.span).span_label(item.span, "bad attribute argument").emit(); - } + let reason = match try_parse_reason_metadata(tail_li, self.sess) { + ParseLintReasonResult::Ok(reason) => { + metas.pop().unwrap(); + Some(reason) } - } + ParseLintReasonResult::MalformedReason => { + metas.pop().unwrap(); + None + } + ParseLintReasonResult::NotFound => None, + }; for li in metas { let sp = li.span(); @@ -568,6 +543,57 @@ impl<'s> LintLevelsBuilder<'s> { } } +pub(crate) enum ParseLintReasonResult { + /// The reason was found and is returned as part of this value. + Ok(Symbol), + /// Indicates that the reason field was found but was malformed. + MalformedReason, + /// The checked item is not a reason field. + NotFound, +} + +pub(crate) fn try_parse_reason_metadata( + item: &NestedMetaItem, + sess: &Session, +) -> ParseLintReasonResult { + let bad_attr = |span| struct_span_err!(sess, span, E0452, "malformed lint attribute input"); + if let Some(item) = item.meta_item() { + match item.kind { + ast::MetaItemKind::Word => {} // actual lint names handled later + ast::MetaItemKind::NameValue(ref name_value) => { + if item.path == sym::reason { + // FIXME (#55112): issue unused-attributes lint if we thereby + // don't have any lint names (`#[level(reason = "foo")]`) + if let ast::LitKind::Str(rationale, _) = name_value.kind { + if !sess.features_untracked().lint_reasons { + feature_err( + &sess.parse_sess, + sym::lint_reasons, + item.span, + "lint reasons are experimental", + ) + .emit(); + } + return ParseLintReasonResult::Ok(rationale); + } else { + bad_attr(name_value.span) + .span_label(name_value.span, "reason must be a string literal") + .emit(); + return ParseLintReasonResult::MalformedReason; + } + } else { + bad_attr(item.span).span_label(item.span, "bad attribute argument").emit(); + } + } + ast::MetaItemKind::List(_) => { + bad_attr(item.span).span_label(item.span, "bad attribute argument").emit(); + } + } + } + + ParseLintReasonResult::NotFound +} + pub fn is_known_lint_tool(m_item: Symbol, sess: &Session, attrs: &[ast::Attribute]) -> bool { if [sym::clippy, sym::rustc, sym::rustdoc].contains(&m_item) { return true; diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs index 1786f1e70343a..312c77d2867c9 100644 --- a/compiler/rustc_lint/src/lib.rs +++ b/compiler/rustc_lint/src/lib.rs @@ -49,6 +49,7 @@ mod array_into_iter; pub mod builtin; mod context; mod early; +mod expect; mod internal; mod late; mod levels; @@ -99,6 +100,7 @@ pub use rustc_session::lint::{LintArray, LintPass}; pub fn provide(providers: &mut Providers) { levels::provide(providers); + expect::provide(providers); *providers = Providers { lint_mod, ..*providers }; } diff --git a/compiler/rustc_lint_defs/src/builtin.rs b/compiler/rustc_lint_defs/src/builtin.rs index 5b1cd0bcb3ffe..9421b1c50c370 100644 --- a/compiler/rustc_lint_defs/src/builtin.rs +++ b/compiler/rustc_lint_defs/src/builtin.rs @@ -456,6 +456,39 @@ declare_lint! { "unrecognized lint attribute" } +declare_lint! { + /// The `unfulfilled_lint_expectations` lint detects lint trigger expectations + /// that have not been fulfilled. + /// + /// ### Example + /// + /// ```rust + /// #![feature(lint_reasons)] + /// + /// #[expect(unused_variables)] + /// let x = 10; + /// println!("{}", x); + /// ``` + /// + /// {{produces}} + /// + /// ### Explanation + /// + /// It was expected that the marked code would emit a lint. This expectation + /// has not been fulfilled. + /// + /// The `expect` attribute can be removed if this is intended behavior otherwise + /// it should be investigated why the expected lint is no longer issued. + /// + /// Part of RFC 2383. The progress is being tracked in [#54503] + /// + /// [#54503]: https://github.com/rust-lang/rust/issues/54503 + pub UNFULFILLED_LINT_EXPECTATIONS, + Warn, + "unfulfilled lint expectation", + @feature_gate = rustc_span::sym::lint_reasons; +} + declare_lint! { /// The `unused_variables` lint detects variables which are not used in /// any way. @@ -2898,6 +2931,7 @@ declare_lint_pass! { UNUSED_CRATE_DEPENDENCIES, UNUSED_QUALIFICATIONS, UNKNOWN_LINTS, + UNFULFILLED_LINT_EXPECTATIONS, UNUSED_VARIABLES, UNUSED_ASSIGNMENTS, DEAD_CODE, diff --git a/compiler/rustc_lint_defs/src/lib.rs b/compiler/rustc_lint_defs/src/lib.rs index 6c38b8f5bc0a1..477a5835691c4 100644 --- a/compiler/rustc_lint_defs/src/lib.rs +++ b/compiler/rustc_lint_defs/src/lib.rs @@ -47,12 +47,31 @@ pub enum Applicability { } /// Setting for how to handle a lint. +/// +/// See: https://doc.rust-lang.org/rustc/lints/levels.html #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] pub enum Level { + /// The `allow` level will not issue any message. Allow, + /// The `expect` level will suppress the lint message but intern produce a message + /// if the lint wasn't issued in the expected scope. Expect should not be used as + /// an initial level for a lint. + /// + /// Note that this still means that the lint is enabled in this position and should + /// be passed onwards to the `LintContext` which will fulfill the expectation and + /// suppress the lint. + /// + /// See RFC 2383. + Expect, + /// The `warn` level will produce a warning if the lint was violated, however the + /// compiler will continue with its execution. Warn, ForceWarn, + /// The `deny` level will produce an error and stop further execution after the lint + /// pass is complete. Deny, + /// `Forbid` is equivalent to the `deny` level but can't be overwritten like the previous + /// levels. Forbid, } @@ -63,6 +82,7 @@ impl Level { pub fn as_str(self) -> &'static str { match self { Level::Allow => "allow", + Level::Expect => "expect", Level::Warn => "warn", Level::ForceWarn => "force-warn", Level::Deny => "deny", @@ -74,6 +94,7 @@ impl Level { pub fn from_str(x: &str) -> Option { match x { "allow" => Some(Level::Allow), + "expect" => Some(Level::Expect), "warn" => Some(Level::Warn), "deny" => Some(Level::Deny), "forbid" => Some(Level::Forbid), @@ -85,6 +106,7 @@ impl Level { pub fn from_symbol(x: Symbol) -> Option { match x { sym::allow => Some(Level::Allow), + sym::expect => Some(Level::Expect), sym::warn => Some(Level::Warn), sym::deny => Some(Level::Deny), sym::forbid => Some(Level::Forbid), diff --git a/compiler/rustc_middle/src/lint.rs b/compiler/rustc_middle/src/lint.rs index 6ad68877235dc..2e5ed6b1eb2ff 100644 --- a/compiler/rustc_middle/src/lint.rs +++ b/compiler/rustc_middle/src/lint.rs @@ -243,6 +243,16 @@ pub fn struct_lint_level<'s, 'd>( return; } } + (Level::Expect, _) => { + // This case is special as we actually allow the lint itself in this context, but + // we can't return early like in the case for `Level::Allow` because we still + // need the lint diagnostic to be emitted to `rustc_error::HanderInner`. + // + // We can also not save the diagnostic here right away as it could for instance + // still be cancelled in the decorate closure. All of this means that we simply + // create a `DiagnosticBuilder` and continue as we would for warnings. + sess.struct_expect("") + } (Level::Warn, Some(span)) => sess.struct_span_warn(span, ""), (Level::Warn, None) => sess.struct_warn(""), (Level::ForceWarn, Some(span)) => sess.struct_span_force_warn(span, ""), @@ -274,6 +284,17 @@ pub fn struct_lint_level<'s, 'd>( } let name = lint.name_lower(); + + // Lint diagnostics that are covered by the expect level will not be emitted outside + // the compiler. It is therefore not necessary to add any information for the user. + // This will therefore directly call the decorate function which will intern emit + // the `Diagnostic`. + if level == Level::Expect { + err.code(DiagnosticId::Lint { name, has_future_breakage, is_force_warn: false }); + decorate(LintDiagnosticBuilder::new(err)); + return; + } + match src { LintLevelSource::Default => { sess.diag_note_once( @@ -289,6 +310,9 @@ pub fn struct_lint_level<'s, 'd>( Level::Forbid => "-F", Level::Allow => "-A", Level::ForceWarn => "--force-warn", + Level::Expect => { + unreachable!("lints with the level of `expect` should not run this code"); + } }; let hyphen_case_lint_name = name.replace("_", "-"); if lint_flag_val.as_str() == name { diff --git a/compiler/rustc_middle/src/query/mod.rs b/compiler/rustc_middle/src/query/mod.rs index 2de836c058cf1..51563a4f497ae 100644 --- a/compiler/rustc_middle/src/query/mod.rs +++ b/compiler/rustc_middle/src/query/mod.rs @@ -656,6 +656,10 @@ rustc_queries! { desc { |tcx| "linting {}", describe_as_module(key, tcx) } } + query check_expect_lint(_: ()) -> () { + desc { "checking lint expectations"} + } + /// Checks the attributes in the module. query check_mod_attrs(key: LocalDefId) -> () { desc { |tcx| "checking attributes in {}", describe_as_module(key, tcx) } diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index 369af437c4384..f7e877f80a5fc 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -360,6 +360,9 @@ impl Session { pub fn struct_allow(&self, msg: &str) -> DiagnosticBuilder<'_> { self.diagnostic().struct_allow(msg) } + pub fn struct_expect(&self, msg: &str) -> DiagnosticBuilder<'_> { + self.diagnostic().struct_expect(msg) + } pub fn struct_span_err>(&self, sp: S, msg: &str) -> DiagnosticBuilder<'_> { self.diagnostic().struct_span_err(sp, msg) } diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect-consumes-all-warnings.rs b/src/test/ui/lint/rfc-2383-lint-reason/expect-consumes-all-warnings.rs new file mode 100644 index 0000000000000..b7abcbc9f4cfc --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect-consumes-all-warnings.rs @@ -0,0 +1,21 @@ +// check-pass + +#![feature(lint_reasons)] + +#[expect(unused_variables, reason = "All emissions should be consumed by the nested expect")] +//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations] +//~| NOTE #[warn(unfulfilled_lint_expectations)]` on by default +//~| NOTE All emissions should be consumed by the nested expect +mod oof { + #[expect(unused_variables, reason = "This should collect all unused variable emissions")] + fn bar() { + let mut c = 0; + let mut l = 0; + let mut l = 0; + let mut i = 0; + let mut p = 0; + let mut y = 0; + } +} + +fn main() {} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect-consumes-all-warnings.stderr b/src/test/ui/lint/rfc-2383-lint-reason/expect-consumes-all-warnings.stderr new file mode 100644 index 0000000000000..b29687403b1ef --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect-consumes-all-warnings.stderr @@ -0,0 +1,11 @@ +warning: this lint expectation is unfulfilled + --> $DIR/expect-consumes-all-warnings.rs:5:1 + | +LL | #[expect(unused_variables, reason = "All emissions should be consumed by the nested expect")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unfulfilled_lint_expectations)]` on by default + = note: All emissions should be consumed by the nested expect + +warning: 1 warning emitted + diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_as_crate_attribute.rs b/src/test/ui/lint/rfc-2383-lint-reason/expect_as_crate_attribute.rs new file mode 100644 index 0000000000000..74c1cd9ad568a --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_as_crate_attribute.rs @@ -0,0 +1,8 @@ +// check-pass + +#![feature(lint_reasons)] + +#![expect(unused_mut)] +//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations] + +fn main() {} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_as_crate_attribute.stderr b/src/test/ui/lint/rfc-2383-lint-reason/expect_as_crate_attribute.stderr new file mode 100644 index 0000000000000..5c480e501af35 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_as_crate_attribute.stderr @@ -0,0 +1,10 @@ +warning: this lint expectation is unfulfilled + --> $DIR/expect_as_crate_attribute.rs:5:1 + | +LL | #![expect(unused_mut)] + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unfulfilled_lint_expectations)]` on by default + +warning: 1 warning emitted + diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_enabled_feature_gate.rs b/src/test/ui/lint/rfc-2383-lint-reason/expect_enabled_feature_gate.rs new file mode 100644 index 0000000000000..d9f7f525f7221 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_enabled_feature_gate.rs @@ -0,0 +1,9 @@ +// check-pass + +#![feature(lint_reasons)] + +// should be fine due to the enabled feature gate +#[expect(unused_variables)] +fn main() { + let x = 2; +} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_missing_feature_gate.rs b/src/test/ui/lint/rfc-2383-lint-reason/expect_missing_feature_gate.rs new file mode 100644 index 0000000000000..7f9fe9cc1ec86 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_missing_feature_gate.rs @@ -0,0 +1,7 @@ +// should error due to missing feature gate. + +#[expect(unused)] +//~^ ERROR: the `#[expect]` attribute is an experimental feature [E0658] +fn main() { + let x = 1; +} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_missing_feature_gate.stderr b/src/test/ui/lint/rfc-2383-lint-reason/expect_missing_feature_gate.stderr new file mode 100644 index 0000000000000..234bd2c995cc2 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_missing_feature_gate.stderr @@ -0,0 +1,12 @@ +error[E0658]: the `#[expect]` attribute is an experimental feature + --> $DIR/expect_missing_feature_gate.rs:3:1 + | +LL | #[expect(unused)] + | ^^^^^^^^^^^^^^^^^ + | + = note: see issue #54503 for more information + = help: add `#![feature(lint_reasons)]` to the crate attributes to enable + +error: aborting due to previous error + +For more information about this error, try `rustc --explain E0658`. diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_reason.rs b/src/test/ui/lint/rfc-2383-lint-reason/expect_reason.rs new file mode 100644 index 0000000000000..456fda9953708 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_reason.rs @@ -0,0 +1,10 @@ +// check-pass + +#![feature(lint_reasons)] + +#![expect(unused_variables, reason = "")] +//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations] +//~| NOTE `#[warn(unfulfilled_lint_expectations)]` on by default +//~| NOTE + +fn main() {} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_reason.stderr b/src/test/ui/lint/rfc-2383-lint-reason/expect_reason.stderr new file mode 100644 index 0000000000000..00da2aa381b1f --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_reason.stderr @@ -0,0 +1,11 @@ +warning: this lint expectation is unfulfilled + --> $DIR/expect_reason.rs:5:1 + | +LL | #![expect(unused_variables, reason = "")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unfulfilled_lint_expectations)]` on by default + = note: + +warning: 1 warning emitted + diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_with_nested_levels.rs b/src/test/ui/lint/rfc-2383-lint-reason/expect_with_nested_levels.rs new file mode 100644 index 0000000000000..98523d1e59f2d --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_with_nested_levels.rs @@ -0,0 +1,29 @@ +// check-pass + +#![feature(lint_reasons)] + +#[expect(unused_mut, reason = "This should trigger because `unused_mut` was allow")] +//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations] +//~| NOTE `#[warn(unfulfilled_lint_expectations)]` on by default +//~| NOTE This should trigger because `unused_mut` was allow +mod foo { + fn bar() { + #[allow(unused_mut, reason = "v is unused")] + let mut v = 0; + } +} + +#[expect(unused_mut, reason = "This should trigger because `unused_mut` is no longer expected")] +//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations] +//~| NOTE This should trigger because `unused_mut` is no longer expected +mod oof { + #[warn(unused_mut, reason = "We no longer expect it in this scope")] + //~^ NOTE the lint level is defined here + fn bar() { + let mut v = 0; + //~^ WARNING variable does not need to be mutable [unused_mut] + //~| NOTE We no longer expect it in this scope + } +} + +fn main() {} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expect_with_nested_levels.stderr b/src/test/ui/lint/rfc-2383-lint-reason/expect_with_nested_levels.stderr new file mode 100644 index 0000000000000..d9d4e12d7ed7c --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expect_with_nested_levels.stderr @@ -0,0 +1,34 @@ +warning: variable does not need to be mutable + --> $DIR/expect_with_nested_levels.rs:23:13 + | +LL | let mut v = 0; + | ----^ + | | + | help: remove this `mut` + | + = note: We no longer expect it in this scope +note: the lint level is defined here + --> $DIR/expect_with_nested_levels.rs:20:12 + | +LL | #[warn(unused_mut, reason = "We no longer expect it in this scope")] + | ^^^^^^^^^^ + +warning: this lint expectation is unfulfilled + --> $DIR/expect_with_nested_levels.rs:5:1 + | +LL | #[expect(unused_mut, reason = "This should trigger because `unused_mut` was allow")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unfulfilled_lint_expectations)]` on by default + = note: This should trigger because `unused_mut` was allow + +warning: this lint expectation is unfulfilled + --> $DIR/expect_with_nested_levels.rs:16:1 + | +LL | #[expect(unused_mut, reason = "This should trigger because `unused_mut` is no longer expected")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: This should trigger because `unused_mut` is no longer expected + +warning: 3 warnings emitted + diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expecting_missed_expectations.rs b/src/test/ui/lint/rfc-2383-lint-reason/expecting_missed_expectations.rs new file mode 100644 index 0000000000000..c50c08f15459e --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expecting_missed_expectations.rs @@ -0,0 +1,21 @@ +// check-pass + +#![feature(lint_reasons)] + +#[expect(unfulfilled_lint_expectations, reason = "this should catch the nested expect")] +mod foo { + #[expect(unfulfilled_lint_expectations, reason = "issuing a lint and getting caught above")] + fn bar() { + #[expect(unused_mut, reason = "v is unused")] + let mut v = 0; + } +} + +// make sure it doesn't catch itself +#[expect(unfulfilled_lint_expectations, reason = "this should issue a warning")] +//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations] +//~| NOTE `#[warn(unfulfilled_lint_expectations)]` on by default +//~| NOTE this should issue a warning +mod oof {} + +fn main() {} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/expecting_missed_expectations.stderr b/src/test/ui/lint/rfc-2383-lint-reason/expecting_missed_expectations.stderr new file mode 100644 index 0000000000000..eb2bbb9a7afa4 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/expecting_missed_expectations.stderr @@ -0,0 +1,11 @@ +warning: this lint expectation is unfulfilled + --> $DIR/expecting_missed_expectations.rs:15:1 + | +LL | #[expect(unfulfilled_lint_expectations, reason = "this should issue a warning")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unfulfilled_lint_expectations)]` on by default + = note: this should issue a warning + +warning: 1 warning emitted + diff --git a/src/test/ui/lint/rfc-2383-lint-reason/invalid_expect_attribute.rs b/src/test/ui/lint/rfc-2383-lint-reason/invalid_expect_attribute.rs new file mode 100644 index 0000000000000..d03f9c2e77429 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/invalid_expect_attribute.rs @@ -0,0 +1,9 @@ +#![feature(lint_reasons)] + +#![expect(reason = "This should trigger because `unused_mut` was allow", unused_mut)] +//~^ ERROR malformed lint attribute +//~| ERROR malformed lint attribute +//~| NOTE reason in lint attribute must come last +//~| NOTE reason in lint attribute must come last + +fn main() {} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/invalid_expect_attribute.stderr b/src/test/ui/lint/rfc-2383-lint-reason/invalid_expect_attribute.stderr new file mode 100644 index 0000000000000..33e9239f804cf --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/invalid_expect_attribute.stderr @@ -0,0 +1,15 @@ +error[E0452]: malformed lint attribute input + --> $DIR/invalid_expect_attribute.rs:3:11 + | +LL | #![expect(reason = "This should trigger because `unused_mut` was allow", unused_mut)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reason in lint attribute must come last + +error[E0452]: malformed lint attribute input + --> $DIR/invalid_expect_attribute.rs:3:11 + | +LL | #![expect(reason = "This should trigger because `unused_mut` was allow", unused_mut)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reason in lint attribute must come last + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0452`. diff --git a/src/test/ui/lint/rfc-2383-lint-reason/multiple_expected_lints.rs b/src/test/ui/lint/rfc-2383-lint-reason/multiple_expected_lints.rs new file mode 100644 index 0000000000000..52efc58dcd8bd --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/multiple_expected_lints.rs @@ -0,0 +1,8 @@ +// check-pass + +#![feature(lint_reasons)] + +#![expect(unconditional_panic, unused, reason = "Don't trigger because `unused` was triggered")] +fn main() { + let x = 0; +} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/tool_lint_in_expect.rs b/src/test/ui/lint/rfc-2383-lint-reason/tool_lint_in_expect.rs new file mode 100644 index 0000000000000..2a326322cc5f5 --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/tool_lint_in_expect.rs @@ -0,0 +1,10 @@ +// check-pass + +#![feature(lint_reasons)] + +#![expect( + clippy::almost_swapped, + reason = "This should be ignored in a normal run but trigger in a clippy run")] +fn main() { + // See lint doc https://rust-lang.github.io/rust-clippy/master/index.html#almost_swapped +} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/unknown_lint_in_expect.rs b/src/test/ui/lint/rfc-2383-lint-reason/unknown_lint_in_expect.rs new file mode 100644 index 0000000000000..20d28160a327a --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/unknown_lint_in_expect.rs @@ -0,0 +1,7 @@ +// check-pass + +#![feature(lint_reasons)] + +#![expect(this_lint_does_not_exist)] +//~^ WARNING unknown lint: `this_lint_does_not_exist` [unknown_lints] +fn main() {} diff --git a/src/test/ui/lint/rfc-2383-lint-reason/unknown_lint_in_expect.stderr b/src/test/ui/lint/rfc-2383-lint-reason/unknown_lint_in_expect.stderr new file mode 100644 index 0000000000000..45eee1ff343dc --- /dev/null +++ b/src/test/ui/lint/rfc-2383-lint-reason/unknown_lint_in_expect.stderr @@ -0,0 +1,10 @@ +warning: unknown lint: `this_lint_does_not_exist` + --> $DIR/unknown_lint_in_expect.rs:5:11 + | +LL | #![expect(this_lint_does_not_exist)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unknown_lints)]` on by default + +warning: 1 warning emitted +