44//! loading warnings from JSON files, and generating human-readable diffs
55//! between different linting runs.
66
7- use std:: fs ;
8- use std:: path :: Path ;
7+ use std:: path :: { Path , PathBuf } ;
8+ use std:: { fmt , fs } ;
99
1010use itertools:: { EitherOrBoth , Itertools } ;
1111use serde:: { Deserialize , Serialize } ;
@@ -17,7 +17,6 @@ const DEFAULT_LIMIT_PER_LINT: usize = 300;
1717/// Target for total warnings to display across all lints when truncating output.
1818const TRUNCATION_TOTAL_TARGET : usize = 1000 ;
1919
20- /// Representation of a single Clippy warning for JSON serialization.
2120#[ derive( Debug , Deserialize , Serialize ) ]
2221struct LintJson {
2322 /// The lint name e.g. `clippy::bytes_nth`
@@ -29,7 +28,6 @@ struct LintJson {
2928}
3029
3130impl LintJson {
32- /// Returns a tuple of name and `file_line` for sorting and comparison.
3331 fn key ( & self ) -> impl Ord + ' _ {
3432 ( self . name . as_str ( ) , self . file_line . as_str ( ) )
3533 }
@@ -40,6 +38,57 @@ impl LintJson {
4038 }
4139}
4240
41+ #[ derive( Debug , Serialize ) ]
42+ struct SummaryRow {
43+ name : String ,
44+ added : usize ,
45+ removed : usize ,
46+ changed : usize ,
47+ }
48+
49+ #[ derive( Debug , Serialize ) ]
50+ struct Summary ( Vec < SummaryRow > ) ;
51+
52+ impl fmt:: Display for Summary {
53+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
54+ f. write_str (
55+ "\
56+ | Lint | Added | Removed | Changed |
57+ | ---- | ----: | ------: | ------: |
58+ " ,
59+ ) ?;
60+
61+ for SummaryRow {
62+ name,
63+ added,
64+ changed,
65+ removed,
66+ } in & self . 0
67+ {
68+ let html_id = to_html_id ( name) ;
69+ writeln ! ( f, "| [`{name}`](#{html_id}) | {added} | {changed} | {removed} |" ) ?;
70+ }
71+
72+ Ok ( ( ) )
73+ }
74+ }
75+
76+ impl Summary {
77+ fn new ( lints : & [ LintWarnings ] ) -> Self {
78+ Summary (
79+ lints
80+ . iter ( )
81+ . map ( |lint| SummaryRow {
82+ name : lint. name . clone ( ) ,
83+ added : lint. added . len ( ) ,
84+ removed : lint. removed . len ( ) ,
85+ changed : lint. changed . len ( ) ,
86+ } )
87+ . collect ( ) ,
88+ )
89+ }
90+ }
91+
4392/// Creates the log file output for [`crate::config::OutputFormat::Json`]
4493pub ( crate ) fn output ( clippy_warnings : Vec < ClippyWarning > ) -> String {
4594 let mut lints: Vec < LintJson > = clippy_warnings
@@ -74,7 +123,7 @@ fn load_warnings(path: &Path) -> Vec<LintJson> {
74123///
75124/// Compares warnings from `old_path` and `new_path`, then displays a summary table
76125/// and detailed information about added, removed, and changed warnings.
77- pub ( crate ) fn diff ( old_path : & Path , new_path : & Path , truncate : bool ) {
126+ pub ( crate ) fn diff ( old_path : & Path , new_path : & Path , truncate : bool , write_summary : Option < PathBuf > ) {
78127 let old_warnings = load_warnings ( old_path) ;
79128 let new_warnings = load_warnings ( new_path) ;
80129
@@ -108,13 +157,16 @@ pub(crate) fn diff(old_path: &Path, new_path: &Path, truncate: bool) {
108157 }
109158 }
110159
111- print_summary_table ( & lint_warnings) ;
112- println ! ( ) ;
113-
114160 if lint_warnings. is_empty ( ) {
115161 return ;
116162 }
117163
164+ let summary = Summary :: new ( & lint_warnings) ;
165+ if let Some ( path) = write_summary {
166+ let json = serde_json:: to_string ( & summary) . unwrap ( ) ;
167+ fs:: write ( path, json) . unwrap ( ) ;
168+ }
169+
118170 let truncate_after = if truncate {
119171 // Max 15 ensures that we at least have five messages per lint
120172 DEFAULT_LIMIT_PER_LINT
@@ -126,6 +178,7 @@ pub(crate) fn diff(old_path: &Path, new_path: &Path, truncate: bool) {
126178 usize:: MAX
127179 } ;
128180
181+ println ! ( "{summary}" ) ;
129182 for lint in lint_warnings {
130183 print_lint_warnings ( & lint, truncate_after) ;
131184 }
@@ -140,13 +193,11 @@ struct LintWarnings {
140193 changed : Vec < ( LintJson , LintJson ) > ,
141194}
142195
143- /// Prints a formatted report for a single lint type with its warnings.
144196fn print_lint_warnings ( lint : & LintWarnings , truncate_after : usize ) {
145197 let name = & lint. name ;
146198 let html_id = to_html_id ( name) ;
147199
148- // The additional anchor is added for non GH viewers that don't prefix ID's
149- println ! ( r#"## `{name}` <a id="user-content-{html_id}"></a>"# ) ;
200+ println ! ( r#"<h2 id="{html_id}"><code>{name}</code></h2>"# ) ;
150201 println ! ( ) ;
151202
152203 print ! (
@@ -162,22 +213,6 @@ fn print_lint_warnings(lint: &LintWarnings, truncate_after: usize) {
162213 print_changed_diff ( & lint. changed , truncate_after / 3 ) ;
163214}
164215
165- /// Prints a summary table of all lints with counts of added, removed, and changed warnings.
166- fn print_summary_table ( lints : & [ LintWarnings ] ) {
167- println ! ( "| Lint | Added | Removed | Changed |" ) ;
168- println ! ( "| ------------------------------------------ | ------: | ------: | ------: |" ) ;
169-
170- for lint in lints {
171- println ! (
172- "| {:<62} | {:>7} | {:>7} | {:>7} |" ,
173- format!( "[`{}`](#user-content-{})" , lint. name, to_html_id( & lint. name) ) ,
174- lint. added. len( ) ,
175- lint. removed. len( ) ,
176- lint. changed. len( )
177- ) ;
178- }
179- }
180-
181216/// Prints a section of warnings with a header and formatted code blocks.
182217fn print_warnings ( title : & str , warnings : & [ LintJson ] , truncate_after : usize ) {
183218 if warnings. is_empty ( ) {
@@ -248,17 +283,16 @@ fn truncate<T>(list: &[T], truncate_after: usize) -> &[T] {
248283 }
249284}
250285
251- /// Prints a level 3 heading with an appropriate HTML ID for linking.
252286fn print_h3 ( lint : & str , title : & str ) {
253287 let html_id = to_html_id ( lint) ;
254- // We have to use HTML here to be able to manually add an id.
255- println ! ( r#"### {title} <a id="user-content- {html_id}-{title}"></a >"# ) ;
288+ // We have to use HTML here to be able to manually add an id, GitHub doesn't add them automatically
289+ println ! ( r#"<h3 id="{html_id}-{title}">{title}</h3 >"# ) ;
256290}
257291
258- /// GitHub's markdown parsers doesn't like IDs with `:: ` and `_`. This simplifies
259- /// the lint name for the HTML ID.
292+ /// Creates a custom ID allowed by GitHub, they must start with `user-content- ` and cannot contain
293+ /// `::`/`_`
260294fn to_html_id ( lint_name : & str ) -> String {
261- lint_name. replace ( "clippy::" , "" ) . replace ( '_' , "-" )
295+ lint_name. replace ( "clippy::" , "user-content- " ) . replace ( '_' , "-" )
262296}
263297
264298/// This generates the `x added` string for the start of the job summery.
@@ -270,9 +304,6 @@ fn count_string(lint: &str, label: &str, count: usize) -> String {
270304 format ! ( "0 {label}" )
271305 } else {
272306 let html_id = to_html_id ( lint) ;
273- // GitHub's job summaries don't add HTML ids to headings. That's why we
274- // manually have to add them. GitHub prefixes these manual ids with
275- // `user-content-` and that's how we end up with these awesome links :D
276- format ! ( "[{count} {label}](#user-content-{html_id}-{label})" )
307+ format ! ( "[{count} {label}](#{html_id}-{label})" )
277308 }
278309}
0 commit comments