#![cfg(feature = "contains_regex")]
use regex::{escape, Regex, RegexBuilder};
use semver::{Version, VersionReq};
use crate::helpers::{read_file, version_matches_request, Result};
const SEMVER_RE: &str = concat!(
r"(?P<major>0|[1-9]\d*)",
r"(?:\.(?P<minor>0|[1-9]\d*)",
r"(?:\.(?P<patch>0|[1-9]\d*)",
r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)",
r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?",
r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?",
r")?", r")?", );
pub fn check_contains_regex(
path: &str,
template: &str,
pkg_name: &str,
pkg_version: &str,
) -> Result<()> {
let pattern = template
.replace("{name}", &escape(pkg_name))
.replace("{version}", &escape(pkg_version));
let mut builder = RegexBuilder::new(&pattern);
builder.multi_line(true);
let re = builder
.build()
.map_err(|err| format!("could not parse template: {}", err))?;
let text = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?;
println!("Searching for \"{pattern}\" in {path}...");
match re.find(&text) {
Some(m) => {
let line_no = text[..m.start()].lines().count();
println!("{} (line {}) ... ok", path, line_no + 1);
Ok(())
}
None => Err(format!("could not find \"{pattern}\" in {path}")),
}
}
pub fn check_only_contains_regex(
path: &str,
template: &str,
pkg_name: &str,
pkg_version: &str,
) -> Result<()> {
let version = Version::parse(pkg_version)
.map_err(|err| format!("bad package version {:?}: {}", pkg_version, err))?;
let pattern = template
.replace("{name}", &escape(pkg_name))
.replace("{version}", SEMVER_RE);
let re = RegexBuilder::new(&pattern)
.multi_line(true)
.build()
.map_err(|err| format!("could not parse template: {}", err))?;
let semver_re = Regex::new(SEMVER_RE).unwrap();
let text = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?;
println!("Searching for \"{template}\" in {path}...");
let mut errors = 0;
let mut has_match = false;
for m in re.find_iter(&text) {
has_match = true;
let line_no = text[..m.start()].lines().count() + 1;
for semver in semver_re.find_iter(m.as_str()) {
let semver_request = VersionReq::parse(semver.as_str())
.map_err(|err| format!("could not parse version: {}", err))?;
let result = version_matches_request(&version, &semver_request);
match result {
Err(err) => {
errors += 1;
println!(
"{} (line {}) ... found \"{}\", which does not match version \"{}\": {}",
path,
line_no,
semver.as_str(),
pkg_version,
err
);
}
Ok(()) => {
println!("{path} (line {line_no}) ... ok");
}
}
}
}
if !has_match {
return Err(format!("{path} ... found no matches for \"{template}\""));
}
if errors > 0 {
return Err(format!("{path} ... found {errors} errors"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn bad_regex() {
assert_eq!(
check_contains_regex("README.md", "Version {version} [ups", "foobar", "1.2.3"),
Err([
r"could not parse template: regex parse error:",
r" Version 1\.2\.3 [ups",
r" ^",
r"error: unclosed character class"
]
.join("\n"))
)
}
#[test]
fn not_found() {
assert_eq!(
check_contains_regex("README.md", "should not be found", "foobar", "1.2.3"),
Err(String::from(
"could not find \"should not be found\" in README.md"
))
)
}
#[test]
fn escaping() {
assert_eq!(
check_contains_regex(
"README.md",
"escaped: {name}-{version}, not escaped: foo*bar-1.2.3",
"foo*bar",
"1.2.3"
),
Err([
r#"could not find "escaped: foo\*bar-1\.2\.3,"#,
r#"not escaped: foo*bar-1.2.3" in README.md"#
]
.join(" "))
)
}
#[test]
fn good_pattern() {
assert_eq!(
check_contains_regex("README.md", "{name}", "version-sync", "1.2.3"),
Ok(())
)
}
#[test]
fn line_boundaries() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(b"first line\r\nsecond line\r\nthird line\r\n")
.unwrap();
assert_eq!(
check_contains_regex(file.path().to_str().unwrap(), "^second line$", "", ""),
Ok(())
)
}
#[test]
fn semver_regex() {
let re = Regex::new(&format!("^{SEMVER_RE}$")).unwrap();
assert!(re.is_match("1.2.3"));
assert!(re.is_match("1.2"));
assert!(re.is_match("1"));
assert!(re.is_match("1.2.3-foo.bar.baz.42+build123.2021.12.11"));
assert!(!re.is_match("01"));
assert!(!re.is_match("01.02.03"));
}
#[test]
fn only_contains_success() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(
b"first: docs.rs/foo/1.2.3/foo/fn.bar.html
second: docs.rs/foo/1.2.3/foo/fn.baz.html",
)
.unwrap();
assert_eq!(
check_only_contains_regex(
file.path().to_str().unwrap(),
"docs.rs/{name}/{version}/{name}/",
"foo",
"1.2.3"
),
Ok(())
)
}
#[test]
fn only_contains_success_compatible() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(
b"first: docs.rs/foo/1.2/foo/fn.bar.html
second: docs.rs/foo/1/foo/fn.baz.html",
)
.unwrap();
assert_eq!(
check_only_contains_regex(
file.path().to_str().unwrap(),
"docs.rs/{name}/{version}/{name}/",
"foo",
"1.2.3"
),
Ok(())
)
}
#[test]
fn only_contains_failure() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(
b"first: docs.rs/foo/1.0.0/foo/ <- error
second: docs.rs/foo/2.0.0/foo/ <- ok
third: docs.rs/foo/3.0.0/foo/ <- error",
)
.unwrap();
assert_eq!(
check_only_contains_regex(
file.path().to_str().unwrap(),
"docs.rs/{name}/{version}/{name}/",
"foo",
"2.0.0"
),
Err(format!("{} ... found 2 errors", file.path().display()))
)
}
#[test]
fn only_contains_fails_if_no_match() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(b"not a match").unwrap();
assert_eq!(
check_only_contains_regex(
file.path().to_str().unwrap(),
"docs.rs/{name}/{version}/{name}/",
"foo",
"1.2.3"
),
Err(format!(
r#"{} ... found no matches for "docs.rs/{{name}}/{{version}}/{{name}}/""#,
file.path().display()
))
);
}
}