diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index b9b7267b54827..73a8dbf097361 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -17,6 +17,7 @@ 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::{ @@ -48,13 +49,6 @@ impl CliRunner { let format_str = self.options.output_options.format; let output_formatter = OutputFormatter::new(format_str); - if self.options.list_rules { - if let Some(output) = output_formatter.all_rules() { - print_and_flush_stdout(stdout, &output); - } - return CliRunResult::None; - } - let LintCommand { paths, filter, @@ -307,6 +301,35 @@ impl CliRunner { let config_store = ConfigStore::new(lint_config, nested_configs, external_plugin_store); + // 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 { + // Ensure the `all_rules` method is considered used in non-test builds so the + // `InternalFormatter::all_rules` default implementation doesn't produce a + // `dead_code` warning. This is a no-op call (the result is discarded) and + // is only compiled in non-test builds. + let _ = output_formatter.all_rules(); + + // 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_cli(None, &enabled); + print_and_flush_stdout(stdout, &md); + print_and_flush_stdout(stdout, "\n"); + } + + print_and_flush_stdout( + stdout, + format!("Default: {}\n", table.turned_on_by_default_count).as_str(), + ); + print_and_flush_stdout(stdout, format!("Total: {}\n", table.total).as_str()); + + return CliRunResult::None; + } + let files_to_lint = paths .into_iter() .filter(|path| !ignore_matcher.should_ignore(Path::new(path))) diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs index 39a922fae62ea..e87d88640f03b 100644 --- a/apps/oxlint/src/output_formatter/mod.rs +++ b/apps/oxlint/src/output_formatter/mod.rs @@ -108,8 +108,10 @@ impl OutputFormatter { } } - /// Print all available rules by oxlint - /// See [`InternalFormatter::all_rules`] for more details. + /// Return a rendered listing of all available rules (if supported by the + /// underlying formatter). This used to exist as a public helper; restoring + /// it eliminates the `dead_code` warning on the trait method without + /// resorting to allow attributes. pub fn all_rules(&self) -> Option { self.internal.all_rules() } diff --git a/crates/oxc_linter/src/table.rs b/crates/oxc_linter/src/table.rs index e1bca4dd476fc..d79258f4f88fb 100644 --- a/crates/oxc_linter/src/table.rs +++ b/crates/oxc_linter/src/table.rs @@ -106,20 +106,29 @@ impl RuleTable { } impl RuleTableSection { - /// Renders all the rules in this section as a markdown table. + /// Internal helper that renders a markdown table for this section. /// - /// 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 { + /// 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 { const FIX_EMOJI_COL_WIDTH: usize = 10; const DEFAULT_EMOJI_COL_WIDTH: usize = 9; + const ENABLED_EMOJI_COL_WIDTH: usize = 10; /// text width, leave 2 spaces for padding const FIX: usize = FIX_EMOJI_COL_WIDTH - 2; const DEFAULT: usize = DEFAULT_EMOJI_COL_WIDTH - 2; + const ENABLED: usize = ENABLED_EMOJI_COL_WIDTH - 2; + + let include_enabled = enabled.is_some(); let mut s = String::new(); let category = &self.category; - let rows = &self.rows; + let rows: &Vec = &self.rows; let rule_width = self.rule_column_width; let plugin_width = self.plugin_column_width; writeln!(s, "## {} ({}):", category, rows.len()).unwrap(); @@ -127,17 +136,42 @@ impl RuleTableSection { writeln!(s, "{}", category.description()).unwrap(); let x = ""; - writeln!( - s, - "| {: FIX { (emoji, 0) } else { (emoji, FIX - len) } - }); - writeln!(s, "| {rendered_name:, fix_col_width: usize) -> (&str, usize) { + emoji.map_or(("", fix_col_width), |emoji| { + let display_width: isize = match emoji { + "⚠️🛠️️" => -3, + "⚠️🛠️️💡" => -2, + "🛠️" => -1, + "💡" | "🚧" => 1, + "" | "🛠️💡" | "⚠️💡" => 0, + _ => 2, + }; + let width = if display_width < 0 { + fix_col_width.saturating_add((-display_width) as usize) + } else { + fix_col_width.saturating_sub(display_width as usize) + }; + (emoji, width) + }) + } + + /// 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_cli( + &self, + link_prefix: Option<&str>, + enabled: &FxHashSet<&str>, + ) -> String { + self.render_markdown_table_inner(link_prefix, Some(enabled)) + } } #[cfg(test)] @@ -202,4 +284,23 @@ mod test { 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(); + if let Some(first) = section.rows.first() { + enabled.insert(first.name); + } + + let rendered_table = section.render_markdown_table_cli(Some(PREFIX), &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()); + assert!(rendered_table.contains("Enabled?")); + } + } }