diff --git a/rustfmt-config/src/config_type.rs b/rustfmt-config/src/config_type.rs index 51642570fdf..1f78b8af9d2 100644 --- a/rustfmt-config/src/config_type.rs +++ b/rustfmt-config/src/config_type.rs @@ -107,7 +107,7 @@ macro_rules! create_config { // 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.get().option(false)`. It's pretty ugly. Consider replacing + // `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); diff --git a/rustfmt-config/src/options.rs b/rustfmt-config/src/options.rs index bff6d2298d3..885d89ed752 100644 --- a/rustfmt-config/src/options.rs +++ b/rustfmt-config/src/options.rs @@ -176,6 +176,8 @@ configuration_option_enum! { WriteMode: Plain, // Outputs a checkstyle XML file. Checkstyle, + // Output the changed lines (for internal value only) + Modified, } configuration_option_enum! { Color: diff --git a/rustfmt-core/src/filemap.rs b/rustfmt-core/src/filemap.rs index 5986dbd6fa3..ee2253043a1 100644 --- a/rustfmt-core/src/filemap.rs +++ b/rustfmt-core/src/filemap.rs @@ -16,7 +16,7 @@ use std::path::Path; use checkstyle::{output_checkstyle_file, output_footer, output_header}; use config::{Config, NewlineStyle, WriteMode}; -use rustfmt_diff::{make_diff, print_diff, Mismatch}; +use rustfmt_diff::{make_diff, output_modified, print_diff, Mismatch}; use syntax::codemap::FileName; use FileRecord; @@ -164,6 +164,15 @@ where return Ok(has_diff); } } + WriteMode::Modified => { + let filename = filename_to_path(); + if let Ok((ori, fmt)) = source_and_formatted_text(text, filename, config) { + let mismatch = make_diff(&ori, &fmt, 0); + let has_diff = !mismatch.is_empty(); + output_modified(out, mismatch); + return Ok(has_diff); + } + } WriteMode::Checkstyle => { let filename = filename_to_path(); let diff = create_diff(filename, text, config)?; diff --git a/rustfmt-core/src/lib.rs b/rustfmt-core/src/lib.rs index 6f471d0167f..d94f2a2c12c 100644 --- a/rustfmt-core/src/lib.rs +++ b/rustfmt-core/src/lib.rs @@ -26,7 +26,7 @@ extern crate unicode_segmentation; use std::collections::HashMap; use std::fmt; -use std::io::{self, stdout, Write}; +use std::io::{self, stdout, BufRead, Write}; use std::iter::repeat; use std::path::PathBuf; use std::rc::Rc; @@ -704,6 +704,79 @@ pub fn format_input( } } +/// A single span of changed lines, with 0 or more removed lines +/// and a vector of 0 or more inserted lines. +#[derive(Debug, PartialEq, Eq)] +pub struct ModifiedChunk { + /// The first to be removed from the original text + pub line_number_orig: u32, + /// The number of lines which have been replaced + pub lines_removed: u32, + /// The new lines + pub lines: Vec, +} + +/// Set of changed sections of a file. +#[derive(Debug, PartialEq, Eq)] +pub struct ModifiedLines { + /// The set of changed chunks. + pub chunks: Vec, +} + +/// The successful result of formatting via get_modified_lines(). +pub struct ModifiedLinesResult { + /// The high level summary details + pub summary: Summary, + /// The result Filemap + pub filemap: FileMap, + /// Map of formatting errors + pub report: FormatReport, + /// The sets of updated lines. + pub modified_lines: ModifiedLines, +} + +/// Format a file and return a `ModifiedLines` data structure describing +/// the changed ranges of lines. +pub fn get_modified_lines( + input: Input, + config: &Config, +) -> Result { + let mut data = Vec::new(); + + let mut config = config.clone(); + config.set().write_mode(config::WriteMode::Modified); + let (summary, filemap, formatreport) = format_input(input, &config, Some(&mut data))?; + + 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, + }); + } + Ok(ModifiedLinesResult { + summary: summary, + filemap: filemap, + report: formatreport, + modified_lines: ModifiedLines { chunks }, + }) +} + #[derive(Debug)] pub enum Input { File(PathBuf), diff --git a/rustfmt-core/src/rustfmt_diff.rs b/rustfmt-core/src/rustfmt_diff.rs index 1a2f570f89e..f9919157620 100644 --- a/rustfmt-core/src/rustfmt_diff.rs +++ b/rustfmt-core/src/rustfmt_diff.rs @@ -13,6 +13,7 @@ use diff; use std::collections::VecDeque; use std::io; use term; +use std::io::Write; use utils::use_colored_tty; #[derive(Debug, PartialEq)] @@ -24,14 +25,19 @@ pub enum DiffLine { #[derive(Debug, PartialEq)] pub struct Mismatch { + /// The line number in the formatted version. pub line_number: u32, + /// The line number in the original version. + pub line_number_orig: u32, + /// The set of lines (context and old/new) in the mismatch. pub lines: Vec, } impl Mismatch { - fn new(line_number: u32) -> Mismatch { + fn new(line_number: u32, line_number_orig: u32) -> Mismatch { Mismatch { line_number, + line_number_orig, lines: Vec::new(), } } @@ -77,17 +83,21 @@ impl OutputWriter { // Produces a diff between the expected output and actual output of rustfmt. pub fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec { let mut line_number = 1; + let mut line_number_orig = 1; let mut context_queue: VecDeque<&str> = VecDeque::with_capacity(context_size); let mut lines_since_mismatch = context_size + 1; let mut results = Vec::new(); - let mut mismatch = Mismatch::new(0); + let mut mismatch = Mismatch::new(0, 0); for result in diff::lines(expected, actual) { match result { diff::Result::Left(str) => { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); - mismatch = Mismatch::new(line_number - context_queue.len() as u32); + mismatch = Mismatch::new( + line_number - context_queue.len() as u32, + line_number_orig - context_queue.len() as u32, + ); } while let Some(line) = context_queue.pop_front() { @@ -95,12 +105,16 @@ pub fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); - mismatch = Mismatch::new(line_number - context_queue.len() as u32); + mismatch = Mismatch::new( + line_number - context_queue.len() as u32, + line_number_orig - context_queue.len() as u32, + ); } while let Some(line) = context_queue.pop_front() { @@ -123,6 +137,7 @@ pub fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec(mut out: W, diff: Vec) +where + W: Write, +{ + for mismatch in diff { + let (num_removed, num_added) = mismatch.lines.iter().fold((0, 0), |(rem, add), line| { + match *line { + DiffLine::Context(_) => panic!("No Context expected"), + DiffLine::Expected(_) => (rem, add + 1), + DiffLine::Resulting(_) => (rem + 1, add), + } + }); + // Write a header with enough information to separate the modified lines. + writeln!( + out, + "{} {} {}", + mismatch.line_number_orig, num_removed, num_added + ).unwrap(); + + for line in mismatch.lines { + match line { + DiffLine::Context(_) | DiffLine::Resulting(_) => (), + DiffLine::Expected(ref str) => { + writeln!(out, "{}", str).unwrap(); + } + } + } + } +} + #[cfg(test)] mod test { use super::{make_diff, Mismatch}; @@ -173,6 +224,7 @@ mod test { vec![ Mismatch { line_number: 2, + line_number_orig: 2, lines: vec![ Context("two".to_owned()), Resulting("three".to_owned()), @@ -194,6 +246,7 @@ mod test { vec![ Mismatch { line_number: 2, + line_number_orig: 2, lines: vec![ Context("two".to_owned()), Resulting("three".to_owned()), @@ -203,6 +256,7 @@ mod test { }, Mismatch { line_number: 5, + line_number_orig: 5, lines: vec![ Resulting("five".to_owned()), Expected("cinq".to_owned()), @@ -223,6 +277,7 @@ mod test { vec![ Mismatch { line_number: 3, + line_number_orig: 3, lines: vec![Resulting("three".to_owned()), Expected("trois".to_owned())], }, ] @@ -239,6 +294,7 @@ mod test { vec![ Mismatch { line_number: 5, + line_number_orig: 5, lines: vec![Context("five".to_owned()), Expected("".to_owned())], }, ] diff --git a/rustfmt-core/tests/lib.rs b/rustfmt-core/tests/lib.rs index c618ab9c760..3a990ebe88c 100644 --- a/rustfmt-core/tests/lib.rs +++ b/rustfmt-core/tests/lib.rs @@ -143,6 +143,30 @@ fn checkstyle_test() { assert_output(Path::new(filename), Path::new(expected_filename)); } +#[test] +fn modified_test() { + // Test "modified" output + let filename = "tests/writemode/source/modified.rs"; + let result = get_modified_lines(Input::File(filename.into()), &Config::default()).unwrap(); + assert_eq!( + result.modified_lines, + ModifiedLines { + 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) { @@ -529,6 +553,7 @@ fn rustfmt_diff_make_diff_tests() { vec![ Mismatch { line_number: 1, + line_number_orig: 1, lines: vec![ DiffLine::Context("a".into()), DiffLine::Resulting("b".into()), diff --git a/rustfmt-core/tests/writemode/source/modified.rs b/rustfmt-core/tests/writemode/source/modified.rs new file mode 100644 index 00000000000..948beb348db --- /dev/null +++ b/rustfmt-core/tests/writemode/source/modified.rs @@ -0,0 +1,14 @@ +// rustfmt-write_mode: modified +// Test "modified" output + +fn +blah +() +{ } + + +#[cfg +( a , b +)] +fn +main() {} diff --git a/rustfmt-core/tests/writemode/target/modified.txt b/rustfmt-core/tests/writemode/target/modified.txt new file mode 100644 index 00000000000..5c0539a665e --- /dev/null +++ b/rustfmt-core/tests/writemode/target/modified.txt @@ -0,0 +1,5 @@ +4 4 1 +fn blah() {} +10 5 2 +#[cfg(a, b)] +fn main() {}