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