1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![warn(missing_docs)]
3
4#[cfg(feature = "post_process")]
24mod replace;
25
26#[cfg(not(feature = "post_process"))]
27mod replace {
28 use std::borrow::Cow;
29
30 use crate::Error;
31
32 #[inline]
33 pub(crate) fn replace_markers(s: &str, _replace_doc_blocks: bool) -> Result<Cow<str>, Error> {
34 Ok(Cow::Borrowed(s))
35 }
36}
37
38#[cfg(feature = "post_process")]
40#[cfg(feature = "token_stream")]
41#[cfg(doctest)]
42mod test_readme {
43 macro_rules! external_doc_test {
44 ($x:expr) => {
45 #[doc = $x]
46 extern "C" {}
47 };
48 }
49
50 external_doc_test!(include_str!("../README.md"));
51}
52
53use std::borrow::Cow;
54use std::collections::HashMap;
55use std::default::Default;
56use std::ffi::{OsStr, OsString};
57use std::hash::Hash;
58use std::io::{Read, Write};
59use std::path::{Path, PathBuf};
60use std::process::{Command, Stdio};
61use std::{env, fmt, fs, io, string};
62
63const RUST_FMT: &str = "rustfmt";
64const RUST_FMT_KEY: &str = "RUSTFMT";
65
66#[cfg(feature = "post_process")]
79#[cfg_attr(docsrs, doc(cfg(feature = "post_process")))]
80#[macro_export]
81macro_rules! _blank_ {
82 () => {};
83 ($lit:literal) => {};
84}
85
86#[cfg(feature = "post_process")]
98#[cfg_attr(docsrs, doc(cfg(feature = "post_process")))]
99#[macro_export]
100macro_rules! _comment_ {
101 () => {};
102 ($lit:literal) => {};
103}
104
105#[derive(Debug)]
109pub enum Error {
110 IOError(io::Error),
112 UTFConversionError(string::FromUtf8Error),
114 BadSourceCode(String),
116}
117
118impl fmt::Display for Error {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 match self {
121 Error::IOError(err) => <io::Error as fmt::Display>::fmt(err, f),
122 Error::UTFConversionError(err) => <string::FromUtf8Error as fmt::Display>::fmt(err, f),
123 Error::BadSourceCode(cause) => {
124 f.write_str("An error occurred while formatting the source code: ")?;
125 f.write_str(cause)
126 }
127 }
128 }
129}
130
131impl std::error::Error for Error {}
132
133impl From<io::Error> for Error {
134 #[inline]
135 fn from(err: io::Error) -> Self {
136 Error::IOError(err)
137 }
138}
139
140impl From<string::FromUtf8Error> for Error {
141 #[inline]
142 fn from(err: string::FromUtf8Error) -> Self {
143 Error::UTFConversionError(err)
144 }
145}
146
147#[cfg(feature = "syn")]
148impl From<syn::Error> for Error {
149 #[inline]
150 fn from(err: syn::Error) -> Self {
151 Error::BadSourceCode(err.to_string())
152 }
153}
154
155#[derive(Clone, Copy, Debug)]
159pub enum Edition {
160 Rust2015,
162 Rust2018,
164 Rust2021,
166}
167
168impl Edition {
169 #[inline]
170 fn as_os_str(self) -> &'static OsStr {
171 match self {
172 Edition::Rust2015 => "2015",
173 Edition::Rust2018 => "2018",
174 Edition::Rust2021 => "2021",
175 }
176 .as_ref()
177 }
178}
179
180impl Default for Edition {
181 #[inline]
182 fn default() -> Self {
183 Edition::Rust2021
184 }
185}
186
187#[derive(Clone, Copy, Debug)]
191pub enum PostProcess {
192 None,
194
195 #[cfg(feature = "post_process")]
197 #[cfg_attr(docsrs, doc(cfg(feature = "post_process")))]
198 ReplaceMarkers,
199
200 #[cfg(feature = "post_process")]
202 #[cfg_attr(docsrs, doc(cfg(feature = "post_process")))]
203 ReplaceMarkersAndDocBlocks,
204}
205
206impl PostProcess {
207 #[inline]
210 pub fn replace_markers(self) -> bool {
211 !matches!(self, PostProcess::None)
212 }
213
214 #[cfg(feature = "post_process")]
217 #[inline]
218 pub fn replace_doc_blocks(self) -> bool {
219 matches!(self, PostProcess::ReplaceMarkersAndDocBlocks)
220 }
221
222 #[cfg(not(feature = "post_process"))]
225 #[inline]
226 pub fn replace_doc_blocks(self) -> bool {
227 false
228 }
229}
230
231impl Default for PostProcess {
232 #[inline]
233 fn default() -> Self {
234 PostProcess::None
235 }
236}
237
238#[derive(Clone, Debug, Default)]
243pub struct Config<K, P, V>
244where
245 K: Eq + Hash + AsRef<OsStr>,
246 P: Into<PathBuf>,
247 V: AsRef<OsStr>,
248{
249 rust_fmt: Option<P>,
250 edition: Edition,
251 post_proc: PostProcess,
252 options: HashMap<K, V>,
253}
254
255impl<'a, 'b> Config<&'a str, &'b str, &'a str> {
256 #[inline]
259 pub fn new_str() -> Self {
260 Self::new()
261 }
262
263 #[inline]
266 pub fn from_hash_map_str(options: HashMap<&'a str, &'a str>) -> Self {
267 Self::from_hash_map(options)
268 }
269}
270
271impl<K, P, V> Config<K, P, V>
272where
273 K: Eq + Hash + AsRef<OsStr>,
274 P: Into<PathBuf>,
275 V: AsRef<OsStr>,
276{
277 #[inline]
279 pub fn new() -> Self {
280 Self::from_hash_map(HashMap::default())
281 }
282
283 #[inline]
285 pub fn from_hash_map(options: HashMap<K, V>) -> Self {
286 Self {
287 rust_fmt: None,
288 edition: Edition::Rust2021,
289 post_proc: PostProcess::None,
290 options,
291 }
292 }
293
294 #[inline]
297 pub fn rust_fmt_path(mut self, path: P) -> Self {
298 self.rust_fmt = Some(path);
299 self
300 }
301
302 #[inline]
304 pub fn edition(mut self, edition: Edition) -> Self {
305 self.edition = edition;
306 self
307 }
308
309 #[inline]
311 pub fn post_proc(mut self, post_proc: PostProcess) -> Self {
312 self.post_proc = post_proc;
313 self
314 }
315
316 #[inline]
319 pub fn option(mut self, key: K, value: V) -> Self {
320 self.options.insert(key, value);
321 self
322 }
323}
324
325#[inline]
328fn post_process(post_proc: PostProcess, source: String) -> Result<String, Error> {
329 if post_proc.replace_markers() {
330 match replace::replace_markers(&source, post_proc.replace_doc_blocks())? {
331 Cow::Borrowed(_) => Ok(source),
333 Cow::Owned(source) => Ok(source),
335 }
336 } else {
337 Ok(source)
338 }
339}
340
341#[inline]
342fn file_to_string(path: impl AsRef<Path>) -> Result<String, Error> {
343 let mut file = fs::File::open(path.as_ref())?;
345 let len = file.metadata()?.len();
346 let mut source = String::with_capacity(len as usize);
347
348 file.read_to_string(&mut source)?;
349 Ok(source)
350}
351
352#[inline]
353fn string_to_file(path: impl AsRef<Path>, source: &str) -> Result<(), Error> {
354 let mut file = fs::File::create(path)?;
355 file.write_all(source.as_bytes())?;
356 Ok(())
357}
358
359pub trait Formatter {
364 fn format_str(&self, source: impl AsRef<str>) -> Result<String, Error>;
367
368 fn format_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
371 let source = file_to_string(path.as_ref())?;
372 let result = self.format_str(source)?;
373 string_to_file(path, &result)
374 }
375
376 #[cfg(feature = "token_stream")]
379 #[cfg_attr(docsrs, doc(cfg(feature = "token_stream")))]
380 #[inline]
381 fn format_tokens(&self, tokens: proc_macro2::TokenStream) -> Result<String, Error> {
382 self.format_str(tokens.to_string())
383 }
384}
385
386#[derive(Clone)]
412pub struct RustFmt {
413 rust_fmt: PathBuf,
414 edition: Edition,
415 post_proc: PostProcess,
416 config_str: Option<OsString>,
417}
418
419impl RustFmt {
420 #[inline]
422 pub fn new() -> Self {
423 Self::build(None as Option<Config<&OsStr, &OsStr, &OsStr>>)
424 }
425
426 #[inline]
428 pub fn from_config<K, P, V>(config: Config<K, P, V>) -> Self
429 where
430 K: Default + Eq + Hash + AsRef<OsStr>,
431 P: Default + Into<PathBuf>,
432 V: Default + AsRef<OsStr>,
433 {
434 Self::build(Some(config))
435 }
436
437 fn build<K, P, V>(config: Option<Config<K, P, V>>) -> Self
438 where
439 K: Default + Eq + Hash + AsRef<OsStr>,
440 P: Default + Into<PathBuf>,
441 V: Default + AsRef<OsStr>,
442 {
443 let config = config.unwrap_or_default();
444
445 let rust_fmt = match config.rust_fmt {
448 Some(path) => path.into(),
449 None => env::var_os(RUST_FMT_KEY)
450 .unwrap_or_else(|| RUST_FMT.parse().unwrap())
451 .into(),
452 };
453
454 let edition = config.edition;
455 let config_str = Self::build_config_str(config.options);
456 Self {
457 rust_fmt,
458 edition,
459 post_proc: config.post_proc,
460 config_str,
461 }
462 }
463
464 fn build_config_str<K, V>(cfg_options: HashMap<K, V>) -> Option<OsString>
465 where
466 K: Default + AsRef<OsStr>,
467 V: Default + AsRef<OsStr>,
468 {
469 if !cfg_options.is_empty() {
470 let mut options = OsString::with_capacity(512);
472 let iter = cfg_options.iter();
473
474 for (idx, (k, v)) in iter.enumerate() {
475 if idx > 0 {
477 options.push(",");
478 }
479 options.push(k);
480 options.push("=");
481 options.push(v);
482 }
483
484 Some(options)
485 } else {
486 None
487 }
488 }
489
490 fn build_args<'a, P>(&'a self, path: Option<&'a P>) -> Vec<&'a OsStr>
491 where
492 P: AsRef<Path> + ?Sized,
493 {
494 let mut args = match path {
495 Some(path) => {
496 let mut args = Vec::with_capacity(5);
497 args.push(path.as_ref().as_ref());
498 args
499 }
500 None => Vec::with_capacity(4),
501 };
502
503 args.push("--edition".as_ref());
504 args.push(self.edition.as_os_str());
505
506 if let Some(config_str) = &self.config_str {
507 args.push("--config".as_ref());
508 args.push(config_str);
509 }
510
511 args
512 }
513}
514
515impl Default for RustFmt {
516 #[inline]
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522impl Formatter for RustFmt {
523 fn format_str(&self, source: impl AsRef<str>) -> Result<String, Error> {
524 let args = self.build_args(None as Option<&Path>);
525
526 let mut proc = Command::new(&self.rust_fmt)
528 .stdin(Stdio::piped())
529 .stdout(Stdio::piped())
530 .stderr(Stdio::piped())
531 .args(args)
532 .spawn()?;
533
534 let mut stdin = proc.stdin.take().unwrap();
537 stdin.write_all(source.as_ref().as_bytes())?;
538 drop(stdin);
540
541 let output = proc.wait_with_output()?;
543 let stderr = String::from_utf8(output.stderr)?;
544
545 if output.status.success() {
546 let stdout = String::from_utf8(output.stdout)?;
547 post_process(self.post_proc, stdout)
548 } else {
549 Err(Error::BadSourceCode(stderr))
550 }
551 }
552
553 fn format_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
554 if self.post_proc.replace_markers() {
556 let source = file_to_string(path.as_ref())?;
557 let result = self.format_str(source)?;
558 string_to_file(path, &result)
559 } else {
560 let args = self.build_args(Some(path.as_ref()));
561
562 let proc = Command::new(&self.rust_fmt)
564 .stderr(Stdio::piped())
565 .args(args)
566 .spawn()?;
567
568 let output = proc.wait_with_output()?;
570 let stderr = String::from_utf8(output.stderr)?;
571
572 if output.status.success() {
573 Ok(())
574 } else {
575 Err(Error::BadSourceCode(stderr))
576 }
577 }
578 }
579}
580
581#[cfg(feature = "pretty_please")]
616#[cfg_attr(docsrs, doc(cfg(feature = "pretty_please")))]
617#[derive(Clone, Default)]
618pub struct PrettyPlease {
619 post_proc: PostProcess,
620}
621
622#[cfg(feature = "pretty_please")]
623impl PrettyPlease {
624 #[inline]
626 pub fn new() -> Self {
627 Self::build(None as Option<Config<&OsStr, &OsStr, &OsStr>>)
628 }
629
630 #[inline]
632 pub fn from_config<K, P, V>(config: Config<K, P, V>) -> Self
633 where
634 K: Default + Eq + Hash + AsRef<OsStr>,
635 P: Default + Into<PathBuf>,
636 V: Default + AsRef<OsStr>,
637 {
638 Self::build(Some(config))
639 }
640
641 fn build<K, P, V>(config: Option<Config<K, P, V>>) -> Self
642 where
643 K: Default + Eq + Hash + AsRef<OsStr>,
644 P: Default + Into<PathBuf>,
645 V: Default + AsRef<OsStr>,
646 {
647 let config = config.unwrap_or_default();
648
649 Self {
650 post_proc: config.post_proc,
651 }
652 }
653
654 #[inline]
655 fn format(&self, f: &syn::File) -> Result<String, Error> {
656 let result = prettyplease::unparse(f);
657 post_process(self.post_proc, result)
658 }
659}
660
661#[cfg(feature = "pretty_please")]
662impl Formatter for PrettyPlease {
663 #[inline]
664 fn format_str(&self, source: impl AsRef<str>) -> Result<String, Error> {
665 let f = syn::parse_file(source.as_ref())?;
666 self.format(&f)
667 }
668
669 #[inline]
670 #[cfg(feature = "token_stream")]
671 #[cfg_attr(docsrs, doc(cfg(feature = "token_stream")))]
672 fn format_tokens(&self, tokens: proc_macro2::TokenStream) -> Result<String, Error> {
673 let f = syn::parse2::<syn::File>(tokens)?;
674 self.format(&f)
675 }
676}
677
678#[cfg(test)]
681mod tests {
682 use std::io::{Read, Seek, Write};
683
684 use pretty_assertions::assert_eq;
685
686 #[cfg(feature = "post_process")]
687 use crate::PostProcess;
688 #[cfg(feature = "pretty_please")]
689 use crate::PrettyPlease;
690 use crate::{Config, Error, Formatter, RustFmt, RUST_FMT, RUST_FMT_KEY};
691
692 const PLAIN_EXPECTED: &str = r#"#[doc = " This is main"]
693fn main() {
694 _comment_!("This prints hello world");
695 println!("Hello World!");
696 _blank_!();
697}
698"#;
699 #[cfg(feature = "pretty_please")]
700 const PLAIN_PP_EXPECTED: &str = r#"/// This is main
701fn main() {
702 _comment_!("This prints hello world");
703 println!("Hello World!");
704 _blank_!();
705}
706"#;
707 #[cfg(feature = "post_process")]
708 const REPLACE_EXPECTED: &str = r#"#[doc = " This is main"]
709fn main() {
710 // This prints hello world
711 println!("Hello World!");
712
713}
714"#;
715 #[cfg(feature = "post_process")]
716 const REPLACE_BLOCKS_EXPECTED: &str = r#"/// This is main
717fn main() {
718 // This prints hello world
719 println!("Hello World!");
720
721}
722"#;
723
724 #[test]
725 fn rustfmt_bad_env_path() {
726 temp_env::with_var(
727 RUST_FMT_KEY,
728 Some("this_is_never_going_to_be_a_valid_executable"),
729 || match RustFmt::new().format_str("bogus") {
730 Err(Error::IOError(_)) => {}
731 _ => panic!("'rustfmt' should have failed due to bad path"),
732 },
733 );
734 }
735
736 #[test]
737 fn rustfmt_bad_config_path() {
738 temp_env::with_var(RUST_FMT_KEY, Some(RUST_FMT), || {
739 let config =
740 Config::new_str().rust_fmt_path("this_is_never_going_to_be_a_valid_executable");
741 match RustFmt::from_config(config).format_str("bogus") {
742 Err(Error::IOError(_)) => {}
743 _ => panic!("'rustfmt' should have failed due to bad path"),
744 }
745 });
746 }
747
748 fn format_file(fmt: impl Formatter, expected: &str) {
749 let source = r#"#[doc = " This is main"] fn main() { _comment_!("This prints hello world");
751 println!("Hello World!"); _blank_!(); }"#;
752 let mut file = tempfile::NamedTempFile::new().unwrap();
753 file.write_all(source.as_bytes()).unwrap();
754
755 fmt.format_file(file.path()).unwrap();
756
757 file.rewind().unwrap();
759 let mut actual = String::with_capacity(128);
760 file.read_to_string(&mut actual).unwrap();
761
762 assert_eq!(expected, actual);
763 }
764
765 #[test]
766 fn rustfmt_file() {
767 temp_env::with_var(RUST_FMT_KEY, Some(RUST_FMT), || {
768 format_file(RustFmt::new(), PLAIN_EXPECTED);
769 });
770 }
771
772 #[cfg(feature = "pretty_please")]
774 #[test]
775 fn prettyplease_file() {
776 format_file(PrettyPlease::new(), PLAIN_PP_EXPECTED);
777 }
778
779 #[cfg(feature = "post_process")]
780 #[test]
781 fn rustfmt_file_replace_markers() {
782 temp_env::with_var(RUST_FMT_KEY, Some(RUST_FMT), || {
783 let config = Config::new_str().post_proc(PostProcess::ReplaceMarkers);
784 format_file(RustFmt::from_config(config), REPLACE_EXPECTED);
785 });
786 }
787
788 #[cfg(feature = "post_process")]
790 #[cfg(feature = "pretty_please")]
791 #[test]
792 fn prettyplease_file_replace_markers() {
793 let config = Config::new_str().post_proc(PostProcess::ReplaceMarkers);
794 format_file(PrettyPlease::from_config(config), REPLACE_BLOCKS_EXPECTED);
795 }
796
797 #[cfg(feature = "post_process")]
798 #[test]
799 fn rustfmt_file_replace_markers_and_docs() {
800 temp_env::with_var(RUST_FMT_KEY, Some(RUST_FMT), || {
801 let config = Config::new_str().post_proc(PostProcess::ReplaceMarkersAndDocBlocks);
802 format_file(RustFmt::from_config(config), REPLACE_BLOCKS_EXPECTED);
803 });
804 }
805
806 #[cfg(feature = "post_process")]
807 #[cfg(feature = "pretty_please")]
808 #[test]
809 fn prettyplease_file_replace_markers_and_docs() {
810 let config = Config::new_str().post_proc(PostProcess::ReplaceMarkersAndDocBlocks);
811 format_file(PrettyPlease::from_config(config), REPLACE_BLOCKS_EXPECTED);
812 }
813
814 fn bad_format_file(fmt: impl Formatter) {
815 let source = r#"use"#;
817 let mut file = tempfile::NamedTempFile::new().unwrap();
818 file.write_all(source.as_bytes()).unwrap();
819
820 match fmt.format_file(file.path()) {
821 Err(Error::BadSourceCode(_)) => {}
822 _ => panic!("Expected bad source code"),
823 }
824 }
825
826 #[test]
827 fn rustfmt_bad_file() {
828 temp_env::with_var(RUST_FMT_KEY, Some(RUST_FMT), || {
829 bad_format_file(RustFmt::new());
830 });
831 }
832
833 #[cfg(feature = "pretty_please")]
834 #[test]
835 fn prettyplease_bad_file() {
836 bad_format_file(PrettyPlease::new());
837 }
838}