blob: a10a04386aab73f160ed419c9c1d1fd4ff575371 [file] [log] [blame]
// Inspired by Clang's clang-format-diff:
//
// https://github.com/llvm-mirror/clang/blob/master/tools/clang-format/clang-format-diff.py
#![deny(warnings)]
use env_logger;
#[macro_use]
extern crate failure;
use getopts;
#[macro_use]
extern crate log;
use regex;
#[macro_use]
extern crate serde_derive;
use 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<getopts::Fail> for FormatDiffError {
fn from(fail: getopts::Fail) -> Self {
FormatDiffError::IncorrectOptions(fail)
}
}
impl From<regex::Error> for FormatDiffError {
fn from(err: regex::Error) -> Self {
FormatDiffError::IncorrectFilter(err)
}
}
impl From<io::Error> 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(&e.to_string()));
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::<u32>().ok())
.unwrap_or(0);
let (files, ranges) = scan_diff(io::stdin(), skip_prefix, &filter)?;
run_rustfmt(&files, &ranges)
}
fn run_rustfmt(files: &HashSet<String>, 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<R>(
from: R,
skip_prefix: u32,
file_filter: &str,
) -> Result<(HashSet<String>, Vec<Range>), 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::<u32>()
.unwrap();
let line_count = match lines_captures.get(3) {
Some(line_count) => line_count.as_str().parse::<u32>().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: &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],
},
]
);
}