cpp_linter/clang_tools/
clang_format.rs1use std::{
5 fs,
6 process::Command,
7 sync::{Arc, Mutex, MutexGuard},
8};
9
10use anyhow::{Context, Result, anyhow};
11use log::Level;
12use serde::Deserialize;
13
14use super::MakeSuggestions;
16use crate::{
17 cli::ClangParams,
18 common_fs::{FileObj, get_line_count_from_offset},
19};
20
21#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
22pub struct FormatAdvice {
23 #[serde(rename(deserialize = "replacement"), default)]
25 pub replacements: Vec<Replacement>,
26
27 pub patched: Option<Vec<u8>>,
28}
29
30impl MakeSuggestions for FormatAdvice {
31 fn get_suggestion_help(&self, _start_line: u32, _end_line: u32) -> String {
32 String::from("### clang-format suggestions\n")
33 }
34
35 fn get_tool_name(&self) -> String {
36 "clang-format".to_string()
37 }
38}
39
40#[derive(Debug, PartialEq, Eq, Default, Clone, Copy, Deserialize)]
42pub struct Replacement {
43 #[serde(rename = "@offset")]
45 pub offset: u32,
46
47 #[serde(default)]
52 pub line: u32,
53}
54
55pub fn summarize_style(style: &str) -> String {
57 let mut char_iter = style.chars();
58 if ["google", "chromium", "microsoft", "mozilla", "webkit"].contains(&style)
59 && let Some(first_char) = char_iter.next()
60 {
61 first_char.to_ascii_uppercase().to_string() + char_iter.as_str()
63 } else if style == "llvm" || style == "gnu" {
64 style.to_ascii_uppercase()
65 } else {
66 String::from("Custom")
67 }
68}
69
70pub fn tally_format_advice(files: &[Arc<Mutex<FileObj>>]) -> Result<u64, String> {
72 let mut total = 0;
73 for file in files {
74 let file = file.lock().map_err(|e| e.to_string())?;
75 if let Some(advice) = &file.format_advice
76 && !advice.replacements.is_empty()
77 {
78 total += 1;
79 }
80 }
81 Ok(total)
82}
83
84pub fn run_clang_format(
86 file: &mut MutexGuard<FileObj>,
87 clang_params: &ClangParams,
88) -> Result<Vec<(log::Level, String)>> {
89 let cmd_path = clang_params
90 .clang_format_command
91 .as_ref()
92 .ok_or(anyhow!("clang-format path unknown"))?;
93 let mut cmd = Command::new(cmd_path);
94 let mut logs = vec![];
95 cmd.args(["--style", &clang_params.style]);
96 let ranges = file.get_ranges(&clang_params.lines_changed_only);
97 for range in &ranges {
98 cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
99 }
100 let file_name = file.name.to_string_lossy().to_string();
101 cmd.arg(file.name.to_path_buf().as_os_str());
102 let patched = if !clang_params.format_review {
103 None
104 } else {
105 logs.push((
106 Level::Info,
107 format!(
108 "Getting format fixes with \"{} {}\"",
109 cmd.get_program().to_string_lossy(),
110 cmd.get_args()
111 .map(|a| a.to_string_lossy())
112 .collect::<Vec<_>>()
113 .join(" ")
114 ),
115 ));
116 Some(
117 cmd.output()
118 .with_context(|| format!("Failed to get fixes from clang-format: {file_name}"))?
119 .stdout,
120 )
121 };
122 cmd.arg("--output-replacements-xml");
123 logs.push((
124 log::Level::Info,
125 format!(
126 "Running \"{} {}\"",
127 cmd.get_program().to_string_lossy(),
128 cmd.get_args()
129 .map(|x| x.to_string_lossy())
130 .collect::<Vec<_>>()
131 .join(" ")
132 ),
133 ));
134 let output = cmd
135 .output()
136 .with_context(|| format!("Failed to get replacements from clang-format: {file_name}"))?;
137 if !output.stderr.is_empty() || !output.status.success() {
138 logs.push((
139 log::Level::Debug,
140 format!(
141 "clang-format raised the follow errors:\n{}",
142 String::from_utf8_lossy(&output.stderr)
143 ),
144 ));
145 }
146 let mut format_advice = if !output.stdout.is_empty() {
147 let xml = String::from_utf8(output.stdout).with_context(|| {
148 format!("XML output from clang-format was not UTF-8 encoded: {file_name}")
149 })?;
150 quick_xml::de::from_str::<FormatAdvice>(&xml).with_context(|| {
151 format!("Failed to parse XML output from clang-format for {file_name}")
152 })?
153 } else {
154 FormatAdvice::default()
155 };
156 format_advice.patched = patched;
157 if !format_advice.replacements.is_empty() {
158 let original_contents = fs::read(&file.name).with_context(|| {
159 format!(
160 "Failed to read file's original content before translating byte offsets: {file_name}",
161 )
162 })?;
163 let mut filtered_replacements = Vec::new();
165 for replacement in &mut format_advice.replacements {
166 let line_number = get_line_count_from_offset(&original_contents, replacement.offset);
167 replacement.line = line_number;
168 for range in &ranges {
169 if range.contains(&line_number) {
170 filtered_replacements.push(*replacement);
171 break;
172 }
173 }
174 if ranges.is_empty() {
175 filtered_replacements.push(*replacement);
177 }
178 }
179 format_advice.replacements = filtered_replacements;
180 }
181 file.format_advice = Some(format_advice);
182 Ok(logs)
183}
184
185#[cfg(test)]
186mod tests {
187 #![allow(clippy::unwrap_used)]
188
189 use super::{FormatAdvice, Replacement, summarize_style};
190
191 #[test]
192 fn parse_blank_xml() {
193 let xml = String::new();
194 let result = quick_xml::de::from_str::<FormatAdvice>(&xml);
195 assert!(result.is_err());
196 }
197
198 #[test]
199 fn parse_xml_no_replacements() {
200 let xml_raw = r#"<?xml version='1.0'?>
201<replacements xml:space='preserve' incomplete_format='false'>
202</replacements>"#
203 .as_bytes()
204 .to_vec();
205 let expected = FormatAdvice::default();
206 let xml = String::from_utf8(xml_raw).unwrap();
207 let document = quick_xml::de::from_str::<FormatAdvice>(&xml).unwrap();
208 assert_eq!(expected, document);
209 }
210
211 #[test]
212 fn parse_xml() {
213 let xml_raw = r#"<?xml version='1.0'?>
214<replacements xml:space='preserve' incomplete_format='false'>
215<replacement offset='113' length='5'> </replacement>
216<replacement offset='147' length='0'> </replacement>
217<replacement offset='161' length='0'></replacement>
218<replacement offset='165' length='19'> </replacement>
219</replacements>"#
220 .as_bytes()
221 .to_vec();
222
223 let expected = FormatAdvice {
224 replacements: [113, 147, 161, 165]
225 .iter()
226 .map(|offset| Replacement {
227 offset: *offset,
228 ..Default::default()
229 })
230 .collect(),
231 patched: None,
232 };
233
234 let xml = String::from_utf8(xml_raw).unwrap();
235
236 let document = quick_xml::de::from_str::<FormatAdvice>(&xml).unwrap();
237 assert_eq!(expected, document);
238 }
239
240 fn formalize_style(style: &str, expected: &str) {
241 assert_eq!(summarize_style(style), expected);
242 }
243
244 #[test]
245 fn formalize_llvm_style() {
246 formalize_style("llvm", "LLVM");
247 }
248
249 #[test]
250 fn formalize_google_style() {
251 formalize_style("google", "Google");
252 }
253
254 #[test]
255 fn formalize_custom_style() {
256 formalize_style("file", "Custom");
257 }
258}