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    fn miscapitalized_boolean(name: Symbol) -> Option<bool> {
142        if name.as_str().eq_ignore_ascii_case("false") {
143            Some(false)
144        } else if name.as_str().eq_ignore_ascii_case("true") {
145            Some(true)
146        } else {
147            None
148        }
149    }
150
151    let code_sugg = if is_feature_cfg && is_from_cargo {
152        lints::unexpected_cfg_name::CodeSuggestion::DefineFeatures
153    // Suggest correct `version("..")` predicate syntax
154    } else if let Some((_value, value_span)) = value
155        && name == sym::version
156    {
157        lints::unexpected_cfg_name::CodeSuggestion::VersionSyntax {
158            between_name_and_value: name_span.between(value_span),
159            after_value: value_span.shrink_to_hi(),
160        }
161    // Suggest a literal `false` instead
162    // Detect miscapitalized `False`/`FALSE` etc, ensuring that this isn't `r#false`
163    } else if value.is_none()
164        // If this is a miscapitalized False/FALSE, suggest the boolean literal instead
165        && let Some(boolean) = miscapitalized_boolean(name)
166        // Check this isn't a raw identifier
167        && sess
168            .source_map()
169            .span_to_snippet(name_span)
170            .map_or(true, |snippet| !snippet.contains("r#"))
171    {
172        lints::unexpected_cfg_name::CodeSuggestion::BooleanLiteral {
173            span: name_span,
174            literal: boolean,
175        }
176    // Suggest the most probable if we found one
177    } else if let Some(best_match) = find_best_match_for_name(&possibilities, name, None) {
178        is_feature_cfg |= best_match == sym::feature;
179
180        if let Some(ExpectedValues::Some(best_match_values)) =
181            sess.psess.check_config.expecteds.get(&best_match)
182        {
183            // We will soon sort, so the initial order does not matter.
184            #[allow(rustc::potential_query_instability)]
185            let mut possibilities = best_match_values.iter().flatten().collect::<Vec<_>>();
186            possibilities.sort_by_key(|s| s.as_str());
187
188            let get_possibilities_sub = || {
189                if !possibilities.is_empty() {
190                    let possibilities =
191                        possibilities.iter().copied().cloned().collect::<Vec<_>>().into();
192                    Some(lints::unexpected_cfg_name::ExpectedValues { best_match, possibilities })
193                } else {
194                    None
195                }
196            };
197
198            let best_match = Ident::new(best_match, name_span);
199            if let Some((value, value_span)) = value {
200                if best_match_values.contains(&Some(value)) {
201                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameAndValue {
202                        span: name_span,
203                        code: best_match.to_string(),
204                    }
205                } else if best_match_values.contains(&None) {
206                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameNoValue {
207                        span: name_span.to(value_span),
208                        code: best_match.to_string(),
209                    }
210                } else if let Some(first_value) = possibilities.first() {
211                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues {
212                        span: name_span.to(value_span),
213                        code: format!("{best_match} = \"{first_value}\""),
214                        expected: get_possibilities_sub(),
215                    }
216                } else {
217                    lints::unexpected_cfg_name::CodeSuggestion::SimilarNameDifferentValues {
218                        span: name_span.to(value_span),
219                        code: best_match.to_string(),
220                        expected: get_possibilities_sub(),
221                    }
222                }
223            } else {
224                lints::unexpected_cfg_name::CodeSuggestion::SimilarName {
225                    span: name_span,
226                    code: best_match.to_string(),
227                    expected: get_possibilities_sub(),
228                }
229            }
230        } else {
231            lints::unexpected_cfg_name::CodeSuggestion::SimilarName {
232                span: name_span,
233                code: best_match.to_string(),
234                expected: None,
235            }
236        }
237    } else {
238        let similar_values = if !names_possibilities.is_empty() && names_possibilities.len() <= 3 {
239            names_possibilities.sort();
240            names_possibilities
241                .iter()
242                .map(|cfg_name| lints::unexpected_cfg_name::FoundWithSimilarValue {
243                    span: name_span,
244                    code: format!("{cfg_name} = \"{name}\""),
245                })
246                .collect()
247        } else {
248            vec![]
249        };
250
251        let (possibilities, and_more) =
252            sort_and_truncate_possibilities(sess, possibilities, FilterWellKnownNames::Yes);
253        let expected_names = if !possibilities.is_empty() {
254            let possibilities: Vec<_> =
255                possibilities.into_iter().map(|s| Ident::new(s, name_span)).collect();
256            Some(lints::unexpected_cfg_name::ExpectedNames {
257                possibilities: possibilities.into(),
258                and_more,
259            })
260        } else {
261            None
262        };
263        lints::unexpected_cfg_name::CodeSuggestion::SimilarValues {
264            with_similar_values: similar_values,
265            expected_names,
266        }
267    };
268
269    let inst = |escape_quotes| {
270        to_check_cfg_arg(Ident::new(name, name_span), value.map(|(v, _s)| v), escape_quotes)
271    };
272
273    let invocation_help = if is_from_cargo {
274        let help = if !is_feature_cfg && !is_from_external_macro {
275            Some(cargo_help_sub(sess, &inst))
276        } else {
277            None
278        };
279        lints::unexpected_cfg_name::InvocationHelp::Cargo {
280            help,
281            macro_help: cargo_macro_help(tcx, name_span),
282        }
283    } else {
284        let help = lints::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No));
285        lints::unexpected_cfg_name::InvocationHelp::Rustc {
286            help,
287            macro_help: rustc_macro_help(name_span),
288        }
289    };
290
291    lints::UnexpectedCfgName { code_sugg, invocation_help, name }
292}
293
294pub(super) fn unexpected_cfg_value(
295    sess: &Session,
296    tcx: Option<TyCtxt<'_>>,
297    (name, name_span): (Symbol, Span),
298    value: Option<(Symbol, Span)>,
299) -> lints::UnexpectedCfgValue {
300    let Some(ExpectedValues::Some(values)) = &sess.psess.check_config.expecteds.get(&name) else {
301        bug!(
302            "it shouldn't be possible to have a diagnostic on a value whose name is not in values"
303        );
304    };
305    let mut have_none_possibility = false;
306    // We later sort possibilities if it is not empty, so the
307    // order here does not matter.
308    #[allow(rustc::potential_query_instability)]
309    let possibilities: Vec<Symbol> = values
310        .iter()
311        .inspect(|a| have_none_possibility |= a.is_none())
312        .copied()
313        .flatten()
314        .collect();
315
316    let is_from_cargo = rustc_session::utils::was_invoked_from_cargo();
317    let is_from_external_macro = name_span.in_external_macro(sess.source_map());
318
319    // Show the full list if all possible values for a given name, but don't do it
320    // for names as the possibilities could be very long
321    let code_sugg = if !possibilities.is_empty() {
322        let expected_values = {
323            let (possibilities, and_more) = sort_and_truncate_possibilities(
324                sess,
325                possibilities.clone(),
326                FilterWellKnownNames::No,
327            );
328            lints::unexpected_cfg_value::ExpectedValues {
329                name,
330                have_none_possibility,
331                possibilities: possibilities.into(),
332                and_more,
333            }
334        };
335
336        let suggestion = if let Some((value, value_span)) = value {
337            // Suggest the most probable if we found one
338            if let Some(best_match) = find_best_match_for_name(&possibilities, value, None) {
339                Some(lints::unexpected_cfg_value::ChangeValueSuggestion::SimilarName {
340                    span: value_span,
341                    best_match,
342                })
343            } else {
344                None
345            }
346        } else if let &[first_possibility] = &possibilities[..] {
347            Some(lints::unexpected_cfg_value::ChangeValueSuggestion::SpecifyValue {
348                span: name_span.shrink_to_hi(),
349                first_possibility,
350            })
351        } else {
352            None
353        };
354
355        lints::unexpected_cfg_value::CodeSuggestion::ChangeValue { expected_values, suggestion }
356    } else if have_none_possibility {
357        let suggestion =
358            value.map(|(_value, value_span)| lints::unexpected_cfg_value::RemoveValueSuggestion {
359                span: name_span.shrink_to_hi().to(value_span),
360            });
361        lints::unexpected_cfg_value::CodeSuggestion::RemoveValue { suggestion, name }
362    } else {
363        let span = if let Some((_value, value_span)) = value {
364            name_span.to(value_span)
365        } else {
366            name_span
367        };
368        let suggestion = lints::unexpected_cfg_value::RemoveConditionSuggestion { span };
369        lints::unexpected_cfg_value::CodeSuggestion::RemoveCondition { suggestion, name }
370    };
371
372    // We don't want to encourage people to add values to a well-known names, as these are
373    // defined by rustc/Rust itself. Users can still do this if they wish, but should not be
374    // encouraged to do so.
375    let can_suggest_adding_value = !sess.psess.check_config.well_known_names.contains(&name)
376        // Except when working on rustc or the standard library itself, in which case we want to
377        // suggest adding these cfgs to the "normal" place because of bootstrapping reasons. As a
378        // basic heuristic, we use the "cheat" unstable feature enable method and the
379        // non-ui-testing enabled option.
380        || (matches!(sess.psess.unstable_features, rustc_feature::UnstableFeatures::Cheat)
381            && !sess.opts.unstable_opts.ui_testing);
382
383    let inst = |escape_quotes| {
384        to_check_cfg_arg(Ident::new(name, name_span), value.map(|(v, _s)| v), escape_quotes)
385    };
386
387    let invocation_help = if is_from_cargo {
388        let help = if name == sym::feature && !is_from_external_macro {
389            if let Some((value, _value_span)) = value {
390                Some(lints::unexpected_cfg_value::CargoHelp::AddFeature { value })
391            } else {
392                Some(lints::unexpected_cfg_value::CargoHelp::DefineFeatures)
393            }
394        } else if can_suggest_adding_value && !is_from_external_macro {
395            Some(lints::unexpected_cfg_value::CargoHelp::Other(cargo_help_sub(sess, &inst)))
396        } else {
397            None
398        };
399        lints::unexpected_cfg_value::InvocationHelp::Cargo {
400            help,
401            macro_help: cargo_macro_help(tcx, name_span),
402        }
403    } else {
404        let help = if can_suggest_adding_value {
405            Some(lints::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No)))
406        } else {
407            None
408        };
409        lints::unexpected_cfg_value::InvocationHelp::Rustc {
410            help,
411            macro_help: rustc_macro_help(name_span),
412        }
413    };
414
415    lints::UnexpectedCfgValue {
416        code_sugg,
417        invocation_help,
418        has_value: value.is_some(),
419        value: value.map_or_else(String::new, |(v, _span)| v.to_string()),
420    }
421}