diff --git a/crates/oxc_linter/src/rules/eslint/sort_keys.rs b/crates/oxc_linter/src/rules/eslint/sort_keys.rs index 95864d884fc2c..5a2384deda41d 100644 --- a/crates/oxc_linter/src/rules/eslint/sort_keys.rs +++ b/crates/oxc_linter/src/rules/eslint/sort_keys.rs @@ -13,10 +13,11 @@ use serde::{Deserialize, Serialize}; use crate::{AstNode, context::LintContext, rule::Rule}; #[derive(Debug, Default, Clone)] -pub struct SortKeys(Box); +pub struct SortKeys(Box); #[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] +/// Sorting order for keys. Accepts "asc" for ascending or "desc" for descending. pub enum SortOrder { Desc, #[default] @@ -26,8 +27,6 @@ pub enum SortOrder { #[derive(Debug, Clone, JsonSchema)] #[serde(rename_all = "camelCase", default)] pub struct SortKeysOptions { - /// Sorting order for keys. Accepts "asc" for ascending or "desc" for descending. - sort_order: SortOrder, /// Whether the sort comparison is case-sensitive (A < a when true). case_sensitive: bool, /// Use natural sort order so that, for example, "a2" comes before "a10". @@ -42,7 +41,6 @@ impl Default for SortKeysOptions { fn default() -> Self { // we follow the eslint defaults Self { - sort_order: SortOrder::Asc, case_sensitive: true, natural: false, min_keys: 2, @@ -51,11 +49,16 @@ impl Default for SortKeysOptions { } } -impl std::ops::Deref for SortKeys { - type Target = SortKeysOptions; +#[derive(Debug, Default, Clone, JsonSchema)] +#[serde(default)] +pub struct SortKeysConfig(SortOrder, SortKeysOptions); - fn deref(&self) -> &Self::Target { - &self.0 +impl SortKeys { + fn sort_order(&self) -> &SortOrder { + &(*self.0).0 + } + fn options(&self) -> &SortKeysOptions { + &(*self.0).1 } } @@ -94,7 +97,7 @@ declare_oxc_lint!( eslint, style, conditional_fix, - config = SortKeysOptions + config = SortKeysConfig ); impl Rule for SortKeys { @@ -130,20 +133,19 @@ impl Rule for SortKeys { .and_then(serde_json::Value::as_bool) .unwrap_or(false); - Self(Box::new(SortKeysOptions { + Self(Box::new(SortKeysConfig( sort_order, - case_sensitive, - natural, - min_keys, - allow_line_separated_groups, - })) + SortKeysOptions { case_sensitive, natural, min_keys, allow_line_separated_groups }, + ))) } fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { if let AstKind::ObjectExpression(dec) = node.kind() { - if dec.properties.len() < self.min_keys { + let options = self.options(); + if dec.properties.len() < options.min_keys { return; } + let sort_order = self.sort_order().clone(); let mut property_groups: Vec> = vec![vec![]]; @@ -157,7 +159,7 @@ impl Rule for SortKeys { } ObjectPropertyKind::ObjectProperty(obj) => { let Some(key) = obj.key.static_name() else { continue }; - if i != dec.properties.len() - 1 && self.allow_line_separated_groups { + if i != dec.properties.len() - 1 && options.allow_line_separated_groups { let text_between = extract_text_between_spans( source_text, prop.span(), @@ -175,7 +177,7 @@ impl Rule for SortKeys { } } - if !self.case_sensitive { + if !options.case_sensitive { for group in &mut property_groups { *group = group .iter() @@ -186,13 +188,13 @@ impl Rule for SortKeys { let mut sorted_property_groups = property_groups.clone(); for group in &mut sorted_property_groups { - if self.natural { + if options.natural { natural_sort(group); } else { alphanumeric_sort(group); } - if self.sort_order == SortOrder::Desc { + if sort_order == SortOrder::Desc { group.reverse(); } } @@ -259,7 +261,7 @@ impl Rule for SortKeys { let keys_for_cmp: Vec = props .iter() .map(|(k, _)| { - if self.case_sensitive { + if options.case_sensitive { k.clone() } else { k.cow_to_ascii_lowercase().to_string() @@ -270,12 +272,12 @@ impl Rule for SortKeys { // Compute the sorted key order using the same helpers as the main rule // so the autofix ordering matches the diagnostic ordering. let mut sorted_keys = keys_for_cmp.clone(); - if self.natural { + if options.natural { natural_sort(&mut sorted_keys); } else { alphanumeric_sort(&mut sorted_keys); } - if self.sort_order == SortOrder::Desc { + if sort_order == SortOrder::Desc { sorted_keys.reverse(); } diff --git a/tasks/website/src/linter/json_schema.rs b/tasks/website/src/linter/json_schema.rs index 85b308500ed91..37480f9511308 100644 --- a/tasks/website/src/linter/json_schema.rs +++ b/tasks/website/src/linter/json_schema.rs @@ -68,8 +68,8 @@ struct Root { pub(super) struct Section { level: String, title: String, - instance_type: Option, - description: String, + pub(super) instance_type: Option, + pub(super) description: String, pub(super) default: Option, sections: Vec
, } @@ -144,13 +144,23 @@ impl Renderer { return array .items .iter() - .map(|item| match item { + .flat_map(|item| match item { + // array SingleOrVec::Single(schema) => { let schema_object = Self::get_schema_object(schema); let key = parent_key.map_or_else(String::new, |k| format!("{k}[n]")); - self.render_schema_impl(depth + 1, &key, schema_object) + vec![self.render_schema_impl(depth + 1, &key, schema_object)] } - SingleOrVec::Vec(_) => panic!(), + // tuple + SingleOrVec::Vec(schema) => schema + .iter() + .enumerate() + .map(|(i, schema)| { + let schema_object = Self::get_schema_object(schema); + let key = parent_key.map_or_else(String::new, |k| format!("{k}[{i}]")); + self.render_schema_impl(depth + 1, &key, schema_object) + }) + .collect(), }) .collect(); } diff --git a/tasks/website/src/linter/rules/doc_page.rs b/tasks/website/src/linter/rules/doc_page.rs index ca5d29b06d4f7..b4f831e576035 100644 --- a/tasks/website/src/linter/rules/doc_page.rs +++ b/tasks/website/src/linter/rules/doc_page.rs @@ -6,6 +6,7 @@ use std::{ path::PathBuf, }; +use itertools::Itertools; use oxc_linter::{LintPlugins, table::RuleTableRow}; use schemars::{ JsonSchema, SchemaGenerator, @@ -122,6 +123,40 @@ const source = `{}`; } fn rule_config(&self, schema: &SchemaObject) -> String { + if let Some(array) = &schema.array + && let Some(SingleOrVec::Vec(options)) = &array.items + { + // multiple options + return options + .iter() + .enumerate() + .map(|(i, schema)| match schema { + Schema::Object(schema_object) => { + let section = self.renderer.render_schema(3, "", schema_object); + let title = format!("\n### The {} option\n", ordinal(i + 1)); + let instance_type = section.instance_type.as_ref().map_or_else( + String::new, + |instance_type| { + if instance_type == "object" { + "\nThis option is an object with the following properties:\n" + .to_string() + } else { + format!("\ntype: `{instance_type}`\n") + } + }, + ); + let rendered = section.to_md(&self.renderer); + let description = if section.description.is_empty() { + section.description + } else { + format!("\n{}\n", section.description) + }; + format!("{title}{instance_type}{description}{rendered}") + } + Schema::Bool(_) => panic!(), + }) + .join(""); + } let mut section = self.renderer.render_schema(2, "", schema); if section.default.is_none() && let Some(SingleOrVec::Single(ty)) = &schema.instance_type @@ -262,3 +297,16 @@ To **enable** this rule in the CLI or using the config file, you can use: " ) } + +fn ordinal(n: usize) -> String { + let suffix = match n % 100 { + 11..=13 => "th", + _ => match n % 10 { + 1 => "st", + 2 => "nd", // spellchecker:disable-line + 3 => "rd", + _ => "th", + }, + }; + format!("{n}{suffix}") +}