From 2838a9ca1bbbe6b9e3ae5837a984b1a48702507b Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 17 Nov 2025 16:25:16 -0700 Subject: [PATCH 1/6] feat(linter): Pass enabled rules for all markdown tables. And also pass it to the JSON output of all_rules, but that's not implemented quite yet. --- apps/oxlint/src/lint.rs | 2 +- apps/oxlint/src/output_formatter/default.rs | 7 ++++--- apps/oxlint/src/output_formatter/json.rs | 3 ++- apps/oxlint/src/output_formatter/mod.rs | 5 +++-- crates/oxc_linter/src/table.rs | 21 ++++++++------------- tasks/website/src/linter/rules/table.rs | 2 +- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 4c36af07d6929..775f9077fd054 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -312,7 +312,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(None, Some(&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 236bb96f017c9..ed67e4f1cb344 100644 --- a/apps/oxlint/src/output_formatter/default.rs +++ b/apps/oxlint/src/output_formatter/default.rs @@ -6,16 +6,17 @@ use oxc_diagnostics::{ reporter::{DiagnosticReporter, DiagnosticResult}, }; use oxc_linter::table::RuleTable; +use rustc_hash::FxHashSet; #[derive(Debug)] pub struct DefaultOutputFormatter; impl InternalFormatter for DefaultOutputFormatter { - fn all_rules(&self) -> Option { + fn all_rules(&self, enabled: Option<&FxHashSet<&str>>) -> Option { 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(None, enabled).as_str()); output.push('\n'); } output.push_str(format!("Default: {}\n", table.turned_on_by_default_count).as_str()); @@ -167,7 +168,7 @@ mod test { #[test] fn all_rules() { let formatter = DefaultOutputFormatter; - let result = formatter.all_rules(); + let result = formatter.all_rules(None); assert!(result.is_some()); } diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs index 095902056cbac..24a7a08f1c374 100644 --- a/apps/oxlint/src/output_formatter/json.rs +++ b/apps/oxlint/src/output_formatter/json.rs @@ -1,6 +1,7 @@ use std::{cell::RefCell, rc::Rc}; use miette::JSONReportHandler; +use rustc_hash::FxHashSet; use serde::Serialize; use oxc_diagnostics::{ @@ -17,7 +18,7 @@ pub struct JsonOutputFormatter { } impl InternalFormatter for JsonOutputFormatter { - fn all_rules(&self) -> Option { + fn all_rules(&self, _enabled: Option<&FxHashSet<&str>>) -> Option { #[derive(Debug, Serialize)] struct RuleInfoJson<'a> { scope: &'a str, diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs index 39a922fae62ea..27f599aac29d4 100644 --- a/apps/oxlint/src/output_formatter/mod.rs +++ b/apps/oxlint/src/output_formatter/mod.rs @@ -15,6 +15,7 @@ use checkstyle::CheckStyleOutputFormatter; use github::GithubOutputFormatter; use gitlab::GitlabOutputFormatter; use junit::JUnitOutputFormatter; +use rustc_hash::FxHashSet; use stylish::StylishOutputFormatter; use unix::UnixOutputFormatter; @@ -72,7 +73,7 @@ pub struct LintCommandInfo { /// The Formatter is then managed by [`OutputFormatter`]. trait InternalFormatter { /// Print all available rules by oxlint - fn all_rules(&self) -> Option { + fn all_rules(&self, _enabled: Option<&FxHashSet<&str>>) -> Option { None } @@ -111,7 +112,7 @@ impl OutputFormatter { /// Print all available rules by oxlint /// See [`InternalFormatter::all_rules`] for more details. pub fn all_rules(&self) -> Option { - self.internal.all_rules() + self.internal.all_rules(None) } /// At the end of the Lint command we may output extra information. diff --git a/crates/oxc_linter/src/table.rs b/crates/oxc_linter/src/table.rs index d79258f4f88fb..92d3fb38f7849 100644 --- a/crates/oxc_linter/src/table.rs +++ b/crates/oxc_linter/src/table.rs @@ -225,16 +225,11 @@ impl RuleTableSection { /// /// 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_cli( - &self, - link_prefix: Option<&str>, - enabled: &FxHashSet<&str>, - ) -> String { - self.render_markdown_table_inner(link_prefix, Some(enabled)) + /// + /// Provide [`Some`] set of enabled rule names to include an "Enabled?" column. + /// Provide [`None`] to omit the column. + pub fn render_markdown_table(&self, link_prefix: Option<&str>, enabled: Option<&FxHashSet<&str>>) -> String { + self.render_markdown_table_inner(link_prefix, enabled) } } @@ -256,7 +251,7 @@ mod test { fn test_table_no_links() { let options = Options::gfm(); for section in &table().sections { - let rendered_table = section.render_markdown_table(None); + let rendered_table = section.render_markdown_table(None, None); assert!(!rendered_table.is_empty()); assert_eq!(rendered_table.split('\n').count(), 5 + section.rows.len()); @@ -274,7 +269,7 @@ mod test { 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(Some(PREFIX), None); assert!(!rendered_table.is_empty()); assert_eq!(rendered_table.split('\n').count(), 5 + section.rows.len()); @@ -296,7 +291,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(Some(PREFIX), Some(&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/tasks/website/src/linter/rules/table.rs b/tasks/website/src/linter/rules/table.rs index ba5cec35ce423..f7c675d73f735 100644 --- a/tasks/website/src/linter/rules/table.rs +++ b/tasks/website/src/linter/rules/table.rs @@ -14,7 +14,7 @@ pub fn render_rules_table(table: &RuleTable, docs_prefix: &str) -> String { let body = table .sections .iter() - .map(|s| s.render_markdown_table(Some(docs_prefix))) + .map(|s| s.render_markdown_table(Some(docs_prefix), None)) .collect::>() .join("\n"); From f083330fc4a6234f45eea06f286099bbf0bcb48d Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 17 Nov 2025 16:26:33 -0700 Subject: [PATCH 2/6] Print the enabled rules count in the default output formatter. --- apps/oxlint/src/lint.rs | 1 + apps/oxlint/src/output_formatter/default.rs | 3 +++ crates/oxc_linter/src/table.rs | 6 +++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 775f9077fd054..29c3d7f8e1495 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -321,6 +321,7 @@ impl CliRunner { stdout, format!("Default: {}\n", table.turned_on_by_default_count).as_str(), ); + print_and_flush_stdout(stdout, format!("Enabled: {}\n", enabled.len()).as_str()); print_and_flush_stdout(stdout, format!("Total: {}\n", table.total).as_str()); } else if let Some(output) = output_formatter.all_rules() { print_and_flush_stdout(stdout, &output); diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs index ed67e4f1cb344..3960783388da1 100644 --- a/apps/oxlint/src/output_formatter/default.rs +++ b/apps/oxlint/src/output_formatter/default.rs @@ -20,6 +20,9 @@ impl InternalFormatter for DefaultOutputFormatter { output.push('\n'); } output.push_str(format!("Default: {}\n", table.turned_on_by_default_count).as_str()); + if enabled.is_some() { + output.push_str(format!("Enabled: {}\n", enabled.unwrap().len()).as_str()); + } output.push_str(format!("Total: {}\n", table.total).as_str()); Some(output) } diff --git a/crates/oxc_linter/src/table.rs b/crates/oxc_linter/src/table.rs index 92d3fb38f7849..cbecc7a4270b1 100644 --- a/crates/oxc_linter/src/table.rs +++ b/crates/oxc_linter/src/table.rs @@ -228,7 +228,11 @@ impl RuleTableSection { /// /// Provide [`Some`] set of enabled rule names to include an "Enabled?" column. /// Provide [`None`] to omit the column. - pub fn render_markdown_table(&self, link_prefix: Option<&str>, enabled: Option<&FxHashSet<&str>>) -> String { + pub fn render_markdown_table( + &self, + link_prefix: Option<&str>, + enabled: Option<&FxHashSet<&str>>, + ) -> String { self.render_markdown_table_inner(link_prefix, enabled) } } From 2ba524cf1b83ff7b0b35f9982c3566ffab8d1496 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 18 Nov 2025 17:08:50 -0700 Subject: [PATCH 3/6] refactor(linter): Simplify the markdown table generation so we only define this logic in one place, rather than two. --- apps/oxlint/src/lint.rs | 29 ++++++++----------------- apps/oxlint/src/output_formatter/mod.rs | 4 ++-- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 29c3d7f8e1495..c2501b2053b19 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -17,7 +17,6 @@ use oxc_diagnostics::{DiagnosticSender, DiagnosticService, GraphicalReportHandle use oxc_linter::{ AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore, InvalidFilterKind, LintFilter, LintOptions, LintRunner, LintServiceOptions, Linter, Oxlintrc, - table::RuleTable, }; use crate::{ @@ -304,26 +303,16 @@ impl CliRunner { // If the user requested `--rules`, print a CLI-specific table that // includes an "Enabled?" column based on the resolved configuration. if self.options.list_rules { - // Preserve previous behavior of `--rules` output when `-f` is set - if self.options.output_options.format == OutputFormat::Default { - // Build the set of enabled builtin rule names from the resolved config. - let enabled: FxHashSet<&str> = - config_store.rules().iter().map(|(rule, _)| rule.name()).collect(); - - let table = RuleTable::default(); - for section in &table.sections { - let md = section.render_markdown_table(None, Some(&enabled)); - print_and_flush_stdout(stdout, &md); - print_and_flush_stdout(stdout, "\n"); - } + // Put together the enabled hashset if the format is default, otherwise None + let enabled: Option> = + if self.options.output_options.format == OutputFormat::Default { + // Build the set of enabled builtin rule names from the resolved config. + Some(config_store.rules().iter().map(|(rule, _)| rule.name()).collect()) + } else { + None + }; - print_and_flush_stdout( - stdout, - format!("Default: {}\n", table.turned_on_by_default_count).as_str(), - ); - print_and_flush_stdout(stdout, format!("Enabled: {}\n", enabled.len()).as_str()); - print_and_flush_stdout(stdout, format!("Total: {}\n", table.total).as_str()); - } else if let Some(output) = output_formatter.all_rules() { + if let Some(output) = output_formatter.all_rules(enabled.as_ref()) { print_and_flush_stdout(stdout, &output); } diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs index 27f599aac29d4..830da899aaa29 100644 --- a/apps/oxlint/src/output_formatter/mod.rs +++ b/apps/oxlint/src/output_formatter/mod.rs @@ -111,8 +111,8 @@ impl OutputFormatter { /// Print all available rules by oxlint /// See [`InternalFormatter::all_rules`] for more details. - pub fn all_rules(&self) -> Option { - self.internal.all_rules(None) + pub fn all_rules(&self, enabled: Option<&FxHashSet<&str>>) -> Option { + self.internal.all_rules(enabled) } /// At the end of the Lint command we may output extra information. From 54bc7c6dd6e6810ea3a801690b91b59751da5a81 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 18 Nov 2025 17:30:01 -0700 Subject: [PATCH 4/6] Add a basic test for the default reporter and ensuring it handles the `enabled` set. --- apps/oxlint/src/output_formatter/default.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs index 3960783388da1..18e82f76481a2 100644 --- a/apps/oxlint/src/output_formatter/default.rs +++ b/apps/oxlint/src/output_formatter/default.rs @@ -167,6 +167,7 @@ mod test { default::{DefaultOutputFormatter, GraphicalReporter}, }; use oxc_diagnostics::reporter::{DiagnosticReporter, DiagnosticResult}; + use rustc_hash::FxHashSet; #[test] fn all_rules() { @@ -176,6 +177,18 @@ mod test { assert!(result.is_some()); } + #[test] + fn all_rules_with_enabled() { + let formatter = DefaultOutputFormatter; + // Pass in one enabled rule to make sure it renders fine: + let mut enabled = FxHashSet::default(); + enabled.insert("no-unused-vars"); + let result = formatter.all_rules(Some(&enabled)); + + assert!(result.is_some()); + assert!(result.unwrap().contains("Enabled: 1\n")); + } + #[test] fn lint_command_info() { let formatter = DefaultOutputFormatter; From c2d812240fdf009e1d3f5f60bae1e00b82219be6 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 18 Nov 2025 17:36:30 -0700 Subject: [PATCH 5/6] Update apps/oxlint/src/output_formatter/default.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Connor Shea --- apps/oxlint/src/output_formatter/default.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs index 18e82f76481a2..938be973c3abc 100644 --- a/apps/oxlint/src/output_formatter/default.rs +++ b/apps/oxlint/src/output_formatter/default.rs @@ -20,8 +20,8 @@ impl InternalFormatter for DefaultOutputFormatter { output.push('\n'); } output.push_str(format!("Default: {}\n", table.turned_on_by_default_count).as_str()); - if enabled.is_some() { - output.push_str(format!("Enabled: {}\n", enabled.unwrap().len()).as_str()); + if let Some(enabled) = enabled { + output.push_str(format!("Enabled: {}\n", enabled.len()).as_str()); } output.push_str(format!("Total: {}\n", table.total).as_str()); Some(output) From b8f615b1185ba5ff4fbad58d59cdd404603e33bd Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 18 Nov 2025 18:06:29 -0700 Subject: [PATCH 6/6] Pass `enabled` for the json formatter. This will print whether each rule is enabled in the JSON output of `--rules`. Maybe we should make this return a string instead of a boolean, to distinguish between off/warn/error? --- apps/oxlint/src/lint.rs | 21 ++++++++++++-------- apps/oxlint/src/output_formatter/json.rs | 4 +++- crates/oxc_linter/src/config/config_store.rs | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index c2501b2053b19..aa4c0e2941abb 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -303,14 +303,18 @@ impl CliRunner { // If the user requested `--rules`, print a CLI-specific table that // includes an "Enabled?" column based on the resolved configuration. if self.options.list_rules { - // Put together the enabled hashset if the format is default, otherwise None - let enabled: Option> = - if self.options.output_options.format == OutputFormat::Default { - // Build the set of enabled builtin rule names from the resolved config. - Some(config_store.rules().iter().map(|(rule, _)| rule.name()).collect()) - } else { - None - }; + // Put together the enabled hashset if the format is default or json, otherwise None + let is_format_with_enabled = matches!( + self.options.output_options.format, + OutputFormat::Default | OutputFormat::Json + ); + + let enabled: Option> = if is_format_with_enabled { + // Build the set of enabled builtin rule names from the resolved config. + Some(config_store.rules().iter().map(|(rule, _)| rule.name()).collect()) + } else { + None + }; if let Some(output) = output_formatter.all_rules(enabled.as_ref()) { print_and_flush_stdout(stdout, &output); @@ -1300,6 +1304,7 @@ mod test { assert!(rule_obj.contains_key("scope"), "Rule should contain 'scope' field"); assert!(rule_obj.contains_key("value"), "Rule should contain 'value' field"); assert!(rule_obj.contains_key("category"), "Rule should contain 'category' field"); + assert!(rule_obj.contains_key("enabled"), "Rule should contain 'enabled' field"); } } diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs index 24a7a08f1c374..0996e63de0b07 100644 --- a/apps/oxlint/src/output_formatter/json.rs +++ b/apps/oxlint/src/output_formatter/json.rs @@ -18,18 +18,20 @@ pub struct JsonOutputFormatter { } impl InternalFormatter for JsonOutputFormatter { - fn all_rules(&self, _enabled: Option<&FxHashSet<&str>>) -> Option { + fn all_rules(&self, enabled: Option<&FxHashSet<&str>>) -> Option { #[derive(Debug, Serialize)] struct RuleInfoJson<'a> { scope: &'a str, value: &'a str, category: RuleCategory, + enabled: bool, } let rules_info = RULES.iter().map(|rule| RuleInfoJson { scope: rule.plugin_name(), value: rule.name(), category: rule.category(), + enabled: enabled.is_some_and(|enabled_set| enabled_set.contains(rule.name())), }); Some( diff --git a/crates/oxc_linter/src/config/config_store.rs b/crates/oxc_linter/src/config/config_store.rs index 0b94ad4944221..a6a89d54c35fa 100644 --- a/crates/oxc_linter/src/config/config_store.rs +++ b/crates/oxc_linter/src/config/config_store.rs @@ -302,6 +302,7 @@ impl ConfigStore { Some(count) } + // TODO: fix this to return rules based on whether type_aware is enabled pub fn rules(&self) -> &Arc<[(RuleEnum, AllowWarnDeny)]> { &self.base.base.rules }