Skip to main content

rustc_attr_parsing/
check_cfg.rs

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