@@ -3,6 +3,7 @@ use crate::error::Result;
33use crate :: rules:: { Severity , Violation } ;
44use colored:: * ;
55use serde:: { Deserialize , Serialize } ;
6+ use std:: collections:: BTreeMap ;
67use std:: io:: { self , Write } ;
78use std:: path:: { Path , PathBuf } ;
89use std:: time:: Duration ;
@@ -109,31 +110,54 @@ impl OutputFormatter {
109110 elapsed : Duration ,
110111 ) -> Result < ( ) > {
111112 let mut stdout = io:: stdout ( ) ;
113+ let gutter = "┃" . dimmed ( ) ;
112114
113- for violation in violations {
114- let severity_icon = match violation. severity {
115- Severity :: Error => "✗" . red ( ) . bold ( ) ,
116- Severity :: Warning => "⚠" . yellow ( ) . bold ( ) ,
117- } ;
118- let path_str = self . relative_path ( & violation. path ) ;
119- let rule_info = format ! (
120- "[{}:{}]" ,
121- violation. rule_name,
122- match violation. severity {
123- Severity :: Error => "error" ,
124- Severity :: Warning => "warning" ,
125- }
126- )
127- . dimmed ( ) ;
115+ let mut by_rule: BTreeMap < & str , Vec < & Violation > > = BTreeMap :: new ( ) ;
116+ for v in violations {
117+ by_rule. entry ( & v. rule_name ) . or_default ( ) . push ( v) ;
118+ }
128119
129- writeln ! (
130- stdout,
131- "{} {}: {} {}" ,
132- severity_icon,
133- path_str. bold( ) ,
134- violation. message,
135- rule_info
136- ) ?;
120+ for ( rule_name, rule_violations) in & by_rule {
121+ writeln ! ( stdout, "{}" , rule_name. bold( ) ) ?;
122+ writeln ! ( stdout, "{gutter}" ) ?;
123+
124+ let mut errors: Vec < & Violation > = Vec :: new ( ) ;
125+ let mut warnings: Vec < & Violation > = Vec :: new ( ) ;
126+ for v in rule_violations {
127+ match v. severity {
128+ Severity :: Error => errors. push ( v) ,
129+ Severity :: Warning => warnings. push ( v) ,
130+ }
131+ }
132+ errors. sort_by ( |a, b| b. sort_key . cmp ( & a. sort_key ) ) ;
133+ warnings. sort_by ( |a, b| b. sort_key . cmp ( & a. sort_key ) ) ;
134+
135+ for ( severity_group, marker, color_fn) in [
136+ (
137+ & errors,
138+ "[E]" ,
139+ ColoredString :: red as fn ( ColoredString ) -> ColoredString ,
140+ ) ,
141+ ( & warnings, "[W]" , ColoredString :: yellow) ,
142+ ] {
143+ if severity_group. is_empty ( ) {
144+ continue ;
145+ }
146+ let message = & severity_group[ 0 ] . message ;
147+ writeln ! ( stdout, "{gutter} {} {}" , color_fn( marker. bold( ) ) , message) ?;
148+ for v in severity_group {
149+ let path_str = self . relative_path ( & v. path ) ;
150+ match & v. actual_value {
151+ Some ( actual) => {
152+ writeln ! ( stdout, "{gutter} {} ({})" , path_str. bold( ) , actual) ?;
153+ }
154+ None => {
155+ writeln ! ( stdout, "{gutter} {}" , path_str. bold( ) ) ?;
156+ }
157+ }
158+ }
159+ }
160+ writeln ! ( stdout) ?;
137161 }
138162
139163 if !self . quiet {
0 commit comments