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                    let possibilities =
187                        possibilities.iter().copied().cloned().collect::<Vec<_>>().into();
188                    Some(diagnostics::unexpected_cfg_name::ExpectedValues {
189                        best_match,
190                        possibilities,
191                    })
192                } else {
193                    None
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            let possibilities: Vec<_> =
254                possibilities.into_iter().map(|s| Ident::new(s, name_span)).collect();
255            Some(diagnostics::unexpected_cfg_name::ExpectedNames {
256                possibilities: possibilities.into(),
257                and_more,
258            })
259        } else {
260            None
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            if let Some(best_match) = find_best_match_for_name(&possibilities, value, None) {
355                Some(diagnostics::unexpected_cfg_value::ChangeValueSuggestion::SimilarName {
356                    span: value_span,
357                    best_match,
358                })
359            } else {
360                None
361            }
362        } else if let &[first_possibility] = &possibilities[..] {
363            Some(diagnostics::unexpected_cfg_value::ChangeValueSuggestion::SpecifyValue {
364                span: name_span.shrink_to_hi(),
365                first_possibility,
366            })
367        } else {
368            None
369        };
370
371        diagnostics::unexpected_cfg_value::CodeSuggestion::ChangeValue {
372            expected_values,
373            suggestion,
374        }
375    } else if have_none_possibility {
376        let suggestion = value.map(|(_value, value_span)| {
377            diagnostics::unexpected_cfg_value::RemoveValueSuggestion {
378                span: name_span.shrink_to_hi().to(value_span),
379            }
380        });
381        diagnostics::unexpected_cfg_value::CodeSuggestion::RemoveValue { suggestion, name }
382    } else {
383        let span = if let Some((_value, value_span)) = value {
384            name_span.to(value_span)
385        } else {
386            name_span
387        };
388        let suggestion = diagnostics::unexpected_cfg_value::RemoveConditionSuggestion { span };
389        diagnostics::unexpected_cfg_value::CodeSuggestion::RemoveCondition { suggestion, name }
390    };
391
392    // We don't want to encourage people to add values to a well-known names, as these are
393    // defined by rustc/Rust itself. Users can still do this if they wish, but should not be
394    // encouraged to do so.
395    let can_suggest_adding_value = !sess.check_config.well_known_names.contains(&name)
396        // Except when working on rustc or the standard library itself, in which case we want to
397        // suggest adding these cfgs to the "normal" place because of bootstrapping reasons. As a
398        // basic heuristic, we use the "cheat" unstable feature enable method and the
399        // non-ui-testing enabled option.
400        || (#[allow(non_exhaustive_omitted_patterns)] match sess.unstable_features {
    rustc_feature::UnstableFeatures::Cheat => true,
    _ => false,
}matches!(sess.unstable_features, rustc_feature::UnstableFeatures::Cheat)
401            && !sess.opts.unstable_opts.ui_testing);
402
403    let inst = |escape_quotes| {
404        to_check_cfg_arg(Ident::new(name, name_span), value.map(|(v, _s)| v), escape_quotes)
405    };
406
407    let invocation_help = if is_from_cargo {
408        let help = if name == sym::feature && !is_from_external_macro {
409            if let Some((value, _value_span)) = value {
410                Some(diagnostics::unexpected_cfg_value::CargoHelp::AddFeature { value })
411            } else {
412                Some(diagnostics::unexpected_cfg_value::CargoHelp::DefineFeatures)
413            }
414        } else if can_suggest_adding_value && !is_from_external_macro {
415            Some(diagnostics::unexpected_cfg_value::CargoHelp::Other(cargo_help_sub(sess, &inst)))
416        } else {
417            None
418        };
419        diagnostics::unexpected_cfg_value::InvocationHelp::Cargo {
420            help,
421            macro_help: cargo_macro_help(name_span),
422        }
423    } else {
424        let help = if can_suggest_adding_value {
425            Some(diagnostics::UnexpectedCfgRustcHelp::new(&inst(EscapeQuotes::No)))
426        } else {
427            None
428        };
429        diagnostics::unexpected_cfg_value::InvocationHelp::Rustc {
430            help,
431            macro_help: rustc_macro_help(name_span),
432        }
433    };
434
435    diagnostics::UnexpectedCfgValue {
436        code_sugg,
437        invocation_help,
438        has_value: value.is_some(),
439        value: value.map_or_else(String::new, |(v, _span)| v.to_string()),
440    }
441}
442
443fn possible_well_known_names_for_cfg_value(sess: &Session, value: Symbol) -> Vec<Symbol> {
444    #[allow(rustc::potential_query_instability)]
445    let mut names = sess
446        .check_config
447        .well_known_names
448        .iter()
449        .filter(|name| {
450            sess.check_config
451                .expecteds
452                .get(*name)
453                .map(|expected_values| expected_values.contains(&Some(value)))
454                .unwrap_or_default()
455        })
456        .copied()
457        .collect::<Vec<_>>();
458    names.sort_by(|a, b| a.as_str().cmp(b.as_str()));
459    names
460}