1use std::{
5 process::Command,
6 sync::{Arc, Mutex, MutexGuard},
7};
8
9use anyhow::{Context, Result};
10use log::Level;
11use serde::Deserialize;
13use serde_xml_rs::de::Deserializer;
14
15use super::MakeSuggestions;
17use crate::{
18 cli::ClangParams,
19 common_fs::{get_line_cols_from_offset, FileObj},
20};
21
22#[derive(Debug, Deserialize, PartialEq, Clone)]
24#[serde(rename = "replacements")]
25pub struct FormatAdvice {
26 #[serde(rename = "$value")]
28 pub replacements: Vec<Replacement>,
29
30 pub patched: Option<Vec<u8>>,
31}
32
33impl MakeSuggestions for FormatAdvice {
34 fn get_suggestion_help(&self, _start_line: u32, _end_line: u32) -> String {
35 String::from("### clang-format suggestions\n")
36 }
37
38 fn get_tool_name(&self) -> String {
39 "clang-format".to_string()
40 }
41}
42
43#[derive(Debug, Deserialize, PartialEq)]
45pub struct Replacement {
46 pub offset: usize,
48
49 pub length: usize,
51
52 #[serde(rename = "$value")]
54 pub value: Option<String>,
55
56 #[serde(default)]
61 pub line: usize,
62
63 #[serde(default)]
68 pub cols: usize,
69}
70
71impl Clone for Replacement {
72 fn clone(&self) -> Self {
73 Replacement {
74 offset: self.offset,
75 length: self.length,
76 value: self.value.clone(),
77 line: self.line,
78 cols: self.cols,
79 }
80 }
81}
82
83pub fn summarize_style(style: &str) -> String {
85 if ["google", "chromium", "microsoft", "mozilla", "webkit"].contains(&style) {
86 let mut char_iter = style.chars();
88 let first_char = char_iter.next().unwrap();
89 first_char.to_uppercase().collect::<String>() + char_iter.as_str()
90 } else if style == "llvm" || style == "gnu" {
91 style.to_ascii_uppercase()
92 } else {
93 String::from("Custom")
94 }
95}
96
97pub fn tally_format_advice(files: &[Arc<Mutex<FileObj>>]) -> u64 {
99 let mut total = 0;
100 for file in files {
101 let file = file.lock().unwrap();
102 if let Some(advice) = &file.format_advice {
103 if !advice.replacements.is_empty() {
104 total += 1;
105 }
106 }
107 }
108 total
109}
110
111pub fn run_clang_format(
113 file: &mut MutexGuard<FileObj>,
114 clang_params: &ClangParams,
115) -> Result<Vec<(log::Level, String)>> {
116 let mut cmd = Command::new(clang_params.clang_format_command.as_ref().unwrap());
117 let mut logs = vec![];
118 cmd.args(["--style", &clang_params.style]);
119 let ranges = file.get_ranges(&clang_params.lines_changed_only);
120 for range in &ranges {
121 cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
122 }
123 let file_name = file.name.to_string_lossy().to_string();
124 cmd.arg(file.name.to_path_buf().as_os_str());
125 let mut patched = None;
126 if clang_params.format_review {
127 logs.push((
128 Level::Info,
129 format!(
130 "Getting format fixes with \"{} {}\"",
131 clang_params
132 .clang_format_command
133 .as_ref()
134 .unwrap()
135 .to_str()
136 .unwrap_or_default(),
137 cmd.get_args()
138 .map(|a| a.to_str().unwrap())
139 .collect::<Vec<&str>>()
140 .join(" ")
141 ),
142 ));
143 patched = Some(
144 cmd.output()
145 .with_context(|| format!("Failed to get fixes from clang-format: {file_name}"))?
146 .stdout,
147 );
148 }
149 cmd.arg("--output-replacements-xml");
150 logs.push((
151 log::Level::Info,
152 format!(
153 "Running \"{} {}\"",
154 cmd.get_program().to_string_lossy(),
155 cmd.get_args()
156 .map(|x| x.to_str().unwrap())
157 .collect::<Vec<&str>>()
158 .join(" ")
159 ),
160 ));
161 let output = cmd
162 .output()
163 .with_context(|| format!("Failed to get replacements from clang-format: {file_name}"))?;
164 if !output.stderr.is_empty() || !output.status.success() {
165 logs.push((
166 log::Level::Debug,
167 format!(
168 "clang-format raised the follow errors:\n{}",
169 String::from_utf8_lossy(&output.stderr)
170 ),
171 ));
172 }
173 if output.stdout.is_empty() {
174 return Ok(logs);
175 }
176 let xml = String::from_utf8(output.stdout)
177 .with_context(|| format!("stdout from clang-format was not UTF-8 encoded: {file_name}"))?
178 .lines()
179 .collect::<Vec<&str>>()
180 .join("");
181 let config = serde_xml_rs::ParserConfig::new()
182 .trim_whitespace(false)
183 .whitespace_to_characters(true)
184 .ignore_root_level_whitespace(true);
185 let event_reader = serde_xml_rs::EventReader::new_with_config(xml.as_bytes(), config);
186 let mut format_advice = FormatAdvice::deserialize(&mut Deserializer::new(event_reader))
187 .unwrap_or(FormatAdvice {
188 replacements: vec![],
189 patched: None,
190 });
191 format_advice.patched = patched;
192 if !format_advice.replacements.is_empty() {
193 let mut filtered_replacements = Vec::new();
195 for replacement in &mut format_advice.replacements {
196 let (line_number, columns) = get_line_cols_from_offset(&file.name, replacement.offset);
197 replacement.line = line_number;
198 replacement.cols = columns;
199 for range in &ranges {
200 if range.contains(&line_number.try_into().unwrap_or(0)) {
201 filtered_replacements.push(replacement.clone());
202 break;
203 }
204 }
205 if ranges.is_empty() {
206 filtered_replacements.push(replacement.clone());
208 }
209 }
210 format_advice.replacements = filtered_replacements;
211 }
212 file.format_advice = Some(format_advice);
213 Ok(logs)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::{summarize_style, FormatAdvice, Replacement};
219 use serde::Deserialize;
220
221 #[test]
222 fn parse_xml() {
223 let xml_raw = r#"<?xml version='1.0'?>
224<replacements xml:space='preserve' incomplete_format='false'>
225<replacement offset='113' length='5'> </replacement>
226<replacement offset='147' length='0'> </replacement>
227<replacement offset='161' length='0'></replacement>
228<replacement offset='165' length='19'> </replacement>
229</replacements>"#;
230 let xml = xml_raw.lines().collect::<Vec<&str>>().join("");
232
233 let expected = FormatAdvice {
234 replacements: vec![
235 Replacement {
236 offset: 113,
237 length: 5,
238 value: Some(String::from("\n ")),
239 line: 0,
240 cols: 0,
241 },
242 Replacement {
243 offset: 147,
244 length: 0,
245 value: Some(String::from(" ")),
246 line: 0,
247 cols: 0,
248 },
249 Replacement {
250 offset: 161,
251 length: 0,
252 value: None,
253 line: 0,
254 cols: 0,
255 },
256 Replacement {
257 offset: 165,
258 length: 19,
259 value: Some(String::from("\n\n")),
260 line: 0,
261 cols: 0,
262 },
263 ],
264 patched: None,
265 };
266 let config = serde_xml_rs::ParserConfig::new()
267 .trim_whitespace(false)
268 .whitespace_to_characters(true)
269 .ignore_root_level_whitespace(true);
270 let event_reader = serde_xml_rs::EventReader::new_with_config(xml.as_bytes(), config);
271 let document =
272 FormatAdvice::deserialize(&mut serde_xml_rs::de::Deserializer::new(event_reader))
273 .unwrap();
274 assert_eq!(expected, document);
275 }
276
277 fn formalize_style(style: &str, expected: &str) {
278 assert_eq!(summarize_style(style), expected);
279 }
280
281 #[test]
282 fn formalize_llvm_style() {
283 formalize_style("llvm", "LLVM");
284 }
285
286 #[test]
287 fn formalize_google_style() {
288 formalize_style("google", "Google");
289 }
290
291 #[test]
292 fn formalize_custom_style() {
293 formalize_style("file", "Custom");
294 }
295}