diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a77d57b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# +# Dependabot configuration file +# + +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9712afe..e38a160 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,6 +14,10 @@ jobs: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt - name: Report cargo version run: cargo --version - name: Report rustfmt version @@ -28,6 +32,18 @@ jobs: os: [ ubuntu-18.04, windows-2019, macos-10.15 ] steps: - uses: actions/checkout@v2 + - name: Install nightly + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + components: rustfmt + default: false + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + default: true - name: Build run: cargo build --tests --verbose - name: Run tests diff --git a/.gitignore b/.gitignore index 96ef6c0..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /target -Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5d7eff7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,318 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "newline-converter" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f81c2b19eebbc4249b3ca6aff70ae05bf18d6a99b7cc63cf0248774e640565" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustfmt-wrapper" +version = "0.2.0" +dependencies = [ + "newline-converter", + "quote", + "serde", + "tempfile", + "thiserror", + "toml", + "toolchain_find", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "toolchain_find" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e85654a10e7a07a47c6f19d93818f3f343e22927f2fa280c84f7c8042743413" +dependencies = [ + "home", + "lazy_static", + "regex", + "semver", + "walkdir", +] + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index d351705..01a3e29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "rustfmt-wrapper" -version = "0.1.0" +version = "0.2.0" authors = ["Adam H. Leventhal "] -edition = "2018" +edition = "2021" license = "Apache-2.0" description = "Library wrapper around rustfmt for use by code generators" repository = "https://github.com/oxidecomputer/rustfmt-wrapper" @@ -12,9 +12,11 @@ categories = ["development-tools", "development-tools::procedural-macro-helpers"] [dependencies] -toolchain_find = "0.2.0" -thiserror = "1.0.25" +serde = { version = "1", features = ["derive"] } tempfile = "3.2.0" +thiserror = "1.0.25" +toml = "0.5.9" +toolchain_find = "0.2.0" [dev-dependencies] quote = "1.0.9" diff --git a/README.md b/README.md index 765a758..01827f7 100644 --- a/README.md +++ b/README.md @@ -12,5 +12,28 @@ let codegen = quote::quote!{ struct Foo { bar: String } }; let formatted: String = rustfmt_wrapper::rustfmt(codegen).unwrap(); ``` -Thanks to David Tolnay for so many tools including `cargo-expand` from which -this borrows. \ No newline at end of file +If you need more control over the **vast** array of [`rustfmt` configuration +options](https://rust-lang.github.io/rustfmt), you can use the second form: + +```rust +let codegen = quote::quote!{ + async fn go() { + let _ = Client::new().operation_id().send().await?; + } +}; +let config = Config { + max_width: Some(45), + ..Default::default() +}; + +let narrow_formatted = rustfmt_config(config, codegen).unwrap(); +``` + +Note that in order to use unstable configuration options, you will need to have +a the nightly version of `rustfmt` installed. + +--- + +Thanks to David Tolnay for so many tools including +[`cargo-expand`](https://github.com/dtolnay/cargo-expand) from which this +borrows. \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..3e3b72b --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,219 @@ +// Copyright 2022 Oxide Computer Company + +use serde::Serialize; + +// Process the configuration options lifted from the rustfmt repo. Note that +// the default value is ignored, but left in to simplify updates. +macro_rules! create_config { + ($($i:ident: $ty:ty, $def:expr, $stb:expr, $( $dstring:expr ),+ );+ $(;)*) => { + /// `rustfmt` configuration. + /// + /// See the [`rustfmt` documentation](https://rust-lang.github.io/rustfmt) + /// for the descriptions of these stable and non-stable options. + #[derive(Serialize, Default)] + pub struct Config { + $( + $( + #[doc = $dstring] + )* + #[serde(skip_serializing_if = "Option::is_none")] + pub $i: Option<$ty>, + )* + } + + impl Config { + pub(crate) fn unstable(&self) -> bool { + false + $( + // Not stable and explicitly set. + || (!$stb && self.$i.is_some()) + )* + } + + pub(crate) fn list_unstable(&self) -> String { + let mut list = Vec::new(); + $( + if !$stb && self.$i.is_some() { + list.push(stringify!($i)); + } + )* + list.join(", ") + } + } + }; +} + +// Per https://github.com/rust-lang/rustfmt/blob/master/src/config/mod.rs +// ... skipping the section titled "Not user-facing" and "Control options". +// +// This macro defines configuration options used in rustfmt. Each option +// is defined as follows: +// +// `name: value type, default value, is stable, description;` +create_config! { + // Fundamental stuff + max_width: usize, 100, true, "Maximum width of each line"; + hard_tabs: bool, false, true, "Use tab characters for indentation, spaces for alignment"; + tab_spaces: usize, 4, true, "Number of spaces per tab"; + newline_style: NewlineStyle, NewlineStyle::Auto, true, "Unix or Windows line endings"; + indent_style: IndentStyle, IndentStyle::Block, false, "How do we indent expressions or items"; + + // Width Heuristics + use_small_heuristics: Heuristics, Heuristics::Default, true, "Whether to use different \ + formatting for items and expressions if they satisfy a heuristic notion of 'small'"; + fn_call_width: usize, 60, true, "Maximum width of the args of a function call before \ + falling back to vertical formatting."; + attr_fn_like_width: usize, 70, true, "Maximum width of the args of a function-like \ + attributes before falling back to vertical formatting."; + struct_lit_width: usize, 18, true, "Maximum width in the body of a struct lit before \ + falling back to vertical formatting."; + struct_variant_width: usize, 35, true, "Maximum width in the body of a struct variant before \ + falling back to vertical formatting."; + array_width: usize, 60, true, "Maximum width of an array literal before falling \ + back to vertical formatting."; + chain_width: usize, 60, true, "Maximum length of a chain to fit on a single line."; + single_line_if_else_max_width: usize, 50, true, "Maximum line length for single line if-else \ + expressions. A value of zero means always break if-else expressions."; + + // Comments. macros, and strings + wrap_comments: bool, false, false, "Break comments to fit on the line"; + format_code_in_doc_comments: bool, false, false, "Format the code snippet in doc comments."; + doc_comment_code_block_width: usize, 100, false, "Maximum width for code snippets in doc \ + comments. No effect unless format_code_in_doc_comments = true"; + comment_width: usize, 80, false, + "Maximum length of comments. No effect unless wrap_comments = true"; + normalize_comments: bool, false, false, "Convert /* */ comments to // comments where possible"; + normalize_doc_attributes: bool, false, false, "Normalize doc attributes as doc comments"; + format_strings: bool, false, false, "Format string literals where necessary"; + format_macro_matchers: bool, false, false, + "Format the metavariable matching patterns in macros"; + format_macro_bodies: bool, true, false, "Format the bodies of macros"; + hex_literal_case: HexLiteralCase, HexLiteralCase::Preserve, false, + "Format hexadecimal integer literals"; + + // Single line expressions and items + empty_item_single_line: bool, true, false, + "Put empty-body functions and impls on a single line"; + struct_lit_single_line: bool, true, false, + "Put small struct literals on a single line"; + fn_single_line: bool, false, false, "Put single-expression functions on a single line"; + where_single_line: bool, false, false, "Force where-clauses to be on a single line"; + + // Imports + imports_indent: IndentStyle, IndentStyle::Block, false, "Indent of imports"; + imports_layout: ListTactic, ListTactic::Mixed, false, "Item layout inside a import block"; + imports_granularity: ImportGranularity, ImportGranularity::Preserve, false, + "Merge or split imports to the provided granularity"; + group_imports: GroupImportsTactic, GroupImportsTactic::Preserve, false, + "Controls the strategy for how imports are grouped together"; + merge_imports: bool, false, false, "(deprecated: use imports_granularity instead)"; + + // Ordering + reorder_imports: bool, true, true, "Reorder import and extern crate statements alphabetically"; + reorder_modules: bool, true, true, "Reorder module statements alphabetically in group"; + reorder_impl_items: bool, false, false, "Reorder impl items"; + + // Spaces around punctuation + type_punctuation_density: TypeDensity, TypeDensity::Wide, false, + "Determines if '+' or '=' are wrapped in spaces in the punctuation of types"; + space_before_colon: bool, false, false, "Leave a space before the colon"; + space_after_colon: bool, true, false, "Leave a space after the colon"; + spaces_around_ranges: bool, false, false, "Put spaces around the .. and ..= range operators"; + binop_separator: SeparatorPlace, SeparatorPlace::Front, false, + "Where to put a binary operator when a binary expression goes multiline"; + + // Misc. + remove_nested_parens: bool, true, true, "Remove nested parens"; + combine_control_expr: bool, true, false, "Combine control expressions with function calls"; + short_array_element_width_threshold: usize, 10, true, + "Width threshold for an array element to be considered short"; + overflow_delimited_expr: bool, false, false, + "Allow trailing bracket/brace delimited expressions to overflow"; + struct_field_align_threshold: usize, 0, false, + "Align struct fields if their diffs fits within threshold"; + enum_discrim_align_threshold: usize, 0, false, + "Align enum variants discrims, if their diffs fit within threshold"; + match_arm_blocks: bool, true, false, "Wrap the body of arms in blocks when it does not fit on \ + the same line with the pattern of arms"; + match_arm_leading_pipes: MatchArmLeadingPipe, MatchArmLeadingPipe::Never, true, + "Determines whether leading pipes are emitted on match arms"; + force_multiline_blocks: bool, false, false, + "Force multiline closure bodies and match arms to be wrapped in a block"; + fn_args_layout: Density, Density::Tall, true, + "Control the layout of arguments in a function"; + brace_style: BraceStyle, BraceStyle::SameLineWhere, false, "Brace style for items"; + control_brace_style: ControlBraceStyle, ControlBraceStyle::AlwaysSameLine, false, + "Brace style for control flow constructs"; + trailing_semicolon: bool, true, false, + "Add trailing semicolon after break, continue and return"; + trailing_comma: SeparatorTactic, SeparatorTactic::Vertical, false, + "How to handle trailing commas for lists"; + match_block_trailing_comma: bool, false, true, + "Put a trailing comma after a block based match arm (non-block arms are not affected)"; + blank_lines_upper_bound: usize, 1, false, + "Maximum number of blank lines which can be put between items"; + blank_lines_lower_bound: usize, 0, false, + "Minimum number of blank lines which must be put between items"; + edition: Edition, Edition::Edition2015, true, "The edition of the parser (RFC 2052)"; + version: Version, Version::One, false, "Version of formatting rules"; + inline_attribute_width: usize, 0, false, + "Write an item and its attribute on the same line \ + if their combined width is below a threshold"; + format_generated_files: bool, true, false, "Format generated files"; + + // Options that can change the source code beyond whitespace/blocks (somewhat linty things) + merge_derives: bool, true, true, "Merge multiple `#[derive(...)]` into a single one"; + use_try_shorthand: bool, false, true, "Replace uses of the try! macro by the ? shorthand"; + use_field_init_shorthand: bool, false, true, "Use field initialization shorthand if possible"; + force_explicit_abi: bool, true, true, "Always print the abi for extern items"; + condense_wildcard_suffixes: bool, false, false, "Replace strings of _ wildcards by a single .. \ + in tuple patterns"; + + // Control options (changes the operation of rustfmt, rather than the formatting) + // [..] deleted + + // Not user-facing + // [..] deleted +} + +macro_rules! make_enum { + ($name:ident $(, $v:ident)+) => { + #[derive(Serialize)] + pub enum $name { + $( $v, )* + } + }; +} + +make_enum!(NewlineStyle, Auto, Windows, Unix, Native); +make_enum!(IndentStyle, Visual, Block); +make_enum!(Heuristics, Off, Max, Default); +make_enum!(HexLiteralCase, Preserve, Upper, Lower); +make_enum!(ListTactic, Vertical, Horizontal, HorizontalVertical, Mixed); +make_enum!(ImportGranularity, Preserve, Crate, Module, Item, One); +make_enum!(GroupImportsTactic, Preserve, StdExternalCrate, One); +make_enum!(TypeDensity, Compressed, Wide); +make_enum!(SeparatorPlace, Front, Back); +make_enum!(MatchArmLeadingPipe, Always, Never, Preserve); +make_enum!(Density, Compressed, Tall, Vertical); +make_enum!(BraceStyle, AlwaysNextLine, PreferSameLine, SameLineWhere); +make_enum!( + ControlBraceStyle, + AlwaysSameLine, + ClosingNextLine, + AlwaysNextLine +); +make_enum!(SeparatorTactic, Always, Never, Vertical); +make_enum!(Version, One, Two); + +#[derive(Serialize)] +pub enum Edition { + #[serde(rename = "2015")] + Edition2015, + #[serde(rename = "2018")] + Edition2018, + #[serde(rename = "2021")] + Edition2021, + #[serde(rename = "2024")] + Edition2024, +} diff --git a/src/lib.rs b/src/lib.rs index 1028be0..ac0cfca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2021 Oxide Computer Company +// Copyright 2022 Oxide Computer Company //! Use `rustfmt` to format generated code: //! ``` @@ -15,6 +15,8 @@ use std::{ use thiserror::Error; +pub mod config; + #[derive(Error, Debug)] pub enum Error { /// Command `rustfmt` could not be found @@ -23,6 +25,9 @@ pub enum Error { /// Command `rustfmt` produced an error at runtime. #[error("rustfmt runtime error")] Rustfmt(String), + /// Nightly channel required, but not found. + #[error("nightly channel required for unstable options")] + Unstable(String), /// Error with file IO #[error(transparent)] IO(#[from] std::io::Error), @@ -31,21 +36,46 @@ pub enum Error { Conversion(#[from] std::string::FromUtf8Error), } +/// Use the `rustfmt` command to format the input. pub fn rustfmt(input: T) -> Result { + // The only rustfmt default we override is edition = 2018 (vs 2015) + let config = config::Config { + edition: Some(config::Edition::Edition2018), + ..Default::default() + }; + rustfmt_config(config, input) +} + +/// Use the `rustfmt` command to format the input with the given [`Config`]. +/// +/// [`Config`]: config::Config +pub fn rustfmt_config(mut config: config::Config, input: T) -> Result { let input = input.to_string(); + // rustfmt's default edition is 2015; our default is 2021. + if config.edition.is_none() { + config.edition = Some(config::Edition::Edition2018); + } + let mut builder = tempfile::Builder::new(); builder.prefix("rustfmt-wrapper"); let outdir = builder.tempdir().expect("failed to create tmp file"); let rustfmt_config_path = outdir.as_ref().join("rustfmt.toml"); - std::fs::write(rustfmt_config_path, "")?; + std::fs::write( + rustfmt_config_path, + toml::to_string_pretty(&config).unwrap(), + )?; let rustfmt = which_rustfmt().ok_or(Error::NoRustfmt)?; + let mut args = vec![format!("--config-path={}", outdir.path().to_str().unwrap())]; + if config.unstable() { + args.push("--unstable-features".to_string()) + } + let mut command = Command::new(&rustfmt) - .arg("--edition=2018") - .arg(format!("--config-path={}", outdir.path().to_str().unwrap())) + .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -63,7 +93,12 @@ pub fn rustfmt(input: T) -> Result { if output.status.success() { Ok(String::from_utf8(output.stdout)?) } else { - Err(Error::Rustfmt(String::from_utf8(output.stderr)?)) + let err_str = String::from_utf8(output.stderr)?; + if err_str.contains("Unrecognized option: 'unstable-features'") { + Err(Error::Unstable(config.list_unstable())) + } else { + Err(Error::Rustfmt(err_str)) + } } } @@ -82,19 +117,68 @@ fn which_rustfmt() -> Option { #[cfg(test)] mod tests { - use crate::rustfmt; + use crate::{config::Config, rustfmt, rustfmt_config}; use newline_converter::dos2unix; use quote::quote; #[test] fn test_basics() { + let code = quote! { struct Foo { bar: String } }; assert_eq!( - dos2unix( - rustfmt(quote! { struct Foo { bar: String }}) - .unwrap() - .as_str() - ), + dos2unix(rustfmt(code).unwrap().as_str()), "struct Foo {\n bar: String,\n}\n" ); } + + #[test] + fn test_doc_comments() { + let comment = "This is a very long doc comment that could span \ + multiple lines of text. For the purposes of this test, we're hoping \ + that it gets formatted into a single, nice doc comment."; + let code = quote! { + #[doc = #comment] + struct Foo { bar: String } + }; + + let config = Config { + normalize_doc_attributes: Some(true), + wrap_comments: Some(true), + ..Default::default() + }; + + assert_eq!( + dos2unix(rustfmt_config(config, code).unwrap().as_str()), + r#"///This is a very long doc comment that could span multiple lines of text. For +/// the purposes of this test, we're hoping that it gets formatted into a +/// single, nice doc comment. +struct Foo { + bar: String, +} +"#, + ); + } + + #[test] + fn test_narrow_call() { + let code = quote! { + async fn go() { + let _ = Client::new().operation_id().send().await?; + } + }; + + let config = Config { + max_width: Some(45), + ..Default::default() + }; + + assert_eq!( + dos2unix(rustfmt_config(config, code).unwrap().as_str()), + "async fn go() { + let _ = Client::new() + .operation_id() + .send() + .await?; +}\n" + ); + } }