clang_format/
lib.rs

1// SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3// SPDX-FileContributor: Gerhard de Clercq <gerhard.declercq@kdab.com>
4//
5// SPDX-License-Identifier: MIT OR Apache-2.0
6
7#![deny(missing_docs)]
8
9//! A basic clang-format Rust wrapper.
10//!
11//! This allows for formatting a given input using `clang-format` from the system.
12
13use std::env;
14use std::io::Write;
15use std::process::{Command, Stdio};
16use thiserror::Error;
17
18/// Describes the style to pass to clang-format
19///
20/// This list is created from
21/// <https://clang.llvm.org/docs/ClangFormatStyleOptions.html#basedonstyle>
22#[derive(Debug, PartialEq)]
23#[non_exhaustive]
24pub enum ClangFormatStyle {
25    /// A style complying with [Chromium’s style guide](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/styleguide/styleguide.md)
26    Chromium,
27    /// Use the default clang-format style
28    Default,
29    /// clang-format will try to find the .clang-format file located in the closest parent directory of the current directory.
30    File,
31    /// A style complying with the [GNU coding standards](https://www.gnu.org/prep/standards/standards.html)
32    ///
33    /// Since clang-format 11
34    GNU,
35    /// A style complying with [Google’s C++ style guide](https://google.github.io/styleguide/cppguide.html)
36    Google,
37    /// A style complying with the [LLVM coding standards](https://llvm.org/docs/CodingStandards.html)
38    Llvm,
39    /// A style complying with [Microsoft’s style guide](https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference)
40    ///
41    /// Since clang-format 9
42    Microsoft,
43    /// A style complying with [Mozilla’s style guide](https://firefox-source-docs.mozilla.org/code-quality/coding-style/index.html)
44    Mozilla,
45    /// A style complying with [WebKit’s style guide](https://www.webkit.org/coding/coding-style.html)
46    WebKit,
47    /// Specify a custom input to the `--style` argument of clang-format
48    ///
49    /// # Example
50    ///
51    /// ```
52    /// # use clang_format::{clang_format_with_style, ClangFormatStyle};
53    /// # fn main() {
54    /// # let input = r#"
55    /// #     struct Test {
56    /// #         bool field;
57    /// #     };
58    /// # "#;
59    /// let style = ClangFormatStyle::Custom("{ BasedOnStyle: Mozilla, IndentWidth: 8 }".to_string());
60    /// # let output = clang_format_with_style(input, &style);
61    /// # assert!(output.is_ok());
62    /// # assert_eq!(output.unwrap(), "\nstruct Test\n{\n        bool field;\n};\n");
63    /// # }
64    /// ```
65    Custom(String),
66}
67
68impl ClangFormatStyle {
69    /// Converts the enum ClangFormatStyle to a string that clang-format expects
70    fn as_str(&self) -> &str {
71        match self {
72            Self::Chromium => "Chromium",
73            // Will use clang-format default options
74            Self::Default => "{}",
75            // Will look in parent directories for a .clang-format file
76            Self::File => "file",
77            Self::GNU => "GNU",
78            Self::Google => "Google",
79            Self::Llvm => "LLVM",
80            Self::Microsoft => "Microsoft",
81            Self::Mozilla => "Mozilla",
82            Self::WebKit => "WebKit",
83            // Custom style arguments
84            Self::Custom(custom) => custom.as_str(),
85        }
86    }
87}
88
89/// Describes which error spawning clang-format failed with
90#[derive(Error, Debug)]
91enum ClangFormatError {
92    #[error(transparent)]
93    Io(#[from] std::io::Error),
94    #[error(transparent)]
95    FromUtf8Error(#[from] std::string::FromUtf8Error),
96    // TODO: use ExitStatusError once it is a stable feature
97    // https://doc.rust-lang.org/stable/std/process/struct.ExitStatusError.html
98    // https://github.com/rust-lang/rust/issues/84908
99    #[error("Clang format process exited with a non-zero status")]
100    NonZeroExitStatus,
101}
102
103/// Execute clang-format with the given input, using the given style, and collect the output
104///
105/// # Example
106///
107/// ```
108/// # use clang_format::{clang_format_with_style, ClangFormatStyle};
109/// # fn main() {
110/// let input = r#"
111///     struct Test {
112///
113///     };
114/// "#;
115/// let output = clang_format_with_style(input, &ClangFormatStyle::Mozilla);
116/// assert!(output.is_ok());
117/// assert_eq!(output.unwrap(), "\nstruct Test\n{};\n");
118/// # }
119/// ```
120pub fn clang_format_with_style(
121    input: &str,
122    style: &ClangFormatStyle,
123) -> Result<String, impl std::error::Error> {
124    // Create and try to spawn the command with the specified style
125    let clang_binary = env::var("CLANG_FORMAT_BINARY").unwrap_or("clang-format".to_string());
126    let mut child = Command::new(clang_binary.as_str())
127        .arg(format!("--style={}", style.as_str()))
128        .stdin(Stdio::piped())
129        .stdout(Stdio::piped())
130        .spawn()?;
131
132    // Write the input to stdin
133    //
134    // Note we place inside a scope to ensure that stdin is closed
135    {
136        let mut stdin = child.stdin.take().expect("no stdin handle");
137        write!(stdin, "{}", input)?;
138    }
139
140    // Wait for the output and parse it
141    let output = child.wait_with_output()?;
142    // TODO: use exit_ok() once it is a stable feature
143    // https://doc.rust-lang.org/stable/std/process/struct.ExitStatus.html#method.exit_ok
144    // https://github.com/rust-lang/rust/issues/84908
145    if output.status.success() {
146        Ok(String::from_utf8(output.stdout)?)
147    } else {
148        Err(ClangFormatError::NonZeroExitStatus)
149    }
150}
151
152/// Execute clang-format with the given input and collect the output
153///
154/// Note that this uses `ClangFormatStyle::Default` as the style.
155///
156/// # Example
157///
158/// ```
159/// # use clang_format::clang_format;
160/// # fn main() {
161/// let input = r#"
162///     struct Test {
163///
164///     };
165/// "#;
166/// let output = clang_format(input);
167/// assert!(output.is_ok());
168/// assert_eq!(output.unwrap(), "\nstruct Test {};\n");
169/// # }
170/// ```
171pub fn clang_format(input: &str) -> Result<String, impl std::error::Error> {
172    clang_format_with_style(input, &ClangFormatStyle::Default)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn format_default() {
181        let input = r#"
182            struct Test {
183
184            };
185        "#;
186        let output = clang_format_with_style(input, &ClangFormatStyle::Default);
187        assert!(output.is_ok());
188        assert_eq!(output.unwrap(), "\nstruct Test {};\n");
189    }
190
191    #[test]
192    fn format_mozilla() {
193        let input = r#"
194            struct Test {
195
196            };
197        "#;
198        let output = clang_format_with_style(input, &ClangFormatStyle::Mozilla);
199        assert!(output.is_ok());
200        assert_eq!(output.unwrap(), "\nstruct Test\n{};\n");
201    }
202
203    #[test]
204    fn format_custom() {
205        let input = r#"
206            struct Test {
207                bool field;
208            };
209        "#;
210
211        // Test multiple lines and single quotes
212        {
213            let output = clang_format_with_style(
214                input,
215                &ClangFormatStyle::Custom(
216                    "{BasedOnStyle: 'Mozilla',
217                    IndentWidth: 8}"
218                        .to_string(),
219                ),
220            );
221            assert!(output.is_ok());
222            assert_eq!(
223                output.unwrap(),
224                "\nstruct Test\n{\n        bool field;\n};\n"
225            );
226        }
227
228        // Test single line and double quotes
229        {
230            let output = clang_format_with_style(
231                input,
232                &ClangFormatStyle::Custom(
233                    "{ BasedOnStyle: \"Mozilla\", IndentWidth: 4 }".to_string(),
234                ),
235            );
236            assert!(output.is_ok());
237            assert_eq!(output.unwrap(), "\nstruct Test\n{\n    bool field;\n};\n");
238        }
239    }
240}