cargo_diff_tools/
lib.rs

1use crate::diagnostics::{Diagnostic, Level};
2use crate::diff::{parse_diff, FileChanges};
3use crate::intervals::intersect_intervals;
4use crate::reporters::{report_diagnostic, OutputKind};
5use anyhow::{bail, Context, Result};
6use clap::{crate_authors, crate_description, crate_version, value_t, App, AppSettings, Arg};
7use std::process::{Command, Stdio};
8use std::{
9    env,
10    io::{self, BufRead, BufReader, Write},
11};
12
13mod diagnostics;
14mod diff;
15mod intervals;
16mod reporters;
17
18pub fn build_app(binary_name: &str, subcommand: Option<(&str, &[&str])>) -> Result<()> {
19    // Rip off the arguments to be passed to the subcommand
20    let app_args: Vec<String>;
21    let subcommand_extra_args: Vec<String>;
22    if subcommand.is_none() {
23        app_args = env::args().collect();
24        subcommand_extra_args = vec![];
25    } else {
26        let args: Vec<_> = env::args().collect();
27        if let Some(split_pos) = args.iter().position(|v| v == "--") {
28            app_args = args[0..split_pos].into();
29            if split_pos + 1 == args.len() {
30                subcommand_extra_args = vec![];
31            } else {
32                subcommand_extra_args = args[(split_pos + 1)..].into();
33            }
34        } else {
35            app_args = args;
36            subcommand_extra_args = vec![];
37        }
38    }
39
40    // Parse the argument of this binary
41    let matches = App::new(binary_name)
42        .version(crate_version!())
43        .author(crate_authors!("\n"))
44        .about(crate_description!())
45        .setting(AppSettings::TrailingVarArg)
46        .setting(AppSettings::AllowLeadingHyphen)
47        .arg(
48            Arg::with_name("output")
49                .short("o")
50                .long("output")
51                .value_name("FORMAT")
52                .help("Format of the output")
53                .possible_values(&OutputKind::variants())
54                .case_insensitive(true),
55        )
56        .arg(
57            Arg::with_name("args")
58                .value_name("FORMAT")
59                .help("Additional arguments to pass to `git diff`")
60                .multiple(true),
61        )
62        .get_matches_from(&app_args);
63
64    // Read `git diff` arguments
65    let git_diff_args = matches.values_of("args").unwrap_or_default();
66
67    // Obtain diff
68    let output = Command::new("git")
69        .arg("diff")
70        .arg("--unified=0")
71        .args(git_diff_args)
72        .output()
73        .with_context(|| "Failed to start `git diff`")?;
74    if !output.stderr.is_empty() {
75        io::stderr()
76            .write_all(&output.stderr)
77            .with_context(|| "Failed to report the stderr of `git diff`")?;
78    }
79    if !output.status.success() {
80        bail!(
81            "`git diff` terminated with exit status {:?}",
82            output.status.code().unwrap()
83        );
84    }
85    let diff = String::from_utf8_lossy(&output.stdout);
86    let file_changes = parse_diff(&diff)?;
87
88    // Filter and report JSON diagnostic messages from standard input
89    if let Some((subcommand_name, subcommand_args)) = subcommand {
90        let output_kind = value_t!(matches, "output", OutputKind).unwrap_or(OutputKind::Rendered);
91
92        let json_arg = if matches!(output_kind, OutputKind::GitHub) {
93            // Colorless
94            "--message-format=json"
95        } else {
96            // Colored
97            "--message-format=json-diagnostic-rendered-ansi"
98        };
99
100        // Spawn the subprocess
101        let mut child = Command::new(subcommand_name)
102            .args(subcommand_args)
103            .arg(json_arg)
104            .args(&subcommand_extra_args)
105            .stdout(Stdio::piped()) // filter stdout
106            .stderr(Stdio::inherit()) // do not filter stderr
107            .spawn()
108            .with_context(|| {
109                format!(
110                    "Failed to start subprocess {:?} with arguments {:?}",
111                    subcommand, subcommand_args,
112                )
113            })?;
114
115        // Process output
116        let stdout = child
117            .stdout
118            .as_mut()
119            .with_context(|| "Failed to open standard output of subprocess")?;
120        process_stream(BufReader::new(stdout), &file_changes, output_kind)?;
121
122        // Wait for end of subprocess
123        let exit_status = child
124            .wait()
125            .with_context(|| "Failed to wait for subprocess")?;
126        if !exit_status.success() {
127            bail!(
128                "Subprocess terminated with exit code {}",
129                exit_status.code().unwrap_or(-1)
130            )
131        }
132    } else {
133        // Process standard input
134        let output_kind = value_t!(matches, "output", OutputKind).unwrap_or(OutputKind::Json);
135        process_stream(io::stdin().lock(), &file_changes, output_kind)?;
136    }
137
138    Ok(())
139}
140
141fn process_stream<T: BufRead>(
142    stream: T,
143    file_changes: &FileChanges,
144    output: OutputKind,
145) -> Result<()> {
146    for maybe_line in stream.lines() {
147        let json_line =
148            maybe_line.with_context(|| "Failed to read line from standard output of subprocess")?;
149        let diagnostic: Diagnostic = serde_json::from_str(&json_line).with_context(|| {
150            format!("Failed to parse JSON from standard input: {:?}", json_line)
151        })?;
152        if should_report_diagnostic(&diagnostic, &file_changes) {
153            report_diagnostic(&json_line, &diagnostic, output);
154        }
155    }
156    Ok(())
157}
158
159/// Return `false` iff the message is a warning not related to changed lines.
160fn should_report_diagnostic(diagnostic: &Diagnostic, file_changes: &FileChanges) -> bool {
161    if let Some(ref message) = diagnostic.message {
162        if matches!(message.level, Level::Warning) {
163            let mut intersects_changes = false;
164            for span in &message.spans {
165                if let Some(file_changes) = file_changes.get(&span.file_name).as_ref() {
166                    if intersect_intervals(span.line_start, span.line_end, file_changes) {
167                        intersects_changes = true;
168                        break;
169                    }
170                }
171            }
172            if !intersects_changes {
173                return false;
174            }
175        }
176    }
177    true
178}