use semver_parser::range::parse as parse_request;
use semver_parser::version::parse as parse_version;
use semver_parser::version::Version;
use syn::spanned::Spanned;
use url::Url;
use crate::helpers::{indent, read_file, version_matches_request, Result};
fn url_matches(value: &str, pkg_name: &str, version: &Version) -> Result<()> {
let url = Url::parse(value).map_err(|err| format!("parse error: {}", err))?;
if url.domain().is_some() && url.domain() != Some("docs.rs") {
return Ok(());
}
if url.scheme() != "https" {
return Err(format!("expected \"https\", found {:?}", url.scheme()));
}
let mut path_segments = url
.path_segments()
.ok_or_else(|| String::from("no path in URL"))?;
let name = path_segments
.next()
.and_then(|path| if path.is_empty() { None } else { Some(path) })
.ok_or_else(|| String::from("missing package name"))?;
let request = path_segments
.next()
.and_then(|path| if path.is_empty() { None } else { Some(path) })
.ok_or_else(|| String::from("missing version number"))?;
if name != pkg_name {
Err(format!(
"expected package \"{}\", found \"{}\"",
pkg_name, name
))
} else {
parse_request(request)
.map_err(|err| format!("could not parse version in URL: {}", err))
.and_then(|request| version_matches_request(version, &request))
}
}
pub fn check_html_root_url(path: &str, pkg_name: &str, pkg_version: &str) -> Result<()> {
let code = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?;
let version = parse_version(pkg_version)
.map_err(|err| format!("bad package version {:?}: {}", pkg_version, err))?;
let krate: syn::File = syn::parse_file(&code)
.map_err(|_| format!("could not parse {}: please run \"cargo build\"", path))?;
println!("Checking doc attributes in {}...", path);
for attr in krate.attrs {
if let syn::AttrStyle::Outer = attr.style {
continue;
}
let (ident, nested_meta_items) = match attr.parse_meta() {
Ok(syn::Meta::List(syn::MetaList { ident, nested, .. })) => (ident, nested),
_ => continue,
};
if ident != "doc" {
continue;
}
for nested_meta_item in nested_meta_items {
let meta_item = match nested_meta_item {
syn::NestedMeta::Meta(ref meta_item) => meta_item,
_ => continue,
};
let check_result = match *meta_item {
syn::Meta::NameValue(syn::MetaNameValue {
ref ident, ref lit, ..
}) if ident == "html_root_url" => {
match *lit {
syn::Lit::Str(ref s) => url_matches(&s.value(), pkg_name, &version),
_ => continue,
}
}
syn::Meta::Word(ref name) if name == "html_root_url" => {
Err(String::from("html_root_url attribute without URL"))
}
_ => continue,
};
let first_line = attr.span().start().line;
let last_line = attr.span().end().line;
let source_lines = code.lines().take(last_line).skip(first_line - 1);
match check_result {
Ok(()) => {
println!("{} (line {}) ... ok", path, first_line);
return Ok(());
}
Err(err) => {
println!("{} (line {}) ... {} in", path, first_line, err);
for line in source_lines {
println!("{}", indent(line));
}
return Err(format!("html_root_url errors in {}", path));
}
}
}
}
Ok(())
}
#[cfg(test)]
mod test_url_matches {
use super::*;
#[test]
fn good_url() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo/1.2.3", "foo", &ver),
Ok(())
);
}
#[test]
fn trailing_slash() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo/1.2.3/", "foo", &ver),
Ok(())
);
}
#[test]
fn without_patch() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(url_matches("https://docs.rs/foo/1.2", "foo", &ver), Ok(()));
}
#[test]
fn without_minor() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(url_matches("https://docs.rs/foo/1", "foo", &ver), Ok(()));
}
#[test]
fn different_domain() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(url_matches("https://example.net/foo/", "bar", &ver), Ok(()));
}
#[test]
fn different_domain_http() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("http://example.net/foo/1.2.3", "foo", &ver),
Ok(())
);
}
#[test]
fn http_url() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("http://docs.rs/foo/1.2.3", "foo", &ver),
Err(String::from("expected \"https\", found \"http\""))
);
}
#[test]
fn bad_scheme() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("mailto:foo@example.net", "foo", &ver),
Err(String::from("expected \"https\", found \"mailto\""))
);
}
#[test]
fn no_package() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs", "foo", &ver),
Err(String::from("missing package name"))
);
}
#[test]
fn no_package_trailing_slash() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/", "foo", &ver),
Err(String::from("missing package name"))
);
}
#[test]
fn no_version() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo", "foo", &ver),
Err(String::from("missing version number"))
);
}
#[test]
fn no_version_trailing_slash() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo/", "foo", &ver),
Err(String::from("missing version number"))
);
}
#[test]
fn bad_url() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("docs.rs/foo/bar", "foo", &ver),
Err(String::from("parse error: relative URL without a base"))
);
}
#[test]
fn bad_pkg_version() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo/1.2.bad/", "foo", &ver),
Err(String::from(
"could not parse version in URL: \
encountered unexpected token: AlphaNumeric(\"bad\")"
))
);
}
#[test]
fn wrong_pkg_name() {
let ver = parse_version("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo/1.2.3/", "bar", &ver),
Err(String::from("expected package \"bar\", found \"foo\""))
);
}
}
#[cfg(test)]
mod test_check_html_root_url {
use super::*;
#[test]
fn bad_path() {
let no_such_file = if cfg!(unix) {
"No such file or directory (os error 2)"
} else {
"The system cannot find the file specified. (os error 2)"
};
let errmsg = format!("could not read no-such-file.md: {}", no_such_file);
assert_eq!(
check_html_root_url("no-such-file.md", "foobar", "1.2.3"),
Err(errmsg)
);
}
#[test]
fn bad_pkg_version() {
assert_eq!(
check_html_root_url("src/lib.rs", "foobar", "1.2"),
Err(String::from(
"bad package version \"1.2\": expected more input"
))
);
}
}