cpp_linter/clang_tools/
clang_format.rs1use std::{
5 fs,
6 process::Command,
7 sync::{Arc, Mutex, MutexGuard},
8};
9
10use anyhow::{Context, Result};
11use log::Level;
12use serde::Deserialize;
13
14use super::MakeSuggestions;
16use crate::{
17 cli::ClangParams,
18 common_fs::{get_line_count_from_offset, FileObj},
19};
20
21#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
22pub struct FormatAdvice {
23 #[serde(rename(deserialize = "replacement"))]
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 if ["google", "chromium", "microsoft", "mozilla", "webkit"].contains(&style) {
58 let mut char_iter = style.chars();
60 let first_char = char_iter.next().unwrap();
61 first_char.to_uppercase().collect::<String>() + char_iter.as_str()
62 } else if style == "llvm" || style == "gnu" {
63 style.to_ascii_uppercase()
64 } else {
65 String::from("Custom")
66 }
67}
68
69pub fn tally_format_advice(files: &[Arc<Mutex<FileObj>>]) -> u64 {
71 let mut total = 0;
72 for file in files {
73 let file = file.lock().unwrap();
74 if let Some(advice) = &file.format_advice {
75 if !advice.replacements.is_empty() {
76 total += 1;
77 }
78 }
79 }
80 total
81}
82
83pub fn run_clang_format(
85 file: &mut MutexGuard<FileObj>,
86 clang_params: &ClangParams,
87) -> Result<Vec<(log::Level, String)>> {
88 let mut cmd = Command::new(clang_params.clang_format_command.as_ref().unwrap());
89 let mut logs = vec![];
90 cmd.args(["--style", &clang_params.style]);
91 let ranges = file.get_ranges(&clang_params.lines_changed_only);
92 for range in &ranges {
93 cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
94 }
95 let file_name = file.name.to_string_lossy().to_string();
96 cmd.arg(file.name.to_path_buf().as_os_str());
97 let patched = if !clang_params.format_review {
98 None
99 } else {
100 logs.push((
101 Level::Info,
102 format!(
103 "Getting format fixes with \"{} {}\"",
104 clang_params
105 .clang_format_command
106 .as_ref()
107 .unwrap()
108 .to_str()
109 .unwrap_or_default(),
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 {
155 replacements: vec![],
156 patched: None,
157 }
158 };
159 format_advice.patched = patched;
160 if !format_advice.replacements.is_empty() {
161 let original_contents = fs::read(&file.name).with_context(|| {
162 format!(
163 "Failed to read file's original content before translating byte offsets: {file_name}",
164 )
165 })?;
166 let mut filtered_replacements = Vec::new();
168 for replacement in &mut format_advice.replacements {
169 let line_number = get_line_count_from_offset(&original_contents, replacement.offset);
170 replacement.line = line_number;
171 for range in &ranges {
172 if range.contains(&line_number) {
173 filtered_replacements.push(*replacement);
174 break;
175 }
176 }
177 if ranges.is_empty() {
178 filtered_replacements.push(*replacement);
180 }
181 }
182 format_advice.replacements = filtered_replacements;
183 }
184 file.format_advice = Some(format_advice);
185 Ok(logs)
186}
187
188#[cfg(test)]
189mod tests {
190 use super::{summarize_style, FormatAdvice, Replacement};
191
192 #[test]
193 fn parse_blank_xml() {
194 let xml = String::new();
195 let result = quick_xml::de::from_str::<FormatAdvice>(&xml);
196 assert!(result.is_err());
197 }
198
199 #[test]
200 fn parse_xml() {
201 let xml_raw = r#"<?xml version='1.0'?>
202<replacements xml:space='preserve' incomplete_format='false'>
203<replacement offset='113' length='5'> </replacement>
204<replacement offset='147' length='0'> </replacement>
205<replacement offset='161' length='0'></replacement>
206<replacement offset='165' length='19'> </replacement>
207</replacements>"#
208 .as_bytes()
209 .to_vec();
210
211 let expected = FormatAdvice {
212 replacements: [113, 147, 161, 165]
213 .iter()
214 .map(|offset| Replacement {
215 offset: *offset,
216 ..Default::default()
217 })
218 .collect(),
219 patched: None,
220 };
221
222 let xml = String::from_utf8(xml_raw).unwrap();
223
224 let document = quick_xml::de::from_str::<FormatAdvice>(&xml).unwrap();
225 assert_eq!(expected, document);
226 }
227
228 fn formalize_style(style: &str, expected: &str) {
229 assert_eq!(summarize_style(style), expected);
230 }
231
232 #[test]
233 fn formalize_llvm_style() {
234 formalize_style("llvm", "LLVM");
235 }
236
237 #[test]
238 fn formalize_google_style() {
239 formalize_style("google", "Google");
240 }
241
242 #[test]
243 fn formalize_custom_style() {
244 formalize_style("file", "Custom");
245 }
246}