Skip to content

Commit 560b6b7

Browse files
committed
allow multiple clippy.tomls and make them inherit from eachother
1 parent f93df98 commit 560b6b7

File tree

10 files changed

+177
-64
lines changed

10 files changed

+177
-64
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;
@@ -337,7 +337,7 @@ mod zero_div_zero;
337337
mod zero_sized_map_values;
338338
// end lints modules, do not remove this comment, it’s used in `update_lints`
339339

340-
pub use crate::utils::conf::{lookup_conf_file, Conf};
340+
pub use crate::utils::conf::{lookup_conf_files, Conf};
341341
use crate::utils::{
342342
conf::{metadata::get_configuration_metadata, TryConf},
343343
FindAll,
@@ -360,33 +360,39 @@ pub fn register_pre_expansion_lints(store: &mut rustc_lint::LintStore, sess: &Se
360360
}
361361

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

378-
let TryConf { conf, errors, warnings } = utils::conf::read(sess, file_name);
379+
let TryConf { conf, errors, warnings } = utils::conf::read(sess, file_names);
379380
// all conf errors are non-fatal, we just use the default conf in case of error
380381
for error in errors {
381382
if let Some(span) = error.span {
382383
sess.span_err(
383384
span,
384385
format!("error reading Clippy's configuration file: {}", error.message),
385386
);
386-
} else {
387+
} else if let Some(file) = error.file {
387388
sess.err(format!(
388389
"error reading Clippy's configuration file `{}`: {}",
389-
file_name.display(),
390+
file.display(),
391+
error.message
392+
));
393+
} else {
394+
sess.err(format!(
395+
"error reading any of Clippy's configuration files: {}",
390396
error.message
391397
));
392398
}
@@ -398,10 +404,15 @@ pub fn read_conf(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>
398404
span,
399405
format!("error reading Clippy's configuration file: {}", warning.message),
400406
);
401-
} else {
402-
sess.warn(format!(
407+
} else if let Some(file) = warning.file {
408+
sess.err(format!(
403409
"error reading Clippy's configuration file `{}`: {}",
404-
file_name.display(),
410+
file.display(),
411+
warning.message
412+
));
413+
} else {
414+
sess.err(format!(
415+
"error reading any of Clippy's configuration files: {}",
405416
warning.message
406417
));
407418
}

clippy_lints/src/utils/conf.rs

Lines changed: 82 additions & 49 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

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

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

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

130142
impl From<io::Error> for ConfError {
131143
fn from(value: io::Error) -> Self {
132144
Self {
133145
message: value.to_string(),
146+
file: None,
134147
span: None,
135148
}
136149
}
@@ -143,6 +156,7 @@ macro_rules! define_Conf {
143156
($name:ident: $ty:ty = $default:expr),
144157
)*) => {
145158
/// Clippy lint configuration
159+
#[derive(Deserialize)]
146160
pub struct Conf {
147161
$($(#[doc = $doc])+ pub $name: $ty,)*
148162
}
@@ -157,15 +171,15 @@ macro_rules! define_Conf {
157171
}
158172
}
159173

174+
#[allow(non_camel_case_types)]
160175
#[derive(Deserialize)]
161176
#[serde(field_identifier, rename_all = "kebab-case")]
162-
#[allow(non_camel_case_types)]
163177
enum Field { $($name,)* third_party, }
164178

165-
struct ConfVisitor<'a>(&'a SourceFile);
179+
struct ConfVisitor<'a>(&'a SourceFile, &'a mut TryConf);
166180

167181
impl<'de> Visitor<'de> for ConfVisitor<'_> {
168-
type Value = TryConf;
182+
type Value = ();
169183

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

@@ -524,12 +544,17 @@ define_Conf! {
524544
(allow_private_module_inception: bool = false),
525545
}
526546

527-
/// Search for the configuration file.
547+
/// Search for any configuration files. The index corresponds to the priority; the higher the index,
548+
/// the lower the priority.
549+
///
550+
/// Note: It's up to the caller to reverse the priority of configuration files, otherwise the last
551+
/// configuration file will have the highest priority.
528552
///
529553
/// # Errors
530554
///
531-
/// Returns any unexpected filesystem error encountered when searching for the config file
532-
pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> {
555+
/// Returns any unexpected filesystem error encountered when searching for the config file or when
556+
/// running `cargo metadata`.
557+
pub fn lookup_conf_files() -> Result<(Vec<PathBuf>, Vec<String>), Box<dyn Error + Send + Sync>> {
533558
/// Possible filename to search for.
534559
const CONFIG_FILE_NAMES: [&str; 2] = [".clippy.toml", "clippy.toml"];
535560

@@ -540,60 +565,68 @@ pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> {
540565
.map_or_else(|| PathBuf::from("."), PathBuf::from)
541566
.canonicalize()?;
542567

543-
let mut found_config: Option<PathBuf> = None;
568+
let mut found_configs: Vec<PathBuf> = vec![];
544569
let mut warnings = vec![];
545570

571+
// TODO: This will continue searching even outside of the workspace, and even add an erroneous
572+
// configuration file to the list! Is it worth fixing this? `workspace_root` on `cargo metadata`
573+
// doesn't work for clippy_lints' clippy.toml in cwd. We likely can't just use cwd as what if
574+
// it's called in src?
546575
loop {
547576
for config_file_name in &CONFIG_FILE_NAMES {
548577
if let Ok(config_file) = current.join(config_file_name).canonicalize() {
549578
match fs::metadata(&config_file) {
550579
Err(e) if e.kind() == io::ErrorKind::NotFound => {},
551-
Err(e) => return Err(e),
580+
Err(e) => return Err(e.into()),
552581
Ok(md) if md.is_dir() => {},
553582
Ok(_) => {
554-
// warn if we happen to find two config files #8323
555-
if let Some(ref found_config) = found_config {
583+
// Warn if we happen to find two config files #8323
584+
if let [.., last_config] = &*found_configs
585+
&& let Some(last_config_dir) = last_config.parent()
586+
&& let Some(config_file_dir) = config_file.parent()
587+
&& last_config_dir == config_file_dir
588+
{
556589
warnings.push(format!(
557590
"using config file `{}`, `{}` will be ignored",
558-
found_config.display(),
591+
last_config.display(),
559592
config_file.display()
560593
));
561594
} else {
562-
found_config = Some(config_file);
595+
found_configs.push(config_file);
563596
}
564597
},
565598
}
566599
}
567600
}
568601

569-
if found_config.is_some() {
570-
return Ok((found_config, warnings));
571-
}
572-
573-
// If the current directory has no parent, we're done searching.
574602
if !current.pop() {
575-
return Ok((None, warnings));
603+
break;
576604
}
577605
}
606+
607+
Ok((found_configs, warnings))
578608
}
579609

580610
/// Read the `toml` configuration file.
581611
///
582612
/// In case of error, the function tries to continue as much as possible.
583-
pub fn read(sess: &Session, path: &Path) -> TryConf {
584-
let file = match sess.source_map().load_file(path) {
585-
Err(e) => return e.into(),
586-
Ok(file) => file,
587-
};
588-
match toml::de::Deserializer::new(file.src.as_ref().unwrap()).deserialize_map(ConfVisitor(&file)) {
589-
Ok(mut conf) => {
590-
extend_vec_if_indicator_present(&mut conf.conf.doc_valid_idents, DEFAULT_DOC_VALID_IDENTS);
591-
extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES);
592-
593-
conf
594-
},
595-
Err(e) => TryConf::from_toml_error(&file, &e),
613+
pub fn read(sess: &Session, paths: &[PathBuf]) -> TryConf {
614+
let mut conf = TryConf::default();
615+
for file in paths.iter().rev() {
616+
let file = match sess.source_map().load_file(file) {
617+
Err(e) => return e.into(),
618+
Ok(file) => file,
619+
};
620+
match toml::de::Deserializer::new(file.src.as_ref().unwrap()).deserialize_map(ConfVisitor(&file, &mut conf)) {
621+
Ok(_) => {
622+
extend_vec_if_indicator_present(&mut conf.conf.doc_valid_idents, DEFAULT_DOC_VALID_IDENTS);
623+
extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES);
624+
},
625+
Err(e) => return TryConf::from_toml_error(&file, &e),
626+
}
596627
}
628+
629+
conf
597630
}
598631

599632
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)