Skip to content

Commit

Permalink
lintcheck: Add JSON output, diff subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexendoo committed Jun 15, 2024
1 parent aaade2d commit 477b0c6
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 67 deletions.
2 changes: 2 additions & 0 deletions lintcheck/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ cargo_metadata = "0.15.3"
clap = { version = "4.4", features = ["derive", "env"] }
crates_io_api = "0.8.1"
crossbeam-channel = "0.5.6"
diff = "0.1.13"
flate2 = "1.0"
indicatif = "0.17.3"
rayon = "1.5.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
strip-ansi-escapes = "0.1.1"
tar = "0.4"
toml = "0.7.3"
ureq = "2.2"
Expand Down
37 changes: 31 additions & 6 deletions lintcheck/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use clap::Parser;
use clap::{Parser, Subcommand, ValueEnum};
use std::num::NonZero;
use std::path::PathBuf;

#[derive(Clone, Debug, Parser)]
#[derive(Parser, Clone, Debug)]
#[command(args_conflicts_with_subcommands = true)]
pub(crate) struct LintcheckConfig {
/// Number of threads to use (default: all unless --fix or --recursive)
#[clap(
Expand Down Expand Up @@ -35,12 +36,36 @@ pub(crate) struct LintcheckConfig {
/// Apply a filter to only collect specified lints, this also overrides `allow` attributes
#[clap(long = "filter", value_name = "clippy_lint_name", use_value_delimiter = true)]
pub lint_filter: Vec<String>,
/// Change the reports table to use markdown links
#[clap(long)]
pub markdown: bool,
/// Set the output format of the log file
#[clap(long, short, default_value = "text")]
pub format: OutputFormat,
/// Run clippy on the dependencies of crates specified in crates-toml
#[clap(long, conflicts_with("max_jobs"))]
pub recursive: bool,
#[command(subcommand)]
pub subcommand: Option<Commands>,
}

#[derive(Subcommand, Clone, Debug)]
pub(crate) enum Commands {
Diff { old: PathBuf, new: PathBuf },
}

#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OutputFormat {
Text,
Markdown,
Json,
}

impl OutputFormat {
fn file_extension(self) -> &'static str {
match self {
OutputFormat::Text => "txt",
OutputFormat::Markdown => "md",
OutputFormat::Json => "json",
}
}
}

impl LintcheckConfig {
Expand All @@ -53,7 +78,7 @@ impl LintcheckConfig {
config.lintcheck_results_path = PathBuf::from(format!(
"lintcheck-logs/{}_logs.{}",
filename.display(),
if config.markdown { "md" } else { "txt" }
config.format.file_extension(),
));

// look at the --threads arg, if 0 is passed, use the threads count
Expand Down
122 changes: 122 additions & 0 deletions lintcheck/src/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use std::collections::HashMap;
use std::fmt::Write;
use std::fs;
use std::hash::Hash;
use std::path::Path;

use crate::ClippyWarning;

/// Creates the log file output for [`crate::config::OutputFormat::Json`]
pub(crate) fn output(clippy_warnings: &[ClippyWarning]) -> String {
serde_json::to_string(&clippy_warnings).unwrap()
}

fn load_warnings(path: &Path) -> Vec<ClippyWarning> {
let file = fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));

serde_json::from_slice(&file).unwrap_or_else(|e| panic!("failed to deserialize {}: {e}", path.display()))
}

/// Group warnings by their primary span location + lint name
fn create_map(warnings: &[ClippyWarning]) -> HashMap<impl Eq + Hash + '_, Vec<&ClippyWarning>> {
let mut map = HashMap::<_, Vec<_>>::with_capacity(warnings.len());

for warning in warnings {
let span = warning.span();
let key = (&warning.lint_type, &span.file_name, span.byte_start, span.byte_end);

map.entry(key).or_default().push(warning);
}

map
}

fn print_warnings(title: &str, warnings: &[&ClippyWarning]) {
if warnings.is_empty() {
return;
}

println!("### {title}");
println!("```");
for warning in warnings {
print!("{}", warning.diag);
}
println!("```");
}

fn print_changed_diff(changed: &[(&[&ClippyWarning], &[&ClippyWarning])]) {
fn render(warnings: &[&ClippyWarning]) -> String {
let mut rendered = String::new();
for warning in warnings {
write!(&mut rendered, "{}", warning.diag).unwrap();
}
rendered
}

if changed.is_empty() {
return;
}

println!("### Changed");
println!("```diff");
for &(old, new) in changed {
let old_rendered = render(old);
let new_rendered = render(new);

for change in diff::lines(&old_rendered, &new_rendered) {
use diff::Result::{Both, Left, Right};

match change {
Both(unchanged, _) => {
println!(" {unchanged}");
},
Left(removed) => {
println!("-{removed}");
},
Right(added) => {
println!("+{added}");
},
}
}
}
println!("```");
}

pub(crate) fn diff(old_path: &Path, new_path: &Path) {
let old_warnings = load_warnings(old_path);
let new_warnings = load_warnings(new_path);

let old_map = create_map(&old_warnings);
let new_map = create_map(&new_warnings);

let mut added = Vec::new();
let mut removed = Vec::new();
let mut changed = Vec::new();

for (key, new) in &new_map {
if let Some(old) = old_map.get(key) {
if old != new {
changed.push((old.as_slice(), new.as_slice()));
}
} else {
added.extend(new);
}
}

for (key, old) in &old_map {
if !new_map.contains_key(key) {
removed.extend(old);
}
}

print!(
"{} added, {} removed, {} changed\n\n",
added.len(),
removed.len(),
changed.len()
);

print_warnings("Added", &added);
print_warnings("Removed", &removed);
print_changed_diff(&changed);
}
Loading

0 comments on commit 477b0c6

Please sign in to comment.