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/import/namespace.rs b/crates/oxc_linter/src/rules/import/namespace.rs index 9209b11b20d44..af1264baec0db 100644 --- a/crates/oxc_linter/src/rules/import/namespace.rs +++ b/crates/oxc_linter/src/rules/import/namespace.rs @@ -40,10 +40,12 @@ pub struct Namespace { declare_oxc_lint!( /// ### What it does - /// Enforces names exist at the time they are dereferenced, when imported as a full namespace (i.e. import * as foo from './foo'; foo.bar(); will report if bar is not exported by ./foo.). - /// Will report at the import declaration if there are no exported names found. - /// Also, will report for computed references (i.e. foo["bar"]()). - /// Reports on assignment to a member of an imported namespace. + /// Enforces names exist at the time they are dereferenced, when imported as + /// a full namespace (i.e. `import * as foo from './foo'; foo.bar();` will + /// report if bar is not exported by `./foo.`). Will report at the import + /// declaration if there are no exported names found. Also, will report for + /// computed references (i.e. `foo["bar"]()`). Reports on assignment to a + /// member of an imported namespace. Namespace, correctness ); 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/rules/jsx_a11y/lang.rs b/crates/oxc_linter/src/rules/jsx_a11y/lang.rs index a16e28a21dd41..4c894be11e8bd 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/lang.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/lang.rs @@ -26,7 +26,7 @@ pub struct Lang; declare_oxc_lint!( /// ### What it does /// - /// The lang prop on the element must be a valid IETF's BCP 47 language tag. + /// The lang prop on the `` element must be a valid IETF's BCP 47 language tag. /// /// ### Why is this bad? /// @@ -39,13 +39,13 @@ declare_oxc_lint!( /// ### Example /// /// // good - /// ```javascript + /// ```jsx /// /// /// ``` /// /// // bad - /// ```javascript + /// ```jsx /// /// /// ```` diff --git a/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs b/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs index faf44d9c20ffe..88bfa3de54efc 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs @@ -22,15 +22,17 @@ declare_oxc_lint!( /// /// ### Why is this necessary? /// - /// Elements that can be visually distracting can cause accessibility issues with visually impaired users. - /// Such elements are most likely deprecated, and should be avoided. By default, and elements are visually distracting. + /// Elements that can be visually distracting can cause accessibility issues + /// with visually impaired users. Such elements are most likely deprecated, + /// and should be avoided. By default, `` and `` elements + /// are visually distracting. /// /// ### What it checks /// /// This rule checks for marquee and blink element. /// /// ### Example - /// ```javascript + /// ```jsx /// // Bad /// /// diff --git a/crates/oxc_linter/src/rules/jsx_a11y/scope.rs b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs index ec7662aa6422a..5b0d22ed23427 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/scope.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs @@ -23,7 +23,7 @@ pub struct Scope; declare_oxc_lint!( /// ### What it does /// - /// The scope prop should be used only on elements. + /// The scope prop should be used only on `` elements. /// /// ### Why is this bad? /// The scope attribute makes table navigation much easier for screen reader users, provided that it is used correctly. @@ -31,7 +31,7 @@ declare_oxc_lint!( /// A screen reader operates under the assumption that a table has a header and that this header specifies a scope. Because of the way screen readers function, having an accurate header makes viewing a table far more accessible and more efficient for people who use the device. /// /// ### Example - /// ```javascript + /// ```jsx /// // Bad ///
/// diff --git a/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs b/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs index ef05f50fdad25..2c6cb5fbc51d3 100644 --- a/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs +++ b/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs @@ -10,13 +10,13 @@ pub struct NoDuplicateHead; declare_oxc_lint!( /// ### What it does - /// Prevent duplicate usage of in pages/_document.js. + /// Prevent duplicate usage of `` in `pages/_document.js``. /// /// ### Why is this bad? /// This can cause unexpected behavior in your application. /// /// ### Example - /// ```javascript + /// ```jsx /// import Document, { Html, Head, Main, NextScript } from 'next/document' /// class MyDocument extends Document { /// static async getInitialProps(ctx) { diff --git a/crates/oxc_linter/src/rules/typescript/no_this_alias.rs b/crates/oxc_linter/src/rules/typescript/no_this_alias.rs index 3f7eb0b78e5f0..9d8f618950d88 100644 --- a/crates/oxc_linter/src/rules/typescript/no_this_alias.rs +++ b/crates/oxc_linter/src/rules/typescript/no_this_alias.rs @@ -59,12 +59,14 @@ declare_oxc_lint!( /// /// ### Why is this bad? /// - /// Generic type parameters () in TypeScript may be "constrained" with an extends keyword. - /// When no extends is provided, type parameters default a constraint to unknown. It is therefore redundant to extend from any or unknown. + /// Generic type parameters (``) in TypeScript may be "constrained" with + /// an extends keyword. When no extends is provided, type parameters + /// default a constraint to unknown. It is therefore redundant to extend + /// from any or unknown. /// - /// the rule doesn't allow const {allowedName} = this + /// the rule doesn't allow `const {allowedName} = this` /// this is to keep 1:1 with eslint implementation - /// sampe with obj. = this + /// sampe with `obj. = this` /// ``` NoThisAlias, correctness diff --git a/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs b/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs index be98cad392d1a..f4289633c4fcb 100644 --- a/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs +++ b/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs @@ -28,11 +28,11 @@ declare_oxc_lint!( /// /// ### Why is this bad? /// - /// Generic type parameters () in TypeScript may be "constrained" with an extends keyword. + /// Generic type parameters (``) in TypeScript may be "constrained" with an extends keyword. /// When no extends is provided, type parameters default a constraint to unknown. It is therefore redundant to extend from any or unknown. /// /// ### Example - /// ```javascript + /// ```typescript /// interface FooAny {} /// interface FooUnknown {} /// type BarAny = {}; 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..151a658d8c0a8 --- /dev/null +++ b/tasks/website/src/linter/rules/mod.rs @@ -0,0 +1,128 @@ +mod doc_page; +mod html; +mod table; + +use std::{ + borrow::Cow, + env, fs, + path::{Path, PathBuf}, + process, +}; + +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(); + + let (prefix, root) = rules_dir.as_ref().and_then(|p| p.as_os_str().to_str()).map_or( + (Cow::Borrowed(""), None), + |p| { + if p.contains("src/docs") { + let split = p.split("src/docs").collect::>(); + assert!(split.len() > 1); + let root = split[0]; + let root = pwd.join(root).canonicalize().unwrap(); + let prefix = Cow::Owned("/docs".to_string() + split.last().unwrap()); + (prefix, Some(root)) + } else { + (Cow::Borrowed(p), None) + } + }, + ); + + if let Some(table_path) = table_path { + let table_path = pwd.join(table_path).canonicalize().unwrap(); + + 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); + + // auto-fix code and natural language issues + if let Some(root) = root { + println!("Formatting rule doc pages..."); + prettier(&root, &rules_dir); + println!("Fixing textlint issues..."); + textlint(&root); + } + } + + 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(); + } +} + +/// Run prettier and fix style issues in generated rule doc pages. +fn prettier(website_root: &Path, rule_docs_path: &Path) { + assert!(rule_docs_path.is_dir(), "Rule docs path must be a directory."); + assert!(rule_docs_path.is_absolute(), "Rule docs path must be an absolute path."); + let relative_path = rule_docs_path.strip_prefix(website_root).unwrap(); + let path_str = + relative_path.to_str().expect("Invalid rule docs path: could not convert to str"); + let generated_md_glob = format!("{path_str}/**/*.md"); + + process::Command::new("pnpm") + .current_dir(website_root) + .args(["run", "fmt", "--write", &generated_md_glob]) + .status() + .unwrap(); +} + +/// Run textlint and fix any issues it finds. +fn textlint(website_root: &Path) { + assert!(website_root.is_dir(), "Rule docs path must be a directory."); + process::Command::new("pnpm") + .current_dir(website_root) + .args(["run", "textlint:fix"]) + .status() + .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."), } }