rustc_lint/early/diagnostics/
check_cfg.rs

1use rustc_hir::def_id::LOCAL_CRATE;
2use rustc_middle::bug;
3use rustc_middle::ty::TyCtxt;
4use rustc_session::Session;
5use rustc_session::config::ExpectedValues;
6use rustc_span::edit_distance::find_best_match_for_name;
7use rustc_span::{ExpnKind, Ident, Span, Symbol, sym};
8
9use crate::lints;
10
11const MAX_CHECK_CFG_NAMES_OR_VALUES: usize = 35;
12
13enum FilterWellKnownNames {
14    Yes,
15    No,
16}
17
18fn sort_and_truncate_possibilities(
19    sess: &Session,
20    mut possibilities: Vec<Symbol>,
21    filter_well_known_names: FilterWellKnownNames,
22) -> (Vec<Symbol>, usize) {
23    let possibilities_len = possibilities.len();
24
25    let n_possibilities = if sess.opts.unstable_opts.check_cfg_all_expected {
26        possibilities.len()
27    } else {
28        match filter_well_known_names {
29            FilterWellKnownNames::Yes => {
30                possibilities.retain(|cfg_name| {
31                    !sess.psess.check_config.well_known_names.contains(cfg_name)
32                });
33            }
34            FilterWellKnownNames::No => {}
35        };
36        std::cmp::min(possibilities.len(), MAX_CHECK_CFG_NAMES_OR_VALUES)
37    };
38
39    possibilities.sort_by(|s1, s2| s1.as_str().cmp(s2.as_str()));
40
41    let and_more = possibilities_len.saturating_sub(n_possibilities);
42    possibilities.truncate(n_possibilities);
43    (possibilities, and_more)
44}
45
46enum EscapeQuotes {
47    Yes,
48    No,
49}
50
51fn to_check_cfg_arg(name: Ident, value: Option<Symbol>, quotes: EscapeQuotes) -> String {
52    if let Some(value) = value {
53        let value = str::escape_debug(value.as_str()).to_string();
54        let values = match quotes {
55            EscapeQuotes::Yes => format!("\\\"{}\\\"", value.replace("\"", "\\\\\\\\\"")),
56            EscapeQuotes::No => format!("\"{value}\""),
57        };
58        format!("cfg({name}, values({values}))")
59    } else {
60        format!("cfg({name})")
61    }
62}
63
64fn cargo_help_sub(
65    sess: &Session,
66    inst: &impl Fn(EscapeQuotes) -> String,
67) -> lints::UnexpectedCfgCargoHelp {
68    // We don't want to suggest the `build.rs` way to expected cfgs if we are already in a
69    // `build.rs`. We therefor do a best effort check (looking if the `--crate-name` is
70    // `build_script_build`) to try to figure out if we are building a Cargo build script
71
72    let unescaped = &inst(EscapeQuotes::No);
73    if matches!(&sess.opts.crate_name, Some(crate_name) if crate_name == "build_script_build") {
74        lints::UnexpectedCfgCargoHelp::lint_cfg(unescaped)
75    } else {
76        lints::UnexpectedCfgCargoHelp::lint_cfg_and_build_rs(unescaped, &inst(EscapeQuotes::Yes))
77    }
78}
79
80fn rustc_macro_help(span: Span) -> Option<lints::UnexpectedCfgRustcMacroHelp> {
81    let oexpn = span.ctxt().outer_expn_data();
82    if let Some(def_id) = oexpn.macro_def_id
83        && let ExpnKind::Macro(macro_kind, macro_name) = oexpn.kind
84        && def_id.krate != LOCAL_CRATE
85    {
86        Some(lints::UnexpectedCfgRustcMacroHelp { macro_kind: macro_kind.descr(), macro_name })
87    } else {
88        None
89    }
90}
91
92fn cargo_macro_help(
93    tcx: Option<TyCtxt<'_>>,
94    span: Span,
95) -> Option<lints::UnexpectedCfgCargoMacroHelp> {
96    let oexpn = span.ctxt().outer_expn_data();
97    if let Some(def_id) = oexpn.macro_def_id
98        && let ExpnKind::Macro(macro_kind, macro_name) = oexpn.kind
99        && def_id.krate != LOCAL_CRATE
100        && let Some(tcx) = tcx
101    {
102        Some(lints::UnexpectedCfgCargoMacroHelp {
103            macro_kind: macro_kind.descr(),
104            macro_name,
105            crate_name: tcx.crate_name(def_id.krate),
106        })
107    } else {
108        None
109    }
110}
111
112pub(super) fn unexpected_cfg_name(
113    sess: &Session,
114    tcx: Option<TyCtxt<'_>>,
115    (name, name_span): (Symbol, Span),
116    value: Option<(Symbol, Span)>,
117) -> lints::UnexpectedCfgName {
118    #[allow(rustc::potential_query_instability)]
119    let possibilities: Vec<Symbol> = sess.psess.check_config.expecteds.keys().copied().collect();
120
121    let mut names_possibilities: Vec<_> = if value.is_none() {
122        // We later sort and display all the possibilities, so the order here does not matter.
123        #[allow(rustc::potential_query_instability)]
124        sess.psess
125            .check_config
126            .expecteds
127            .iter()
128            .filter_map(|(k, v)| match v {
129                ExpectedValues::Some(v) if v.contains(&Some(name)) => Some(k),
130                _ => None,
131            })
132            .collect()
133    } else {
134        Vec::new()
135    };
136
137    let is_from_cargo = rustc_session::utils::was_invoked_from_cargo();
138    let is_from_external_macro = name_span.in_external_macro(sess.source_map());
139    let mut is_feature_cfg = name == sym::feature;
140
141    let code_sugg = if is_feature_cfg && is_from_cargo {
142        lints::unexpected_cfg_name::CodeSuggestion::DefineFeatures
143    // Suggest correct `version("..")` predicate syntax
144    } else if let Some((_value, value_span)) = value
145        && name == sym::version
146    {
147        lints::unexpected_cfg_name::CodeSuggestion::VersionSyntax {
148            between_name_and_value: name_span.between(value_span),
149            after_value: value_span.shrink_to_hi(),
150        }
151    // Suggest the most probable if we found one
152    } else if let Some(best_match) = find_best_match_for_name(&possibilities, name, None) {
153        is_feature_cfg |= best_match == sym::feature;
154
155        if let Some(ExpectedValues::Some(best_match_values)) =
156            sess.psess.check_config.expecteds.get(&best_match)
157        {
158            // We will soon sort, so the initial order does not matter.
159            #[allow(rustc::potential_query_instability)]
160            let mut possibilities = best_match_values.iter().flatten().collect::<Vec<_>>();
161            possibilities.sort_by_key(|s| s.as_str());
162
163            let get_possibilities_sub = || {
164                if !possibilities.is_empty() {
165                    let possibilities =
166                        possibilities.iter().copied().cloned().collect::<Vec<_>>().into();
167                    Some(lints::unexpected_cfg_name::ExpectedValues { best_match, possibilities })
168                } else {
169                    None
170                }
171            };
172
173            let best_match = Ident::new(best_match, name_span);
174            if let Some((value, value_span)) = value {
175                if best_match_values.contains(&Some(value)) {
176                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameAndValue {
177                        span: name_span,
178                        code: best_match.to_string(),
179                    }
180                } else if best_match_values.contains(&None) {
181                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameNoValue {
182                        span: name_span.to(value_span),
183                        code: best_match.to_string(),
184                    }
185                } else if let Some(first_value) = possibilities.first() {
186                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues {
187                        span: name_span.to(value_span),
188                        code: format!("{best_match} = \"{first_value}\""),
189                        expected: get_possibilities_sub(),
190                    }
191                } else {
192                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues {
193                        span: name_span.to(value_span),
194                        code: best_match.to_string(),
195                        expected: get_possibilities_sub(),
196                    }
197                }
198            } else {
199                lints::unexpected_cfg_name::CodeSuggestion::SimilarName {
200                    span: name_span,
201                    code: best_match.to_string(),
202                    expected: get_possibilities_sub(),
203                }
204            }
205        } else {
206            lints::unexpected_cfg_name::CodeSuggestion::SimilarName {
207                span: name_span,
208                code: best_match.to_string(),
209                expected: None,
210            }
211        }
212    } else {
213        let similar_values = if !names_possibilities.is_empty() && names_possibilities.len() <= 3 {
214            names_possibilities.sort();
215            names_possibilities
216                .iter()
217                .map(|cfg_name| lints::unexpected_cfg_name::FoundWithSimilarValue {
218                    span: name_span,
219                    code: format!("{cfg_name} = \"{name}\""),
220                })
221                .collect()
222        } else {
223            vec![]
224        };
225
226        let (possibilities, and_more) =
227            sort_and_truncate_possibilities(sess, possibilities, FilterWellKnownNames::Yes);
228        let expected_names = if !possibilities.is_empty() {
229            let possibilities: Vec<_> =
230                possibilities.into_iter().map(|s| Ident::new(s, name_span)).collect();
231            Some(lints::unexpected_cfg_name::ExpectedNames {
232                possibilities: possibilities.into(),
233                and_more,
234            })
235        } else {
236            None
237        };
238        lints::unexpected_cfg_name::CodeSuggestion::SimilarValues {
239            with_similar_values: similar_values,
240            expected_names,
241        }
242    };
243
244    let inst = |escape_quotes| {
245        to_check_cfg_arg(Ident::new(name, name_span), value.map(|(v, _s)| v), escape_quotes)
246    };
247
248    let invocation_help = if is_from_cargo {
249        let help = if !is_feature_cfg && !is_from_external_macro {
250            Some(cargo_help_sub(sess, &inst))
251        } else {
252            None
253        };
254        lints::unexpected_cfg_name::InvocationHelp::Cargo {
255            help,
256            macro_help: cargo_macro_help(tcx, name_span),
257        }
258    } else {
259        let help = lints::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No));
260        lints::unexpected_cfg_name::InvocationHelp::Rustc {
261            help,
262            macro_help: rustc_macro_help(name_span),
263        }
264    };
265
266    lints::UnexpectedCfgName { code_sugg, invocation_help, name }
267}
268
269pub(super) fn unexpected_cfg_value(
270    sess: &Session,
271    tcx: Option<TyCtxt<'_>>,
272    (name, name_span): (Symbol, Span),
273    value: Option<(Symbol, Span)>,
274) -> lints::UnexpectedCfgValue {
275    let Some(ExpectedValues::Some(values)) = &sess.psess.check_config.expecteds.get(&name) else {
276        bug!(
277            "it shouldn't be possible to have a diagnostic on a value whose name is not in values"
278        );
279    };
280    let mut have_none_possibility = false;
281    // We later sort possibilities if it is not empty, so the
282    // order here does not matter.
283    #[allow(rustc::potential_query_instability)]
284    let possibilities: Vec<Symbol> = values
285        .iter()
286        .inspect(|a| have_none_possibility |= a.is_none())
287        .copied()
288        .flatten()
289        .collect();
290
291    let is_from_cargo = rustc_session::utils::was_invoked_from_cargo();
292    let is_from_external_macro = name_span.in_external_macro(sess.source_map());
293
294    // Show the full list if all possible values for a given name, but don't do it
295    // for names as the possibilities could be very long
296    let code_sugg = if !possibilities.is_empty() {
297        let expected_values = {
298            let (possibilities, and_more) = sort_and_truncate_possibilities(
299                sess,
300                possibilities.clone(),
301                FilterWellKnownNames::No,
302            );
303            lints::unexpected_cfg_value::ExpectedValues {
304                name,
305                have_none_possibility,
306                possibilities: possibilities.into(),
307                and_more,
308            }
309        };
310
311        let suggestion = if let Some((value, value_span)) = value {
312            // Suggest the most probable if we found one
313            if let Some(best_match) = find_best_match_for_name(&possibilities, value, None) {
314                Some(lints::unexpected_cfg_value::ChangeValueSuggestion::SimilarName {
315                    span: value_span,
316                    best_match,
317                })
318            } else {
319                None
320            }
321        } else if let &[first_possibility] = &possibilities[..] {
322            Some(lints::unexpected_cfg_value::ChangeValueSuggestion::SpecifyValue {
323                span: name_span.shrink_to_hi(),
324                first_possibility,
325            })
326        } else {
327            None
328        };
329
330        lints::unexpected_cfg_value::CodeSuggestion::ChangeValue { expected_values, suggestion }
331    } else if have_none_possibility {
332        let suggestion =
333            value.map(|(_value, value_span)| lints::unexpected_cfg_value::RemoveValueSuggestion {
334                span: name_span.shrink_to_hi().to(value_span),
335            });
336        lints::unexpected_cfg_value::CodeSuggestion::RemoveValue { suggestion, name }
337    } else {
338        let span = if let Some((_value, value_span)) = value {
339            name_span.to(value_span)
340        } else {
341            name_span
342        };
343        let suggestion = lints::unexpected_cfg_value::RemoveConditionSuggestion { span };
344        lints::unexpected_cfg_value::CodeSuggestion::RemoveCondition { suggestion, name }
345    };
346
347    // We don't want to encourage people to add values to a well-known names, as these are
348    // defined by rustc/Rust itself. Users can still do this if they wish, but should not be
349    // encouraged to do so.
350    let can_suggest_adding_value = !sess.psess.check_config.well_known_names.contains(&name)
351        // Except when working on rustc or the standard library itself, in which case we want to
352        // suggest adding these cfgs to the "normal" place because of bootstrapping reasons. As a
353        // basic heuristic, we use the "cheat" unstable feature enable method and the
354        // non-ui-testing enabled option.
355        || (matches!(sess.psess.unstable_features, rustc_feature::UnstableFeatures::Cheat)
356            && !sess.opts.unstable_opts.ui_testing);
357
358    let inst = |escape_quotes| {
359        to_check_cfg_arg(Ident::new(name, name_span), value.map(|(v, _s)| v), escape_quotes)
360    };
361
362    let invocation_help = if is_from_cargo {
363        let help = if name == sym::feature && !is_from_external_macro {
364            if let Some((value, _value_span)) = value {
365                Some(lints::unexpected_cfg_value::CargoHelp::AddFeature { value })
366            } else {
367                Some(lints::unexpected_cfg_value::CargoHelp::DefineFeatures)
368            }
369        } else if can_suggest_adding_value && !is_from_external_macro {
370            Some(lints::unexpected_cfg_value::CargoHelp::Other(cargo_help_sub(sess, &inst)))
371        } else {
372            None
373        };
374        lints::unexpected_cfg_value::InvocationHelp::Cargo {
375            help,
376            macro_help: cargo_macro_help(tcx, name_span),
377        }
378    } else {
379        let help = if can_suggest_adding_value {
380            Some(lints::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No)))
381        } else {
382            None
383        };
384        lints::unexpected_cfg_value::InvocationHelp::Rustc {
385            help,
386            macro_help: rustc_macro_help(name_span),
387        }
388    };
389
390    lints::UnexpectedCfgValue {
391        code_sugg,
392        invocation_help,
393        has_value: value.is_some(),
394        value: value.map_or_else(String::new, |(v, _span)| v.to_string()),
395    }
396}