#![cfg(feature = "html_root_url_updated")]
use semver::{Version, VersionReq};
use syn::spanned::Spanned;
use syn::token;
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 \"{pkg_name}\", found \"{name}\""))
} else {
VersionReq::parse(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 = Version::parse(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;
}
if !attr.path().is_ident("doc") {
continue;
}
if let syn::Meta::List(ref list) = attr.meta {
list.parse_nested_meta(|meta| {
if meta.path.is_ident("html_root_url") {
let check_result = match meta.value() {
Ok(value) => match value.parse()? {
syn::Lit::Str(ref s) => url_matches(&s.value(), pkg_name, &version),
_ => return Ok(()),
},
Err(_err) => Err(String::from("html_root_url attribute without URL")),
};
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(meta.error(format!("html_root_url errors in {}", path)));
}
}
}
else if meta.input.peek(token::Eq) {
let value = meta.value()?;
value.parse::<proc_macro2::TokenTree>()?;
} else if meta.input.peek(token::Paren) {
let value;
syn::parenthesized!(value in meta.input);
while !value.is_empty() {
value.parse::<proc_macro2::TokenTree>()?;
}
} else {
return Err(meta.error("unknown doc attribute"));
}
Ok(())
})
.map_err(|err| err.to_string())?;
}
}
Ok(())
}
#[cfg(test)]
mod test_url_matches {
use super::*;
#[test]
fn good_url() {
let ver = Version::parse("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo/1.2.3", "foo", &ver),
Ok(())
);
}
#[test]
fn trailing_slash() {
let ver = Version::parse("1.2.3").unwrap();
assert_eq!(
url_matches("https://docs.rs/foo/1.2.3/", "foo", &ver),
Ok(())
);
}
#[test]
fn without_patch() {
let ver = Version::parse("1.2.3").unwrap();
assert_eq!(url_matches("https://docs.rs/foo/1.2/", "foo", &ver), Ok(()));
}
#[test]
fn without_minor() {
let ver = Version::parse("1.2.3").unwrap();
assert_eq!(url_matches("https://docs.rs/foo/1/", "foo", &ver), Ok(()));
}
#[test]
fn different_domain() {
let ver = Version::parse("1.2.3").unwrap();
assert_eq!(url_matches("https://example.net/foo/", "bar", &ver), Ok(()));
}
#[test]
fn different_domain_http() {
let ver = Version::parse("1.2.3").unwrap();
assert_eq!(
url_matches("http://example.net/foo/1.2.3", "foo", &ver),
Ok(())
);
}
#[test]
fn http_url() {
let ver = Version::parse("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 = Version::parse("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 = Version::parse("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 = Version::parse("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 = Version::parse("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 = Version::parse("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 = Version::parse("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 = Version::parse("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: \
unexpected character 'b' while parsing patch version number"
))
);
}
#[test]
fn wrong_pkg_name() {
let ver = Version::parse("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\": unexpected end of input while parsing minor version number"
))
);
}
}