diff options
Diffstat (limited to 'compiler/rustc_lint/src/early/diagnostics/check_cfg.rs')
| -rw-r--r-- | compiler/rustc_lint/src/early/diagnostics/check_cfg.rs | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/compiler/rustc_lint/src/early/diagnostics/check_cfg.rs b/compiler/rustc_lint/src/early/diagnostics/check_cfg.rs new file mode 100644 index 00000000000..63a722f6067 --- /dev/null +++ b/compiler/rustc_lint/src/early/diagnostics/check_cfg.rs @@ -0,0 +1,362 @@ +use rustc_hir::def_id::LOCAL_CRATE; +use rustc_middle::bug; +use rustc_session::Session; +use rustc_session::config::ExpectedValues; +use rustc_span::edit_distance::find_best_match_for_name; +use rustc_span::symbol::Ident; +use rustc_span::{ExpnKind, Span, Symbol, sym}; + +use crate::lints; + +const MAX_CHECK_CFG_NAMES_OR_VALUES: usize = 35; + +fn sort_and_truncate_possibilities( + sess: &Session, + mut possibilities: Vec<Symbol>, +) -> (Vec<Symbol>, usize) { + let n_possibilities = if sess.opts.unstable_opts.check_cfg_all_expected { + possibilities.len() + } else { + 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, +) -> lints::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 matches!(&sess.opts.crate_name, Some(crate_name) if crate_name == "build_script_build") { + lints::UnexpectedCfgCargoHelp::lint_cfg(unescaped) + } else { + lints::UnexpectedCfgCargoHelp::lint_cfg_and_build_rs(unescaped, &inst(EscapeQuotes::Yes)) + } +} + +fn rustc_macro_help(span: Span) -> Option<lints::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(lints::UnexpectedCfgRustcMacroHelp { macro_kind: macro_kind.descr(), macro_name }) + } else { + None + } +} + +fn cargo_macro_help(span: Span) -> Option<lints::UnexpectedCfgCargoMacroHelp> { + 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(lints::UnexpectedCfgCargoMacroHelp { + macro_kind: macro_kind.descr(), + macro_name, + // FIXME: Get access to a `TyCtxt` from an `EarlyContext` + // crate_name: cx.tcx.crate_name(def_id.krate), + }) + } else { + None + } +} + +pub(super) fn unexpected_cfg_name( + sess: &Session, + (name, name_span): (Symbol, Span), + value: Option<(Symbol, Span)>, +) -> lints::UnexpectedCfgName { + #[allow(rustc::potential_query_instability)] + let possibilities: Vec<Symbol> = sess.psess.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.psess + .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 = rustc_middle::lint::in_external_macro(sess, name_span); + let mut is_feature_cfg = name == sym::feature; + + let code_sugg = if is_feature_cfg && is_from_cargo { + lints::unexpected_cfg_name::CodeSuggestion::DefineFeatures + // 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.psess.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() { + let possibilities = + possibilities.iter().copied().cloned().collect::<Vec<_>>().into(); + Some(lints::unexpected_cfg_name::ExpectedValues { best_match, possibilities }) + } else { + None + } + }; + + let best_match = Ident::new(best_match, name_span); + if let Some((value, value_span)) = value { + if best_match_values.contains(&Some(value)) { + lints::unexpected_cfg_name::CodeSuggestion::SimilarNameAndValue { + span: name_span, + code: best_match.to_string(), + } + } else if best_match_values.contains(&None) { + lints::unexpected_cfg_name::CodeSuggestion::SimilarNameNoValue { + span: name_span.to(value_span), + code: best_match.to_string(), + } + } else if let Some(first_value) = possibilities.first() { + lints::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues { + span: name_span.to(value_span), + code: format!("{best_match} = \"{first_value}\""), + expected: get_possibilities_sub(), + } + } else { + lints::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues { + span: name_span.to(value_span), + code: best_match.to_string(), + expected: get_possibilities_sub(), + } + } + } else { + lints::unexpected_cfg_name::CodeSuggestion::SimilarName { + span: name_span, + code: best_match.to_string(), + expected: get_possibilities_sub(), + } + } + } else { + lints::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| lints::unexpected_cfg_name::FoundWithSimilarValue { + span: name_span, + code: format!("{cfg_name} = \"{name}\""), + }) + .collect() + } else { + vec![] + }; + let expected_names = if !possibilities.is_empty() { + let (possibilities, and_more) = sort_and_truncate_possibilities(sess, possibilities); + let possibilities: Vec<_> = + possibilities.into_iter().map(|s| Ident::new(s, name_span)).collect(); + Some(lints::unexpected_cfg_name::ExpectedNames { + possibilities: possibilities.into(), + and_more, + }) + } else { + None + }; + lints::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 + }; + lints::unexpected_cfg_name::InvocationHelp::Cargo { + help, + macro_help: cargo_macro_help(name_span), + } + } else { + let help = lints::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No)); + lints::unexpected_cfg_name::InvocationHelp::Rustc { + help, + macro_help: rustc_macro_help(name_span), + } + }; + + lints::UnexpectedCfgName { code_sugg, invocation_help, name } +} + +pub(super) fn unexpected_cfg_value( + sess: &Session, + (name, name_span): (Symbol, Span), + value: Option<(Symbol, Span)>, +) -> lints::UnexpectedCfgValue { + let Some(ExpectedValues::Some(values)) = &sess.psess.check_config.expecteds.get(&name) else { + bug!( + "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 = rustc_middle::lint::in_external_macro(sess, name_span); + + // 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 code_sugg = if !possibilities.is_empty() { + let expected_values = { + let (possibilities, and_more) = + sort_and_truncate_possibilities(sess, possibilities.clone()); + lints::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 + if let Some(best_match) = find_best_match_for_name(&possibilities, value, None) { + Some(lints::unexpected_cfg_value::ChangeValueSuggestion::SimilarName { + span: value_span, + best_match, + }) + } else { + None + } + } else if let &[first_possibility] = &possibilities[..] { + Some(lints::unexpected_cfg_value::ChangeValueSuggestion::SpecifyValue { + span: name_span.shrink_to_hi(), + first_possibility, + }) + } else { + None + }; + + lints::unexpected_cfg_value::CodeSuggestion::ChangeValue { expected_values, suggestion } + } else if have_none_possibility { + let suggestion = + value.map(|(_value, value_span)| lints::unexpected_cfg_value::RemoveValueSuggestion { + span: name_span.shrink_to_hi().to(value_span), + }); + lints::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 = lints::unexpected_cfg_value::RemoveConditionSuggestion { span }; + lints::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.psess.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.psess.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(lints::unexpected_cfg_value::CargoHelp::AddFeature { value }) + } else { + Some(lints::unexpected_cfg_value::CargoHelp::DefineFeatures) + } + } else if can_suggest_adding_value && !is_from_external_macro { + Some(lints::unexpected_cfg_value::CargoHelp::Other(cargo_help_sub(sess, &inst))) + } else { + None + }; + lints::unexpected_cfg_value::InvocationHelp::Cargo { + help, + macro_help: cargo_macro_help(name_span), + } + } else { + let help = if can_suggest_adding_value { + Some(lints::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No))) + } else { + None + }; + lints::unexpected_cfg_value::InvocationHelp::Rustc { + help, + macro_help: rustc_macro_help(name_span), + } + }; + + lints::UnexpectedCfgValue { + code_sugg, + invocation_help, + has_value: value.is_some(), + value: value.map_or_else(String::new, |(v, _span)| v.to_string()), + } +} |
