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    fs,
6    process::Command,
7    sync::{Arc, Mutex, MutexGuard},
8};
9
10use anyhow::{Context, Result};
11use log::Level;
12use serde::Deserialize;
13
14// project-specific crates/modules
15use 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    /// A list of [`Replacement`]s that clang-tidy wants to make.
24    #[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/// A single replacement that clang-format wants to make.
41#[derive(Debug, PartialEq, Eq, Default, Clone, Copy, Deserialize)]
42pub struct Replacement {
43    /// The byte offset where the replacement will start.
44    #[serde(rename = "@offset")]
45    pub offset: u32,
46
47    /// The line number described by the [`Replacement::offset`].
48    ///
49    /// This value is not provided by the XML output, but we calculate it after
50    /// deserialization.
51    #[serde(default)]
52    pub line: u32,
53}
54
55/// Get a string that summarizes the given `--style`
56pub fn summarize_style(style: &str) -> String {
57    if ["google", "chromium", "microsoft", "mozilla", "webkit"].contains(&style) {
58        // capitalize the first letter
59        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
69/// Get a total count of clang-format advice from the given list of [FileObj]s.
70pub 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
83/// Run clang-tidy for a specific `file`, then parse and return it's XML output.
84pub 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        // get line and column numbers from format_advice.offset
167        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                // lines_changed_only is disabled
179                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'>&#10;      </replacement>
204<replacement offset='147' length='0'> </replacement>
205<replacement offset='161' length='0'></replacement>
206<replacement offset='165' length='19'>&#10;&#10;</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}