1use crate::ClippyConfiguration;
2use crate::types::{
3 DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename, SourceItemOrdering,
4 SourceItemOrderingCategory, SourceItemOrderingModuleItemGroupings, SourceItemOrderingModuleItemKind,
5 SourceItemOrderingTraitAssocItemKind, SourceItemOrderingTraitAssocItemKinds,
6};
7use clippy_utils::msrvs::Msrv;
8use rustc_errors::Applicability;
9use rustc_session::Session;
10use rustc_span::edit_distance::edit_distance;
11use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
12use serde::de::{IgnoredAny, IntoDeserializer, MapAccess, Visitor};
13use serde::{Deserialize, Deserializer, Serialize};
14use std::fmt::{Debug, Display, Formatter};
15use std::ops::Range;
16use std::path::PathBuf;
17use std::str::FromStr;
18use std::sync::OnceLock;
19use std::{cmp, env, fmt, fs, io};
20
21#[rustfmt::skip]
22const DEFAULT_DOC_VALID_IDENTS: &[&str] = &[
23 "KiB", "MiB", "GiB", "TiB", "PiB", "EiB",
24 "MHz", "GHz", "THz",
25 "AccessKit",
26 "CoAP", "CoreFoundation", "CoreGraphics", "CoreText",
27 "DevOps",
28 "Direct2D", "Direct3D", "DirectWrite", "DirectX",
29 "ECMAScript",
30 "GPLv2", "GPLv3",
31 "GitHub", "GitLab",
32 "IPv4", "IPv6",
33 "ClojureScript", "CoffeeScript", "JavaScript", "PostScript", "PureScript", "TypeScript",
34 "WebAssembly",
35 "NaN", "NaNs",
36 "OAuth", "GraphQL",
37 "OCaml",
38 "OpenAL", "OpenDNS", "OpenGL", "OpenMP", "OpenSSH", "OpenSSL", "OpenStreetMap", "OpenTelemetry",
39 "OpenType",
40 "WebGL", "WebGL2", "WebGPU", "WebRTC", "WebSocket", "WebTransport",
41 "WebP", "OpenExr", "YCbCr", "sRGB",
42 "TensorFlow",
43 "TrueType",
44 "iOS", "macOS", "FreeBSD", "NetBSD", "OpenBSD",
45 "TeX", "LaTeX", "BibTeX", "BibLaTeX",
46 "MinGW",
47 "CamelCase",
48];
49const DEFAULT_DISALLOWED_NAMES: &[&str] = &["foo", "baz", "quux"];
50const DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS: &[&str] = &["i", "j", "x", "y", "z", "w", "n"];
51const DEFAULT_ALLOWED_PREFIXES: &[&str] = &["to", "as", "into", "from", "try_into", "try_from"];
52const DEFAULT_ALLOWED_TRAITS_WITH_RENAMED_PARAMS: &[&str] =
53 &["core::convert::From", "core::convert::TryFrom", "core::str::FromStr"];
54const DEFAULT_MODULE_ITEM_ORDERING_GROUPS: &[(&str, &[SourceItemOrderingModuleItemKind])] = {
55 #[allow(clippy::enum_glob_use)] use SourceItemOrderingModuleItemKind::*;
57 &[
58 ("modules", &[ExternCrate, Mod, ForeignMod]),
59 ("use", &[Use]),
60 ("macros", &[Macro]),
61 ("global_asm", &[GlobalAsm]),
62 ("UPPER_SNAKE_CASE", &[Static, Const]),
63 ("PascalCase", &[TyAlias, Enum, Struct, Union, Trait, TraitAlias, Impl]),
64 ("lower_snake_case", &[Fn]),
65 ]
66};
67const DEFAULT_TRAIT_ASSOC_ITEM_KINDS_ORDER: &[SourceItemOrderingTraitAssocItemKind] = {
68 #[allow(clippy::enum_glob_use)] use SourceItemOrderingTraitAssocItemKind::*;
70 &[Const, Type, Fn]
71};
72const DEFAULT_SOURCE_ITEM_ORDERING: &[SourceItemOrderingCategory] = {
73 #[allow(clippy::enum_glob_use)] use SourceItemOrderingCategory::*;
75 &[Enum, Impl, Module, Struct, Trait]
76};
77
78#[derive(Default)]
80struct TryConf {
81 conf: Conf,
82 errors: Vec<ConfError>,
83 warnings: Vec<ConfError>,
84}
85
86impl TryConf {
87 fn from_toml_error(file: &SourceFile, error: &toml::de::Error) -> Self {
88 Self {
89 conf: Conf::default(),
90 errors: vec![ConfError::from_toml(file, error)],
91 warnings: vec![],
92 }
93 }
94}
95
96#[derive(Debug)]
97struct ConfError {
98 message: String,
99 suggestion: Option<Suggestion>,
100 span: Span,
101}
102
103impl ConfError {
104 fn from_toml(file: &SourceFile, error: &toml::de::Error) -> Self {
105 let span = error.span().unwrap_or(0..file.source_len.0 as usize);
106 Self::spanned(file, error.message(), None, span)
107 }
108
109 fn spanned(
110 file: &SourceFile,
111 message: impl Into<String>,
112 suggestion: Option<Suggestion>,
113 span: Range<usize>,
114 ) -> Self {
115 Self {
116 message: message.into(),
117 suggestion,
118 span: Span::new(
119 file.start_pos + BytePos::from_usize(span.start),
120 file.start_pos + BytePos::from_usize(span.end),
121 SyntaxContext::root(),
122 None,
123 ),
124 }
125 }
126}
127
128pub fn sanitize_explanation(raw_docs: &str) -> String {
130 let mut explanation = String::with_capacity(128);
132 let mut in_code = false;
133 for line in raw_docs.lines() {
134 let line = line.strip_prefix(' ').unwrap_or(line);
135
136 if let Some(lang) = line.strip_prefix("```") {
137 let tag = lang.split_once(',').map_or(lang, |(left, _)| left);
138 if !in_code && matches!(tag, "" | "rust" | "ignore" | "should_panic" | "no_run" | "compile_fail") {
139 explanation += "```rust\n";
140 } else {
141 explanation += line;
142 explanation.push('\n');
143 }
144 in_code = !in_code;
145 } else if !(in_code && line.starts_with("# ")) {
146 explanation += line;
147 explanation.push('\n');
148 }
149 }
150
151 explanation
152}
153
154macro_rules! wrap_option {
155 () => {
156 None
157 };
158 ($x:literal) => {
159 Some($x)
160 };
161}
162
163macro_rules! default_text {
164 ($value:expr) => {{
165 let mut text = String::new();
166 $value.serialize(toml::ser::ValueSerializer::new(&mut text)).unwrap();
167 text
168 }};
169 ($value:expr, $override:expr) => {
170 $override.to_string()
171 };
172}
173
174macro_rules! define_Conf {
175 ($(
176 $(#[doc = $doc:literal])+
177 $(#[conf_deprecated($dep:literal, $new_conf:ident)])?
178 $(#[default_text = $default_text:expr])?
179 $(#[lints($($for_lints:ident),* $(,)?)])?
180 $name:ident: $ty:ty = $default:expr,
181 )*) => {
182 pub struct Conf {
184 $($(#[cfg_attr(doc, doc = $doc)])+ pub $name: $ty,)*
185 }
186
187 mod defaults {
188 use super::*;
189 $(pub fn $name() -> $ty { $default })*
190 }
191
192 impl Default for Conf {
193 fn default() -> Self {
194 Self { $($name: defaults::$name(),)* }
195 }
196 }
197
198 #[derive(Deserialize)]
199 #[serde(field_identifier, rename_all = "kebab-case")]
200 #[allow(non_camel_case_types)]
201 enum Field { $($name,)* third_party, }
202
203 struct ConfVisitor<'a>(&'a SourceFile);
204
205 impl<'de> Visitor<'de> for ConfVisitor<'_> {
206 type Value = TryConf;
207
208 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
209 formatter.write_str("Conf")
210 }
211
212 fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> where V: MapAccess<'de> {
213 let mut errors = Vec::new();
214 let mut warnings = Vec::new();
215 $(let mut $name = None;)*
216 while let Some(name) = map.next_key::<toml::Spanned<String>>()? {
218 match Field::deserialize(name.get_ref().as_str().into_deserializer()) {
219 Err(e) => {
220 let e: FieldError = e;
221 errors.push(ConfError::spanned(self.0, e.error, e.suggestion, name.span()));
222 }
223 $(Ok(Field::$name) => {
224 $(warnings.push(ConfError::spanned(self.0, format!("deprecated field `{}`. {}", name.get_ref(), $dep), None, name.span()));)?
225 let raw_value = map.next_value::<toml::Spanned<toml::Value>>()?;
226 let value_span = raw_value.span();
227 match <$ty>::deserialize(raw_value.into_inner()) {
228 Err(e) => errors.push(ConfError::spanned(self.0, e.to_string().replace('\n', " ").trim(), None, value_span)),
229 Ok(value) => match $name {
230 Some(_) => {
231 errors.push(ConfError::spanned(self.0, format!("duplicate field `{}`", name.get_ref()), None, name.span()));
232 }
233 None => {
234 $name = Some(value);
235 $(match $new_conf {
238 Some(_) => errors.push(ConfError::spanned(self.0, concat!(
239 "duplicate field `", stringify!($new_conf),
240 "` (provided as `", stringify!($name), "`)"
241 ), None, name.span())),
242 None => $new_conf = $name.clone(),
243 })?
244 },
245 }
246 }
247 })*
248 Ok(Field::third_party) => drop(map.next_value::<IgnoredAny>())
250 }
251 }
252 let conf = Conf { $($name: $name.unwrap_or_else(defaults::$name),)* };
253 Ok(TryConf { conf, errors, warnings })
254 }
255 }
256
257 pub fn get_configuration_metadata() -> Vec<ClippyConfiguration> {
258 vec![$(
259 ClippyConfiguration {
260 name: stringify!($name).replace('_', "-"),
261 default: default_text!(defaults::$name() $(, $default_text)?),
262 lints: &[$($(stringify!($for_lints)),*)?],
263 doc: concat!($($doc, '\n',)*),
264 deprecation_reason: wrap_option!($($dep)?)
265 },
266 )*]
267 }
268 };
269}
270
271define_Conf! {
272 #[lints(absolute_paths)]
274 absolute_paths_allowed_crates: Vec<String> = Vec::new(),
275 #[lints(absolute_paths)]
278 absolute_paths_max_segments: u64 = 2,
279 #[lints(undocumented_unsafe_blocks)]
281 accept_comment_above_attributes: bool = true,
282 #[lints(undocumented_unsafe_blocks)]
284 accept_comment_above_statement: bool = true,
285 #[lints(modulo_arithmetic)]
287 allow_comparison_to_zero: bool = true,
288 #[lints(dbg_macro)]
290 allow_dbg_in_tests: bool = false,
291 #[lints(expect_used)]
293 allow_expect_in_tests: bool = false,
294 #[lints(indexing_slicing)]
296 allow_indexing_slicing_in_tests: bool = false,
297 #[lints(uninlined_format_args)]
299 allow_mixed_uninlined_format_args: bool = true,
300 #[lints(unnecessary_raw_string_hashes)]
302 allow_one_hash_in_raw_strings: bool = false,
303 #[lints(panic)]
305 allow_panic_in_tests: bool = false,
306 #[lints(print_stderr, print_stdout)]
308 allow_print_in_tests: bool = false,
309 #[lints(module_inception)]
311 allow_private_module_inception: bool = false,
312 #[lints(renamed_function_params)]
326 allow_renamed_params_for: Vec<String> =
327 DEFAULT_ALLOWED_TRAITS_WITH_RENAMED_PARAMS.iter().map(ToString::to_string).collect(),
328 #[lints(unwrap_used)]
330 allow_unwrap_in_tests: bool = false,
331 #[lints(useless_vec)]
333 allow_useless_vec_in_tests: bool = false,
334 #[lints(path_ends_with_ext)]
336 allowed_dotfiles: Vec<String> = Vec::default(),
337 #[lints(multiple_crate_versions)]
339 allowed_duplicate_crates: Vec<String> = Vec::new(),
340 #[lints(min_ident_chars)]
344 allowed_idents_below_min_chars: Vec<String> =
345 DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS.iter().map(ToString::to_string).collect(),
346 #[lints(module_name_repetitions)]
364 allowed_prefixes: Vec<String> = DEFAULT_ALLOWED_PREFIXES.iter().map(ToString::to_string).collect(),
365 #[lints(disallowed_script_idents)]
367 allowed_scripts: Vec<String> = vec!["Latin".to_string()],
368 #[lints(wildcard_imports)]
382 allowed_wildcard_imports: Vec<String> = Vec::new(),
383 #[lints(arithmetic_side_effects)]
398 arithmetic_side_effects_allowed: Vec<String> = <_>::default(),
399 #[lints(arithmetic_side_effects)]
414 arithmetic_side_effects_allowed_binary: Vec<(String, String)> = <_>::default(),
415 #[lints(arithmetic_side_effects)]
423 arithmetic_side_effects_allowed_unary: Vec<String> = <_>::default(),
424 #[lints(large_const_arrays, large_stack_arrays)]
426 array_size_threshold: u64 = 16 * 1024,
427 #[lints(
429 box_collection,
430 enum_variant_names,
431 large_types_passed_by_value,
432 linkedlist,
433 needless_pass_by_ref_mut,
434 option_option,
435 rc_buffer,
436 rc_mutex,
437 redundant_allocation,
438 ref_option,
439 single_call_fn,
440 trivially_copy_pass_by_ref,
441 unnecessary_box_returns,
442 unnecessary_wraps,
443 unused_self,
444 upper_case_acronyms,
445 vec_box,
446 wrong_self_convention,
447 )]
448 avoid_breaking_exported_api: bool = true,
449 #[lints(await_holding_invalid_type)]
451 await_holding_invalid_types: Vec<DisallowedPath> = Vec::new(),
452 #[conf_deprecated("Please use `disallowed-names` instead", disallowed_names)]
456 blacklisted_names: Vec<String> = Vec::new(),
457 #[lints(cargo_common_metadata)]
459 cargo_ignore_publish: bool = false,
460 #[lints(missing_errors_doc, missing_panics_doc, missing_safety_doc, unnecessary_safety_doc)]
462 check_private_items: bool = false,
463 #[lints(cognitive_complexity)]
465 cognitive_complexity_threshold: u64 = 25,
466 #[conf_deprecated("Please use `cognitive-complexity-threshold` instead", cognitive_complexity_threshold)]
470 cyclomatic_complexity_threshold: u64 = 25,
471 #[lints(disallowed_macros)]
473 disallowed_macros: Vec<DisallowedPath> = Vec::new(),
474 #[lints(disallowed_methods)]
476 disallowed_methods: Vec<DisallowedPath> = Vec::new(),
477 #[lints(disallowed_names)]
481 disallowed_names: Vec<String> = DEFAULT_DISALLOWED_NAMES.iter().map(ToString::to_string).collect(),
482 #[lints(disallowed_types)]
484 disallowed_types: Vec<DisallowedPath> = Vec::new(),
485 #[lints(doc_markdown)]
491 doc_valid_idents: Vec<String> = DEFAULT_DOC_VALID_IDENTS.iter().map(ToString::to_string).collect(),
492 #[lints(non_send_fields_in_send_ty)]
494 enable_raw_pointer_heuristic_for_send: bool = true,
495 #[lints(explicit_iter_loop)]
513 enforce_iter_loop_reborrow: bool = false,
514 #[lints(missing_enforced_import_renames)]
516 enforced_import_renames: Vec<Rename> = Vec::new(),
517 #[lints(enum_variant_names)]
519 enum_variant_name_threshold: u64 = 3,
520 #[lints(large_enum_variant)]
522 enum_variant_size_threshold: u64 = 200,
523 #[lints(excessive_nesting)]
525 excessive_nesting_threshold: u64 = 0,
526 #[lints(large_futures)]
528 future_size_threshold: u64 = 16 * 1024,
529 #[lints(borrow_interior_mutable_const, declare_interior_mutable_const, ifs_same_cond, mutable_key_type)]
531 ignore_interior_mutability: Vec<String> = Vec::from(["bytes::Bytes".into()]),
532 #[lints(result_large_err)]
534 large_error_threshold: u64 = 128,
535 #[lints(inconsistent_struct_constructor)]
554 lint_inconsistent_struct_field_initializers: bool = false,
555 #[lints(decimal_literal_representation)]
557 literal_representation_threshold: u64 = 16384,
558 #[lints(manual_let_else)]
561 matches_for_let_else: MatchLintBehaviour = MatchLintBehaviour::WellKnownTypes,
562 #[lints(fn_params_excessive_bools)]
564 max_fn_params_bools: u64 = 3,
565 #[lints(large_include_file)]
567 max_include_file_size: u64 = 1_000_000,
568 #[lints(struct_excessive_bools)]
570 max_struct_bools: u64 = 3,
571 #[lints(index_refutable_slice)]
575 max_suggested_slice_pattern_length: u64 = 3,
576 #[lints(type_repetition_in_bounds)]
578 max_trait_bounds: u64 = 3,
579 #[lints(min_ident_chars)]
581 min_ident_chars_threshold: u64 = 1,
582 #[lints(missing_docs_in_private_items)]
585 missing_docs_in_crate_items: bool = false,
586 #[lints(arbitrary_source_item_ordering)]
588 module_item_order_groupings: SourceItemOrderingModuleItemGroupings = DEFAULT_MODULE_ITEM_ORDERING_GROUPS.into(),
589 #[default_text = "current version"]
591 #[lints(
592 allow_attributes,
593 allow_attributes_without_reason,
594 almost_complete_range,
595 approx_constant,
596 assigning_clones,
597 borrow_as_ptr,
598 cast_abs_to_unsigned,
599 checked_conversions,
600 cloned_instead_of_copied,
601 collapsible_match,
602 collapsible_str_replace,
603 deprecated_cfg_attr,
604 derivable_impls,
605 err_expect,
606 filter_map_next,
607 from_over_into,
608 if_then_some_else_none,
609 index_refutable_slice,
610 iter_kv_map,
611 legacy_numeric_constants,
612 manual_bits,
613 manual_c_str_literals,
614 manual_clamp,
615 manual_hash_one,
616 manual_is_ascii_check,
617 manual_let_else,
618 manual_non_exhaustive,
619 manual_pattern_char_comparison,
620 manual_range_contains,
621 manual_rem_euclid,
622 manual_repeat_n,
623 manual_retain,
624 manual_slice_fill,
625 manual_split_once,
626 manual_str_repeat,
627 manual_strip,
628 manual_try_fold,
629 map_clone,
630 map_unwrap_or,
631 map_with_unused_argument_over_ranges,
632 match_like_matches_macro,
633 mem_replace_with_default,
634 missing_const_for_fn,
635 needless_borrow,
636 non_std_lazy_statics,
637 option_as_ref_deref,
638 option_map_unwrap_or,
639 ptr_as_ptr,
640 redundant_field_names,
641 redundant_static_lifetimes,
642 same_item_push,
643 seek_from_current,
644 seek_rewind,
645 transmute_ptr_to_ref,
646 tuple_array_conversions,
647 type_repetition_in_bounds,
648 unchecked_duration_subtraction,
649 uninlined_format_args,
650 unnecessary_lazy_evaluations,
651 unnested_or_patterns,
652 unused_trait_names,
653 use_self,
654 )]
655 msrv: Msrv = Msrv::empty(),
656 #[lints(large_types_passed_by_value)]
658 pass_by_value_size_limit: u64 = 256,
659 #[lints(pub_underscore_fields)]
662 pub_underscore_fields_behavior: PubUnderscoreFieldsBehaviour = PubUnderscoreFieldsBehaviour::PubliclyExported,
663 #[lints(semicolon_inside_block)]
665 semicolon_inside_block_ignore_singleline: bool = false,
666 #[lints(semicolon_outside_block)]
668 semicolon_outside_block_ignore_multiline: bool = false,
669 #[lints(many_single_char_names)]
671 single_char_binding_names_threshold: u64 = 4,
672 #[lints(arbitrary_source_item_ordering)]
674 source_item_ordering: SourceItemOrdering = DEFAULT_SOURCE_ITEM_ORDERING.into(),
675 #[lints(large_stack_frames)]
677 stack_size_threshold: u64 = 512_000,
678 #[lints(nonstandard_macro_braces)]
684 standard_macro_braces: Vec<MacroMatcher> = Vec::new(),
685 #[lints(struct_field_names)]
687 struct_field_name_threshold: u64 = 3,
688 #[lints(indexing_slicing)]
694 suppress_restriction_lint_in_const: bool = false,
695 #[lints(boxed_local, useless_vec)]
697 too_large_for_stack: u64 = 200,
698 #[lints(too_many_arguments)]
700 too_many_arguments_threshold: u64 = 7,
701 #[lints(too_many_lines)]
703 too_many_lines_threshold: u64 = 100,
704 #[lints(arbitrary_source_item_ordering)]
706 trait_assoc_item_kinds_order: SourceItemOrderingTraitAssocItemKinds = DEFAULT_TRAIT_ASSOC_ITEM_KINDS_ORDER.into(),
707 #[default_text = "target_pointer_width * 2"]
710 #[lints(trivially_copy_pass_by_ref)]
711 trivial_copy_size_limit: Option<u64> = None,
712 #[lints(type_complexity)]
714 type_complexity_threshold: u64 = 250,
715 #[lints(unnecessary_box_returns)]
717 unnecessary_box_size: u64 = 128,
718 #[lints(unreadable_literal)]
720 unreadable_literal_lint_fractions: bool = true,
721 #[lints(upper_case_acronyms)]
723 upper_case_acronyms_aggressive: bool = false,
724 #[lints(vec_box)]
726 vec_box_size_threshold: u64 = 4096,
727 #[lints(verbose_bit_mask)]
729 verbose_bit_mask_threshold: u64 = 1,
730 #[lints(wildcard_imports)]
732 warn_on_all_wildcard_imports: bool = false,
733 #[lints(macro_metavars_in_unsafe)]
735 warn_unsafe_macro_metavars_in_private_macros: bool = false,
736}
737
738pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> {
744 const CONFIG_FILE_NAMES: [&str; 2] = [".clippy.toml", "clippy.toml"];
746
747 let mut current = env::var_os("CLIPPY_CONF_DIR")
750 .or_else(|| env::var_os("CARGO_MANIFEST_DIR"))
751 .map_or_else(|| PathBuf::from("."), PathBuf::from)
752 .canonicalize()?;
753
754 let mut found_config: Option<PathBuf> = None;
755 let mut warnings = vec![];
756
757 loop {
758 for config_file_name in &CONFIG_FILE_NAMES {
759 if let Ok(config_file) = current.join(config_file_name).canonicalize() {
760 match fs::metadata(&config_file) {
761 Err(e) if e.kind() == io::ErrorKind::NotFound => {},
762 Err(e) => return Err(e),
763 Ok(md) if md.is_dir() => {},
764 Ok(_) => {
765 if let Some(ref found_config) = found_config {
767 warnings.push(format!(
768 "using config file `{}`, `{}` will be ignored",
769 found_config.display(),
770 config_file.display()
771 ));
772 } else {
773 found_config = Some(config_file);
774 }
775 },
776 }
777 }
778 }
779
780 if found_config.is_some() {
781 return Ok((found_config, warnings));
782 }
783
784 if !current.pop() {
786 return Ok((None, warnings));
787 }
788 }
789}
790
791fn deserialize(file: &SourceFile) -> TryConf {
792 match toml::de::Deserializer::new(file.src.as_ref().unwrap()).deserialize_map(ConfVisitor(file)) {
793 Ok(mut conf) => {
794 extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES);
795 extend_vec_if_indicator_present(&mut conf.conf.allowed_prefixes, DEFAULT_ALLOWED_PREFIXES);
796 extend_vec_if_indicator_present(
797 &mut conf.conf.allow_renamed_params_for,
798 DEFAULT_ALLOWED_TRAITS_WITH_RENAMED_PARAMS,
799 );
800 if conf.conf.allowed_idents_below_min_chars.iter().any(|e| e == "..") {
802 conf.conf
803 .allowed_idents_below_min_chars
804 .extend(DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS.iter().map(ToString::to_string));
805 }
806 if conf.conf.doc_valid_idents.iter().any(|e| e == "..") {
807 conf.conf
808 .doc_valid_idents
809 .extend(DEFAULT_DOC_VALID_IDENTS.iter().map(ToString::to_string));
810 }
811
812 conf
813 },
814 Err(e) => TryConf::from_toml_error(file, &e),
815 }
816}
817
818fn extend_vec_if_indicator_present(vec: &mut Vec<String>, default: &[&str]) {
819 if vec.contains(&"..".to_string()) {
820 vec.extend(default.iter().map(ToString::to_string));
821 }
822}
823
824impl Conf {
825 pub fn read(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>)>) -> &'static Conf {
826 static CONF: OnceLock<Conf> = OnceLock::new();
827 CONF.get_or_init(|| Conf::read_inner(sess, path))
828 }
829
830 fn read_inner(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>)>) -> Conf {
831 match path {
832 Ok((_, warnings)) => {
833 for warning in warnings {
834 sess.dcx().warn(warning.clone());
835 }
836 },
837 Err(error) => {
838 sess.dcx()
839 .err(format!("error finding Clippy's configuration file: {error}"));
840 },
841 }
842
843 let TryConf {
844 mut conf,
845 errors,
846 warnings,
847 } = match path {
848 Ok((Some(path), _)) => match sess.source_map().load_file(path) {
849 Ok(file) => deserialize(&file),
850 Err(error) => {
851 sess.dcx().err(format!("failed to read `{}`: {error}", path.display()));
852 TryConf::default()
853 },
854 },
855 _ => TryConf::default(),
856 };
857
858 conf.msrv.read_cargo(sess);
859
860 for error in errors {
862 let mut diag = sess.dcx().struct_span_err(
863 error.span,
864 format!("error reading Clippy's configuration file: {}", error.message),
865 );
866
867 if let Some(sugg) = error.suggestion {
868 diag.span_suggestion(error.span, sugg.message, sugg.suggestion, Applicability::MaybeIncorrect);
869 }
870
871 diag.emit();
872 }
873
874 for warning in warnings {
875 sess.dcx().span_warn(
876 warning.span,
877 format!("error reading Clippy's configuration file: {}", warning.message),
878 );
879 }
880
881 conf
882 }
883}
884
885const SEPARATOR_WIDTH: usize = 4;
886
887#[derive(Debug)]
888struct FieldError {
889 error: String,
890 suggestion: Option<Suggestion>,
891}
892
893#[derive(Debug)]
894struct Suggestion {
895 message: &'static str,
896 suggestion: &'static str,
897}
898
899impl std::error::Error for FieldError {}
900
901impl Display for FieldError {
902 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
903 f.pad(&self.error)
904 }
905}
906
907impl serde::de::Error for FieldError {
908 fn custom<T: Display>(msg: T) -> Self {
909 Self {
910 error: msg.to_string(),
911 suggestion: None,
912 }
913 }
914
915 fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
916 use fmt::Write;
919
920 let mut expected = expected.to_vec();
921 expected.sort_unstable();
922
923 let (rows, column_widths) = calculate_dimensions(&expected);
924
925 let mut msg = format!("unknown field `{field}`, expected one of");
926 for row in 0..rows {
927 writeln!(msg).unwrap();
928 for (column, column_width) in column_widths.iter().copied().enumerate() {
929 let index = column * rows + row;
930 let field = expected.get(index).copied().unwrap_or_default();
931 write!(msg, "{:SEPARATOR_WIDTH$}{field:column_width$}", " ").unwrap();
932 }
933 }
934
935 let suggestion = expected
936 .iter()
937 .filter_map(|expected| {
938 let dist = edit_distance(field, expected, 4)?;
939 Some((dist, expected))
940 })
941 .min_by_key(|&(dist, _)| dist)
942 .map(|(_, suggestion)| Suggestion {
943 message: "perhaps you meant",
944 suggestion,
945 });
946
947 Self { error: msg, suggestion }
948 }
949}
950
951fn calculate_dimensions(fields: &[&str]) -> (usize, Vec<usize>) {
952 let columns = env::var("CLIPPY_TERMINAL_WIDTH")
953 .ok()
954 .and_then(|s| <usize as FromStr>::from_str(&s).ok())
955 .map_or(1, |terminal_width| {
956 let max_field_width = fields.iter().map(|field| field.len()).max().unwrap();
957 cmp::max(1, terminal_width / (SEPARATOR_WIDTH + max_field_width))
958 });
959
960 let rows = fields.len().div_ceil(columns);
961
962 let column_widths = (0..columns)
963 .map(|column| {
964 if column < columns - 1 {
965 (0..rows)
966 .map(|row| {
967 let index = column * rows + row;
968 let field = fields.get(index).copied().unwrap_or_default();
969 field.len()
970 })
971 .max()
972 .unwrap()
973 } else {
974 0
976 }
977 })
978 .collect::<Vec<_>>();
979
980 (rows, column_widths)
981}
982
983#[cfg(test)]
984mod tests {
985 use serde::de::IgnoredAny;
986 use std::collections::{HashMap, HashSet};
987 use std::fs;
988 use walkdir::WalkDir;
989
990 #[test]
991 fn configs_are_tested() {
992 let mut names: HashSet<String> = crate::get_configuration_metadata()
993 .into_iter()
994 .map(|meta| meta.name.replace('_', "-"))
995 .collect();
996
997 let toml_files = WalkDir::new("../tests")
998 .into_iter()
999 .map(Result::unwrap)
1000 .filter(|entry| entry.file_name() == "clippy.toml");
1001
1002 for entry in toml_files {
1003 let file = fs::read_to_string(entry.path()).unwrap();
1004 #[allow(clippy::zero_sized_map_values)]
1005 if let Ok(map) = toml::from_str::<HashMap<String, IgnoredAny>>(&file) {
1006 for name in map.keys() {
1007 names.remove(name.as_str());
1008 }
1009 }
1010 }
1011
1012 assert!(
1013 names.is_empty(),
1014 "Configuration variable lacks test: {names:?}\nAdd a test to `tests/ui-toml`"
1015 );
1016 }
1017}