]> git.lizzy.rs Git - rust.git/blobdiff - src/lib.rs
Trigger an internal error if we skip formatting due to a lost comment
[rust.git] / src / lib.rs
index 1048e295d2af99a282bbb73943ae798f6fa64654..820134c2085d4e6efaf50ddfefbf03282d4a2f47 100644 (file)
 #![allow(unused_attributes)]
 #![feature(type_ascription)]
 #![feature(unicode_internals)]
+#![feature(extern_prelude)]
 
 #[macro_use]
 extern crate derive_new;
 extern crate diff;
-#[macro_use]
 extern crate failure;
+extern crate isatty;
 extern crate itertools;
 #[cfg(test)]
 #[macro_use]
 extern crate serde_derive;
 extern crate serde_json;
 extern crate syntax;
-extern crate term;
+extern crate syntax_pos;
 extern crate toml;
 extern crate unicode_segmentation;
 
 use std::cell::RefCell;
 use std::collections::HashMap;
 use std::fmt;
-use std::io::{self, stdout, Write};
+use std::io::{self, Write};
 use std::panic::{catch_unwind, AssertUnwindSafe};
 use std::path::PathBuf;
 use std::rc::Rc;
 use failure::Fail;
 use issues::{BadIssueSeeker, Issue};
 use shape::Indent;
-use utils::use_colored_tty;
 use visitor::{FmtVisitor, SnippetProvider};
 
+pub use checkstyle::{footer as checkstyle_footer, header as checkstyle_header};
 pub use config::summary::Summary;
 pub use config::{
-    load_config, CliOptions, Color, Config, EmitMode, FileLines, FileName, Verbosity,
+    load_config, CliOptions, Color, Config, EmitMode, FileLines, FileName, NewlineStyle, Range,
+    Verbosity,
 };
 
 #[macro_use]
@@ -84,6 +86,7 @@
 mod missed_spans;
 pub(crate) mod modules;
 mod overflow;
+mod pairs;
 mod patterns;
 mod reorder;
 mod rewrite;
 
 pub(crate) type FileRecord = (FileName, String);
 
+/// The various errors that can occur during formatting. Note that not all of
+/// these can currently be propagated to clients.
 #[derive(Fail, Debug)]
 pub enum ErrorKind {
-    // Line has exceeded character limit (found, maximum)
+    /// Line has exceeded character limit (found, maximum).
     #[fail(
         display = "line formatted, but exceeded maximum width \
                    (maximum: {} (see `max_width` option), found: {})",
@@ -112,23 +117,43 @@ pub enum ErrorKind {
         _1
     )]
     LineOverflow(usize, usize),
-    // Line ends in whitespace
+    /// Line ends in whitespace.
     #[fail(display = "left behind trailing whitespace")]
     TrailingWhitespace,
-    // TODO or FIXME item without an issue number
+    /// TODO or FIXME item without an issue number.
     #[fail(display = "found {}", _0)]
     BadIssue(Issue),
-    // License check has failed
+    /// License check has failed.
     #[fail(display = "license check failed")]
     LicenseCheck,
-    // Used deprecated skip attribute
+    /// Used deprecated skip attribute.
     #[fail(display = "`rustfmt_skip` is deprecated; use `rustfmt::skip`")]
     DeprecatedAttr,
-    // Used a rustfmt:: attribute other than skip
+    /// Used a rustfmt:: attribute other than skip.
     #[fail(display = "invalid attribute")]
     BadAttr,
+    /// An io error during reading or writing.
     #[fail(display = "io error: {}", _0)]
     IoError(io::Error),
+    /// Parse error occured when parsing the Input.
+    #[fail(display = "parse error")]
+    ParseError,
+    /// The user mandated a version and the current version of Rustfmt does not
+    /// satisfy that requirement.
+    #[fail(display = "version mismatch")]
+    VersionMismatch,
+    /// If we had formatted the given node, then we would have lost a comment.
+    #[fail(display = "not formatted because a comment would be lost")]
+    LostComment,
+}
+
+impl ErrorKind {
+    fn is_comment(&self) -> bool {
+        match self {
+            ErrorKind::LostComment => true,
+            _ => false,
+        }
+    }
 }
 
 impl From<io::Error> for ErrorKind {
@@ -149,8 +174,8 @@ impl FormattingError {
     fn from_span(span: &Span, codemap: &CodeMap, kind: ErrorKind) -> FormattingError {
         FormattingError {
             line: codemap.lookup_char_pos(span.lo()).line,
+            is_comment: kind.is_comment(),
             kind,
-            is_comment: false,
             is_string: false,
             line_buffer: codemap
                 .span_to_lines(*span)
@@ -165,10 +190,12 @@ fn from_span(span: &Span, codemap: &CodeMap, kind: ErrorKind) -> FormattingError
     }
     fn msg_prefix(&self) -> &str {
         match self.kind {
-            ErrorKind::LineOverflow(..) | ErrorKind::TrailingWhitespace | ErrorKind::IoError(_) => {
-                "internal error:"
-            }
-            ErrorKind::LicenseCheck | ErrorKind::BadAttr => "error:",
+            ErrorKind::LineOverflow(..)
+            | ErrorKind::TrailingWhitespace
+            | ErrorKind::IoError(_)
+            | ErrorKind::ParseError
+            | ErrorKind::LostComment => "internal error:",
+            ErrorKind::LicenseCheck | ErrorKind::BadAttr | ErrorKind::VersionMismatch => "error:",
             ErrorKind::BadIssue(_) | ErrorKind::DeprecatedAttr => "warning:",
         }
     }
@@ -186,7 +213,10 @@ fn msg_suffix(&self) -> &str {
     fn format_len(&self) -> (usize, usize) {
         match self.kind {
             ErrorKind::LineOverflow(found, max) => (max, found - max),
-            ErrorKind::TrailingWhitespace | ErrorKind::DeprecatedAttr | ErrorKind::BadAttr => {
+            ErrorKind::TrailingWhitespace
+            | ErrorKind::DeprecatedAttr
+            | ErrorKind::BadAttr
+            | ErrorKind::LostComment => {
                 let trailing_ws_start = self
                     .line_buffer
                     .rfind(|c: char| !c.is_whitespace())
@@ -202,8 +232,11 @@ fn format_len(&self) -> (usize, usize) {
     }
 }
 
+/// Reports on any issues that occurred during a run of Rustfmt.
+///
+/// Can be reported to the user via its `Display` implementation of `print_fancy`.
 #[derive(Clone)]
-struct FormatReport {
+pub struct FormatReport {
     // Maps stringified file paths to their associated formatting errors.
     internal: Rc<RefCell<(FormatErrorMap, ReportedErrors)>>,
 }
@@ -246,7 +279,8 @@ fn track_errors(&self, new_errors: &[FormattingError]) {
                 ErrorKind::BadIssue(_)
                 | ErrorKind::LicenseCheck
                 | ErrorKind::DeprecatedAttr
-                | ErrorKind::BadAttr => {
+                | ErrorKind::BadAttr
+                | ErrorKind::VersionMismatch => {
                     errs.has_check_errors = true;
                 }
                 _ => {}
@@ -263,11 +297,14 @@ fn warning_count(&self) -> usize {
             .sum()
     }
 
-    fn has_warnings(&self) -> bool {
+    /// Whether any warnings or errors are present in the report.
+    pub fn has_warnings(&self) -> bool {
         self.warning_count() > 0
     }
 
-    fn print_warnings_fancy(
+    /// Print the report to a terminal using colours and potentially other
+    /// fancy output.
+    pub fn fancy_print(
         &self,
         mut t: Box<term::Terminal<Output = io::Stderr>>,
     ) -> Result<(), term::Error> {
@@ -408,13 +445,14 @@ fn format_ast<F>(
     config: &Config,
     report: FormatReport,
     mut after_file: F,
-) -> Result<(FileMap, bool), io::Error>
+) -> Result<(FileMap, bool, bool), io::Error>
 where
     F: FnMut(&FileName, &mut String, &[(usize, usize)], &FormatReport) -> Result<bool, io::Error>,
 {
     let mut result = FileMap::new();
     // diff mode: check if any files are differing
     let mut has_diff = false;
+    let mut has_macro_rewrite_failure = false;
 
     let skip_children = config.skip_children();
     for (path, module) in modules::list_files(krate, parse_session.codemap())? {
@@ -458,10 +496,12 @@ fn format_ast<F>(
             }
         };
 
+        has_macro_rewrite_failure |= visitor.macro_rewrite_failure;
+
         result.push((path.clone(), visitor.buffer));
     }
 
-    Ok((result, has_diff))
+    Ok((result, has_diff, has_macro_rewrite_failure))
 }
 
 /// Returns true if the line with the given line number was skipped by `#[rustfmt::skip]`.
@@ -477,7 +517,7 @@ fn should_report_error(
     is_string: bool,
     error_kind: &ErrorKind,
 ) -> bool {
-    let allow_error_report = if char_kind.is_comment() || is_string {
+    let allow_error_report = if char_kind.is_comment() || is_string || error_kind.is_comment() {
         config.error_on_unformatted()
     } else {
         true
@@ -485,7 +525,7 @@ fn should_report_error(
 
     match error_kind {
         ErrorKind::LineOverflow(..) => config.error_on_line_overflow() && allow_error_report,
-        ErrorKind::TrailingWhitespace => allow_error_report,
+        ErrorKind::TrailingWhitespace | ErrorKind::LostComment => allow_error_report,
         _ => true,
     }
 }
@@ -499,8 +539,7 @@ fn format_lines(
     config: &Config,
     report: &FormatReport,
 ) {
-    let mut trims = vec![];
-    let mut last_wspace: Option<usize> = None;
+    let mut last_was_space = false;
     let mut line_len = 0;
     let mut cur_line = 1;
     let mut newline_count = 0;
@@ -525,7 +564,7 @@ fn format_lines(
     }
 
     // Iterate over the chars in the file map.
-    for (kind, (b, c)) in CharClasses::new(text.chars().enumerate()) {
+    for (kind, c) in CharClasses::new(text.chars()) {
         if c == '\r' {
             continue;
         }
@@ -546,10 +585,17 @@ fn format_lines(
         if c == '\n' {
             if format_line {
                 // Check for (and record) trailing whitespace.
-                if let Some(..) = last_wspace {
+                if last_was_space {
                     if should_report_error(config, kind, is_string, &ErrorKind::TrailingWhitespace)
+                        && !is_skipped_line(cur_line, skipped_range)
                     {
-                        trims.push((cur_line, kind, line_buffer.clone()));
+                        errors.push(FormattingError {
+                            line: cur_line,
+                            kind: ErrorKind::TrailingWhitespace,
+                            is_comment: kind.is_comment(),
+                            is_string: kind.is_string(),
+                            line_buffer: line_buffer.clone(),
+                        });
                     }
                     line_len -= 1;
                 }
@@ -574,19 +620,13 @@ fn format_lines(
             cur_line += 1;
             format_line = config.file_lines().contains_line(name, cur_line);
             newline_count += 1;
-            last_wspace = None;
+            last_was_space = false;
             line_buffer.clear();
             is_string = false;
         } else {
             newline_count = 0;
             line_len += if c == '\t' { config.tab_spaces() } else { 1 };
-            if c.is_whitespace() {
-                if last_wspace.is_none() {
-                    last_wspace = Some(b);
-                }
-            } else {
-                last_wspace = None;
-            }
+            last_was_space = c.is_whitespace();
             line_buffer.push(c);
             if kind.is_string() {
                 is_string = true;
@@ -600,18 +640,6 @@ fn format_lines(
         text.truncate(line);
     }
 
-    for &(l, kind, ref b) in &trims {
-        if !is_skipped_line(l, skipped_range) {
-            errors.push(FormattingError {
-                line: l,
-                kind: ErrorKind::TrailingWhitespace,
-                is_comment: kind.is_comment(),
-                is_string: kind.is_string(),
-                line_buffer: b.clone(),
-            });
-        }
-    }
-
     report.append(name.clone(), errors);
 }
 
@@ -672,6 +700,7 @@ fn format_snippet(snippet: &str, config: &Config) -> Option<String> {
     config.set().hide_parse_errors(true);
     match format_input(input, &config, Some(&mut out)) {
         // `format_input()` returns an empty string on parsing error.
+        Ok((summary, _)) if summary.has_macro_formatting_failure() => None,
         Ok(..) if out.is_empty() && !snippet.is_empty() => None,
         Ok(..) => String::from_utf8(out).ok(),
         Err(..) => None,
@@ -707,10 +736,18 @@ fn format_code_block(code_snippet: &str, config: &Config) -> Option<String> {
     let mut result = String::with_capacity(snippet.len());
     let mut is_first = true;
 
+    // While formatting the code, ignore the config's newline style setting and always use "\n"
+    // instead of "\r\n" for the newline characters. This is okay because the output here is
+    // not directly outputted by rustfmt command, but used by the comment formatter's input.
+    // We have output-file-wide "\n" ==> "\r\n" conversion proccess after here if it's necessary.
+    let mut config_with_unix_newline = config.clone();
+    config_with_unix_newline
+        .set()
+        .newline_style(NewlineStyle::Unix);
+    let formatted = format_snippet(&snippet, &config_with_unix_newline)?;
+
     // Trim "fn main() {" on the first line and "}" on the last line,
     // then unindent the whole code block.
-    let formatted = format_snippet(&snippet, config)?;
-    // 2 = "}\n"
     let block_len = formatted.rfind('}').unwrap_or(formatted.len());
     let mut is_indented = true;
     for (kind, ref line) in LineClasses::new(&formatted[FN_MAIN_PREFIX.len()..block_len]) {
@@ -749,12 +786,24 @@ fn format_code_block(code_snippet: &str, config: &Config) -> Option<String> {
     Some(result)
 }
 
+#[derive(Debug)]
+pub enum Input {
+    File(PathBuf),
+    Text(String),
+}
+
+/// The main entry point for Rustfmt. Formats the given input according to the
+/// given config. `out` is only necessary if required by the configuration.
 pub fn format_input<T: Write>(
     input: Input,
     config: &Config,
     out: Option<&mut T>,
-) -> Result<Summary, (ErrorKind, Summary)> {
-    syntax::with_globals(|| format_input_inner(input, config, out)).map(|tup| tup.0)
+) -> Result<(Summary, FormatReport), (ErrorKind, Summary)> {
+    if !config.version_meets_requirement() {
+        return Err((ErrorKind::VersionMismatch, Summary::default()));
+    }
+
+    syntax::with_globals(|| format_input_inner(input, config, out)).map(|tup| (tup.0, tup.2))
 }
 
 fn format_input_inner<T: Write>(
@@ -762,6 +811,7 @@ fn format_input_inner<T: Write>(
     config: &Config,
     mut out: Option<&mut T>,
 ) -> Result<(Summary, FileMap, FormatReport), (ErrorKind, Summary)> {
+    syntax_pos::hygiene::set_default_edition(config.edition().to_libsyntax_pos_edition());
     let mut summary = Summary::default();
     if config.disable_all_formatting() {
         // When the input is from stdin, echo back the input.
@@ -814,7 +864,7 @@ fn format_input_inner<T: Write>(
                 ParseError::Recovered => {}
             }
             summary.add_parsing_error();
-            return Ok((summary, FileMap::new(), FormatReport::new()));
+            return Err((ErrorKind::ParseError, summary));
         }
     };
 
@@ -843,6 +893,7 @@ fn format_input_inner<T: Write>(
             filemap::append_newline(file);
 
             format_lines(file, file_name, skipped_range, config, report);
+            replace_with_system_newlines(file, config);
 
             if let Some(ref mut out) = out {
                 return filemap::write_file(file, file_name, out, config);
@@ -876,7 +927,7 @@ fn duration_to_f32(d: Duration) -> f32 {
     }
 
     match format_result {
-        Ok((file_map, has_diff)) => {
+        Ok((file_map, has_diff, has_macro_rewrite_failure)) => {
             if report.has_warnings() {
                 summary.add_formatting_error();
             }
@@ -885,12 +936,44 @@ fn duration_to_f32(d: Duration) -> f32 {
                 summary.add_diff();
             }
 
+            if has_macro_rewrite_failure {
+                summary.add_macro_foramt_failure();
+            }
+
             Ok((summary, file_map, report))
         }
         Err(e) => Err((From::from(e), summary)),
     }
 }
 
+fn replace_with_system_newlines(text: &mut String, config: &Config) -> () {
+    let style = if config.newline_style() == NewlineStyle::Native {
+        if cfg!(windows) {
+            NewlineStyle::Windows
+        } else {
+            NewlineStyle::Unix
+        }
+    } else {
+        config.newline_style()
+    };
+
+    match style {
+        NewlineStyle::Unix => return,
+        NewlineStyle::Windows => {
+            let mut transformed = String::with_capacity(text.capacity());
+            for c in text.chars() {
+                match c {
+                    '\n' => transformed.push_str("\r\n"),
+                    '\r' => continue,
+                    c => transformed.push(c),
+                }
+            }
+            *text = transformed;
+        }
+        NewlineStyle::Native => unreachable!(),
+    }
+}
+
 /// 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)]
@@ -950,61 +1033,6 @@ fn get_modified_lines(
     Ok(ModifiedLines { chunks })
 }
 
-#[derive(Debug)]
-pub enum Input {
-    File(PathBuf),
-    Text(String),
-}
-
-pub fn format_and_emit_report(input: Input, config: &Config) -> Result<Summary, failure::Error> {
-    if !config.version_meets_requirement() {
-        return Err(format_err!("Version mismatch"));
-    }
-    let out = &mut stdout();
-    match syntax::with_globals(|| format_input_inner(input, config, Some(out))) {
-        Ok((summary, _, report)) => {
-            if report.has_warnings() {
-                match term::stderr() {
-                    Some(ref t)
-                        if use_colored_tty(config.color())
-                            && t.supports_color()
-                            && t.supports_attr(term::Attr::Bold) =>
-                    {
-                        match report.print_warnings_fancy(term::stderr().unwrap()) {
-                            Ok(..) => (),
-                            Err(..) => panic!("Unable to write to stderr: {}", report),
-                        }
-                    }
-                    _ => eprintln!("{}", report),
-                }
-            }
-
-            Ok(summary)
-        }
-        Err((msg, mut summary)) => {
-            eprintln!("Error writing files: {}", msg);
-            summary.add_operational_error();
-            Ok(summary)
-        }
-    }
-}
-
-pub fn emit_pre_matter(config: &Config) -> Result<(), ErrorKind> {
-    if config.emit_mode() == EmitMode::Checkstyle {
-        let mut out = &mut stdout();
-        checkstyle::output_header(&mut out)?;
-    }
-    Ok(())
-}
-
-pub fn emit_post_matter(config: &Config) -> Result<(), ErrorKind> {
-    if config.emit_mode() == EmitMode::Checkstyle {
-        let mut out = &mut stdout();
-        checkstyle::output_footer(&mut out)?;
-    }
-    Ok(())
-}
-
 #[cfg(test)]
 mod unit_tests {
     use super::{format_code_block, format_snippet, Config};
@@ -1029,9 +1057,14 @@ fn test_format_inner<F>(formatter: F, input: &str, expected: &str) -> bool
     #[test]
     fn test_format_snippet() {
         let snippet = "fn main() { println!(\"hello, world\"); }";
+        #[cfg(not(windows))]
         let expected = "fn main() {\n    \
                         println!(\"hello, world\");\n\
                         }\n";
+        #[cfg(windows)]
+        let expected = "fn main() {\r\n    \
+                        println!(\"hello, world\");\r\n\
+                        }\r\n";
         assert!(test_format_inner(format_snippet, snippet, expected));
     }