diff --git a/Configurations.md b/Configurations.md index 324546f5a0d..8540bc36383 100644 --- a/Configurations.md +++ b/Configurations.md @@ -2115,3 +2115,23 @@ Enable unstable featuers on stable channel. - **Default value**: `false` - **Possible values**: `true`, `false` - **Stable**: Yes + +## `license_template_path` + +Check whether beginnings of files match a license template. + +- **Default value**: `""`` +- **Possible values**: path to a license template file +- **Stable**: No + +A license template is a plain text file which is matched literally against the +beginning of each source file, except for `{}`-delimited blocks, which are +matched as regular expressions. The following license template therefore +matches strings like `// Copyright 2017 The Rust Project Developers.`, `// +Copyright 2018 The Rust Project Developers.`, etc.: + +``` +// Copyright {\d+} The Rust Project Developers. +``` + +`\{`, `\}` and `\\` match literal braces / backslashes. diff --git a/src/config/config_type.rs b/src/config/config_type.rs index 950225679a5..dc768490fbb 100644 --- a/src/config/config_type.rs +++ b/src/config/config_type.rs @@ -78,6 +78,9 @@ macro_rules! create_config { #[derive(Clone)] pub struct Config { + // if a license_template_path has been specified, successfully read, parsed and compiled + // into a regex, it will be stored here + pub license_template: Option, // For each config item, we store a bool indicating whether it has // been accessed and the value, and a bool whether the option was // manually initialised, or taken from the default, @@ -118,8 +121,10 @@ macro_rules! create_config { $( pub fn $i(&mut self, value: $ty) { (self.0).$i.2 = value; - if stringify!($i) == "use_small_heuristics" { - self.0.set_heuristics(); + match stringify!($i) { + "use_small_heuristics" => self.0.set_heuristics(), + "license_template_path" => self.0.set_license_template(), + &_ => (), } } )+ @@ -189,6 +194,7 @@ macro_rules! create_config { } )+ self.set_heuristics(); + self.set_license_template(); self } @@ -276,8 +282,10 @@ macro_rules! create_config { _ => panic!("Unknown config key in override: {}", key) } - if key == "use_small_heuristics" { - self.set_heuristics(); + match key { + "use_small_heuristics" => self.set_heuristics(), + "license_template_path" => self.set_license_template(), + &_ => (), } } @@ -382,12 +390,24 @@ macro_rules! create_config { self.set().width_heuristics(WidthHeuristics::null()); } } + + fn set_license_template(&mut self) { + if self.was_set().license_template_path() { + let lt_path = self.license_template_path(); + match license::load_and_compile_template(<_path) { + Ok(re) => self.license_template = Some(re), + Err(msg) => eprintln!("Warning for license template file {:?}: {}", + lt_path, msg), + } + } + } } // Template for the default configuration impl Default for Config { fn default() -> Config { Config { + license_template: None, $( $i: (Cell::new(false), false, $def, $stb), )+ diff --git a/src/config/license.rs b/src/config/license.rs new file mode 100644 index 00000000000..b2babd5ac19 --- /dev/null +++ b/src/config/license.rs @@ -0,0 +1,267 @@ +use std::io; +use std::fmt; +use std::fs::File; +use std::io::Read; + +use regex; +use regex::Regex; + +#[derive(Debug)] +pub enum LicenseError { + IO(io::Error), + Regex(regex::Error), + Parse(String), +} + +impl fmt::Display for LicenseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + LicenseError::IO(ref err) => err.fmt(f), + LicenseError::Regex(ref err) => err.fmt(f), + LicenseError::Parse(ref err) => write!(f, "parsing failed, {}", err), + } + } +} + +impl From for LicenseError { + fn from(err: io::Error) -> LicenseError { + LicenseError::IO(err) + } +} + +impl From for LicenseError { + fn from(err: regex::Error) -> LicenseError { + LicenseError::Regex(err) + } +} + +// the template is parsed using a state machine +enum ParsingState { + Lit, + LitEsc, + // the u32 keeps track of brace nesting + Re(u32), + ReEsc(u32), + Abort(String), +} + +use self::ParsingState::*; + +pub struct TemplateParser { + parsed: String, + buffer: String, + state: ParsingState, + linum: u32, + open_brace_line: u32, +} + +impl TemplateParser { + fn new() -> Self { + Self { + parsed: "^".to_owned(), + buffer: String::new(), + state: Lit, + linum: 1, + // keeps track of last line on which a regex placeholder was started + open_brace_line: 0, + } + } + + /// Convert a license template into a string which can be turned into a regex. + /// + /// The license template could use regex syntax directly, but that would require a lot of manual + /// escaping, which is inconvenient. It is therefore literal by default, with optional regex + /// subparts delimited by `{` and `}`. Additionally: + /// + /// - to insert literal `{`, `}` or `\`, escape it with `\` + /// - an empty regex placeholder (`{}`) is shorthand for `{.*?}` + /// + /// This function parses this input format and builds a properly escaped *string* representation + /// of the equivalent regular expression. It **does not** however guarantee that the returned + /// string is a syntactically valid regular expression. + /// + /// # Examples + /// + /// ``` + /// # use rustfmt_nightly::config::license::TemplateParser; + /// assert_eq!( + /// TemplateParser::parse( + /// r" + /// // Copyright {\d+} The \} Rust \\ Project \{ Developers. See the {([A-Z]+)} + /// // file at the top-level directory of this distribution and at + /// // {}. + /// // + /// // 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. + /// " + /// ).unwrap(), + /// r"^ + /// // Copyright \d+ The \} Rust \\ Project \{ Developers\. See the ([A-Z]+) + /// // file at the top\-level directory of this distribution and at + /// // .*?\. + /// // + /// // 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\. + /// " + /// ); + /// ``` + pub fn parse(template: &str) -> Result { + let mut parser = Self::new(); + for chr in template.chars() { + if chr == '\n' { + parser.linum += 1; + } + parser.state = match parser.state { + Lit => parser.trans_from_lit(chr), + LitEsc => parser.trans_from_litesc(chr), + Re(brace_nesting) => parser.trans_from_re(chr, brace_nesting), + ReEsc(brace_nesting) => parser.trans_from_reesc(chr, brace_nesting), + Abort(msg) => return Err(LicenseError::Parse(msg)), + }; + } + // check if we've ended parsing in a valid state + match parser.state { + Abort(msg) => return Err(LicenseError::Parse(msg)), + Re(_) | ReEsc(_) => { + return Err(LicenseError::Parse(format!( + "escape or balance opening brace on l. {}", + parser.open_brace_line + ))); + } + LitEsc => { + return Err(LicenseError::Parse(format!( + "incomplete escape sequence on l. {}", + parser.linum + ))) + } + _ => (), + } + parser.parsed.push_str(®ex::escape(&parser.buffer)); + + Ok(parser.parsed) + } + + fn trans_from_lit(&mut self, chr: char) -> ParsingState { + match chr { + '{' => { + self.parsed.push_str(®ex::escape(&self.buffer)); + self.buffer.clear(); + self.open_brace_line = self.linum; + Re(1) + } + '}' => Abort(format!( + "escape or balance closing brace on l. {}", + self.linum + )), + '\\' => LitEsc, + _ => { + self.buffer.push(chr); + Lit + } + } + } + + fn trans_from_litesc(&mut self, chr: char) -> ParsingState { + self.buffer.push(chr); + Lit + } + + fn trans_from_re(&mut self, chr: char, brace_nesting: u32) -> ParsingState { + match chr { + '{' => { + self.buffer.push(chr); + Re(brace_nesting + 1) + } + '}' => { + match brace_nesting { + 1 => { + // default regex for empty placeholder {} + if self.buffer.is_empty() { + self.parsed.push_str(".*?"); + } else { + self.parsed.push_str(&self.buffer); + } + self.buffer.clear(); + Lit + } + _ => { + self.buffer.push(chr); + Re(brace_nesting - 1) + } + } + } + '\\' => { + self.buffer.push(chr); + ReEsc(brace_nesting) + } + _ => { + self.buffer.push(chr); + Re(brace_nesting) + } + } + } + + fn trans_from_reesc(&mut self, chr: char, brace_nesting: u32) -> ParsingState { + self.buffer.push(chr); + Re(brace_nesting) + } +} + +pub fn load_and_compile_template(path: &str) -> Result { + let mut lt_file = File::open(&path)?; + let mut lt_str = String::new(); + lt_file.read_to_string(&mut lt_str)?; + let lt_parsed = TemplateParser::parse(<_str)?; + Ok(Regex::new(<_parsed)?) +} + +#[cfg(test)] +mod test { + use super::TemplateParser; + + #[test] + fn test_parse_license_template() { + assert_eq!( + TemplateParser::parse("literal (.*)").unwrap(), + r"^literal \(\.\*\)" + ); + assert_eq!( + TemplateParser::parse(r"escaping \}").unwrap(), + r"^escaping \}" + ); + assert!(TemplateParser::parse("unbalanced } without escape").is_err()); + assert_eq!( + TemplateParser::parse(r"{\d+} place{-?}holder{s?}").unwrap(), + r"^\d+ place-?holders?" + ); + assert_eq!(TemplateParser::parse("default {}").unwrap(), "^default .*?"); + assert_eq!( + TemplateParser::parse(r"unbalanced nested braces {\{{3}}").unwrap(), + r"^unbalanced nested braces \{{3}" + ); + assert_eq!( + &TemplateParser::parse("parsing error }") + .unwrap_err() + .to_string(), + "parsing failed, escape or balance closing brace on l. 1" + ); + assert_eq!( + &TemplateParser::parse("parsing error {\nsecond line") + .unwrap_err() + .to_string(), + "parsing failed, escape or balance opening brace on l. 1" + ); + assert_eq!( + &TemplateParser::parse(r"parsing error \") + .unwrap_err() + .to_string(), + "parsing failed, incomplete escape sequence on l. 1" + ); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 7e9fae81318..0d4ec8557d3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -15,6 +15,8 @@ use std::fs::File; use std::io::{Error, ErrorKind, Read}; use std::path::{Path, PathBuf}; +use regex::Regex; + #[macro_use] mod config_type; #[macro_use] @@ -23,6 +25,7 @@ mod options; pub mod file_lines; pub mod lists; pub mod summary; +pub mod license; use config::config_type::ConfigType; use config::file_lines::FileLines; @@ -50,6 +53,7 @@ create_config! { comment_width: usize, 80, false, "Maximum length of comments. No effect unless wrap_comments = true"; normalize_comments: bool, false, true, "Convert /* */ comments to // comments where possible"; + license_template_path: String, String::default(), false, "Beginning of file must match license template"; // Single line expressions and items. empty_item_single_line: bool, true, false, diff --git a/src/lib.rs b/src/lib.rs index a3302a86a0f..e53f8bfb907 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,8 @@ pub enum ErrorKind { TrailingWhitespace, // TO-DO or FIX-ME item without an issue number BadIssue(Issue), + // License check has failed + LicenseCheck, } impl fmt::Display for ErrorKind { @@ -111,6 +113,7 @@ impl fmt::Display for ErrorKind { ), ErrorKind::TrailingWhitespace => write!(fmt, "left behind trailing whitespace"), ErrorKind::BadIssue(issue) => write!(fmt, "found {}", issue), + ErrorKind::LicenseCheck => write!(fmt, "license check failed"), } } } @@ -127,7 +130,9 @@ pub struct FormattingError { impl FormattingError { fn msg_prefix(&self) -> &str { match self.kind { - ErrorKind::LineOverflow(..) | ErrorKind::TrailingWhitespace => "error:", + ErrorKind::LineOverflow(..) + | ErrorKind::TrailingWhitespace + | ErrorKind::LicenseCheck => "error:", ErrorKind::BadIssue(_) => "WARNING:", } } @@ -406,7 +411,6 @@ fn should_report_error( } // Formatting done on a char by char or line by line basis. -// FIXME(#209) warn on bad license // FIXME(#20) other stuff for parity with make tidy fn format_lines( text: &mut String, @@ -415,7 +419,6 @@ fn format_lines( config: &Config, report: &mut FormatReport, ) { - // Iterate over the chars in the file map. let mut trims = vec![]; let mut last_wspace: Option = None; let mut line_len = 0; @@ -428,6 +431,20 @@ fn format_lines( let mut format_line = config.file_lines().contains_line(name, cur_line); let allow_issue_seek = !issue_seeker.is_disabled(); + // Check license. + if let Some(ref license_template) = config.license_template { + if !license_template.is_match(text) { + errors.push(FormattingError { + line: cur_line, + kind: ErrorKind::LicenseCheck, + is_comment: false, + is_string: false, + line_buffer: String::new(), + }); + } + } + + // Iterate over the chars in the file map. for (kind, (b, c)) in CharClasses::new(text.chars().enumerate()) { if c == '\r' { continue;