Skip to content

Commit

Permalink
fix(linter): docs for jsx-a11y/anchor-is-valid
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac committed Aug 5, 2024
1 parent a330773 commit 4c3c1d2
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 38 deletions.
4 changes: 2 additions & 2 deletions crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub use crate::{
fixer::FixKind,
frameworks::FrameworkFlags,
options::{AllowWarnDeny, LintOptions},
rule::{RuleCategory, RuleMeta, RuleWithSeverity},
rule::{RuleCategory, RuleFixMeta, RuleMeta, RuleWithSeverity},
service::{LintService, LintServiceOptions},
};
use crate::{
Expand Down Expand Up @@ -146,7 +146,7 @@ impl Linter {
pub fn print_rules<W: Write>(writer: &mut W) {
let table = RuleTable::new();
for section in table.sections {
writeln!(writer, "{}", section.render_markdown_table()).unwrap();
writeln!(writer, "{}", section.render_markdown_table(None)).unwrap();
}
writeln!(writer, "Default: {}", table.turned_on_by_default_count).unwrap();
writeln!(writer, "Total: {}", table.total).unwrap();
Expand Down
21 changes: 16 additions & 5 deletions crates/oxc_linter/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ impl fmt::Display for RuleCategory {

// NOTE: this could be packed into a single byte if we wanted. I don't think
// this is needed, but we could do it if it would have a performance impact.
/// Describes the auto-fixing capabilities of a [`Rule`].
/// Describes the auto-fixing capabilities of a `Rule`.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RuleFixMeta {
/// An auto-fix is not available.
Expand All @@ -132,14 +132,24 @@ pub enum RuleFixMeta {
}

impl RuleFixMeta {
/// Does this [`Rule`] have some kind of auto-fix available?
#[inline]
pub fn is_none(self) -> bool {
matches!(self, Self::None)
}

/// Does this `Rule` have some kind of auto-fix available?
///
/// Also returns `true` for suggestions.
#[inline]
pub fn has_fix(self) -> bool {
matches!(self, Self::Fixable(_) | Self::Conditional(_))
}

#[inline]
pub fn is_pending(self) -> bool {
matches!(self, Self::FixPending)
}

pub fn supports_fix(self, kind: FixKind) -> bool {
matches!(self, Self::Fixable(fix_kind) | Self::Conditional(fix_kind) if fix_kind.can_apply(kind))
}
Expand All @@ -163,9 +173,10 @@ impl RuleFixMeta {
let mut message =
if kind.is_dangerous() { format!("dangerous {noun}") } else { noun.into() };

let article = match message.chars().next().unwrap() {
'a' | 'e' | 'i' | 'o' | 'u' => "An",
_ => "A",
let article = match message.chars().next() {
Some('a' | 'e' | 'i' | 'o' | 'u') => "An",
Some(_) => "A",
None => unreachable!(),
};

if matches!(self, Self::Conditional(_)) {
Expand Down
24 changes: 5 additions & 19 deletions crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct AnchorIsValidConfig {

declare_oxc_lint!(
/// ### What it does
/// The HTML <a> element, with a valid href attribute, is formally defined as representing a **hyperlink**.
/// The HTML `<a>` element, with a valid href attribute, is formally defined as representing a **hyperlink**.
/// That is, a link between one HTML document and another, or between one location inside an HTML document and another location inside the same document.
///
/// While before it was possible to attach logic to an anchor element, with the advent of JSX libraries,
Expand All @@ -56,15 +56,15 @@ declare_oxc_lint!(
///
/// Consider the following:
///
/// ```javascript
/// ```jsx
/// <a href="javascript:void(0)" onClick={foo}>Perform action</a>
/// <a href="#" onClick={foo}>Perform action</a>
/// <a onClick={foo}>Perform action</a>
/// ````
///
/// All these anchor implementations indicate that the element is only used to execute JavaScript code. All the above should be replaced with:
///
/// ```javascript
/// ```jsx
/// <button onClick={foo}>Perform action</button>
/// ```
/// `
Expand All @@ -78,33 +78,19 @@ declare_oxc_lint!(
///
/// #### Valid
///
/// ```javascript
/// ```jsx
/// <a href={`https://www.javascript.com`}>navigate here</a>
/// ```
///
/// ```javascript
/// <a href={somewhere}>navigate here</a>
/// ```
///
/// ```javascript
/// <a {...spread}>navigate here</a>
/// ```
///
/// #### Invalid
///
/// ```javascript
/// ```jsx
/// <a href={null}>navigate here</a>
/// ```
/// ```javascript
/// <a href={undefined}>navigate here</a>
/// ```
/// ```javascript
/// <a href>navigate here</a>
/// ```
/// ```javascript
/// <a href="javascript:void(0)">navigate here</a>
/// ```
/// ```javascript
/// <a href="https://example.com" onClick={something}>navigate here</a>
/// ```
///
Expand Down
19 changes: 15 additions & 4 deletions crates/oxc_linter/src/table.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::fmt::Write;
use std::{borrow::Cow, fmt::Write};

use rustc_hash::{FxHashMap, FxHashSet};

use crate::{rules::RULES, Linter, RuleCategory};
use crate::{rules::RULES, Linter, RuleCategory, RuleFixMeta};

pub struct RuleTable {
pub sections: Vec<RuleTableSection>,
Expand All @@ -23,6 +23,7 @@ pub struct RuleTableRow {
pub category: RuleCategory,
pub documentation: Option<&'static str>,
pub turned_on_by_default: bool,
pub autofix: RuleFixMeta,
}

impl Default for RuleTable {
Expand All @@ -49,6 +50,7 @@ impl RuleTable {
plugin: rule.plugin_name().to_string(),
category: rule.category(),
turned_on_by_default: default_rules.contains(name),
autofix: rule.fix(),
}
})
.collect::<Vec<_>>();
Expand Down Expand Up @@ -88,7 +90,11 @@ impl RuleTable {
}

impl RuleTableSection {
pub fn render_markdown_table(&self) -> String {
/// Renders all the rules in this section as a markdown table.
///
/// Provide [`Some`] prefix to render the rule name as a link. Provide
/// [`None`] to just display the rule name as text.
pub fn render_markdown_table(&self, link_prefix: Option<&str>) -> String {
let mut s = String::new();
let category = &self.category;
let rows = &self.rows;
Expand All @@ -108,7 +114,12 @@ impl RuleTableSection {
let plugin_name = &row.plugin;
let (default, default_width) =
if row.turned_on_by_default { ("✅", 6) } else { ("", 7) };
writeln!(s, "| {rule_name:<rule_width$} | {plugin_name:<plugin_width$} | {default:<default_width$} |").unwrap();
let rendered_name = if let Some(prefix) = link_prefix {
Cow::Owned(format!("[{rule_name}]({prefix}/{plugin_name}/{rule_name}.html)"))
} else {
Cow::Borrowed(rule_name)
};
writeln!(s, "| {rendered_name:<rule_width$} | {plugin_name:<plugin_width$} | {default:<default_width$} |").unwrap();
}

s
Expand Down
57 changes: 57 additions & 0 deletions tasks/website/src/linter/rules/doc_page.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Create documentation pages for each rule. Pages are printed as Markdown and
//! get added to the website.

use oxc_linter::{table::RuleTableRow, RuleFixMeta};
use std::fmt::{self, Write};

use crate::linter::rules::html::HtmlWriter;

pub fn render_rule_docs_page(rule: &RuleTableRow) -> Result<String, fmt::Error> {
const APPROX_FIX_CATEGORY_AND_PLUGIN_LEN: usize = 512;
let RuleTableRow { name, documentation, plugin, turned_on_by_default, autofix, .. } = rule;

let mut page = HtmlWriter::with_capacity(
documentation.map_or(0, str::len) + name.len() + APPROX_FIX_CATEGORY_AND_PLUGIN_LEN,
);

writeln!(
page,
"<!-- This file is auto-generated by {}. Do not edit it manually. -->\n",
file!()
)?;
writeln!(page, "# {plugin}/{name}\n")?;

// rule metadata
page.div(r#"class="rule-meta""#, |p| {
if *turned_on_by_default {
p.span(r#"class="default-on""#, |p| {
p.writeln("✅ This rule is turned on by default.")
})?;
}

if let Some(emoji) = fix_emoji(*autofix) {
p.span(r#"class="fix""#, |p| {
p.writeln(format!("{} {}", emoji, autofix.description()))
})?;
}

Ok(())
})?;

// rule documentation
if let Some(docs) = documentation {
writeln!(page, "\n{}", *docs)?;
}

// TODO: link to rule source

Ok(page.into())
}

fn fix_emoji(fix: RuleFixMeta) -> Option<&'static str> {
match fix {
RuleFixMeta::None => None,
RuleFixMeta::FixPending => Some("🚧"),
RuleFixMeta::Conditional(_) | RuleFixMeta::Fixable(_) => Some("🛠️"),
}
}
121 changes: 121 additions & 0 deletions tasks/website/src/linter/rules/html.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::{
cell::RefCell,
fmt::{self, Write},
};

#[derive(Debug)]
pub(crate) struct HtmlWriter {
inner: RefCell<String>,
}

impl fmt::Write for HtmlWriter {
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
self.inner.get_mut().write_fmt(args)
}

#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
self.inner.get_mut().write_char(c)
}

#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.inner.get_mut().write_str(s)
}
}

impl From<HtmlWriter> for String {
#[inline]
fn from(html: HtmlWriter) -> Self {
html.into_inner()
}
}

impl HtmlWriter {
pub fn new() -> Self {
Self { inner: RefCell::new(String::new()) }
}

pub fn with_capacity(capacity: usize) -> Self {
Self { inner: RefCell::new(String::with_capacity(capacity)) }
}

pub fn writeln<S: AsRef<str>>(&self, line: S) -> fmt::Result {
writeln!(self.inner.borrow_mut(), "{}", line.as_ref())
}

pub fn into_inner(self) -> String {
self.inner.into_inner()
}

pub fn html<F>(&self, tag: &'static str, attrs: &str, inner: F) -> fmt::Result
where
F: FnOnce(&Self) -> fmt::Result,
{
// Allocate space for the HTML being printed
let write_amt_guess = {
// opening tag. 2 extra for '<' and '>'
2 + tag.len() + attrs.len() +
// approximate inner content length
256 +
// closing tag. 3 extra for '</' and '>'
3 + tag.len()
};
let mut s = self.inner.borrow_mut();
s.reserve(write_amt_guess);

// Write the opening tag
write!(s, "<{tag}")?;
if attrs.is_empty() {
writeln!(s, ">")?;
} else {
writeln!(s, " {attrs}>")?;
}

// Callback produces the inner content
drop(s);
inner(self)?;

// Write the closing tag
writeln!(self.inner.borrow_mut(), "</{tag}>")?;

Ok(())
}
}

macro_rules! make_tag {
($name:ident) => {
impl HtmlWriter {
#[inline]
pub fn $name<F>(&self, attrs: &str, inner: F) -> fmt::Result
where
F: FnOnce(&Self) -> fmt::Result,
{
self.html(stringify!($name), attrs, inner)
}
}
};
}

make_tag!(div);
make_tag!(span);

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_div() {
let html = HtmlWriter::new();
html.div("", |html| html.writeln("Hello, world!")).unwrap();

assert_eq!(
html.into_inner().as_str(),
"<div>
Hello, world!
</div>
"
);
}
}
Loading

0 comments on commit 4c3c1d2

Please sign in to comment.