1#![deprecated(since = "0.1.1", note = "please use the `version-sync` crate instead")]
2extern crate pulldown_cmark;
3extern crate toml;
4extern crate semver_parser;
5
6use std::fs::File;
7use std::io::Read;
8use std::result;
9
10use pulldown_cmark::{Parser, Event, Tag};
11use semver_parser::range::parse as parse_request;
12use semver_parser::range::{VersionReq, Op};
13use semver_parser::version::Version;
14use semver_parser::version::parse as parse_version;
15use toml::Value;
16
17type Result<T> = result::Result<T, String>;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22struct CodeBlock<'a> {
23 content: &'a str,
25 first_line: usize,
27}
28
29impl<'a> CodeBlock<'a> {
30 fn new(text: &'a str, start: usize, end: usize) -> CodeBlock {
35 let last_nl = match text[..end - 1].rfind('\n') {
39 Some(i) => i + 1,
40 None => start,
41 };
42 let first_line = 1 + text[..start].lines().count();
43 CodeBlock {
44 content: &text[start..last_nl],
45 first_line: first_line,
46 }
47 }
48}
49
50fn read_file(path: &str) -> std::io::Result<String> {
52 let mut file = File::open(path)?;
53 let mut buf = String::new();
54 file.read_to_string(&mut buf)?;
55 Ok(buf)
56}
57
58fn indent(text: &str) -> String {
60 text.lines()
61 .map(|line| String::from(" ") + line)
62 .collect::<Vec<_>>()
63 .join("\n")
64}
65
66fn version_matches_request(version: &Version, request: &VersionReq) -> Result<()> {
68 if request.predicates.len() != 1 {
69 return Ok(());
71 }
72
73 let pred = &request.predicates[0];
74 match pred.op {
75 Op::Tilde | Op::Compatible => {
76 if pred.major != version.major {
77 return Err(format!(
78 "expected major version {}, found {}",
79 version.major,
80 pred.major,
81 ));
82 }
83 if let Some(minor) = pred.minor {
84 if minor != version.minor {
85 return Err(format!("expected minor version {}, found {}",
86 version.minor,
87 minor));
88 }
89 }
90 if let Some(patch) = pred.patch {
91 if patch != version.patch {
92 return Err(format!("expected patch version {}, found {}",
93 version.patch,
94 patch));
95 }
96 }
97 }
98 _ => return Ok(()), }
100
101 Ok(())
102}
103
104fn extract_version_request(pkg_name: &str, block: &str) -> Result<VersionReq> {
106 match block.parse::<Value>() {
107 Ok(value) => {
108 let version = value
109 .get("dependencies")
110 .or_else(|| value.get("dev-dependencies"))
111 .and_then(|deps| deps.get(pkg_name))
112 .and_then(|dep| dep.get("version").or_else(|| Some(dep)))
113 .and_then(|version| version.as_str());
114 match version {
115 Some(version) => {
116 parse_request(version)
117 .map_err(|err| format!("could not parse dependency: {}", err))
118 }
119 None => Err(format!("no dependency on {}", pkg_name)),
120 }
121 }
122 Err(err) => Err(format!("TOML parse error: {}", err)),
123 }
124}
125
126fn is_toml_block(lang: &str) -> bool {
128 lang.split(|c: char| !(c == '_' || c == '-' || c.is_alphanumeric()))
131 .any(|token| token.trim() == "toml")
132}
133
134fn find_toml_blocks(text: &str) -> Vec<CodeBlock> {
136 let mut parser = Parser::new(text);
137 let mut code_blocks = Vec::new();
138 let mut start = 0;
139 while let Some(event) = parser.next() {
143 match event {
144 Event::Start(Tag::CodeBlock(_)) => {
145 start = parser.get_offset();
146 }
147 Event::End(Tag::CodeBlock(lang)) => {
148 if is_toml_block(&lang) {
150 let end = parser.get_offset();
151 code_blocks.push(CodeBlock::new(text, start, end));
152 }
153 }
154 _ => {}
155 }
156 }
157
158 code_blocks
159}
160
161pub fn check_markdown_deps(path: &str, pkg_name: &str, pkg_version: &str) -> Result<()> {
174 let text = read_file(path)
175 .map_err(|err| format!("could not read {}: {}", path, err))?;
176 let version = parse_version(pkg_version)
177 .map_err(|err| format!("bad package version {:?}: {}", pkg_version, err))?;
178
179 println!("Checking code blocks in {}...", path);
180 let mut failed = false;
181 for block in find_toml_blocks(&text) {
182 let result = extract_version_request(pkg_name, block.content)
183 .and_then(|request| version_matches_request(&version, &request));
184 match result {
185 Err(err) => {
186 failed = true;
187 println!("{} (line {}) ... {} in", path, block.first_line, err);
188 println!("{}\n", indent(block.content));
189 }
190 Ok(()) => println!("{} (line {}) ... ok", path, block.first_line),
191 }
192 }
193
194 if failed {
195 return Err(format!("dependency errors in {}", path));
196 }
197 Ok(())
198}
199
200#[macro_export]
238macro_rules! assert_markdown_deps_updated {
239 ($path:expr) => {
240 let pkg_name = env!("CARGO_PKG_NAME");
241 let pkg_version = env!("CARGO_PKG_VERSION");
242 if let Err(err) = $crate::check_markdown_deps($path, pkg_name, pkg_version) {
243 panic!(err);
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn code_block_new() {
254 let text = "Preceding text.\n\
255 ```\n\
256 foo\n\
257 ```\n\
258 Trailing text";
259 let start = text.find("```\n").unwrap() + 4;
260 let end = text.rfind("```\n").unwrap() + 4;
261 assert_eq!(CodeBlock::new(text, start, end),
262 CodeBlock { content: "foo\n", first_line: 3 });
263 }
264
265 #[test]
266 fn is_toml_block_simple() {
267 assert!(!is_toml_block("rust"))
268 }
269
270 #[test]
271 fn is_toml_block_comma() {
272 assert!(is_toml_block("foo,toml"))
273 }
274
275 mod test_version_matches_request {
276 use super::*;
277
278 #[test]
279 fn implicit_compatible() {
280 let version = parse_version("1.2.3").unwrap();
281 let request = parse_request("1.2.3").unwrap();
282 assert_eq!(version_matches_request(&version, &request), Ok(()));
283 }
284
285 #[test]
286 fn compatible() {
287 let version = parse_version("1.2.3").unwrap();
288 let request = parse_request("^1.2.3").unwrap();
289 assert_eq!(version_matches_request(&version, &request), Ok(()));
290 }
291
292 #[test]
293 fn tilde() {
294 let version = parse_version("1.2.3").unwrap();
295 let request = parse_request("~1.2.3").unwrap();
296 assert_eq!(version_matches_request(&version, &request), Ok(()));
297 }
298
299 #[test]
300 fn no_patch() {
301 let version = parse_version("1.2.3").unwrap();
302 let request = parse_request("1.2").unwrap();
303 assert_eq!(version_matches_request(&version, &request), Ok(()));
304 }
305
306 #[test]
307 fn no_minor() {
308 let version = parse_version("1.2.3").unwrap();
309 let request = parse_request("1").unwrap();
310 assert_eq!(version_matches_request(&version, &request), Ok(()));
311 }
312
313 #[test]
314 fn multiple_predicates() {
315 let version = parse_version("1.2.3").unwrap();
316 let request = parse_request(">= 1.2.3, < 2.0").unwrap();
317 assert_eq!(version_matches_request(&version, &request), Ok(()));
318 }
319
320 #[test]
321 fn unhandled_operator() {
322 let version = parse_version("1.2.3").unwrap();
323 let request = parse_request("< 2.0").unwrap();
324 assert_eq!(version_matches_request(&version, &request), Ok(()));
325 }
326
327 #[test]
328 fn bad_major() {
329 let version = parse_version("2.0.0").unwrap();
330 let request = parse_request("1.2.3").unwrap();
331 assert_eq!(version_matches_request(&version, &request),
332 Err(String::from("expected major version 2, found 1")));
333 }
334
335 #[test]
336 fn bad_minor() {
337 let version = parse_version("1.3.0").unwrap();
338 let request = parse_request("1.2.3").unwrap();
339 assert_eq!(version_matches_request(&version, &request),
340 Err(String::from("expected minor version 3, found 2")));
341 }
342
343 #[test]
344 fn bad_patch() {
345 let version = parse_version("1.2.4").unwrap();
346 let request = parse_request("1.2.3").unwrap();
347 assert_eq!(version_matches_request(&version, &request),
348 Err(String::from("expected patch version 4, found 3")));
349 }
350 }
351
352 mod test_extract_version_request {
353 use super::*;
354
355 #[test]
356 fn simple() {
357 let block = "[dependencies]\n\
358 foobar = '1.5'";
359 let request = extract_version_request("foobar", block);
360 assert_eq!(request.unwrap().predicates,
361 parse_request("1.5").unwrap().predicates);
362 }
363
364 #[test]
365 fn table() {
366 let block = "[dependencies]\n\
367 foobar = { version = '1.5', default-features = false }";
368 let request = extract_version_request("foobar", block);
369 assert_eq!(request.unwrap().predicates,
370 parse_request("1.5").unwrap().predicates);
371 }
372
373 #[test]
374 fn dev_dependencies() {
375 let block = "[dev-dependencies]\n\
376 foobar = '1.5'";
377 let request = extract_version_request("foobar", block);
378 assert_eq!(request.unwrap().predicates,
379 parse_request("1.5").unwrap().predicates);
380 }
381
382 #[test]
383 fn bad_version() {
384 let block = "[dependencies]\n\
385 foobar = '1.5.bad'";
386 let request = extract_version_request("foobar", block);
387 assert_eq!(request.unwrap_err(),
388 "could not parse dependency: Extra junk after valid predicate: .bad");
389 }
390
391 #[test]
392 fn missing_dependency() {
393 let block = "[dependencies]\n\
394 baz = '1.5.8'";
395 let request = extract_version_request("foobar", block);
396 assert_eq!(request.unwrap_err(), "no dependency on foobar");
397 }
398
399 #[test]
400 fn empty() {
401 let request = extract_version_request("foobar", "");
402 assert_eq!(request.unwrap_err(), "no dependency on foobar");
403 }
404
405 #[test]
406 fn bad_toml() {
407 let block = "[dependencies]\n\
408 foobar = 1.5.8";
409 let request = extract_version_request("foobar", block);
410 assert_eq!(request.unwrap_err(),
411 "TOML parse error: expected newline, found a period at line 2");
412 }
413 }
414
415 mod test_find_toml_blocks {
416 use super::*;
417
418 #[test]
419 fn empty() {
420 assert_eq!(find_toml_blocks(""), vec![]);
421 }
422
423 #[test]
424 fn indented_block() {
425 assert_eq!(find_toml_blocks(" code block\n"), vec![]);
426 }
427
428 #[test]
429 fn single() {
430 assert_eq!(find_toml_blocks("```toml\n```"),
431 vec![CodeBlock { content: "", first_line: 2 }]);
432 }
433
434 #[test]
435 fn no_close_fence() {
436 assert_eq!(find_toml_blocks("```toml\n"),
437 vec![CodeBlock { content: "", first_line: 2 }]);
438 }
439 }
440
441 mod test_check_markdown_deps {
442 use super::*;
443
444 #[test]
445 fn bad_path() {
446 let no_such_file = if cfg!(unix) {
447 "No such file or directory (os error 2)"
448 } else {
449 "The system cannot find the file specified. (os error 2)"
450 };
451 let errmsg = format!("could not read no-such-file.md: {}", no_such_file);
452 assert_eq!(check_markdown_deps("no-such-file.md", "foobar", "1.2.3"),
453 Err(errmsg));
454 }
455
456 #[test]
457 fn bad_pkg_version() {
458 assert_eq!(check_markdown_deps("README.md", "foobar", "1.2"),
460 Err(String::from("bad package version \"1.2\": \
461 Expected dot")));
462 }
463 }
464}