Skip to content

Commit 0e84699

Browse files
authored
Merge pull request #1842 from emilio/rustfmt-format-diff
bin: Add a very simple rustfmt-format-diff.
2 parents b9e5afd + 068bcad commit 0e84699

File tree

3 files changed

+340
-1
lines changed

3 files changed

+340
-1
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ name = "rustfmt"
2020
[[bin]]
2121
name = "cargo-fmt"
2222

23+
[[bin]]
24+
name = "rustfmt-format-diff"
25+
2326
[features]
24-
default = ["cargo-fmt"]
27+
default = ["cargo-fmt", "rustfmt-format-diff"]
2528
cargo-fmt = []
29+
rustfmt-format-diff = []
2630

2731
[dependencies]
2832
toml = "0.4"

src/bin/rustfmt-format-diff.rs

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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.
4+
//
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.
10+
11+
// Inspired by Clang's clang-format-diff:
12+
//
13+
// https://github.com/llvm-mirror/clang/blob/master/tools/clang-format/clang-format-diff.py
14+
15+
#![deny(warnings)]
16+
17+
extern crate env_logger;
18+
extern crate getopts;
19+
#[macro_use]
20+
extern crate log;
21+
extern crate regex;
22+
extern crate serde;
23+
#[macro_use]
24+
extern crate serde_derive;
25+
extern crate serde_json as json;
26+
27+
use std::{env, fmt, process};
28+
use std::collections::HashSet;
29+
use std::error::Error;
30+
use std::io::{self, BufRead};
31+
32+
use regex::Regex;
33+
34+
/// The default pattern of files to format.
35+
///
36+
/// We only want to format rust files by default.
37+
const DEFAULT_PATTERN: &'static str = r".*\.rs";
38+
39+
#[derive(Debug)]
40+
enum FormatDiffError {
41+
IncorrectOptions(getopts::Fail),
42+
IncorrectFilter(regex::Error),
43+
IoError(io::Error),
44+
}
45+
46+
impl fmt::Display for FormatDiffError {
47+
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
48+
fmt::Display::fmt(self.cause().unwrap(), f)
49+
}
50+
}
51+
52+
impl Error for FormatDiffError {
53+
fn description(&self) -> &str {
54+
self.cause().unwrap().description()
55+
}
56+
57+
fn cause(&self) -> Option<&Error> {
58+
Some(match *self {
59+
FormatDiffError::IoError(ref e) => e,
60+
FormatDiffError::IncorrectFilter(ref e) => e,
61+
FormatDiffError::IncorrectOptions(ref e) => e,
62+
})
63+
}
64+
}
65+
66+
impl From<getopts::Fail> for FormatDiffError {
67+
fn from(fail: getopts::Fail) -> Self {
68+
FormatDiffError::IncorrectOptions(fail)
69+
}
70+
}
71+
72+
impl From<regex::Error> for FormatDiffError {
73+
fn from(err: regex::Error) -> Self {
74+
FormatDiffError::IncorrectFilter(err)
75+
}
76+
}
77+
78+
impl From<io::Error> for FormatDiffError {
79+
fn from(fail: io::Error) -> Self {
80+
FormatDiffError::IoError(fail)
81+
}
82+
}
83+
84+
fn main() {
85+
let _ = env_logger::init();
86+
87+
let mut opts = getopts::Options::new();
88+
opts.optflag("h", "help", "show this message");
89+
opts.optopt(
90+
"p",
91+
"skip-prefix",
92+
"skip the smallest prefix containing NUMBER slashes",
93+
"NUMBER",
94+
);
95+
opts.optopt(
96+
"f",
97+
"filter",
98+
"custom pattern selecting file paths to reformat",
99+
"PATTERN",
100+
);
101+
102+
if let Err(e) = run(&opts) {
103+
println!("{}", opts.usage(e.description()));
104+
process::exit(1);
105+
}
106+
}
107+
108+
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
109+
struct Range {
110+
file: String,
111+
range: [u32; 2],
112+
}
113+
114+
fn run(opts: &getopts::Options) -> Result<(), FormatDiffError> {
115+
let matches = opts.parse(env::args().skip(1))?;
116+
117+
if matches.opt_present("h") {
118+
println!("{}", opts.usage("usage: "));
119+
return Ok(());
120+
}
121+
122+
let filter = matches
123+
.opt_str("f")
124+
.unwrap_or_else(|| DEFAULT_PATTERN.into());
125+
126+
let skip_prefix = matches
127+
.opt_str("p")
128+
.and_then(|p| p.parse::<u32>().ok())
129+
.unwrap_or(0);
130+
131+
let (files, ranges) = scan_diff(io::stdin(), skip_prefix, &filter)?;
132+
133+
run_rustfmt(&files, &ranges)
134+
}
135+
136+
fn run_rustfmt(files: &HashSet<String>, ranges: &[Range]) -> Result<(), FormatDiffError> {
137+
if files.is_empty() || ranges.is_empty() {
138+
debug!("No files to format found");
139+
return Ok(());
140+
}
141+
142+
let ranges_as_json = json::to_string(ranges).unwrap();
143+
144+
debug!("Files: {:?}", files);
145+
debug!("Ranges: {:?}", ranges);
146+
147+
let exit_status = process::Command::new("rustfmt")
148+
.args(files)
149+
.arg("--file-lines")
150+
.arg(ranges_as_json)
151+
.status()?;
152+
153+
if !exit_status.success() {
154+
return Err(FormatDiffError::IoError(io::Error::new(
155+
io::ErrorKind::Other,
156+
format!("rustfmt failed with {}", exit_status),
157+
)));
158+
}
159+
Ok(())
160+
}
161+
162+
/// Scans a diff from `from`, and returns the set of files found, and the ranges
163+
/// in those files.
164+
fn scan_diff<R>(
165+
from: R,
166+
skip_prefix: u32,
167+
file_filter: &str,
168+
) -> Result<(HashSet<String>, Vec<Range>), FormatDiffError>
169+
where
170+
R: io::Read,
171+
{
172+
let diff_pattern = format!(r"^\+\+\+\s(?:.*?/){{{}}}(\S*)", skip_prefix);
173+
let diff_pattern = Regex::new(&diff_pattern).unwrap();
174+
175+
let lines_pattern = Regex::new(r"^@@.*\+(\d+)(,(\d+))?").unwrap();
176+
177+
let file_filter = Regex::new(&format!("^{}$", file_filter))?;
178+
179+
let mut current_file = None;
180+
181+
let mut files = HashSet::new();
182+
let mut ranges = vec![];
183+
for line in io::BufReader::new(from).lines() {
184+
let line = line.unwrap();
185+
186+
if let Some(captures) = diff_pattern.captures(&line) {
187+
current_file = Some(captures.get(1).unwrap().as_str().to_owned());
188+
}
189+
190+
let file = match current_file {
191+
Some(ref f) => &**f,
192+
None => continue,
193+
};
194+
195+
// TODO(emilio): We could avoid this most of the time if needed, but
196+
// it's not clear it's worth it.
197+
if !file_filter.is_match(file) {
198+
continue;
199+
}
200+
201+
let lines_captures = match lines_pattern.captures(&line) {
202+
Some(captures) => captures,
203+
None => continue,
204+
};
205+
206+
let start_line = lines_captures
207+
.get(1)
208+
.unwrap()
209+
.as_str()
210+
.parse::<u32>()
211+
.unwrap();
212+
let line_count = match lines_captures.get(3) {
213+
Some(line_count) => line_count.as_str().parse::<u32>().unwrap(),
214+
None => 1,
215+
};
216+
217+
if line_count == 0 {
218+
continue;
219+
}
220+
221+
let end_line = start_line + line_count - 1;
222+
files.insert(file.to_owned());
223+
ranges.push(Range {
224+
file: file.to_owned(),
225+
range: [start_line, end_line],
226+
});
227+
}
228+
229+
Ok((files, ranges))
230+
}
231+
232+
#[test]
233+
fn scan_simple_git_diff() {
234+
const DIFF: &'static str = include_str!("test/bindgen.diff");
235+
let (files, ranges) = scan_diff(DIFF.as_bytes(), 1, r".*\.rs").expect("scan_diff failed?");
236+
237+
assert!(
238+
files.contains("src/ir/traversal.rs"),
239+
"Should've matched the filter"
240+
);
241+
242+
assert!(
243+
!files.contains("tests/headers/anon_enum.hpp"),
244+
"Shouldn't have matched the filter"
245+
);
246+
247+
assert_eq!(
248+
&ranges,
249+
&[
250+
Range {
251+
file: "src/ir/item.rs".into(),
252+
range: [148, 158],
253+
},
254+
Range {
255+
file: "src/ir/item.rs".into(),
256+
range: [160, 170],
257+
},
258+
Range {
259+
file: "src/ir/traversal.rs".into(),
260+
range: [9, 16],
261+
},
262+
Range {
263+
file: "src/ir/traversal.rs".into(),
264+
range: [35, 43],
265+
}
266+
]
267+
);
268+
}

src/bin/test/bindgen.diff

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
diff --git a/src/ir/item.rs b/src/ir/item.rs
2+
index 7f3afefb..90d15e96 100644
3+
--- a/src/ir/item.rs
4+
+++ b/src/ir/item.rs
5+
@@ -148,7 +148,11 @@ impl<'a, 'b> Iterator for ItemAncestorsIter<'a, 'b>
6+
impl AsTemplateParam for ItemId {
7+
type Extra = ();
8+
9+
- fn as_template_param(&self, ctx: &BindgenContext, _: &()) -> Option<ItemId> {
10+
+ fn as_template_param(
11+
+ &self,
12+
+ ctx: &BindgenContext,
13+
+ _: &(),
14+
+ ) -> Option<ItemId> {
15+
ctx.resolve_item(*self).as_template_param(ctx, &())
16+
}
17+
}
18+
@@ -156,7 +160,11 @@ impl AsTemplateParam for ItemId {
19+
impl AsTemplateParam for Item {
20+
type Extra = ();
21+
22+
- fn as_template_param(&self, ctx: &BindgenContext, _: &()) -> Option<ItemId> {
23+
+ fn as_template_param(
24+
+ &self,
25+
+ ctx: &BindgenContext,
26+
+ _: &(),
27+
+ ) -> Option<ItemId> {
28+
self.kind.as_template_param(ctx, self)
29+
}
30+
}
31+
diff --git a/src/ir/traversal.rs b/src/ir/traversal.rs
32+
index 762a3e2d..b9c9dd4e 100644
33+
--- a/src/ir/traversal.rs
34+
+++ b/src/ir/traversal.rs
35+
@@ -9,6 +9,8 @@ use std::collections::{BTreeMap, VecDeque};
36+
///
37+
/// from --> to
38+
///
39+
+/// Random content to generate a diff.
40+
+///
41+
/// The `from` is left implicit: it is the concrete `Trace` implementer which
42+
/// yielded this outgoing edge.
43+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
44+
@@ -33,7 +35,9 @@ impl Into<ItemId> for Edge {
45+
}
46+
}
47+
48+
-/// The kind of edge reference. This is useful when we wish to only consider
49+
+/// The kind of edge reference.
50+
+///
51+
+/// This is useful when we wish to only consider
52+
/// certain kinds of edges for a particular traversal or analysis.
53+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
54+
pub enum EdgeKind {
55+
diff --git a/tests/headers/anon_enum.hpp b/tests/headers/anon_enum.hpp
56+
index 1961fe6c..34759df3 100644
57+
--- a/tests/headers/anon_enum.hpp
58+
+++ b/tests/headers/anon_enum.hpp
59+
@@ -1,7 +1,7 @@
60+
struct Test {
61+
int foo;
62+
float bar;
63+
- enum { T_NONE };
64+
+ enum { T_NONE, T_SOME };
65+
};
66+
67+
typedef enum {

0 commit comments

Comments
 (0)