From 118821edf8b355b8dc92303ab41aa9103d9bf22f Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Sun, 4 Aug 2024 21:15:45 -0400 Subject: [PATCH] fix(linter): docs for jsx-a11y/anchor-is-valid --- crates/oxc_linter/src/lib.rs | 4 +- crates/oxc_linter/src/rule.rs | 21 ++- .../src/rules/jsx_a11y/anchor_is_valid.rs | 22 +--- crates/oxc_linter/src/table.rs | 19 ++- tasks/website/src/linter/rules/doc_page.rs | 57 +++++++++ tasks/website/src/linter/rules/html.rs | 121 ++++++++++++++++++ tasks/website/src/linter/rules/mod.rs | 87 +++++++++++++ .../src/linter/{rules.rs => rules/table.rs} | 14 +- tasks/website/src/main.rs | 2 +- 9 files changed, 310 insertions(+), 37 deletions(-) create mode 100644 tasks/website/src/linter/rules/doc_page.rs create mode 100644 tasks/website/src/linter/rules/html.rs create mode 100644 tasks/website/src/linter/rules/mod.rs rename tasks/website/src/linter/{rules.rs => rules/table.rs} (71%) diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index db0a174f886d0..38c221f27bb98 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -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::{ @@ -146,7 +146,7 @@ impl Linter { pub fn print_rules(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(); diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 2d240170fa6f4..3fb081fb1315d 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -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. @@ -132,7 +132,12 @@ 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] @@ -140,6 +145,11 @@ impl RuleFixMeta { 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)) } @@ -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(_)) { diff --git a/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs b/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs index 858e5d7c2ae63..5818637998967 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs @@ -56,7 +56,7 @@ declare_oxc_lint!( /// /// Consider the following: /// - /// ```javascript + /// ```jsx /// Perform action /// Perform action /// Perform action @@ -64,7 +64,7 @@ declare_oxc_lint!( /// /// All these anchor implementations indicate that the element is only used to execute JavaScript code. All the above should be replaced with: /// - /// ```javascript + /// ```jsx /// /// ``` /// ` @@ -78,33 +78,19 @@ declare_oxc_lint!( /// /// #### Valid /// - /// ```javascript + /// ```jsx /// navigate here - /// ``` - /// - /// ```javascript /// navigate here - /// ``` - /// - /// ```javascript /// navigate here /// ``` /// /// #### Invalid /// - /// ```javascript + /// ```jsx /// navigate here - /// ``` - /// ```javascript /// navigate here - /// ``` - /// ```javascript /// navigate here - /// ``` - /// ```javascript /// navigate here - /// ``` - /// ```javascript /// navigate here /// ``` /// diff --git a/crates/oxc_linter/src/table.rs b/crates/oxc_linter/src/table.rs index e59c488da0737..ab45d824a6bd7 100644 --- a/crates/oxc_linter/src/table.rs +++ b/crates/oxc_linter/src/table.rs @@ -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, @@ -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 { @@ -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::>(); @@ -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; @@ -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: Result { + 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, + "\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("🛠️"), + } +} diff --git a/tasks/website/src/linter/rules/html.rs b/tasks/website/src/linter/rules/html.rs new file mode 100644 index 0000000000000..9441ad5dff622 --- /dev/null +++ b/tasks/website/src/linter/rules/html.rs @@ -0,0 +1,121 @@ +use std::{ + cell::RefCell, + fmt::{self, Write}, +}; + +#[derive(Debug)] +pub(crate) struct HtmlWriter { + inner: RefCell, +} + +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 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>(&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(&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 '' + 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(), "")?; + + Ok(()) + } +} + +macro_rules! make_tag { + ($name:ident) => { + impl HtmlWriter { + #[inline] + pub fn $name(&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(), + "
+Hello, world! +
+" + ); + } +} diff --git a/tasks/website/src/linter/rules/mod.rs b/tasks/website/src/linter/rules/mod.rs new file mode 100644 index 0000000000000..c6f1d62a5cca0 --- /dev/null +++ b/tasks/website/src/linter/rules/mod.rs @@ -0,0 +1,87 @@ +mod doc_page; +mod html; +mod table; + +use std::{ + borrow::Cow, + env, fs, + path::{Path, PathBuf}, +}; + +use doc_page::render_rule_docs_page; +use oxc_linter::table::RuleTable; +use pico_args::Arguments; +use table::render_rules_table; + +const HELP: &str = " +usage: linter-rules [args] + +Arguments: + -t,--table Path to file where rule markdown table will be saved. + -r,--rule-docs Path to directory where rule doc pages will be saved. + A directory will be created if one doesn't exist. + -h,--help Show this help message. + +"; + +/// `cargo run -p website linter-rules --table +/// /path/to/oxc/oxc-project.github.io/src/docs/guide/usage/linter/generated-rules.md +/// --rule-docs /path/to/oxc/oxc-project.github.io/src/docs/guide/usage/linter/rules +/// ` +/// +pub fn print_rules(mut args: Arguments) { + let pwd = PathBuf::from(env::var("PWD").unwrap()); + if args.contains(["-h", "--help"]) { + println!("{HELP}"); + return; + } + + let table = RuleTable::new(); + let table_path = args.opt_value_from_str::<_, PathBuf>(["-t", "--table"]).unwrap(); + let rules_dir = args.opt_value_from_str::<_, PathBuf>(["-r", "--rule-docs"]).unwrap(); + + if let Some(table_path) = table_path { + let table_path = pwd.join(table_path).canonicalize().unwrap(); + + let prefix = rules_dir.as_ref().and_then(|p| p.as_os_str().to_str()).map_or( + Cow::Borrowed(""), + |p| { + if p.contains("src/docs") { + Cow::Owned("/docs".to_string() + p.split("src/docs").last().unwrap()) + } else { + Cow::Borrowed(p) + } + }, + ); + println!("Rendering rules table..."); + let rules_table = render_rules_table(&table, prefix.as_ref()); + fs::write(table_path, rules_table).unwrap(); + } + + if let Some(rules_dir) = rules_dir { + println!("Rendering rule doc pages..."); + let rules_dir = pwd.join(rules_dir); + if !rules_dir.exists() { + fs::create_dir_all(&rules_dir).unwrap(); + } + let rules_dir = rules_dir.canonicalize().unwrap(); + assert!( + !rules_dir.is_file(), + "Cannot write rule docs to a file. Please specify a directory." + ); + write_rule_doc_pages(&table, &rules_dir); + } + + println!("Done."); +} + +fn write_rule_doc_pages(table: &RuleTable, outdir: &Path) { + for rule in table.sections.iter().flat_map(|section| §ion.rows) { + let plugin_path = outdir.join(&rule.plugin); + fs::create_dir_all(&plugin_path).unwrap(); + let page_path = plugin_path.join(format!("{}.md", rule.name)); + println!("{}", page_path.display()); + let docs = render_rule_docs_page(rule).unwrap(); + fs::write(&page_path, docs).unwrap(); + } +} diff --git a/tasks/website/src/linter/rules.rs b/tasks/website/src/linter/rules/table.rs similarity index 71% rename from tasks/website/src/linter/rules.rs rename to tasks/website/src/linter/rules/table.rs index b0a9d45ee2ec0..963f899328d1e 100644 --- a/tasks/website/src/linter/rules.rs +++ b/tasks/website/src/linter/rules/table.rs @@ -2,20 +2,20 @@ use oxc_linter::table::RuleTable; // `cargo run -p website linter-rules > /path/to/oxc/oxc-project.github.io/src/docs/guide/usage/linter/generated-rules.md` // -pub fn print_rules() { - let table = RuleTable::new(); - +/// `docs_prefix` is a path prefix to the base URL all rule documentation pages +/// share in common. +pub fn render_rules_table(table: &RuleTable, docs_prefix: &str) -> String { let total = table.total; let turned_on_by_default_count = table.turned_on_by_default_count; let body = table .sections - .into_iter() - .map(|section| section.render_markdown_table()) + .iter() + .map(|s| s.render_markdown_table(Some(docs_prefix))) .collect::>() .join("\n"); - println!(" + format!(" # Rules The progress of all rule implementations is tracked [here](https://github.com/oxc-project/oxc/issues/481). @@ -29,5 +29,5 @@ The progress of all rule implementations is tracked [here](https://github.com/ox -"); +") } diff --git a/tasks/website/src/main.rs b/tasks/website/src/main.rs index 48b872f02f742..d0b1c654c1e29 100644 --- a/tasks/website/src/main.rs +++ b/tasks/website/src/main.rs @@ -12,7 +12,7 @@ fn main() { "linter-schema-json" => linter::print_schema_json(), "linter-schema-markdown" => linter::print_schema_markdown(), "linter-cli" => linter::print_cli(), - "linter-rules" => linter::print_rules(), + "linter-rules" => linter::print_rules(args), _ => println!("Missing task command."), } }