From 8c9a988bdc56625bd460be1b12879a0bb4b62102 Mon Sep 17 00:00:00 2001 From: crw5996 Date: Mon, 27 Aug 2018 12:31:26 -0400 Subject: [PATCH 1/2] Fixed #2955. Added value to determine whether or not rustfmt has condensed a tuple-struct --- src/patterns.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/patterns.rs b/src/patterns.rs index 465d64627e8..809143c556b 100644 --- a/src/patterns.rs +++ b/src/patterns.rs @@ -340,10 +340,11 @@ fn rewrite_tuple_pat( if pat_vec.is_empty() { return Some(format!("{}()", path_str.unwrap_or_default())); } - + let mut condensed_wildcards = false; let wildcard_suffix_len = count_wildcard_suffix_len(context, &pat_vec, span, shape); let (pat_vec, span) = if context.config.condense_wildcard_suffixes() && wildcard_suffix_len >= 2 { + condensed_wildcards = true; let new_item_count = 1 + pat_vec.len() - wildcard_suffix_len; let sp = pat_vec[new_item_count - 1].span(); let snippet = context.snippet(sp); @@ -358,7 +359,8 @@ fn rewrite_tuple_pat( }; // add comma if `(x,)` - let add_comma = path_str.is_none() && pat_vec.len() == 1 && dotdot_pos.is_none(); + let add_comma = + path_str.is_none() && pat_vec.len() == 1 && dotdot_pos.is_none() && !condensed_wildcards; let path_str = path_str.unwrap_or_default(); let pat_ref_vec = pat_vec.iter().collect::>(); From 5d642e8b234e96b360ef94734f724af12b36abb6 Mon Sep 17 00:00:00 2001 From: crw5996 Date: Mon, 27 Aug 2018 12:31:26 -0400 Subject: [PATCH 2/2] Fixed #2955. Added value to determine whether or not rustfmt has condensed a tuple-struct Refactored to not use a mutable variable --- bin/main.rs | 589 +++++++++++++++++++++ cargo-fmt/main.rs | 381 ++++++++++++++ config/config_type.rs | 452 ++++++++++++++++ config/file_lines.rs | 422 +++++++++++++++ config/license.rs | 266 ++++++++++ config/lists.rs | 105 ++++ config/mod.rs | 360 +++++++++++++ config/options.rs | 482 +++++++++++++++++ format-diff/main.rs | 251 +++++++++ format-diff/test/bindgen.diff | 67 +++ git-rustfmt/main.rs | 206 ++++++++ src/patterns.rs | 33 +- test/mod.rs | 964 ++++++++++++++++++++++++++++++++++ 13 files changed, 4561 insertions(+), 17 deletions(-) create mode 100644 bin/main.rs create mode 100644 cargo-fmt/main.rs create mode 100644 config/config_type.rs create mode 100644 config/file_lines.rs create mode 100644 config/license.rs create mode 100644 config/lists.rs create mode 100644 config/mod.rs create mode 100644 config/options.rs create mode 100644 format-diff/main.rs create mode 100644 format-diff/test/bindgen.diff create mode 100644 git-rustfmt/main.rs create mode 100644 test/mod.rs diff --git a/bin/main.rs b/bin/main.rs new file mode 100644 index 00000000000..760333955a0 --- /dev/null +++ b/bin/main.rs @@ -0,0 +1,589 @@ +// Copyright 2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![cfg(not(test))] +#![feature(extern_prelude)] + +extern crate env_logger; +#[macro_use] +extern crate failure; +extern crate getopts; +extern crate rustfmt_nightly as rustfmt; + +use std::env; +use std::fs::File; +use std::io::{self, stdout, Read, Write}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use failure::err_msg; + +use getopts::{Matches, Options}; + +use rustfmt::{ + load_config, CliOptions, Color, Config, EmitMode, ErrorKind, FileLines, FileName, Input, + Session, Verbosity, +}; + +fn main() { + env_logger::init(); + let opts = make_opts(); + + let exit_code = match execute(&opts) { + Ok(code) => code, + Err(e) => { + eprintln!("{}", e.to_string()); + 1 + } + }; + // Make sure standard output is flushed before we exit. + std::io::stdout().flush().unwrap(); + + // Exit with given exit code. + // + // NOTE: This immediately terminates the process without doing any cleanup, + // so make sure to finish all necessary cleanup before this is called. + std::process::exit(exit_code); +} + +/// Rustfmt operations. +enum Operation { + /// Format files and their child modules. + Format { + files: Vec, + minimal_config_path: Option, + }, + /// Print the help message. + Help(HelpOp), + // Print version information + Version, + /// Output default config to a file, or stdout if None + ConfigOutputDefault { + path: Option, + }, + /// No file specified, read from stdin + Stdin { + input: String, + }, +} + +/// Arguments to `--help` +enum HelpOp { + None, + Config, + FileLines, +} + +fn make_opts() -> Options { + let mut opts = Options::new(); + + opts.optflag( + "", + "check", + "Run in 'check' mode. Exits with 0 if input if formatted correctly. Exits \ + with 1 and prints a diff if formatting is required.", + ); + let is_nightly = is_nightly(); + let emit_opts = if is_nightly { + "[files|stdout|coverage|checkstyle]" + } else { + "[files|stdout]" + }; + opts.optopt("", "emit", "What data to emit and how", emit_opts); + opts.optflag("", "backup", "Backup any modified files."); + opts.optopt( + "", + "config-path", + "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]", + ); + opts.optopt( + "", + "color", + "Use colored output (if supported)", + "[always|never|auto]", + ); + opts.optopt( + "", + "print-config", + "Dumps a default or minimal config to PATH. A minimal config is the \ + subset of the current config file used for formatting the current program.", + "[minimal|default] PATH", + ); + + if is_nightly { + opts.optflag( + "", + "unstable-features", + "Enables unstable features. Only available on nightly channel.", + ); + opts.optopt( + "", + "file-lines", + "Format specified line ranges. Run with `--help=file-lines` for \ + more detail (unstable).", + "JSON", + ); + opts.optflag( + "", + "error-on-unformatted", + "Error if unable to get comments or string literals within max_width, \ + or they are left with trailing whitespaces (unstable).", + ); + opts.optflag( + "", + "skip-children", + "Don't reformat child modules (unstable).", + ); + } + + opts.optflag("v", "verbose", "Print verbose output"); + opts.optflag("q", "quiet", "Print less output"); + opts.optflag("V", "version", "Show version information"); + opts.optflagopt( + "h", + "help", + "Show this message or help about a specific topic: `config` or `file-lines`", + "=TOPIC", + ); + + opts +} + +fn is_nightly() -> bool { + option_env!("CFG_RELEASE_CHANNEL") + .map(|c| c == "nightly" || c == "dev") + .unwrap_or(false) +} + +// Returned i32 is an exit code +fn execute(opts: &Options) -> Result { + let matches = opts.parse(env::args().skip(1))?; + let options = GetOptsOptions::from_matches(&matches)?; + + match determine_operation(&matches)? { + Operation::Help(HelpOp::None) => { + print_usage_to_stdout(opts, ""); + return Ok(0); + } + Operation::Help(HelpOp::Config) => { + Config::print_docs(&mut stdout(), options.unstable_features); + return Ok(0); + } + Operation::Help(HelpOp::FileLines) => { + print_help_file_lines(); + return Ok(0); + } + Operation::Version => { + print_version(); + return Ok(0); + } + Operation::ConfigOutputDefault { path } => { + let toml = Config::default().all_options().to_toml().map_err(err_msg)?; + if let Some(path) = path { + let mut file = File::create(path)?; + file.write_all(toml.as_bytes())?; + } else { + io::stdout().write_all(toml.as_bytes())?; + } + return Ok(0); + } + Operation::Stdin { input } => format_string(input, options), + Operation::Format { + files, + minimal_config_path, + } => format(files, minimal_config_path, options), + } +} + +fn format_string(input: String, options: GetOptsOptions) -> Result { + // try to read config from local directory + let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?; + + // emit mode is always Stdout for Stdin. + config.set().emit_mode(EmitMode::Stdout); + config.set().verbose(Verbosity::Quiet); + + // parse file_lines + config.set().file_lines(options.file_lines); + for f in config.file_lines().files() { + match *f { + FileName::Stdin => {} + _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f), + } + } + + let out = &mut stdout(); + let mut session = Session::new(config, Some(out)); + format_and_emit_report(&mut session, Input::Text(input)); + + let exit_code = if session.has_operational_errors() || session.has_parsing_errors() { + 1 + } else { + 0 + }; + Ok(exit_code) +} + +fn format( + files: Vec, + minimal_config_path: Option, + options: GetOptsOptions, +) -> Result { + options.verify_file_lines(&files); + let (config, config_path) = load_config(None, Some(options.clone()))?; + + if config.verbose() == Verbosity::Verbose { + if let Some(path) = config_path.as_ref() { + println!("Using rustfmt config file {}", path.display()); + } + } + + let out = &mut stdout(); + let mut session = Session::new(config, Some(out)); + + for file in files { + if !file.exists() { + eprintln!("Error: file `{}` does not exist", file.to_str().unwrap()); + session.add_operational_error(); + } else if file.is_dir() { + eprintln!("Error: `{}` is a directory", file.to_str().unwrap()); + session.add_operational_error(); + } else { + // Check the file directory if the config-path could not be read or not provided + if config_path.is_none() { + let (local_config, config_path) = + load_config(Some(file.parent().unwrap()), Some(options.clone()))?; + if local_config.verbose() == Verbosity::Verbose { + if let Some(path) = config_path { + println!( + "Using rustfmt config file {} for {}", + path.display(), + file.display() + ); + } + } + + session.override_config(local_config, |sess| { + format_and_emit_report(sess, Input::File(file)) + }); + } else { + format_and_emit_report(&mut session, Input::File(file)); + } + } + } + + // If we were given a path via dump-minimal-config, output any options + // that were used during formatting as TOML. + if let Some(path) = minimal_config_path { + let mut file = File::create(path)?; + let toml = session.config.used_options().to_toml().map_err(err_msg)?; + file.write_all(toml.as_bytes())?; + } + + let exit_code = if session.has_operational_errors() + || session.has_parsing_errors() + || ((session.has_diff() || session.has_check_errors()) && options.check) + { + 1 + } else { + 0 + }; + Ok(exit_code) +} + +fn format_and_emit_report(session: &mut Session, input: Input) { + match session.format(input) { + Ok(report) => { + if report.has_warnings() { + match term::stderr() { + Some(ref t) + if session.config.color().use_colored_tty() + && t.supports_color() + && t.supports_attr(term::Attr::Bold) => + { + match report.fancy_print(term::stderr().unwrap()) { + Ok(..) => (), + Err(..) => panic!("Unable to write to stderr: {}", report), + } + } + _ => eprintln!("{}", report), + } + } + } + Err(msg) => { + eprintln!("Error writing files: {}", msg); + session.add_operational_error(); + } + } +} + +fn print_usage_to_stdout(opts: &Options, reason: &str) { + let sep = if reason.is_empty() { + String::new() + } else { + format!("{}\n\n", reason) + }; + let msg = format!( + "{}Format Rust code\n\nusage: {} [options] ...", + sep, + env::args_os().next().unwrap().to_string_lossy() + ); + println!("{}", opts.usage(&msg)); +} + +fn print_help_file_lines() { + println!( + "If you want to restrict reformatting to specific sets of lines, you can +use the `--file-lines` option. Its argument is a JSON array of objects +with `file` and `range` properties, where `file` is a file name, and +`range` is an array representing a range of lines like `[7,13]`. Ranges +are 1-based and inclusive of both end points. Specifying an empty array +will result in no files being formatted. For example, + +``` +rustfmt --file-lines '[ + {{\"file\":\"src/lib.rs\",\"range\":[7,13]}}, + {{\"file\":\"src/lib.rs\",\"range\":[21,29]}}, + {{\"file\":\"src/foo.rs\",\"range\":[10,11]}}, + {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]' +``` + +would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`, +and `15` of `src/foo.rs`. No other files would be formatted, even if they +are included as out of line modules from `src/lib.rs`." + ); +} + +fn print_version() { + let version_info = format!( + "{}-{}", + option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"), + include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")) + ); + + println!("rustfmt {}", version_info); +} + +fn determine_operation(matches: &Matches) -> Result { + if matches.opt_present("h") { + let topic = matches.opt_str("h"); + if topic == None { + return Ok(Operation::Help(HelpOp::None)); + } else if topic == Some("config".to_owned()) { + return Ok(Operation::Help(HelpOp::Config)); + } else if topic == Some("file-lines".to_owned()) { + return Ok(Operation::Help(HelpOp::FileLines)); + } else { + println!("Unknown help topic: `{}`\n", topic.unwrap()); + return Ok(Operation::Help(HelpOp::None)); + } + } + + let mut minimal_config_path = None; + if let Some(ref kind) = matches.opt_str("print-config") { + let path = matches.free.get(0).cloned(); + if kind == "default" { + return Ok(Operation::ConfigOutputDefault { path }); + } else if kind == "minimal" { + minimal_config_path = path; + if minimal_config_path.is_none() { + println!("WARNING: PATH required for `--print-config minimal`"); + } + } + } + + if matches.opt_present("version") { + return Ok(Operation::Version); + } + + // if no file argument is supplied, read from stdin + if matches.free.is_empty() { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + + return Ok(Operation::Stdin { input: buffer }); + } + + let files: Vec<_> = matches + .free + .iter() + .map(|s| { + let p = PathBuf::from(s); + // we will do comparison later, so here tries to canonicalize first + // to get the expected behavior. + p.canonicalize().unwrap_or(p) + }).collect(); + + Ok(Operation::Format { + files, + minimal_config_path, + }) +} + +const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff]; + +/// Parsed command line options. +#[derive(Clone, Debug, Default)] +struct GetOptsOptions { + skip_children: Option, + quiet: bool, + verbose: bool, + config_path: Option, + emit_mode: EmitMode, + backup: bool, + check: bool, + color: Option, + file_lines: FileLines, // Default is all lines in all files. + unstable_features: bool, + error_on_unformatted: Option, +} + +impl GetOptsOptions { + pub fn from_matches(matches: &Matches) -> Result { + let mut options = GetOptsOptions::default(); + options.verbose = matches.opt_present("verbose"); + options.quiet = matches.opt_present("quiet"); + if options.verbose && options.quiet { + return Err(format_err!("Can't use both `--verbose` and `--quiet`")); + } + + let rust_nightly = is_nightly(); + + if rust_nightly { + options.unstable_features = matches.opt_present("unstable-features"); + + if options.unstable_features { + if matches.opt_present("skip-children") { + options.skip_children = Some(true); + } + if matches.opt_present("error-on-unformatted") { + options.error_on_unformatted = Some(true); + } + if let Some(ref file_lines) = matches.opt_str("file-lines") { + options.file_lines = file_lines.parse().map_err(err_msg)?; + } + } else { + let mut unstable_options = vec![]; + if matches.opt_present("skip-children") { + unstable_options.push("`--skip-children`"); + } + if matches.opt_present("error-on-unformatted") { + unstable_options.push("`--error-on-unformatted`"); + } + if matches.opt_present("file-lines") { + unstable_options.push("`--file-lines`"); + } + if !unstable_options.is_empty() { + let s = if unstable_options.len() == 1 { "" } else { "s" }; + return Err(format_err!( + "Unstable option{} ({}) used without `--unstable-features`", + s, + unstable_options.join(", "), + )); + } + } + } + + options.config_path = matches.opt_str("config-path").map(PathBuf::from); + + options.check = matches.opt_present("check"); + if let Some(ref emit_str) = matches.opt_str("emit") { + if options.check { + return Err(format_err!("Invalid to use `--emit` and `--check`")); + } + if let Ok(emit_mode) = emit_mode_from_emit_str(emit_str) { + options.emit_mode = emit_mode; + } else { + return Err(format_err!("Invalid value for `--emit`")); + } + } + + if matches.opt_present("backup") { + options.backup = true; + } + + if !rust_nightly { + if !STABLE_EMIT_MODES.contains(&options.emit_mode) { + return Err(format_err!( + "Invalid value for `--emit` - using an unstable \ + value without `--unstable-features`", + )); + } + } + + if let Some(ref color) = matches.opt_str("color") { + match Color::from_str(color) { + Ok(color) => options.color = Some(color), + _ => return Err(format_err!("Invalid color: {}", color)), + } + } + + Ok(options) + } + + fn verify_file_lines(&self, files: &[PathBuf]) { + for f in self.file_lines.files() { + match *f { + FileName::Real(ref f) if files.contains(f) => {} + FileName::Real(_) => { + eprintln!("Warning: Extra file listed in file_lines option '{}'", f) + } + FileName::Stdin => eprintln!("Warning: Not a file '{}'", f), + } + } + } +} + +impl CliOptions for GetOptsOptions { + fn apply_to(self, config: &mut Config) { + if self.verbose { + config.set().verbose(Verbosity::Verbose); + } else if self.quiet { + config.set().verbose(Verbosity::Quiet); + } else { + config.set().verbose(Verbosity::Normal); + } + config.set().file_lines(self.file_lines); + config.set().unstable_features(self.unstable_features); + if let Some(skip_children) = self.skip_children { + config.set().skip_children(skip_children); + } + if let Some(error_on_unformatted) = self.error_on_unformatted { + config.set().error_on_unformatted(error_on_unformatted); + } + if self.check { + config.set().emit_mode(EmitMode::Diff); + } else { + config.set().emit_mode(self.emit_mode); + } + if self.backup { + config.set().make_backup(true); + } + if let Some(color) = self.color { + config.set().color(color); + } + } + + fn config_path(&self) -> Option<&Path> { + self.config_path.as_ref().map(|p| &**p) + } +} + +fn emit_mode_from_emit_str(emit_str: &str) -> Result { + match emit_str { + "files" => Ok(EmitMode::Files), + "stdout" => Ok(EmitMode::Stdout), + "coverage" => Ok(EmitMode::Coverage), + "checkstyle" => Ok(EmitMode::Checkstyle), + _ => Err(format_err!("Invalid value for `--emit`")), + } +} diff --git a/cargo-fmt/main.rs b/cargo-fmt/main.rs new file mode 100644 index 00000000000..2d8234ef41e --- /dev/null +++ b/cargo-fmt/main.rs @@ -0,0 +1,381 @@ +// Copyright 2015-2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/) + +#![cfg(not(test))] +#![deny(warnings)] + +extern crate cargo_metadata; +extern crate getopts; +extern crate serde_json as json; + +use std::collections::HashSet; +use std::env; +use std::fs; +use std::hash::{Hash, Hasher}; +use std::io::{self, Write}; +use std::iter::FromIterator; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus}; +use std::str; + +use getopts::{Matches, Options}; + +fn main() { + let exit_status = execute(); + std::io::stdout().flush().unwrap(); + std::process::exit(exit_status); +} + +const SUCCESS: i32 = 0; +const FAILURE: i32 = 1; + +fn execute() -> i32 { + let mut opts = getopts::Options::new(); + opts.optflag("h", "help", "show this message"); + opts.optflag("q", "quiet", "no output printed to stdout"); + opts.optflag("v", "verbose", "use verbose output"); + opts.optmulti( + "p", + "package", + "specify package to format (only usable in workspaces)", + "", + ); + opts.optflag("", "version", "print rustfmt version and exit"); + opts.optflag("", "all", "format all packages (only usable in workspaces)"); + + // If there is any invalid argument passed to `cargo fmt`, return without formatting. + let mut is_package_arg = false; + for arg in env::args().skip(2).take_while(|a| a != "--") { + if arg.starts_with('-') { + is_package_arg = arg.starts_with("--package"); + } else if !is_package_arg { + print_usage_to_stderr(&opts, &format!("Invalid argument: `{}`.", arg)); + return FAILURE; + } else { + is_package_arg = false; + } + } + + let matches = match opts.parse(env::args().skip(1).take_while(|a| a != "--")) { + Ok(m) => m, + Err(e) => { + print_usage_to_stderr(&opts, &e.to_string()); + return FAILURE; + } + }; + + let verbosity = match (matches.opt_present("v"), matches.opt_present("q")) { + (false, false) => Verbosity::Normal, + (false, true) => Verbosity::Quiet, + (true, false) => Verbosity::Verbose, + (true, true) => { + print_usage_to_stderr(&opts, "quiet mode and verbose mode are not compatible"); + return FAILURE; + } + }; + + if matches.opt_present("h") { + print_usage_to_stdout(&opts, ""); + return SUCCESS; + } + + if matches.opt_present("version") { + return handle_command_status(get_version(verbosity), &opts); + } + + let strategy = CargoFmtStrategy::from_matches(&matches); + handle_command_status(format_crate(verbosity, &strategy), &opts) +} + +macro_rules! print_usage { + ($print:ident, $opts:ident, $reason:expr) => {{ + let msg = format!("{}\nusage: cargo fmt [options]", $reason); + $print!( + "{}\nThis utility formats all bin and lib files of the current crate using rustfmt. \ + Arguments after `--` are passed to rustfmt.", + $opts.usage(&msg) + ); + }}; +} + +fn print_usage_to_stdout(opts: &Options, reason: &str) { + print_usage!(println, opts, reason); +} + +fn print_usage_to_stderr(opts: &Options, reason: &str) { + print_usage!(eprintln, opts, reason); +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Verbosity { + Verbose, + Normal, + Quiet, +} + +fn handle_command_status(status: Result, opts: &getopts::Options) -> i32 { + match status { + Err(e) => { + print_usage_to_stderr(opts, &e.to_string()); + FAILURE + } + Ok(status) => { + if status.success() { + SUCCESS + } else { + status.code().unwrap_or(FAILURE) + } + } + } +} + +fn get_version(verbosity: Verbosity) -> Result { + run_rustfmt(&[], &[String::from("--version")], verbosity) +} + +fn format_crate( + verbosity: Verbosity, + strategy: &CargoFmtStrategy, +) -> Result { + let rustfmt_args = get_fmt_args(); + let targets = if rustfmt_args + .iter() + .any(|s| ["--print-config", "-h", "--help", "-V", "--verison"].contains(&s.as_str())) + { + HashSet::new() + } else { + get_targets(strategy)? + }; + + // Currently only bin and lib files get formatted + let files: Vec<_> = targets + .into_iter() + .inspect(|t| { + if verbosity == Verbosity::Verbose { + println!("[{}] {:?}", t.kind, t.path) + } + }).map(|t| t.path) + .collect(); + + run_rustfmt(&files, &rustfmt_args, verbosity) +} + +fn get_fmt_args() -> Vec { + // All arguments after -- are passed to rustfmt + env::args().skip_while(|a| a != "--").skip(1).collect() +} + +/// Target uses a `path` field for equality and hashing. +#[derive(Debug)] +pub struct Target { + /// A path to the main source file of the target. + path: PathBuf, + /// A kind of target (e.g. lib, bin, example, ...). + kind: String, +} + +impl Target { + pub fn from_target(target: &cargo_metadata::Target) -> Self { + let path = PathBuf::from(&target.src_path); + let canonicalized = fs::canonicalize(&path).unwrap_or(path); + + Target { + path: canonicalized, + kind: target.kind[0].clone(), + } + } +} + +impl PartialEq for Target { + fn eq(&self, other: &Target) -> bool { + self.path == other.path + } +} + +impl Eq for Target {} + +impl Hash for Target { + fn hash(&self, state: &mut H) { + self.path.hash(state); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum CargoFmtStrategy { + /// Format every packages and dependencies. + All, + /// Format packages that are specified by the command line argument. + Some(Vec), + /// Format the root packages only. + Root, +} + +impl CargoFmtStrategy { + pub fn from_matches(matches: &Matches) -> CargoFmtStrategy { + match (matches.opt_present("all"), matches.opt_present("p")) { + (false, false) => CargoFmtStrategy::Root, + (true, _) => CargoFmtStrategy::All, + (false, true) => CargoFmtStrategy::Some(matches.opt_strs("p")), + } + } +} + +/// Based on the specified `CargoFmtStrategy`, returns a set of main source files. +fn get_targets(strategy: &CargoFmtStrategy) -> Result, io::Error> { + let mut targets = HashSet::new(); + + match *strategy { + CargoFmtStrategy::Root => get_targets_root_only(&mut targets)?, + CargoFmtStrategy::All => get_targets_recursive(None, &mut targets, &mut HashSet::new())?, + CargoFmtStrategy::Some(ref hitlist) => get_targets_with_hitlist(hitlist, &mut targets)?, + } + + if targets.is_empty() { + Err(io::Error::new( + io::ErrorKind::Other, + "Failed to find targets".to_owned(), + )) + } else { + Ok(targets) + } +} + +fn get_targets_root_only(targets: &mut HashSet) -> Result<(), io::Error> { + let metadata = get_cargo_metadata(None)?; + let current_dir = env::current_dir()?.canonicalize()?; + let current_dir_manifest = current_dir.join("Cargo.toml"); + let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?; + let in_workspace_root = workspace_root_path == current_dir; + + for package in metadata.packages { + if in_workspace_root || PathBuf::from(&package.manifest_path) == current_dir_manifest { + for target in package.targets { + targets.insert(Target::from_target(&target)); + } + } + } + + Ok(()) +} + +fn get_targets_recursive( + manifest_path: Option<&Path>, + mut targets: &mut HashSet, + visited: &mut HashSet, +) -> Result<(), io::Error> { + let metadata = get_cargo_metadata(manifest_path)?; + + for package in metadata.packages { + add_targets(&package.targets, &mut targets); + + // Look for local dependencies. + for dependency in package.dependencies { + if dependency.source.is_some() || visited.contains(&dependency.name) { + continue; + } + + let mut manifest_path = PathBuf::from(&package.manifest_path); + + manifest_path.pop(); + manifest_path.push(&dependency.name); + manifest_path.push("Cargo.toml"); + + if manifest_path.exists() { + visited.insert(dependency.name); + get_targets_recursive(Some(&manifest_path), &mut targets, visited)?; + } + } + } + + Ok(()) +} + +fn get_targets_with_hitlist( + hitlist: &[String], + targets: &mut HashSet, +) -> Result<(), io::Error> { + let metadata = get_cargo_metadata(None)?; + + let mut workspace_hitlist: HashSet<&String> = HashSet::from_iter(hitlist); + + for package in metadata.packages { + if workspace_hitlist.remove(&package.name) { + for target in package.targets { + targets.insert(Target::from_target(&target)); + } + } + } + + if workspace_hitlist.is_empty() { + Ok(()) + } else { + let package = workspace_hitlist.iter().next().unwrap(); + Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("package `{}` is not a member of the workspace", package), + )) + } +} + +fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut HashSet) { + for target in target_paths { + targets.insert(Target::from_target(target)); + } +} + +fn run_rustfmt( + files: &[PathBuf], + fmt_args: &[String], + verbosity: Verbosity, +) -> Result { + let stdout = if verbosity == Verbosity::Quiet { + std::process::Stdio::null() + } else { + std::process::Stdio::inherit() + }; + + if verbosity == Verbosity::Verbose { + print!("rustfmt"); + for a in fmt_args { + print!(" {}", a); + } + for f in files { + print!(" {}", f.display()); + } + println!(); + } + + let mut command = Command::new("rustfmt") + .stdout(stdout) + .args(files) + .args(fmt_args) + .spawn() + .map_err(|e| match e.kind() { + io::ErrorKind::NotFound => io::Error::new( + io::ErrorKind::Other, + "Could not run rustfmt, please make sure it is in your PATH.", + ), + _ => e, + })?; + + command.wait() +} + +fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result { + match cargo_metadata::metadata(manifest_path) { + Ok(metadata) => Ok(metadata), + Err(..) => Err(io::Error::new( + io::ErrorKind::Other, + "`cargo manifest` failed.", + )), + } +} diff --git a/config/config_type.rs b/config/config_type.rs new file mode 100644 index 00000000000..aea19b34375 --- /dev/null +++ b/config/config_type.rs @@ -0,0 +1,452 @@ +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use config::file_lines::FileLines; +use config::options::{IgnoreList, WidthHeuristics}; + +/// Trait for types that can be used in `Config`. +pub trait ConfigType: Sized { + /// Returns hint text for use in `Config::print_docs()`. For enum types, this is a + /// pipe-separated list of variants; for other types it returns "". + fn doc_hint() -> String; +} + +impl ConfigType for bool { + fn doc_hint() -> String { + String::from("") + } +} + +impl ConfigType for usize { + fn doc_hint() -> String { + String::from("") + } +} + +impl ConfigType for isize { + fn doc_hint() -> String { + String::from("") + } +} + +impl ConfigType for String { + fn doc_hint() -> String { + String::from("") + } +} + +impl ConfigType for FileLines { + fn doc_hint() -> String { + String::from("") + } +} + +impl ConfigType for WidthHeuristics { + fn doc_hint() -> String { + String::new() + } +} + +impl ConfigType for IgnoreList { + fn doc_hint() -> String { + String::from("[,..]") + } +} + +/// Check if we're in a nightly build. +/// +/// The environment variable `CFG_RELEASE_CHANNEL` is set during the rustc bootstrap +/// to "stable", "beta", or "nightly" depending on what toolchain is being built. +/// If we are being built as part of the stable or beta toolchains, we want +/// to disable unstable configuration options. +/// +/// If we're being built by cargo (e.g. `cargo +nightly install rustfmt-nightly`), +/// `CFG_RELEASE_CHANNEL` is not set. As we only support being built against the +/// nightly compiler when installed from crates.io, default to nightly mode. +macro_rules! is_nightly_channel { + () => { + option_env!("CFG_RELEASE_CHANNEL") + .map(|c| c == "nightly" || c == "dev") + .unwrap_or(true) + }; +} + +macro_rules! create_config { + ($($i:ident: $ty:ty, $def:expr, $stb:expr, $( $dstring:expr ),+ );+ $(;)*) => ( + #[cfg(test)] + use std::collections::HashSet; + use std::io::Write; + + #[derive(Clone)] + pub struct Config { + // if a license_template_path has been specified, successfully read, parsed and compiled + // into a regex, it will be stored here + pub license_template: Option, + // For each config item, we store a bool indicating whether it has + // been accessed and the value, and a bool whether the option was + // manually initialised, or taken from the default, + $($i: (Cell, bool, $ty, bool)),+ + } + + // Just like the Config struct but with each property wrapped + // as Option. This is used to parse a rustfmt.toml that doesn't + // specify all properties of `Config`. + // We first parse into `PartialConfig`, then create a default `Config` + // and overwrite the properties with corresponding values from `PartialConfig`. + #[derive(Deserialize, Serialize, Clone)] + pub struct PartialConfig { + $(pub $i: Option<$ty>),+ + } + + impl PartialConfig { + pub fn to_toml(&self) -> Result { + // Non-user-facing options can't be specified in TOML + let mut cloned = self.clone(); + cloned.file_lines = None; + cloned.verbose = None; + cloned.width_heuristics = None; + + ::toml::to_string(&cloned) + .map_err(|e| format!("Could not output config: {}", e.to_string())) + } + } + + // Macro hygiene won't allow us to make `set_$i()` methods on Config + // for each item, so this struct is used to give the API to set values: + // `config.set().option(false)`. It's pretty ugly. Consider replacing + // with `config.set_option(false)` if we ever get a stable/usable + // `concat_idents!()`. + pub struct ConfigSetter<'a>(&'a mut Config); + + impl<'a> ConfigSetter<'a> { + $( + pub fn $i(&mut self, value: $ty) { + (self.0).$i.2 = value; + match stringify!($i) { + "max_width" | "use_small_heuristics" => self.0.set_heuristics(), + "license_template_path" => self.0.set_license_template(), + &_ => (), + } + } + )+ + } + + // Query each option, returns true if the user set the option, false if + // a default was used. + pub struct ConfigWasSet<'a>(&'a Config); + + impl<'a> ConfigWasSet<'a> { + $( + pub fn $i(&self) -> bool { + (self.0).$i.1 + } + )+ + } + + impl Config { + pub(crate) fn version_meets_requirement(&self) -> bool { + if self.was_set().required_version() { + let version = env!("CARGO_PKG_VERSION"); + let required_version = self.required_version(); + if version != required_version { + println!( + "Error: rustfmt version ({}) doesn't match the required version ({})", + version, + required_version, + ); + return false; + } + } + + true + } + + $( + pub fn $i(&self) -> $ty { + self.$i.0.set(true); + self.$i.2.clone() + } + )+ + + pub fn set<'a>(&'a mut self) -> ConfigSetter<'a> { + ConfigSetter(self) + } + + pub fn was_set<'a>(&'a self) -> ConfigWasSet<'a> { + ConfigWasSet(self) + } + + fn fill_from_parsed_config(mut self, parsed: PartialConfig, dir: &Path) -> Config { + $( + if let Some(val) = parsed.$i { + if self.$i.3 { + self.$i.1 = true; + self.$i.2 = val; + } else { + if is_nightly_channel!() { + self.$i.1 = true; + self.$i.2 = val; + } else { + eprintln!("Warning: can't set `{} = {:?}`, unstable features are only \ + available in nightly channel.", stringify!($i), val); + } + } + } + )+ + self.set_heuristics(); + self.set_license_template(); + self.set_ignore(dir); + self + } + + /// Returns a hash set initialized with every user-facing config option name. + #[cfg(test)] + pub(crate) fn hash_set() -> HashSet { + let mut hash_set = HashSet::new(); + $( + hash_set.insert(stringify!($i).to_owned()); + )+ + hash_set + } + + pub(crate) fn is_valid_name(name: &str) -> bool { + match name { + $( + stringify!($i) => true, + )+ + _ => false, + } + } + + pub(crate) fn from_toml(toml: &str, dir: &Path) -> Result { + let parsed: ::toml::Value = + toml.parse().map_err(|e| format!("Could not parse TOML: {}", e))?; + let mut err: String = String::new(); + { + let table = parsed + .as_table() + .ok_or(String::from("Parsed config was not table"))?; + for key in table.keys() { + if !Config::is_valid_name(key) { + let msg = &format!("Warning: Unknown configuration option `{}`\n", key); + err.push_str(msg) + } + } + } + match parsed.try_into() { + Ok(parsed_config) => { + if !err.is_empty() { + eprint!("{}", err); + } + Ok(Config::default().fill_from_parsed_config(parsed_config, dir: &Path)) + } + Err(e) => { + err.push_str("Error: Decoding config file failed:\n"); + err.push_str(format!("{}\n", e).as_str()); + err.push_str("Please check your config file."); + Err(err) + } + } + } + + pub fn used_options(&self) -> PartialConfig { + PartialConfig { + $( + $i: if self.$i.0.get() { + Some(self.$i.2.clone()) + } else { + None + }, + )+ + } + } + + pub fn all_options(&self) -> PartialConfig { + PartialConfig { + $( + $i: Some(self.$i.2.clone()), + )+ + } + } + + pub fn override_value(&mut self, key: &str, val: &str) + { + match key { + $( + stringify!($i) => { + self.$i.1 = true; + self.$i.2 = val.parse::<$ty>() + .expect(&format!("Failed to parse override for {} (\"{}\") as a {}", + stringify!($i), + val, + stringify!($ty))); + } + )+ + _ => panic!("Unknown config key in override: {}", key) + } + + match key { + "max_width" | "use_small_heuristics" => self.set_heuristics(), + "license_template_path" => self.set_license_template(), + &_ => (), + } + } + + /// Construct a `Config` from the toml file specified at `file_path`. + /// + /// This method only looks at the provided path, for a method that + /// searches parents for a `rustfmt.toml` see `from_resolved_toml_path`. + /// + /// Return a `Config` if the config could be read and parsed from + /// the file, Error otherwise. + pub(super) fn from_toml_path(file_path: &Path) -> Result { + let mut file = File::open(&file_path)?; + let mut toml = String::new(); + file.read_to_string(&mut toml)?; + Config::from_toml(&toml, file_path.parent().unwrap()) + .map_err(|err| Error::new(ErrorKind::InvalidData, err)) + } + + /// Resolve the config for input in `dir`. + /// + /// Searches for `rustfmt.toml` beginning with `dir`, and + /// recursively checking parents of `dir` if no config file is found. + /// If no config file exists in `dir` or in any parent, a + /// default `Config` will be returned (and the returned path will be empty). + /// + /// Returns the `Config` to use, and the path of the project file if there was + /// one. + pub(super) fn from_resolved_toml_path( + dir: &Path, + ) -> Result<(Config, Option), Error> { + /// Try to find a project file in the given directory and its parents. + /// Returns the path of a the nearest project file if one exists, + /// or `None` if no project file was found. + fn resolve_project_file(dir: &Path) -> Result, Error> { + let mut current = if dir.is_relative() { + env::current_dir()?.join(dir) + } else { + dir.to_path_buf() + }; + + current = fs::canonicalize(current)?; + + loop { + match get_toml_path(¤t) { + Ok(Some(path)) => return Ok(Some(path)), + Err(e) => return Err(e), + _ => () + } + + // If the current directory has no parent, we're done searching. + if !current.pop() { + return Ok(None); + } + } + } + + match resolve_project_file(dir)? { + None => Ok((Config::default(), None)), + Some(path) => Config::from_toml_path(&path).map(|config| (config, Some(path))), + } + } + + pub fn is_hidden_option(name: &str) -> bool { + const HIDE_OPTIONS: [&str; 4] = + ["verbose", "verbose_diff", "file_lines", "width_heuristics"]; + HIDE_OPTIONS.contains(&name) + } + + pub fn print_docs(out: &mut Write, include_unstable: bool) { + use std::cmp; + let max = 0; + $( let max = cmp::max(max, stringify!($i).len()+1); )+ + let mut space_str = String::with_capacity(max); + for _ in 0..max { + space_str.push(' '); + } + writeln!(out, "Configuration Options:").unwrap(); + $( + if $stb || include_unstable { + let name_raw = stringify!($i); + + if !Config::is_hidden_option(name_raw) { + 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(' '); + writeln!(out, + "{}{} Default: {:?}{}", + name_out, + <$ty>::doc_hint(), + $def, + if !$stb { " (unstable)" } else { "" }).unwrap(); + $( + writeln!(out, "{}{}", space_str, $dstring).unwrap(); + )+ + writeln!(out).unwrap(); + } + } + )+ + } + + fn set_heuristics(&mut self) { + if self.use_small_heuristics.2 == Heuristics::Default { + let max_width = self.max_width.2; + self.set().width_heuristics(WidthHeuristics::scaled(max_width)); + } else if self.use_small_heuristics.2 == Heuristics::Max { + let max_width = self.max_width.2; + self.set().width_heuristics(WidthHeuristics::set(max_width)); + } else { + self.set().width_heuristics(WidthHeuristics::null()); + } + } + + fn set_license_template(&mut self) { + if self.was_set().license_template_path() { + let lt_path = self.license_template_path(); + match license::load_and_compile_template(<_path) { + Ok(re) => self.license_template = Some(re), + Err(msg) => eprintln!("Warning for license template file {:?}: {}", + lt_path, msg), + } + } + } + + fn set_ignore(&mut self, dir: &Path) { + self.ignore.2.add_prefix(dir); + } + + /// Returns true if the config key was explicitely set and is the default value. + pub fn is_default(&self, key: &str) -> bool { + $( + if let stringify!($i) = key { + return self.$i.1 && self.$i.2 == $def; + } + )+ + false + } + } + + // Template for the default configuration + impl Default for Config { + fn default() -> Config { + Config { + license_template: None, + $( + $i: (Cell::new(false), false, $def, $stb), + )+ + } + } + } + ) +} diff --git a/config/file_lines.rs b/config/file_lines.rs new file mode 100644 index 00000000000..e113118c643 --- /dev/null +++ b/config/file_lines.rs @@ -0,0 +1,422 @@ +// Copyright 2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! This module contains types and functions to support formatting specific line ranges. + +use std::collections::HashMap; +use std::path::PathBuf; +use std::rc::Rc; +use std::{cmp, fmt, iter, str}; + +use serde::de::{Deserialize, Deserializer}; +use serde::ser::{self, Serialize, Serializer}; +use serde_json as json; + +use syntax::source_map::{self, SourceFile}; + +/// A range of lines in a file, inclusive of both ends. +pub struct LineRange { + pub file: Rc, + pub lo: usize, + pub hi: usize, +} + +/// Defines the name of an input - either a file or stdin. +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FileName { + Real(PathBuf), + Stdin, +} + +impl From for FileName { + fn from(name: source_map::FileName) -> FileName { + match name { + source_map::FileName::Real(p) => FileName::Real(p), + source_map::FileName::Custom(ref f) if f == "stdin" => FileName::Stdin, + _ => unreachable!(), + } + } +} + +impl fmt::Display for FileName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FileName::Real(p) => write!(f, "{}", p.to_str().unwrap()), + FileName::Stdin => write!(f, "stdin"), + } + } +} + +impl<'de> Deserialize<'de> for FileName { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s == "stdin" { + Ok(FileName::Stdin) + } else { + Ok(FileName::Real(s.into())) + } + } +} + +impl Serialize for FileName { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = match self { + FileName::Stdin => Ok("stdin"), + FileName::Real(path) => path + .to_str() + .ok_or_else(|| ser::Error::custom("path can't be serialized as UTF-8 string")), + }; + + s.and_then(|s| serializer.serialize_str(s)) + } +} + +impl LineRange { + pub fn file_name(&self) -> FileName { + self.file.name.clone().into() + } +} + +/// A range that is inclusive of both ends. +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize)] +pub struct Range { + lo: usize, + hi: usize, +} + +impl<'a> From<&'a LineRange> for Range { + fn from(range: &'a LineRange) -> Range { + Range::new(range.lo, range.hi) + } +} + +impl Range { + pub fn new(lo: usize, hi: usize) -> Range { + Range { lo, hi } + } + + fn is_empty(self) -> bool { + self.lo > self.hi + } + + #[allow(dead_code)] + fn contains(self, other: Range) -> bool { + if other.is_empty() { + true + } else { + !self.is_empty() && self.lo <= other.lo && self.hi >= other.hi + } + } + + fn intersects(self, other: Range) -> bool { + if self.is_empty() || other.is_empty() { + false + } else { + (self.lo <= other.hi && other.hi <= self.hi) + || (other.lo <= self.hi && self.hi <= other.hi) + } + } + + fn adjacent_to(self, other: Range) -> bool { + if self.is_empty() || other.is_empty() { + false + } else { + self.hi + 1 == other.lo || other.hi + 1 == self.lo + } + } + + /// Returns a new `Range` with lines from `self` and `other` if they were adjacent or + /// intersect; returns `None` otherwise. + fn merge(self, other: Range) -> Option { + if self.adjacent_to(other) || self.intersects(other) { + Some(Range::new( + cmp::min(self.lo, other.lo), + cmp::max(self.hi, other.hi), + )) + } else { + None + } + } +} + +/// A set of lines in files. +/// +/// It is represented as a multimap keyed on file names, with values a collection of +/// non-overlapping ranges sorted by their start point. An inner `None` is interpreted to mean all +/// lines in all files. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FileLines(Option>>); + +/// Normalizes the ranges so that the invariants for `FileLines` hold: ranges are non-overlapping, +/// and ordered by their start point. +fn normalize_ranges(ranges: &mut HashMap>) { + for ranges in ranges.values_mut() { + ranges.sort(); + let mut result = vec![]; + { + let mut iter = ranges.into_iter().peekable(); + while let Some(next) = iter.next() { + let mut next = *next; + while let Some(&&mut peek) = iter.peek() { + if let Some(merged) = next.merge(peek) { + iter.next().unwrap(); + next = merged; + } else { + break; + } + } + result.push(next) + } + } + *ranges = result; + } +} + +impl FileLines { + /// Creates a `FileLines` that contains all lines in all files. + pub(crate) fn all() -> FileLines { + FileLines(None) + } + + /// Returns true if this `FileLines` contains all lines in all files. + pub(crate) fn is_all(&self) -> bool { + self.0.is_none() + } + + pub fn from_ranges(mut ranges: HashMap>) -> FileLines { + normalize_ranges(&mut ranges); + FileLines(Some(ranges)) + } + + /// Returns an iterator over the files contained in `self`. + pub fn files(&self) -> Files { + Files(self.0.as_ref().map(|m| m.keys())) + } + + /// Returns JSON representation as accepted by the `--file-lines JSON` arg. + pub fn to_json_spans(&self) -> Vec { + match &self.0 { + None => vec![], + Some(file_ranges) => file_ranges + .iter() + .flat_map(|(file, ranges)| ranges.iter().map(move |r| (file, r))) + .map(|(file, range)| JsonSpan { + file: file.to_owned(), + range: (range.lo, range.hi), + }).collect(), + } + } + + /// Returns true if `self` includes all lines in all files. Otherwise runs `f` on all ranges in + /// the designated file (if any) and returns true if `f` ever does. + fn file_range_matches(&self, file_name: &FileName, f: F) -> bool + where + F: FnMut(&Range) -> bool, + { + let map = match self.0 { + // `None` means "all lines in all files". + None => return true, + Some(ref map) => map, + }; + + match canonicalize_path_string(file_name).and_then(|file| map.get(&file)) { + Some(ranges) => ranges.iter().any(f), + None => false, + } + } + + /// Returns true if `range` is fully contained in `self`. + #[allow(dead_code)] + pub(crate) fn contains(&self, range: &LineRange) -> bool { + self.file_range_matches(&range.file_name(), |r| r.contains(Range::from(range))) + } + + /// Returns true if any lines in `range` are in `self`. + pub(crate) fn intersects(&self, range: &LineRange) -> bool { + self.file_range_matches(&range.file_name(), |r| r.intersects(Range::from(range))) + } + + /// Returns true if `line` from `file_name` is in `self`. + pub(crate) fn contains_line(&self, file_name: &FileName, line: usize) -> bool { + self.file_range_matches(file_name, |r| r.lo <= line && r.hi >= line) + } + + /// Returns true if all the lines between `lo` and `hi` from `file_name` are in `self`. + pub(crate) fn contains_range(&self, file_name: &FileName, lo: usize, hi: usize) -> bool { + self.file_range_matches(file_name, |r| r.contains(Range::new(lo, hi))) + } +} + +/// `FileLines` files iterator. +pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, FileName, Vec>>); + +impl<'a> iter::Iterator for Files<'a> { + type Item = &'a FileName; + + fn next(&mut self) -> Option<&'a FileName> { + self.0.as_mut().and_then(Iterator::next) + } +} + +fn canonicalize_path_string(file: &FileName) -> Option { + match *file { + FileName::Real(ref path) => path.canonicalize().ok().map(FileName::Real), + _ => Some(file.clone()), + } +} + +// This impl is needed for `Config::override_value` to work for use in tests. +impl str::FromStr for FileLines { + type Err = String; + + fn from_str(s: &str) -> Result { + let v: Vec = json::from_str(s).map_err(|e| e.to_string())?; + let mut m = HashMap::new(); + for js in v { + let (s, r) = JsonSpan::into_tuple(js)?; + m.entry(s).or_insert_with(|| vec![]).push(r); + } + Ok(FileLines::from_ranges(m)) + } +} + +// For JSON decoding. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] +pub struct JsonSpan { + file: FileName, + range: (usize, usize), +} + +impl JsonSpan { + fn into_tuple(self) -> Result<(FileName, Range), String> { + let (lo, hi) = self.range; + let canonical = canonicalize_path_string(&self.file) + .ok_or_else(|| format!("Can't canonicalize {}", &self.file))?; + Ok((canonical, Range::new(lo, hi))) + } +} + +// This impl is needed for inclusion in the `Config` struct. We don't have a toml representation +// for `FileLines`, so it will just panic instead. +impl<'de> ::serde::de::Deserialize<'de> for FileLines { + fn deserialize(_: D) -> Result + where + D: ::serde::de::Deserializer<'de>, + { + panic!( + "FileLines cannot be deserialized from a project rustfmt.toml file: please \ + specify it via the `--file-lines` option instead" + ); + } +} + +// We also want to avoid attempting to serialize a FileLines to toml. The +// `Config` struct should ensure this impl is never reached. +impl ::serde::ser::Serialize for FileLines { + fn serialize(&self, _: S) -> Result + where + S: ::serde::ser::Serializer, + { + unreachable!("FileLines cannot be serialized. This is a rustfmt bug."); + } +} + +#[cfg(test)] +mod test { + use super::Range; + + #[test] + fn test_range_intersects() { + assert!(Range::new(1, 2).intersects(Range::new(1, 1))); + assert!(Range::new(1, 2).intersects(Range::new(2, 2))); + assert!(!Range::new(1, 2).intersects(Range::new(0, 0))); + assert!(!Range::new(1, 2).intersects(Range::new(3, 10))); + assert!(!Range::new(1, 3).intersects(Range::new(5, 5))); + } + + #[test] + fn test_range_adjacent_to() { + assert!(!Range::new(1, 2).adjacent_to(Range::new(1, 1))); + assert!(!Range::new(1, 2).adjacent_to(Range::new(2, 2))); + assert!(Range::new(1, 2).adjacent_to(Range::new(0, 0))); + assert!(Range::new(1, 2).adjacent_to(Range::new(3, 10))); + assert!(!Range::new(1, 3).adjacent_to(Range::new(5, 5))); + } + + #[test] + fn test_range_contains() { + assert!(Range::new(1, 2).contains(Range::new(1, 1))); + assert!(Range::new(1, 2).contains(Range::new(2, 2))); + assert!(!Range::new(1, 2).contains(Range::new(0, 0))); + assert!(!Range::new(1, 2).contains(Range::new(3, 10))); + } + + #[test] + fn test_range_merge() { + assert_eq!(None, Range::new(1, 3).merge(Range::new(5, 5))); + assert_eq!(None, Range::new(4, 7).merge(Range::new(0, 1))); + assert_eq!( + Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(4, 7)) + ); + assert_eq!( + Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(5, 7)) + ); + assert_eq!( + Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(6, 7)) + ); + assert_eq!( + Some(Range::new(3, 7)), + Range::new(3, 7).merge(Range::new(4, 5)) + ); + } + + use super::json::{self, json}; + use super::{FileLines, FileName}; + use std::{collections::HashMap, path::PathBuf}; + + #[test] + fn file_lines_to_json() { + let ranges: HashMap> = [ + ( + FileName::Real(PathBuf::from("src/main.rs")), + vec![Range::new(1, 3), Range::new(5, 7)], + ), + ( + FileName::Real(PathBuf::from("src/lib.rs")), + vec![Range::new(1, 7)], + ), + ] + .iter() + .cloned() + .collect(); + + let file_lines = FileLines::from_ranges(ranges); + let mut spans = file_lines.to_json_spans(); + spans.sort(); + let json = json::to_value(&spans).unwrap(); + assert_eq!( + json, + json! {[ + {"file": "src/lib.rs", "range": [1, 7]}, + {"file": "src/main.rs", "range": [1, 3]}, + {"file": "src/main.rs", "range": [5, 7]}, + ]} + ); + } +} diff --git a/config/license.rs b/config/license.rs new file mode 100644 index 00000000000..630399319c1 --- /dev/null +++ b/config/license.rs @@ -0,0 +1,266 @@ +use std::fmt; +use std::fs::File; +use std::io; +use std::io::Read; + +use regex; +use regex::Regex; + +#[derive(Debug)] +pub enum LicenseError { + IO(io::Error), + Regex(regex::Error), + Parse(String), +} + +impl fmt::Display for LicenseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + LicenseError::IO(ref err) => err.fmt(f), + LicenseError::Regex(ref err) => err.fmt(f), + LicenseError::Parse(ref err) => write!(f, "parsing failed, {}", err), + } + } +} + +impl From for LicenseError { + fn from(err: io::Error) -> LicenseError { + LicenseError::IO(err) + } +} + +impl From for LicenseError { + fn from(err: regex::Error) -> LicenseError { + LicenseError::Regex(err) + } +} + +// the template is parsed using a state machine +enum ParsingState { + Lit, + LitEsc, + // the u32 keeps track of brace nesting + Re(u32), + ReEsc(u32), + Abort(String), +} + +use self::ParsingState::*; + +pub struct TemplateParser { + parsed: String, + buffer: String, + state: ParsingState, + linum: u32, + open_brace_line: u32, +} + +impl TemplateParser { + fn new() -> Self { + Self { + parsed: "^".to_owned(), + buffer: String::new(), + state: Lit, + linum: 1, + // keeps track of last line on which a regex placeholder was started + open_brace_line: 0, + } + } + + /// Convert a license template into a string which can be turned into a regex. + /// + /// The license template could use regex syntax directly, but that would require a lot of manual + /// escaping, which is inconvenient. It is therefore literal by default, with optional regex + /// subparts delimited by `{` and `}`. Additionally: + /// + /// - to insert literal `{`, `}` or `\`, escape it with `\` + /// - an empty regex placeholder (`{}`) is shorthand for `{.*?}` + /// + /// This function parses this input format and builds a properly escaped *string* representation + /// of the equivalent regular expression. It **does not** however guarantee that the returned + /// string is a syntactically valid regular expression. + /// + /// # Examples + /// + /// ```ignore + /// assert_eq!( + /// TemplateParser::parse( + /// r" + /// // Copyright {\d+} The \} Rust \\ Project \{ Developers. See the {([A-Z]+)} + /// // file at the top-level directory of this distribution and at + /// // {}. + /// // + /// // Licensed under the Apache License, Version 2.0 or the MIT license + /// // , at your + /// // option. This file may not be copied, modified, or distributed + /// // except according to those terms. + /// " + /// ).unwrap(), + /// r"^ + /// // Copyright \d+ The \} Rust \\ Project \{ Developers\. See the ([A-Z]+) + /// // file at the top\-level directory of this distribution and at + /// // .*?\. + /// // + /// // Licensed under the Apache License, Version 2\.0 or the MIT license + /// // , at your + /// // option\. This file may not be copied, modified, or distributed + /// // except according to those terms\. + /// " + /// ); + /// ``` + pub fn parse(template: &str) -> Result { + let mut parser = Self::new(); + for chr in template.chars() { + if chr == '\n' { + parser.linum += 1; + } + parser.state = match parser.state { + Lit => parser.trans_from_lit(chr), + LitEsc => parser.trans_from_litesc(chr), + Re(brace_nesting) => parser.trans_from_re(chr, brace_nesting), + ReEsc(brace_nesting) => parser.trans_from_reesc(chr, brace_nesting), + Abort(msg) => return Err(LicenseError::Parse(msg)), + }; + } + // check if we've ended parsing in a valid state + match parser.state { + Abort(msg) => return Err(LicenseError::Parse(msg)), + Re(_) | ReEsc(_) => { + return Err(LicenseError::Parse(format!( + "escape or balance opening brace on l. {}", + parser.open_brace_line + ))); + } + LitEsc => { + return Err(LicenseError::Parse(format!( + "incomplete escape sequence on l. {}", + parser.linum + ))) + } + _ => (), + } + parser.parsed.push_str(®ex::escape(&parser.buffer)); + + Ok(parser.parsed) + } + + fn trans_from_lit(&mut self, chr: char) -> ParsingState { + match chr { + '{' => { + self.parsed.push_str(®ex::escape(&self.buffer)); + self.buffer.clear(); + self.open_brace_line = self.linum; + Re(1) + } + '}' => Abort(format!( + "escape or balance closing brace on l. {}", + self.linum + )), + '\\' => LitEsc, + _ => { + self.buffer.push(chr); + Lit + } + } + } + + fn trans_from_litesc(&mut self, chr: char) -> ParsingState { + self.buffer.push(chr); + Lit + } + + fn trans_from_re(&mut self, chr: char, brace_nesting: u32) -> ParsingState { + match chr { + '{' => { + self.buffer.push(chr); + Re(brace_nesting + 1) + } + '}' => { + match brace_nesting { + 1 => { + // default regex for empty placeholder {} + if self.buffer.is_empty() { + self.parsed.push_str(".*?"); + } else { + self.parsed.push_str(&self.buffer); + } + self.buffer.clear(); + Lit + } + _ => { + self.buffer.push(chr); + Re(brace_nesting - 1) + } + } + } + '\\' => { + self.buffer.push(chr); + ReEsc(brace_nesting) + } + _ => { + self.buffer.push(chr); + Re(brace_nesting) + } + } + } + + fn trans_from_reesc(&mut self, chr: char, brace_nesting: u32) -> ParsingState { + self.buffer.push(chr); + Re(brace_nesting) + } +} + +pub fn load_and_compile_template(path: &str) -> Result { + let mut lt_file = File::open(&path)?; + let mut lt_str = String::new(); + lt_file.read_to_string(&mut lt_str)?; + let lt_parsed = TemplateParser::parse(<_str)?; + Ok(Regex::new(<_parsed)?) +} + +#[cfg(test)] +mod test { + use super::TemplateParser; + + #[test] + fn test_parse_license_template() { + assert_eq!( + TemplateParser::parse("literal (.*)").unwrap(), + r"^literal \(\.\*\)" + ); + assert_eq!( + TemplateParser::parse(r"escaping \}").unwrap(), + r"^escaping \}" + ); + assert!(TemplateParser::parse("unbalanced } without escape").is_err()); + assert_eq!( + TemplateParser::parse(r"{\d+} place{-?}holder{s?}").unwrap(), + r"^\d+ place-?holders?" + ); + assert_eq!(TemplateParser::parse("default {}").unwrap(), "^default .*?"); + assert_eq!( + TemplateParser::parse(r"unbalanced nested braces {\{{3}}").unwrap(), + r"^unbalanced nested braces \{{3}" + ); + assert_eq!( + &TemplateParser::parse("parsing error }") + .unwrap_err() + .to_string(), + "parsing failed, escape or balance closing brace on l. 1" + ); + assert_eq!( + &TemplateParser::parse("parsing error {\nsecond line") + .unwrap_err() + .to_string(), + "parsing failed, escape or balance opening brace on l. 1" + ); + assert_eq!( + &TemplateParser::parse(r"parsing error \") + .unwrap_err() + .to_string(), + "parsing failed, incomplete escape sequence on l. 1" + ); + } +} diff --git a/config/lists.rs b/config/lists.rs new file mode 100644 index 00000000000..04406e8d566 --- /dev/null +++ b/config/lists.rs @@ -0,0 +1,105 @@ +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Configuration options related to rewriting a list. + +use config::config_type::ConfigType; +use config::IndentStyle; + +/// The definitive formatting tactic for lists. +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum DefinitiveListTactic { + Vertical, + Horizontal, + Mixed, + /// Special case tactic for `format!()`, `write!()` style macros. + SpecialMacro(usize), +} + +impl DefinitiveListTactic { + pub fn ends_with_newline(&self, indent_style: IndentStyle) -> bool { + match indent_style { + IndentStyle::Block => *self != DefinitiveListTactic::Horizontal, + IndentStyle::Visual => false, + } + } +} + +/// Formatting tactic for lists. This will be cast down to a +/// `DefinitiveListTactic` depending on the number and length of the items and +/// their comments. +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum ListTactic { + // One item per row. + Vertical, + // All items on one row. + Horizontal, + // Try Horizontal layout, if that fails then vertical. + HorizontalVertical, + // HorizontalVertical with a soft limit of n characters. + LimitedHorizontalVertical(usize), + // Pack as many items as possible per row over (possibly) many rows. + Mixed, +} + +impl_enum_serialize_and_deserialize!(ListTactic, Vertical, Horizontal, HorizontalVertical, Mixed); + +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum SeparatorTactic { + Always, + Never, + Vertical, +} + +impl_enum_serialize_and_deserialize!(SeparatorTactic, Always, Never, Vertical); + +impl SeparatorTactic { + pub fn from_bool(b: bool) -> SeparatorTactic { + if b { + SeparatorTactic::Always + } else { + SeparatorTactic::Never + } + } +} + +/// Where to put separator. +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum SeparatorPlace { + Front, + Back, +} + +impl_enum_serialize_and_deserialize!(SeparatorPlace, Front, Back); + +impl SeparatorPlace { + pub fn is_front(&self) -> bool { + *self == SeparatorPlace::Front + } + + pub fn is_back(&self) -> bool { + *self == SeparatorPlace::Back + } + + pub fn from_tactic( + default: SeparatorPlace, + tactic: DefinitiveListTactic, + sep: &str, + ) -> SeparatorPlace { + match tactic { + DefinitiveListTactic::Vertical => default, + _ => if sep == "," { + SeparatorPlace::Back + } else { + default + }, + } + } +} diff --git a/config/mod.rs b/config/mod.rs new file mode 100644 index 00000000000..f240c7b13c6 --- /dev/null +++ b/config/mod.rs @@ -0,0 +1,360 @@ +// Copyright 2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use regex::Regex; +use std::cell::Cell; +use std::default::Default; +use std::fs::File; +use std::io::{Error, ErrorKind, Read}; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +use config::config_type::ConfigType; +pub use config::file_lines::{FileLines, FileName, Range}; +pub use config::lists::*; +pub use config::options::*; + +#[macro_use] +pub mod config_type; +#[macro_use] +pub mod options; + +pub mod file_lines; +pub mod license; +pub mod lists; + +/// 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"; + use_small_heuristics: Heuristics, Heuristics::Default, true, "Whether to use different \ + formatting for items and expressions if they satisfy a heuristic notion of 'small'"; + indent_style: IndentStyle, IndentStyle::Block, false, "How do we indent expressions or items"; + + // Comments. macros, and strings + wrap_comments: bool, false, false, "Break comments to fit on the line"; + 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"; + license_template_path: String, String::default(), false, + "Beginning of file must match license template"; + 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"; + + // 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"; + merge_imports: bool, false, false, "Merge imports"; + + // 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"; + struct_field_align_threshold: usize, 0, false, "Align struct fields if their diffs fits 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"; + force_multiline_blocks: bool, false, false, + "Force multiline closure bodies and match arms to be wrapped in a block"; + fn_args_density: Density, Density::Tall, false, "Argument density in functions"; + 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, false, + "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, false, "The edition of the parser (RFC 2052)"; + + // 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) + color: Color, Color::Auto, false, + "What Color option to use when none is supplied: Always, Never, Auto"; + required_version: String, env!("CARGO_PKG_VERSION").to_owned(), false, + "Require a specific version of rustfmt"; + unstable_features: bool, false, false, + "Enables unstable features. Only available on nightly channel"; + disable_all_formatting: bool, false, false, "Don't reformat anything"; + skip_children: bool, false, false, "Don't reformat out of line modules"; + hide_parse_errors: bool, false, false, "Hide errors from the parser"; + error_on_line_overflow: bool, false, false, "Error if unable to get all lines within max_width"; + error_on_unformatted: bool, false, false, + "Error if unable to get comments or string literals within max_width, \ + or they are left with trailing whitespaces"; + report_todo: ReportTactic, ReportTactic::Never, false, + "Report all, none or unnumbered occurrences of TODO in source file comments"; + report_fixme: ReportTactic, ReportTactic::Never, false, + "Report all, none or unnumbered occurrences of FIXME in source file comments"; + ignore: IgnoreList, IgnoreList::default(), false, + "Skip formatting the specified files and directories"; + + // Not user-facing + verbose: Verbosity, Verbosity::Normal, false, "How much to information to emit to the user"; + file_lines: FileLines, FileLines::all(), false, + "Lines to format; this is not supported in rustfmt.toml, and can only be specified \ + via the --file-lines option"; + width_heuristics: WidthHeuristics, WidthHeuristics::scaled(100), false, + "'small' heuristic values"; + emit_mode: EmitMode, EmitMode::Files, false, + "What emit Mode to use when none is supplied"; + make_backup: bool, false, false, "Backup changed files"; +} + +/// Load a config by checking the client-supplied options and if appropriate, the +/// file system (including searching the file system for overrides). +pub fn load_config( + file_path: Option<&Path>, + options: Option, +) -> Result<(Config, Option), Error> { + let over_ride = match options { + Some(ref opts) => config_path(opts)?, + None => None, + }; + + let result = if let Some(over_ride) = over_ride { + Config::from_toml_path(over_ride.as_ref()).map(|p| (p, Some(over_ride.to_owned()))) + } else if let Some(file_path) = file_path { + Config::from_resolved_toml_path(file_path) + } else { + Ok((Config::default(), None)) + }; + + result.map(|(mut c, p)| { + if let Some(options) = options { + options.apply_to(&mut c); + } + (c, p) + }) +} + +// Check for the presence of known config file names (`rustfmt.toml, `.rustfmt.toml`) in `dir` +// +// Return the path if a config file exists, empty if no file exists, and Error for IO errors +fn get_toml_path(dir: &Path) -> Result, Error> { + const CONFIG_FILE_NAMES: [&str; 2] = [".rustfmt.toml", "rustfmt.toml"]; + for config_file_name in &CONFIG_FILE_NAMES { + let config_file = dir.join(config_file_name); + match fs::metadata(&config_file) { + // Only return if it's a file to handle the unlikely situation of a directory named + // `rustfmt.toml`. + Ok(ref md) if md.is_file() => return Ok(Some(config_file)), + // Return the error if it's something other than `NotFound`; otherwise we didn't + // find the project file yet, and continue searching. + Err(e) => { + if e.kind() != ErrorKind::NotFound { + return Err(e); + } + } + _ => {} + } + } + Ok(None) +} + +fn config_path(options: &CliOptions) -> Result, Error> { + let config_path_not_found = |path: &str| -> Result, Error> { + Err(Error::new( + ErrorKind::NotFound, + format!( + "Error: unable to find a config file for the given path: `{}`", + path + ), + )) + }; + + // Read the config_path and convert to parent dir if a file is provided. + // If a config file cannot be found from the given path, return error. + match options.config_path() { + Some(path) if !path.exists() => config_path_not_found(path.to_str().unwrap()), + Some(path) if path.is_dir() => { + let config_file_path = get_toml_path(path)?; + if config_file_path.is_some() { + Ok(config_file_path) + } else { + config_path_not_found(path.to_str().unwrap()) + } + } + path => Ok(path.map(|p| p.to_owned())), + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::str; + + #[allow(dead_code)] + mod mock { + use super::super::*; + + create_config! { + // Options that are used by the generated functions + max_width: usize, 100, true, "Maximum width of each line"; + use_small_heuristics: Heuristics, Heuristics::Default, true, + "Whether to use different formatting for items and \ + expressions if they satisfy a heuristic notion of 'small'."; + license_template_path: String, String::default(), false, + "Beginning of file must match license template"; + required_version: String, env!("CARGO_PKG_VERSION").to_owned(), false, + "Require a specific version of rustfmt."; + ignore: IgnoreList, IgnoreList::default(), false, + "Skip formatting the specified files and directories."; + verbose: Verbosity, Verbosity::Normal, false, + "How much to information to emit to the user"; + file_lines: FileLines, FileLines::all(), false, + "Lines to format; this is not supported in rustfmt.toml, and can only be specified \ + via the --file-lines option"; + width_heuristics: WidthHeuristics, WidthHeuristics::scaled(100), false, + "'small' heuristic values"; + + // Options that are used by the tests + stable_option: bool, false, true, "A stable option"; + unstable_option: bool, false, false, "An unstable option"; + } + } + + #[test] + fn test_config_set() { + let mut config = Config::default(); + config.set().verbose(Verbosity::Quiet); + assert_eq!(config.verbose(), Verbosity::Quiet); + config.set().verbose(Verbosity::Normal); + assert_eq!(config.verbose(), Verbosity::Normal); + } + + #[test] + fn test_config_used_to_toml() { + let config = Config::default(); + + let merge_derives = config.merge_derives(); + let skip_children = config.skip_children(); + + let used_options = config.used_options(); + let toml = used_options.to_toml().unwrap(); + assert_eq!( + toml, + format!( + "merge_derives = {}\nskip_children = {}\n", + merge_derives, skip_children, + ) + ); + } + + #[test] + fn test_was_set() { + use std::path::Path; + let config = Config::from_toml("hard_tabs = true", Path::new("")).unwrap(); + + assert_eq!(config.was_set().hard_tabs(), true); + assert_eq!(config.was_set().verbose(), false); + } + + #[test] + fn test_print_docs_exclude_unstable() { + use self::mock::Config; + + let mut output = Vec::new(); + Config::print_docs(&mut output, false); + + let s = str::from_utf8(&output).unwrap(); + + assert_eq!(s.contains("stable_option"), true); + assert_eq!(s.contains("unstable_option"), false); + assert_eq!(s.contains("(unstable)"), false); + } + + #[test] + fn test_print_docs_include_unstable() { + use self::mock::Config; + + let mut output = Vec::new(); + Config::print_docs(&mut output, true); + + let s = str::from_utf8(&output).unwrap(); + assert_eq!(s.contains("stable_option"), true); + assert_eq!(s.contains("unstable_option"), true); + assert_eq!(s.contains("(unstable)"), true); + } + + // FIXME(#2183) these tests cannot be run in parallel because they use env vars + // #[test] + // fn test_as_not_nightly_channel() { + // let mut config = Config::default(); + // assert_eq!(config.was_set().unstable_features(), false); + // config.set().unstable_features(true); + // assert_eq!(config.was_set().unstable_features(), false); + // } + + // #[test] + // fn test_as_nightly_channel() { + // let v = ::std::env::var("CFG_RELEASE_CHANNEL").unwrap_or(String::from("")); + // ::std::env::set_var("CFG_RELEASE_CHANNEL", "nightly"); + // let mut config = Config::default(); + // config.set().unstable_features(true); + // assert_eq!(config.was_set().unstable_features(), false); + // config.set().unstable_features(true); + // assert_eq!(config.unstable_features(), true); + // ::std::env::set_var("CFG_RELEASE_CHANNEL", v); + // } + + // #[test] + // fn test_unstable_from_toml() { + // let mut config = Config::from_toml("unstable_features = true").unwrap(); + // assert_eq!(config.was_set().unstable_features(), false); + // let v = ::std::env::var("CFG_RELEASE_CHANNEL").unwrap_or(String::from("")); + // ::std::env::set_var("CFG_RELEASE_CHANNEL", "nightly"); + // config = Config::from_toml("unstable_features = true").unwrap(); + // assert_eq!(config.was_set().unstable_features(), true); + // assert_eq!(config.unstable_features(), true); + // ::std::env::set_var("CFG_RELEASE_CHANNEL", v); + // } +} diff --git a/config/options.rs b/config/options.rs new file mode 100644 index 00000000000..d2a74f54677 --- /dev/null +++ b/config/options.rs @@ -0,0 +1,482 @@ +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use config::config_type::ConfigType; +use config::lists::*; +use config::{Config, FileName}; + +use isatty::stdout_isatty; + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; + +/// Macro for deriving implementations of Serialize/Deserialize for enums +#[macro_export] +macro_rules! impl_enum_serialize_and_deserialize { + ( $e:ident, $( $x:ident ),* ) => { + impl ::serde::ser::Serialize for $e { + fn serialize(&self, serializer: S) -> Result + where S: ::serde::ser::Serializer + { + use serde::ser::Error; + + // We don't know whether the user of the macro has given us all options. + #[allow(unreachable_patterns)] + match *self { + $( + $e::$x => serializer.serialize_str(stringify!($x)), + )* + _ => { + Err(S::Error::custom(format!("Cannot serialize {:?}", self))) + } + } + } + } + + impl<'de> ::serde::de::Deserialize<'de> for $e { + fn deserialize(d: D) -> Result + where D: ::serde::Deserializer<'de> { + use serde::de::{Error, Visitor}; + use std::marker::PhantomData; + use std::fmt; + struct StringOnly(PhantomData); + impl<'de, T> Visitor<'de> for StringOnly + where T: ::serde::Deserializer<'de> { + type Value = String; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string") + } + fn visit_str(self, value: &str) -> Result { + Ok(String::from(value)) + } + } + let s = d.deserialize_string(StringOnly::(PhantomData))?; + $( + if stringify!($x).eq_ignore_ascii_case(&s) { + return Ok($e::$x); + } + )* + static ALLOWED: &'static[&str] = &[$(stringify!($x),)*]; + Err(D::Error::unknown_variant(&s, ALLOWED)) + } + } + + impl ::std::str::FromStr for $e { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + $( + if stringify!($x).eq_ignore_ascii_case(s) { + return Ok($e::$x); + } + )* + Err("Bad variant") + } + } + + impl ConfigType for $e { + fn doc_hint() -> String { + let mut variants = Vec::new(); + $( + variants.push(stringify!($x)); + )* + format!("[{}]", variants.join("|")) + } + } + }; +} + +macro_rules! configuration_option_enum{ + ($e:ident: $( $x:ident ),+ $(,)*) => { + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + pub enum $e { + $( $x ),+ + } + + impl_enum_serialize_and_deserialize!($e, $( $x ),+); + } +} + +configuration_option_enum! { NewlineStyle: + Auto, // Auto-detect based on the raw source input + Windows, // \r\n + Unix, // \n + Native, // \r\n in Windows, \n on other platforms +} + +impl NewlineStyle { + fn auto_detect(raw_input_text: &str) -> NewlineStyle { + if let Some(pos) = raw_input_text.find('\n') { + let pos = pos.saturating_sub(1); + if let Some('\r') = raw_input_text.chars().nth(pos) { + NewlineStyle::Windows + } else { + NewlineStyle::Unix + } + } else { + NewlineStyle::Native + } + } + + fn native() -> NewlineStyle { + if cfg!(windows) { + NewlineStyle::Windows + } else { + NewlineStyle::Unix + } + } + + /// Apply this newline style to the formatted text. When the style is set + /// to `Auto`, the `raw_input_text` is used to detect the existing line + /// endings. + /// + /// If the style is set to `Auto` and `raw_input_text` contains no + /// newlines, the `Native` style will be used. + pub(crate) fn apply(self, formatted_text: &mut String, raw_input_text: &str) { + use NewlineStyle::*; + let mut style = self; + if style == Auto { + style = Self::auto_detect(raw_input_text); + } + if style == Native { + style = Self::native(); + } + match style { + Windows => { + let mut transformed = String::with_capacity(2 * formatted_text.capacity()); + for c in formatted_text.chars() { + match c { + '\n' => transformed.push_str("\r\n"), + '\r' => continue, + c => transformed.push(c), + } + } + *formatted_text = transformed; + } + Unix => return, + Native => unreachable!("NewlineStyle::Native"), + Auto => unreachable!("NewlineStyle::Auto"), + } + } +} + +configuration_option_enum! { BraceStyle: + AlwaysNextLine, + PreferSameLine, + // Prefer same line except where there is a where clause, in which case force + // the brace to the next line. + SameLineWhere, +} + +configuration_option_enum! { ControlBraceStyle: + // K&R style, Rust community default + AlwaysSameLine, + // Stroustrup style + ClosingNextLine, + // Allman style + AlwaysNextLine, +} + +configuration_option_enum! { IndentStyle: + // First line on the same line as the opening brace, all lines aligned with + // the first line. + Visual, + // First line is on a new line and all lines align with block indent. + Block, +} + +configuration_option_enum! { Density: + // Fit as much on one line as possible. + Compressed, + // Use more lines. + Tall, + // Place every item on a separate line. + Vertical, +} + +configuration_option_enum! { TypeDensity: + // No spaces around "=" and "+" + Compressed, + // Spaces around " = " and " + " + Wide, +} + +configuration_option_enum! { Heuristics: + // Turn off any heuristics + Off, + // Turn on max heuristics + Max, + // Use Rustfmt's defaults + Default, +} + +impl Density { + pub fn to_list_tactic(self) -> ListTactic { + match self { + Density::Compressed => ListTactic::Mixed, + Density::Tall => ListTactic::HorizontalVertical, + Density::Vertical => ListTactic::Vertical, + } + } +} + +configuration_option_enum! { ReportTactic: + Always, + Unnumbered, + Never, +} + +// What Rustfmt should emit. Mostly corresponds to the `--emit` command line +// option. +configuration_option_enum! { EmitMode: + // Emits to files. + Files, + // Writes the output to stdout. + Stdout, + // Displays how much of the input file was processed + Coverage, + // Unfancy stdout + Checkstyle, + // Output the changed lines (for internal value only) + ModifiedLines, + // Checks if a diff can be generated. If so, rustfmt outputs a diff and quits with exit code 1. + // This option is designed to be run in CI where a non-zero exit signifies non-standard code + // formatting. Used for `--check`. + Diff, +} + +// Client-preference for coloured output. +configuration_option_enum! { Color: + // Always use color, whether it is a piped or terminal output + Always, + // Never use color + Never, + // Automatically use color, if supported by terminal + Auto, +} + +impl Color { + /// Whether we should use a coloured terminal. + pub fn use_colored_tty(&self) -> bool { + match self { + Color::Always => true, + Color::Never => false, + Color::Auto => stdout_isatty(), + } + } +} + +// How chatty should Rustfmt be? +configuration_option_enum! { Verbosity: + // Emit more. + Verbose, + Normal, + // Emit as little as possible. + Quiet, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +pub struct WidthHeuristics { + // Maximum width of the args of a function call before falling back + // to vertical formatting. + pub fn_call_width: usize, + // Maximum width in the body of a struct lit before falling back to + // vertical formatting. + pub struct_lit_width: usize, + // Maximum width in the body of a struct variant before falling back + // to vertical formatting. + pub struct_variant_width: usize, + // Maximum width of an array literal before falling back to vertical + // formatting. + pub array_width: usize, + // Maximum length of a chain to fit on a single line. + pub chain_width: usize, + // Maximum line length for single line if-else expressions. A value + // of zero means always break if-else expressions. + pub single_line_if_else_max_width: usize, +} + +impl WidthHeuristics { + // Using this WidthHeuristics means we ignore heuristics. + pub fn null() -> WidthHeuristics { + WidthHeuristics { + fn_call_width: usize::max_value(), + struct_lit_width: 0, + struct_variant_width: 0, + array_width: usize::max_value(), + chain_width: usize::max_value(), + single_line_if_else_max_width: 0, + } + } + + pub fn set(max_width: usize) -> WidthHeuristics { + WidthHeuristics { + fn_call_width: max_width, + struct_lit_width: max_width, + struct_variant_width: max_width, + array_width: max_width, + chain_width: max_width, + single_line_if_else_max_width: max_width, + } + } + + // scale the default WidthHeuristics according to max_width + pub fn scaled(max_width: usize) -> WidthHeuristics { + const DEFAULT_MAX_WIDTH: usize = 100; + let max_width_ratio = if max_width > DEFAULT_MAX_WIDTH { + let ratio = max_width as f32 / DEFAULT_MAX_WIDTH as f32; + // round to the closest 0.1 + (ratio * 10.0).round() / 10.0 + } else { + 1.0 + }; + WidthHeuristics { + fn_call_width: (60.0 * max_width_ratio).round() as usize, + struct_lit_width: (18.0 * max_width_ratio).round() as usize, + struct_variant_width: (35.0 * max_width_ratio).round() as usize, + array_width: (60.0 * max_width_ratio).round() as usize, + chain_width: (60.0 * max_width_ratio).round() as usize, + single_line_if_else_max_width: (50.0 * max_width_ratio).round() as usize, + } + } +} + +impl ::std::str::FromStr for WidthHeuristics { + type Err = &'static str; + + fn from_str(_: &str) -> Result { + Err("WidthHeuristics is not parsable") + } +} + +impl Default for EmitMode { + fn default() -> EmitMode { + EmitMode::Files + } +} + +/// A set of directories, files and modules that rustfmt should ignore. +#[derive(Default, Deserialize, Serialize, Clone, Debug, PartialEq)] +pub struct IgnoreList(HashSet); + +impl IgnoreList { + pub fn add_prefix(&mut self, dir: &Path) { + self.0 = self + .0 + .iter() + .map(|s| { + if s.has_root() { + s.clone() + } else { + let mut path = PathBuf::from(dir); + path.push(s); + path + } + }).collect(); + } + + fn skip_file_inner(&self, file: &Path) -> bool { + self.0.iter().any(|path| file.starts_with(path)) + } + + pub fn skip_file(&self, file: &FileName) -> bool { + if let FileName::Real(ref path) = file { + self.skip_file_inner(path) + } else { + false + } + } +} + +impl ::std::str::FromStr for IgnoreList { + type Err = &'static str; + + fn from_str(_: &str) -> Result { + Err("IgnoreList is not parsable") + } +} + +/// Maps client-supplied options to Rustfmt's internals, mostly overriding +/// values in a config with values from the command line. +pub trait CliOptions { + fn apply_to(self, config: &mut Config); + fn config_path(&self) -> Option<&Path>; +} + +/// The edition of the compiler (RFC 2052) +configuration_option_enum!{ Edition: + Edition2015, + Edition2018, +} + +impl Edition { + pub(crate) fn to_libsyntax_pos_edition(&self) -> syntax_pos::edition::Edition { + match self { + Edition::Edition2015 => syntax_pos::edition::Edition::Edition2015, + Edition::Edition2018 => syntax_pos::edition::Edition::Edition2018, + } + } +} + +#[test] +fn test_newline_style_auto_detect() { + let lf = "One\nTwo\nThree"; + let crlf = "One\r\nTwo\r\nThree"; + let none = "One Two Three"; + + assert_eq!(NewlineStyle::Unix, NewlineStyle::auto_detect(lf)); + assert_eq!(NewlineStyle::Windows, NewlineStyle::auto_detect(crlf)); + assert_eq!(NewlineStyle::Native, NewlineStyle::auto_detect(none)); +} + +#[test] +fn test_newline_style_auto_apply() { + let auto = NewlineStyle::Auto; + + let formatted_text = "One\nTwo\nThree"; + let raw_input_text = "One\nTwo\nThree"; + + let mut out = String::from(formatted_text); + auto.apply(&mut out, raw_input_text); + assert_eq!("One\nTwo\nThree", &out, "auto should detect 'lf'"); + + let formatted_text = "One\nTwo\nThree"; + let raw_input_text = "One\r\nTwo\r\nThree"; + + let mut out = String::from(formatted_text); + auto.apply(&mut out, raw_input_text); + assert_eq!("One\r\nTwo\r\nThree", &out, "auto should detect 'crlf'"); + + #[cfg(not(windows))] + { + let formatted_text = "One\nTwo\nThree"; + let raw_input_text = "One Two Three"; + + let mut out = String::from(formatted_text); + auto.apply(&mut out, raw_input_text); + assert_eq!( + "One\nTwo\nThree", &out, + "auto-native-unix should detect 'lf'" + ); + } + + #[cfg(windows)] + { + let formatted_text = "One\nTwo\nThree"; + let raw_input_text = "One Two Three"; + + let mut out = String::from(formatted_text); + auto.apply(&mut out, raw_input_text); + assert_eq!( + "One\r\nTwo\r\nThree", &out, + "auto-native-windows should detect 'crlf'" + ); + } +} diff --git a/format-diff/main.rs b/format-diff/main.rs new file mode 100644 index 00000000000..37ad4f35a1d --- /dev/null +++ b/format-diff/main.rs @@ -0,0 +1,251 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// Inspired by Clang's clang-format-diff: +// +// https://github.com/llvm-mirror/clang/blob/master/tools/clang-format/clang-format-diff.py + +#![deny(warnings)] + +extern crate env_logger; +#[macro_use] +extern crate failure; +extern crate getopts; +#[macro_use] +extern crate log; +extern crate regex; +#[macro_use] +extern crate serde_derive; +extern crate serde_json as json; + +use std::collections::HashSet; +use std::io::{self, BufRead}; +use std::{env, process}; + +use regex::Regex; + +/// The default pattern of files to format. +/// +/// We only want to format rust files by default. +const DEFAULT_PATTERN: &str = r".*\.rs"; + +#[derive(Fail, Debug)] +enum FormatDiffError { + #[fail(display = "{}", _0)] + IncorrectOptions(#[cause] getopts::Fail), + #[fail(display = "{}", _0)] + IncorrectFilter(#[cause] regex::Error), + #[fail(display = "{}", _0)] + IoError(#[cause] io::Error), +} + +impl From for FormatDiffError { + fn from(fail: getopts::Fail) -> Self { + FormatDiffError::IncorrectOptions(fail) + } +} + +impl From for FormatDiffError { + fn from(err: regex::Error) -> Self { + FormatDiffError::IncorrectFilter(err) + } +} + +impl From for FormatDiffError { + fn from(fail: io::Error) -> Self { + FormatDiffError::IoError(fail) + } +} + +fn main() { + env_logger::init(); + + let mut opts = getopts::Options::new(); + opts.optflag("h", "help", "show this message"); + opts.optopt( + "p", + "skip-prefix", + "skip the smallest prefix containing NUMBER slashes", + "NUMBER", + ); + opts.optopt( + "f", + "filter", + "custom pattern selecting file paths to reformat", + "PATTERN", + ); + + if let Err(e) = run(&opts) { + println!("{}", opts.usage(&format!("{}", e))); + process::exit(1); + } +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +struct Range { + file: String, + range: [u32; 2], +} + +fn run(opts: &getopts::Options) -> Result<(), FormatDiffError> { + let matches = opts.parse(env::args().skip(1))?; + + if matches.opt_present("h") { + println!("{}", opts.usage("usage: ")); + return Ok(()); + } + + let filter = matches + .opt_str("f") + .unwrap_or_else(|| DEFAULT_PATTERN.to_owned()); + + let skip_prefix = matches + .opt_str("p") + .and_then(|p| p.parse::().ok()) + .unwrap_or(0); + + let (files, ranges) = scan_diff(io::stdin(), skip_prefix, &filter)?; + + run_rustfmt(&files, &ranges) +} + +fn run_rustfmt(files: &HashSet, ranges: &[Range]) -> Result<(), FormatDiffError> { + if files.is_empty() || ranges.is_empty() { + debug!("No files to format found"); + return Ok(()); + } + + let ranges_as_json = json::to_string(ranges).unwrap(); + + debug!("Files: {:?}", files); + debug!("Ranges: {:?}", ranges); + + let exit_status = process::Command::new("rustfmt") + .args(files) + .arg("--file-lines") + .arg(ranges_as_json) + .status()?; + + if !exit_status.success() { + return Err(FormatDiffError::IoError(io::Error::new( + io::ErrorKind::Other, + format!("rustfmt failed with {}", exit_status), + ))); + } + Ok(()) +} + +/// Scans a diff from `from`, and returns the set of files found, and the ranges +/// in those files. +fn scan_diff( + from: R, + skip_prefix: u32, + file_filter: &str, +) -> Result<(HashSet, Vec), FormatDiffError> +where + R: io::Read, +{ + let diff_pattern = format!(r"^\+\+\+\s(?:.*?/){{{}}}(\S*)", skip_prefix); + let diff_pattern = Regex::new(&diff_pattern).unwrap(); + + let lines_pattern = Regex::new(r"^@@.*\+(\d+)(,(\d+))?").unwrap(); + + let file_filter = Regex::new(&format!("^{}$", file_filter))?; + + let mut current_file = None; + + let mut files = HashSet::new(); + let mut ranges = vec![]; + for line in io::BufReader::new(from).lines() { + let line = line.unwrap(); + + if let Some(captures) = diff_pattern.captures(&line) { + current_file = Some(captures.get(1).unwrap().as_str().to_owned()); + } + + let file = match current_file { + Some(ref f) => &**f, + None => continue, + }; + + // FIXME(emilio): We could avoid this most of the time if needed, but + // it's not clear it's worth it. + if !file_filter.is_match(file) { + continue; + } + + let lines_captures = match lines_pattern.captures(&line) { + Some(captures) => captures, + None => continue, + }; + + let start_line = lines_captures + .get(1) + .unwrap() + .as_str() + .parse::() + .unwrap(); + let line_count = match lines_captures.get(3) { + Some(line_count) => line_count.as_str().parse::().unwrap(), + None => 1, + }; + + if line_count == 0 { + continue; + } + + let end_line = start_line + line_count - 1; + files.insert(file.to_owned()); + ranges.push(Range { + file: file.to_owned(), + range: [start_line, end_line], + }); + } + + Ok((files, ranges)) +} + +#[test] +fn scan_simple_git_diff() { + const DIFF: &'static str = include_str!("test/bindgen.diff"); + let (files, ranges) = scan_diff(DIFF.as_bytes(), 1, r".*\.rs").expect("scan_diff failed?"); + + assert!( + files.contains("src/ir/traversal.rs"), + "Should've matched the filter" + ); + + assert!( + !files.contains("tests/headers/anon_enum.hpp"), + "Shouldn't have matched the filter" + ); + + assert_eq!( + &ranges, + &[ + Range { + file: "src/ir/item.rs".to_owned(), + range: [148, 158], + }, + Range { + file: "src/ir/item.rs".to_owned(), + range: [160, 170], + }, + Range { + file: "src/ir/traversal.rs".to_owned(), + range: [9, 16], + }, + Range { + file: "src/ir/traversal.rs".to_owned(), + range: [35, 43], + }, + ] + ); +} diff --git a/format-diff/test/bindgen.diff b/format-diff/test/bindgen.diff new file mode 100644 index 00000000000..d2fd379f471 --- /dev/null +++ b/format-diff/test/bindgen.diff @@ -0,0 +1,67 @@ +diff --git a/src/ir/item.rs b/src/ir/item.rs +index 7f3afefb..90d15e96 100644 +--- a/src/ir/item.rs ++++ b/src/ir/item.rs +@@ -148,7 +148,11 @@ impl<'a, 'b> Iterator for ItemAncestorsIter<'a, 'b> + impl AsTemplateParam for ItemId { + type Extra = (); + +- fn as_template_param(&self, ctx: &BindgenContext, _: &()) -> Option { ++ fn as_template_param( ++ &self, ++ ctx: &BindgenContext, ++ _: &(), ++ ) -> Option { + ctx.resolve_item(*self).as_template_param(ctx, &()) + } + } +@@ -156,7 +160,11 @@ impl AsTemplateParam for ItemId { + impl AsTemplateParam for Item { + type Extra = (); + +- fn as_template_param(&self, ctx: &BindgenContext, _: &()) -> Option { ++ fn as_template_param( ++ &self, ++ ctx: &BindgenContext, ++ _: &(), ++ ) -> Option { + self.kind.as_template_param(ctx, self) + } + } +diff --git a/src/ir/traversal.rs b/src/ir/traversal.rs +index 762a3e2d..b9c9dd4e 100644 +--- a/src/ir/traversal.rs ++++ b/src/ir/traversal.rs +@@ -9,6 +9,8 @@ use std::collections::{BTreeMap, VecDeque}; + /// + /// from --> to + /// ++/// Random content to generate a diff. ++/// + /// The `from` is left implicit: it is the concrete `Trace` implementer which + /// yielded this outgoing edge. + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +@@ -33,7 +35,9 @@ impl Into for Edge { + } + } + +-/// The kind of edge reference. This is useful when we wish to only consider ++/// The kind of edge reference. ++/// ++/// This is useful when we wish to only consider + /// certain kinds of edges for a particular traversal or analysis. + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum EdgeKind { +diff --git a/tests/headers/anon_enum.hpp b/tests/headers/anon_enum.hpp +index 1961fe6c..34759df3 100644 +--- a/tests/headers/anon_enum.hpp ++++ b/tests/headers/anon_enum.hpp +@@ -1,7 +1,7 @@ + struct Test { + int foo; + float bar; +- enum { T_NONE }; ++ enum { T_NONE, T_SOME }; + }; + + typedef enum { diff --git a/git-rustfmt/main.rs b/git-rustfmt/main.rs new file mode 100644 index 00000000000..0042ff353a7 --- /dev/null +++ b/git-rustfmt/main.rs @@ -0,0 +1,206 @@ +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +extern crate env_logger; +extern crate getopts; +#[macro_use] +extern crate log; +extern crate rustfmt_nightly as rustfmt; + +use std::env; +use std::io::stdout; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str::FromStr; + +use getopts::{Matches, Options}; + +use rustfmt::{load_config, CliOptions, Input, Session}; + +fn prune_files(files: Vec<&str>) -> Vec<&str> { + let prefixes: Vec<_> = files + .iter() + .filter(|f| f.ends_with("mod.rs") || f.ends_with("lib.rs")) + .map(|f| &f[..f.len() - 6]) + .collect(); + + let mut pruned_prefixes = vec![]; + for p1 in prefixes { + if p1.starts_with("src/bin/") || pruned_prefixes.iter().all(|p2| !p1.starts_with(p2)) { + pruned_prefixes.push(p1); + } + } + debug!("prefixes: {:?}", pruned_prefixes); + + files + .into_iter() + .filter(|f| { + if f.ends_with("mod.rs") || f.ends_with("lib.rs") || f.starts_with("src/bin/") { + return true; + } + pruned_prefixes.iter().all(|pp| !f.starts_with(pp)) + }).collect() +} + +fn git_diff(commits: &str) -> String { + let mut cmd = Command::new("git"); + cmd.arg("diff"); + if commits != "0" { + cmd.arg(format!("HEAD~{}", commits)); + } + let output = cmd.output().expect("Couldn't execute `git diff`"); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +fn get_files(input: &str) -> Vec<&str> { + input + .lines() + .filter(|line| line.starts_with("+++ b/") && line.ends_with(".rs")) + .map(|line| &line[6..]) + .collect() +} + +fn fmt_files(files: &[&str]) -> i32 { + let (config, _) = + load_config::(Some(Path::new(".")), None).expect("couldn't load config"); + + let mut exit_code = 0; + let mut out = stdout(); + let mut session = Session::new(config, Some(&mut out)); + for file in files { + let report = session.format(Input::File(PathBuf::from(file))).unwrap(); + if report.has_warnings() { + eprintln!("{}", report); + } + if !session.has_no_errors() { + exit_code = 1; + } + } + exit_code +} + +struct NullOptions; + +impl CliOptions for NullOptions { + fn apply_to(self, _: &mut rustfmt::Config) { + unreachable!(); + } + fn config_path(&self) -> Option<&Path> { + unreachable!(); + } +} + +fn uncommitted_files() -> Vec { + let mut cmd = Command::new("git"); + cmd.arg("ls-files"); + cmd.arg("--others"); + cmd.arg("--modified"); + cmd.arg("--exclude-standard"); + let output = cmd.output().expect("Couldn't execute Git"); + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .lines() + .filter(|s| s.ends_with(".rs")) + .map(|s| s.to_owned()) + .collect() +} + +fn check_uncommitted() { + let uncommitted = uncommitted_files(); + debug!("uncommitted files: {:?}", uncommitted); + if !uncommitted.is_empty() { + println!("Found untracked changes:"); + for f in &uncommitted { + println!(" {}", f); + } + println!("Commit your work, or run with `-u`."); + println!("Exiting."); + std::process::exit(1); + } +} + +fn make_opts() -> Options { + let mut opts = Options::new(); + opts.optflag("h", "help", "show this message"); + opts.optflag("c", "check", "check only, don't format (unimplemented)"); + opts.optflag("u", "uncommitted", "format uncommitted files"); + opts +} + +struct Config { + commits: String, + uncommitted: bool, + check: bool, +} + +impl Config { + fn from_args(matches: &Matches, opts: &Options) -> Config { + // `--help` display help message and quit + if matches.opt_present("h") { + let message = format!( + "\nusage: {} [options]\n\n\ + commits: number of commits to format, default: 1", + env::args_os().next().unwrap().to_string_lossy() + ); + println!("{}", opts.usage(&message)); + std::process::exit(0); + } + + let mut config = Config { + commits: "1".to_owned(), + uncommitted: false, + check: false, + }; + + if matches.opt_present("c") { + config.check = true; + unimplemented!(); + } + + if matches.opt_present("u") { + config.uncommitted = true; + } + + if matches.free.len() > 1 { + panic!("unknown arguments, use `-h` for usage"); + } + if matches.free.len() == 1 { + let commits = matches.free[0].trim(); + if u32::from_str(commits).is_err() { + panic!("Couldn't parse number of commits"); + } + config.commits = commits.to_owned(); + } + + config + } +} + +fn main() { + env_logger::init(); + + let opts = make_opts(); + let matches = opts + .parse(env::args().skip(1)) + .expect("Couldn't parse command line"); + let config = Config::from_args(&matches, &opts); + + if !config.uncommitted { + check_uncommitted(); + } + + let stdout = git_diff(&config.commits); + let files = get_files(&stdout); + debug!("files: {:?}", files); + let files = prune_files(files); + debug!("pruned files: {:?}", files); + let exit_code = fmt_files(&files); + std::process::exit(exit_code); +} diff --git a/src/patterns.rs b/src/patterns.rs index 90725972bd5..d5c4802904b 100644 --- a/src/patterns.rs +++ b/src/patterns.rs @@ -336,29 +336,28 @@ fn rewrite_tuple_pat( )); pat_vec.insert(pos, dotdot); } - if pat_vec.is_empty() { return Some(format!("{}()", path_str.unwrap_or_default())); } - let wildcard_suffix_len = count_wildcard_suffix_len(context, &pat_vec, span, shape); - let (pat_vec, span) = if context.config.condense_wildcard_suffixes() && wildcard_suffix_len >= 2 - { - let new_item_count = 1 + pat_vec.len() - wildcard_suffix_len; - let sp = pat_vec[new_item_count - 1].span(); - let snippet = context.snippet(sp); - let lo = sp.lo() + BytePos(snippet.find_uncommented("_").unwrap() as u32); - pat_vec[new_item_count - 1] = TuplePatField::Dotdot(mk_sp(lo, lo + BytePos(1))); - ( - &pat_vec[..new_item_count], - mk_sp(span.lo(), lo + BytePos(1)), - ) - } else { - (&pat_vec[..], span) - }; + let (pat_vec, span, condensed) = + if context.config.condense_wildcard_suffixes() && wildcard_suffix_len >= 2 { + let new_item_count = 1 + pat_vec.len() - wildcard_suffix_len; + let sp = pat_vec[new_item_count - 1].span(); + let snippet = context.snippet(sp); + let lo = sp.lo() + BytePos(snippet.find_uncommented("_").unwrap() as u32); + pat_vec[new_item_count - 1] = TuplePatField::Dotdot(mk_sp(lo, lo + BytePos(1))); + ( + &pat_vec[..new_item_count], + mk_sp(span.lo(), lo + BytePos(1)), + true, + ) + } else { + (&pat_vec[..], span, false) + }; // add comma if `(x,)` - let add_comma = path_str.is_none() && pat_vec.len() == 1 && dotdot_pos.is_none(); + let add_comma = path_str.is_none() && pat_vec.len() == 1 && dotdot_pos.is_none() && !condensed; let path_str = path_str.unwrap_or_default(); let pat_ref_vec = pat_vec.iter().collect::>(); diff --git a/test/mod.rs b/test/mod.rs new file mode 100644 index 00000000000..8b2358f1ada --- /dev/null +++ b/test/mod.rs @@ -0,0 +1,964 @@ +// Copyright 2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +extern crate assert_cli; + +use std::collections::{HashMap, HashSet}; +use std::env; +use std::fs; +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::iter::{Enumerate, Peekable}; +use std::mem; +use std::path::{Path, PathBuf}; +use std::str::Chars; + +use config::{Color, Config, EmitMode, FileName, ReportTactic}; +use formatting::{ModifiedChunk, SourceFile}; +use rustfmt_diff::{make_diff, print_diff, DiffLine, Mismatch, OutputWriter}; +use source_file; +use {FormatReport, Input, Session}; + +const DIFF_CONTEXT_SIZE: usize = 3; +const CONFIGURATIONS_FILE_NAME: &str = "Configurations.md"; + +// Returns a `Vec` containing `PathBuf`s of files with a rs extension in the +// given path. The `recursive` argument controls if files from subdirectories +// are also returned. +fn get_test_files(path: &Path, recursive: bool) -> Vec { + let mut files = vec![]; + if path.is_dir() { + for entry in fs::read_dir(path).expect(&format!( + "Couldn't read directory {}", + path.to_str().unwrap() + )) { + let entry = entry.expect("Couldn't get DirEntry"); + let path = entry.path(); + if path.is_dir() && recursive { + files.append(&mut get_test_files(&path, recursive)); + } else if path.extension().map_or(false, |f| f == "rs") { + files.push(path); + } + } + } + files +} + +fn verify_config_used(path: &Path, config_name: &str) { + for entry in fs::read_dir(path).expect(&format!( + "Couldn't read {} directory", + path.to_str().unwrap() + )) { + let entry = entry.expect("Couldn't get directory entry"); + let path = entry.path(); + if path.extension().map_or(false, |f| f == "rs") { + // check if "// rustfmt-:" appears in the file. + let filebuf = BufReader::new( + fs::File::open(&path).expect(&format!("Couldn't read file {}", path.display())), + ); + assert!( + filebuf + .lines() + .map(|l| l.unwrap()) + .take_while(|l| l.starts_with("//")) + .any(|l| l.starts_with(&format!("// rustfmt-{}", config_name))), + format!( + "config option file {} does not contain expected config name", + path.display() + ) + ); + } + } +} + +#[test] +fn verify_config_test_names() { + for path in &[ + Path::new("tests/source/configs"), + Path::new("tests/target/configs"), + ] { + for entry in fs::read_dir(path).expect("Couldn't read configs directory") { + let entry = entry.expect("Couldn't get directory entry"); + let path = entry.path(); + if path.is_dir() { + let config_name = path.file_name().unwrap().to_str().unwrap(); + + // Make sure that config name is used in the files in the directory. + verify_config_used(&path, config_name); + } + } + } +} + +// This writes to the terminal using the same approach (via term::stdout or +// println!) that is used by `rustfmt::rustfmt_diff::print_diff`. Writing +// using only one or the other will cause the output order to differ when +// `print_diff` selects the approach not used. +fn write_message(msg: &str) { + let mut writer = OutputWriter::new(Color::Auto); + writer.writeln(&format!("{}", msg), None); +} + +// Integration tests. The files in the tests/source are formatted and compared +// to their equivalent in tests/target. The target file and config can be +// overridden by annotations in the source file. The input and output must match +// exactly. +#[test] +fn system_tests() { + // Get all files in the tests/source directory. + let files = get_test_files(Path::new("tests/source"), true); + let (_reports, count, fails) = check_files(files, None); + + // Display results. + println!("Ran {} system tests.", count); + assert_eq!(fails, 0, "{} system tests failed", fails); +} + +// Do the same for tests/coverage-source directory +// the only difference is the coverage mode +#[test] +fn coverage_tests() { + let files = get_test_files(Path::new("tests/coverage/source"), true); + let (_reports, count, fails) = check_files(files, None); + + println!("Ran {} tests in coverage mode.", count); + assert_eq!(fails, 0, "{} tests failed", fails); +} + +#[test] +fn checkstyle_test() { + let filename = "tests/writemode/source/fn-single-line.rs"; + let expected_filename = "tests/writemode/target/checkstyle.xml"; + assert_output(Path::new(filename), Path::new(expected_filename)); +} + +#[test] +fn modified_test() { + use std::io::BufRead; + + // Test "modified" output + let filename = "tests/writemode/source/modified.rs"; + let mut data = Vec::new(); + let mut config = Config::default(); + config.set().emit_mode(::config::EmitMode::ModifiedLines); + + { + let mut session = Session::new(config, Some(&mut data)); + session.format(Input::File(filename.into())).unwrap(); + } + + let mut lines = data.lines(); + let mut chunks = Vec::new(); + while let Some(Ok(header)) = lines.next() { + // Parse the header line + let values: Vec<_> = header + .split(' ') + .map(|s| s.parse::().unwrap()) + .collect(); + assert_eq!(values.len(), 3); + let line_number_orig = values[0]; + let lines_removed = values[1]; + let num_added = values[2]; + let mut added_lines = Vec::new(); + for _ in 0..num_added { + added_lines.push(lines.next().unwrap().unwrap()); + } + chunks.push(ModifiedChunk { + line_number_orig, + lines_removed, + lines: added_lines, + }); + } + + assert_eq!( + chunks, + vec![ + ModifiedChunk { + line_number_orig: 4, + lines_removed: 4, + lines: vec!["fn blah() {}".into()], + }, + ModifiedChunk { + line_number_orig: 9, + lines_removed: 6, + lines: vec!["#[cfg(a, b)]".into(), "fn main() {}".into()], + }, + ], + ); +} + +// Helper function for comparing the results of rustfmt +// to a known output file generated by one of the write modes. +fn assert_output(source: &Path, expected_filename: &Path) { + let config = read_config(source); + let (_, source_file, _) = format_file(source, config.clone()); + + // Populate output by writing to a vec. + let mut out = vec![]; + let _ = source_file::write_all_files(&source_file, &mut out, &config); + let output = String::from_utf8(out).unwrap(); + + let mut expected_file = fs::File::open(&expected_filename).expect("Couldn't open target"); + let mut expected_text = String::new(); + expected_file + .read_to_string(&mut expected_text) + .expect("Failed reading target"); + + let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE); + if !compare.is_empty() { + let mut failures = HashMap::new(); + failures.insert(source.to_owned(), compare); + print_mismatches_default_message(failures); + assert!(false, "Text does not match expected output"); + } +} + +// Idempotence tests. Files in tests/target are checked to be unaltered by +// rustfmt. +#[test] +fn idempotence_tests() { + match option_env!("CFG_RELEASE_CHANNEL") { + None | Some("nightly") => {} + _ => return, // these tests require nightly + } + // Get all files in the tests/target directory. + let files = get_test_files(Path::new("tests/target"), true); + let (_reports, count, fails) = check_files(files, None); + + // Display results. + println!("Ran {} idempotent tests.", count); + assert_eq!(fails, 0, "{} idempotent tests failed", fails); +} + +// Run rustfmt on itself. This operation must be idempotent. We also check that +// no warnings are emitted. +#[test] +fn self_tests() { + let mut files = get_test_files(Path::new("tests"), false); + let bin_directories = vec!["cargo-fmt", "git-rustfmt", "bin", "format-diff"]; + for dir in bin_directories { + let mut path = PathBuf::from("src"); + path.push(dir); + path.push("main.rs"); + files.push(path); + } + files.push(PathBuf::from("src/lib.rs")); + + let (reports, count, fails) = check_files(files, Some(PathBuf::from("rustfmt.toml"))); + let mut warnings = 0; + + // Display results. + println!("Ran {} self tests.", count); + assert_eq!(fails, 0, "{} self tests failed", fails); + + for format_report in reports { + println!("{}", format_report); + warnings += format_report.warning_count(); + } + + assert_eq!( + warnings, 0, + "Rustfmt's code generated {} warnings", + warnings + ); +} + +#[test] +fn stdin_formatting_smoke_test() { + let input = Input::Text("fn main () {}".to_owned()); + let mut config = Config::default(); + config.set().emit_mode(EmitMode::Stdout); + let mut buf: Vec = vec![]; + { + let mut session = Session::new(config, Some(&mut buf)); + session.format(input).unwrap(); + assert!(session.has_no_errors()); + } + //eprintln!("{:?}", ); + #[cfg(not(windows))] + assert_eq!(buf, "fn main() {}\n".as_bytes()); + #[cfg(windows)] + assert_eq!(buf, "fn main() {}\r\n".as_bytes()); +} + +// FIXME(#1990) restore this test +// #[test] +// fn stdin_disable_all_formatting_test() { +// let input = String::from("fn main() { println!(\"This should not be formatted.\"); }"); +// let mut child = Command::new("./target/debug/rustfmt") +// .stdin(Stdio::piped()) +// .stdout(Stdio::piped()) +// .arg("--config-path=./tests/config/disable_all_formatting.toml") +// .spawn() +// .expect("failed to execute child"); + +// { +// let stdin = child.stdin.as_mut().expect("failed to get stdin"); +// stdin +// .write_all(input.as_bytes()) +// .expect("failed to write stdin"); +// } +// let output = child.wait_with_output().expect("failed to wait on child"); +// assert!(output.status.success()); +// assert!(output.stderr.is_empty()); +// assert_eq!(input, String::from_utf8(output.stdout).unwrap()); +// } + +#[test] +fn format_lines_errors_are_reported() { + let long_identifier = String::from_utf8(vec![b'a'; 239]).unwrap(); + let input = Input::Text(format!("fn {}() {{}}", long_identifier)); + let mut config = Config::default(); + config.set().error_on_line_overflow(true); + let mut session = Session::::new(config, None); + session.format(input).unwrap(); + assert!(session.has_formatting_errors()); +} + +#[test] +fn format_lines_errors_are_reported_with_tabs() { + let long_identifier = String::from_utf8(vec![b'a'; 97]).unwrap(); + let input = Input::Text(format!("fn a() {{\n\t{}\n}}", long_identifier)); + let mut config = Config::default(); + config.set().error_on_line_overflow(true); + config.set().hard_tabs(true); + let mut session = Session::::new(config, None); + session.format(input).unwrap(); + assert!(session.has_formatting_errors()); +} + +// For each file, run rustfmt and collect the output. +// Returns the number of files checked and the number of failures. +fn check_files(files: Vec, opt_config: Option) -> (Vec, u32, u32) { + let mut count = 0; + let mut fails = 0; + let mut reports = vec![]; + + for file_name in files { + debug!("Testing '{}'...", file_name.display()); + + match idempotent_check(&file_name, &opt_config) { + Ok(ref report) if report.has_warnings() => { + print!("{}", report); + fails += 1; + } + Ok(report) => reports.push(report), + Err(err) => { + if let IdempotentCheckError::Mismatch(msg) = err { + print_mismatches_default_message(msg); + } + fails += 1; + } + } + + count += 1; + } + + (reports, count, fails) +} + +fn print_mismatches_default_message(result: HashMap>) { + for (file_name, diff) in result { + let mismatch_msg_formatter = + |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num); + print_diff(diff, &mismatch_msg_formatter, &Default::default()); + } + + if let Some(mut t) = term::stdout() { + t.reset().unwrap_or(()); + } +} + +fn print_mismatches String>( + result: HashMap>, + mismatch_msg_formatter: T, +) { + for (_file_name, diff) in result { + print_diff(diff, &mismatch_msg_formatter, &Default::default()); + } + + if let Some(mut t) = term::stdout() { + t.reset().unwrap_or(()); + } +} + +fn read_config(filename: &Path) -> Config { + let sig_comments = read_significant_comments(filename); + // Look for a config file... If there is a 'config' property in the significant comments, use + // that. Otherwise, if there are no significant comments at all, look for a config file with + // the same name as the test file. + let mut config = if !sig_comments.is_empty() { + get_config(sig_comments.get("config").map(Path::new)) + } else { + get_config(filename.with_extension("toml").file_name().map(Path::new)) + }; + + for (key, val) in &sig_comments { + if key != "target" && key != "config" { + config.override_value(key, val); + if config.is_default(key) { + warn!("Default value {} used explicitly for {}", val, key); + } + } + } + + // Don't generate warnings for to-do items. + config.set().report_todo(ReportTactic::Never); + + config +} + +fn format_file>(filepath: P, config: Config) -> (bool, SourceFile, FormatReport) { + let filepath = filepath.into(); + let input = Input::File(filepath); + let mut session = Session::::new(config, None); + let result = session.format(input).unwrap(); + let parsing_errors = session.has_parsing_errors(); + let mut source_file = SourceFile::new(); + mem::swap(&mut session.source_file, &mut source_file); + (parsing_errors, source_file, result) +} + +enum IdempotentCheckError { + Mismatch(HashMap>), + Parse, +} + +fn idempotent_check( + filename: &PathBuf, + opt_config: &Option, +) -> Result { + let sig_comments = read_significant_comments(filename); + let config = if let Some(ref config_file_path) = opt_config { + Config::from_toml_path(config_file_path).expect("rustfmt.toml not found") + } else { + read_config(filename) + }; + let (parsing_errors, source_file, format_report) = format_file(filename, config); + if parsing_errors { + return Err(IdempotentCheckError::Parse); + } + + let mut write_result = HashMap::new(); + for (filename, text) in source_file { + if let FileName::Real(ref filename) = filename { + write_result.insert(filename.to_owned(), text); + } + } + + let target = sig_comments.get("target").map(|x| &(*x)[..]); + + handle_result(write_result, target).map(|_| format_report) +} + +// Reads test config file using the supplied (optional) file name. If there's no file name or the +// file doesn't exist, just return the default config. Otherwise, the file must be read +// successfully. +fn get_config(config_file: Option<&Path>) -> Config { + let config_file_name = match config_file { + None => return Default::default(), + Some(file_name) => { + let mut full_path = PathBuf::from("tests/config/"); + full_path.push(file_name); + if !full_path.exists() { + return Default::default(); + }; + full_path + } + }; + + let mut def_config_file = fs::File::open(config_file_name).expect("Couldn't open config"); + let mut def_config = String::new(); + def_config_file + .read_to_string(&mut def_config) + .expect("Couldn't read config"); + + Config::from_toml(&def_config, Path::new("tests/config/")).expect("Invalid toml") +} + +// Reads significant comments of the form: // rustfmt-key: value +// into a hash map. +fn read_significant_comments(file_name: &Path) -> HashMap { + let file = + fs::File::open(file_name).expect(&format!("Couldn't read file {}", file_name.display())); + let reader = BufReader::new(file); + let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)"; + let regex = regex::Regex::new(pattern).expect("Failed creating pattern 1"); + + // Matches lines containing significant comments or whitespace. + let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)") + .expect("Failed creating pattern 2"); + + reader + .lines() + .map(|line| line.expect("Failed getting line")) + .take_while(|line| line_regex.is_match(line)) + .filter_map(|line| { + regex.captures_iter(&line).next().map(|capture| { + ( + capture + .get(1) + .expect("Couldn't unwrap capture") + .as_str() + .to_owned(), + capture + .get(2) + .expect("Couldn't unwrap capture") + .as_str() + .to_owned(), + ) + }) + }).collect() +} + +// Compare output to input. +// TODO: needs a better name, more explanation. +fn handle_result( + result: HashMap, + target: Option<&str>, +) -> Result<(), IdempotentCheckError> { + let mut failures = HashMap::new(); + + for (file_name, fmt_text) in result { + // If file is in tests/source, compare to file with same name in tests/target. + let target = get_target(&file_name, target); + let open_error = format!("Couldn't open target {:?}", &target); + let mut f = fs::File::open(&target).expect(&open_error); + + let mut text = String::new(); + let read_error = format!("Failed reading target {:?}", &target); + f.read_to_string(&mut text).expect(&read_error); + + // Ignore LF and CRLF difference for Windows. + if !string_eq_ignore_newline_repr(&fmt_text, &text) { + let diff = make_diff(&text, &fmt_text, DIFF_CONTEXT_SIZE); + assert!( + !diff.is_empty(), + "Empty diff? Maybe due to a missing a newline at the end of a file?" + ); + failures.insert(file_name, diff); + } + } + + if failures.is_empty() { + Ok(()) + } else { + Err(IdempotentCheckError::Mismatch(failures)) + } +} + +// Map source file paths to their target paths. +fn get_target(file_name: &Path, target: Option<&str>) -> PathBuf { + if let Some(n) = file_name + .components() + .position(|c| c.as_os_str() == "source") + { + let mut target_file_name = PathBuf::new(); + for (i, c) in file_name.components().enumerate() { + if i == n { + target_file_name.push("target"); + } else { + target_file_name.push(c.as_os_str()); + } + } + if let Some(replace_name) = target { + target_file_name.with_file_name(replace_name) + } else { + target_file_name + } + } else { + // This is either and idempotence check or a self check + file_name.to_owned() + } +} + +#[test] +fn rustfmt_diff_make_diff_tests() { + let diff = make_diff("a\nb\nc\nd", "a\ne\nc\nd", 3); + assert_eq!( + diff, + vec![Mismatch { + line_number: 1, + line_number_orig: 1, + lines: vec![ + DiffLine::Context("a".into()), + DiffLine::Resulting("b".into()), + DiffLine::Expected("e".into()), + DiffLine::Context("c".into()), + DiffLine::Context("d".into()), + ], + }] + ); +} + +#[test] +fn rustfmt_diff_no_diff_test() { + let diff = make_diff("a\nb\nc\nd", "a\nb\nc\nd", 3); + assert_eq!(diff, vec![]); +} + +// Compare strings without distinguishing between CRLF and LF +fn string_eq_ignore_newline_repr(left: &str, right: &str) -> bool { + let left = CharsIgnoreNewlineRepr(left.chars().peekable()); + let right = CharsIgnoreNewlineRepr(right.chars().peekable()); + left.eq(right) +} + +struct CharsIgnoreNewlineRepr<'a>(Peekable>); + +impl<'a> Iterator for CharsIgnoreNewlineRepr<'a> { + type Item = char; + + fn next(&mut self) -> Option { + self.0.next().map(|c| { + if c == '\r' { + if *self.0.peek().unwrap_or(&'\0') == '\n' { + self.0.next(); + '\n' + } else { + '\r' + } + } else { + c + } + }) + } +} + +#[test] +fn string_eq_ignore_newline_repr_test() { + assert!(string_eq_ignore_newline_repr("", "")); + assert!(!string_eq_ignore_newline_repr("", "abc")); + assert!(!string_eq_ignore_newline_repr("abc", "")); + assert!(string_eq_ignore_newline_repr("a\nb\nc\rd", "a\nb\r\nc\rd")); + assert!(string_eq_ignore_newline_repr("a\r\n\r\n\r\nb", "a\n\n\nb")); + assert!(!string_eq_ignore_newline_repr("a\r\nbcd", "a\nbcdefghijk")); +} + +// This enum is used to represent one of three text features in Configurations.md: a block of code +// with its starting line number, the name of a rustfmt configuration option, or the value of a +// rustfmt configuration option. +enum ConfigurationSection { + CodeBlock((String, u32)), // (String: block of code, u32: line number of code block start) + ConfigName(String), + ConfigValue(String), +} + +impl ConfigurationSection { + fn get_section>( + file: &mut Enumerate, + ) -> Option { + lazy_static! { + static ref CONFIG_NAME_REGEX: regex::Regex = + regex::Regex::new(r"^## `([^`]+)`").expect("Failed creating configuration pattern"); + static ref CONFIG_VALUE_REGEX: regex::Regex = + regex::Regex::new(r#"^#### `"?([^`"]+)"?`"#) + .expect("Failed creating configuration value pattern"); + } + + loop { + match file.next() { + Some((i, line)) => { + if line.starts_with("```rust") { + // Get the lines of the code block. + let lines: Vec = file + .map(|(_i, l)| l) + .take_while(|l| !l.starts_with("```")) + .collect(); + let block = format!("{}\n", lines.join("\n")); + + // +1 to translate to one-based indexing + // +1 to get to first line of code (line after "```") + let start_line = (i + 2) as u32; + + return Some(ConfigurationSection::CodeBlock((block, start_line))); + } else if let Some(c) = CONFIG_NAME_REGEX.captures(&line) { + return Some(ConfigurationSection::ConfigName(String::from(&c[1]))); + } else if let Some(c) = CONFIG_VALUE_REGEX.captures(&line) { + return Some(ConfigurationSection::ConfigValue(String::from(&c[1]))); + } + } + None => return None, // reached the end of the file + } + } + } +} + +// This struct stores the information about code blocks in the configurations +// file, formats the code blocks, and prints formatting errors. +struct ConfigCodeBlock { + config_name: Option, + config_value: Option, + code_block: Option, + code_block_start: Option, +} + +impl ConfigCodeBlock { + fn new() -> ConfigCodeBlock { + ConfigCodeBlock { + config_name: None, + config_value: None, + code_block: None, + code_block_start: None, + } + } + + fn set_config_name(&mut self, name: Option) { + self.config_name = name; + self.config_value = None; + } + + fn set_config_value(&mut self, value: Option) { + self.config_value = value; + } + + fn set_code_block(&mut self, code_block: String, code_block_start: u32) { + self.code_block = Some(code_block); + self.code_block_start = Some(code_block_start); + } + + fn get_block_config(&self) -> Config { + let mut config = Config::default(); + if self.config_value.is_some() && self.config_value.is_some() { + config.override_value( + self.config_name.as_ref().unwrap(), + self.config_value.as_ref().unwrap(), + ); + } + config + } + + fn code_block_valid(&self) -> bool { + // We never expect to not have a code block. + assert!(self.code_block.is_some() && self.code_block_start.is_some()); + + // See if code block begins with #![rustfmt::skip]. + let fmt_skip = self + .code_block + .as_ref() + .unwrap() + .split('\n') + .nth(0) + .unwrap_or("") + == "#![rustfmt::skip]"; + + if self.config_name.is_none() && !fmt_skip { + write_message(&format!( + "No configuration name for {}:{}", + CONFIGURATIONS_FILE_NAME, + self.code_block_start.unwrap() + )); + return false; + } + if self.config_value.is_none() && !fmt_skip { + write_message(&format!( + "No configuration value for {}:{}", + CONFIGURATIONS_FILE_NAME, + self.code_block_start.unwrap() + )); + return false; + } + true + } + + fn has_parsing_errors(&self, session: &Session) -> bool { + if session.has_parsing_errors() { + write_message(&format!( + "\u{261d}\u{1f3fd} Cannot format {}:{}", + CONFIGURATIONS_FILE_NAME, + self.code_block_start.unwrap() + )); + return true; + } + + false + } + + fn print_diff(&self, compare: Vec) { + let mut mismatches = HashMap::new(); + mismatches.insert(PathBuf::from(CONFIGURATIONS_FILE_NAME), compare); + print_mismatches(mismatches, |line_num| { + format!( + "\nMismatch at {}:{}:", + CONFIGURATIONS_FILE_NAME, + line_num + self.code_block_start.unwrap() - 1 + ) + }); + } + + fn formatted_has_diff(&self, text: &str) -> bool { + let compare = make_diff(self.code_block.as_ref().unwrap(), text, DIFF_CONTEXT_SIZE); + if !compare.is_empty() { + self.print_diff(compare); + return true; + } + + false + } + + // Return a bool indicating if formatting this code block is an idempotent + // operation. This function also triggers printing any formatting failure + // messages. + fn formatted_is_idempotent(&self) -> bool { + // Verify that we have all of the expected information. + if !self.code_block_valid() { + return false; + } + + let input = Input::Text(self.code_block.as_ref().unwrap().to_owned()); + let mut config = self.get_block_config(); + config.set().emit_mode(EmitMode::Stdout); + let mut buf: Vec = vec![]; + + { + let mut session = Session::new(config, Some(&mut buf)); + session.format(input).unwrap(); + if self.has_parsing_errors(&session) { + return false; + } + } + + !self.formatted_has_diff(&String::from_utf8(buf).unwrap()) + } + + // Extract a code block from the iterator. Behavior: + // - Rust code blocks are identifed by lines beginning with "```rust". + // - One explicit configuration setting is supported per code block. + // - Rust code blocks with no configuration setting are illegal and cause an + // assertion failure, unless the snippet begins with #![rustfmt::skip]. + // - Configuration names in Configurations.md must be in the form of + // "## `NAME`". + // - Configuration values in Configurations.md must be in the form of + // "#### `VALUE`". + fn extract>( + file: &mut Enumerate, + prev: Option<&ConfigCodeBlock>, + hash_set: &mut HashSet, + ) -> Option { + let mut code_block = ConfigCodeBlock::new(); + code_block.config_name = prev.and_then(|cb| cb.config_name.clone()); + + loop { + match ConfigurationSection::get_section(file) { + Some(ConfigurationSection::CodeBlock((block, start_line))) => { + code_block.set_code_block(block, start_line); + break; + } + Some(ConfigurationSection::ConfigName(name)) => { + assert!( + Config::is_valid_name(&name), + "an unknown configuration option was found: {}", + name + ); + assert!( + hash_set.remove(&name), + "multiple configuration guides found for option {}", + name + ); + code_block.set_config_name(Some(name)); + } + Some(ConfigurationSection::ConfigValue(value)) => { + code_block.set_config_value(Some(value)); + } + None => return None, // end of file was reached + } + } + + Some(code_block) + } +} + +#[test] +fn configuration_snippet_tests() { + // Read Configurations.md and build a `Vec` of `ConfigCodeBlock` structs with one + // entry for each Rust code block found. + fn get_code_blocks() -> Vec { + let mut file_iter = BufReader::new( + fs::File::open(Path::new(CONFIGURATIONS_FILE_NAME)) + .expect(&format!("Couldn't read file {}", CONFIGURATIONS_FILE_NAME)), + ).lines() + .map(|l| l.unwrap()) + .enumerate(); + let mut code_blocks: Vec = Vec::new(); + let mut hash_set = Config::hash_set(); + + while let Some(cb) = + ConfigCodeBlock::extract(&mut file_iter, code_blocks.last(), &mut hash_set) + { + code_blocks.push(cb); + } + + for name in hash_set { + if !Config::is_hidden_option(&name) { + panic!("{} does not have a configuration guide", name); + } + } + + code_blocks + } + + let blocks = get_code_blocks(); + let failures = blocks + .iter() + .map(|b| b.formatted_is_idempotent()) + .fold(0, |acc, r| acc + (!r as u32)); + + // Display results. + println!("Ran {} configurations tests.", blocks.len()); + assert_eq!(failures, 0, "{} configurations tests failed", failures); +} + +struct TempFile { + path: PathBuf, +} + +fn make_temp_file(file_name: &'static str) -> TempFile { + use std::env::var; + use std::fs::File; + + // Used in the Rust build system. + let target_dir = var("RUSTFMT_TEST_DIR").unwrap_or_else(|_| ".".to_owned()); + let path = Path::new(&target_dir).join(file_name); + + let mut file = File::create(&path).expect("Couldn't create temp file"); + let content = "fn main() {}\n"; + file.write_all(content.as_bytes()) + .expect("Couldn't write temp file"); + TempFile { path } +} + +impl Drop for TempFile { + fn drop(&mut self) { + use std::fs::remove_file; + remove_file(&self.path).expect("Couldn't delete temp file"); + } +} + +fn rustfmt() -> PathBuf { + let mut me = env::current_exe().expect("failed to get current executable"); + me.pop(); // chop of the test name + me.pop(); // chop off `deps` + me.push("rustfmt"); + assert!( + me.is_file() || me.with_extension("exe").is_file(), + "no rustfmt bin, try running `cargo build` before testing" + ); + return me; +} + +#[test] +fn verify_check_works() { + let temp_file = make_temp_file("temp_check.rs"); + assert_cli::Assert::command(&[ + rustfmt().to_str().unwrap(), + "--check", + temp_file.path.to_str().unwrap(), + ]).succeeds() + .unwrap(); +}