diff --git a/Cargo.lock b/Cargo.lock index 40b04c3ef16..9ae5a4ee6f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ dependencies = [ "env_logger 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", "strings 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -77,6 +78,11 @@ dependencies = [ "libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "nom" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "regex" version = "0.1.54" diff --git a/Cargo.toml b/Cargo.toml index 94ff9ce1ec6..c4cff7f4ca8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ syntex_syntax = "0.23.0" log = "0.3.2" env_logger = "0.3.1" getopts = "0.2" +nom = "1.2.1" diff --git a/src/bin/rustfmt.rs b/src/bin/rustfmt.rs index 1524384fc12..6882fa77484 100644 --- a/src/bin/rustfmt.rs +++ b/src/bin/rustfmt.rs @@ -12,13 +12,19 @@ #[macro_use] extern crate log; +#[macro_use] +extern crate nom; + extern crate rustfmt; extern crate toml; extern crate env_logger; extern crate getopts; +use nom::IResult; + use rustfmt::{run, run_from_stdin}; use rustfmt::config::{Config, WriteMode}; +use rustfmt::config::{FileLinesMap, LineRanges}; use std::env; use std::fs::{self, File}; @@ -50,6 +56,10 @@ enum Operation { input: String, config_path: Option, }, + /// Format a set of line ranges. + FormatLineRanges { + file_lines_map: FileLinesMap, + }, } /// Try to find a project file in the given directory and its parents. Returns the path of a the @@ -120,7 +130,9 @@ fn match_cli_path_or_file(config_path: Option, fn update_config(config: &mut Config, matches: &Matches) -> Result<(), String> { config.verbose = matches.opt_present("verbose"); - config.skip_children = matches.opt_present("skip-children"); + // `file-lines` implies `skip-children`. + config.skip_children = matches.opt_present("skip-children") || + (file_lines_enabled() && matches.opt_present("file-lines")); let write_mode = matches.opt_str("write-mode"); match matches.opt_str("write-mode").map(|wm| WriteMode::from_str(&wm)) { @@ -133,6 +145,13 @@ fn update_config(config: &mut Config, matches: &Matches) -> Result<(), String> { } } +fn file_lines_enabled() -> bool { + match env::var("RUSTFMT_EXPERIMENTAL_FILE_LINES") { + Ok(ref v) if v == "1" => true, + _ => false, + } +} + fn execute() -> i32 { let mut opts = Options::new(); opts.optflag("h", "help", "show this message"); @@ -152,6 +171,13 @@ fn execute() -> i32 { "Recursively searches the given path for the rustfmt.toml config file. If not \ found reverts to the input file path", "[Path for the configuration file]"); + if file_lines_enabled() { + opts.optmulti("", + "file-lines", + "Format specified line RANGEs in FILE. RANGEs are inclusive of both \ + endpoints. May be specified multiple times.", + "FILE:RANGE,RANGE,..."); + } let matches = match opts.parse(env::args().skip(1)) { Ok(m) => m, @@ -228,6 +254,27 @@ fn execute() -> i32 { } 0 } + // TODO: figure out what to do with config_path. + Operation::FormatLineRanges { file_lines_map } => { + for (file, line_ranges) in file_lines_map { + let (mut config, config_path) = resolve_config(file.parent().unwrap()) + .expect(&format!("Error resolving config \ + for {}", + file.display())); + if let Some(path) = config_path.as_ref() { + println!("Using rustfmt config file {} for {}", + path.display(), + file.display()); + } + if let Err(e) = update_config(&mut config, &matches) { + print_usage(&opts, &e); + return 1; + } + config.line_ranges = line_ranges; + run(&file, &config); + } + 0 + } } } @@ -283,6 +330,29 @@ fn determine_operation(matches: &Matches) -> Operation { Some(dir) }); + if file_lines_enabled() && matches.opt_present("file-lines") { + let file_lines = matches.opt_strs("file-lines"); + let mut file_lines_map = FileLinesMap::new(); + for range_spec in file_lines { + let invalid = || { + Operation::InvalidInput { + reason: format!("invalid file-lines argument: {}", range_spec), + } + }; + + let (file, line_ranges) = match parse::file_lines_arg(&range_spec) { + IResult::Error(_) | + IResult::Incomplete(_) => return invalid(), + IResult::Done(remaining, _) if !remaining.is_empty() => return invalid(), + IResult::Done(_, (file, line_ranges)) => (file, line_ranges), + }; + + let entry = file_lines_map.entry(file).or_insert(LineRanges(Vec::new())); + entry.0.extend(line_ranges.0); + } + return Operation::FormatLineRanges { file_lines_map: file_lines_map }; + } + // if no file argument is supplied, read from stdin if matches.free.is_empty() { @@ -305,3 +375,59 @@ fn determine_operation(matches: &Matches) -> Operation { config_path: config_path, } } + + +/// Parser for the `file-lines` argument. +mod parse { + use std::path::PathBuf; + use std::str::FromStr; + use rustfmt::config::{LineRange, LineRanges}; + + use nom::digit; + + named!(pub file_lines_arg<&str, (PathBuf, LineRanges)>, + chain!( + file: map!( + is_not_s!(":"), + PathBuf::from + ) ~ + tag_s!(":") ~ + line_ranges: line_ranges, + || (file, line_ranges) + ) + ); + + named!(usize_digit<&str, usize>, + map_res!( + digit, + FromStr::from_str + ) + ); + + named!(line_range<&str, LineRange>, + map_res!( + separated_pair!( + usize_digit, + tag_s!("-"), + usize_digit + ), + |pair| { + let (lo, hi) = pair; + if lo < hi { + return Err(format!("empty line range: {}-{}", lo, hi)); + } + Ok(LineRange { lo: lo, hi: hi }) + } + ) + ); + + named!(line_ranges<&str, LineRanges>, + map!( + separated_nonempty_list!( + tag_s!(","), + line_range + ), + LineRanges + ) + ); +} diff --git a/src/config.rs b/src/config.rs index 448ab3b8d51..6632527bd29 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,10 @@ extern crate toml; +use std::collections::HashMap; +use std::path::PathBuf; +use std::str; + use lists::{SeparatorTactic, ListTactic}; macro_rules! configuration_option_enum{ @@ -80,6 +84,40 @@ configuration_option_enum! { TypeDensity: Wide, } +/// A range of lines, inclusive of both ends. +#[derive(Clone, Debug, RustcDecodable)] +pub struct LineRange { + pub lo: usize, + pub hi: usize, +} + +// Newtype required to implement ConfigType and FromStr for the `create_config` macro. +#[derive(Clone, Debug, RustcDecodable)] +pub struct LineRanges(pub Vec); + +impl LineRanges { + pub fn new() -> LineRanges { + LineRanges(Vec::new()) + } +} + +// This impl is needed by the `create_config` macro. +impl str::FromStr for LineRanges { + type Err = (); + fn from_str(_: &str) -> Result { + unimplemented!(); + } +} + +// This impl is needed by the `create_config` macro. +impl ConfigType for LineRanges { + fn get_variant_names() -> String { + String::from("") + } +} + +pub type FileLinesMap = HashMap; + impl Density { pub fn to_list_tactic(self) -> ListTactic { match self { @@ -186,8 +224,18 @@ impl ConfigHelpItem { } } +/// Controls if a config field is printed in docs. +enum ConfigDoc { + /// Include in docs. + Doc, + /// Do not include in docs. + #[allow(dead_code)] + NoDoc, +} +use self::ConfigDoc::*; + macro_rules! create_config { - ($($i:ident: $ty:ty, $def:expr, $( $dstring:expr ),+ );+ $(;)*) => ( + ($($doc:ident $i:ident: $ty:ty, $def:expr, $( $dstring:expr ),+ );+ $(;)*) => ( #[derive(RustcDecodable, Clone)] pub struct Config { $(pub $i: $ty),+ @@ -253,21 +301,23 @@ macro_rules! create_config { } println!("Configuration Options:"); $( - let name_raw = stringify!($i); - let mut name_out = String::with_capacity(max); - for _ in name_raw.len()..max-1 { - name_out.push(' ') + if let ConfigDoc::Doc = $doc { + let name_raw = stringify!($i); + let mut name_out = String::with_capacity(max); + for _ in name_raw.len()..max-1 { + name_out.push(' ') + } + name_out.push_str(name_raw); + name_out.push(' '); + println!("{}{} Default: {:?}", + name_out, + <$ty>::get_variant_names(), + $def); + $( + println!("{}{}", space_str, $dstring); + )+ + println!(""); } - name_out.push_str(name_raw); - name_out.push(' '); - println!("{}{} Default: {:?}", - name_out, - <$ty>::get_variant_names(), - $def); - $( - println!("{}{}", space_str, $dstring); - )+ - println!(""); )+ } } @@ -286,66 +336,70 @@ macro_rules! create_config { } create_config! { - verbose: bool, false, "Use verbose output"; - skip_children: bool, false, "Don't reformat out of line modules"; - max_width: usize, 100, "Maximum width of each line"; - ideal_width: usize, 80, "Ideal width of each line"; - tab_spaces: usize, 4, "Number of spaces per tab"; - fn_call_width: usize, 60, + Doc verbose: bool, false, "Use verbose output"; + Doc skip_children: bool, false, "Don't reformat out of line modules"; + NoDoc line_ranges: LineRanges, LineRanges::new(), "Ranges of lines to format"; + Doc max_width: usize, 100, "Maximum width of each line"; + Doc ideal_width: usize, 80, "Ideal width of each line"; + Doc tab_spaces: usize, 4, "Number of spaces per tab"; + Doc fn_call_width: usize, 60, "Maximum width of the args of a function call before falling back to vertical formatting"; - struct_lit_width: usize, 16, + Doc struct_lit_width: usize, 16, "Maximum width in the body of a struct lit before falling back to vertical formatting"; - newline_style: NewlineStyle, NewlineStyle::Unix, "Unix or Windows line endings"; - fn_brace_style: BraceStyle, BraceStyle::SameLineWhere, "Brace style for functions"; - item_brace_style: BraceStyle, BraceStyle::SameLineWhere, "Brace style for structs and enums"; - impl_empty_single_line: bool, true, "Put empty-body implementations on a single line"; - fn_empty_single_line: bool, true, "Put empty-body functions on a single line"; - fn_single_line: bool, false, "Put single-expression functions on a single line"; - fn_return_indent: ReturnIndent, ReturnIndent::WithArgs, + Doc newline_style: NewlineStyle, NewlineStyle::Unix, "Unix or Windows line endings"; + Doc fn_brace_style: BraceStyle, BraceStyle::SameLineWhere, "Brace style for functions"; + Doc item_brace_style: BraceStyle, BraceStyle::SameLineWhere, + "Brace style for structs and enums"; + Doc impl_empty_single_line: bool, true, "Put empty-body implementations on a single line"; + Doc fn_empty_single_line: bool, true, "Put empty-body functions on a single line"; + Doc fn_single_line: bool, false, "Put single-expression functions on a single line"; + Doc fn_return_indent: ReturnIndent, ReturnIndent::WithArgs, "Location of return type in function declaration"; - fn_args_paren_newline: bool, true, "If function argument parenthesis goes on a newline"; - fn_args_density: Density, Density::Tall, "Argument density in functions"; - fn_args_layout: StructLitStyle, StructLitStyle::Visual, "Layout of function arguments"; - fn_arg_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indent on function arguments"; - type_punctuation_density: TypeDensity, TypeDensity::Wide, + Doc fn_args_paren_newline: bool, true, "If function argument parenthesis goes on a newline"; + Doc fn_args_density: Density, Density::Tall, "Argument density in functions"; + Doc fn_args_layout: StructLitStyle, StructLitStyle::Visual, "Layout of function arguments"; + Doc fn_arg_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indent on function arguments"; + Doc type_punctuation_density: TypeDensity, TypeDensity::Wide, "Determines if '+' or '=' are wrapped in spaces in the punctuation of types"; // Should we at least try to put the where clause on the same line as the rest of the // function decl? - where_density: Density, Density::CompressedIfEmpty, "Density of a where clause"; + Doc where_density: Density, Density::CompressedIfEmpty, "Density of a where clause"; // Visual will be treated like Tabbed - where_indent: BlockIndentStyle, BlockIndentStyle::Tabbed, "Indentation of a where clause"; - where_layout: ListTactic, ListTactic::Vertical, "Element layout inside a where clause"; - where_pred_indent: BlockIndentStyle, BlockIndentStyle::Visual, + Doc where_indent: BlockIndentStyle, BlockIndentStyle::Tabbed, "Indentation of a where clause"; + Doc where_layout: ListTactic, ListTactic::Vertical, "Element layout inside a where clause"; + Doc where_pred_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indentation style of a where predicate"; - where_trailing_comma: bool, false, "Put a trailing comma on where clauses"; - generics_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indentation of generics"; - struct_trailing_comma: SeparatorTactic, SeparatorTactic::Vertical, + Doc where_trailing_comma: bool, false, "Put a trailing comma on where clauses"; + Doc generics_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indentation of generics"; + Doc struct_trailing_comma: SeparatorTactic, SeparatorTactic::Vertical, "If there is a trailing comma on structs"; - struct_lit_trailing_comma: SeparatorTactic, SeparatorTactic::Vertical, + Doc struct_lit_trailing_comma: SeparatorTactic, SeparatorTactic::Vertical, "If there is a trailing comma on literal structs"; - struct_lit_style: StructLitStyle, StructLitStyle::Block, "Style of struct definition"; - struct_lit_multiline_style: MultilineStyle, MultilineStyle::PreferSingle, + Doc struct_lit_style: StructLitStyle, StructLitStyle::Block, "Style of struct definition"; + Doc struct_lit_multiline_style: MultilineStyle, MultilineStyle::PreferSingle, "Multiline style on literal structs"; - enum_trailing_comma: bool, true, "Put a trailing comma on enum declarations"; - report_todo: ReportTactic, ReportTactic::Never, + Doc enum_trailing_comma: bool, true, "Put a trailing comma on enum declarations"; + Doc report_todo: ReportTactic, ReportTactic::Never, "Report all, none or unnumbered occurrences of TODO in source file comments"; - report_fixme: ReportTactic, ReportTactic::Never, + Doc report_fixme: ReportTactic, ReportTactic::Never, "Report all, none or unnumbered occurrences of FIXME in source file comments"; - chain_base_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indent on chain base"; - chain_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indentation of chain"; - reorder_imports: bool, false, "Reorder import statements alphabetically"; - single_line_if_else: bool, false, "Put else on same line as closing brace for if statements"; - format_strings: bool, true, "Format string literals where necessary"; - force_format_strings: bool, false, "Always format string literals"; - chains_overflow_last: bool, true, "Allow last call in method chain to break the line"; - take_source_hints: bool, true, "Retain some formatting characteristics from the source code"; - hard_tabs: bool, false, "Use tab characters for indentation, spaces for alignment"; - wrap_comments: bool, false, "Break comments to fit on the line"; - normalise_comments: bool, true, "Convert /* */ comments to // comments where possible"; - wrap_match_arms: bool, true, "Wrap multiline match arms in blocks"; - match_block_trailing_comma: bool, false, + Doc chain_base_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indent on chain base"; + Doc chain_indent: BlockIndentStyle, BlockIndentStyle::Visual, "Indentation of chain"; + Doc reorder_imports: bool, false, "Reorder import statements alphabetically"; + Doc single_line_if_else: bool, false, + "Put else on same line as closing brace for if statements"; + Doc format_strings: bool, true, "Format string literals where necessary"; + Doc force_format_strings: bool, false, "Always format string literals"; + Doc chains_overflow_last: bool, true, "Allow last call in method chain to break the line"; + Doc take_source_hints: bool, true, + "Retain some formatting characteristics from the source code"; + Doc hard_tabs: bool, false, "Use tab characters for indentation, spaces for alignment"; + Doc wrap_comments: bool, false, "Break comments to fit on the line"; + Doc normalise_comments: bool, true, "Convert /* */ comments to // comments where possible"; + Doc wrap_match_arms: bool, true, "Wrap multiline match arms in blocks"; + Doc match_block_trailing_comma: bool, false, "Put a trailing comma after a block based match arm (non-block arms are not affected)"; - match_wildcard_trailing_comma: bool, true, "Put a trailing comma after a wildcard arm"; - write_mode: WriteMode, WriteMode::Replace, + Doc match_wildcard_trailing_comma: bool, true, "Put a trailing comma after a wildcard arm"; + Doc write_mode: WriteMode, WriteMode::Replace, "What Write Mode to use when none is supplied: Replace, Overwrite, Display, Diff, Coverage"; }