| use rustc_session::Session; | |
| use rustc_session::config::ExpectedValues; | |
| use rustc_span::def_id::LOCAL_CRATE; | |
| use rustc_span::edit_distance::find_best_match_for_name; | |
| use rustc_span::{ExpnKind, Ident, Span, Symbol, sym}; | |
| use crate::diagnostics; | |
| const MAX_CHECK_CFG_NAMES_OR_VALUES: usize = 35; | |
| enum FilterWellKnownNames { | |
| Yes, | |
| No, | |
| } | |
| fn sort_and_truncate_possibilities( | |
| sess: &Session, | |
| mut possibilities: Vec<Symbol>, | |
| filter_well_known_names: FilterWellKnownNames, | |
| ) -> (Vec<Symbol>, usize) { | |
| let possibilities_len = possibilities.len(); | |
| let n_possibilities = if sess.opts.unstable_opts.check_cfg_all_expected { | |
| possibilities.len() | |
| } else { | |
| match filter_well_known_names { | |
| FilterWellKnownNames::Yes => { | |
| possibilities | |
| .retain(|cfg_name| !sess.check_config.well_known_names.contains(cfg_name)); | |
| } | |
| FilterWellKnownNames::No => {} | |
| }; | |
| std::cmp::min(possibilities.len(), MAX_CHECK_CFG_NAMES_OR_VALUES) | |
| }; | |
| possibilities.sort_by(|s1, s2| s1.as_str().cmp(s2.as_str())); | |
| let and_more = possibilities_len.saturating_sub(n_possibilities); | |
| possibilities.truncate(n_possibilities); | |
| (possibilities, and_more) | |
| } | |
| enum EscapeQuotes { | |
| Yes, | |
| No, | |
| } | |
| fn to_check_cfg_arg(name: Ident, value: Option<Symbol>, quotes: EscapeQuotes) -> String { | |
| if let Some(value) = value { | |
| let value = str::escape_debug(value.as_str()).to_string(); | |
| let values = match quotes { | |
| EscapeQuotes::Yes => format!("\\\"{}\\\"", value.replace('"', "\\\\\\\\\"")), | |
| EscapeQuotes::No => format!("\"{value}\""), | |
| }; | |
| format!("cfg({name}, values({values}))") | |
| } else { | |
| format!("cfg({name})") | |
| } | |
| } | |
| fn cargo_help_sub( | |
| sess: &Session, | |
| inst: &impl Fn(EscapeQuotes) -> String, | |
| ) -> diagnostics::UnexpectedCfgCargoHelp { | |
| // We don't want to suggest the `build.rs` way to expected cfgs if we are already in a | |
| // `build.rs`. We therefor do a best effort check (looking if the `--crate-name` is | |
| // `build_script_build`) to try to figure out if we are building a Cargo build script | |
| let unescaped = &inst(EscapeQuotes::No); | |
| if let Some("build_script_build") = sess.opts.crate_name.as_deref() { | |
| diagnostics::UnexpectedCfgCargoHelp::lint_cfg(unescaped) | |
| } else { | |
| diagnostics::UnexpectedCfgCargoHelp::lint_cfg_and_build_rs( | |
| unescaped, | |
| &inst(EscapeQuotes::Yes), | |
| ) | |
| } | |
| } | |
| fn rustc_macro_help(span: Span) -> Option<diagnostics::UnexpectedCfgRustcMacroHelp> { | |
| let oexpn = span.ctxt().outer_expn_data(); | |
| if let Some(def_id) = oexpn.macro_def_id | |
| && let ExpnKind::Macro(macro_kind, macro_name) = oexpn.kind | |
| && def_id.krate != LOCAL_CRATE | |
| { | |
| Some(diagnostics::UnexpectedCfgRustcMacroHelp { | |
| macro_kind: macro_kind.descr(), | |
| macro_name, | |
| }) | |
| } else { | |
| None | |
| } | |
| } | |
| fn cargo_macro_help(span: Span) -> Option<diagnostics::UnexpectedCfgCargoMacroHelp> { | |
| let oexpn = span.ctxt().outer_expn_data(); | |
| if let Some(def_id) = oexpn.macro_def_id | |
| && def_id.krate != LOCAL_CRATE | |
| && let ExpnKind::Macro(macro_kind, macro_name) = oexpn.kind | |
| { | |
| Some(diagnostics::UnexpectedCfgCargoMacroHelp { | |
| macro_kind: macro_kind.descr(), | |
| macro_name, | |
| }) | |
| } else { | |
| None | |
| } | |
| } | |
| pub(crate) fn unexpected_cfg_name( | |
| sess: &Session, | |
| (name, name_span): (Symbol, Span), | |
| value: Option<(Symbol, Span)>, | |
| ) -> diagnostics::UnexpectedCfgName { | |
| #[allow(rustc::potential_query_instability)] | |
| let possibilities: Vec<Symbol> = sess.check_config.expecteds.keys().copied().collect(); | |
| let mut names_possibilities: Vec<_> = if value.is_none() { | |
| // We later sort and display all the possibilities, so the order here does not matter. | |
| #[allow(rustc::potential_query_instability)] | |
| sess.check_config | |
| .expecteds | |
| .iter() | |
| .filter_map(|(k, v)| match v { | |
| ExpectedValues::Some(v) if v.contains(&Some(name)) => Some(k), | |
| _ => None, | |
| }) | |
| .collect() | |
| } else { | |
| Vec::new() | |
| }; | |
| let is_from_cargo = rustc_session::utils::was_invoked_from_cargo(); | |
| let is_from_external_macro = name_span.in_external_macro(sess.source_map()); | |
| let mut is_feature_cfg = name == sym::feature; | |
| fn miscapitalized_boolean(name: Symbol) -> Option<bool> { | |
| if name.as_str().eq_ignore_ascii_case("false") { | |
| Some(false) | |
| } else if name.as_str().eq_ignore_ascii_case("true") { | |
| Some(true) | |
| } else { | |
| None | |
| } | |
| } | |
| let code_sugg = if is_feature_cfg && is_from_cargo { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::DefineFeatures | |
| // Suggest correct `version("..")` predicate syntax | |
| } else if let Some((_value, value_span)) = value | |
| && name == sym::version | |
| { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::VersionSyntax { | |
| between_name_and_value: name_span.between(value_span), | |
| after_value: value_span.shrink_to_hi(), | |
| } | |
| // Suggest a literal `false` instead | |
| // Detect miscapitalized `False`/`FALSE` etc, ensuring that this isn't `r#false` | |
| } else if value.is_none() | |
| // If this is a miscapitalized False/FALSE, suggest the boolean literal instead | |
| && let Some(boolean) = miscapitalized_boolean(name) | |
| // Check this isn't a raw identifier | |
| && sess | |
| .source_map() | |
| .span_to_snippet(name_span) | |
| .map_or(true, |snippet| !snippet.contains("r#")) | |
| { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::BooleanLiteral { | |
| span: name_span, | |
| literal: boolean, | |
| } | |
| // Suggest the most probable if we found one | |
| } else if let Some(best_match) = find_best_match_for_name(&possibilities, name, None) { | |
| is_feature_cfg |= best_match == sym::feature; | |
| if let Some(ExpectedValues::Some(best_match_values)) = | |
| sess.check_config.expecteds.get(&best_match) | |
| { | |
| // We will soon sort, so the initial order does not matter. | |
| #[allow(rustc::potential_query_instability)] | |
| let mut possibilities = best_match_values.iter().flatten().collect::<Vec<_>>(); | |
| possibilities.sort_by_key(|s| s.as_str()); | |
| let get_possibilities_sub = || { | |
| if possibilities.is_empty() { | |
| None | |
| } else { | |
| let possibilities = | |
| possibilities.iter().copied().copied().collect::<Vec<_>>().into(); | |
| Some(diagnostics::unexpected_cfg_name::ExpectedValues { | |
| best_match, | |
| possibilities, | |
| }) | |
| } | |
| }; | |
| let best_match = Ident::new(best_match, name_span); | |
| if let Some((value, value_span)) = value { | |
| if best_match_values.contains(&Some(value)) { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::SimilarNameAndValue { | |
| span: name_span, | |
| code: best_match.to_string(), | |
| } | |
| } else if best_match_values.contains(&None) { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::SimilarNameNoValue { | |
| span: name_span.to(value_span), | |
| code: best_match.to_string(), | |
| } | |
| } else if let Some(first_value) = possibilities.first() { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues { | |
| span: name_span.to(value_span), | |
| code: format!("{best_match} = \"{first_value}\""), | |
| expected: get_possibilities_sub(), | |
| } | |
| } else { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues { | |
| span: name_span.to(value_span), | |
| code: best_match.to_string(), | |
| expected: get_possibilities_sub(), | |
| } | |
| } | |
| } else { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::SimilarName { | |
| span: name_span, | |
| code: best_match.to_string(), | |
| expected: get_possibilities_sub(), | |
| } | |
| } | |
| } else { | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::SimilarName { | |
| span: name_span, | |
| code: best_match.to_string(), | |
| expected: None, | |
| } | |
| } | |
| } else { | |
| let similar_values = if !names_possibilities.is_empty() && names_possibilities.len() <= 3 { | |
| names_possibilities.sort(); | |
| names_possibilities | |
| .iter() | |
| .map(|cfg_name| diagnostics::unexpected_cfg_name::FoundWithSimilarValue { | |
| span: name_span, | |
| code: format!("{cfg_name} = \"{name}\""), | |
| }) | |
| .collect() | |
| } else { | |
| vec![] | |
| }; | |
| let (possibilities, and_more) = | |
| sort_and_truncate_possibilities(sess, possibilities, FilterWellKnownNames::Yes); | |
| let expected_names = if possibilities.is_empty() { | |
| None | |
| } else { | |
| let possibilities: Vec<_> = | |
| possibilities.into_iter().map(|s| Ident::new(s, name_span)).collect(); | |
| Some(diagnostics::unexpected_cfg_name::ExpectedNames { | |
| possibilities: possibilities.into(), | |
| and_more, | |
| }) | |
| }; | |
| diagnostics::unexpected_cfg_name::CodeSuggestion::SimilarValues { | |
| with_similar_values: similar_values, | |
| expected_names, | |
| } | |
| }; | |
| let inst = |escape_quotes| { | |
| to_check_cfg_arg(Ident::new(name, name_span), value.map(|(v, _s)| v), escape_quotes) | |
| }; | |
| let invocation_help = if is_from_cargo { | |
| let help = if !is_feature_cfg && !is_from_external_macro { | |
| Some(cargo_help_sub(sess, &inst)) | |
| } else { | |
| None | |
| }; | |
| diagnostics::unexpected_cfg_name::InvocationHelp::Cargo { | |
| help, | |
| macro_help: cargo_macro_help(name_span), | |
| } | |
| } else { | |
| let help = diagnostics::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No)); | |
| diagnostics::unexpected_cfg_name::InvocationHelp::Rustc { | |
| help, | |
| macro_help: rustc_macro_help(name_span), | |
| } | |
| }; | |
| diagnostics::UnexpectedCfgName { code_sugg, invocation_help, name } | |
| } | |
| pub(crate) fn unexpected_cfg_value( | |
| sess: &Session, | |
| (name, name_span): (Symbol, Span), | |
| value: Option<(Symbol, Span)>, | |
| ) -> diagnostics::UnexpectedCfgValue { | |
| let Some(ExpectedValues::Some(values)) = &sess.check_config.expecteds.get(&name) else { | |
| panic!( | |
| "it shouldn't be possible to have a diagnostic on a value whose name is not in values" | |
| ); | |
| }; | |
| let mut have_none_possibility = false; | |
| // We later sort possibilities if it is not empty, so the | |
| // order here does not matter. | |
| #[allow(rustc::potential_query_instability)] | |
| let possibilities: Vec<Symbol> = values | |
| .iter() | |
| .inspect(|a| have_none_possibility |= a.is_none()) | |
| .copied() | |
| .flatten() | |
| .collect(); | |
| let is_from_cargo = rustc_session::utils::was_invoked_from_cargo(); | |
| let is_from_external_macro = name_span.in_external_macro(sess.source_map()); | |
| let code_sugg = if let Some((value, _)) = value | |
| && sess.check_config.well_known_names.contains(&name) | |
| && let valid_names = possible_well_known_names_for_cfg_value(sess, value) | |
| && !valid_names.is_empty() | |
| { | |
| // Suggest changing the name to something for which `value` is an expected value. | |
| let max_suggestions = 3; | |
| let suggestions = valid_names | |
| .iter() | |
| .take(max_suggestions) | |
| .copied() | |
| .map(|name| diagnostics::unexpected_cfg_value::ChangeNameSuggestion { | |
| span: name_span, | |
| name, | |
| value, | |
| }) | |
| .collect::<Vec<_>>(); | |
| diagnostics::unexpected_cfg_value::CodeSuggestion::ChangeName { suggestions } | |
| } else if !possibilities.is_empty() { | |
| // Show the full list if all possible values for a given name, but don't do it | |
| // for names as the possibilities could be very long | |
| let expected_values = { | |
| let (possibilities, and_more) = sort_and_truncate_possibilities( | |
| sess, | |
| possibilities.clone(), | |
| FilterWellKnownNames::No, | |
| ); | |
| diagnostics::unexpected_cfg_value::ExpectedValues { | |
| name, | |
| have_none_possibility, | |
| possibilities: possibilities.into(), | |
| and_more, | |
| } | |
| }; | |
| let suggestion = if let Some((value, value_span)) = value { | |
| // Suggest the most probable if we found one | |
| find_best_match_for_name(&possibilities, value, None).map(|best_match| { | |
| diagnostics::unexpected_cfg_value::ChangeValueSuggestion::SimilarName { | |
| span: value_span, | |
| best_match, | |
| } | |
| }) | |
| } else if let &[first_possibility] = &possibilities[..] { | |
| Some(diagnostics::unexpected_cfg_value::ChangeValueSuggestion::SpecifyValue { | |
| span: name_span.shrink_to_hi(), | |
| first_possibility, | |
| }) | |
| } else { | |
| None | |
| }; | |
| diagnostics::unexpected_cfg_value::CodeSuggestion::ChangeValue { | |
| expected_values, | |
| suggestion, | |
| } | |
| } else if have_none_possibility { | |
| let suggestion = value.map(|(_value, value_span)| { | |
| diagnostics::unexpected_cfg_value::RemoveValueSuggestion { | |
| span: name_span.shrink_to_hi().to(value_span), | |
| } | |
| }); | |
| diagnostics::unexpected_cfg_value::CodeSuggestion::RemoveValue { suggestion, name } | |
| } else { | |
| let span = if let Some((_value, value_span)) = value { | |
| name_span.to(value_span) | |
| } else { | |
| name_span | |
| }; | |
| let suggestion = diagnostics::unexpected_cfg_value::RemoveConditionSuggestion { span }; | |
| diagnostics::unexpected_cfg_value::CodeSuggestion::RemoveCondition { suggestion, name } | |
| }; | |
| // We don't want to encourage people to add values to a well-known names, as these are | |
| // defined by rustc/Rust itself. Users can still do this if they wish, but should not be | |
| // encouraged to do so. | |
| let can_suggest_adding_value = !sess.check_config.well_known_names.contains(&name) | |
| // Except when working on rustc or the standard library itself, in which case we want to | |
| // suggest adding these cfgs to the "normal" place because of bootstrapping reasons. As a | |
| // basic heuristic, we use the "cheat" unstable feature enable method and the | |
| // non-ui-testing enabled option. | |
| || (matches!(sess.unstable_features, rustc_feature::UnstableFeatures::Cheat) | |
| && !sess.opts.unstable_opts.ui_testing); | |
| let inst = |escape_quotes| { | |
| to_check_cfg_arg(Ident::new(name, name_span), value.map(|(v, _s)| v), escape_quotes) | |
| }; | |
| let invocation_help = if is_from_cargo { | |
| let help = if name == sym::feature && !is_from_external_macro { | |
| if let Some((value, _value_span)) = value { | |
| Some(diagnostics::unexpected_cfg_value::CargoHelp::AddFeature { value }) | |
| } else { | |
| Some(diagnostics::unexpected_cfg_value::CargoHelp::DefineFeatures) | |
| } | |
| } else if can_suggest_adding_value && !is_from_external_macro { | |
| Some(diagnostics::unexpected_cfg_value::CargoHelp::Other(cargo_help_sub(sess, &inst))) | |
| } else { | |
| None | |
| }; | |
| diagnostics::unexpected_cfg_value::InvocationHelp::Cargo { | |
| help, | |
| macro_help: cargo_macro_help(name_span), | |
| } | |
| } else { | |
| let help = if can_suggest_adding_value { | |
| Some(diagnostics::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No))) | |
| } else { | |
| None | |
| }; | |
| diagnostics::unexpected_cfg_value::InvocationHelp::Rustc { | |
| help, | |
| macro_help: rustc_macro_help(name_span), | |
| } | |
| }; | |
| diagnostics::UnexpectedCfgValue { | |
| code_sugg, | |
| invocation_help, | |
| has_value: value.is_some(), | |
| value: value.map_or_else(String::new, |(v, _span)| v.to_string()), | |
| } | |
| } | |
| fn possible_well_known_names_for_cfg_value(sess: &Session, value: Symbol) -> Vec<Symbol> { | |
| #[allow(rustc::potential_query_instability)] | |
| let mut names = sess | |
| .check_config | |
| .well_known_names | |
| .iter() | |
| .filter(|name| { | |
| sess.check_config | |
| .expecteds | |
| .get(*name) | |
| .is_some_and(|expected_values| expected_values.contains(&Some(value))) | |
| }) | |
| .copied() | |
| .collect::<Vec<_>>(); | |
| names.sort_by(|a, b| a.as_str().cmp(b.as_str())); | |
| names | |
| } |