From dfee0dad260cf22efd0d56e0b07021d0fc86a564 Mon Sep 17 00:00:00 2001 From: Sasha Pourcelot Date: Fri, 5 Dec 2025 18:06:42 +0000 Subject: [PATCH 1/2] Add test for misspelled feature name --- tests/ui/feature-gates/unknown-feature.rs | 13 +++++++++++- tests/ui/feature-gates/unknown-feature.stderr | 20 +++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/ui/feature-gates/unknown-feature.rs b/tests/ui/feature-gates/unknown-feature.rs index 20fd932d4c2f6..2ecc9b82a48d5 100644 --- a/tests/ui/feature-gates/unknown-feature.rs +++ b/tests/ui/feature-gates/unknown-feature.rs @@ -1,3 +1,14 @@ -#![feature(unknown_rust_feature)] //~ ERROR unknown feature +#![feature( + unknown_rust_feature, + //~^ ERROR unknown feature + + // Typo for lang feature + associated_types_default, + //~^ ERROR unknown feature + + // Typo for lib feature + core_intrnisics, + //~^ ERROR unknown feature +)] fn main() {} diff --git a/tests/ui/feature-gates/unknown-feature.stderr b/tests/ui/feature-gates/unknown-feature.stderr index 0a731ddcc0b62..a0cadb3f817fb 100644 --- a/tests/ui/feature-gates/unknown-feature.stderr +++ b/tests/ui/feature-gates/unknown-feature.stderr @@ -1,9 +1,21 @@ error[E0635]: unknown feature `unknown_rust_feature` - --> $DIR/unknown-feature.rs:1:12 + --> $DIR/unknown-feature.rs:2:5 | -LL | #![feature(unknown_rust_feature)] - | ^^^^^^^^^^^^^^^^^^^^ +LL | unknown_rust_feature, + | ^^^^^^^^^^^^^^^^^^^^ -error: aborting due to 1 previous error +error[E0635]: unknown feature `associated_types_default` + --> $DIR/unknown-feature.rs:6:5 + | +LL | associated_types_default, + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0635]: unknown feature `core_intrnisics` + --> $DIR/unknown-feature.rs:10:5 + | +LL | core_intrnisics, + | ^^^^^^^^^^^^^^^ + +error: aborting due to 3 previous errors For more information about this error, try `rustc --explain E0635`. From a57470ff727f5b527504d85b5eddfb2415ed9522 Mon Sep 17 00:00:00 2001 From: Sasha Pourcelot Date: Thu, 4 Dec 2025 14:33:47 +0000 Subject: [PATCH 2/2] Look for typos when reporting an unknown nightly feature --- .../rustc_parse/src/parser/diagnostics.rs | 24 +++++++-------- compiler/rustc_passes/messages.ftl | 2 ++ compiler/rustc_passes/src/errors.rs | 15 ++++++++++ compiler/rustc_passes/src/stability.rs | 30 +++++++++++++++---- compiler/rustc_span/src/symbol.rs | 22 ++++++++++++++ tests/ui/feature-gates/unknown-feature.rs | 4 ++- tests/ui/feature-gates/unknown-feature.stderr | 14 ++++++++- 7 files changed, 89 insertions(+), 22 deletions(-) diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index bd495f6ec1acb..d4667a09af6d9 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -16,7 +16,6 @@ use rustc_errors::{ pluralize, }; use rustc_session::errors::ExprParenthesesNeeded; -use rustc_span::edit_distance::find_best_match_for_name; use rustc_span::source_map::Spanned; use rustc_span::symbol::used_keywords; use rustc_span::{BytePos, DUMMY_SP, Ident, Span, SpanSnippetError, Symbol, kw, sym}; @@ -222,6 +221,8 @@ impl std::fmt::Display for UnaryFixity { style = "verbose" )] struct MisspelledKw { + // We use a String here because `Symbol::into_diag_arg` calls `Symbol::to_ident_string`, which + // prefix the keyword with a `r#` because it aims to print the symbol as an identifier. similar_kw: String, #[primary_span] span: Span, @@ -229,20 +230,15 @@ struct MisspelledKw { } /// Checks if the given `lookup` identifier is similar to any keyword symbol in `candidates`. +/// +/// This is a specialized version of [`Symbol::find_similar`] that constructs an error when a +/// candidate is found. fn find_similar_kw(lookup: Ident, candidates: &[Symbol]) -> Option { - let lowercase = lookup.name.as_str().to_lowercase(); - let lowercase_sym = Symbol::intern(&lowercase); - if candidates.contains(&lowercase_sym) { - Some(MisspelledKw { similar_kw: lowercase, span: lookup.span, is_incorrect_case: true }) - } else if let Some(similar_sym) = find_best_match_for_name(candidates, lookup.name, None) { - Some(MisspelledKw { - similar_kw: similar_sym.to_string(), - span: lookup.span, - is_incorrect_case: false, - }) - } else { - None - } + lookup.name.find_similar(candidates).map(|(similar_kw, is_incorrect_case)| MisspelledKw { + similar_kw: similar_kw.to_string(), + is_incorrect_case, + span: lookup.span, + }) } struct MultiSugg { diff --git a/compiler/rustc_passes/messages.ftl b/compiler/rustc_passes/messages.ftl index 09ac3ae1c3a92..70e91c081776a 100644 --- a/compiler/rustc_passes/messages.ftl +++ b/compiler/rustc_passes/messages.ftl @@ -421,6 +421,8 @@ passes_missing_panic_handler = passes_missing_stability_attr = {$descr} has missing stability attribute +passes_misspelled_feature = there is a feature with a similar name: `{$actual_name}` + passes_mixed_export_name_and_no_mangle = `{$no_mangle_attr}` attribute may not be used in combination with `{$export_name_attr}` .label = `{$no_mangle_attr}` is ignored .note = `{$export_name_attr}` takes precedence diff --git a/compiler/rustc_passes/src/errors.rs b/compiler/rustc_passes/src/errors.rs index e4826cccd32f8..6aa0f5212af70 100644 --- a/compiler/rustc_passes/src/errors.rs +++ b/compiler/rustc_passes/src/errors.rs @@ -1183,6 +1183,21 @@ pub(crate) struct UnknownFeature { #[primary_span] pub span: Span, pub feature: Symbol, + #[subdiagnostic] + pub suggestion: Option, +} + +#[derive(Subdiagnostic)] +#[suggestion( + passes_misspelled_feature, + style = "verbose", + code = "{actual_name}", + applicability = "maybe-incorrect" +)] +pub(crate) struct MisspelledFeature { + #[primary_span] + pub span: Span, + pub actual_name: Symbol, } #[derive(Diagnostic)] diff --git a/compiler/rustc_passes/src/stability.rs b/compiler/rustc_passes/src/stability.rs index 39830db2b11db..9d94c4cc62256 100644 --- a/compiler/rustc_passes/src/stability.rs +++ b/compiler/rustc_passes/src/stability.rs @@ -6,7 +6,7 @@ use std::num::NonZero; use rustc_ast_lowering::stability::extern_abi_stability; use rustc_data_structures::fx::FxIndexMap; use rustc_data_structures::unord::{ExtendUnord, UnordMap, UnordSet}; -use rustc_feature::{EnabledLangFeature, EnabledLibFeature}; +use rustc_feature::{EnabledLangFeature, EnabledLibFeature, UNSTABLE_LANG_FEATURES}; use rustc_hir::attrs::{AttributeKind, DeprecatedSince}; use rustc_hir::def::{DefKind, Res}; use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE, LocalDefId, LocalModDefId}; @@ -1062,11 +1062,13 @@ pub fn check_unused_or_stable_features(tcx: TyCtxt<'_>) { // no unknown features, because the collection also does feature attribute validation. let local_defined_features = tcx.lib_features(LOCAL_CRATE); if !remaining_lib_features.is_empty() || !remaining_implications.is_empty() { + let crates = tcx.crates(()); + // Loading the implications of all crates is unavoidable to be able to emit the partial // stabilization diagnostic, but it can be avoided when there are no // `remaining_lib_features`. let mut all_implications = remaining_implications.clone(); - for &cnum in tcx.crates(()) { + for &cnum in crates { all_implications .extend_unord(tcx.stability_implications(cnum).items().map(|(k, v)| (*k, *v))); } @@ -1079,7 +1081,7 @@ pub fn check_unused_or_stable_features(tcx: TyCtxt<'_>) { &all_implications, ); - for &cnum in tcx.crates(()) { + for &cnum in crates { if remaining_lib_features.is_empty() && remaining_implications.is_empty() { break; } @@ -1091,10 +1093,26 @@ pub fn check_unused_or_stable_features(tcx: TyCtxt<'_>) { &all_implications, ); } - } - for (feature, span) in remaining_lib_features { - tcx.dcx().emit_err(errors::UnknownFeature { span, feature }); + if !remaining_lib_features.is_empty() { + let lang_features = + UNSTABLE_LANG_FEATURES.iter().map(|feature| feature.name).collect::>(); + let lib_features = crates + .into_iter() + .flat_map(|&cnum| { + tcx.lib_features(cnum).stability.keys().copied().into_sorted_stable_ord() + }) + .collect::>(); + + let valid_feature_names = [lang_features, lib_features].concat(); + + for (feature, span) in remaining_lib_features { + let suggestion = feature + .find_similar(&valid_feature_names) + .map(|(actual_name, _)| errors::MisspelledFeature { span, actual_name }); + tcx.dcx().emit_err(errors::UnknownFeature { span, feature, suggestion }); + } + } } for (&implied_by, &feature) in remaining_implications.to_sorted_stable_ord() { diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 7e513160de0c3..171539b42a6e2 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -14,6 +14,7 @@ use rustc_data_structures::stable_hasher::{ use rustc_data_structures::sync::Lock; use rustc_macros::{Decodable, Encodable, HashStable_Generic, symbols}; +use crate::edit_distance::find_best_match_for_name; use crate::{DUMMY_SP, Edition, Span, with_session_globals}; #[cfg(test)] @@ -2843,6 +2844,27 @@ impl Symbol { // Avoid creating an empty identifier, because that asserts in debug builds. if self == sym::empty { String::new() } else { Ident::with_dummy_span(self).to_string() } } + + /// Checks if `self` is similar to any symbol in `candidates`. + /// + /// The returned boolean represents whether the candidate is the same symbol with a different + /// casing. + /// + /// All the candidates are assumed to be lowercase. + pub fn find_similar( + self, + candidates: &[Symbol], + ) -> Option<(Symbol, /* is incorrect case */ bool)> { + let lowercase = self.as_str().to_lowercase(); + let lowercase_sym = Symbol::intern(&lowercase); + if candidates.contains(&lowercase_sym) { + Some((lowercase_sym, true)) + } else if let Some(similar_sym) = find_best_match_for_name(candidates, self, None) { + Some((similar_sym, false)) + } else { + None + } + } } impl fmt::Debug for Symbol { diff --git a/tests/ui/feature-gates/unknown-feature.rs b/tests/ui/feature-gates/unknown-feature.rs index 2ecc9b82a48d5..a9e8e046eb168 100644 --- a/tests/ui/feature-gates/unknown-feature.rs +++ b/tests/ui/feature-gates/unknown-feature.rs @@ -1,14 +1,16 @@ #![feature( unknown_rust_feature, //~^ ERROR unknown feature - + // Typo for lang feature associated_types_default, //~^ ERROR unknown feature + //~| HELP there is a feature with a similar name // Typo for lib feature core_intrnisics, //~^ ERROR unknown feature + //~| HELP there is a feature with a similar name )] fn main() {} diff --git a/tests/ui/feature-gates/unknown-feature.stderr b/tests/ui/feature-gates/unknown-feature.stderr index a0cadb3f817fb..1e5b953e99cab 100644 --- a/tests/ui/feature-gates/unknown-feature.stderr +++ b/tests/ui/feature-gates/unknown-feature.stderr @@ -9,12 +9,24 @@ error[E0635]: unknown feature `associated_types_default` | LL | associated_types_default, | ^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: there is a feature with a similar name: `associated_type_defaults` + | +LL - associated_types_default, +LL + associated_type_defaults, + | error[E0635]: unknown feature `core_intrnisics` - --> $DIR/unknown-feature.rs:10:5 + --> $DIR/unknown-feature.rs:11:5 | LL | core_intrnisics, | ^^^^^^^^^^^^^^^ + | +help: there is a feature with a similar name: `core_intrinsics` + | +LL - core_intrnisics, +LL + core_intrinsics, + | error: aborting due to 3 previous errors