diff --git a/eipw-lint-js/src/lib.rs b/eipw-lint-js/src/lib.rs index d1ed248d..478b2da4 100644 --- a/eipw-lint-js/src/lib.rs +++ b/eipw-lint-js/src/lib.rs @@ -174,7 +174,7 @@ pub async fn lint(sources: Vec, options: Option) -> Result Result { - let value: serde_json::Value = serde_wasm_bindgen::from_value(snippet.deref().clone())?; + let value: serde_json::Value = serde_wasm_bindgen::from_value(snippet.clone())?; let obj = match value { serde_json::Value::Object(o) => o, diff --git a/eipw-lint/src/lints/markdown/relative_links.rs b/eipw-lint/src/lints/markdown/relative_links.rs index c41ca317..73681c63 100644 --- a/eipw-lint/src/lints/markdown/relative_links.rs +++ b/eipw-lint/src/lints/markdown/relative_links.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use annotate_snippets::snippet::{Annotation, Slice, Snippet}; +use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet}; use comrak::nodes::Ast; @@ -33,6 +33,7 @@ where { fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { let re = Regex::new("(^/)|(://)").unwrap(); + let eip_re = Regex::new(r"^(https?:)?//(?:eips|ercs)\.ethereum\.org/(?:EIPS|ERCS)/(?:eip|erc)-(\d+)|(assets/.+)$").unwrap(); let exceptions = RegexSet::new(&self.exceptions).map_err(Error::custom)?; @@ -44,14 +45,45 @@ where .into_iter() .filter(|l| re.is_match(&l.address) && !exceptions.is_match(&l.address)); - for Link { line_start, .. } in links { + for Link { + address, + line_start, + } in links + { + let (suggestion, extra_help) = if let Some(caps) = eip_re.captures(&address) { + if let Some(id_number) = caps.get(2) { + let suggestion = format!("./eip-{}.md", id_number.as_str()); + (suggestion, true) + } else if let Some(asset_path) = caps.get(3) { + let suggestion = format!("../{}", asset_path.as_str()); + (suggestion, true) + } else { + (address, false) + } + } else if address.contains("//creativecommons.org/publicdomain/zero/1.0/") { + ("../LICENSE.md".to_string(), true) + } else { + (address, false) + }; + + let mut footer = vec![]; + + let suggestion_label = format!("use `{}` instead", suggestion); + if extra_help { + footer.push(Annotation { + annotation_type: AnnotationType::Help, + label: Some(&suggestion_label), + id: None, + }); + } + ctx.report(Snippet { title: Some(Annotation { id: Some(slug), annotation_type: ctx.annotation_type(), label: Some("non-relative link or image"), }), - footer: vec![], + footer, slices: vec![Slice { line_start, fold: false, diff --git a/eipw-lint/tests/lint_markdown_relative_links.rs b/eipw-lint/tests/lint_markdown_relative_links.rs index 64c66fb5..a2b5ed00 100644 --- a/eipw-lint/tests/lint_markdown_relative_links.rs +++ b/eipw-lint/tests/lint_markdown_relative_links.rs @@ -1,7 +1,7 @@ /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * file. You can obtain one at https://mozilla.org/MPL/2.0/. */ use eipw_lint::lints::markdown::RelativeLinks; @@ -69,6 +69,141 @@ header: value1 ); } +#[tokio::test] +async fn inline_link_with_scheme_to_eips_ethereum_org() { + let src = r#"--- +header: value1 +--- + +[hello](https://eips.ethereum.org/EIPS/eip-1234) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | [hello](https://eips.ethereum.org/EIPS/eip-1234) + | + = help: use `./eip-1234.md` instead +"# + ); +} + +#[tokio::test] +async fn inline_link_with_scheme_to_ercs_ethereum_org() { + let src = r#"--- +header: value1 +--- + +[hello](https://ercs.ethereum.org/ERCS/erc-1234) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | [hello](https://ercs.ethereum.org/ERCS/erc-1234) + | + = help: use `./eip-1234.md` instead +"# + ); +} + +#[tokio::test] +async fn inline_link_with_scheme_to_creativecommons_copyright() { + let src = r#"--- +header: value1 +--- + +[copyright](https://creativecommons.org/publicdomain/zero/1.0/) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | [copyright](https://creativecommons.org/publicdomain/zero/1.0/) + | + = help: use `../LICENSE.md` instead +"# + ); +} + +#[tokio::test] +async fn inline_link_with_scheme_and_numbers() { + let src = r#"--- +header: value1 +--- + +[hi](https://example.com/4444) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | [hi](https://example.com/4444) + | +"# + ); +} + #[tokio::test] async fn inline_link_protocol_relative() { let src = r#"--- @@ -224,6 +359,78 @@ Hello [hi][hello]! assert_eq!(reports, ""); } +#[tokio::test] +async fn reference_link_with_scheme_to_eips_ethereum_org() { + let src = r#"--- +header: value1 +--- + +Hello [hi][hello]! + +[hello]: https://eips.ethereum.org/EIPS/eip-1234 +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | Hello [hi][hello]! + | + = help: use `./eip-1234.md` instead +"# + ); +} + +#[tokio::test] +async fn reference_link_with_scheme_to_ercs_ethereum_org() { + let src = r#"--- +header: value1 +--- + +Hello [hi][hello]! + +[hello]: https://ercs.ethereum.org/ERCS/erc-1234 +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | Hello [hi][hello]! + | + = help: use `./eip-1234.md` instead +"# + ); +} + #[tokio::test] async fn inline_autolink() { let src = r#"--- @@ -325,6 +532,74 @@ header: value1 ); } +#[tokio::test] +async fn anchor_link_protocol_relative_to_eips_ethereum_org() { + let src = r#"--- +header: value1 +--- + +example +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | example + | + = help: use `./eip-1234.md` instead +"# + ); +} + +#[tokio::test] +async fn anchor_link_protocol_relative_to_ercs_ethereum_org() { + let src = r#"--- +header: value1 +--- + +example +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | example + | + = help: use `./eip-1234.md` instead +"# + ); +} + #[tokio::test] async fn anchor_link_relative_double_slash() { let src = r#"--- @@ -351,6 +626,40 @@ header: value1 assert_eq!(reports, ""); } +#[tokio::test] +async fn anchor_link_protocol_relative_to_creativecommons_copyright() { + let src = r#"--- +header: value1 +--- + +copyright +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | copyright + | + = help: use `../LICENSE.md` instead +"# + ); +} + #[tokio::test] async fn img_relative_double_slash() { let src = r#"--- @@ -409,3 +718,139 @@ header: value1 "# ); } + +#[tokio::test] +async fn img_protocol_relative_to_eips_ethereum_org() { + let src = r#"--- +header: value1 +--- + + +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | + | + = help: use `../assets/eip-712/eth_sign.png` instead +"# + ); +} + +#[tokio::test] +async fn img_protocol_relative_to_ercs_ethereum_org() { + let src = r#"--- +header: value1 +--- + + +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | + | + = help: use `../assets/erc-712/eth_sign.png` instead +"# + ); +} + +#[tokio::test] +async fn img_with_scheme_to_eips_ethereum_org() { + let src = r#"--- +header: value1 +--- + + +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | + | + = help: use `../assets/eip-712/eth_sign.png` instead +"# + ); +} + +#[tokio::test] +async fn img_with_scheme_to_ercs_ethereum_org() { + let src = r#"--- +header: value1 +--- + + +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-rel", + RelativeLinks { + exceptions: Vec::<&str>::new(), + }, + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + assert_eq!( + reports, + r#"error[markdown-rel]: non-relative link or image + | +5 | + | + = help: use `../assets/erc-712/eth_sign.png` instead +"# + ); +}