rustfmt_wrapper/
lib.rs

1// Copyright 2022 Oxide Computer Company
2
3//! Use `rustfmt` to format generated code:
4//! ```
5//! let codegen = quote::quote!{ struct Foo { bar: String } };
6//! let formatted: String = rustfmt_wrapper::rustfmt(codegen).unwrap();
7//! ```
8
9use std::{
10    env,
11    io::Write,
12    path::PathBuf,
13    process::{Command, Stdio},
14};
15
16use thiserror::Error;
17
18pub mod config;
19
20#[derive(Error, Debug)]
21pub enum Error {
22    /// Command `rustfmt` could not be found
23    #[error("rustfmt is not installed")]
24    NoRustfmt,
25    /// Command `rustfmt` produced an error at runtime.
26    #[error("rustfmt runtime error")]
27    Rustfmt(String),
28    /// Nightly channel required, but not found.
29    #[error("nightly channel required for unstable options")]
30    Unstable(String),
31    /// Error with file IO
32    #[error(transparent)]
33    IO(#[from] std::io::Error),
34    /// Error from reading stdin of rustfmt
35    #[error(transparent)]
36    Conversion(#[from] std::string::FromUtf8Error),
37}
38
39/// Use the `rustfmt` command to format the input.
40pub fn rustfmt<T: ToString>(input: T) -> Result<String, Error> {
41    // The only rustfmt default we override is edition = 2018 (vs 2015)
42    let config = config::Config {
43        edition: Some(config::Edition::Edition2018),
44        ..Default::default()
45    };
46    rustfmt_config(config, input)
47}
48
49/// Use the `rustfmt` command to format the input with the given [`Config`].
50///
51/// [`Config`]: config::Config
52pub fn rustfmt_config<T: ToString>(mut config: config::Config, input: T) -> Result<String, Error> {
53    let input = input.to_string();
54
55    // rustfmt's default edition is 2015; our default is 2021.
56    if config.edition.is_none() {
57        config.edition = Some(config::Edition::Edition2018);
58    }
59
60    let mut builder = tempfile::Builder::new();
61    builder.prefix("rustfmt-wrapper");
62    let outdir = builder.tempdir().expect("failed to create tmp file");
63
64    let rustfmt_config_path = outdir.as_ref().join("rustfmt.toml");
65    std::fs::write(
66        rustfmt_config_path,
67        toml::to_string_pretty(&config).unwrap(),
68    )?;
69
70    let rustfmt = which_rustfmt().ok_or(Error::NoRustfmt)?;
71
72    let mut args = vec![format!("--config-path={}", outdir.path().to_str().unwrap())];
73    if config.unstable() {
74        args.push("--unstable-features".to_string())
75    }
76
77    let mut command = Command::new(&rustfmt)
78        .args(args)
79        .stdin(Stdio::piped())
80        .stdout(Stdio::piped())
81        .stderr(Stdio::piped())
82        .spawn()
83        .unwrap();
84
85    let mut stdin = command.stdin.take().unwrap();
86    std::thread::spawn(move || {
87        stdin
88            .write_all(input.as_bytes())
89            .expect("Failed to write to stdin");
90    });
91
92    let output = command.wait_with_output()?;
93    if output.status.success() {
94        Ok(String::from_utf8(output.stdout)?)
95    } else {
96        let err_str = String::from_utf8(output.stderr)?;
97        if err_str.contains("Unrecognized option: 'unstable-features'") {
98            Err(Error::Unstable(config.list_unstable()))
99        } else {
100            Err(Error::Rustfmt(err_str))
101        }
102    }
103}
104
105fn which_rustfmt() -> Option<PathBuf> {
106    match env::var_os("RUSTFMT") {
107        Some(which) => {
108            if which.is_empty() {
109                None
110            } else {
111                Some(PathBuf::from(which))
112            }
113        }
114        None => toolchain_find::find_installed_component("rustfmt"),
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use crate::{config::Config, rustfmt, rustfmt_config};
121    use newline_converter::dos2unix;
122    use quote::quote;
123
124    #[test]
125    fn test_basics() {
126        let code = quote! { struct Foo { bar: String } };
127        assert_eq!(
128            dos2unix(rustfmt(code).unwrap().as_str()),
129            "struct Foo {\n    bar: String,\n}\n"
130        );
131    }
132
133    #[test]
134    fn test_doc_comments() {
135        let comment = "This is a very long doc comment that could span \
136        multiple lines of text. For the purposes of this test, we're hoping \
137        that it gets formatted into a single, nice doc comment.";
138        let code = quote! {
139           #[doc = #comment]
140           struct Foo { bar: String }
141        };
142
143        let config = Config {
144            normalize_doc_attributes: Some(true),
145            wrap_comments: Some(true),
146            ..Default::default()
147        };
148
149        assert_eq!(
150            dos2unix(rustfmt_config(config, code).unwrap().as_str()),
151            r#"///This is a very long doc comment that could span multiple lines of text. For
152/// the purposes of this test, we're hoping that it gets formatted into a
153/// single, nice doc comment.
154struct Foo {
155    bar: String,
156}
157"#,
158        );
159    }
160
161    #[test]
162    fn test_narrow_call() {
163        let code = quote! {
164            async fn go() {
165                let _ = Client::new().operation_id().send().await?;
166            }
167        };
168
169        let config = Config {
170            max_width: Some(45),
171            ..Default::default()
172        };
173
174        assert_eq!(
175            dos2unix(rustfmt_config(config, code).unwrap().as_str()),
176            "async fn go() {
177    let _ = Client::new()
178        .operation_id()
179        .send()
180        .await?;
181}\n"
182        );
183    }
184}
OSZAR »