1+ #![ expect( unused) ]
2+
3+ use std:: collections:: BTreeSet ;
4+ use std:: fmt:: Write ;
15use std:: fs;
26use std:: path:: Path ;
37
@@ -6,7 +10,10 @@ use serde::{Deserialize, Serialize};
610
711use crate :: ClippyWarning ;
812
9- #[ derive( Deserialize , Serialize ) ]
13+ const DEFAULT_LIMIT_PER_LINT : usize = 300 ;
14+ const TRUNCATION_TOTAL_TARGET : usize = 1000 ;
15+
16+ #[ derive( Debug , Deserialize , Serialize ) ]
1017struct LintJson {
1118 lint : String ,
1219 krate : String ,
@@ -52,14 +59,16 @@ fn load_warnings(path: &Path) -> Vec<LintJson> {
5259 serde_json:: from_slice ( & file) . unwrap_or_else ( |e| panic ! ( "failed to deserialize {}: {e}" , path. display( ) ) )
5360}
5461
55- fn print_warnings ( title : & str , warnings : & [ LintJson ] ) {
62+ fn print_warnings ( title : & str , warnings : & [ LintJson ] , truncate_after : usize ) {
5663 if warnings. is_empty ( ) {
5764 return ;
5865 }
5966
60- //We have to use HTML here to be able to manually add an id.
61- println ! ( r#"<h3 id="{title}">{title}</h3>"# ) ;
67+ print_h3 ( & warnings[ 0 ] . lint , title) ;
6268 println ! ( ) ;
69+
70+ let warnings = truncate ( warnings, truncate_after) ;
71+
6372 for warning in warnings {
6473 println ! ( "{}" , warning. info_text( title) ) ;
6574 println ! ( ) ;
@@ -70,14 +79,16 @@ fn print_warnings(title: &str, warnings: &[LintJson]) {
7079 }
7180}
7281
73- fn print_changed_diff ( changed : & [ ( LintJson , LintJson ) ] ) {
82+ fn print_changed_diff ( changed : & [ ( LintJson , LintJson ) ] , truncate_after : usize ) {
7483 if changed. is_empty ( ) {
7584 return ;
7685 }
7786
78- //We have to use HTML here to be able to manually add an id.
79- println ! ( r#"<h3 id="changed">Changed</h3>"# ) ;
87+ print_h3 ( & changed[ 0 ] . 0 . lint , "Changed" ) ;
8088 println ! ( ) ;
89+
90+ let changes = truncate ( changed, truncate_after) ;
91+
8192 for ( old, new) in changed {
8293 println ! ( "{}" , new. info_text( "Changed" ) ) ;
8394 println ! ( ) ;
@@ -101,7 +112,25 @@ fn print_changed_diff(changed: &[(LintJson, LintJson)]) {
101112 }
102113}
103114
104- pub ( crate ) fn diff ( old_path : & Path , new_path : & Path ) {
115+ fn print_h3 ( lint : & str , title : & str ) {
116+ let html_id = to_html_id ( lint) ;
117+ // We have to use HTML here to be able to manually add an id.
118+ println ! ( r#"<h3 id="user-content-{html_id}-{title}">{title}</h3>"# ) ;
119+ }
120+
121+ fn truncate < T > ( list : & [ T ] , truncate_after : usize ) -> & [ T ] {
122+ if list. len ( ) > truncate_after {
123+ println ! ( "{} warnings have been truncated for this summary." , list. len( ) - truncate_after) ;
124+ println ! ( ) ;
125+
126+ list. split_at ( truncate_after) . 0
127+ } else {
128+ list
129+ }
130+
131+ }
132+
133+ pub ( crate ) fn diff ( old_path : & Path , new_path : & Path , truncate : bool ) {
105134 let old_warnings = load_warnings ( old_path) ;
106135 let new_warnings = load_warnings ( new_path) ;
107136
@@ -121,31 +150,141 @@ pub(crate) fn diff(old_path: &Path, new_path: &Path) {
121150 }
122151 }
123152
153+ let lint_warnings = group_by_lint ( added, removed, changed) ;
154+
155+ print_summary_table ( & lint_warnings) ;
156+ println ! ( ) ;
157+
158+ let truncate_after = if truncate {
159+ // Max 9 ensures that we at least have three messages per lint
160+ DEFAULT_LIMIT_PER_LINT . min ( TRUNCATION_TOTAL_TARGET / lint_warnings. len ( ) ) . max ( 9 )
161+ } else {
162+ // No lint should ever each this number of lint emissions, so this is equivialent to
163+ // No truncation
164+ usize:: MAX
165+ } ;
166+
167+ for lint in lint_warnings {
168+ print_lint_warnings ( & lint, truncate_after)
169+ }
170+ }
171+
172+ fn print_lint_warnings ( lint : & LintWarnings , truncate_after : usize ) {
173+ let name = & lint. name ;
174+ let html_id = to_html_id ( name) ;
175+
176+ // The additional anchor is added for non GH viewers that don't prefix ID's
177+ println ! ( r#"<h2 id="user-content-{html_id}">{name}</h2>"# ) ;
178+ println ! ( ) ;
179+
124180 print ! (
125181 r##"{}, {}, {}"## ,
126- count_string( "added" , added. len( ) ) ,
127- count_string( "removed" , removed. len( ) ) ,
128- count_string( "changed" , changed. len( ) ) ,
182+ count_string( name , "added" , lint . added. len( ) ) ,
183+ count_string( name , "removed" , lint . removed. len( ) ) ,
184+ count_string( name , "changed" , lint . changed. len( ) ) ,
129185 ) ;
130186 println ! ( ) ;
131- println ! ( ) ;
132187
133- print_warnings ( "Added" , & added) ;
134- print_warnings ( "Removed" , & removed) ;
135- print_changed_diff ( & changed) ;
188+ print_warnings ( "Added" , & lint. added , truncate_after / 3 ) ;
189+ print_warnings ( "Removed" , & lint. removed , truncate_after / 3 ) ;
190+ print_changed_diff ( & lint. changed , truncate_after / 3 ) ;
191+ }
192+
193+ #[ derive( Debug ) ]
194+ struct LintWarnings {
195+ name : String ,
196+ added : Vec < LintJson > ,
197+ removed : Vec < LintJson > ,
198+ changed : Vec < ( LintJson , LintJson ) > ,
199+ }
200+
201+ fn group_by_lint (
202+ mut added : Vec < LintJson > ,
203+ mut removed : Vec < LintJson > ,
204+ mut changed : Vec < ( LintJson , LintJson ) > ,
205+ ) -> Vec < LintWarnings > {
206+ /// Collects items from an iterator while the condition is met
207+ fn collect_while < T , F > ( iter : & mut std:: iter:: Peekable < impl Iterator < Item = T > > , mut condition : F ) -> Vec < T >
208+ where
209+ F : FnMut ( & T ) -> bool ,
210+ {
211+ let mut items = vec ! [ ] ;
212+ while iter. peek ( ) . map_or ( false , |item| condition ( item) ) {
213+ items. push ( iter. next ( ) . unwrap ( ) ) ;
214+ }
215+ items
216+ }
217+
218+ // Sort
219+ added. sort_unstable_by ( |a, b| a. lint . cmp ( & b. lint ) ) ;
220+ removed. sort_unstable_by ( |a, b| a. lint . cmp ( & b. lint ) ) ;
221+ changed. sort_unstable_by ( |( a, _) , ( b, _) | a. lint . cmp ( & b. lint ) ) ;
222+
223+ // Collect lint names
224+ let lint_names: BTreeSet < _ > = added
225+ . iter ( )
226+ . chain ( removed. iter ( ) )
227+ . chain ( changed. iter ( ) . map ( |( a, _) | a) )
228+ . map ( |warning| & warning. lint )
229+ . cloned ( )
230+ . collect ( ) ;
231+
232+ let mut added_iter = added. into_iter ( ) . peekable ( ) ;
233+ let mut removed_iter = removed. into_iter ( ) . peekable ( ) ;
234+ let mut changed_iter = changed. into_iter ( ) . peekable ( ) ;
235+
236+ let mut lints = vec ! [ ] ;
237+ for name in lint_names. into_iter ( ) {
238+ lints. push ( LintWarnings {
239+ added : collect_while ( & mut added_iter, |warning| warning. lint == name) ,
240+ removed : collect_while ( & mut removed_iter, |warning| warning. lint == name) ,
241+ changed : collect_while ( & mut changed_iter, |( warning, _) | warning. lint == name) ,
242+ name,
243+ } ) ;
244+ }
245+
246+ lints
247+ }
248+
249+ // This function limits the number of lints in the collection if needed.
250+ //
251+ // It's honestly amazing that a function like this is needed. But some restriction
252+ // lints (Looking at you `implicit_return` and `if_then_some_else_none`) trigger a lot
253+ // like 60'000+ times in our CI.
254+ //
255+ // Each lint in the list will be limited to 200 messages
256+
257+ fn print_summary_table ( lints : & [ LintWarnings ] ) {
258+ println ! ( "| Lint | Added | Removed | Changed |" ) ;
259+ println ! ( "| ------------------------------------------ | ------: | ------: | ------: |" ) ;
260+
261+ for lint in lints {
262+ println ! (
263+ "| {:<62} | {:>7} | {:>7} | {:>7} |" ,
264+ format!( "[`{}`](#user-content-{})" , lint. name, to_html_id( & lint. name) ) ,
265+ lint. added. len( ) ,
266+ lint. removed. len( ) ,
267+ lint. changed. len( )
268+ ) ;
269+ }
270+ }
271+
272+ fn to_html_id ( lint_name : & str ) -> String {
273+ lint_name. replace ( "clippy::" , "" ) . replace ( "_" , "-" )
136274}
137275
138276/// This generates the `x added` string for the start of the job summery.
139277/// It linkifies them if possible to jump to the respective heading.
140- fn count_string ( label : & str , count : usize ) -> String {
278+ fn count_string ( lint : & str , label : & str , count : usize ) -> String {
141279 // Headlines are only added, if anything will be displayed under the headline.
142280 // We therefore only want to add links to them if they exist
143281 if count == 0 {
144282 format ! ( "0 {label}" )
145283 } else {
284+ let lint_id = to_html_id ( lint) ;
146285 // GitHub's job summaries don't add HTML ids to headings. That's why we
147286 // manually have to add them. GitHub prefixes these manual ids with
148287 // `user-content-` and that's how we end up with these awesome links :D
149- format ! ( "[{count} {label}](#user-content-{label})" )
288+ format ! ( "[{count} {label}](#user-content-{lint_id}-{ label})" )
150289 }
151290}
0 commit comments