From 7f0d87cc5162705b94a92c834d7b94cf0f02738f Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Wed, 25 May 2016 20:41:26 +0200 Subject: [PATCH 01/11] Derive Debug for the Input enum --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index e672efb8fea..9b6d69e2f16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -470,6 +470,7 @@ pub fn format_input(input: Input, } } +#[derive(Debug)] pub enum Input { File(PathBuf), Text(String), From 80c56a01ff9722ab0e52a8bd73b50603bc5a9a96 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Wed, 25 May 2016 20:41:26 +0200 Subject: [PATCH 02/11] visitor: Add debug log for FmtVisitor::visit_stmt() --- src/visitor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/visitor.rs b/src/visitor.rs index 503153463df..45c72e2fdf8 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -42,6 +42,10 @@ pub struct FmtVisitor<'a> { impl<'a> FmtVisitor<'a> { fn visit_stmt(&mut self, stmt: &ast::Stmt) { + debug!("visit_stmt: {:?} {:?}", + self.codemap.lookup_char_pos(stmt.span.lo), + self.codemap.lookup_char_pos(stmt.span.hi)); + match stmt.node { ast::StmtKind::Decl(ref decl, _) => { if let ast::DeclKind::Item(ref item) = decl.node { From bd10af127ee4acbc6cd9e3221f78620d4f1e83be Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Wed, 25 May 2016 20:41:26 +0200 Subject: [PATCH 03/11] utils: Move codemap related utilities to a dedicated module This commit adds a `codemap` module, and moves the `CodemapSpanUtils` added in #857 to it. This is preparation for adding more `Codemap` specific utilities. Refs #434 --- src/codemap.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/expr.rs | 5 +++-- src/imports.rs | 2 +- src/items.rs | 5 +++-- src/lib.rs | 1 + src/macros.rs | 3 ++- src/patterns.rs | 3 ++- src/types.rs | 3 ++- src/utils.rs | 39 +-------------------------------------- src/visitor.rs | 3 ++- 10 files changed, 56 insertions(+), 47 deletions(-) create mode 100644 src/codemap.rs diff --git a/src/codemap.rs b/src/codemap.rs new file mode 100644 index 00000000000..c70decd8422 --- /dev/null +++ b/src/codemap.rs @@ -0,0 +1,39 @@ +use syntax::codemap::{BytePos, CodeMap, Span}; + +use comment::FindUncommented; + +pub trait SpanUtils { + fn span_after(&self, original: Span, needle: &str) -> BytePos; + fn span_after_last(&self, original: Span, needle: &str) -> BytePos; + fn span_before(&self, original: Span, needle: &str) -> BytePos; +} + +impl SpanUtils for CodeMap { + #[inline] + fn span_after(&self, original: Span, needle: &str) -> BytePos { + let snippet = self.span_to_snippet(original).unwrap(); + let offset = snippet.find_uncommented(needle).unwrap() + needle.len(); + + original.lo + BytePos(offset as u32) + } + + #[inline] + fn span_after_last(&self, original: Span, needle: &str) -> BytePos { + let snippet = self.span_to_snippet(original).unwrap(); + let mut offset = 0; + + while let Some(additional_offset) = snippet[offset..].find_uncommented(needle) { + offset += additional_offset + needle.len(); + } + + original.lo + BytePos(offset as u32) + } + + #[inline] + fn span_before(&self, original: Span, needle: &str) -> BytePos { + let snippet = self.span_to_snippet(original).unwrap(); + let offset = snippet.find_uncommented(needle).unwrap(); + + original.lo + BytePos(offset as u32) + } +} diff --git a/src/expr.rs b/src/expr.rs index fc7f9cc6ca3..af0492b2913 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -16,12 +16,13 @@ use std::iter::ExactSizeIterator; use std::fmt::Write; use {Indent, Spanned}; +use codemap::SpanUtils; use rewrite::{Rewrite, RewriteContext}; use lists::{write_list, itemize_list, ListFormatting, SeparatorTactic, ListTactic, DefinitiveListTactic, definitive_tactic, ListItem, format_item_list}; use string::{StringFormat, rewrite_string}; -use utils::{CodeMapSpanUtils, extra_offset, last_line_width, wrap_str, binary_search, - first_line_width, semicolon_for_stmt, trimmed_last_line_width, left_most_sub_expr}; +use utils::{extra_offset, last_line_width, wrap_str, binary_search, first_line_width, + semicolon_for_stmt, trimmed_last_line_width, left_most_sub_expr}; use visitor::FmtVisitor; use config::{Config, StructLitStyle, MultilineStyle, ElseIfBraceStyle, ControlBraceStyle}; use comment::{FindUncommented, rewrite_comment, contains_comment, recover_comment_removed}; diff --git a/src/imports.rs b/src/imports.rs index cb644485199..89030392774 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -9,9 +9,9 @@ // except according to those terms. use Indent; +use codemap::SpanUtils; use lists::{write_list, itemize_list, ListItem, ListFormatting, SeparatorTactic, definitive_tactic}; use types::rewrite_path; -use utils::CodeMapSpanUtils; use rewrite::{Rewrite, RewriteContext}; use syntax::ast; diff --git a/src/items.rs b/src/items.rs index defd8e1a742..d83ad3c88ee 100644 --- a/src/items.rs +++ b/src/items.rs @@ -11,8 +11,9 @@ // Formatting top-level items - functions, structs, enums, traits, impls. use Indent; -use utils::{CodeMapSpanUtils, format_mutability, format_visibility, contains_skip, end_typaram, - wrap_str, last_line_width, semicolon_for_expr, format_unsafety, trim_newlines}; +use codemap::SpanUtils; +use utils::{format_mutability, format_visibility, contains_skip, end_typaram, wrap_str, + last_line_width, semicolon_for_expr, format_unsafety, trim_newlines}; use lists::{write_list, itemize_list, ListItem, ListFormatting, SeparatorTactic, DefinitiveListTactic, ListTactic, definitive_tactic, format_item_list}; use expr::{is_empty_block, is_simple_block_stmt, rewrite_assign_rhs}; diff --git a/src/lib.rs b/src/lib.rs index 9b6d69e2f16..8df2c069bcc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,7 @@ pub use self::summary::Summary; #[macro_use] mod utils; pub mod config; +pub mod codemap; pub mod filemap; pub mod visitor; mod checkstyle; diff --git a/src/macros.rs b/src/macros.rs index e9992fb0eb0..9cb0f8619c0 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -25,10 +25,11 @@ use syntax::parse::tts_to_parser; use syntax::codemap::{mk_sp, BytePos}; use Indent; +use codemap::SpanUtils; use rewrite::{Rewrite, RewriteContext}; use expr::{rewrite_call, rewrite_array}; use comment::{FindUncommented, contains_comment}; -use utils::{CodeMapSpanUtils, wrap_str}; +use utils::wrap_str; const FORCED_BRACKET_MACROS: &'static [&'static str] = &["vec!"]; diff --git a/src/patterns.rs b/src/patterns.rs index 08d9db85201..a0b15a0f8c9 100644 --- a/src/patterns.rs +++ b/src/patterns.rs @@ -9,8 +9,9 @@ // except according to those terms. use Indent; +use codemap::SpanUtils; use rewrite::{Rewrite, RewriteContext}; -use utils::{CodeMapSpanUtils, wrap_str, format_mutability}; +use utils::{wrap_str, format_mutability}; use lists::{format_item_list, itemize_list}; use expr::{rewrite_unary_prefix, rewrite_pair, rewrite_tuple}; use types::rewrite_path; diff --git a/src/types.rs b/src/types.rs index 656701b6c78..b1b9b749dd8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -17,9 +17,10 @@ use syntax::codemap::{self, Span, BytePos}; use syntax::abi; use {Indent, Spanned}; +use codemap::SpanUtils; use lists::{format_item_list, itemize_list, format_fn_args}; use rewrite::{Rewrite, RewriteContext}; -use utils::{CodeMapSpanUtils, extra_offset, format_mutability, wrap_str}; +use utils::{extra_offset, format_mutability, wrap_str}; use expr::{rewrite_unary_prefix, rewrite_pair, rewrite_tuple}; use config::TypeDensity; diff --git a/src/utils.rs b/src/utils.rs index 188174e6021..82fbb8d5925 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -14,51 +14,14 @@ use std::cmp::Ordering; use itertools::Itertools; use syntax::ast::{self, Visibility, Attribute, MetaItem, MetaItemKind, Path}; -use syntax::codemap::{CodeMap, Span, BytePos}; +use syntax::codemap::BytePos; use syntax::abi; use Indent; -use comment::FindUncommented; use rewrite::{Rewrite, RewriteContext}; use SKIP_ANNOTATION; -pub trait CodeMapSpanUtils { - fn span_after(&self, original: Span, needle: &str) -> BytePos; - fn span_after_last(&self, original: Span, needle: &str) -> BytePos; - fn span_before(&self, original: Span, needle: &str) -> BytePos; -} - -impl CodeMapSpanUtils for CodeMap { - #[inline] - fn span_after(&self, original: Span, needle: &str) -> BytePos { - let snippet = self.span_to_snippet(original).unwrap(); - let offset = snippet.find_uncommented(needle).unwrap() + needle.len(); - - original.lo + BytePos(offset as u32) - } - - #[inline] - fn span_after_last(&self, original: Span, needle: &str) -> BytePos { - let snippet = self.span_to_snippet(original).unwrap(); - let mut offset = 0; - - while let Some(additional_offset) = snippet[offset..].find_uncommented(needle) { - offset += additional_offset + needle.len(); - } - - original.lo + BytePos(offset as u32) - } - - #[inline] - fn span_before(&self, original: Span, needle: &str) -> BytePos { - let snippet = self.span_to_snippet(original).unwrap(); - let offset = snippet.find_uncommented(needle).unwrap(); - - original.lo + BytePos(offset as u32) - } -} - // Computes the length of a string's last line, minus offset. #[inline] pub fn extra_offset(text: &str, offset: Indent) -> usize { diff --git a/src/visitor.rs b/src/visitor.rs index 45c72e2fdf8..c2ebcd17119 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -15,7 +15,8 @@ use syntax::parse::ParseSess; use strings::string_buffer::StringBuffer; use Indent; -use utils::{self, CodeMapSpanUtils}; +use utils; +use codemap::SpanUtils; use config::Config; use rewrite::{Rewrite, RewriteContext}; use comment::rewrite_comment; From ed27b4799a34607627ea9b2e3c73a693820e4bbf Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Thu, 26 May 2016 13:49:19 +0200 Subject: [PATCH 04/11] codemap: Add utilities for looking up line ranges of spans This commit adds extension methods to `Codemap` to allow looking up line ranges for spans. Refs #434 --- src/codemap.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/codemap.rs b/src/codemap.rs index c70decd8422..43c269e40a9 100644 --- a/src/codemap.rs +++ b/src/codemap.rs @@ -1,13 +1,37 @@ -use syntax::codemap::{BytePos, CodeMap, Span}; +use std::rc::Rc; + +use syntax::codemap::{BytePos, CodeMap, FileMap, Span}; use comment::FindUncommented; +/// A range of lines in a file, inclusive of both ends. +pub struct LineRange { + pub file: Rc, + pub lo: usize, + pub hi: usize, +} + +impl LineRange { + pub fn file_name(&self) -> &str { + self.file.as_ref().name.as_str() + } +} + pub trait SpanUtils { fn span_after(&self, original: Span, needle: &str) -> BytePos; fn span_after_last(&self, original: Span, needle: &str) -> BytePos; fn span_before(&self, original: Span, needle: &str) -> BytePos; } +pub trait LineRangeUtils { + /// Returns the `LineRange` that corresponds to `span` in `self`. + /// + /// # Panics + /// + /// Panics if `span` crosses a file boundary, which shouldn't happen. + fn lookup_line_range(&self, span: Span) -> LineRange; +} + impl SpanUtils for CodeMap { #[inline] fn span_after(&self, original: Span, needle: &str) -> BytePos { @@ -37,3 +61,21 @@ impl SpanUtils for CodeMap { original.lo + BytePos(offset as u32) } } + +impl LineRangeUtils for CodeMap { + fn lookup_line_range(&self, span: Span) -> LineRange { + let lo = self.lookup_char_pos(span.lo); + let hi = self.lookup_char_pos(span.hi); + + assert!(lo.file.name == hi.file.name, + "span crossed file boundary: lo: {:?}, hi: {:?}", + lo, + hi); + + LineRange { + file: lo.file.clone(), + lo: lo.line, + hi: hi.line, + } + } +} From c311b30cacf91e8bfc4b6fb586dc3ec843140e26 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Thu, 26 May 2016 14:17:20 +0200 Subject: [PATCH 05/11] Add type to represent collection of lines in files This commit adds a type to represent lines in files, and adds it to the `Config` struct. It will be used for restricting formatting to specific lines. Refs #434 --- Cargo.lock | 6 ++ Cargo.toml | 1 + src/config.rs | 9 ++ src/file_lines.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 5 files changed, 239 insertions(+) create mode 100644 src/file_lines.rs diff --git a/Cargo.lock b/Cargo.lock index 621659c737f..fc8070979d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,6 +7,7 @@ dependencies = [ "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "multimap 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "strings 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -80,6 +81,11 @@ dependencies = [ "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "multimap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "regex" version = "0.1.71" diff --git a/Cargo.toml b/Cargo.toml index 756fcc600e9..9d8ad68ed8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ log = "0.3" env_logger = "0.3" getopts = "0.2" itertools = "0.4.15" +multimap = "0.3" diff --git a/src/config.rs b/src/config.rs index 8a51b3ecefa..ecb63851417 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ extern crate toml; +use file_lines::FileLines; use lists::{SeparatorTactic, ListTactic}; use std::io::Write; @@ -200,6 +201,12 @@ impl ConfigType for String { } } +impl ConfigType for FileLines { + fn doc_hint() -> String { + String::from("") + } +} + pub struct ConfigHelpItem { option_name: &'static str, doc_string: &'static str, @@ -327,6 +334,8 @@ macro_rules! create_config { create_config! { verbose: bool, false, "Use verbose output"; skip_children: bool, false, "Don't reformat out of line modules"; + file_lines: FileLines, FileLines::all(), + "Lines to format"; max_width: usize, 100, "Maximum width of each line"; ideal_width: usize, 80, "Ideal width of each line"; tab_spaces: usize, 4, "Number of spaces per tab"; diff --git a/src/file_lines.rs b/src/file_lines.rs new file mode 100644 index 00000000000..e437202360d --- /dev/null +++ b/src/file_lines.rs @@ -0,0 +1,221 @@ +//! This module contains types and functions to support formatting specific line ranges. +use std::{cmp, iter, str}; + +use itertools::Itertools; +use multimap::MultiMap; +use rustc_serialize::{self, json}; + +use codemap::LineRange; + +/// A range that is inclusive of both ends. +#[derive(Clone, Copy, Debug, Eq, PartialEq, RustcDecodable)] +struct Range { + pub lo: usize, + pub hi: usize, +} + +impl<'a> From<&'a LineRange> for Range { + fn from(range: &'a LineRange) -> Range { + Range::new(range.lo, range.hi) + } +} + +impl Range { + fn new(lo: usize, hi: usize) -> Range { + Range { lo: lo, hi: hi } + } + + fn is_empty(self) -> bool { + self.lo > self.hi + } + + fn contains(self, other: Range) -> bool { + if other.is_empty() { + true + } else { + !self.is_empty() && self.lo <= other.lo && self.hi >= other.hi + } + } + + fn intersects(self, other: Range) -> bool { + if self.is_empty() || other.is_empty() { + false + } else { + (self.lo <= other.hi && other.hi <= self.hi) || + (other.lo <= self.hi && self.hi <= other.hi) + } + } + + fn adjacent_to(self, other: Range) -> bool { + if self.is_empty() || other.is_empty() { + false + } else { + self.hi + 1 == other.lo || other.hi + 1 == self.lo + } + } + + /// Returns a new `Range` with lines from `self` and `other` if they were adjacent or + /// intersect; returns `None` otherwise. + fn merge(self, other: Range) -> Option { + if self.adjacent_to(other) || self.intersects(other) { + Some(Range::new(cmp::min(self.lo, other.lo), cmp::max(self.hi, other.hi))) + } else { + None + } + } +} + +/// A set of lines in files. +/// +/// It is represented as a multimap keyed on file names, with values a collection of +/// non-overlapping ranges sorted by their start point. An inner `None` is interpreted to mean all +/// lines in all files. +#[derive(Clone, Debug, Default)] +pub struct FileLines(Option>); + +/// Normalizes the ranges so that the invariants for `FileLines` hold: ranges are non-overlapping, +/// and ordered by their start point. +fn normalize_ranges(map: &mut MultiMap) { + for (_, ranges) in map.iter_all_mut() { + ranges.sort_by_key(|x| x.lo); + let merged = ranges.drain(..).coalesce(|x, y| x.merge(y).ok_or((x, y))).collect(); + *ranges = merged; + } +} + +impl FileLines { + /// Creates a `FileLines` that contains all lines in all files. + pub fn all() -> FileLines { + FileLines(None) + } + + /// Creates a `FileLines` from a `MultiMap`, ensuring that the invariants hold. + fn from_multimap(map: MultiMap) -> FileLines { + let mut map = map; + normalize_ranges(&mut map); + FileLines(Some(map)) + } + + /// Returns an iterator over the files contained in `self`. + pub fn files(&self) -> Files { + Files(self.0.as_ref().map(MultiMap::keys)) + } + + /// Returns true if `range` is fully contained in `self`. + pub fn contains(&self, range: &LineRange) -> bool { + let map = match self.0 { + // `None` means "all lines in all files". + None => return true, + Some(ref map) => map, + }; + + match map.get_vec(range.file_name()) { + None => false, + Some(ranges) => ranges.iter().any(|r| r.contains(Range::from(range))), + } + } + + /// Returns true if any lines in `range` are in `self`. + pub fn intersects(&self, range: &LineRange) -> bool { + let map = match self.0 { + // `None` means "all lines in all files". + None => return true, + Some(ref map) => map, + }; + + match map.get_vec(range.file_name()) { + None => false, + Some(ranges) => ranges.iter().any(|r| r.intersects(Range::from(range))), + } + } +} + +/// FileLines files iterator. +pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, String, Vec>>); + +impl<'a> iter::Iterator for Files<'a> { + type Item = &'a String; + + fn next(&mut self) -> Option<&'a String> { + self.0.as_mut().and_then(Iterator::next) + } +} + +// This impl is needed for `Config::override_value` to work for use in tests. +impl str::FromStr for FileLines { + type Err = String; + + fn from_str(s: &str) -> Result { + let v: Vec = try!(json::decode(s).map_err(|e| e.to_string())); + let m = v.into_iter().map(JsonSpan::into_tuple).collect(); + Ok(FileLines::from_multimap(m)) + } +} + +// For JSON decoding. +#[derive(Clone, Debug, RustcDecodable)] +struct JsonSpan { + file: String, + range: (usize, usize), +} + +impl JsonSpan { + // To allow `collect()`ing into a `MultiMap`. + fn into_tuple(self) -> (String, Range) { + let (lo, hi) = self.range; + (self.file, Range::new(lo, hi)) + } +} + +// This impl is needed for inclusion in the `Config` struct. We don't have a toml representation +// for `FileLines`, so it will just panic instead. +impl rustc_serialize::Decodable for FileLines { + fn decode(_: &mut D) -> Result { + unimplemented!(); + } +} + +#[cfg(test)] +mod test { + use super::Range; + + #[test] + fn test_range_intersects() { + assert!(Range::new(1, 2).intersects(Range::new(1, 1))); + assert!(Range::new(1, 2).intersects(Range::new(2, 2))); + assert!(!Range::new(1, 2).intersects(Range::new(0, 0))); + assert!(!Range::new(1, 2).intersects(Range::new(3, 10))); + assert!(!Range::new(1, 3).intersects(Range::new(5, 5))); + } + + #[test] + fn test_range_adjacent_to() { + assert!(!Range::new(1, 2).adjacent_to(Range::new(1, 1))); + assert!(!Range::new(1, 2).adjacent_to(Range::new(2, 2))); + assert!(Range::new(1, 2).adjacent_to(Range::new(0, 0))); + assert!(Range::new(1, 2).adjacent_to(Range::new(3, 10))); + assert!(!Range::new(1, 3).adjacent_to(Range::new(5, 5))); + } + + #[test] + fn test_range_contains() { + assert!(Range::new(1, 2).contains(Range::new(1, 1))); + assert!(Range::new(1, 2).contains(Range::new(2, 2))); + assert!(!Range::new(1, 2).contains(Range::new(0, 0))); + assert!(!Range::new(1, 2).contains(Range::new(3, 10))); + } + + #[test] + fn test_range_merge() { + assert_eq!(None, Range::new(1, 3).merge(Range::new(5, 5))); + assert_eq!(None, Range::new(4, 7).merge(Range::new(0, 1))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(4, 7))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(5, 7))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(6, 7))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 7).merge(Range::new(4, 5))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8df2c069bcc..322eed374de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ extern crate regex; extern crate diff; extern crate term; extern crate itertools; +extern crate multimap; use syntax::ast; use syntax::codemap::{mk_sp, CodeMap, Span}; @@ -54,6 +55,7 @@ mod utils; pub mod config; pub mod codemap; pub mod filemap; +pub mod file_lines; pub mod visitor; mod checkstyle; mod items; From 9fa5a91fc50a5b7a7c2980e36e1bf63df3179500 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Fri, 11 Mar 2016 19:19:16 -0500 Subject: [PATCH 06/11] visitor: Handle specified line ranges in visit_stmt This commit adds a very rough implementation of handling the specified line ranges in `config.file_lines_map` for statements. It reformats a statement if its span is fully contained in the set of lines specified for the file. The implementation here is intended as a proof of concept, and demonstration that the machinery added in the preceding commits is functional. A final implementation would likely hook in via the `Rewrite` trait. Refs #434 --- src/visitor.rs | 7 ++++++- tests/source/file-lines-1.rs | 29 +++++++++++++++++++++++++++++ tests/source/file-lines-2.rs | 29 +++++++++++++++++++++++++++++ tests/source/file-lines-3.rs | 29 +++++++++++++++++++++++++++++ tests/source/file-lines-4.rs | 30 ++++++++++++++++++++++++++++++ tests/target/file-lines-1.rs | 30 ++++++++++++++++++++++++++++++ tests/target/file-lines-2.rs | 24 ++++++++++++++++++++++++ tests/target/file-lines-3.rs | 25 +++++++++++++++++++++++++ tests/target/file-lines-4.rs | 30 ++++++++++++++++++++++++++++++ 9 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 tests/source/file-lines-1.rs create mode 100644 tests/source/file-lines-2.rs create mode 100644 tests/source/file-lines-3.rs create mode 100644 tests/source/file-lines-4.rs create mode 100644 tests/target/file-lines-1.rs create mode 100644 tests/target/file-lines-2.rs create mode 100644 tests/target/file-lines-3.rs create mode 100644 tests/target/file-lines-4.rs diff --git a/src/visitor.rs b/src/visitor.rs index c2ebcd17119..48b5e0ff7d2 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -16,7 +16,7 @@ use strings::string_buffer::StringBuffer; use Indent; use utils; -use codemap::SpanUtils; +use codemap::{LineRangeUtils, SpanUtils}; use config::Config; use rewrite::{Rewrite, RewriteContext}; use comment::rewrite_comment; @@ -47,6 +47,11 @@ impl<'a> FmtVisitor<'a> { self.codemap.lookup_char_pos(stmt.span.lo), self.codemap.lookup_char_pos(stmt.span.hi)); + // FIXME(#434): Move this check to somewhere more central, eg Rewrite. + if !self.config.file_lines.contains(&self.codemap.lookup_line_range(stmt.span)) { + return; + } + match stmt.node { ast::StmtKind::Decl(ref decl, _) => { if let ast::DeclKind::Item(ref item) = decl.node { diff --git a/tests/source/file-lines-1.rs b/tests/source/file-lines-1.rs new file mode 100644 index 00000000000..43c18d8f46f --- /dev/null +++ b/tests/source/file-lines-1.rs @@ -0,0 +1,29 @@ +// rustfmt-file_lines: [{"file":"tests/source/file-lines-1.rs","range":[4,8]}] + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call().method_call(); + + let y = if cond { + val1 + } else { + val2 + } + .method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} diff --git a/tests/source/file-lines-2.rs b/tests/source/file-lines-2.rs new file mode 100644 index 00000000000..6f8e9e6db3a --- /dev/null +++ b/tests/source/file-lines-2.rs @@ -0,0 +1,29 @@ +// rustfmt-file_lines: [{"file":"tests/source/file-lines-2.rs","range":[10,15]}] + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call().method_call(); + + let y = if cond { + val1 + } else { + val2 + } + .method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} diff --git a/tests/source/file-lines-3.rs b/tests/source/file-lines-3.rs new file mode 100644 index 00000000000..73d482695d6 --- /dev/null +++ b/tests/source/file-lines-3.rs @@ -0,0 +1,29 @@ +// rustfmt-file_lines: [{"file":"tests/source/file-lines-3.rs","range":[4,8]},{"file":"tests/source/file-lines-3.rs","range":[10,15]}] + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call().method_call(); + + let y = if cond { + val1 + } else { + val2 + } + .method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} diff --git a/tests/source/file-lines-4.rs b/tests/source/file-lines-4.rs new file mode 100644 index 00000000000..381f021515f --- /dev/null +++ b/tests/source/file-lines-4.rs @@ -0,0 +1,30 @@ +// rustfmt-file_lines: [] +// (Test that nothing is formatted if an empty array is specified.) + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call().method_call(); + + let y = if cond { + val1 + } else { + val2 + } + .method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} diff --git a/tests/target/file-lines-1.rs b/tests/target/file-lines-1.rs new file mode 100644 index 00000000000..5b8478e0303 --- /dev/null +++ b/tests/target/file-lines-1.rs @@ -0,0 +1,30 @@ +// rustfmt-file_lines: [{"file":"tests/source/file-lines-1.rs","range":[4,8]}] + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call() + .method_call(); + + let y = if cond { + val1 + } else { + val2 + } + .method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} diff --git a/tests/target/file-lines-2.rs b/tests/target/file-lines-2.rs new file mode 100644 index 00000000000..db4902d49b7 --- /dev/null +++ b/tests/target/file-lines-2.rs @@ -0,0 +1,24 @@ +// rustfmt-file_lines: [{"file":"tests/source/file-lines-2.rs","range":[10,15]}] + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call().method_call(); + + let y = if cond { val1 } else { val2 }.method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} diff --git a/tests/target/file-lines-3.rs b/tests/target/file-lines-3.rs new file mode 100644 index 00000000000..19359b2ddf9 --- /dev/null +++ b/tests/target/file-lines-3.rs @@ -0,0 +1,25 @@ +// rustfmt-file_lines: [{"file":"tests/source/file-lines-3.rs","range":[4,8]},{"file":"tests/source/file-lines-3.rs","range":[10,15]}] + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call() + .method_call(); + + let y = if cond { val1 } else { val2 }.method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} diff --git a/tests/target/file-lines-4.rs b/tests/target/file-lines-4.rs new file mode 100644 index 00000000000..381f021515f --- /dev/null +++ b/tests/target/file-lines-4.rs @@ -0,0 +1,30 @@ +// rustfmt-file_lines: [] +// (Test that nothing is formatted if an empty array is specified.) + +fn floaters() { + let x = Foo { + field1: val1, + field2: val2, + } + .method_call().method_call(); + + let y = if cond { + val1 + } else { + val2 + } + .method_call(); + + { + match x { + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_owned()), + }] + .clone()); + } + } + } +} From bef5d095a4c59577d1400d980b7da62519bbe473 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Sun, 10 Apr 2016 13:03:54 -0400 Subject: [PATCH 07/11] rustfmt: Add option to specify line ranges for formatting This commit adds the `--experimental-file-lines` option to rustfmt. This allows specifying line ranges to format from the command line. Refs #434 --- README.md | 19 +++++++++++++++++++ src/bin/rustfmt.rs | 26 +++++++++++++++++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 60b29956218..4f0714a9997 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,25 @@ the command line. For example `rustfmt --write-mode=display src/filename.rs` `cargo fmt` uses `--write-mode=replace` by default. +If you want to restrict reformatting to specific sets of lines, you can +use the `--file-lines` option. Its argument is a JSON array of objects +with `file` and `range` properties, where `file` is a file name, and +`range` is an array representing a range of lines like `[7,13]`. Ranges +are inclusive of both end points. Specifying an empty array will result in +no files being formatted. For example, + +``` +rustfmt --file-lines '[ + {"file":"src/lib.rs","range":[7,13]}, + {"file":"src/lib.rs","range":[21,29]}, + {"file":"src/foo.rs","range":[10,11]}, + {"file":"src/foo.rs","range":[15,15]}]' +``` + +would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`, +and `15` of `src/foo.rs`. No other files would be formatted, even if they +are included as out of line modules from `src/lib.rs`. + If `rustfmt` successfully reformatted the code it will exit with `0` exit status. Exit status `1` signals some unexpected error, like an unknown option or a failure to read a file. Exit status `2` is returned if there are syntax errors diff --git a/src/bin/rustfmt.rs b/src/bin/rustfmt.rs index 2105a962997..5aa7cc4e080 100644 --- a/src/bin/rustfmt.rs +++ b/src/bin/rustfmt.rs @@ -18,6 +18,7 @@ extern crate env_logger; extern crate getopts; use rustfmt::{run, Input, Summary}; +use rustfmt::file_lines::FileLines; use rustfmt::config::{Config, WriteMode}; use std::{env, error}; @@ -57,6 +58,7 @@ struct CliOptions { skip_children: bool, verbose: bool, write_mode: Option, + file_lines: FileLines, // Default is all lines in all files. } impl CliOptions { @@ -73,12 +75,17 @@ impl CliOptions { } } + if let Some(ref file_lines) = matches.opt_str("file-lines") { + options.file_lines = try!(file_lines.parse()); + } + Ok(options) } - fn apply_to(&self, config: &mut Config) { + fn apply_to(self, config: &mut Config) { config.skip_children = self.skip_children; config.verbose = self.verbose; + config.file_lines = self.file_lines; if let Some(write_mode) = self.write_mode { config.write_mode = write_mode; } @@ -168,6 +175,10 @@ fn make_opts() -> Options { "Recursively searches the given path for the rustfmt.toml config file. If not \ found reverts to the input file path", "[Path for the configuration file]"); + opts.optopt("", + "file-lines", + "Format specified line ranges. See README for more detail on the JSON format.", + "JSON"); opts } @@ -198,8 +209,12 @@ fn execute(opts: &Options) -> FmtResult { Ok(run(Input::Text(input), &config)) } - Operation::Format { files, config_path } => { + Operation::Format { mut files, config_path } => { let options = try!(CliOptions::from_matches(&matches)); + + // Add any additional files that were specified via `--file-lines`. + files.extend(options.file_lines.files().cloned().map(PathBuf::from)); + let mut config = Config::default(); let mut path = None; // Load the config path file if provided @@ -227,7 +242,7 @@ fn execute(opts: &Options) -> FmtResult { config = config_tmp; } - options.apply_to(&mut config); + options.clone().apply_to(&mut config); error_summary.add(run(Input::File(file), &config)); } Ok(error_summary) @@ -306,8 +321,8 @@ fn determine_operation(matches: &Matches) -> FmtResult { Some(dir) }); - // if no file argument is supplied, read from stdin - if matches.free.is_empty() { + // if no file argument is supplied and `--file-lines` is not specified, read from stdin + if matches.free.is_empty() && !matches.opt_present("file-lines") { let mut buffer = String::new(); try!(io::stdin().read_to_string(&mut buffer)); @@ -318,6 +333,7 @@ fn determine_operation(matches: &Matches) -> FmtResult { }); } + // We append files from `--file-lines` later in `execute()`. let files: Vec<_> = matches.free.iter().map(PathBuf::from).collect(); Ok(Operation::Format { From a83f1a197e852820f0587ed39bf66bb79bda4707 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Mon, 30 May 2016 16:10:12 +0200 Subject: [PATCH 08/11] Add copyright notices to added files --- src/codemap.rs | 10 ++++++++++ src/file_lines.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/codemap.rs b/src/codemap.rs index 43c269e40a9..666bc67944d 100644 --- a/src/codemap.rs +++ b/src/codemap.rs @@ -1,3 +1,13 @@ +// Copyright 2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + use std::rc::Rc; use syntax::codemap::{BytePos, CodeMap, FileMap, Span}; diff --git a/src/file_lines.rs b/src/file_lines.rs index e437202360d..9c60821a3bb 100644 --- a/src/file_lines.rs +++ b/src/file_lines.rs @@ -1,3 +1,13 @@ +// Copyright 2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + //! This module contains types and functions to support formatting specific line ranges. use std::{cmp, iter, str}; From 8775fe417280413f37f8a22377404d2c870d3e83 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Mon, 30 May 2016 16:10:26 +0200 Subject: [PATCH 09/11] codemap: Add module description --- src/codemap.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/codemap.rs b/src/codemap.rs index 666bc67944d..9ec5c663064 100644 --- a/src/codemap.rs +++ b/src/codemap.rs @@ -8,6 +8,9 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +//! This module contains utilities that work with the `CodeMap` from libsyntax / syntex_syntax. +//! This includes extension traits and methods for looking up spans and line ranges for AST nodes. + use std::rc::Rc; use syntax::codemap::{BytePos, CodeMap, FileMap, Span}; From 66483017f66b37b2c585cdfae4df59e89df41e42 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Mon, 30 May 2016 16:41:43 +0200 Subject: [PATCH 10/11] Explain that FileLines cannot be given in rustfmt.toml This adds a note to both the `--config-help` output for `file_lines`, and to the panic message on attempting to deserialize a `FileLines` struct. --- src/config.rs | 3 ++- src/file_lines.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index ecb63851417..51daa7a505a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -335,7 +335,8 @@ create_config! { verbose: bool, false, "Use verbose output"; skip_children: bool, false, "Don't reformat out of line modules"; file_lines: FileLines, FileLines::all(), - "Lines to format"; + "Lines to format; this is not supported in rustfmt.toml, and can only be specified \ + via the --file-lines option"; max_width: usize, 100, "Maximum width of each line"; ideal_width: usize, 80, "Ideal width of each line"; tab_spaces: usize, 4, "Number of spaces per tab"; diff --git a/src/file_lines.rs b/src/file_lines.rs index 9c60821a3bb..28fedad4cfd 100644 --- a/src/file_lines.rs +++ b/src/file_lines.rs @@ -181,7 +181,8 @@ impl JsonSpan { // for `FileLines`, so it will just panic instead. impl rustc_serialize::Decodable for FileLines { fn decode(_: &mut D) -> Result { - unimplemented!(); + panic!("FileLines cannot be deserialized from a project rustfmt.toml file: please \ + specify it via the `--file-lines` option instead"); } } From e252100cf67ae2afe8d4d9687dcafe7a4fbb8f72 Mon Sep 17 00:00:00 2001 From: Kamal Marhubi Date: Tue, 31 May 2016 00:42:14 +0200 Subject: [PATCH 11/11] README: Explain that --file-lines ranges are 1-based --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f0714a9997..5d9f262d659 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ If you want to restrict reformatting to specific sets of lines, you can use the `--file-lines` option. Its argument is a JSON array of objects with `file` and `range` properties, where `file` is a file name, and `range` is an array representing a range of lines like `[7,13]`. Ranges -are inclusive of both end points. Specifying an empty array will result in -no files being formatted. For example, +are 1-based and inclusive of both end points. Specifying an empty array +will result in no files being formatted. For example, ``` rustfmt --file-lines '[