1 // Copyright 2017 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
11 // Inspired by Clang's clang-format-diff:
13 // https://github.com/llvm-mirror/clang/blob/master/tools/clang-format/clang-format-diff.py
17 extern crate env_logger;
23 extern crate serde_derive;
24 extern crate serde_json as json;
26 use std::{env, fmt, process};
27 use std::collections::HashSet;
28 use std::error::Error;
29 use std::io::{self, BufRead};
33 /// The default pattern of files to format.
35 /// We only want to format rust files by default.
36 const DEFAULT_PATTERN: &str = r".*\.rs";
39 enum FormatDiffError {
40 IncorrectOptions(getopts::Fail),
41 IncorrectFilter(regex::Error),
45 impl fmt::Display for FormatDiffError {
46 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
47 fmt::Display::fmt(self.cause().unwrap(), f)
51 impl Error for FormatDiffError {
52 fn description(&self) -> &str {
53 self.cause().unwrap().description()
56 fn cause(&self) -> Option<&Error> {
58 FormatDiffError::IoError(ref e) => e,
59 FormatDiffError::IncorrectFilter(ref e) => e,
60 FormatDiffError::IncorrectOptions(ref e) => e,
65 impl From<getopts::Fail> for FormatDiffError {
66 fn from(fail: getopts::Fail) -> Self {
67 FormatDiffError::IncorrectOptions(fail)
71 impl From<regex::Error> for FormatDiffError {
72 fn from(err: regex::Error) -> Self {
73 FormatDiffError::IncorrectFilter(err)
77 impl From<io::Error> for FormatDiffError {
78 fn from(fail: io::Error) -> Self {
79 FormatDiffError::IoError(fail)
84 let _ = env_logger::init();
86 let mut opts = getopts::Options::new();
87 opts.optflag("h", "help", "show this message");
91 "skip the smallest prefix containing NUMBER slashes",
97 "custom pattern selecting file paths to reformat",
101 if let Err(e) = run(&opts) {
102 println!("{}", opts.usage(e.description()));
107 #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
113 fn run(opts: &getopts::Options) -> Result<(), FormatDiffError> {
114 let matches = opts.parse(env::args().skip(1))?;
116 if matches.opt_present("h") {
117 println!("{}", opts.usage("usage: "));
123 .unwrap_or_else(|| DEFAULT_PATTERN.to_owned());
125 let skip_prefix = matches
127 .and_then(|p| p.parse::<u32>().ok())
130 let (files, ranges) = scan_diff(io::stdin(), skip_prefix, &filter)?;
132 run_rustfmt(&files, &ranges)
135 fn run_rustfmt(files: &HashSet<String>, ranges: &[Range]) -> Result<(), FormatDiffError> {
136 if files.is_empty() || ranges.is_empty() {
137 debug!("No files to format found");
141 let ranges_as_json = json::to_string(ranges).unwrap();
143 debug!("Files: {:?}", files);
144 debug!("Ranges: {:?}", ranges);
146 let exit_status = process::Command::new("rustfmt")
152 if !exit_status.success() {
153 return Err(FormatDiffError::IoError(io::Error::new(
154 io::ErrorKind::Other,
155 format!("rustfmt failed with {}", exit_status),
161 /// Scans a diff from `from`, and returns the set of files found, and the ranges
167 ) -> Result<(HashSet<String>, Vec<Range>), FormatDiffError>
171 let diff_pattern = format!(r"^\+\+\+\s(?:.*?/){{{}}}(\S*)", skip_prefix);
172 let diff_pattern = Regex::new(&diff_pattern).unwrap();
174 let lines_pattern = Regex::new(r"^@@.*\+(\d+)(,(\d+))?").unwrap();
176 let file_filter = Regex::new(&format!("^{}$", file_filter))?;
178 let mut current_file = None;
180 let mut files = HashSet::new();
181 let mut ranges = vec![];
182 for line in io::BufReader::new(from).lines() {
183 let line = line.unwrap();
185 if let Some(captures) = diff_pattern.captures(&line) {
186 current_file = Some(captures.get(1).unwrap().as_str().to_owned());
189 let file = match current_file {
194 // TODO(emilio): We could avoid this most of the time if needed, but
195 // it's not clear it's worth it.
196 if !file_filter.is_match(file) {
200 let lines_captures = match lines_pattern.captures(&line) {
201 Some(captures) => captures,
205 let start_line = lines_captures
211 let line_count = match lines_captures.get(3) {
212 Some(line_count) => line_count.as_str().parse::<u32>().unwrap(),
220 let end_line = start_line + line_count - 1;
221 files.insert(file.to_owned());
223 file: file.to_owned(),
224 range: [start_line, end_line],
232 fn scan_simple_git_diff() {
233 const DIFF: &'static str = include_str!("test/bindgen.diff");
234 let (files, ranges) = scan_diff(DIFF.as_bytes(), 1, r".*\.rs").expect("scan_diff failed?");
237 files.contains("src/ir/traversal.rs"),
238 "Should've matched the filter"
242 !files.contains("tests/headers/anon_enum.hpp"),
243 "Shouldn't have matched the filter"
250 file: "src/ir/item.rs".to_owned(),
254 file: "src/ir/item.rs".to_owned(),
258 file: "src/ir/traversal.rs".to_owned(),
262 file: "src/ir/traversal.rs".to_owned(),