Skip to content

Commit 72660f7

Browse files
authored
feat(linter): Support auto generate config document for tuple lint option (#15904)
Related to #15797. This allows configuration for rule like `eslint/sort-keys` to render correctly. The generated result is bellow ```markdown ## Configuration ### The 1st option type: `"desc" | "asc"` Sorting order for keys. Accepts "asc" for ascending or "desc" for descending. ### The 2nd option This option is a object with the following properties: #### allowLineSeparatedGroups type: `boolean` default: `false` When true, groups of properties separated by a blank line are sorted independently. #### caseSensitive type: `boolean` default: `true` Whether the sort comparison is case-sensitive (A < a when true). #### minKeys type: `integer` default: `2` Minimum number of properties required in an object before sorting is enforced. #### natural type: `boolean` default: `false` Use natural sort order so that, for example, "a2" comes before "a10". ```
1 parent 0c1f82b commit 72660f7

File tree

3 files changed

+88
-28
lines changed

3 files changed

+88
-28
lines changed

crates/oxc_linter/src/rules/eslint/sort_keys.rs

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ use serde::{Deserialize, Serialize};
1313
use crate::{AstNode, context::LintContext, rule::Rule};
1414

1515
#[derive(Debug, Default, Clone)]
16-
pub struct SortKeys(Box<SortKeysOptions>);
16+
pub struct SortKeys(Box<SortKeysConfig>);
1717

1818
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
1919
#[serde(rename_all = "lowercase")]
20+
/// Sorting order for keys. Accepts "asc" for ascending or "desc" for descending.
2021
pub enum SortOrder {
2122
Desc,
2223
#[default]
@@ -26,8 +27,6 @@ pub enum SortOrder {
2627
#[derive(Debug, Clone, JsonSchema)]
2728
#[serde(rename_all = "camelCase", default)]
2829
pub struct SortKeysOptions {
29-
/// Sorting order for keys. Accepts "asc" for ascending or "desc" for descending.
30-
sort_order: SortOrder,
3130
/// Whether the sort comparison is case-sensitive (A < a when true).
3231
case_sensitive: bool,
3332
/// Use natural sort order so that, for example, "a2" comes before "a10".
@@ -42,7 +41,6 @@ impl Default for SortKeysOptions {
4241
fn default() -> Self {
4342
// we follow the eslint defaults
4443
Self {
45-
sort_order: SortOrder::Asc,
4644
case_sensitive: true,
4745
natural: false,
4846
min_keys: 2,
@@ -51,11 +49,16 @@ impl Default for SortKeysOptions {
5149
}
5250
}
5351

54-
impl std::ops::Deref for SortKeys {
55-
type Target = SortKeysOptions;
52+
#[derive(Debug, Default, Clone, JsonSchema)]
53+
#[serde(default)]
54+
pub struct SortKeysConfig(SortOrder, SortKeysOptions);
5655

57-
fn deref(&self) -> &Self::Target {
58-
&self.0
56+
impl SortKeys {
57+
fn sort_order(&self) -> &SortOrder {
58+
&(*self.0).0
59+
}
60+
fn options(&self) -> &SortKeysOptions {
61+
&(*self.0).1
5962
}
6063
}
6164

@@ -94,7 +97,7 @@ declare_oxc_lint!(
9497
eslint,
9598
style,
9699
conditional_fix,
97-
config = SortKeysOptions
100+
config = SortKeysConfig
98101
);
99102

100103
impl Rule for SortKeys {
@@ -130,20 +133,19 @@ impl Rule for SortKeys {
130133
.and_then(serde_json::Value::as_bool)
131134
.unwrap_or(false);
132135

133-
Self(Box::new(SortKeysOptions {
136+
Self(Box::new(SortKeysConfig(
134137
sort_order,
135-
case_sensitive,
136-
natural,
137-
min_keys,
138-
allow_line_separated_groups,
139-
}))
138+
SortKeysOptions { case_sensitive, natural, min_keys, allow_line_separated_groups },
139+
)))
140140
}
141141

142142
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
143143
if let AstKind::ObjectExpression(dec) = node.kind() {
144-
if dec.properties.len() < self.min_keys {
144+
let options = self.options();
145+
if dec.properties.len() < options.min_keys {
145146
return;
146147
}
148+
let sort_order = self.sort_order().clone();
147149

148150
let mut property_groups: Vec<Vec<String>> = vec![vec![]];
149151

@@ -157,7 +159,7 @@ impl Rule for SortKeys {
157159
}
158160
ObjectPropertyKind::ObjectProperty(obj) => {
159161
let Some(key) = obj.key.static_name() else { continue };
160-
if i != dec.properties.len() - 1 && self.allow_line_separated_groups {
162+
if i != dec.properties.len() - 1 && options.allow_line_separated_groups {
161163
let text_between = extract_text_between_spans(
162164
source_text,
163165
prop.span(),
@@ -175,7 +177,7 @@ impl Rule for SortKeys {
175177
}
176178
}
177179

178-
if !self.case_sensitive {
180+
if !options.case_sensitive {
179181
for group in &mut property_groups {
180182
*group = group
181183
.iter()
@@ -186,13 +188,13 @@ impl Rule for SortKeys {
186188

187189
let mut sorted_property_groups = property_groups.clone();
188190
for group in &mut sorted_property_groups {
189-
if self.natural {
191+
if options.natural {
190192
natural_sort(group);
191193
} else {
192194
alphanumeric_sort(group);
193195
}
194196

195-
if self.sort_order == SortOrder::Desc {
197+
if sort_order == SortOrder::Desc {
196198
group.reverse();
197199
}
198200
}
@@ -259,7 +261,7 @@ impl Rule for SortKeys {
259261
let keys_for_cmp: Vec<String> = props
260262
.iter()
261263
.map(|(k, _)| {
262-
if self.case_sensitive {
264+
if options.case_sensitive {
263265
k.clone()
264266
} else {
265267
k.cow_to_ascii_lowercase().to_string()
@@ -270,12 +272,12 @@ impl Rule for SortKeys {
270272
// Compute the sorted key order using the same helpers as the main rule
271273
// so the autofix ordering matches the diagnostic ordering.
272274
let mut sorted_keys = keys_for_cmp.clone();
273-
if self.natural {
275+
if options.natural {
274276
natural_sort(&mut sorted_keys);
275277
} else {
276278
alphanumeric_sort(&mut sorted_keys);
277279
}
278-
if self.sort_order == SortOrder::Desc {
280+
if sort_order == SortOrder::Desc {
279281
sorted_keys.reverse();
280282
}
281283

tasks/website/src/linter/json_schema.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ struct Root {
6868
pub(super) struct Section {
6969
level: String,
7070
title: String,
71-
instance_type: Option<String>,
72-
description: String,
71+
pub(super) instance_type: Option<String>,
72+
pub(super) description: String,
7373
pub(super) default: Option<String>,
7474
sections: Vec<Section>,
7575
}
@@ -144,13 +144,23 @@ impl Renderer {
144144
return array
145145
.items
146146
.iter()
147-
.map(|item| match item {
147+
.flat_map(|item| match item {
148+
// array
148149
SingleOrVec::Single(schema) => {
149150
let schema_object = Self::get_schema_object(schema);
150151
let key = parent_key.map_or_else(String::new, |k| format!("{k}[n]"));
151-
self.render_schema_impl(depth + 1, &key, schema_object)
152+
vec![self.render_schema_impl(depth + 1, &key, schema_object)]
152153
}
153-
SingleOrVec::Vec(_) => panic!(),
154+
// tuple
155+
SingleOrVec::Vec(schema) => schema
156+
.iter()
157+
.enumerate()
158+
.map(|(i, schema)| {
159+
let schema_object = Self::get_schema_object(schema);
160+
let key = parent_key.map_or_else(String::new, |k| format!("{k}[{i}]"));
161+
self.render_schema_impl(depth + 1, &key, schema_object)
162+
})
163+
.collect(),
154164
})
155165
.collect();
156166
}

tasks/website/src/linter/rules/doc_page.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{
66
path::PathBuf,
77
};
88

9+
use itertools::Itertools;
910
use oxc_linter::{LintPlugins, table::RuleTableRow};
1011
use schemars::{
1112
JsonSchema, SchemaGenerator,
@@ -122,6 +123,40 @@ const source = `{}`;
122123
}
123124

124125
fn rule_config(&self, schema: &SchemaObject) -> String {
126+
if let Some(array) = &schema.array
127+
&& let Some(SingleOrVec::Vec(options)) = &array.items
128+
{
129+
// multiple options
130+
return options
131+
.iter()
132+
.enumerate()
133+
.map(|(i, schema)| match schema {
134+
Schema::Object(schema_object) => {
135+
let section = self.renderer.render_schema(3, "", schema_object);
136+
let title = format!("\n### The {} option\n", ordinal(i + 1));
137+
let instance_type = section.instance_type.as_ref().map_or_else(
138+
String::new,
139+
|instance_type| {
140+
if instance_type == "object" {
141+
"\nThis option is an object with the following properties:\n"
142+
.to_string()
143+
} else {
144+
format!("\ntype: `{instance_type}`\n")
145+
}
146+
},
147+
);
148+
let rendered = section.to_md(&self.renderer);
149+
let description = if section.description.is_empty() {
150+
section.description
151+
} else {
152+
format!("\n{}\n", section.description)
153+
};
154+
format!("{title}{instance_type}{description}{rendered}")
155+
}
156+
Schema::Bool(_) => panic!(),
157+
})
158+
.join("");
159+
}
125160
let mut section = self.renderer.render_schema(2, "", schema);
126161
if section.default.is_none()
127162
&& 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:
262297
"
263298
)
264299
}
300+
301+
fn ordinal(n: usize) -> String {
302+
let suffix = match n % 100 {
303+
11..=13 => "th",
304+
_ => match n % 10 {
305+
1 => "st",
306+
2 => "nd", // spellchecker:disable-line
307+
3 => "rd",
308+
_ => "th",
309+
},
310+
};
311+
format!("{n}{suffix}")
312+
}

0 commit comments

Comments
 (0)