diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs
index 2c4744b694f29..d2a3fe02ab4d6 100644
--- a/apps/oxlint/src/lib.rs
+++ b/apps/oxlint/src/lib.rs
@@ -44,3 +44,15 @@ mod js_plugins;
))]
#[global_allocator]
static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc;
+
+/// Return a JSON blob containing metadata for all available oxlint rules.
+///
+/// This uses the internal JSON output formatter to generate the full list.
+///
+/// # Panics
+/// Panics if the JSON generation fails, which should never happen under normal circumstances.
+pub fn get_all_rules_json() -> String {
+ use crate::output_formatter::{OutputFormat, OutputFormatter};
+
+ OutputFormatter::new(OutputFormat::Json).all_rules().expect("Failed to generate rules JSON")
+}
diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs
index 13bc95dfa1481..69f5c326f3e90 100644
--- a/apps/oxlint/src/lint.rs
+++ b/apps/oxlint/src/lint.rs
@@ -323,7 +323,7 @@ impl CliRunner {
let table = RuleTable::default();
for section in &table.sections {
- let md = section.render_markdown_table_cli(None, &enabled);
+ let md = section.render_markdown_table_cli(&enabled);
print_and_flush_stdout(stdout, &md);
print_and_flush_stdout(stdout, "\n");
}
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index cde336314f388..490d7e3d260d7 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -15,7 +15,7 @@ impl InternalFormatter for DefaultOutputFormatter {
let mut output = String::new();
let table = RuleTable::default();
for section in table.sections {
- output.push_str(section.render_markdown_table(None).as_str());
+ output.push_str(section.render_markdown_table().as_str());
output.push('\n');
}
output.push_str(format!("Default: {}\n", table.turned_on_by_default_count).as_str());
diff --git a/crates/oxc_linter/src/table.rs b/crates/oxc_linter/src/table.rs
index f856316fb18a0..4d9efa358940a 100644
--- a/crates/oxc_linter/src/table.rs
+++ b/crates/oxc_linter/src/table.rs
@@ -119,11 +119,7 @@ impl RuleTableSection {
/// When `enabled` is [`Some`], an "Enabled?" column is added and the set
/// is used to determine which rules are enabled. When `enabled` is
/// [`None`], the column is omitted (used by docs rendering).
- fn render_markdown_table_inner(
- &self,
- link_prefix: Option<&str>,
- enabled: Option<&FxHashSet<&str>>,
- ) -> String {
+ fn render_markdown_table_inner(&self, enabled: Option<&FxHashSet<&str>>) -> String {
const FIX_EMOJI_COL_WIDTH: usize = 10;
const DEFAULT_EMOJI_COL_WIDTH: usize = 9;
const ENABLED_EMOJI_COL_WIDTH: usize = 10;
@@ -182,11 +178,7 @@ impl RuleTableSection {
let (default, default_width) =
if row.turned_on_by_default { ("✅", DEFAULT - 1) } else { ("", DEFAULT) };
- 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)
- };
+ let rendered_name = Cow::Borrowed(rule_name);
// Improved mapping for emoji column alignment, allowing FIX to grow for negative display widths
let (fix_emoji, fix_emoji_width) =
Self::calculate_fix_emoji_width(row.autofix.emoji(), FIX);
@@ -230,19 +222,12 @@ impl RuleTableSection {
}
/// 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 {
- self.render_markdown_table_inner(link_prefix, None)
+ pub fn render_markdown_table(&self) -> String {
+ self.render_markdown_table_inner(None)
}
- pub fn render_markdown_table_cli(
- &self,
- link_prefix: Option<&str>,
- enabled: &FxHashSet<&str>,
- ) -> String {
- self.render_markdown_table_inner(link_prefix, Some(enabled))
+ pub fn render_markdown_table_cli(&self, enabled: &FxHashSet<&str>) -> String {
+ self.render_markdown_table_inner(Some(enabled))
}
}
@@ -264,39 +249,18 @@ mod test {
fn test_table_no_links() {
let options = Options::gfm();
for section in &table().sections {
- let rendered_table = section.render_markdown_table(None);
- assert!(!rendered_table.is_empty());
- assert_eq!(rendered_table.split('\n').count(), 5 + section.rows.len());
-
- let html = to_html_with_options(&rendered_table, &options).unwrap();
- assert!(!html.is_empty());
- assert!(html.contains("
"));
- }
- }
-
- #[test]
- fn test_table_with_links() {
- const PREFIX: &str = "/foo/bar";
- const PREFIX_WITH_SLASH: &str = "/foo/bar/";
-
- let options = Options::gfm();
-
- for section in &table().sections {
- let rendered_table = section.render_markdown_table(Some(PREFIX));
+ let rendered_table = section.render_markdown_table();
assert!(!rendered_table.is_empty());
assert_eq!(rendered_table.split('\n').count(), 5 + section.rows.len());
let html = to_html_with_options(&rendered_table, &options).unwrap();
assert!(!html.is_empty());
assert!(html.contains(""));
- assert!(html.contains(PREFIX_WITH_SLASH));
}
}
#[test]
fn test_table_cli_enabled_column() {
- const PREFIX: &str = "/foo/bar";
-
for section in &table().sections {
// enable the first rule in the section for the CLI view
let mut enabled = FxHashSet::default();
@@ -304,7 +268,7 @@ mod test {
enabled.insert(first.name);
}
- let rendered_table = section.render_markdown_table_cli(Some(PREFIX), &enabled);
+ let rendered_table = section.render_markdown_table_cli(&enabled);
assert!(!rendered_table.is_empty());
// same number of lines as other renderer (header + desc + separator + rows + trailing newline)
assert_eq!(rendered_table.split('\n').count(), 5 + section.rows.len());
diff --git a/justfile b/justfile
index 86729a811f157..c0127a43aa05e 100755
--- a/justfile
+++ b/justfile
@@ -253,7 +253,7 @@ watch-playground:
# When testing changes to the website documentation, you may also want to run `pnpm run fmt`
# in the website directory.
website path:
- cargo run -p website_linter rules --table {{path}}/src/docs/guide/usage/linter/generated-rules.md --rule-docs {{path}}/src/docs/guide/usage/linter/rules --git-ref $(git rev-parse HEAD) --rule-count {{path}}/src/docs/guide/usage
+ cargo run -p website_linter rules --rules-json {{path}}/.vitepress/data/rules.json --rule-docs {{path}}/src/docs/guide/usage/linter/rules --git-ref $(git rev-parse HEAD) --rule-count {{path}}/src/docs/guide/usage
cargo run -p website_linter cli > {{path}}/src/docs/guide/usage/linter/generated-cli.md
cargo run -p website_linter schema-markdown > {{path}}/src/docs/guide/usage/linter/generated-config.md
cargo run -p website_formatter cli > {{path}}/src/docs/guide/usage/formatter/generated-cli.md
diff --git a/tasks/website_linter/src/rules/mod.rs b/tasks/website_linter/src/rules/mod.rs
index f95b74d2879a6..f2b261e30719a 100644
--- a/tasks/website_linter/src/rules/mod.rs
+++ b/tasks/website_linter/src/rules/mod.rs
@@ -1,10 +1,8 @@
#![expect(clippy::print_stderr)]
mod doc_page;
mod html;
-mod table;
use std::{
- borrow::Cow,
env, fs,
path::{Path, PathBuf},
};
@@ -14,13 +12,12 @@ use html::HtmlWriter;
use oxc_linter::{Oxlintrc, table::RuleTable};
use pico_args::Arguments;
use schemars::{SchemaGenerator, r#gen::SchemaSettings};
-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.
+ -j,--rules-json Path to file where rules json blob 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.
-c,--rule-count Path to directory where rule count data file will be saved.
@@ -32,11 +29,11 @@ Arguments:
/// `print_rules`
///
-/// `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
-/// --rule-count /path/to/oxc/oxc-project.github.io/src/docs/guide/usage
-/// --git-ref dc9dc03872101c15b0d02f05ce45705565665829
+/// `cargo run -p website linter-rules
+/// --rules-json /path/to/oxc/oxc-project.github.io/.vitepress/data/rules.json
+/// --rule-docs /path/to/oxc/oxc-project.github.io/src/docs/guide/usage/linter/rules
+/// --rule-count /path/to/oxc/oxc-project.github.io/src/docs/guide/usage
+/// --git-ref dc9dc03872101c15b0d02f05ce45705565665829
/// `
///
#[expect(clippy::print_stdout)]
@@ -48,30 +45,19 @@ pub fn print_rules(mut args: Arguments) {
}
let git_ref: Option = args.opt_value_from_str("--git-ref").unwrap();
- let table_path = args.opt_value_from_str::<_, PathBuf>(["-t", "--table"]).unwrap();
+ let rules_json_path = args.opt_value_from_str::<_, PathBuf>(["-j", "--rules-json"]).unwrap();
let rules_dir = args.opt_value_from_str::<_, PathBuf>(["-r", "--rule-docs"]).unwrap();
let rule_count_dir = args.opt_value_from_str::<_, PathBuf>(["-c", "--rule-count"]).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") {
- let split = p.split("src/docs").collect::>();
- assert!(split.len() > 1);
- Cow::Owned("/docs".to_string() + split.last().unwrap())
- } else {
- Cow::Borrowed(p)
- }
- });
-
let mut generator = SchemaGenerator::new(SchemaSettings::default());
let table = RuleTable::new(Some(&mut generator));
- if let Some(table_path) = table_path {
- let table_path = pwd.join(table_path).canonicalize().unwrap();
+ if let Some(rules_json_path) = rules_json_path {
+ let rules_json_path = pwd.join(rules_json_path).canonicalize().unwrap();
- eprintln!("Rendering rules table...");
- let rules_table = render_rules_table(&table, prefix.as_ref());
- fs::write(table_path, rules_table).unwrap();
+ eprintln!("Rendering rules JSON blob...");
+ let rules_json = oxlint::get_all_rules_json();
+ fs::write(rules_json_path, rules_json).unwrap();
}
if let Some(rules_dir) = &rules_dir {
diff --git a/tasks/website_linter/src/rules/table.rs b/tasks/website_linter/src/rules/table.rs
deleted file mode 100644
index 788d99b56945b..0000000000000
--- a/tasks/website_linter/src/rules/table.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use oxc_linter::table::RuleTable;
-
-/// Renders the website's Rules page. Each [`category`] gets its own table with
-/// links to documentation pages for each rule.
-///
-/// `docs_prefix` is a path prefix to the base URL all rule documentation pages
-/// share in common.
-///
-/// [`category`]: oxc_linter::RuleCategory
-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 rules_with_fixes = table.rules_with_fixes;
- let body = table
- .sections
- .iter()
- .map(|s| s.render_markdown_table(Some(docs_prefix)))
- .collect::>()
- .join("\n");
-
- format!("
----
-search: false
----
-# Rules
-
-The progress of all rule implementations is tracked [here](https://github.com/oxc-project/oxc/issues/481).
-
-- Total number of rules: {total}
-- Rules turned on by default: {turned_on_by_default_count}
-- Rules with fixes available: {rules_with_fixes}
-
-**Legend for 'Fixable?' column:**
-- 🛠️: an auto-fix is available for this rule
-- 💡: a suggestion is available for this rule
-- ⚠️🛠️: a dangerous auto-fix is available for this rule
-- ⚠️💡: a dangerous suggestion is available for this rule
-- 🚧: an auto-fix or suggestion is possible, but currently not implemented
-
-{body}
-").trim().to_string()
-}