cpp_linter/clang_tools/
clang_format.rs

1//! This module holds functionality specific to running clang-format and parsing it's
2//! output.
3
4use std::{
5    process::Command,
6    sync::{Arc, Mutex, MutexGuard},
7};
8
9use anyhow::{Context, Result};
10use log::Level;
11// non-std crates
12use serde::Deserialize;
13use serde_xml_rs::de::Deserializer;
14
15// project-specific crates/modules
16use super::MakeSuggestions;
17use crate::{
18    cli::ClangParams,
19    common_fs::{get_line_cols_from_offset, FileObj},
20};
21
22/// A Structure used to deserialize clang-format's XML output.
23#[derive(Debug, Deserialize, PartialEq, Clone)]
24#[serde(rename = "replacements")]
25pub struct FormatAdvice {
26    /// A list of [`Replacement`]s that clang-tidy wants to make.
27    #[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/// A single replacement that clang-format wants to make.
44#[derive(Debug, Deserialize, PartialEq)]
45pub struct Replacement {
46    /// The byte offset where the replacement will start.
47    pub offset: usize,
48
49    /// The amount of bytes that will be removed.
50    pub length: usize,
51
52    /// The bytes (UTF-8 encoded) that will be added at the [`Replacement::offset`] position.
53    #[serde(rename = "$value")]
54    pub value: Option<String>,
55
56    /// The line number described by the [`Replacement::offset`].
57    ///
58    /// This value is not provided by the XML output, but we calculate it after
59    /// deserialization.
60    #[serde(default)]
61    pub line: usize,
62
63    /// The column number on the line described by the [`Replacement::offset`].
64    ///
65    /// This value is not provided by the XML output, but we calculate it after
66    /// deserialization.
67    #[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
83/// Get a string that summarizes the given `--style`
84pub fn summarize_style(style: &str) -> String {
85    if ["google", "chromium", "microsoft", "mozilla", "webkit"].contains(&style) {
86        // capitalize the first letter
87        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
97/// Get a total count of clang-format advice from the given list of [FileObj]s.
98pub 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
111/// Run clang-tidy for a specific `file`, then parse and return it's XML output.
112pub 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        // get line and column numbers from format_advice.offset
194        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                // lines_changed_only is disabled
207                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'>&#10;      </replacement>
226<replacement offset='147' length='0'> </replacement>
227<replacement offset='161' length='0'></replacement>
228<replacement offset='165' length='19'>&#10;&#10;</replacement>
229</replacements>"#;
230        //since whitespace is part of the elements' body, we need to remove the LFs first
231        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}