2017-07-29 22:59:30 +00:00
|
|
|
// Inspired by Clang's clang-format-diff:
|
|
|
|
//
|
|
|
|
// https://github.com/llvm-mirror/clang/blob/master/tools/clang-format/clang-format-diff.py
|
|
|
|
|
|
|
|
#![deny(warnings)]
|
|
|
|
|
2018-04-24 05:45:28 +00:00
|
|
|
#[macro_use]
|
2017-07-31 08:38:24 +00:00
|
|
|
extern crate log;
|
2021-02-17 04:33:20 +00:00
|
|
|
|
2019-05-09 17:22:44 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2019-02-09 06:53:12 +00:00
|
|
|
use serde_json as json;
|
2019-12-03 00:20:17 +00:00
|
|
|
use thiserror::Error;
|
2017-07-29 22:59:30 +00:00
|
|
|
|
|
|
|
use std::collections::HashSet;
|
2020-09-16 14:09:10 +00:00
|
|
|
use std::env;
|
|
|
|
use std::ffi::OsStr;
|
2017-07-29 22:59:30 +00:00
|
|
|
use std::io::{self, BufRead};
|
2019-05-22 19:51:36 +00:00
|
|
|
use std::process;
|
2017-07-29 22:59:30 +00:00
|
|
|
|
|
|
|
use regex::Regex;
|
|
|
|
|
2019-05-22 19:51:36 +00:00
|
|
|
use structopt::clap::AppSettings;
|
|
|
|
use structopt::StructOpt;
|
|
|
|
|
2017-07-29 22:59:30 +00:00
|
|
|
/// The default pattern of files to format.
|
|
|
|
///
|
|
|
|
/// We only want to format rust files by default.
|
2017-11-05 05:02:46 +00:00
|
|
|
const DEFAULT_PATTERN: &str = r".*\.rs";
|
2017-07-29 22:59:30 +00:00
|
|
|
|
2019-12-03 00:20:17 +00:00
|
|
|
#[derive(Error, Debug)]
|
2017-07-29 22:59:30 +00:00
|
|
|
enum FormatDiffError {
|
2019-12-03 00:20:17 +00:00
|
|
|
#[error("{0}")]
|
|
|
|
IncorrectOptions(#[from] getopts::Fail),
|
|
|
|
#[error("{0}")]
|
|
|
|
IncorrectFilter(#[from] regex::Error),
|
|
|
|
#[error("{0}")]
|
|
|
|
IoError(#[from] io::Error),
|
2017-07-29 22:59:30 +00:00
|
|
|
}
|
|
|
|
|
2019-05-22 19:51:36 +00:00
|
|
|
#[derive(StructOpt, Debug)]
|
|
|
|
#[structopt(
|
|
|
|
name = "rustfmt-format-diff",
|
2019-09-08 14:33:21 +00:00
|
|
|
setting = AppSettings::DisableVersion,
|
|
|
|
setting = AppSettings::NextLineHelp
|
2019-05-22 19:51:36 +00:00
|
|
|
)]
|
|
|
|
pub struct Opts {
|
|
|
|
/// Skip the smallest prefix containing NUMBER slashes
|
|
|
|
#[structopt(
|
|
|
|
short = "p",
|
|
|
|
long = "skip-prefix",
|
|
|
|
value_name = "NUMBER",
|
|
|
|
default_value = "0"
|
|
|
|
)]
|
|
|
|
skip_prefix: u32,
|
2017-07-31 08:38:24 +00:00
|
|
|
|
2019-05-22 19:51:36 +00:00
|
|
|
/// Custom pattern selecting file paths to reformat
|
|
|
|
#[structopt(
|
|
|
|
short = "f",
|
|
|
|
long = "filter",
|
|
|
|
value_name = "PATTERN",
|
2019-09-08 14:33:21 +00:00
|
|
|
default_value = DEFAULT_PATTERN
|
2019-05-22 19:51:36 +00:00
|
|
|
)]
|
|
|
|
filter: String,
|
|
|
|
}
|
2017-07-29 22:59:30 +00:00
|
|
|
|
2019-05-22 19:51:36 +00:00
|
|
|
fn main() {
|
|
|
|
env_logger::init();
|
|
|
|
let opts = Opts::from_args();
|
|
|
|
if let Err(e) = run(opts) {
|
|
|
|
println!("{}", e);
|
|
|
|
Opts::clap().print_help().expect("cannot write to stdout");
|
2017-07-29 22:59:30 +00:00
|
|
|
process::exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
|
|
|
struct Range {
|
|
|
|
file: String,
|
|
|
|
range: [u32; 2],
|
|
|
|
}
|
|
|
|
|
2019-05-22 19:51:36 +00:00
|
|
|
fn run(opts: Opts) -> Result<(), FormatDiffError> {
|
|
|
|
let (files, ranges) = scan_diff(io::stdin(), opts.skip_prefix, &opts.filter)?;
|
2017-07-31 08:38:24 +00:00
|
|
|
run_rustfmt(&files, &ranges)
|
2017-07-29 22:59:30 +00:00
|
|
|
}
|
|
|
|
|
2017-07-31 08:38:24 +00:00
|
|
|
fn run_rustfmt(files: &HashSet<String>, ranges: &[Range]) -> Result<(), FormatDiffError> {
|
2017-07-29 22:59:30 +00:00
|
|
|
if files.is_empty() || ranges.is_empty() {
|
2017-07-31 08:38:24 +00:00
|
|
|
debug!("No files to format found");
|
2017-07-29 22:59:30 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
let ranges_as_json = json::to_string(ranges).unwrap();
|
2017-07-31 08:38:24 +00:00
|
|
|
|
|
|
|
debug!("Files: {:?}", files);
|
|
|
|
debug!("Ranges: {:?}", ranges);
|
2017-07-29 22:59:30 +00:00
|
|
|
|
2020-09-16 14:09:10 +00:00
|
|
|
let rustfmt_var = env::var_os("RUSTFMT");
|
|
|
|
let rustfmt = match &rustfmt_var {
|
|
|
|
Some(rustfmt) => rustfmt,
|
|
|
|
None => OsStr::new("rustfmt"),
|
|
|
|
};
|
|
|
|
let exit_status = process::Command::new(rustfmt)
|
2017-07-29 22:59:30 +00:00
|
|
|
.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,
|
|
|
|
};
|
|
|
|
|
2018-06-22 02:41:17 +00:00
|
|
|
// FIXME(emilio): We could avoid this most of the time if needed, but
|
2017-07-29 22:59:30 +00:00
|
|
|
// 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() {
|
2018-09-01 20:26:47 +00:00
|
|
|
const DIFF: &str = include_str!("test/bindgen.diff");
|
2017-07-29 22:59:30 +00:00
|
|
|
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 {
|
2017-12-08 04:07:42 +00:00
|
|
|
file: "src/ir/item.rs".to_owned(),
|
2017-07-29 22:59:30 +00:00
|
|
|
range: [148, 158],
|
|
|
|
},
|
|
|
|
Range {
|
2017-12-08 04:07:42 +00:00
|
|
|
file: "src/ir/item.rs".to_owned(),
|
2017-07-29 22:59:30 +00:00
|
|
|
range: [160, 170],
|
|
|
|
},
|
|
|
|
Range {
|
2017-12-08 04:07:42 +00:00
|
|
|
file: "src/ir/traversal.rs".to_owned(),
|
2017-07-29 22:59:30 +00:00
|
|
|
range: [9, 16],
|
|
|
|
},
|
|
|
|
Range {
|
2017-12-08 04:07:42 +00:00
|
|
|
file: "src/ir/traversal.rs".to_owned(),
|
2017-07-29 22:59:30 +00:00
|
|
|
range: [35, 43],
|
2018-03-25 22:38:39 +00:00
|
|
|
},
|
2017-07-29 22:59:30 +00:00
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
2019-05-22 19:51:36 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod cmd_line_tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn default_options() {
|
|
|
|
let empty: Vec<String> = vec![];
|
|
|
|
let o = Opts::from_iter(&empty);
|
|
|
|
assert_eq!(DEFAULT_PATTERN, o.filter);
|
|
|
|
assert_eq!(0, o.skip_prefix);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn good_options() {
|
|
|
|
let o = Opts::from_iter(&["test", "-p", "10", "-f", r".*\.hs"]);
|
|
|
|
assert_eq!(r".*\.hs", o.filter);
|
|
|
|
assert_eq!(10, o.skip_prefix);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn unexpected_option() {
|
|
|
|
assert!(
|
|
|
|
Opts::clap()
|
|
|
|
.get_matches_from_safe(&["test", "unexpected"])
|
|
|
|
.is_err()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn unexpected_flag() {
|
|
|
|
assert!(
|
|
|
|
Opts::clap()
|
|
|
|
.get_matches_from_safe(&["test", "--flag"])
|
|
|
|
.is_err()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn overridden_option() {
|
|
|
|
assert!(
|
|
|
|
Opts::clap()
|
|
|
|
.get_matches_from_safe(&["test", "-p", "10", "-p", "20"])
|
|
|
|
.is_err()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn negative_filter() {
|
|
|
|
assert!(
|
|
|
|
Opts::clap()
|
|
|
|
.get_matches_from_safe(&["test", "-p", "-1"])
|
|
|
|
.is_err()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|