Skip to content

Commit 50ab3ce

Browse files
committed
Auto merge of rust-lang#10607 - beetrees:toml-spans, r=giraffate
Add spans to `clippy.toml` error messages Adds spans to errors and warnings encountered when parsing `clippy.toml`. changelog: Errors and warnings generated when parsing `clippy.toml` now point to the location in the TOML file the error/warning occurred.
2 parents 9524cff + 6f13a37 commit 50ab3ce

File tree

20 files changed

+278
-141
lines changed

20 files changed

+278
-141
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ termize = "0.1"
3030
compiletest_rs = { version = "0.10", features = ["tmp"] }
3131
tester = "0.9"
3232
regex = "1.5"
33-
toml = "0.5"
33+
toml = "0.7.3"
3434
walkdir = "2.3"
3535
# This is used by the `collect-metadata` alias.
3636
filetime = "0.2"

clippy_lints/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ regex-syntax = "0.7"
2121
serde = { version = "1.0", features = ["derive"] }
2222
serde_json = { version = "1.0", optional = true }
2323
tempfile = { version = "3.3.0", optional = true }
24-
toml = "0.5"
24+
toml = "0.7.3"
2525
unicode-normalization = "0.1"
2626
unicode-script = { version = "0.5", default-features = false }
2727
semver = "1.0"

clippy_lints/src/lib.rs

+26-13
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ mod zero_sized_map_values;
336336

337337
pub use crate::utils::conf::{lookup_conf_file, Conf};
338338
use crate::utils::{
339-
conf::{format_error, metadata::get_configuration_metadata, TryConf},
339+
conf::{metadata::get_configuration_metadata, TryConf},
340340
FindAll,
341341
};
342342

@@ -372,23 +372,36 @@ pub fn read_conf(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>
372372
},
373373
};
374374

375-
let TryConf { conf, errors, warnings } = utils::conf::read(file_name);
375+
let TryConf { conf, errors, warnings } = utils::conf::read(sess, file_name);
376376
// all conf errors are non-fatal, we just use the default conf in case of error
377377
for error in errors {
378-
sess.err(format!(
379-
"error reading Clippy's configuration file `{}`: {}",
380-
file_name.display(),
381-
format_error(error)
382-
));
378+
if let Some(span) = error.span {
379+
sess.span_err(
380+
span,
381+
format!("error reading Clippy's configuration file: {}", error.message),
382+
);
383+
} else {
384+
sess.err(format!(
385+
"error reading Clippy's configuration file `{}`: {}",
386+
file_name.display(),
387+
error.message
388+
));
389+
}
383390
}
384391

385392
for warning in warnings {
386-
sess.struct_warn(format!(
387-
"error reading Clippy's configuration file `{}`: {}",
388-
file_name.display(),
389-
format_error(warning)
390-
))
391-
.emit();
393+
if let Some(span) = warning.span {
394+
sess.span_warn(
395+
span,
396+
format!("error reading Clippy's configuration file: {}", warning.message),
397+
);
398+
} else {
399+
sess.warn(format!(
400+
"error reading Clippy's configuration file `{}`: {}",
401+
file_name.display(),
402+
warning.message
403+
));
404+
}
392405
}
393406

394407
conf

clippy_lints/src/nonstandard_macro_braces.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ impl<'de> Deserialize<'de> for MacroMatcher {
241241
V: de::MapAccess<'de>,
242242
{
243243
let mut name = None;
244-
let mut brace: Option<&str> = None;
244+
let mut brace: Option<String> = None;
245245
while let Some(key) = map.next_key()? {
246246
match key {
247247
Field::Name => {

clippy_lints/src/utils/conf.rs

+112-95
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
33
#![allow(clippy::module_name_repetitions)]
44

5+
use rustc_session::Session;
6+
use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
57
use serde::de::{Deserializer, IgnoredAny, IntoDeserializer, MapAccess, Visitor};
68
use serde::Deserialize;
7-
use std::error::Error;
9+
use std::fmt::{Debug, Display, Formatter};
10+
use std::ops::Range;
811
use std::path::{Path, PathBuf};
912
use std::str::FromStr;
10-
use std::{cmp, env, fmt, fs, io, iter};
13+
use std::{cmp, env, fmt, fs, io};
1114

1215
#[rustfmt::skip]
1316
const DEFAULT_DOC_VALID_IDENTS: &[&str] = &[
@@ -67,33 +70,70 @@ impl DisallowedPath {
6770
#[derive(Default)]
6871
pub struct TryConf {
6972
pub conf: Conf,
70-
pub errors: Vec<Box<dyn Error>>,
71-
pub warnings: Vec<Box<dyn Error>>,
73+
pub errors: Vec<ConfError>,
74+
pub warnings: Vec<ConfError>,
7275
}
7376

7477
impl TryConf {
75-
fn from_error(error: impl Error + 'static) -> Self {
78+
fn from_toml_error(file: &SourceFile, error: &toml::de::Error) -> Self {
79+
ConfError::from_toml(file, error).into()
80+
}
81+
}
82+
83+
impl From<ConfError> for TryConf {
84+
fn from(value: ConfError) -> Self {
7685
Self {
7786
conf: Conf::default(),
78-
errors: vec![Box::new(error)],
87+
errors: vec![value],
7988
warnings: vec![],
8089
}
8190
}
8291
}
8392

93+
impl From<io::Error> for TryConf {
94+
fn from(value: io::Error) -> Self {
95+
ConfError::from(value).into()
96+
}
97+
}
98+
8499
#[derive(Debug)]
85-
struct ConfError(String);
100+
pub struct ConfError {
101+
pub message: String,
102+
pub span: Option<Span>,
103+
}
104+
105+
impl ConfError {
106+
fn from_toml(file: &SourceFile, error: &toml::de::Error) -> Self {
107+
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+
}
114+
}
115+
}
86116

87-
impl fmt::Display for ConfError {
88-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89-
<String as fmt::Display>::fmt(&self.0, f)
117+
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+
)),
126+
}
90127
}
91128
}
92129

93-
impl Error for ConfError {}
94-
95-
fn conf_error(s: impl Into<String>) -> Box<dyn Error> {
96-
Box::new(ConfError(s.into()))
130+
impl From<io::Error> for ConfError {
131+
fn from(value: io::Error) -> Self {
132+
Self {
133+
message: value.to_string(),
134+
span: None,
135+
}
136+
}
97137
}
98138

99139
macro_rules! define_Conf {
@@ -117,20 +157,14 @@ macro_rules! define_Conf {
117157
}
118158
}
119159

120-
impl<'de> Deserialize<'de> for TryConf {
121-
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> {
122-
deserializer.deserialize_map(ConfVisitor)
123-
}
124-
}
125-
126160
#[derive(Deserialize)]
127161
#[serde(field_identifier, rename_all = "kebab-case")]
128162
#[allow(non_camel_case_types)]
129163
enum Field { $($name,)* third_party, }
130164

131-
struct ConfVisitor;
165+
struct ConfVisitor<'a>(&'a SourceFile);
132166

133-
impl<'de> Visitor<'de> for ConfVisitor {
167+
impl<'de> Visitor<'de> for ConfVisitor<'_> {
134168
type Value = TryConf;
135169

136170
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -141,32 +175,38 @@ macro_rules! define_Conf {
141175
let mut errors = Vec::new();
142176
let mut warnings = Vec::new();
143177
$(let mut $name = None;)*
144-
// could get `Field` here directly, but get `str` first for diagnostics
145-
while let Some(name) = map.next_key::<&str>()? {
146-
match Field::deserialize(name.into_deserializer())? {
147-
$(Field::$name => {
148-
$(warnings.push(conf_error(format!("deprecated field `{}`. {}", name, $dep)));)?
149-
match map.next_value() {
150-
Err(e) => errors.push(conf_error(e.to_string())),
178+
// could get `Field` here directly, but get `String` first for diagnostics
179+
while let Some(name) = map.next_key::<toml::Spanned<String>>()? {
180+
match Field::deserialize(name.get_ref().as_str().into_deserializer()) {
181+
Err(e) => {
182+
let e: FieldError = e;
183+
errors.push(ConfError::spanned(self.0, e.0, name.span()));
184+
}
185+
$(Ok(Field::$name) => {
186+
$(warnings.push(ConfError::spanned(self.0, format!("deprecated field `{}`. {}", name.get_ref(), $dep), name.span()));)?
187+
let raw_value = map.next_value::<toml::Spanned<toml::Value>>()?;
188+
let value_span = raw_value.span();
189+
match <$ty>::deserialize(raw_value.into_inner()) {
190+
Err(e) => errors.push(ConfError::spanned(self.0, e.to_string().replace('\n', " ").trim(), value_span)),
151191
Ok(value) => match $name {
152-
Some(_) => errors.push(conf_error(format!("duplicate field `{}`", name))),
192+
Some(_) => errors.push(ConfError::spanned(self.0, format!("duplicate field `{}`", name.get_ref()), name.span())),
153193
None => {
154194
$name = Some(value);
155195
// $new_conf is the same as one of the defined `$name`s, so
156196
// this variable is defined in line 2 of this function.
157197
$(match $new_conf {
158-
Some(_) => errors.push(conf_error(concat!(
198+
Some(_) => errors.push(ConfError::spanned(self.0, concat!(
159199
"duplicate field `", stringify!($new_conf),
160200
"` (provided as `", stringify!($name), "`)"
161-
))),
201+
), name.span())),
162202
None => $new_conf = $name.clone(),
163203
})?
164204
},
165205
}
166206
}
167207
})*
168-
// white-listed; ignore
169-
Field::third_party => drop(map.next_value::<IgnoredAny>())
208+
// ignore contents of the third_party key
209+
Ok(Field::third_party) => drop(map.next_value::<IgnoredAny>())
170210
}
171211
}
172212
let conf = Conf { $($name: $name.unwrap_or_else(defaults::$name),)* };
@@ -532,19 +572,19 @@ pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> {
532572
/// Read the `toml` configuration file.
533573
///
534574
/// In case of error, the function tries to continue as much as possible.
535-
pub fn read(path: &Path) -> TryConf {
536-
let content = match fs::read_to_string(path) {
537-
Err(e) => return TryConf::from_error(e),
538-
Ok(content) => content,
575+
pub fn read(sess: &Session, path: &Path) -> TryConf {
576+
let file = match sess.source_map().load_file(path) {
577+
Err(e) => return e.into(),
578+
Ok(file) => file,
539579
};
540-
match toml::from_str::<TryConf>(&content) {
580+
match toml::de::Deserializer::new(file.src.as_ref().unwrap()).deserialize_map(ConfVisitor(&file)) {
541581
Ok(mut conf) => {
542582
extend_vec_if_indicator_present(&mut conf.conf.doc_valid_idents, DEFAULT_DOC_VALID_IDENTS);
543583
extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES);
544584

545585
conf
546586
},
547-
Err(e) => TryConf::from_error(e),
587+
Err(e) => TryConf::from_toml_error(&file, &e),
548588
}
549589
}
550590

@@ -556,65 +596,42 @@ fn extend_vec_if_indicator_present(vec: &mut Vec<String>, default: &[&str]) {
556596

557597
const SEPARATOR_WIDTH: usize = 4;
558598

559-
// Check whether the error is "unknown field" and, if so, list the available fields sorted and at
560-
// least one per line, more if `CLIPPY_TERMINAL_WIDTH` is set and allows it.
561-
pub fn format_error(error: Box<dyn Error>) -> String {
562-
let s = error.to_string();
563-
564-
if_chain! {
565-
if error.downcast::<toml::de::Error>().is_ok();
566-
if let Some((prefix, mut fields, suffix)) = parse_unknown_field_message(&s);
567-
then {
568-
use fmt::Write;
569-
570-
fields.sort_unstable();
571-
572-
let (rows, column_widths) = calculate_dimensions(&fields);
573-
574-
let mut msg = String::from(prefix);
575-
for row in 0..rows {
576-
writeln!(msg).unwrap();
577-
for (column, column_width) in column_widths.iter().copied().enumerate() {
578-
let index = column * rows + row;
579-
let field = fields.get(index).copied().unwrap_or_default();
580-
write!(
581-
msg,
582-
"{:SEPARATOR_WIDTH$}{field:column_width$}",
583-
" "
584-
)
585-
.unwrap();
586-
}
587-
}
588-
write!(msg, "\n{suffix}").unwrap();
589-
msg
590-
} else {
591-
s
592-
}
599+
#[derive(Debug)]
600+
struct FieldError(String);
601+
602+
impl std::error::Error for FieldError {}
603+
604+
impl Display for FieldError {
605+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
606+
f.pad(&self.0)
593607
}
594608
}
595609

596-
// `parse_unknown_field_message` will become unnecessary if
597-
// https://github.com/alexcrichton/toml-rs/pull/364 is merged.
598-
fn parse_unknown_field_message(s: &str) -> Option<(&str, Vec<&str>, &str)> {
599-
// An "unknown field" message has the following form:
600-
// unknown field `UNKNOWN`, expected one of `FIELD0`, `FIELD1`, ..., `FIELDN` at line X column Y
601-
// ^^ ^^^^ ^^
602-
if_chain! {
603-
if s.starts_with("unknown field");
604-
let slices = s.split("`, `").collect::<Vec<_>>();
605-
let n = slices.len();
606-
if n >= 2;
607-
if let Some((prefix, first_field)) = slices[0].rsplit_once(" `");
608-
if let Some((last_field, suffix)) = slices[n - 1].split_once("` ");
609-
then {
610-
let fields = iter::once(first_field)
611-
.chain(slices[1..n - 1].iter().copied())
612-
.chain(iter::once(last_field))
613-
.collect::<Vec<_>>();
614-
Some((prefix, fields, suffix))
615-
} else {
616-
None
610+
impl serde::de::Error for FieldError {
611+
fn custom<T: Display>(msg: T) -> Self {
612+
Self(msg.to_string())
613+
}
614+
615+
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
616+
// List the available fields sorted and at least one per line, more if `CLIPPY_TERMINAL_WIDTH` is
617+
// set and allows it.
618+
use fmt::Write;
619+
620+
let mut expected = expected.to_vec();
621+
expected.sort_unstable();
622+
623+
let (rows, column_widths) = calculate_dimensions(&expected);
624+
625+
let mut msg = format!("unknown field `{field}`, expected one of");
626+
for row in 0..rows {
627+
writeln!(msg).unwrap();
628+
for (column, column_width) in column_widths.iter().copied().enumerate() {
629+
let index = column * rows + row;
630+
let field = expected.get(index).copied().unwrap_or_default();
631+
write!(msg, "{:SEPARATOR_WIDTH$}{field:column_width$}", " ").unwrap();
632+
}
617633
}
634+
Self(msg)
618635
}
619636
}
620637

lintcheck/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ rayon = "1.5.1"
2222
serde = { version = "1.0", features = ["derive"] }
2323
serde_json = "1.0.85"
2424
tar = "0.4"
25-
toml = "0.5"
25+
toml = "0.7.3"
2626
ureq = "2.2"
2727
walkdir = "2.3"
2828

0 commit comments

Comments
 (0)