From fd7cebb36b6c8ff1c62172191bcd6a5c06baa4c9 Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Thu, 30 May 2024 16:58:34 +0300 Subject: [PATCH 01/14] initial markdown-link-eip --- eipw-lint/src/lib.rs | 4 +++ eipw-lint/src/lints/known_lints.rs | 6 +++++ eipw-lint/src/lints/markdown.rs | 2 ++ eipw-lint/src/lints/markdown/link_eip.rs | 32 +++++++++++++++++++++++ eipw-lint/tests/lint_markdown_link_eip.rs | 28 ++++++++++++++++++++ 5 files changed, 72 insertions(+) create mode 100644 eipw-lint/src/lints/markdown/link_eip.rs create mode 100644 eipw-lint/tests/lint_markdown_link_eip.rs diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index 41ad25c6..84553ec5 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -419,6 +419,10 @@ pub fn default_lints_enum() -> impl Iterator { MarkdownHtmlComments(markdown::HtmlComments), MarkdownJsonSchema(markdown::JsonSchema), + MarkdownLinkEip(markdown::LinkEip), MarkdownLinkFirst { pattern: markdown::LinkFirst, }, @@ -102,6 +103,7 @@ where Self::MarkdownHtmlComments(l) => Box::new(l), Self::MarkdownJsonSchema(l) => Box::new(l), + Self::MarkdownLinkEip(l) => Box::new(l), Self::MarkdownLinkFirst { pattern } => Box::new(pattern), Self::MarkdownLinkStatus(l) => Box::new(l), Self::MarkdownProposalRef(l) => Box::new(l), @@ -141,6 +143,7 @@ where Self::MarkdownHtmlComments(l) => l, Self::MarkdownJsonSchema(l) => l, + Self::MarkdownLinkEip(l) => l, Self::MarkdownLinkFirst { pattern } => pattern, Self::MarkdownLinkStatus(l) => l, Self::MarkdownProposalRef(l) => l, @@ -257,6 +260,9 @@ where .map(|(a, b)| (a.as_ref(), b.as_ref())) .collect(), }), + Self::MarkdownLinkEip (l) => DefaultLint::MarkdownLinkEip(markdown::LinkEip ( + l.as_ref(), + )), Self::MarkdownLinkFirst { pattern } => DefaultLint::MarkdownLinkFirst { pattern: markdown::LinkFirst(pattern.0.as_ref()), }, diff --git a/eipw-lint/src/lints/markdown.rs b/eipw-lint/src/lints/markdown.rs index 7f1ca018..081e202b 100644 --- a/eipw-lint/src/lints/markdown.rs +++ b/eipw-lint/src/lints/markdown.rs @@ -7,6 +7,7 @@ pub mod headings_space; pub mod html_comments; pub mod json_schema; +pub mod link_eip; pub mod link_first; pub mod link_status; pub mod proposal_ref; @@ -18,6 +19,7 @@ pub mod section_required; pub use self::headings_space::HeadingsSpace; pub use self::html_comments::HtmlComments; pub use self::json_schema::JsonSchema; +pub use self::link_eip::LinkEip; pub use self::link_first::LinkFirst; pub use self::link_status::LinkStatus; pub use self::proposal_ref::ProposalRef; diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs new file mode 100644 index 00000000..bf637485 --- /dev/null +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -0,0 +1,32 @@ +/* + * 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/. + */ + +// use annotate_snippets::snippet::{Annotation, Slice, Snippet}; + +// use comrak::nodes::{Ast, AstNode, NodeCode, NodeCodeBlock, NodeHtmlBlock}; + +use crate::lints::{Context, Error, Lint}; // FetchContext +// use crate::tree::{self, Next, TraverseExt}; + +// use regex::Regex; + +use serde::{Deserialize, Serialize}; + +// use std::collections::HashSet; +use std::fmt::{Debug, Display}; +// use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct LinkEip(pub S); + +impl Lint for LinkEip +where + S: Display + Debug + AsRef, +{ + fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { + Ok(()) + } +} \ No newline at end of file diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs new file mode 100644 index 00000000..c312ffed --- /dev/null +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -0,0 +1,28 @@ +/* + * 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/. + */ + + use eipw_lint::lints::markdown::LinkEip; + use eipw_lint::reporters::Text; + use eipw_lint::Linter; + + #[tokio::test] + async fn link_matches_the_pattern() { + let src = r#"--- + header: value1 + --- + [EIP-1](./eip-2.md) + "#; + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip("EIP-1")) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + // assert_eq!(reports, ""); + } \ No newline at end of file From 011babb208db4af6c97f533ee728323e3d9dc089 Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Tue, 25 Jun 2024 21:54:44 +0300 Subject: [PATCH 02/14] update markdown-link-eip, add markdown-link-other --- eipw-lint/src/lib.rs | 10 +- eipw-lint/src/lints/known_lints.rs | 24 ++- eipw-lint/src/lints/markdown.rs | 2 + eipw-lint/src/lints/markdown/link_eip.rs | 125 +++++++++++- eipw-lint/src/lints/markdown/link_other.rs | 124 ++++++++++++ eipw-lint/tests/lint_markdown_link_eip.rs | 204 ++++++++++++++++++-- eipw-lint/tests/lint_markdown_link_other.rs | 81 ++++++++ 7 files changed, 533 insertions(+), 37 deletions(-) create mode 100644 eipw-lint/src/lints/markdown/link_other.rs create mode 100644 eipw-lint/tests/lint_markdown_link_other.rs diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index 84553ec5..524d1a0e 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -421,7 +421,9 @@ pub fn default_lints_enum() -> impl Iterator impl Iterator { MarkdownHtmlComments(markdown::HtmlComments), MarkdownJsonSchema(markdown::JsonSchema), - MarkdownLinkEip(markdown::LinkEip), + MarkdownLinkEip { + pattern: markdown::LinkEip + }, MarkdownLinkFirst { pattern: markdown::LinkFirst, }, + MarkdownLinkOther { + pattern: markdown::LinkOther + }, MarkdownLinkStatus(markdown::LinkStatus), MarkdownProposalRef(markdown::ProposalRef), MarkdownRegex(markdown::Regex), @@ -103,7 +108,8 @@ where Self::MarkdownHtmlComments(l) => Box::new(l), Self::MarkdownJsonSchema(l) => Box::new(l), - Self::MarkdownLinkEip(l) => Box::new(l), + Self::MarkdownLinkEip { pattern } => Box::new(pattern), + Self::MarkdownLinkOther { pattern } => Box::new(pattern), Self::MarkdownLinkFirst { pattern } => Box::new(pattern), Self::MarkdownLinkStatus(l) => Box::new(l), Self::MarkdownProposalRef(l) => Box::new(l), @@ -143,8 +149,9 @@ where Self::MarkdownHtmlComments(l) => l, Self::MarkdownJsonSchema(l) => l, - Self::MarkdownLinkEip(l) => l, + Self::MarkdownLinkEip { pattern } => pattern, Self::MarkdownLinkFirst { pattern } => pattern, + Self::MarkdownLinkOther { pattern } => pattern, Self::MarkdownLinkStatus(l) => l, Self::MarkdownProposalRef(l) => l, Self::MarkdownRegex(l) => l, @@ -260,9 +267,12 @@ where .map(|(a, b)| (a.as_ref(), b.as_ref())) .collect(), }), - Self::MarkdownLinkEip (l) => DefaultLint::MarkdownLinkEip(markdown::LinkEip ( - l.as_ref(), - )), + Self::MarkdownLinkEip { pattern } => DefaultLint::MarkdownLinkEip { + pattern: markdown::LinkEip(pattern.0.as_ref()), + }, + Self::MarkdownLinkOther { pattern } => DefaultLint::MarkdownLinkOther { + pattern: markdown::LinkOther(pattern.0.as_ref()), + }, Self::MarkdownLinkFirst { pattern } => DefaultLint::MarkdownLinkFirst { pattern: markdown::LinkFirst(pattern.0.as_ref()), }, @@ -281,7 +291,7 @@ where prefix: l.prefix.as_ref(), suffix: l.suffix.as_ref(), }) - } + }, Self::MarkdownRegex(l) => DefaultLint::MarkdownRegex(markdown::Regex { message: l.message.as_ref(), mode: l.mode, diff --git a/eipw-lint/src/lints/markdown.rs b/eipw-lint/src/lints/markdown.rs index 081e202b..a1c01347 100644 --- a/eipw-lint/src/lints/markdown.rs +++ b/eipw-lint/src/lints/markdown.rs @@ -9,6 +9,7 @@ pub mod html_comments; pub mod json_schema; pub mod link_eip; pub mod link_first; +pub mod link_other; pub mod link_status; pub mod proposal_ref; pub mod regex; @@ -21,6 +22,7 @@ pub use self::html_comments::HtmlComments; pub use self::json_schema::JsonSchema; pub use self::link_eip::LinkEip; pub use self::link_first::LinkFirst; +pub use self::link_other::LinkOther; pub use self::link_status::LinkStatus; pub use self::proposal_ref::ProposalRef; pub use self::regex::Regex; diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs index bf637485..a4963393 100644 --- a/eipw-lint/src/lints/markdown/link_eip.rs +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -4,22 +4,20 @@ * 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, AstNode, NodeCode, NodeCodeBlock, NodeHtmlBlock}; +use comrak::nodes::{Ast, NodeLink}; -use crate::lints::{Context, Error, Lint}; // FetchContext -// use crate::tree::{self, Next, TraverseExt}; +use crate::lints::{Context, Error, Lint}; +use crate::tree::{self, Next, TraverseExt}; -// use regex::Regex; +use regex::Regex; use serde::{Deserialize, Serialize}; -// use std::collections::HashSet; use std::fmt::{Debug, Display}; -// use std::path::PathBuf; -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkEip(pub S); impl Lint for LinkEip @@ -27,6 +25,117 @@ where S: Display + Debug + AsRef, { fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { + let pattern = self.0.as_ref(); + let re = Regex::new(pattern).map_err(Error::custom)?; + + let mut visitor = Visitor { + ctx, + re, + slug, + link_depth: 0, + current_link: Link { url: String::new(), text: String::new() }, + }; + ctx.body().traverse().visit(&mut visitor)?; + + Ok(()) + } +} +#[derive(Debug)] +struct Link { + url: String, + text: String, +} + +#[derive(Debug)] +struct Visitor<'a, 'b, 'c> { + ctx: &'c Context<'a, 'b>, + re: Regex, + slug: &'c str, + link_depth: usize, + current_link: Link, +} + +impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { + fn check(&self, ast: &Ast) -> Result { + let pattern = r"(?i)\b(?:eip|erc)-(\d+)\b"; + let url_re = Regex::new(pattern).map_err(Error::custom)?; + + let url_eip_number = if let Some(captures) = url_re.captures(&self.current_link.url) { + captures.get(1).map(|m| m.as_str()) + } else { None }; + + if let Some(url_eip_number) = url_eip_number { + let section_pattern = r"eip-([^.]*)\.md#.+$"; + let url_re = Regex::new(section_pattern).map_err(Error::custom)?; + let dynamic_pattern = if url_re.is_match(&self.current_link.url) { + format!(r"^(EIP|ERC)-{}\s*\S+", regex::escape(&url_eip_number)) + } else { + format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) + }; + let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; + + if text_re.is_match(&self.current_link.text) { + return Ok(Next::TraverseChildren); + }; + + let expected = if url_re.is_match(&self.current_link.url) { + format!("[EIP|ERC-{}]", url_eip_number) + } else { + format!("[EIP|ERC-{}]", url_eip_number) + }; + + let footer_label = format!("link text should match `{}`", expected); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Info, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; + } + + Ok(Next::TraverseChildren) + } +} + +impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { + type Error = Error; + + fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { + if self.re.is_match(&link.url) { + self.current_link = Link { url: link.url.to_owned(), text: String::new() }; + self.link_depth += 1; + } + Ok(Next::TraverseChildren) + } + + fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { + if self.link_depth > 0 { + self.link_depth = self.link_depth.checked_sub(1).unwrap(); + } Ok(()) } + + fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { + if self.link_depth > 0 { + self.current_link.text = txt.to_owned(); + self.check(ast)?; + } + Ok(Next::TraverseChildren) + } } \ No newline at end of file diff --git a/eipw-lint/src/lints/markdown/link_other.rs b/eipw-lint/src/lints/markdown/link_other.rs new file mode 100644 index 00000000..a4ad9ef0 --- /dev/null +++ b/eipw-lint/src/lints/markdown/link_other.rs @@ -0,0 +1,124 @@ +/* + * 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/. + */ + +use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet}; + +use comrak::nodes::{Ast, NodeLink}; + +use crate::lints::{Context, Error, Lint}; +use crate::tree::{self, Next, TraverseExt}; + +use regex::Regex; + +use serde::{Deserialize, Serialize}; + +use std::fmt::{Debug, Display}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LinkOther(pub S); + +impl Lint for LinkOther +where + S: Display + Debug + AsRef, +{ + fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { + let pattern = self.0.as_ref(); + let re = Regex::new(pattern).map_err(Error::custom)?; + + let mut visitor = Visitor { + ctx, + re, + slug, + link_depth: 0, + current_link: Link { url: String::new(), text: String::new() }, + }; + ctx.body().traverse().visit(&mut visitor)?; + + Ok(()) + } +} +#[derive(Debug)] +struct Link { + url: String, + text: String, +} + +#[derive(Debug)] +struct Visitor<'a, 'b, 'c> { + ctx: &'c Context<'a, 'b>, + re: Regex, + slug: &'c str, + link_depth: usize, + current_link: Link, +} + +impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { + fn check(&self, ast: &Ast) -> Result { + let text_eip_number = if let Some(captures) = self.re.captures(&self.current_link.text) { + captures.get(2).map(|m| m.as_str()) + } else { None }; + + if let Some(text_eip_number) = text_eip_number { + let pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); + let re = Regex::new(&pattern).map_err(Error::custom)?; + + if re.is_match(&self.current_link.url) { + return Ok(Next::TraverseChildren); + } + + let footer_label = format!("link destinstion must match text EIP"); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Info, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; + } + + Ok(Next::TraverseChildren) + } +} + +impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { + type Error = Error; + + fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { + self.current_link = Link { url: link.url.to_owned(), text: String::new() }; + self.link_depth += 1; + Ok(Next::TraverseChildren) + } + + fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { + if self.link_depth > 0 { + self.link_depth = self.link_depth.checked_sub(1).unwrap(); + } + Ok(()) + } + + fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { + if self.link_depth > 0 && self.re.is_match(&txt) { + self.current_link.text = txt.to_owned(); + self.check(ast)?; + } + Ok(Next::TraverseChildren) + } +} \ No newline at end of file diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index c312ffed..3eac5dc2 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -4,25 +4,187 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - use eipw_lint::lints::markdown::LinkEip; - use eipw_lint::reporters::Text; - use eipw_lint::Linter; - - #[tokio::test] - async fn link_matches_the_pattern() { - let src = r#"--- - header: value1 - --- - [EIP-1](./eip-2.md) - "#; +use eipw_lint::lints::markdown::LinkEip; +use eipw_lint::reporters::Text; +use eipw_lint::Linter; + +#[tokio::test] +async fn eip_number_mismatch() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-2.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-2.md) + | + = info: link text should match `[EIP|ERC-2]` +"# + ); +} + +#[tokio::test] +async fn link_eip_has_no_section() { + let src = r#"--- +header: value1 +--- +[EIP-1: Foo](./eip-1.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1: Foo](./eip-1.md) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn link_text_missing_eip() { + let src = r#"--- +header: value1 +--- +[Another Proposal](./eip-1.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [Another Proposal](./eip-1.md) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn link_text_missing_section_description() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-1.md#motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-1.md#motivation) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn eip_number_mismatch_with_section() { + let src = r#"--- +header: value1 +--- +[EIP-2: Hello](./eip-1.md#abstract) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-2: Hello](./eip-1.md#abstract) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn link_text_missing_eip_with_section() { + let src = r#"--- +header: value1 +--- +[Another Proposal](./eip-1.md#rationale) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [Another Proposal](./eip-1.md#rationale) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn should_be_ignored() { + let src = r#"--- +header: value1 +--- +[EIP-721's Motivation](./eip-721.md#motivation) +"#; + let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip("EIP-1")) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); - - // assert_eq!(reports, ""); - } \ No newline at end of file + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!(reports, ""); +} \ No newline at end of file diff --git a/eipw-lint/tests/lint_markdown_link_other.rs b/eipw-lint/tests/lint_markdown_link_other.rs new file mode 100644 index 00000000..727aceea --- /dev/null +++ b/eipw-lint/tests/lint_markdown_link_other.rs @@ -0,0 +1,81 @@ +/* + * 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/. + */ + +use eipw_lint::lints::markdown::LinkOther; +use eipw_lint::reporters::Text; +use eipw_lint::Linter; + +#[tokio::test] +async fn link_destination_missing_eip() { + let src = r#"--- +header: value1 +--- +[EIP-2](../assets/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-other]: link text does not match link destination + | +4 | [EIP-2](../assets/foo.txt) + | + = info: link destinstion must match text EIP +"# + ); +} + +#[tokio::test] +async fn link_eip_number_differs_from_text() { + let src = r#"--- +header: value1 +--- +[EIP-1](../assets/eip-2/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!(reports, + r#"error[markdown-link-other]: link text does not match link destination + | +4 | [EIP-1](../assets/eip-2/foo.txt) + | + = info: link destinstion must match text EIP +"# + ); +} + +#[tokio::test] +async fn should_be_ignored() { + let src = r#"--- +header: value1 +--- +[EIP-2](../assets/eip-2/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!(reports, ""); +} \ No newline at end of file From 2829f036a7ca009eaff20f5b4b34481edc3d4063 Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Fri, 5 Jul 2024 17:32:49 +0300 Subject: [PATCH 03/14] bug fixes --- eipw-lint/src/lib.rs | 4 +- eipw-lint/src/lints/markdown/link_eip.rs | 113 +++++++++-------- eipw-lint/src/lints/markdown/link_other.rs | 80 ++++++------ eipw-lint/tests/lint_markdown_link_eip.rs | 134 ++++++++++++++++++-- eipw-lint/tests/lint_markdown_link_other.rs | 37 +++++- 5 files changed, 260 insertions(+), 108 deletions(-) diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index 524d1a0e..9b88c91a 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -422,7 +422,7 @@ pub fn default_lints_enum() -> impl Iterator impl Iterator { } impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { - fn check(&self, ast: &Ast) -> Result { - let pattern = r"(?i)\b(?:eip|erc)-(\d+)\b"; - let url_re = Regex::new(pattern).map_err(Error::custom)?; + fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { + if let Some(captures) = re.captures(text) { + Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + } else { + Ok(String::new()) + } + } - let url_eip_number = if let Some(captures) = url_re.captures(&self.current_link.url) { - captures.get(1).map(|m| m.as_str()) - } else { None }; + fn transform_section_description(description: &str) -> String { + let re = Regex::new(r"[-_]").unwrap(); + let mut description = re.replace_all(description, " ").to_string(); + if let Some(first_char) = description.get_mut(0..1) { + first_char.make_ascii_uppercase(); + } + description + } - if let Some(url_eip_number) = url_eip_number { - let section_pattern = r"eip-([^.]*)\.md#.+$"; - let url_re = Regex::new(section_pattern).map_err(Error::custom)?; - let dynamic_pattern = if url_re.is_match(&self.current_link.url) { - format!(r"^(EIP|ERC)-{}\s*\S+", regex::escape(&url_eip_number)) - } else { - format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) - }; - let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; - - if text_re.is_match(&self.current_link.text) { - return Ok(Next::TraverseChildren); - }; - - let expected = if url_re.is_match(&self.current_link.url) { - format!("[EIP|ERC-{}]", url_eip_number) - } else { - format!("[EIP|ERC-{}]", url_eip_number) - }; + fn check(&self, ast: &Ast) -> Result { + let url_eip_text = self.extract_capture(&self.current_link.url, &self.re, 1)?; + let url_eip_number = self.extract_capture(&self.current_link.url, &self.re, 2)?; + let url_section = self.extract_capture(&self.current_link.url, &self.re, 4)?; - let footer_label = format!("link text should match `{}`", expected); + let dynamic_pattern = if url_section != "" { + format!(r"^(EIP|ERC)-{}(\s*\S+)", regex::escape(&url_eip_number)) + } else { + format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) + }; + let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; + + if text_re.is_match(&self.current_link.text) { + return Ok(Next::TraverseChildren); + }; - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); - self.ctx.report(Snippet { - title: Some(Annotation { - annotation_type: self.ctx.annotation_type(), - id: Some(self.slug), - label: Some("link text does not match link destination"), - }), - slices: vec![Slice { - fold: false, - line_start: ast.sourcepos.start.line, - origin: self.ctx.origin(), - source: &source, - annotations: vec![], - }], - footer: vec![Annotation { - id: None, - annotation_type: AnnotationType::Info, - label: Some(&footer_label), - }], - opt: Default::default(), - })?; - } + let expected = if url_section != "" { + let section_description = Visitor::transform_section_description(&url_section); + format!("[{}{}: {}]({})", url_eip_text.to_uppercase(), url_eip_number, section_description, &self.current_link.url) + } else { + format!("[{}{}]({})", url_eip_text.to_uppercase(), url_eip_number, &self.current_link.url) + }; + + let footer_label = format!("use `{}` instead", expected); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Help, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; Ok(Next::TraverseChildren) } } - + impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { type Error = Error; @@ -126,7 +137,7 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { if self.link_depth > 0 { - self.link_depth = self.link_depth.checked_sub(1).unwrap(); + self.link_depth -= 1; } Ok(()) } @@ -136,6 +147,6 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { self.current_link.text = txt.to_owned(); self.check(ast)?; } - Ok(Next::TraverseChildren) + Ok(Next::SkipChildren) } } \ No newline at end of file diff --git a/eipw-lint/src/lints/markdown/link_other.rs b/eipw-lint/src/lints/markdown/link_other.rs index a4ad9ef0..358ac5ba 100644 --- a/eipw-lint/src/lints/markdown/link_other.rs +++ b/eipw-lint/src/lints/markdown/link_other.rs @@ -56,43 +56,49 @@ struct Visitor<'a, 'b, 'c> { } impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { - fn check(&self, ast: &Ast) -> Result { - let text_eip_number = if let Some(captures) = self.re.captures(&self.current_link.text) { - captures.get(2).map(|m| m.as_str()) - } else { None }; + fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { + if let Some(captures) = re.captures(text) { + Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + } else { + Ok(String::new()) + } + } - if let Some(text_eip_number) = text_eip_number { - let pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); - let re = Regex::new(&pattern).map_err(Error::custom)?; + fn check(&self, ast: &Ast) -> Result { + let text_eip_full = self.extract_capture(&self.current_link.text, &self.re, 1)?; + let text_eip_number = self.extract_capture(&self.current_link.text, &self.re, 2)?; - if re.is_match(&self.current_link.url) { - return Ok(Next::TraverseChildren); - } - - let footer_label = format!("link destinstion must match text EIP"); - - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); - self.ctx.report(Snippet { - title: Some(Annotation { - annotation_type: self.ctx.annotation_type(), - id: Some(self.slug), - label: Some("link text does not match link destination"), - }), - slices: vec![Slice { - fold: false, - line_start: ast.sourcepos.start.line, - origin: self.ctx.origin(), - source: &source, - annotations: vec![], - }], - footer: vec![Annotation { - id: None, - annotation_type: AnnotationType::Info, - label: Some(&footer_label), - }], - opt: Default::default(), - })?; + let dynamic_pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); + let url_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; + + if url_re.is_match(&self.current_link.url) { + return Ok(Next::TraverseChildren); } + + let expected = format!("[{}](./{}.md)", text_eip_full.to_uppercase(), text_eip_full.to_lowercase()); + let footer_label = format!("the link destination should target {}, for example `{}`", text_eip_full.to_uppercase(), expected); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Help, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; Ok(Next::TraverseChildren) } @@ -109,15 +115,15 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { if self.link_depth > 0 { - self.link_depth = self.link_depth.checked_sub(1).unwrap(); + self.link_depth -= 1; } Ok(()) } fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { - if self.link_depth > 0 && self.re.is_match(&txt) { + if self.link_depth > 0 && self.re.is_match(&txt) { self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.check(ast)?; } Ok(Next::TraverseChildren) } diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index 3eac5dc2..cf0bfaab 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -18,7 +18,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -30,7 +30,7 @@ header: value1 | 4 | [EIP-1](./eip-2.md) | - = info: link text should match `[EIP|ERC-2]` + = help: use `[EIP-2](./eip-2.md)` instead "# ); } @@ -45,7 +45,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -57,7 +57,7 @@ header: value1 | 4 | [EIP-1: Foo](./eip-1.md) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1](./eip-1.md)` instead "# ); } @@ -72,7 +72,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#.+)?$".to_string())) .check_slice(None, src) .run() .await @@ -84,7 +84,7 @@ header: value1 | 4 | [Another Proposal](./eip-1.md) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1](./eip-1.md)` instead "# ); } @@ -99,7 +99,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -111,7 +111,61 @@ header: value1 | 4 | [EIP-1](./eip-1.md#motivation) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1: Motivation](./eip-1.md#motivation)` instead +"# + ); +} + +#[tokio::test] +async fn link_text_missing_extended_section_description_with_huphen() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-1.md#eip-motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-1.md#eip-motivation) + | + = help: use `[EIP-1: Eip motivation](./eip-1.md#eip-motivation)` instead +"# + ); +} + +#[tokio::test] +async fn link_text_missing_extended_section_description_with_underscore() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-1.md#eip_motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-1.md#eip_motivation) + | + = help: use `[EIP-1: Eip motivation](./eip-1.md#eip_motivation)` instead "# ); } @@ -126,7 +180,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -138,7 +192,61 @@ header: value1 | 4 | [EIP-2: Hello](./eip-1.md#abstract) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1: Abstract](./eip-1.md#abstract)` instead +"# + ); +} + +#[tokio::test] +async fn eip_number_mismatch_extended_section_description_with_huphen() { + let src = r#"--- +header: value1 +--- +[EIP-2: Hello](./eip-1.md#hello-abstract) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-2: Hello](./eip-1.md#hello-abstract) + | + = help: use `[EIP-1: Hello abstract](./eip-1.md#hello-abstract)` instead +"# + ); +} + +#[tokio::test] +async fn eip_number_mismatch_extended_section_description_with_underscore() { + let src = r#"--- +header: value1 +--- +[EIP-2: Hello](./eip-1.md#hello_abstract) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-2: Hello](./eip-1.md#hello_abstract) + | + = help: use `[EIP-1: Hello abstract](./eip-1.md#hello_abstract)` instead "# ); } @@ -153,7 +261,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -165,7 +273,7 @@ header: value1 | 4 | [Another Proposal](./eip-1.md#rationale) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1: Rationale](./eip-1.md#rationale)` instead "# ); } @@ -180,7 +288,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await diff --git a/eipw-lint/tests/lint_markdown_link_other.rs b/eipw-lint/tests/lint_markdown_link_other.rs index 727aceea..8cd6c941 100644 --- a/eipw-lint/tests/lint_markdown_link_other.rs +++ b/eipw-lint/tests/lint_markdown_link_other.rs @@ -18,7 +18,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) .check_slice(None, src) .run() .await @@ -30,7 +30,34 @@ header: value1 | 4 | [EIP-2](../assets/foo.txt) | - = info: link destinstion must match text EIP + = help: the link destination should target EIP-2, for example `[EIP-2](./eip-2.md)` +"# + ); +} + +#[tokio::test] +async fn link_destination_missing_eip_advanced_text() { + let src = r#"--- +header: value1 +--- +[EIP-2: Foo](../assets/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-other]: link text does not match link destination + | +4 | [EIP-2: Foo](../assets/foo.txt) + | + = help: the link destination should target EIP-2, for example `[EIP-2](./eip-2.md)` "# ); } @@ -45,7 +72,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) .check_slice(None, src) .run() .await @@ -56,7 +83,7 @@ header: value1 | 4 | [EIP-1](../assets/eip-2/foo.txt) | - = info: link destinstion must match text EIP + = help: the link destination should target EIP-1, for example `[EIP-1](./eip-1.md)` "# ); } @@ -71,7 +98,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) .check_slice(None, src) .run() .await From 4930ed4a8ac5b93823eb0ad294eba8089ed9767b Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 6 Jul 2024 11:31:50 -0400 Subject: [PATCH 04/14] Format and fix tests --- eipw-lint/src/lints/known_lints.rs | 6 +- eipw-lint/src/lints/markdown/link_eip.rs | 72 ++++--- eipw-lint/src/lints/markdown/link_other.rs | 63 ++++-- .../markdown-link-too-unstable/expected.txt | 2 +- .../eipv/markdown-link-too-unstable/input.md | 2 +- .../tests/eipv/markdown-unlinked-eip/input.md | 2 +- eipw-lint/tests/lint_markdown_link_eip.rs | 191 ++++++++++-------- eipw-lint/tests/lint_markdown_link_other.rs | 75 ++++--- 8 files changed, 250 insertions(+), 163 deletions(-) diff --git a/eipw-lint/src/lints/known_lints.rs b/eipw-lint/src/lints/known_lints.rs index fe55b89f..029bdbd7 100644 --- a/eipw-lint/src/lints/known_lints.rs +++ b/eipw-lint/src/lints/known_lints.rs @@ -54,13 +54,13 @@ pub enum DefaultLint { MarkdownHtmlComments(markdown::HtmlComments), MarkdownJsonSchema(markdown::JsonSchema), MarkdownLinkEip { - pattern: markdown::LinkEip + pattern: markdown::LinkEip, }, MarkdownLinkFirst { pattern: markdown::LinkFirst, }, MarkdownLinkOther { - pattern: markdown::LinkOther + pattern: markdown::LinkOther, }, MarkdownLinkStatus(markdown::LinkStatus), MarkdownProposalRef(markdown::ProposalRef), @@ -291,7 +291,7 @@ where prefix: l.prefix.as_ref(), suffix: l.suffix.as_ref(), }) - }, + } Self::MarkdownRegex(l) => DefaultLint::MarkdownRegex(markdown::Regex { message: l.message.as_ref(), mode: l.mode, diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs index e7c23ef4..7659aa29 100644 --- a/eipw-lint/src/lints/markdown/link_eip.rs +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -7,19 +7,19 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet}; use comrak::nodes::{Ast, NodeLink}; - + use crate::lints::{Context, Error, Lint}; use crate::tree::{self, Next, TraverseExt}; - + use regex::Regex; - + use serde::{Deserialize, Serialize}; - + use std::fmt::{Debug, Display}; - + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkEip(pub S); - + impl Lint for LinkEip where S: Display + Debug + AsRef, @@ -33,7 +33,10 @@ where re, slug, link_depth: 0, - current_link: Link { url: String::new(), text: String::new() }, + current_link: Link { + url: String::new(), + text: String::new(), + }, }; ctx.body().traverse().visit(&mut visitor)?; @@ -58,7 +61,10 @@ struct Visitor<'a, 'b, 'c> { impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { if let Some(captures) = re.captures(text) { - Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + Ok(captures + .get(index) + .map(|m| m.as_str().to_string()) + .unwrap_or_default()) } else { Ok(String::new()) } @@ -73,32 +79,45 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { description } - fn check(&self, ast: &Ast) -> Result { + fn check(&self, ast: &Ast) -> Result { let url_eip_text = self.extract_capture(&self.current_link.url, &self.re, 1)?; let url_eip_number = self.extract_capture(&self.current_link.url, &self.re, 2)?; let url_section = self.extract_capture(&self.current_link.url, &self.re, 4)?; - - let dynamic_pattern = if url_section != "" { + + let dynamic_pattern = if url_section != "" { format!(r"^(EIP|ERC)-{}(\s*\S+)", regex::escape(&url_eip_number)) } else { format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) }; let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; - + if text_re.is_match(&self.current_link.text) { return Ok(Next::TraverseChildren); }; - + let expected = if url_section != "" { let section_description = Visitor::transform_section_description(&url_section); - format!("[{}{}: {}]({})", url_eip_text.to_uppercase(), url_eip_number, section_description, &self.current_link.url) + format!( + "[{}{}: {}]({})", + url_eip_text.to_uppercase(), + url_eip_number, + section_description, + &self.current_link.url + ) } else { - format!("[{}{}]({})", url_eip_text.to_uppercase(), url_eip_number, &self.current_link.url) + format!( + "[{}{}]({})", + url_eip_text.to_uppercase(), + url_eip_number, + &self.current_link.url + ) }; let footer_label = format!("use `{}` instead", expected); - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + let source = self + .ctx + .source_for_text(ast.sourcepos.start.line, &self.current_link.text); self.ctx.report(Snippet { title: Some(Annotation { annotation_type: self.ctx.annotation_type(), @@ -123,15 +142,18 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { Ok(Next::TraverseChildren) } } - + impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { type Error = Error; - fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { - if self.re.is_match(&link.url) { - self.current_link = Link { url: link.url.to_owned(), text: String::new() }; - self.link_depth += 1; - } + fn enter_link(&mut self, _: &Ast, link: &NodeLink) -> Result { + if self.re.is_match(&link.url) { + self.current_link = Link { + url: link.url.to_owned(), + text: String::new(), + }; + self.link_depth += 1; + } Ok(Next::TraverseChildren) } @@ -145,8 +167,8 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { if self.link_depth > 0 { self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.check(ast)?; } - Ok(Next::SkipChildren) + Ok(Next::SkipChildren) } -} \ No newline at end of file +} diff --git a/eipw-lint/src/lints/markdown/link_other.rs b/eipw-lint/src/lints/markdown/link_other.rs index 358ac5ba..56c78bf1 100644 --- a/eipw-lint/src/lints/markdown/link_other.rs +++ b/eipw-lint/src/lints/markdown/link_other.rs @@ -7,19 +7,19 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet}; use comrak::nodes::{Ast, NodeLink}; - + use crate::lints::{Context, Error, Lint}; use crate::tree::{self, Next, TraverseExt}; - + use regex::Regex; - + use serde::{Deserialize, Serialize}; - + use std::fmt::{Debug, Display}; - + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkOther(pub S); - + impl Lint for LinkOther where S: Display + Debug + AsRef, @@ -33,7 +33,10 @@ where re, slug, link_depth: 0, - current_link: Link { url: String::new(), text: String::new() }, + current_link: Link { + url: String::new(), + text: String::new(), + }, }; ctx.body().traverse().visit(&mut visitor)?; @@ -58,15 +61,18 @@ struct Visitor<'a, 'b, 'c> { impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { if let Some(captures) = re.captures(text) { - Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + Ok(captures + .get(index) + .map(|m| m.as_str().to_string()) + .unwrap_or_default()) } else { Ok(String::new()) } } - fn check(&self, ast: &Ast) -> Result { - let text_eip_full = self.extract_capture(&self.current_link.text, &self.re, 1)?; - let text_eip_number = self.extract_capture(&self.current_link.text, &self.re, 2)?; + fn check(&self, ast: &Ast) -> Result { + let text_eip_full = self.extract_capture(&self.current_link.text, &self.re, 1)?; + let text_eip_number = self.extract_capture(&self.current_link.text, &self.re, 2)?; let dynamic_pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); let url_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; @@ -74,11 +80,21 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { if url_re.is_match(&self.current_link.url) { return Ok(Next::TraverseChildren); } - - let expected = format!("[{}](./{}.md)", text_eip_full.to_uppercase(), text_eip_full.to_lowercase()); - let footer_label = format!("the link destination should target {}, for example `{}`", text_eip_full.to_uppercase(), expected); - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + let expected = format!( + "[{}](./{}.md)", + text_eip_full.to_uppercase(), + text_eip_full.to_lowercase() + ); + let footer_label = format!( + "the link destination should target {}, for example `{}`", + text_eip_full.to_uppercase(), + expected + ); + + let source = self + .ctx + .source_for_text(ast.sourcepos.start.line, &self.current_link.text); self.ctx.report(Snippet { title: Some(Annotation { annotation_type: self.ctx.annotation_type(), @@ -107,9 +123,12 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { type Error = Error; - fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { - self.current_link = Link { url: link.url.to_owned(), text: String::new() }; - self.link_depth += 1; + fn enter_link(&mut self, _: &Ast, link: &NodeLink) -> Result { + self.current_link = Link { + url: link.url.to_owned(), + text: String::new(), + }; + self.link_depth += 1; Ok(Next::TraverseChildren) } @@ -121,10 +140,10 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { } fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { - if self.link_depth > 0 && self.re.is_match(&txt) { + if self.link_depth > 0 && self.re.is_match(&txt) { self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.check(ast)?; } - Ok(Next::TraverseChildren) + Ok(Next::TraverseChildren) } -} \ No newline at end of file +} diff --git a/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt b/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt index 3a8efd4f..1d9be445 100644 --- a/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt +++ b/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt @@ -7,6 +7,6 @@ error[markdown-link-status]: proposal `eip-20.md` is not stable enough for a `st error[markdown-link-status]: proposal `eip-2048.md` is not stable enough for a `status` of `Last Call` --> input.md | -21 | This is the specification for the EIP, [for some reason](./eip-2048.md). +21 | This is the specification for the EIP, [EIP-2048](./eip-2048.md). | = help: because of this link, this proposal's `status` must be one of: `Draft`, `Stagnant` diff --git a/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md b/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md index f4cd0ea3..05968262 100644 --- a/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md +++ b/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md @@ -18,7 +18,7 @@ This is the abstract for the EIP which needs [EIP-20](./eip-20.md). This is the motivation for the EIP, which links to [EIP-1337](./eip-1337.md). ## Specification -This is the specification for the EIP, [for some reason](./eip-2048.md). +This is the specification for the EIP, [EIP-2048](./eip-2048.md). ## Rationale This is the rationale for the EIP. diff --git a/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md b/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md index 54d972d6..91e46dc5 100644 --- a/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md +++ b/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md @@ -19,7 +19,7 @@ This is also the abstract for the EIP, which extends ERC-1236. ## Motivation This is the motivation for the EIP, which is separate from [EIP-1235](./eip-1235.md). -This is also the abstract for the EIP, which extends [ERC-1237](./eip-1236.md). +This is also the abstract for the EIP, which extends [ERC-1237](./eip-1237.md). ## Specification This is the specification for the EIP, mentioning EIP-1235 and ERC-1237 again. diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index cf0bfaab..6405f7d2 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -17,13 +17,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -44,13 +47,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -71,13 +77,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#.+)?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#.+)?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -98,13 +107,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -125,13 +137,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -152,13 +167,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -179,13 +197,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -198,7 +219,7 @@ header: value1 } #[tokio::test] -async fn eip_number_mismatch_extended_section_description_with_huphen() { +async fn eip_number_mismatch_extended_section_description_with_hyphen() { let src = r#"--- header: value1 --- @@ -206,13 +227,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -233,13 +257,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -260,13 +287,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -287,12 +317,15 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!(reports, ""); -} \ No newline at end of file +} diff --git a/eipw-lint/tests/lint_markdown_link_other.rs b/eipw-lint/tests/lint_markdown_link_other.rs index 8cd6c941..b19d4226 100644 --- a/eipw-lint/tests/lint_markdown_link_other.rs +++ b/eipw-lint/tests/lint_markdown_link_other.rs @@ -17,13 +17,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-other]: link text does not match link destination @@ -44,13 +47,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-other]: link text does not match link destination @@ -71,15 +77,19 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); - assert_eq!(reports, - r#"error[markdown-link-other]: link text does not match link destination + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-other]: link text does not match link destination | 4 | [EIP-1](../assets/eip-2/foo.txt) | @@ -97,12 +107,15 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!(reports, ""); -} \ No newline at end of file +} From 09d7de74228d6daffc408655b6bf7b479eb5e8c6 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 6 Jul 2024 11:32:12 -0400 Subject: [PATCH 05/14] Add test for repeated text in markdown-link-eip --- eipw-lint/tests/lint_markdown_link_eip.rs | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index 6405f7d2..eafecce0 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -129,7 +129,29 @@ header: value1 } #[tokio::test] -async fn link_text_missing_extended_section_description_with_huphen() { +async fn link_text_extended_section_description_with_bold() { + let src = r#"--- +header: value1 +--- +[EIP-1: eip motivation**EIP-1: eip motivation**](./eip-1.md#eip-motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_ne!(reports, ""); +} + +#[tokio::test] +async fn link_text_missing_extended_section_description_with_hyphen() { let src = r#"--- header: value1 --- From 0df9027657aa61d957e15c8a85eb1234f24a182d Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 6 Jul 2024 11:35:43 -0400 Subject: [PATCH 06/14] Add second test --- eipw-lint/tests/lint_markdown_link_eip.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index eafecce0..5c5ae66a 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -128,6 +128,28 @@ header: value1 ); } +#[tokio::test] +async fn link_text_with_bold() { + let src = r#"--- +header: value1 +--- +[EIP-1**EIP-1**](./eip-1.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_ne!(reports, ""); +} + #[tokio::test] async fn link_text_extended_section_description_with_bold() { let src = r#"--- From cdfc4d0a31b1918184b96538787af179b42ea758 Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Sat, 6 Jul 2024 21:11:45 +0300 Subject: [PATCH 07/14] add bold text check --- eipw-lint/src/lints/markdown/link_eip.rs | 11 +++++++---- eipw-lint/tests/lint_markdown_link_eip.rs | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs index 7659aa29..98f03e70 100644 --- a/eipw-lint/src/lints/markdown/link_eip.rs +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -33,6 +33,7 @@ where re, slug, link_depth: 0, + text_depth: 0, current_link: Link { url: String::new(), text: String::new(), @@ -55,6 +56,7 @@ struct Visitor<'a, 'b, 'c> { re: Regex, slug: &'c str, link_depth: usize, + text_depth: usize, current_link: Link, } @@ -91,7 +93,7 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { }; let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; - if text_re.is_match(&self.current_link.text) { + if text_re.is_match(&self.current_link.text) && self.text_depth <= 1 { return Ok(Next::TraverseChildren); }; @@ -157,8 +159,9 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { Ok(Next::TraverseChildren) } - fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { + fn depart_link(&mut self, ast: &Ast, _: &NodeLink) -> Result<(), Self::Error> { if self.link_depth > 0 { + self.check(ast)?; self.link_depth -= 1; } Ok(()) @@ -166,8 +169,8 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { if self.link_depth > 0 { - self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.text_depth += 1; + self.current_link.text.push_str(txt); } Ok(Next::SkipChildren) } diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index 5c5ae66a..7cc05bc1 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -147,7 +147,14 @@ header: value1 .await .unwrap() .into_inner(); - assert_ne!(reports, ""); + assert_eq!(reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1**EIP-1**](./eip-1.md) + | + = help: use `[EIP-1](./eip-1.md)` instead +"# + ); } #[tokio::test] @@ -169,7 +176,14 @@ header: value1 .await .unwrap() .into_inner(); - assert_ne!(reports, ""); + assert_eq!(reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1: eip motivation**EIP-1: eip motivation**](./eip-1.md#eip-motivation) + | + = help: use `[EIP-1: Eip motivation](./eip-1.md#eip-motivation)` instead +"# + ); } #[tokio::test] From 87724039aa76d2303ff8ab23ea2347eb391257b7 Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Thu, 30 May 2024 16:58:34 +0300 Subject: [PATCH 08/14] initial markdown-link-eip --- eipw-lint/src/lib.rs | 4 +++ eipw-lint/src/lints/known_lints.rs | 6 +++++ eipw-lint/src/lints/markdown.rs | 2 ++ eipw-lint/src/lints/markdown/link_eip.rs | 32 +++++++++++++++++++++++ eipw-lint/tests/lint_markdown_link_eip.rs | 28 ++++++++++++++++++++ 5 files changed, 72 insertions(+) create mode 100644 eipw-lint/src/lints/markdown/link_eip.rs create mode 100644 eipw-lint/tests/lint_markdown_link_eip.rs diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index f223bf2f..a27bceb4 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -419,6 +419,10 @@ pub fn default_lints_enum() -> impl Iterator { MarkdownHtmlComments(markdown::HtmlComments), MarkdownJsonSchema(markdown::JsonSchema), + MarkdownLinkEip(markdown::LinkEip), MarkdownLinkFirst { pattern: markdown::LinkFirst, }, @@ -105,6 +106,7 @@ where Self::MarkdownHtmlComments(l) => Box::new(l), Self::MarkdownJsonSchema(l) => Box::new(l), + Self::MarkdownLinkEip(l) => Box::new(l), Self::MarkdownLinkFirst { pattern } => Box::new(pattern), Self::MarkdownNoBackticks { pattern } => Box::new(pattern), Self::MarkdownLinkStatus(l) => Box::new(l), @@ -145,6 +147,7 @@ where Self::MarkdownHtmlComments(l) => l, Self::MarkdownJsonSchema(l) => l, + Self::MarkdownLinkEip(l) => l, Self::MarkdownLinkFirst { pattern } => pattern, Self::MarkdownNoBackticks { pattern } => pattern, Self::MarkdownLinkStatus(l) => l, @@ -262,6 +265,9 @@ where .map(|(a, b)| (a.as_ref(), b.as_ref())) .collect(), }), + Self::MarkdownLinkEip (l) => DefaultLint::MarkdownLinkEip(markdown::LinkEip ( + l.as_ref(), + )), Self::MarkdownLinkFirst { pattern } => DefaultLint::MarkdownLinkFirst { pattern: markdown::LinkFirst(pattern.0.as_ref()), }, diff --git a/eipw-lint/src/lints/markdown.rs b/eipw-lint/src/lints/markdown.rs index 264329e1..6c647f4a 100644 --- a/eipw-lint/src/lints/markdown.rs +++ b/eipw-lint/src/lints/markdown.rs @@ -7,6 +7,7 @@ pub mod headings_space; pub mod html_comments; pub mod json_schema; +pub mod link_eip; pub mod link_first; pub mod link_status; pub mod no_backticks; @@ -19,6 +20,7 @@ pub mod section_required; pub use self::headings_space::HeadingsSpace; pub use self::html_comments::HtmlComments; pub use self::json_schema::JsonSchema; +pub use self::link_eip::LinkEip; pub use self::link_first::LinkFirst; pub use self::link_status::LinkStatus; pub use self::no_backticks::NoBackticks; diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs new file mode 100644 index 00000000..bf637485 --- /dev/null +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -0,0 +1,32 @@ +/* + * 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/. + */ + +// use annotate_snippets::snippet::{Annotation, Slice, Snippet}; + +// use comrak::nodes::{Ast, AstNode, NodeCode, NodeCodeBlock, NodeHtmlBlock}; + +use crate::lints::{Context, Error, Lint}; // FetchContext +// use crate::tree::{self, Next, TraverseExt}; + +// use regex::Regex; + +use serde::{Deserialize, Serialize}; + +// use std::collections::HashSet; +use std::fmt::{Debug, Display}; +// use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct LinkEip(pub S); + +impl Lint for LinkEip +where + S: Display + Debug + AsRef, +{ + fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { + Ok(()) + } +} \ No newline at end of file diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs new file mode 100644 index 00000000..c312ffed --- /dev/null +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -0,0 +1,28 @@ +/* + * 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/. + */ + + use eipw_lint::lints::markdown::LinkEip; + use eipw_lint::reporters::Text; + use eipw_lint::Linter; + + #[tokio::test] + async fn link_matches_the_pattern() { + let src = r#"--- + header: value1 + --- + [EIP-1](./eip-2.md) + "#; + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip("EIP-1")) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + + // assert_eq!(reports, ""); + } \ No newline at end of file From 04b0e07868bc56996dba7106023cd7769d1a81e9 Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Tue, 25 Jun 2024 21:54:44 +0300 Subject: [PATCH 09/14] update markdown-link-eip, add markdown-link-other --- eipw-lint/src/lib.rs | 10 +- eipw-lint/src/lints/known_lints.rs | 24 ++- eipw-lint/src/lints/markdown.rs | 2 + eipw-lint/src/lints/markdown/link_eip.rs | 125 +++++++++++- eipw-lint/src/lints/markdown/link_other.rs | 124 ++++++++++++ eipw-lint/tests/lint_markdown_link_eip.rs | 204 ++++++++++++++++++-- eipw-lint/tests/lint_markdown_link_other.rs | 81 ++++++++ 7 files changed, 533 insertions(+), 37 deletions(-) create mode 100644 eipw-lint/src/lints/markdown/link_other.rs create mode 100644 eipw-lint/tests/lint_markdown_link_other.rs diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index a27bceb4..5f8e4123 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -421,7 +421,9 @@ pub fn default_lints_enum() -> impl Iterator impl Iterator { MarkdownHtmlComments(markdown::HtmlComments), MarkdownJsonSchema(markdown::JsonSchema), - MarkdownLinkEip(markdown::LinkEip), + MarkdownLinkEip { + pattern: markdown::LinkEip + }, MarkdownLinkFirst { pattern: markdown::LinkFirst, }, MarkdownNoBackticks { pattern: markdown::NoBackticks, }, + MarkdownLinkOther { + pattern: markdown::LinkOther, + }, MarkdownLinkStatus(markdown::LinkStatus), MarkdownProposalRef(markdown::ProposalRef), MarkdownRegex(markdown::Regex), @@ -106,7 +111,8 @@ where Self::MarkdownHtmlComments(l) => Box::new(l), Self::MarkdownJsonSchema(l) => Box::new(l), - Self::MarkdownLinkEip(l) => Box::new(l), + Self::MarkdownLinkEip { pattern } => Box::new(pattern), + Self::MarkdownLinkOther { pattern } => Box::new(pattern), Self::MarkdownLinkFirst { pattern } => Box::new(pattern), Self::MarkdownNoBackticks { pattern } => Box::new(pattern), Self::MarkdownLinkStatus(l) => Box::new(l), @@ -147,9 +153,10 @@ where Self::MarkdownHtmlComments(l) => l, Self::MarkdownJsonSchema(l) => l, - Self::MarkdownLinkEip(l) => l, + Self::MarkdownLinkEip { pattern } => pattern, Self::MarkdownLinkFirst { pattern } => pattern, Self::MarkdownNoBackticks { pattern } => pattern, + Self::MarkdownLinkOther { pattern } => pattern, Self::MarkdownLinkStatus(l) => l, Self::MarkdownProposalRef(l) => l, Self::MarkdownRegex(l) => l, @@ -265,9 +272,12 @@ where .map(|(a, b)| (a.as_ref(), b.as_ref())) .collect(), }), - Self::MarkdownLinkEip (l) => DefaultLint::MarkdownLinkEip(markdown::LinkEip ( - l.as_ref(), - )), + Self::MarkdownLinkEip { pattern } => DefaultLint::MarkdownLinkEip { + pattern: markdown::LinkEip(pattern.0.as_ref()), + }, + Self::MarkdownLinkOther { pattern } => DefaultLint::MarkdownLinkOther { + pattern: markdown::LinkOther(pattern.0.as_ref()), + }, Self::MarkdownLinkFirst { pattern } => DefaultLint::MarkdownLinkFirst { pattern: markdown::LinkFirst(pattern.0.as_ref()), }, @@ -289,7 +299,7 @@ where prefix: l.prefix.as_ref(), suffix: l.suffix.as_ref(), }) - } + }, Self::MarkdownRegex(l) => DefaultLint::MarkdownRegex(markdown::Regex { message: l.message.as_ref(), mode: l.mode, diff --git a/eipw-lint/src/lints/markdown.rs b/eipw-lint/src/lints/markdown.rs index 6c647f4a..1b3b1182 100644 --- a/eipw-lint/src/lints/markdown.rs +++ b/eipw-lint/src/lints/markdown.rs @@ -9,6 +9,7 @@ pub mod html_comments; pub mod json_schema; pub mod link_eip; pub mod link_first; +pub mod link_other; pub mod link_status; pub mod no_backticks; pub mod proposal_ref; @@ -22,6 +23,7 @@ pub use self::html_comments::HtmlComments; pub use self::json_schema::JsonSchema; pub use self::link_eip::LinkEip; pub use self::link_first::LinkFirst; +pub use self::link_other::LinkOther; pub use self::link_status::LinkStatus; pub use self::no_backticks::NoBackticks; pub use self::proposal_ref::ProposalRef; diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs index bf637485..a4963393 100644 --- a/eipw-lint/src/lints/markdown/link_eip.rs +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -4,22 +4,20 @@ * 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, AstNode, NodeCode, NodeCodeBlock, NodeHtmlBlock}; +use comrak::nodes::{Ast, NodeLink}; -use crate::lints::{Context, Error, Lint}; // FetchContext -// use crate::tree::{self, Next, TraverseExt}; +use crate::lints::{Context, Error, Lint}; +use crate::tree::{self, Next, TraverseExt}; -// use regex::Regex; +use regex::Regex; use serde::{Deserialize, Serialize}; -// use std::collections::HashSet; use std::fmt::{Debug, Display}; -// use std::path::PathBuf; -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkEip(pub S); impl Lint for LinkEip @@ -27,6 +25,117 @@ where S: Display + Debug + AsRef, { fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { + let pattern = self.0.as_ref(); + let re = Regex::new(pattern).map_err(Error::custom)?; + + let mut visitor = Visitor { + ctx, + re, + slug, + link_depth: 0, + current_link: Link { url: String::new(), text: String::new() }, + }; + ctx.body().traverse().visit(&mut visitor)?; + + Ok(()) + } +} +#[derive(Debug)] +struct Link { + url: String, + text: String, +} + +#[derive(Debug)] +struct Visitor<'a, 'b, 'c> { + ctx: &'c Context<'a, 'b>, + re: Regex, + slug: &'c str, + link_depth: usize, + current_link: Link, +} + +impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { + fn check(&self, ast: &Ast) -> Result { + let pattern = r"(?i)\b(?:eip|erc)-(\d+)\b"; + let url_re = Regex::new(pattern).map_err(Error::custom)?; + + let url_eip_number = if let Some(captures) = url_re.captures(&self.current_link.url) { + captures.get(1).map(|m| m.as_str()) + } else { None }; + + if let Some(url_eip_number) = url_eip_number { + let section_pattern = r"eip-([^.]*)\.md#.+$"; + let url_re = Regex::new(section_pattern).map_err(Error::custom)?; + let dynamic_pattern = if url_re.is_match(&self.current_link.url) { + format!(r"^(EIP|ERC)-{}\s*\S+", regex::escape(&url_eip_number)) + } else { + format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) + }; + let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; + + if text_re.is_match(&self.current_link.text) { + return Ok(Next::TraverseChildren); + }; + + let expected = if url_re.is_match(&self.current_link.url) { + format!("[EIP|ERC-{}]", url_eip_number) + } else { + format!("[EIP|ERC-{}]", url_eip_number) + }; + + let footer_label = format!("link text should match `{}`", expected); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Info, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; + } + + Ok(Next::TraverseChildren) + } +} + +impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { + type Error = Error; + + fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { + if self.re.is_match(&link.url) { + self.current_link = Link { url: link.url.to_owned(), text: String::new() }; + self.link_depth += 1; + } + Ok(Next::TraverseChildren) + } + + fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { + if self.link_depth > 0 { + self.link_depth = self.link_depth.checked_sub(1).unwrap(); + } Ok(()) } + + fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { + if self.link_depth > 0 { + self.current_link.text = txt.to_owned(); + self.check(ast)?; + } + Ok(Next::TraverseChildren) + } } \ No newline at end of file diff --git a/eipw-lint/src/lints/markdown/link_other.rs b/eipw-lint/src/lints/markdown/link_other.rs new file mode 100644 index 00000000..a4ad9ef0 --- /dev/null +++ b/eipw-lint/src/lints/markdown/link_other.rs @@ -0,0 +1,124 @@ +/* + * 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/. + */ + +use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet}; + +use comrak::nodes::{Ast, NodeLink}; + +use crate::lints::{Context, Error, Lint}; +use crate::tree::{self, Next, TraverseExt}; + +use regex::Regex; + +use serde::{Deserialize, Serialize}; + +use std::fmt::{Debug, Display}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LinkOther(pub S); + +impl Lint for LinkOther +where + S: Display + Debug + AsRef, +{ + fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> { + let pattern = self.0.as_ref(); + let re = Regex::new(pattern).map_err(Error::custom)?; + + let mut visitor = Visitor { + ctx, + re, + slug, + link_depth: 0, + current_link: Link { url: String::new(), text: String::new() }, + }; + ctx.body().traverse().visit(&mut visitor)?; + + Ok(()) + } +} +#[derive(Debug)] +struct Link { + url: String, + text: String, +} + +#[derive(Debug)] +struct Visitor<'a, 'b, 'c> { + ctx: &'c Context<'a, 'b>, + re: Regex, + slug: &'c str, + link_depth: usize, + current_link: Link, +} + +impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { + fn check(&self, ast: &Ast) -> Result { + let text_eip_number = if let Some(captures) = self.re.captures(&self.current_link.text) { + captures.get(2).map(|m| m.as_str()) + } else { None }; + + if let Some(text_eip_number) = text_eip_number { + let pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); + let re = Regex::new(&pattern).map_err(Error::custom)?; + + if re.is_match(&self.current_link.url) { + return Ok(Next::TraverseChildren); + } + + let footer_label = format!("link destinstion must match text EIP"); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Info, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; + } + + Ok(Next::TraverseChildren) + } +} + +impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { + type Error = Error; + + fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { + self.current_link = Link { url: link.url.to_owned(), text: String::new() }; + self.link_depth += 1; + Ok(Next::TraverseChildren) + } + + fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { + if self.link_depth > 0 { + self.link_depth = self.link_depth.checked_sub(1).unwrap(); + } + Ok(()) + } + + fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { + if self.link_depth > 0 && self.re.is_match(&txt) { + self.current_link.text = txt.to_owned(); + self.check(ast)?; + } + Ok(Next::TraverseChildren) + } +} \ No newline at end of file diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index c312ffed..3eac5dc2 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -4,25 +4,187 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - use eipw_lint::lints::markdown::LinkEip; - use eipw_lint::reporters::Text; - use eipw_lint::Linter; - - #[tokio::test] - async fn link_matches_the_pattern() { - let src = r#"--- - header: value1 - --- - [EIP-1](./eip-2.md) - "#; +use eipw_lint::lints::markdown::LinkEip; +use eipw_lint::reporters::Text; +use eipw_lint::Linter; + +#[tokio::test] +async fn eip_number_mismatch() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-2.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-2.md) + | + = info: link text should match `[EIP|ERC-2]` +"# + ); +} + +#[tokio::test] +async fn link_eip_has_no_section() { + let src = r#"--- +header: value1 +--- +[EIP-1: Foo](./eip-1.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1: Foo](./eip-1.md) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn link_text_missing_eip() { + let src = r#"--- +header: value1 +--- +[Another Proposal](./eip-1.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [Another Proposal](./eip-1.md) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn link_text_missing_section_description() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-1.md#motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-1.md#motivation) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn eip_number_mismatch_with_section() { + let src = r#"--- +header: value1 +--- +[EIP-2: Hello](./eip-1.md#abstract) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-2: Hello](./eip-1.md#abstract) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn link_text_missing_eip_with_section() { + let src = r#"--- +header: value1 +--- +[Another Proposal](./eip-1.md#rationale) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [Another Proposal](./eip-1.md#rationale) + | + = info: link text should match `[EIP|ERC-1]` +"# + ); +} + +#[tokio::test] +async fn should_be_ignored() { + let src = r#"--- +header: value1 +--- +[EIP-721's Motivation](./eip-721.md#motivation) +"#; + let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip("EIP-1")) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); - - // assert_eq!(reports, ""); - } \ No newline at end of file + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!(reports, ""); +} \ No newline at end of file diff --git a/eipw-lint/tests/lint_markdown_link_other.rs b/eipw-lint/tests/lint_markdown_link_other.rs new file mode 100644 index 00000000..727aceea --- /dev/null +++ b/eipw-lint/tests/lint_markdown_link_other.rs @@ -0,0 +1,81 @@ +/* + * 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/. + */ + +use eipw_lint::lints::markdown::LinkOther; +use eipw_lint::reporters::Text; +use eipw_lint::Linter; + +#[tokio::test] +async fn link_destination_missing_eip() { + let src = r#"--- +header: value1 +--- +[EIP-2](../assets/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-other]: link text does not match link destination + | +4 | [EIP-2](../assets/foo.txt) + | + = info: link destinstion must match text EIP +"# + ); +} + +#[tokio::test] +async fn link_eip_number_differs_from_text() { + let src = r#"--- +header: value1 +--- +[EIP-1](../assets/eip-2/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!(reports, + r#"error[markdown-link-other]: link text does not match link destination + | +4 | [EIP-1](../assets/eip-2/foo.txt) + | + = info: link destinstion must match text EIP +"# + ); +} + +#[tokio::test] +async fn should_be_ignored() { + let src = r#"--- +header: value1 +--- +[EIP-2](../assets/eip-2/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!(reports, ""); +} \ No newline at end of file From 47a8bbd63106aca55d4f288ccd8232f42d8b3f76 Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Fri, 5 Jul 2024 17:32:49 +0300 Subject: [PATCH 10/14] bug fixes --- eipw-lint/src/lib.rs | 4 +- eipw-lint/src/lints/markdown/link_eip.rs | 113 +++++++++-------- eipw-lint/src/lints/markdown/link_other.rs | 80 ++++++------ eipw-lint/tests/lint_markdown_link_eip.rs | 134 ++++++++++++++++++-- eipw-lint/tests/lint_markdown_link_other.rs | 37 +++++- 5 files changed, 260 insertions(+), 108 deletions(-) diff --git a/eipw-lint/src/lib.rs b/eipw-lint/src/lib.rs index 5f8e4123..8f3024a4 100644 --- a/eipw-lint/src/lib.rs +++ b/eipw-lint/src/lib.rs @@ -422,7 +422,7 @@ pub fn default_lints_enum() -> impl Iterator impl Iterator { } impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { - fn check(&self, ast: &Ast) -> Result { - let pattern = r"(?i)\b(?:eip|erc)-(\d+)\b"; - let url_re = Regex::new(pattern).map_err(Error::custom)?; + fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { + if let Some(captures) = re.captures(text) { + Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + } else { + Ok(String::new()) + } + } - let url_eip_number = if let Some(captures) = url_re.captures(&self.current_link.url) { - captures.get(1).map(|m| m.as_str()) - } else { None }; + fn transform_section_description(description: &str) -> String { + let re = Regex::new(r"[-_]").unwrap(); + let mut description = re.replace_all(description, " ").to_string(); + if let Some(first_char) = description.get_mut(0..1) { + first_char.make_ascii_uppercase(); + } + description + } - if let Some(url_eip_number) = url_eip_number { - let section_pattern = r"eip-([^.]*)\.md#.+$"; - let url_re = Regex::new(section_pattern).map_err(Error::custom)?; - let dynamic_pattern = if url_re.is_match(&self.current_link.url) { - format!(r"^(EIP|ERC)-{}\s*\S+", regex::escape(&url_eip_number)) - } else { - format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) - }; - let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; - - if text_re.is_match(&self.current_link.text) { - return Ok(Next::TraverseChildren); - }; - - let expected = if url_re.is_match(&self.current_link.url) { - format!("[EIP|ERC-{}]", url_eip_number) - } else { - format!("[EIP|ERC-{}]", url_eip_number) - }; + fn check(&self, ast: &Ast) -> Result { + let url_eip_text = self.extract_capture(&self.current_link.url, &self.re, 1)?; + let url_eip_number = self.extract_capture(&self.current_link.url, &self.re, 2)?; + let url_section = self.extract_capture(&self.current_link.url, &self.re, 4)?; - let footer_label = format!("link text should match `{}`", expected); + let dynamic_pattern = if url_section != "" { + format!(r"^(EIP|ERC)-{}(\s*\S+)", regex::escape(&url_eip_number)) + } else { + format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) + }; + let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; + + if text_re.is_match(&self.current_link.text) { + return Ok(Next::TraverseChildren); + }; - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); - self.ctx.report(Snippet { - title: Some(Annotation { - annotation_type: self.ctx.annotation_type(), - id: Some(self.slug), - label: Some("link text does not match link destination"), - }), - slices: vec![Slice { - fold: false, - line_start: ast.sourcepos.start.line, - origin: self.ctx.origin(), - source: &source, - annotations: vec![], - }], - footer: vec![Annotation { - id: None, - annotation_type: AnnotationType::Info, - label: Some(&footer_label), - }], - opt: Default::default(), - })?; - } + let expected = if url_section != "" { + let section_description = Visitor::transform_section_description(&url_section); + format!("[{}{}: {}]({})", url_eip_text.to_uppercase(), url_eip_number, section_description, &self.current_link.url) + } else { + format!("[{}{}]({})", url_eip_text.to_uppercase(), url_eip_number, &self.current_link.url) + }; + + let footer_label = format!("use `{}` instead", expected); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Help, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; Ok(Next::TraverseChildren) } } - + impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { type Error = Error; @@ -126,7 +137,7 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { if self.link_depth > 0 { - self.link_depth = self.link_depth.checked_sub(1).unwrap(); + self.link_depth -= 1; } Ok(()) } @@ -136,6 +147,6 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { self.current_link.text = txt.to_owned(); self.check(ast)?; } - Ok(Next::TraverseChildren) + Ok(Next::SkipChildren) } } \ No newline at end of file diff --git a/eipw-lint/src/lints/markdown/link_other.rs b/eipw-lint/src/lints/markdown/link_other.rs index a4ad9ef0..358ac5ba 100644 --- a/eipw-lint/src/lints/markdown/link_other.rs +++ b/eipw-lint/src/lints/markdown/link_other.rs @@ -56,43 +56,49 @@ struct Visitor<'a, 'b, 'c> { } impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { - fn check(&self, ast: &Ast) -> Result { - let text_eip_number = if let Some(captures) = self.re.captures(&self.current_link.text) { - captures.get(2).map(|m| m.as_str()) - } else { None }; + fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { + if let Some(captures) = re.captures(text) { + Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + } else { + Ok(String::new()) + } + } - if let Some(text_eip_number) = text_eip_number { - let pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); - let re = Regex::new(&pattern).map_err(Error::custom)?; + fn check(&self, ast: &Ast) -> Result { + let text_eip_full = self.extract_capture(&self.current_link.text, &self.re, 1)?; + let text_eip_number = self.extract_capture(&self.current_link.text, &self.re, 2)?; - if re.is_match(&self.current_link.url) { - return Ok(Next::TraverseChildren); - } - - let footer_label = format!("link destinstion must match text EIP"); - - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); - self.ctx.report(Snippet { - title: Some(Annotation { - annotation_type: self.ctx.annotation_type(), - id: Some(self.slug), - label: Some("link text does not match link destination"), - }), - slices: vec![Slice { - fold: false, - line_start: ast.sourcepos.start.line, - origin: self.ctx.origin(), - source: &source, - annotations: vec![], - }], - footer: vec![Annotation { - id: None, - annotation_type: AnnotationType::Info, - label: Some(&footer_label), - }], - opt: Default::default(), - })?; + let dynamic_pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); + let url_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; + + if url_re.is_match(&self.current_link.url) { + return Ok(Next::TraverseChildren); } + + let expected = format!("[{}](./{}.md)", text_eip_full.to_uppercase(), text_eip_full.to_lowercase()); + let footer_label = format!("the link destination should target {}, for example `{}`", text_eip_full.to_uppercase(), expected); + + let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + self.ctx.report(Snippet { + title: Some(Annotation { + annotation_type: self.ctx.annotation_type(), + id: Some(self.slug), + label: Some("link text does not match link destination"), + }), + slices: vec![Slice { + fold: false, + line_start: ast.sourcepos.start.line, + origin: self.ctx.origin(), + source: &source, + annotations: vec![], + }], + footer: vec![Annotation { + id: None, + annotation_type: AnnotationType::Help, + label: Some(&footer_label), + }], + opt: Default::default(), + })?; Ok(Next::TraverseChildren) } @@ -109,15 +115,15 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { if self.link_depth > 0 { - self.link_depth = self.link_depth.checked_sub(1).unwrap(); + self.link_depth -= 1; } Ok(()) } fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { - if self.link_depth > 0 && self.re.is_match(&txt) { + if self.link_depth > 0 && self.re.is_match(&txt) { self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.check(ast)?; } Ok(Next::TraverseChildren) } diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index 3eac5dc2..cf0bfaab 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -18,7 +18,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -30,7 +30,7 @@ header: value1 | 4 | [EIP-1](./eip-2.md) | - = info: link text should match `[EIP|ERC-2]` + = help: use `[EIP-2](./eip-2.md)` instead "# ); } @@ -45,7 +45,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -57,7 +57,7 @@ header: value1 | 4 | [EIP-1: Foo](./eip-1.md) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1](./eip-1.md)` instead "# ); } @@ -72,7 +72,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#.+)?$".to_string())) .check_slice(None, src) .run() .await @@ -84,7 +84,7 @@ header: value1 | 4 | [Another Proposal](./eip-1.md) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1](./eip-1.md)` instead "# ); } @@ -99,7 +99,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -111,7 +111,61 @@ header: value1 | 4 | [EIP-1](./eip-1.md#motivation) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1: Motivation](./eip-1.md#motivation)` instead +"# + ); +} + +#[tokio::test] +async fn link_text_missing_extended_section_description_with_huphen() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-1.md#eip-motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-1.md#eip-motivation) + | + = help: use `[EIP-1: Eip motivation](./eip-1.md#eip-motivation)` instead +"# + ); +} + +#[tokio::test] +async fn link_text_missing_extended_section_description_with_underscore() { + let src = r#"--- +header: value1 +--- +[EIP-1](./eip-1.md#eip_motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1](./eip-1.md#eip_motivation) + | + = help: use `[EIP-1: Eip motivation](./eip-1.md#eip_motivation)` instead "# ); } @@ -126,7 +180,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -138,7 +192,61 @@ header: value1 | 4 | [EIP-2: Hello](./eip-1.md#abstract) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1: Abstract](./eip-1.md#abstract)` instead +"# + ); +} + +#[tokio::test] +async fn eip_number_mismatch_extended_section_description_with_huphen() { + let src = r#"--- +header: value1 +--- +[EIP-2: Hello](./eip-1.md#hello-abstract) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-2: Hello](./eip-1.md#hello-abstract) + | + = help: use `[EIP-1: Hello abstract](./eip-1.md#hello-abstract)` instead +"# + ); +} + +#[tokio::test] +async fn eip_number_mismatch_extended_section_description_with_underscore() { + let src = r#"--- +header: value1 +--- +[EIP-2: Hello](./eip-1.md#hello_abstract) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-2: Hello](./eip-1.md#hello_abstract) + | + = help: use `[EIP-1: Hello abstract](./eip-1.md#hello_abstract)` instead "# ); } @@ -153,7 +261,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await @@ -165,7 +273,7 @@ header: value1 | 4 | [Another Proposal](./eip-1.md#rationale) | - = info: link text should match `[EIP|ERC-1]` + = help: use `[EIP-1: Rationale](./eip-1.md#rationale)` instead "# ); } @@ -180,7 +288,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-eip", LinkEip(r"eip-([^.]*)\.md(#.+)?$".to_string())) + .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) .check_slice(None, src) .run() .await diff --git a/eipw-lint/tests/lint_markdown_link_other.rs b/eipw-lint/tests/lint_markdown_link_other.rs index 727aceea..8cd6c941 100644 --- a/eipw-lint/tests/lint_markdown_link_other.rs +++ b/eipw-lint/tests/lint_markdown_link_other.rs @@ -18,7 +18,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) .check_slice(None, src) .run() .await @@ -30,7 +30,34 @@ header: value1 | 4 | [EIP-2](../assets/foo.txt) | - = info: link destinstion must match text EIP + = help: the link destination should target EIP-2, for example `[EIP-2](./eip-2.md)` +"# + ); +} + +#[tokio::test] +async fn link_destination_missing_eip_advanced_text() { + let src = r#"--- +header: value1 +--- +[EIP-2: Foo](../assets/foo.txt) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-other]: link text does not match link destination + | +4 | [EIP-2: Foo](../assets/foo.txt) + | + = help: the link destination should target EIP-2, for example `[EIP-2](./eip-2.md)` "# ); } @@ -45,7 +72,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) .check_slice(None, src) .run() .await @@ -56,7 +83,7 @@ header: value1 | 4 | [EIP-1](../assets/eip-2/foo.txt) | - = info: link destinstion must match text EIP + = help: the link destination should target EIP-1, for example `[EIP-1](./eip-1.md)` "# ); } @@ -71,7 +98,7 @@ header: value1 let reports = Linter::>::default() .clear_lints() - .deny("markdown-link-other", LinkOther(r"^(EIP|ERC)-(\d+)\s*\S*$".to_string())) + .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) .check_slice(None, src) .run() .await From ad8e022037c9234cfc0beaf7c674094bf9264def Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 6 Jul 2024 11:31:50 -0400 Subject: [PATCH 11/14] Format and fix tests --- eipw-lint/src/lints/known_lints.rs | 4 +- eipw-lint/src/lints/markdown/link_eip.rs | 72 ++++--- eipw-lint/src/lints/markdown/link_other.rs | 63 ++++-- .../markdown-link-too-unstable/expected.txt | 2 +- .../eipv/markdown-link-too-unstable/input.md | 2 +- .../tests/eipv/markdown-unlinked-eip/input.md | 2 +- eipw-lint/tests/lint_markdown_link_eip.rs | 191 ++++++++++-------- eipw-lint/tests/lint_markdown_link_other.rs | 75 ++++--- 8 files changed, 249 insertions(+), 162 deletions(-) diff --git a/eipw-lint/src/lints/known_lints.rs b/eipw-lint/src/lints/known_lints.rs index b4351b6d..73c6ce3d 100644 --- a/eipw-lint/src/lints/known_lints.rs +++ b/eipw-lint/src/lints/known_lints.rs @@ -54,7 +54,7 @@ pub enum DefaultLint { MarkdownHtmlComments(markdown::HtmlComments), MarkdownJsonSchema(markdown::JsonSchema), MarkdownLinkEip { - pattern: markdown::LinkEip + pattern: markdown::LinkEip, }, MarkdownLinkFirst { pattern: markdown::LinkFirst, @@ -299,7 +299,7 @@ where prefix: l.prefix.as_ref(), suffix: l.suffix.as_ref(), }) - }, + } Self::MarkdownRegex(l) => DefaultLint::MarkdownRegex(markdown::Regex { message: l.message.as_ref(), mode: l.mode, diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs index e7c23ef4..7659aa29 100644 --- a/eipw-lint/src/lints/markdown/link_eip.rs +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -7,19 +7,19 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet}; use comrak::nodes::{Ast, NodeLink}; - + use crate::lints::{Context, Error, Lint}; use crate::tree::{self, Next, TraverseExt}; - + use regex::Regex; - + use serde::{Deserialize, Serialize}; - + use std::fmt::{Debug, Display}; - + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkEip(pub S); - + impl Lint for LinkEip where S: Display + Debug + AsRef, @@ -33,7 +33,10 @@ where re, slug, link_depth: 0, - current_link: Link { url: String::new(), text: String::new() }, + current_link: Link { + url: String::new(), + text: String::new(), + }, }; ctx.body().traverse().visit(&mut visitor)?; @@ -58,7 +61,10 @@ struct Visitor<'a, 'b, 'c> { impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { if let Some(captures) = re.captures(text) { - Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + Ok(captures + .get(index) + .map(|m| m.as_str().to_string()) + .unwrap_or_default()) } else { Ok(String::new()) } @@ -73,32 +79,45 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { description } - fn check(&self, ast: &Ast) -> Result { + fn check(&self, ast: &Ast) -> Result { let url_eip_text = self.extract_capture(&self.current_link.url, &self.re, 1)?; let url_eip_number = self.extract_capture(&self.current_link.url, &self.re, 2)?; let url_section = self.extract_capture(&self.current_link.url, &self.re, 4)?; - - let dynamic_pattern = if url_section != "" { + + let dynamic_pattern = if url_section != "" { format!(r"^(EIP|ERC)-{}(\s*\S+)", regex::escape(&url_eip_number)) } else { format!(r"^(EIP|ERC)-{}$", regex::escape(&url_eip_number)) }; let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; - + if text_re.is_match(&self.current_link.text) { return Ok(Next::TraverseChildren); }; - + let expected = if url_section != "" { let section_description = Visitor::transform_section_description(&url_section); - format!("[{}{}: {}]({})", url_eip_text.to_uppercase(), url_eip_number, section_description, &self.current_link.url) + format!( + "[{}{}: {}]({})", + url_eip_text.to_uppercase(), + url_eip_number, + section_description, + &self.current_link.url + ) } else { - format!("[{}{}]({})", url_eip_text.to_uppercase(), url_eip_number, &self.current_link.url) + format!( + "[{}{}]({})", + url_eip_text.to_uppercase(), + url_eip_number, + &self.current_link.url + ) }; let footer_label = format!("use `{}` instead", expected); - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + let source = self + .ctx + .source_for_text(ast.sourcepos.start.line, &self.current_link.text); self.ctx.report(Snippet { title: Some(Annotation { annotation_type: self.ctx.annotation_type(), @@ -123,15 +142,18 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { Ok(Next::TraverseChildren) } } - + impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { type Error = Error; - fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { - if self.re.is_match(&link.url) { - self.current_link = Link { url: link.url.to_owned(), text: String::new() }; - self.link_depth += 1; - } + fn enter_link(&mut self, _: &Ast, link: &NodeLink) -> Result { + if self.re.is_match(&link.url) { + self.current_link = Link { + url: link.url.to_owned(), + text: String::new(), + }; + self.link_depth += 1; + } Ok(Next::TraverseChildren) } @@ -145,8 +167,8 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { if self.link_depth > 0 { self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.check(ast)?; } - Ok(Next::SkipChildren) + Ok(Next::SkipChildren) } -} \ No newline at end of file +} diff --git a/eipw-lint/src/lints/markdown/link_other.rs b/eipw-lint/src/lints/markdown/link_other.rs index 358ac5ba..56c78bf1 100644 --- a/eipw-lint/src/lints/markdown/link_other.rs +++ b/eipw-lint/src/lints/markdown/link_other.rs @@ -7,19 +7,19 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet}; use comrak::nodes::{Ast, NodeLink}; - + use crate::lints::{Context, Error, Lint}; use crate::tree::{self, Next, TraverseExt}; - + use regex::Regex; - + use serde::{Deserialize, Serialize}; - + use std::fmt::{Debug, Display}; - + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkOther(pub S); - + impl Lint for LinkOther where S: Display + Debug + AsRef, @@ -33,7 +33,10 @@ where re, slug, link_depth: 0, - current_link: Link { url: String::new(), text: String::new() }, + current_link: Link { + url: String::new(), + text: String::new(), + }, }; ctx.body().traverse().visit(&mut visitor)?; @@ -58,15 +61,18 @@ struct Visitor<'a, 'b, 'c> { impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { fn extract_capture(&self, text: &str, re: &Regex, index: usize) -> Result { if let Some(captures) = re.captures(text) { - Ok(captures.get(index).map(|m| m.as_str().to_string()).unwrap_or_default()) + Ok(captures + .get(index) + .map(|m| m.as_str().to_string()) + .unwrap_or_default()) } else { Ok(String::new()) } } - fn check(&self, ast: &Ast) -> Result { - let text_eip_full = self.extract_capture(&self.current_link.text, &self.re, 1)?; - let text_eip_number = self.extract_capture(&self.current_link.text, &self.re, 2)?; + fn check(&self, ast: &Ast) -> Result { + let text_eip_full = self.extract_capture(&self.current_link.text, &self.re, 1)?; + let text_eip_number = self.extract_capture(&self.current_link.text, &self.re, 2)?; let dynamic_pattern = format!(r"(?i)\beip-{}\b", regex::escape(&text_eip_number)); let url_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; @@ -74,11 +80,21 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { if url_re.is_match(&self.current_link.url) { return Ok(Next::TraverseChildren); } - - let expected = format!("[{}](./{}.md)", text_eip_full.to_uppercase(), text_eip_full.to_lowercase()); - let footer_label = format!("the link destination should target {}, for example `{}`", text_eip_full.to_uppercase(), expected); - let source = self.ctx.source_for_text(ast.sourcepos.start.line, &self.current_link.text); + let expected = format!( + "[{}](./{}.md)", + text_eip_full.to_uppercase(), + text_eip_full.to_lowercase() + ); + let footer_label = format!( + "the link destination should target {}, for example `{}`", + text_eip_full.to_uppercase(), + expected + ); + + let source = self + .ctx + .source_for_text(ast.sourcepos.start.line, &self.current_link.text); self.ctx.report(Snippet { title: Some(Annotation { annotation_type: self.ctx.annotation_type(), @@ -107,9 +123,12 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { type Error = Error; - fn enter_link(&mut self, _: &Ast, link: &NodeLink,) -> Result { - self.current_link = Link { url: link.url.to_owned(), text: String::new() }; - self.link_depth += 1; + fn enter_link(&mut self, _: &Ast, link: &NodeLink) -> Result { + self.current_link = Link { + url: link.url.to_owned(), + text: String::new(), + }; + self.link_depth += 1; Ok(Next::TraverseChildren) } @@ -121,10 +140,10 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { } fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { - if self.link_depth > 0 && self.re.is_match(&txt) { + if self.link_depth > 0 && self.re.is_match(&txt) { self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.check(ast)?; } - Ok(Next::TraverseChildren) + Ok(Next::TraverseChildren) } -} \ No newline at end of file +} diff --git a/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt b/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt index 3a8efd4f..1d9be445 100644 --- a/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt +++ b/eipw-lint/tests/eipv/markdown-link-too-unstable/expected.txt @@ -7,6 +7,6 @@ error[markdown-link-status]: proposal `eip-20.md` is not stable enough for a `st error[markdown-link-status]: proposal `eip-2048.md` is not stable enough for a `status` of `Last Call` --> input.md | -21 | This is the specification for the EIP, [for some reason](./eip-2048.md). +21 | This is the specification for the EIP, [EIP-2048](./eip-2048.md). | = help: because of this link, this proposal's `status` must be one of: `Draft`, `Stagnant` diff --git a/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md b/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md index f4cd0ea3..05968262 100644 --- a/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md +++ b/eipw-lint/tests/eipv/markdown-link-too-unstable/input.md @@ -18,7 +18,7 @@ This is the abstract for the EIP which needs [EIP-20](./eip-20.md). This is the motivation for the EIP, which links to [EIP-1337](./eip-1337.md). ## Specification -This is the specification for the EIP, [for some reason](./eip-2048.md). +This is the specification for the EIP, [EIP-2048](./eip-2048.md). ## Rationale This is the rationale for the EIP. diff --git a/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md b/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md index 54d972d6..91e46dc5 100644 --- a/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md +++ b/eipw-lint/tests/eipv/markdown-unlinked-eip/input.md @@ -19,7 +19,7 @@ This is also the abstract for the EIP, which extends ERC-1236. ## Motivation This is the motivation for the EIP, which is separate from [EIP-1235](./eip-1235.md). -This is also the abstract for the EIP, which extends [ERC-1237](./eip-1236.md). +This is also the abstract for the EIP, which extends [ERC-1237](./eip-1237.md). ## Specification This is the specification for the EIP, mentioning EIP-1235 and ERC-1237 again. diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index cf0bfaab..6405f7d2 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -17,13 +17,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -44,13 +47,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -71,13 +77,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#.+)?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#.+)?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -98,13 +107,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -125,13 +137,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -152,13 +167,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -179,13 +197,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -198,7 +219,7 @@ header: value1 } #[tokio::test] -async fn eip_number_mismatch_extended_section_description_with_huphen() { +async fn eip_number_mismatch_extended_section_description_with_hyphen() { let src = r#"--- header: value1 --- @@ -206,13 +227,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -233,13 +257,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -260,13 +287,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-eip]: link text does not match link destination @@ -287,12 +317,15 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-eip", LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!(reports, ""); -} \ No newline at end of file +} diff --git a/eipw-lint/tests/lint_markdown_link_other.rs b/eipw-lint/tests/lint_markdown_link_other.rs index 8cd6c941..b19d4226 100644 --- a/eipw-lint/tests/lint_markdown_link_other.rs +++ b/eipw-lint/tests/lint_markdown_link_other.rs @@ -17,13 +17,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-other]: link text does not match link destination @@ -44,13 +47,16 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!( reports, r#"error[markdown-link-other]: link text does not match link destination @@ -71,15 +77,19 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); - assert_eq!(reports, - r#"error[markdown-link-other]: link text does not match link destination + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_eq!( + reports, + r#"error[markdown-link-other]: link text does not match link destination | 4 | [EIP-1](../assets/eip-2/foo.txt) | @@ -97,12 +107,15 @@ header: value1 "#; let reports = Linter::>::default() - .clear_lints() - .deny("markdown-link-other", LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string())) - .check_slice(None, src) - .run() - .await - .unwrap() - .into_inner(); + .clear_lints() + .deny( + "markdown-link-other", + LinkOther(r"(?i)^((?:EIP|ERC)-(\d+)).*$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); assert_eq!(reports, ""); -} \ No newline at end of file +} From 0da655d6562f69d1e5e248124ec9936f86e0f55f Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 6 Jul 2024 11:32:12 -0400 Subject: [PATCH 12/14] Add test for repeated text in markdown-link-eip --- eipw-lint/tests/lint_markdown_link_eip.rs | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index 6405f7d2..eafecce0 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -129,7 +129,29 @@ header: value1 } #[tokio::test] -async fn link_text_missing_extended_section_description_with_huphen() { +async fn link_text_extended_section_description_with_bold() { + let src = r#"--- +header: value1 +--- +[EIP-1: eip motivation**EIP-1: eip motivation**](./eip-1.md#eip-motivation) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_ne!(reports, ""); +} + +#[tokio::test] +async fn link_text_missing_extended_section_description_with_hyphen() { let src = r#"--- header: value1 --- From 680beeaaa5e22ac3ceb59c13e3c23afbbc77cafd Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 6 Jul 2024 11:35:43 -0400 Subject: [PATCH 13/14] Add second test --- eipw-lint/tests/lint_markdown_link_eip.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index eafecce0..5c5ae66a 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -128,6 +128,28 @@ header: value1 ); } +#[tokio::test] +async fn link_text_with_bold() { + let src = r#"--- +header: value1 +--- +[EIP-1**EIP-1**](./eip-1.md) +"#; + + let reports = Linter::>::default() + .clear_lints() + .deny( + "markdown-link-eip", + LinkEip(r"(eip-)([^.]*)\.md(#(.+))?$".to_string()), + ) + .check_slice(None, src) + .run() + .await + .unwrap() + .into_inner(); + assert_ne!(reports, ""); +} + #[tokio::test] async fn link_text_extended_section_description_with_bold() { let src = r#"--- From 1cbd7155f5bc5678145d9285c4265ca7b0c0e81d Mon Sep 17 00:00:00 2001 From: Ekaterina Ryazantseva Date: Sat, 6 Jul 2024 21:11:45 +0300 Subject: [PATCH 14/14] add bold text check --- eipw-lint/src/lints/markdown/link_eip.rs | 11 +++++++---- eipw-lint/tests/lint_markdown_link_eip.rs | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/eipw-lint/src/lints/markdown/link_eip.rs b/eipw-lint/src/lints/markdown/link_eip.rs index 7659aa29..98f03e70 100644 --- a/eipw-lint/src/lints/markdown/link_eip.rs +++ b/eipw-lint/src/lints/markdown/link_eip.rs @@ -33,6 +33,7 @@ where re, slug, link_depth: 0, + text_depth: 0, current_link: Link { url: String::new(), text: String::new(), @@ -55,6 +56,7 @@ struct Visitor<'a, 'b, 'c> { re: Regex, slug: &'c str, link_depth: usize, + text_depth: usize, current_link: Link, } @@ -91,7 +93,7 @@ impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> { }; let text_re = Regex::new(&dynamic_pattern).map_err(Error::custom)?; - if text_re.is_match(&self.current_link.text) { + if text_re.is_match(&self.current_link.text) && self.text_depth <= 1 { return Ok(Next::TraverseChildren); }; @@ -157,8 +159,9 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { Ok(Next::TraverseChildren) } - fn depart_link(&mut self, _: &Ast, _: &NodeLink) -> Result<(), Self::Error> { + fn depart_link(&mut self, ast: &Ast, _: &NodeLink) -> Result<(), Self::Error> { if self.link_depth > 0 { + self.check(ast)?; self.link_depth -= 1; } Ok(()) @@ -166,8 +169,8 @@ impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> { fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result { if self.link_depth > 0 { - self.current_link.text = txt.to_owned(); - self.check(ast)?; + self.text_depth += 1; + self.current_link.text.push_str(txt); } Ok(Next::SkipChildren) } diff --git a/eipw-lint/tests/lint_markdown_link_eip.rs b/eipw-lint/tests/lint_markdown_link_eip.rs index 5c5ae66a..7cc05bc1 100644 --- a/eipw-lint/tests/lint_markdown_link_eip.rs +++ b/eipw-lint/tests/lint_markdown_link_eip.rs @@ -147,7 +147,14 @@ header: value1 .await .unwrap() .into_inner(); - assert_ne!(reports, ""); + assert_eq!(reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1**EIP-1**](./eip-1.md) + | + = help: use `[EIP-1](./eip-1.md)` instead +"# + ); } #[tokio::test] @@ -169,7 +176,14 @@ header: value1 .await .unwrap() .into_inner(); - assert_ne!(reports, ""); + assert_eq!(reports, + r#"error[markdown-link-eip]: link text does not match link destination + | +4 | [EIP-1: eip motivation**EIP-1: eip motivation**](./eip-1.md#eip-motivation) + | + = help: use `[EIP-1: Eip motivation](./eip-1.md#eip-motivation)` instead +"# + ); } #[tokio::test]