Skip to content

Commit 4fea519

Browse files
committed
Lintcheck: Order summary by lint and truncate messages
1 parent 4798383 commit 4fea519

File tree

4 files changed

+174
-22
lines changed

4 files changed

+174
-22
lines changed

.github/workflows/lintcheck.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ jobs:
119119
# https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary
120120
# That's why we first log to file and then to the summary and logs
121121
run: |
122-
./target/debug/lintcheck diff {base,head}/ci_crates_logs.json >> ci_crates.diff
123-
head -c 1024000 ci_crates.diff >> $GITHUB_STEP_SUMMARY
124-
cat ci_crates.diff
122+
./target/debug/lintcheck diff {base,head}/ci_crates_logs.json >> full_diff.md
123+
./target/debug/lintcheck diff {base,head}/ci_crates_logs.json --truncate >> truncated_diff.md
124+
head -c 1024000 truncated_diff.md >> $GITHUB_STEP_SUMMARY
125+
cat truncated_diff.md
126+
127+
- name: Upload full diff
128+
uses: actions/upload-artifact@v4
129+
with:
130+
name: diff
131+
path: full_diff.md

lintcheck/src/config.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ pub(crate) struct LintcheckConfig {
5353
#[derive(Subcommand, Clone, Debug)]
5454
pub(crate) enum Commands {
5555
/// Display a markdown diff between two lintcheck log files in JSON format
56-
Diff { old: PathBuf, new: PathBuf },
56+
Diff {
57+
old: PathBuf,
58+
new: PathBuf,
59+
/// This will limit the number of warnings that will be printed for each lint
60+
#[clap(long)]
61+
truncate: bool,
62+
},
5763
/// Create a lintcheck crates TOML file containing the top N popular crates
5864
Popular {
5965
/// Output TOML file name

lintcheck/src/json.rs

Lines changed: 156 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
#![expect(unused)]
2+
3+
use std::collections::BTreeSet;
4+
use std::fmt::Write;
15
use std::fs;
26
use std::path::Path;
37

@@ -6,7 +10,10 @@ use serde::{Deserialize, Serialize};
610

711
use 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)]
1017
struct 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
}

lintcheck/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ fn main() {
260260
let config = LintcheckConfig::new();
261261

262262
match config.subcommand {
263-
Some(Commands::Diff { old, new }) => json::diff(&old, &new),
263+
Some(Commands::Diff { old, new, truncate }) => json::diff(&old, &new, truncate),
264264
Some(Commands::Popular { output, number }) => popular_crates::fetch(output, number).unwrap(),
265265
None => lintcheck(config),
266266
}

0 commit comments

Comments
 (0)