Skip to content

Commit 11ed4ac

Browse files
committed
Add spans to clippy.toml error messages
1 parent 592ea39 commit 11ed4ac

File tree

20 files changed

+263
-141
lines changed

20 files changed

+263
-141
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ termize = "0.1"
3131
compiletest_rs = { version = "0.9", features = ["tmp"] }
3232
tester = "0.9"
3333
regex = "1.5"
34-
toml = "0.5"
34+
toml = "0.7.3"
3535
walkdir = "2.3"
3636
# This is used by the `collect-metadata` alias.
3737
filetime = "0.2"

clippy_lints/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ regex-syntax = "0.6"
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

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ mod zero_div_zero;
330330
mod zero_sized_map_values;
331331
// end lints modules, do not remove this comment, it’s used in `update_lints`
332332

333-
use crate::utils::conf::{format_error, TryConf};
333+
use crate::utils::conf::TryConf;
334334
pub use crate::utils::conf::{lookup_conf_file, Conf};
335335

336336
/// Register all pre expansion lints
@@ -365,23 +365,33 @@ pub fn read_conf(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>
365365
},
366366
};
367367

368-
let TryConf { conf, errors, warnings } = utils::conf::read(file_name);
368+
let TryConf { conf, errors, warnings } = utils::conf::read(sess, file_name);
369369
// all conf errors are non-fatal, we just use the default conf in case of error
370370
for error in errors {
371-
sess.err(format!(
372-
"error reading Clippy's configuration file `{}`: {}",
373-
file_name.display(),
374-
format_error(error)
375-
));
371+
if let Some(span) = error.1 {
372+
sess.span_err(span, format!("error reading Clippy's configuration file: {}", error.0));
373+
} else {
374+
sess.err(format!(
375+
"error reading Clippy's configuration file `{}`: {}",
376+
file_name.display(),
377+
error.0
378+
));
379+
}
376380
}
377381

378382
for warning in warnings {
379-
sess.struct_warn(format!(
380-
"error reading Clippy's configuration file `{}`: {}",
381-
file_name.display(),
382-
format_error(warning)
383-
))
384-
.emit();
383+
if let Some(span) = warning.1 {
384+
sess.span_warn(
385+
span,
386+
format!("error reading Clippy's configuration file: {}", warning.0),
387+
);
388+
} else {
389+
sess.warn(format!(
390+
"error reading Clippy's configuration file `{}`: {}",
391+
file_name.display(),
392+
warning.0
393+
));
394+
}
385395
}
386396

387397
conf

clippy_lints/src/nonstandard_macro_braces.rs

Lines changed: 1 addition & 1 deletion
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

Lines changed: 103 additions & 95 deletions
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,61 @@ 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(pub String, pub Option<Span>);
86101

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)
102+
impl ConfError {
103+
fn from_toml(file: &SourceFile, error: &toml::de::Error) -> Self {
104+
if let Some(span) = error.span() {
105+
Self::spanned(file, error.message(), span)
106+
} else {
107+
Self(error.message().to_string(), None)
108+
}
90109
}
91-
}
92110

93-
impl Error for ConfError {}
111+
fn spanned(file: &SourceFile, message: impl Into<String>, span: Range<usize>) -> Self {
112+
Self(
113+
message.into(),
114+
Some(Span::new(
115+
file.start_pos + BytePos::from_usize(span.start),
116+
file.start_pos + BytePos::from_usize(span.end),
117+
SyntaxContext::root(),
118+
None,
119+
)),
120+
)
121+
}
122+
}
94123

95-
fn conf_error(s: impl Into<String>) -> Box<dyn Error> {
96-
Box::new(ConfError(s.into()))
124+
impl From<io::Error> for ConfError {
125+
fn from(value: io::Error) -> Self {
126+
Self(value.to_string(), None)
127+
}
97128
}
98129

99130
macro_rules! define_Conf {
@@ -117,20 +148,14 @@ macro_rules! define_Conf {
117148
}
118149
}
119150

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-
126151
#[derive(Deserialize)]
127152
#[serde(field_identifier, rename_all = "kebab-case")]
128153
#[allow(non_camel_case_types)]
129154
enum Field { $($name,)* third_party, }
130155

131-
struct ConfVisitor;
156+
struct ConfVisitor<'a>(&'a SourceFile);
132157

133-
impl<'de> Visitor<'de> for ConfVisitor {
158+
impl<'de> Visitor<'de> for ConfVisitor<'_> {
134159
type Value = TryConf;
135160

136161
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -141,32 +166,38 @@ macro_rules! define_Conf {
141166
let mut errors = Vec::new();
142167
let mut warnings = Vec::new();
143168
$(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())),
169+
// could get `Field` here directly, but get `String` first for diagnostics
170+
while let Some(name) = map.next_key::<toml::Spanned<String>>()? {
171+
match Field::deserialize(name.get_ref().as_str().into_deserializer()) {
172+
Err(e) => {
173+
let e: FieldError = e;
174+
errors.push(ConfError::spanned(self.0, e.0, name.span()));
175+
}
176+
$(Ok(Field::$name) => {
177+
$(warnings.push(ConfError::spanned(self.0, format!("deprecated field `{}`. {}", name.get_ref(), $dep), name.span()));)?
178+
let raw_value = map.next_value::<toml::Spanned<toml::Value>>()?;
179+
let value_span = raw_value.span();
180+
match <$ty>::deserialize(raw_value.into_inner()) {
181+
Err(e) => errors.push(ConfError::spanned(self.0, e.to_string().replace('\n', " ").trim(), value_span)),
151182
Ok(value) => match $name {
152-
Some(_) => errors.push(conf_error(format!("duplicate field `{}`", name))),
183+
Some(_) => errors.push(ConfError::spanned(self.0, format!("duplicate field `{}`", name.get_ref()), name.span())),
153184
None => {
154185
$name = Some(value);
155186
// $new_conf is the same as one of the defined `$name`s, so
156187
// this variable is defined in line 2 of this function.
157188
$(match $new_conf {
158-
Some(_) => errors.push(conf_error(concat!(
189+
Some(_) => errors.push(ConfError::spanned(self.0, concat!(
159190
"duplicate field `", stringify!($new_conf),
160191
"` (provided as `", stringify!($name), "`)"
161-
))),
192+
), name.span())),
162193
None => $new_conf = $name.clone(),
163194
})?
164195
},
165196
}
166197
}
167198
})*
168-
// white-listed; ignore
169-
Field::third_party => drop(map.next_value::<IgnoredAny>())
199+
// ignore contents of the third_party key
200+
Ok(Field::third_party) => drop(map.next_value::<IgnoredAny>())
170201
}
171202
}
172203
let conf = Conf { $($name: $name.unwrap_or_else(defaults::$name),)* };
@@ -521,19 +552,19 @@ pub fn lookup_conf_file() -> io::Result<(Option<PathBuf>, Vec<String>)> {
521552
/// Read the `toml` configuration file.
522553
///
523554
/// In case of error, the function tries to continue as much as possible.
524-
pub fn read(path: &Path) -> TryConf {
525-
let content = match fs::read_to_string(path) {
526-
Err(e) => return TryConf::from_error(e),
527-
Ok(content) => content,
555+
pub fn read(sess: &Session, path: &Path) -> TryConf {
556+
let file = match sess.source_map().load_file(path) {
557+
Err(e) => return e.into(),
558+
Ok(file) => file,
528559
};
529-
match toml::from_str::<TryConf>(&content) {
560+
match toml::de::Deserializer::new(file.src.as_ref().unwrap()).deserialize_map(ConfVisitor(&file)) {
530561
Ok(mut conf) => {
531562
extend_vec_if_indicator_present(&mut conf.conf.doc_valid_idents, DEFAULT_DOC_VALID_IDENTS);
532563
extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES);
533564

534565
conf
535566
},
536-
Err(e) => TryConf::from_error(e),
567+
Err(e) => TryConf::from_toml_error(&file, &e),
537568
}
538569
}
539570

@@ -545,65 +576,42 @@ fn extend_vec_if_indicator_present(vec: &mut Vec<String>, default: &[&str]) {
545576

546577
const SEPARATOR_WIDTH: usize = 4;
547578

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

585-
// `parse_unknown_field_message` will become unnecessary if
586-
// https://github.com/alexcrichton/toml-rs/pull/364 is merged.
587-
fn parse_unknown_field_message(s: &str) -> Option<(&str, Vec<&str>, &str)> {
588-
// An "unknown field" message has the following form:
589-
// unknown field `UNKNOWN`, expected one of `FIELD0`, `FIELD1`, ..., `FIELDN` at line X column Y
590-
// ^^ ^^^^ ^^
591-
if_chain! {
592-
if s.starts_with("unknown field");
593-
let slices = s.split("`, `").collect::<Vec<_>>();
594-
let n = slices.len();
595-
if n >= 2;
596-
if let Some((prefix, first_field)) = slices[0].rsplit_once(" `");
597-
if let Some((last_field, suffix)) = slices[n - 1].split_once("` ");
598-
then {
599-
let fields = iter::once(first_field)
600-
.chain(slices[1..n - 1].iter().copied())
601-
.chain(iter::once(last_field))
602-
.collect::<Vec<_>>();
603-
Some((prefix, fields, suffix))
604-
} else {
605-
None
590+
impl serde::de::Error for FieldError {
591+
fn custom<T: Display>(msg: T) -> Self {
592+
Self(msg.to_string())
593+
}
594+
595+
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
596+
// List the available fields sorted and at least one per line, more if `CLIPPY_TERMINAL_WIDTH` is
597+
// set and allows it.
598+
use fmt::Write;
599+
600+
let mut expected = expected.to_vec();
601+
expected.sort_unstable();
602+
603+
let (rows, column_widths) = calculate_dimensions(&expected);
604+
605+
let mut msg = format!("unknown field `{field}`, expected one of");
606+
for row in 0..rows {
607+
writeln!(msg).unwrap();
608+
for (column, column_width) in column_widths.iter().copied().enumerate() {
609+
let index = column * rows + row;
610+
let field = expected.get(index).copied().unwrap_or_default();
611+
write!(msg, "{:SEPARATOR_WIDTH$}{field:column_width$}", " ").unwrap();
612+
}
606613
}
614+
Self(msg)
607615
}
608616
}
609617

lintcheck/Cargo.toml

Lines changed: 1 addition & 1 deletion
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)