1#![cfg(feature = "contains_regex")]
2use regex::{escape, Regex, RegexBuilder};
3use semver::{Version, VersionReq};
4
5use crate::helpers::{read_file, version_matches_request, Result};
6
7const SEMVER_RE: &str = concat!(
9 r"(?P<major>0|[1-9]\d*)",
10 r"(?:\.(?P<minor>0|[1-9]\d*)",
11 r"(?:\.(?P<patch>0|[1-9]\d*)",
12 r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)",
13 r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?",
14 r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?",
15 r")?", r")?", );
18
19pub fn check_contains_regex(
40 path: &str,
41 template: &str,
42 pkg_name: &str,
43 pkg_version: &str,
44) -> Result<()> {
45 let pattern = template
47 .replace("{name}", &escape(pkg_name))
48 .replace("{version}", &escape(pkg_version));
49 let mut builder = RegexBuilder::new(&pattern);
50 builder.multi_line(true);
51 let re = builder
52 .build()
53 .map_err(|err| format!("could not parse template: {}", err))?;
54 let text = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?;
55
56 println!("Searching for \"{pattern}\" in {path}...");
57 match re.find(&text) {
58 Some(m) => {
59 let line_no = text[..m.start()].lines().count();
60 println!("{} (line {}) ... ok", path, line_no + 1);
61 Ok(())
62 }
63 None => Err(format!("could not find \"{pattern}\" in {path}")),
64 }
65}
66
67pub fn check_only_contains_regex(
100 path: &str,
101 template: &str,
102 pkg_name: &str,
103 pkg_version: &str,
104) -> Result<()> {
105 let version = Version::parse(pkg_version)
106 .map_err(|err| format!("bad package version {:?}: {}", pkg_version, err))?;
107
108 let pattern = template
109 .replace("{name}", &escape(pkg_name))
110 .replace("{version}", SEMVER_RE);
111 let re = RegexBuilder::new(&pattern)
112 .multi_line(true)
113 .build()
114 .map_err(|err| format!("could not parse template: {}", err))?;
115
116 let semver_re = Regex::new(SEMVER_RE).unwrap();
117
118 let text = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?;
119
120 println!("Searching for \"{template}\" in {path}...");
121 let mut errors = 0;
122 let mut has_match = false;
123
124 for m in re.find_iter(&text) {
125 has_match = true;
126 let line_no = text[..m.start()].lines().count() + 1;
127
128 for semver in semver_re.find_iter(m.as_str()) {
129 let semver_request = VersionReq::parse(semver.as_str())
130 .map_err(|err| format!("could not parse version: {}", err))?;
131 let result = version_matches_request(&version, &semver_request);
132 match result {
133 Err(err) => {
134 errors += 1;
135 println!(
136 "{} (line {}) ... found \"{}\", which does not match version \"{}\": {}",
137 path,
138 line_no,
139 semver.as_str(),
140 pkg_version,
141 err
142 );
143 }
144 Ok(()) => {
145 println!("{path} (line {line_no}) ... ok");
146 }
147 }
148 }
149 }
150
151 if !has_match {
152 return Err(format!("{path} ... found no matches for \"{template}\""));
153 }
154
155 if errors > 0 {
156 return Err(format!("{path} ... found {errors} errors"));
157 }
158
159 Ok(())
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::io::Write;
166
167 #[test]
168 fn bad_regex() {
169 assert_eq!(
172 check_contains_regex("README.md", "Version {version} [ups", "foobar", "1.2.3"),
173 Err([
174 r"could not parse template: regex parse error:",
175 r" Version 1\.2\.3 [ups",
176 r" ^",
177 r"error: unclosed character class"
178 ]
179 .join("\n"))
180 )
181 }
182
183 #[test]
184 fn not_found() {
185 assert_eq!(
186 check_contains_regex("README.md", "should not be found", "foobar", "1.2.3"),
187 Err(String::from(
188 "could not find \"should not be found\" in README.md"
189 ))
190 )
191 }
192
193 #[test]
194 fn escaping() {
195 assert_eq!(
196 check_contains_regex(
197 "README.md",
198 "escaped: {name}-{version}, not escaped: foo*bar-1.2.3",
199 "foo*bar",
200 "1.2.3"
201 ),
202 Err([
203 r#"could not find "escaped: foo\*bar-1\.2\.3,"#,
204 r#"not escaped: foo*bar-1.2.3" in README.md"#
205 ]
206 .join(" "))
207 )
208 }
209
210 #[test]
211 fn good_pattern() {
212 assert_eq!(
213 check_contains_regex("README.md", "{name}", "version-sync", "1.2.3"),
214 Ok(())
215 )
216 }
217
218 #[test]
219 fn line_boundaries() {
220 let mut file = tempfile::NamedTempFile::new().unwrap();
225
226 file.write_all(b"first line\r\nsecond line\r\nthird line\r\n")
227 .unwrap();
228 assert_eq!(
229 check_contains_regex(file.path().to_str().unwrap(), "^second line$", "", ""),
230 Ok(())
231 )
232 }
233
234 #[test]
235 fn semver_regex() {
236 let re = Regex::new(&format!("^{SEMVER_RE}$")).unwrap();
241 assert!(re.is_match("1.2.3"));
242 assert!(re.is_match("1.2"));
243 assert!(re.is_match("1"));
244 assert!(re.is_match("1.2.3-foo.bar.baz.42+build123.2021.12.11"));
245 assert!(!re.is_match("01"));
246 assert!(!re.is_match("01.02.03"));
247 }
248
249 #[test]
250 fn only_contains_success() {
251 let mut file = tempfile::NamedTempFile::new().unwrap();
252 file.write_all(
253 b"first: docs.rs/foo/1.2.3/foo/fn.bar.html
254 second: docs.rs/foo/1.2.3/foo/fn.baz.html",
255 )
256 .unwrap();
257
258 assert_eq!(
259 check_only_contains_regex(
260 file.path().to_str().unwrap(),
261 "docs.rs/{name}/{version}/{name}/",
262 "foo",
263 "1.2.3"
264 ),
265 Ok(())
266 )
267 }
268
269 #[test]
270 fn only_contains_success_compatible() {
271 let mut file = tempfile::NamedTempFile::new().unwrap();
272 file.write_all(
273 b"first: docs.rs/foo/1.2/foo/fn.bar.html
274 second: docs.rs/foo/1/foo/fn.baz.html",
275 )
276 .unwrap();
277
278 assert_eq!(
279 check_only_contains_regex(
280 file.path().to_str().unwrap(),
281 "docs.rs/{name}/{version}/{name}/",
282 "foo",
283 "1.2.3"
284 ),
285 Ok(())
286 )
287 }
288
289 #[test]
290 fn only_contains_failure() {
291 let mut file = tempfile::NamedTempFile::new().unwrap();
292 file.write_all(
293 b"first: docs.rs/foo/1.0.0/foo/ <- error
294 second: docs.rs/foo/2.0.0/foo/ <- ok
295 third: docs.rs/foo/3.0.0/foo/ <- error",
296 )
297 .unwrap();
298
299 assert_eq!(
300 check_only_contains_regex(
301 file.path().to_str().unwrap(),
302 "docs.rs/{name}/{version}/{name}/",
303 "foo",
304 "2.0.0"
305 ),
306 Err(format!("{} ... found 2 errors", file.path().display()))
307 )
308 }
309
310 #[test]
311 fn only_contains_fails_if_no_match() {
312 let mut file = tempfile::NamedTempFile::new().unwrap();
313 file.write_all(b"not a match").unwrap();
314
315 assert_eq!(
316 check_only_contains_regex(
317 file.path().to_str().unwrap(),
318 "docs.rs/{name}/{version}/{name}/",
319 "foo",
320 "1.2.3"
321 ),
322 Err(format!(
323 r#"{} ... found no matches for "docs.rs/{{name}}/{{version}}/{{name}}/""#,
324 file.path().display()
325 ))
326 );
327 }
328}