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 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 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 let git_diff_args = matches.values_of("args").unwrap_or_default();
66
67 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 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 "--message-format=json"
95 } else {
96 "--message-format=json-diagnostic-rendered-ansi"
98 };
99
100 let mut child = Command::new(subcommand_name)
102 .args(subcommand_args)
103 .arg(json_arg)
104 .args(&subcommand_extra_args)
105 .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn()
108 .with_context(|| {
109 format!(
110 "Failed to start subprocess {:?} with arguments {:?}",
111 subcommand, subcommand_args,
112 )
113 })?;
114
115 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 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 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
159fn 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}