Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NoSmartQuotes lint to reject smart quotes in EIPs #130

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions eipw-lint/src/lints/known_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub enum DefaultLint<S> {
MarkdownNoBackticks {
pattern: markdown::NoBackticks<S>,
},
MarkdownNoSmartQuotes(markdown::NoSmartQuotes),
MarkdownLinkStatus(markdown::LinkStatus<S>),
MarkdownProposalRef(markdown::ProposalRef),
MarkdownRegex(markdown::Regex<S>),
Expand Down Expand Up @@ -108,6 +109,7 @@ where
Self::MarkdownJsonSchema(l) => Box::new(l),
Self::MarkdownLinkFirst { pattern } => Box::new(pattern),
Self::MarkdownNoBackticks { pattern } => Box::new(pattern),
Self::MarkdownNoSmartQuotes(l) => Box::new(l),
Self::MarkdownLinkStatus(l) => Box::new(l),
Self::MarkdownProposalRef(l) => Box::new(l),
Self::MarkdownRegex(l) => Box::new(l),
Expand Down Expand Up @@ -149,6 +151,7 @@ where
Self::MarkdownJsonSchema(l) => l,
Self::MarkdownLinkFirst { pattern } => pattern,
Self::MarkdownNoBackticks { pattern } => pattern,
Self::MarkdownNoSmartQuotes(l) => l,
Self::MarkdownLinkStatus(l) => l,
Self::MarkdownProposalRef(l) => l,
Self::MarkdownRegex(l) => l,
Expand Down Expand Up @@ -266,6 +269,7 @@ where
Self::MarkdownNoBackticks { pattern } => DefaultLint::MarkdownNoBackticks {
pattern: markdown::NoBackticks(pattern.0.as_ref()),
},
Self::MarkdownNoSmartQuotes(_l) => DefaultLint::MarkdownNoSmartQuotes(markdown::NoSmartQuotes),
Self::MarkdownLinkStatus(l) => DefaultLint::MarkdownLinkStatus(markdown::LinkStatus {
pattern: l.pattern.as_ref(),
status: l.status.as_ref(),
Expand Down
2 changes: 2 additions & 0 deletions eipw-lint/src/lints/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod json_schema;
pub mod link_first;
pub mod link_status;
pub mod no_backticks;
pub mod no_smart_quotes;
pub mod proposal_ref;
pub mod regex;
pub mod relative_links;
Expand All @@ -24,6 +25,7 @@ pub use self::json_schema::JsonSchema;
pub use self::link_first::LinkFirst;
pub use self::link_status::LinkStatus;
pub use self::no_backticks::NoBackticks;
pub use self::no_smart_quotes::NoSmartQuotes;
pub use self::proposal_ref::ProposalRef;
pub use self::regex::Regex;
pub use self::relative_links::RelativeLinks;
Expand Down
128 changes: 128 additions & 0 deletions eipw-lint/src/lints/markdown/no_smart_quotes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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_snippets::{Level, Snippet};

use comrak::nodes::{
Ast, NodeCode, NodeCodeBlock, NodeFootnoteDefinition, NodeFootnoteReference, NodeHtmlBlock,
NodeLink,
};

use crate::lints::{Context, Error, Lint};
use crate::tree::{self, Next, TraverseExt};
use crate::SnippetExt;

use ::regex::Regex;

use serde::{Deserialize, Serialize};

use std::fmt::Debug;

// Smart quotes to detect: " " ' ' - using Unicode code points and a fixed regex pattern
// for clarity in the source code
const SMART_QUOTES_PATTERN: &str = "[\u{201C}\u{201D}]|[\u{2018}\u{2019}]";

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct NoSmartQuotes;

impl Lint for NoSmartQuotes {
fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> {
let re = Regex::new(SMART_QUOTES_PATTERN).map_err(Error::custom)?;
let mut visitor = Visitor {
ctx,
re,
slug,
};
ctx.body().traverse().visit(&mut visitor)?;
Ok(())
}
}

struct Visitor<'a, 'b, 'c> {
ctx: &'c Context<'a, 'b>,
re: Regex,
slug: &'c str,
}

impl<'a, 'b, 'c> Visitor<'a, 'b, 'c> {
fn check(&self, ast: &Ast, text: &str) -> Result<Next, Error> {
if !self.re.is_match(text) {
return Ok(Next::TraverseChildren);
}

let footer_label = "Smart quotes detected: replace with straight quotes (\", ')";
let source = self.ctx.source_for_text(ast.sourcepos.start.line, text);
self.ctx.report(
self.ctx
.annotation_level()
.title("smart quotes are not allowed (use straight quotes instead)")
.id(self.slug)
.snippet(
Snippet::source(&source)
.fold(false)
.line_start(ast.sourcepos.start.line)
.origin_opt(self.ctx.origin()),
)
.footer(Level::Info.title(footer_label)),
)?;

Ok(Next::TraverseChildren)
}
}

impl<'a, 'b, 'c> tree::Visitor for Visitor<'a, 'b, 'c> {
type Error = Error;

fn enter_front_matter(&mut self, _: &Ast, _: &str) -> Result<Next, Self::Error> {
Ok(Next::SkipChildren)
}

fn enter_code(&mut self, ast: &Ast, code: &NodeCode) -> Result<Next, Self::Error> {
// Check code blocks for smart quotes which could be especially problematic
self.check(ast, &code.literal)
}

fn enter_code_block(&mut self, ast: &Ast, block: &NodeCodeBlock) -> Result<Next, Self::Error> {
// Check code blocks for smart quotes which could be especially problematic
self.check(ast, &block.literal)
}

fn enter_html_inline(&mut self, _: &Ast, _: &str) -> Result<Next, Self::Error> {
Ok(Next::SkipChildren)
}

fn enter_html_block(&mut self, _: &Ast, _: &NodeHtmlBlock) -> Result<Next, Self::Error> {
Ok(Next::SkipChildren)
}

fn enter_footnote_definition(
&mut self,
ast: &Ast,
defn: &NodeFootnoteDefinition,
) -> Result<Next, Self::Error> {
self.check(ast, &defn.name)
}

fn enter_text(&mut self, ast: &Ast, txt: &str) -> Result<Next, Self::Error> {
self.check(ast, txt)
}

fn enter_link(&mut self, ast: &Ast, link: &NodeLink) -> Result<Next, Self::Error> {
self.check(ast, &link.title)
}

fn enter_image(&mut self, ast: &Ast, link: &NodeLink) -> Result<Next, Self::Error> {
self.check(ast, &link.title)
}

fn enter_footnote_reference(
&mut self,
ast: &Ast,
refn: &NodeFootnoteReference,
) -> Result<Next, Self::Error> {
self.check(ast, &refn.name)
}
}
79 changes: 79 additions & 0 deletions eipw-lint/tests/lint_markdown_no_smart_quotes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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::NoSmartQuotes;
use eipw_lint::reporters::Text;
use eipw_lint::Linter;
use pretty_assertions::assert_eq;

#[tokio::test]
async fn basic_smart_quotes() {
// Using Unicode escape sequences for smart quotes
let src = "---
header: value1
---

This document uses \u{201C}smart quotes\u{201D} which should be flagged.
";

let linter = Linter::<Text<String>>::default()
.clear_lints()
.deny("markdown-no-smart-quotes", NoSmartQuotes);

let reports = linter
.check_slice(None, src)
.run()
.await
.unwrap()
.into_inner();

assert!(reports.contains("smart quotes are not allowed"));
}

#[tokio::test]
async fn no_smart_quotes() {
let src = "---
header: value1
---

This document uses \"straight quotes\" which are fine.
";

let reports = Linter::<Text<String>>::default()
.clear_lints()
.deny("markdown-no-smart-quotes", NoSmartQuotes)
.check_slice(None, src)
.run()
.await
.unwrap()
.into_inner();

assert_eq!(reports, "");
}

#[tokio::test]
async fn smart_single_quotes() {
// Using Unicode escape sequences for smart single quotes
let src = "---
header: value1
---

This document uses \u{2018}smart single quotes\u{2019} which should also be flagged.
";

let linter = Linter::<Text<String>>::default()
.clear_lints()
.deny("markdown-no-smart-quotes", NoSmartQuotes);

let reports = linter
.check_slice(None, src)
.run()
.await
.unwrap()
.into_inner();

assert!(reports.contains("smart quotes are not allowed"));
}