Skip to content

Attempt at checking for license (#209) #2456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 8, 2018
20 changes: 20 additions & 0 deletions Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
28 changes: 24 additions & 4 deletions src/config/config_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Regex>,
// 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,
Expand Down Expand Up @@ -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(),
&_ => (),
}
}
)+
Expand Down Expand Up @@ -189,6 +194,7 @@ macro_rules! create_config {
}
)+
self.set_heuristics();
self.set_license_template();
self
}

Expand Down Expand Up @@ -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(),
&_ => (),
}
}

Expand Down Expand Up @@ -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(&lt_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),
)+
Expand Down
267 changes: 267 additions & 0 deletions src/config/license.rs
Original file line number Diff line number Diff line change
@@ -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<io::Error> for LicenseError {
fn from(err: io::Error) -> LicenseError {
LicenseError::IO(err)
}
}

impl From<regex::Error> 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 <LICENSE-APACHE or
/// // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
/// // <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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 <LICENSE\-APACHE or
/// // http://www\.apache\.org/licenses/LICENSE\-2\.0> or the MIT license
/// // <LICENSE\-MIT or http://opensource\.org/licenses/MIT>, at your
/// // option\. This file may not be copied, modified, or distributed
/// // except according to those terms\.
/// "
/// );
/// ```
pub fn parse(template: &str) -> Result<String, LicenseError> {
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(&regex::escape(&parser.buffer));

Ok(parser.parsed)
}

fn trans_from_lit(&mut self, chr: char) -> ParsingState {
match chr {
'{' => {
self.parsed.push_str(&regex::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<Regex, LicenseError> {
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(&lt_str)?;
Ok(Regex::new(&lt_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"
);
}
}
4 changes: 4 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading