@@ -6,7 +6,8 @@ use serde::de::{Deserializer, IgnoredAny, IntoDeserializer, MapAccess, Visitor};
66use serde:: Deserialize ;
77use std:: error:: Error ;
88use std:: path:: { Path , PathBuf } ;
9- use std:: { env, fmt, fs, io} ;
9+ use std:: str:: FromStr ;
10+ use std:: { cmp, env, fmt, fs, io, iter} ;
1011
1112/// Holds information used by `MISSING_ENFORCED_IMPORT_RENAMES` lint.
1213#[ derive( Clone , Debug , Deserialize ) ]
@@ -43,18 +44,33 @@ pub enum DisallowedType {
4344#[ derive( Default ) ]
4445pub struct TryConf {
4546 pub conf : Conf ,
46- pub errors : Vec < String > ,
47+ pub errors : Vec < Box < dyn Error > > ,
4748}
4849
4950impl TryConf {
50- fn from_error ( error : impl Error ) -> Self {
51+ fn from_error ( error : impl Error + ' static ) -> Self {
5152 Self {
5253 conf : Conf :: default ( ) ,
53- errors : vec ! [ error . to_string ( ) ] ,
54+ errors : vec ! [ Box :: new ( error ) ] ,
5455 }
5556 }
5657}
5758
59+ #[ derive( Debug ) ]
60+ struct ConfError ( String ) ;
61+
62+ impl fmt:: Display for ConfError {
63+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
64+ <String as fmt:: Display >:: fmt ( & self . 0 , f)
65+ }
66+ }
67+
68+ impl Error for ConfError { }
69+
70+ fn conf_error ( s : String ) -> Box < dyn Error > {
71+ Box :: new ( ConfError ( s) )
72+ }
73+
5874macro_rules! define_Conf {
5975 ( $(
6076 $( #[ doc = $doc: literal] ) +
@@ -103,11 +119,11 @@ macro_rules! define_Conf {
103119 while let Some ( name) = map. next_key:: <& str >( ) ? {
104120 match Field :: deserialize( name. into_deserializer( ) ) ? {
105121 $( Field :: $name => {
106- $( errors. push( format!( "deprecated field `{}`. {}" , name, $dep) ) ; ) ?
122+ $( errors. push( conf_error ( format!( "deprecated field `{}`. {}" , name, $dep) ) ) ; ) ?
107123 match map. next_value( ) {
108- Err ( e) => errors. push( e. to_string( ) ) ,
124+ Err ( e) => errors. push( conf_error ( e. to_string( ) ) ) ,
109125 Ok ( value) => match $name {
110- Some ( _) => errors. push( format!( "duplicate field `{}`" , name) ) ,
126+ Some ( _) => errors. push( conf_error ( format!( "duplicate field `{}`" , name) ) ) ,
111127 None => $name = Some ( value) ,
112128 }
113129 }
@@ -383,3 +399,102 @@ pub fn read(path: &Path) -> TryConf {
383399 } ;
384400 toml:: from_str ( & content) . unwrap_or_else ( TryConf :: from_error)
385401}
402+
403+ const SEPARATOR_WIDTH : usize = 4 ;
404+
405+ // Check whether the error is "unknown field" and, if so, list the available fields sorted and at
406+ // least one per line, more if `CLIPPY_TERMINAL_WIDTH` is set and allows it.
407+ pub fn format_error ( error : Box < dyn Error > ) -> String {
408+ let s = error. to_string ( ) ;
409+
410+ if_chain ! {
411+ if error. downcast:: <toml:: de:: Error >( ) . is_ok( ) ;
412+ if let Some ( ( prefix, mut fields, suffix) ) = parse_unknown_field_message( & s) ;
413+ then {
414+ use fmt:: Write ;
415+
416+ fields. sort_unstable( ) ;
417+
418+ let ( rows, column_widths) = calculate_dimensions( & fields) ;
419+
420+ let mut msg = String :: from( prefix) ;
421+ for row in 0 ..rows {
422+ write!( msg, "\n " ) . unwrap( ) ;
423+ for ( column, column_width) in column_widths. iter( ) . copied( ) . enumerate( ) {
424+ let index = column * rows + row;
425+ let field = fields. get( index) . copied( ) . unwrap_or_default( ) ;
426+ write!(
427+ msg,
428+ "{:separator_width$}{:field_width$}" ,
429+ " " ,
430+ field,
431+ separator_width = SEPARATOR_WIDTH ,
432+ field_width = column_width
433+ )
434+ . unwrap( ) ;
435+ }
436+ }
437+ write!( msg, "\n {}" , suffix) . unwrap( ) ;
438+ msg
439+ } else {
440+ s
441+ }
442+ }
443+ }
444+
445+ // `parse_unknown_field_message` will become unnecessary if
446+ // https://github.com/alexcrichton/toml-rs/pull/364 is merged.
447+ fn parse_unknown_field_message ( s : & str ) -> Option < ( & str , Vec < & str > , & str ) > {
448+ // An "unknown field" message has the following form:
449+ // unknown field `UNKNOWN`, expected one of `FIELD0`, `FIELD1`, ..., `FIELDN` at line X column Y
450+ // ^^ ^^^^ ^^
451+ if_chain ! {
452+ if s. starts_with( "unknown field" ) ;
453+ let slices = s. split( "`, `" ) . collect:: <Vec <_>>( ) ;
454+ let n = slices. len( ) ;
455+ if n >= 2 ;
456+ if let Some ( ( prefix, first_field) ) = slices[ 0 ] . rsplit_once( " `" ) ;
457+ if let Some ( ( last_field, suffix) ) = slices[ n - 1 ] . split_once( "` " ) ;
458+ then {
459+ let fields = iter:: once( first_field)
460+ . chain( slices[ 1 ..n - 1 ] . iter( ) . copied( ) )
461+ . chain( iter:: once( last_field) )
462+ . collect:: <Vec <_>>( ) ;
463+ Some ( ( prefix, fields, suffix) )
464+ } else {
465+ None
466+ }
467+ }
468+ }
469+
470+ fn calculate_dimensions ( fields : & [ & str ] ) -> ( usize , Vec < usize > ) {
471+ let columns = env:: var ( "CLIPPY_TERMINAL_WIDTH" )
472+ . ok ( )
473+ . and_then ( |s| <usize as FromStr >:: from_str ( & s) . ok ( ) )
474+ . map_or ( 1 , |terminal_width| {
475+ let max_field_width = fields. iter ( ) . map ( |field| field. len ( ) ) . max ( ) . unwrap ( ) ;
476+ cmp:: max ( 1 , terminal_width / ( SEPARATOR_WIDTH + max_field_width) )
477+ } ) ;
478+
479+ let rows = ( fields. len ( ) + ( columns - 1 ) ) / columns;
480+
481+ let column_widths = ( 0 ..columns)
482+ . map ( |column| {
483+ if column < columns - 1 {
484+ ( 0 ..rows)
485+ . map ( |row| {
486+ let index = column * rows + row;
487+ let field = fields. get ( index) . copied ( ) . unwrap_or_default ( ) ;
488+ field. len ( )
489+ } )
490+ . max ( )
491+ . unwrap ( )
492+ } else {
493+ // Avoid adding extra space to the last column.
494+ 0
495+ }
496+ } )
497+ . collect :: < Vec < _ > > ( ) ;
498+
499+ ( rows, column_widths)
500+ }
0 commit comments