1use 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 #[error("rustfmt is not installed")]
24 NoRustfmt,
25 #[error("rustfmt runtime error")]
27 Rustfmt(String),
28 #[error("nightly channel required for unstable options")]
30 Unstable(String),
31 #[error(transparent)]
33 IO(#[from] std::io::Error),
34 #[error(transparent)]
36 Conversion(#[from] std::string::FromUtf8Error),
37}
38
39pub fn rustfmt<T: ToString>(input: T) -> Result<String, Error> {
41 let config = config::Config {
43 edition: Some(config::Edition::Edition2018),
44 ..Default::default()
45 };
46 rustfmt_config(config, input)
47}
48
49pub fn rustfmt_config<T: ToString>(mut config: config::Config, input: T) -> Result<String, Error> {
53 let input = input.to_string();
54
55 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}