Skip to content

Commit 4c3c1d2

Browse files
committed
fix(linter): docs for jsx-a11y/anchor-is-valid
1 parent a330773 commit 4c3c1d2

File tree

9 files changed

+311
-38
lines changed

9 files changed

+311
-38
lines changed

crates/oxc_linter/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub use crate::{
3131
fixer::FixKind,
3232
frameworks::FrameworkFlags,
3333
options::{AllowWarnDeny, LintOptions},
34-
rule::{RuleCategory, RuleMeta, RuleWithSeverity},
34+
rule::{RuleCategory, RuleFixMeta, RuleMeta, RuleWithSeverity},
3535
service::{LintService, LintServiceOptions},
3636
};
3737
use crate::{
@@ -146,7 +146,7 @@ impl Linter {
146146
pub fn print_rules<W: Write>(writer: &mut W) {
147147
let table = RuleTable::new();
148148
for section in table.sections {
149-
writeln!(writer, "{}", section.render_markdown_table()).unwrap();
149+
writeln!(writer, "{}", section.render_markdown_table(None)).unwrap();
150150
}
151151
writeln!(writer, "Default: {}", table.turned_on_by_default_count).unwrap();
152152
writeln!(writer, "Total: {}", table.total).unwrap();

crates/oxc_linter/src/rule.rs

+16-5
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ impl fmt::Display for RuleCategory {
117117

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

134134
impl RuleFixMeta {
135-
/// Does this [`Rule`] have some kind of auto-fix available?
135+
#[inline]
136+
pub fn is_none(self) -> bool {
137+
matches!(self, Self::None)
138+
}
139+
140+
/// Does this `Rule` have some kind of auto-fix available?
136141
///
137142
/// Also returns `true` for suggestions.
138143
#[inline]
139144
pub fn has_fix(self) -> bool {
140145
matches!(self, Self::Fixable(_) | Self::Conditional(_))
141146
}
142147

148+
#[inline]
149+
pub fn is_pending(self) -> bool {
150+
matches!(self, Self::FixPending)
151+
}
152+
143153
pub fn supports_fix(self, kind: FixKind) -> bool {
144154
matches!(self, Self::Fixable(fix_kind) | Self::Conditional(fix_kind) if fix_kind.can_apply(kind))
145155
}
@@ -163,9 +173,10 @@ impl RuleFixMeta {
163173
let mut message =
164174
if kind.is_dangerous() { format!("dangerous {noun}") } else { noun.into() };
165175

166-
let article = match message.chars().next().unwrap() {
167-
'a' | 'e' | 'i' | 'o' | 'u' => "An",
168-
_ => "A",
176+
let article = match message.chars().next() {
177+
Some('a' | 'e' | 'i' | 'o' | 'u') => "An",
178+
Some(_) => "A",
179+
None => unreachable!(),
169180
};
170181

171182
if matches!(self, Self::Conditional(_)) {

crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs

+5-19
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct AnchorIsValidConfig {
4141

4242
declare_oxc_lint!(
4343
/// ### What it does
44-
/// The HTML <a> element, with a valid href attribute, is formally defined as representing a **hyperlink**.
44+
/// The HTML `<a>` element, with a valid href attribute, is formally defined as representing a **hyperlink**.
4545
/// 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.
4646
///
4747
/// While before it was possible to attach logic to an anchor element, with the advent of JSX libraries,
@@ -56,15 +56,15 @@ declare_oxc_lint!(
5656
///
5757
/// Consider the following:
5858
///
59-
/// ```javascript
59+
/// ```jsx
6060
/// <a href="javascript:void(0)" onClick={foo}>Perform action</a>
6161
/// <a href="#" onClick={foo}>Perform action</a>
6262
/// <a onClick={foo}>Perform action</a>
6363
/// ````
6464
///
6565
/// All these anchor implementations indicate that the element is only used to execute JavaScript code. All the above should be replaced with:
6666
///
67-
/// ```javascript
67+
/// ```jsx
6868
/// <button onClick={foo}>Perform action</button>
6969
/// ```
7070
/// `
@@ -78,33 +78,19 @@ declare_oxc_lint!(
7878
///
7979
/// #### Valid
8080
///
81-
/// ```javascript
81+
/// ```jsx
8282
/// <a href={`https://www.javascript.com`}>navigate here</a>
83-
/// ```
84-
///
85-
/// ```javascript
8683
/// <a href={somewhere}>navigate here</a>
87-
/// ```
88-
///
89-
/// ```javascript
9084
/// <a {...spread}>navigate here</a>
9185
/// ```
9286
///
9387
/// #### Invalid
9488
///
95-
/// ```javascript
89+
/// ```jsx
9690
/// <a href={null}>navigate here</a>
97-
/// ```
98-
/// ```javascript
9991
/// <a href={undefined}>navigate here</a>
100-
/// ```
101-
/// ```javascript
10292
/// <a href>navigate here</a>
103-
/// ```
104-
/// ```javascript
10593
/// <a href="javascript:void(0)">navigate here</a>
106-
/// ```
107-
/// ```javascript
10894
/// <a href="https://example.com" onClick={something}>navigate here</a>
10995
/// ```
11096
///

crates/oxc_linter/src/table.rs

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use std::fmt::Write;
1+
use std::{borrow::Cow, fmt::Write};
22

33
use rustc_hash::{FxHashMap, FxHashSet};
44

5-
use crate::{rules::RULES, Linter, RuleCategory};
5+
use crate::{rules::RULES, Linter, RuleCategory, RuleFixMeta};
66

77
pub struct RuleTable {
88
pub sections: Vec<RuleTableSection>,
@@ -23,6 +23,7 @@ pub struct RuleTableRow {
2323
pub category: RuleCategory,
2424
pub documentation: Option<&'static str>,
2525
pub turned_on_by_default: bool,
26+
pub autofix: RuleFixMeta,
2627
}
2728

2829
impl Default for RuleTable {
@@ -49,6 +50,7 @@ impl RuleTable {
4950
plugin: rule.plugin_name().to_string(),
5051
category: rule.category(),
5152
turned_on_by_default: default_rules.contains(name),
53+
autofix: rule.fix(),
5254
}
5355
})
5456
.collect::<Vec<_>>();
@@ -88,7 +90,11 @@ impl RuleTable {
8890
}
8991

9092
impl RuleTableSection {
91-
pub fn render_markdown_table(&self) -> String {
93+
/// Renders all the rules in this section as a markdown table.
94+
///
95+
/// Provide [`Some`] prefix to render the rule name as a link. Provide
96+
/// [`None`] to just display the rule name as text.
97+
pub fn render_markdown_table(&self, link_prefix: Option<&str>) -> String {
9298
let mut s = String::new();
9399
let category = &self.category;
94100
let rows = &self.rows;
@@ -108,7 +114,12 @@ impl RuleTableSection {
108114
let plugin_name = &row.plugin;
109115
let (default, default_width) =
110116
if row.turned_on_by_default { ("✅", 6) } else { ("", 7) };
111-
writeln!(s, "| {rule_name:<rule_width$} | {plugin_name:<plugin_width$} | {default:<default_width$} |").unwrap();
117+
let rendered_name = if let Some(prefix) = link_prefix {
118+
Cow::Owned(format!("[{rule_name}]({prefix}/{plugin_name}/{rule_name}.html)"))
119+
} else {
120+
Cow::Borrowed(rule_name)
121+
};
122+
writeln!(s, "| {rendered_name:<rule_width$} | {plugin_name:<plugin_width$} | {default:<default_width$} |").unwrap();
112123
}
113124

114125
s
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//! Create documentation pages for each rule. Pages are printed as Markdown and
2+
//! get added to the website.
3+
4+
use oxc_linter::{table::RuleTableRow, RuleFixMeta};
5+
use std::fmt::{self, Write};
6+
7+
use crate::linter::rules::html::HtmlWriter;
8+
9+
pub fn render_rule_docs_page(rule: &RuleTableRow) -> Result<String, fmt::Error> {
10+
const APPROX_FIX_CATEGORY_AND_PLUGIN_LEN: usize = 512;
11+
let RuleTableRow { name, documentation, plugin, turned_on_by_default, autofix, .. } = rule;
12+
13+
let mut page = HtmlWriter::with_capacity(
14+
documentation.map_or(0, str::len) + name.len() + APPROX_FIX_CATEGORY_AND_PLUGIN_LEN,
15+
);
16+
17+
writeln!(
18+
page,
19+
"<!-- This file is auto-generated by {}. Do not edit it manually. -->\n",
20+
file!()
21+
)?;
22+
writeln!(page, "# {plugin}/{name}\n")?;
23+
24+
// rule metadata
25+
page.div(r#"class="rule-meta""#, |p| {
26+
if *turned_on_by_default {
27+
p.span(r#"class="default-on""#, |p| {
28+
p.writeln("✅ This rule is turned on by default.")
29+
})?;
30+
}
31+
32+
if let Some(emoji) = fix_emoji(*autofix) {
33+
p.span(r#"class="fix""#, |p| {
34+
p.writeln(format!("{} {}", emoji, autofix.description()))
35+
})?;
36+
}
37+
38+
Ok(())
39+
})?;
40+
41+
// rule documentation
42+
if let Some(docs) = documentation {
43+
writeln!(page, "\n{}", *docs)?;
44+
}
45+
46+
// TODO: link to rule source
47+
48+
Ok(page.into())
49+
}
50+
51+
fn fix_emoji(fix: RuleFixMeta) -> Option<&'static str> {
52+
match fix {
53+
RuleFixMeta::None => None,
54+
RuleFixMeta::FixPending => Some("🚧"),
55+
RuleFixMeta::Conditional(_) | RuleFixMeta::Fixable(_) => Some("🛠️"),
56+
}
57+
}
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use std::{
2+
cell::RefCell,
3+
fmt::{self, Write},
4+
};
5+
6+
#[derive(Debug)]
7+
pub(crate) struct HtmlWriter {
8+
inner: RefCell<String>,
9+
}
10+
11+
impl fmt::Write for HtmlWriter {
12+
#[inline]
13+
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
14+
self.inner.get_mut().write_fmt(args)
15+
}
16+
17+
#[inline]
18+
fn write_char(&mut self, c: char) -> fmt::Result {
19+
self.inner.get_mut().write_char(c)
20+
}
21+
22+
#[inline]
23+
fn write_str(&mut self, s: &str) -> fmt::Result {
24+
self.inner.get_mut().write_str(s)
25+
}
26+
}
27+
28+
impl From<HtmlWriter> for String {
29+
#[inline]
30+
fn from(html: HtmlWriter) -> Self {
31+
html.into_inner()
32+
}
33+
}
34+
35+
impl HtmlWriter {
36+
pub fn new() -> Self {
37+
Self { inner: RefCell::new(String::new()) }
38+
}
39+
40+
pub fn with_capacity(capacity: usize) -> Self {
41+
Self { inner: RefCell::new(String::with_capacity(capacity)) }
42+
}
43+
44+
pub fn writeln<S: AsRef<str>>(&self, line: S) -> fmt::Result {
45+
writeln!(self.inner.borrow_mut(), "{}", line.as_ref())
46+
}
47+
48+
pub fn into_inner(self) -> String {
49+
self.inner.into_inner()
50+
}
51+
52+
pub fn html<F>(&self, tag: &'static str, attrs: &str, inner: F) -> fmt::Result
53+
where
54+
F: FnOnce(&Self) -> fmt::Result,
55+
{
56+
// Allocate space for the HTML being printed
57+
let write_amt_guess = {
58+
// opening tag. 2 extra for '<' and '>'
59+
2 + tag.len() + attrs.len() +
60+
// approximate inner content length
61+
256 +
62+
// closing tag. 3 extra for '</' and '>'
63+
3 + tag.len()
64+
};
65+
let mut s = self.inner.borrow_mut();
66+
s.reserve(write_amt_guess);
67+
68+
// Write the opening tag
69+
write!(s, "<{tag}")?;
70+
if attrs.is_empty() {
71+
writeln!(s, ">")?;
72+
} else {
73+
writeln!(s, " {attrs}>")?;
74+
}
75+
76+
// Callback produces the inner content
77+
drop(s);
78+
inner(self)?;
79+
80+
// Write the closing tag
81+
writeln!(self.inner.borrow_mut(), "</{tag}>")?;
82+
83+
Ok(())
84+
}
85+
}
86+
87+
macro_rules! make_tag {
88+
($name:ident) => {
89+
impl HtmlWriter {
90+
#[inline]
91+
pub fn $name<F>(&self, attrs: &str, inner: F) -> fmt::Result
92+
where
93+
F: FnOnce(&Self) -> fmt::Result,
94+
{
95+
self.html(stringify!($name), attrs, inner)
96+
}
97+
}
98+
};
99+
}
100+
101+
make_tag!(div);
102+
make_tag!(span);
103+
104+
#[cfg(test)]
105+
mod test {
106+
use super::*;
107+
108+
#[test]
109+
fn test_div() {
110+
let html = HtmlWriter::new();
111+
html.div("", |html| html.writeln("Hello, world!")).unwrap();
112+
113+
assert_eq!(
114+
html.into_inner().as_str(),
115+
"<div>
116+
Hello, world!
117+
</div>
118+
"
119+
);
120+
}
121+
}

0 commit comments

Comments
 (0)