Skip to content

Commit f6fc5fa

Browse files
committed
allow multiple clippy.tomls and make them inherit from eachother
1 parent a3b185b commit f6fc5fa

File tree

10 files changed

+183
-70
lines changed

10 files changed

+183
-70
lines changed

clippy.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
# NOTE: Any changes here must be reversed in `tests/clippy.toml`
12
avoid-breaking-exported-api = false

clippy_lints/src/lib.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ extern crate clippy_utils;
4848
#[macro_use]
4949
extern crate declare_clippy_lint;
5050

51-
use std::io;
51+
use std::error::Error;
5252
use std::path::PathBuf;
5353

5454
use clippy_utils::msrvs::Msrv;
@@ -339,7 +339,7 @@ mod zero_div_zero;
339339
mod zero_sized_map_values;
340340
// end lints modules, do not remove this comment, it’s used in `update_lints`
341341

342-
pub use crate::utils::conf::{lookup_conf_file, Conf};
342+
pub use crate::utils::conf::{lookup_conf_files, Conf};
343343
use crate::utils::{
344344
conf::{metadata::get_configuration_metadata, TryConf},
345345
FindAll,
@@ -362,33 +362,39 @@ pub fn register_pre_expansion_lints(store: &mut rustc_lint::LintStore, sess: &Se
362362
}
363363

364364
#[doc(hidden)]
365-
pub fn read_conf(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>)>) -> Conf {
365+
#[expect(clippy::type_complexity)]
366+
pub fn read_conf(sess: &Session, path: &Result<(Vec<PathBuf>, Vec<String>), Box<dyn Error + Send + Sync>>) -> Conf {
366367
if let Ok((_, warnings)) = path {
367368
for warning in warnings {
368369
sess.warn(warning.clone());
369370
}
370371
}
371-
let file_name = match path {
372-
Ok((Some(path), _)) => path,
373-
Ok((None, _)) => return Conf::default(),
372+
let file_names = match path {
373+
Ok((file_names, _)) if file_names.is_empty() => return Conf::default(),
374+
Ok((file_names, _)) => file_names,
374375
Err(error) => {
375376
sess.err(format!("error finding Clippy's configuration file: {error}"));
376377
return Conf::default();
377378
},
378379
};
379380

380-
let TryConf { conf, errors, warnings } = utils::conf::read(sess, file_name);
381+
let TryConf { conf, errors, warnings } = utils::conf::read(sess, file_names);
381382
// all conf errors are non-fatal, we just use the default conf in case of error
382383
for error in errors {
383384
if let Some(span) = error.span {
384385
sess.span_err(
385386
span,
386387
format!("error reading Clippy's configuration file: {}", error.message),
387388
);
388-
} else {
389+
} else if let Some(file) = error.file {
389390
sess.err(format!(
390391
"error reading Clippy's configuration file `{}`: {}",
391-
file_name.display(),
392+
file.display(),
393+
error.message
394+
));
395+
} else {
396+
sess.err(format!(
397+
"error reading any of Clippy's configuration files: {}",
392398
error.message
393399
));
394400
}
@@ -400,10 +406,15 @@ pub fn read_conf(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>
400406
span,
401407
format!("error reading Clippy's configuration file: {}", warning.message),
402408
);
403-
} else {
404-
sess.warn(format!(
409+
} else if let Some(file) = warning.file {
410+
sess.err(format!(
405411
"error reading Clippy's configuration file `{}`: {}",
406-
file_name.display(),
412+
file.display(),
413+
warning.message
414+
));
415+
} else {
416+
sess.err(format!(
417+
"error reading any of Clippy's configuration files: {}",
407418
warning.message
408419
));
409420
}

clippy_lints/src/utils/conf.rs

Lines changed: 88 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
#![allow(clippy::module_name_repetitions)]
44

55
use rustc_session::Session;
6-
use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
6+
use rustc_span::{BytePos, FileName, Pos, SourceFile, Span, SyntaxContext};
77
use serde::de::{Deserializer, IgnoredAny, IntoDeserializer, MapAccess, Visitor};
88
use serde::Deserialize;
9+
use std::error::Error;
910
use std::fmt::{Debug, Display, Formatter};
1011
use std::ops::Range;
11-
use std::path::{Path, PathBuf};
12+
use std::path::PathBuf;
1213
use std::str::FromStr;
1314
use std::{cmp, env, fmt, fs, io};
1415

@@ -100,38 +101,50 @@ impl From<io::Error> for TryConf {
100101
#[derive(Debug)]
101102
pub struct ConfError {
102103
pub message: String,
104+
pub file: Option<PathBuf>,
103105
pub span: Option<Span>,
104106
}
105107

106108
impl ConfError {
107109
fn from_toml(file: &SourceFile, error: &toml::de::Error) -> Self {
108110
if let Some(span) = error.span() {
109-
Self::spanned(file, error.message(), span)
110-
} else {
111-
Self {
112-
message: error.message().to_string(),
113-
span: None,
114-
}
111+
return Self::spanned(file, error.message(), span);
112+
} else if let FileName::Real(filename) = &file.name
113+
&& let Some(filename) = filename.local_path()
114+
{
115+
return Self {
116+
message: error.message().to_string(),
117+
file: Some(filename.to_owned()),
118+
span: None,
119+
};
115120
}
121+
122+
unreachable!();
116123
}
117124

118125
fn spanned(file: &SourceFile, message: impl Into<String>, span: Range<usize>) -> Self {
119-
Self {
120-
message: message.into(),
121-
span: Some(Span::new(
122-
file.start_pos + BytePos::from_usize(span.start),
123-
file.start_pos + BytePos::from_usize(span.end),
124-
SyntaxContext::root(),
125-
None,
126-
)),
126+
if let FileName::Real(filename) = &file.name && let Some(filename) = filename.local_path() {
127+
return Self {
128+
message: message.into(),
129+
file: Some(filename.to_owned()),
130+
span: Some(Span::new(
131+
file.start_pos + BytePos::from_usize(span.start),
132+
file.start_pos + BytePos::from_usize(span.end),
133+
SyntaxContext::root(),
134+
None,
135+
)),
136+
};
127137
}
138+
139+
unreachable!();
128140
}
129141
}
130142

131143
impl From<io::Error> for ConfError {
132144
fn from(value: io::Error) -> Self {
133145
Self {
134146
message: value.to_string(),
147+
file: None,
135148
span: None,
136149
}
137150
}
@@ -144,6 +157,7 @@ macro_rules! define_Conf {
144157
($name:ident: $ty:ty = $default:expr),
145158
)*) => {
146159
/// Clippy lint configuration
160+
#[derive(Deserialize)]
147161
pub struct Conf {
148162
$($(#[doc = $doc])+ pub $name: $ty,)*
149163
}
@@ -158,15 +172,15 @@ macro_rules! define_Conf {
158172
}
159173
}
160174

175+
#[allow(non_camel_case_types)]
161176
#[derive(Deserialize)]
162177
#[serde(field_identifier, rename_all = "kebab-case")]
163-
#[allow(non_camel_case_types)]
164178
enum Field { $($name,)* third_party, }
165179

166-
struct ConfVisitor<'a>(&'a SourceFile);
180+
struct ConfVisitor<'a>(&'a SourceFile, &'a mut TryConf);
167181

168182
impl<'de> Visitor<'de> for ConfVisitor<'_> {
169-
type Value = TryConf;
183+
type Value = ();
170184

171185
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172186
formatter.write_str("Conf")
@@ -210,8 +224,14 @@ macro_rules! define_Conf {
210224
Ok(Field::third_party) => drop(map.next_value::<IgnoredAny>())
211225
}
212226
}
213-
let conf = Conf { $($name: $name.unwrap_or_else(defaults::$name),)* };
214-
Ok(TryConf { conf, errors, warnings })
227+
$(
228+
if let Some($name) = $name {
229+
self.1.conf.$name = $name;
230+
}
231+
)*
232+
self.1.errors.extend(errors);
233+
self.1.warnings.extend(warnings);
234+
Ok(())
215235
}
216236
}
217237

@@ -536,12 +556,17 @@ define_Conf! {
536556
(min_ident_chars_threshold: u64 = 1),
537557
}
538558

539-
/// Search for the configuration file.
559+
/// Search for any configuration files. The index corresponds to the priority; the higher the index,
560+
/// the lower the priority.
561+
///
562+
/// Note: It's up to the caller to reverse the priority of configuration files, otherwise the last
563+
/// configuration file will have the highest priority.
540564
///
541565
/// # Errors
542566
///
543-
/// Returns any unexpected filesystem error encountered when searching for the config file
544-
pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> {
567+
/// Returns any unexpected filesystem error encountered when searching for the config file or when
568+
/// running `cargo metadata`.
569+
pub fn lookup_conf_files() -> Result<(Vec<PathBuf>, Vec<String>), Box<dyn Error + Send + Sync>> {
545570
/// Possible filename to search for.
546571
const CONFIG_FILE_NAMES: [&str; 2] = [".clippy.toml", "clippy.toml"];
547572

@@ -552,66 +577,74 @@ pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> {
552577
.map_or_else(|| PathBuf::from("."), PathBuf::from)
553578
.canonicalize()?;
554579

555-
let mut found_config: Option<PathBuf> = None;
580+
let mut found_configs: Vec<PathBuf> = vec![];
556581
let mut warnings = vec![];
557582

583+
// TODO: This will continue searching even outside of the workspace, and even add an erroneous
584+
// configuration file to the list! Is it worth fixing this? `workspace_root` on `cargo metadata`
585+
// doesn't work for clippy_lints' clippy.toml in cwd. We likely can't just use cwd as what if
586+
// it's called in src?
558587
loop {
559588
for config_file_name in &CONFIG_FILE_NAMES {
560589
if let Ok(config_file) = current.join(config_file_name).canonicalize() {
561590
match fs::metadata(&config_file) {
562591
Err(e) if e.kind() == io::ErrorKind::NotFound => {},
563-
Err(e) => return Err(e),
592+
Err(e) => return Err(e.into()),
564593
Ok(md) if md.is_dir() => {},
565594
Ok(_) => {
566-
// warn if we happen to find two config files #8323
567-
if let Some(ref found_config) = found_config {
595+
// Warn if we happen to find two config files #8323
596+
if let [.., last_config] = &*found_configs
597+
&& let Some(last_config_dir) = last_config.parent()
598+
&& let Some(config_file_dir) = config_file.parent()
599+
&& last_config_dir == config_file_dir
600+
{
568601
warnings.push(format!(
569602
"using config file `{}`, `{}` will be ignored",
570-
found_config.display(),
603+
last_config.display(),
571604
config_file.display()
572605
));
573606
} else {
574-
found_config = Some(config_file);
607+
found_configs.push(config_file);
575608
}
576609
},
577610
}
578611
}
579612
}
580613

581-
if found_config.is_some() {
582-
return Ok((found_config, warnings));
583-
}
584-
585-
// If the current directory has no parent, we're done searching.
586614
if !current.pop() {
587-
return Ok((None, warnings));
615+
break;
588616
}
589617
}
618+
619+
Ok((found_configs, warnings))
590620
}
591621

592622
/// Read the `toml` configuration file.
593623
///
594624
/// In case of error, the function tries to continue as much as possible.
595-
pub fn read(sess: &Session, path: &Path) -> TryConf {
596-
let file = match sess.source_map().load_file(path) {
597-
Err(e) => return e.into(),
598-
Ok(file) => file,
599-
};
600-
match toml::de::Deserializer::new(file.src.as_ref().unwrap()).deserialize_map(ConfVisitor(&file)) {
601-
Ok(mut conf) => {
602-
extend_vec_if_indicator_present(&mut conf.conf.doc_valid_idents, DEFAULT_DOC_VALID_IDENTS);
603-
extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES);
604-
// TODO: THIS SHOULD BE TESTED, this comment will be gone soon
605-
if conf.conf.allowed_idents_below_min_chars.contains(&"..".to_owned()) {
606-
conf.conf
607-
.allowed_idents_below_min_chars
608-
.extend(DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS.iter().map(ToString::to_string));
609-
}
610-
611-
conf
612-
},
613-
Err(e) => TryConf::from_toml_error(&file, &e),
625+
pub fn read(sess: &Session, paths: &[PathBuf]) -> TryConf {
626+
let mut conf = TryConf::default();
627+
for file in paths.iter().rev() {
628+
let file = match sess.source_map().load_file(file) {
629+
Err(e) => return e.into(),
630+
Ok(file) => file,
631+
};
632+
match toml::de::Deserializer::new(file.src.as_ref().unwrap()).deserialize_map(ConfVisitor(&file, &mut conf)) {
633+
Ok(_) => {
634+
extend_vec_if_indicator_present(&mut conf.conf.doc_valid_idents, DEFAULT_DOC_VALID_IDENTS);
635+
extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES);
636+
// TODO: THIS SHOULD BE TESTED, this comment will be gone soon
637+
if conf.conf.allowed_idents_below_min_chars.contains(&"..".to_owned()) {
638+
conf.conf
639+
.allowed_idents_below_min_chars
640+
.extend(DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS.iter().map(ToString::to_string));
641+
}
642+
},
643+
Err(e) => return TryConf::from_toml_error(&file, &e),
644+
}
614645
}
646+
647+
conf
615648
}
616649

617650
fn extend_vec_if_indicator_present(vec: &mut Vec<String>, default: &[&str]) {

src/driver.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,9 @@ struct ClippyCallbacks {
119119
}
120120

121121
impl rustc_driver::Callbacks for ClippyCallbacks {
122-
// JUSTIFICATION: necessary in clippy driver to set `mir_opt_level`
123-
#[allow(rustc::bad_opt_access)]
122+
#[allow(rustc::bad_opt_access, reason = "necessary in clippy driver to set `mir_opt_level`")]
124123
fn config(&mut self, config: &mut interface::Config) {
125-
let conf_path = clippy_lints::lookup_conf_file();
124+
let conf_path = clippy_lints::lookup_conf_files();
126125
let previous = config.register_lints.take();
127126
let clippy_args_var = self.clippy_args_var.take();
128127
config.parse_sess_created = Some(Box::new(move |parse_sess| {

tests/clippy.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
# default config for tests, overrides clippy.toml at the project root
2+
avoid-breaking-exported-api = true
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
too-many-lines-threshold = 1
2+
single-char-binding-names-threshold = 1
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "inherit-config"
3+
version = "0.1.0"
4+
edition = "2018"
5+
publish = false
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
too-many-lines-threshold = 3
2+
msrv = "1.1"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//@compile-flags: --crate-name=inherit_config
2+
#![warn(clippy::many_single_char_names, clippy::too_many_lines)]
3+
4+
fn main() {
5+
// Inherited from outer config
6+
let (a, b, c) = (1, 2, 3);
7+
_ = ();
8+
_ = ();
9+
_ = ();
10+
// Too many lines, not 1 but 3 because of inner config
11+
}

0 commit comments

Comments
 (0)