Skip to content

Commit 5a66558

Browse files
committed
feat(linter): Handle config docs for string/enum values and add config option docs for various rules. (#15759)
This allows rules like `return-await` to render their possible values accurately on the docs website. Fixes #15078. Also part of #14743. Used GitHub Copilot + Claude Sonnet 4.5 for most of the json schema edits, after figuring out which parts needed modification to enable this. Updates the following rules with config option docs and uses DefaultRuleConfig for configuration handling: - `react/prefer-es6-class` - `unicorn/switch-case-braces` - `vue/define-emits-declaration` - `vue/define-props-declaration` - `typescript/consistent-indexed-object-style` Generated docs are as follows. For `typescript/consistent-indexed-object-style`: ```md ## Configuration This rule accepts one of the following string values: ### `"record"` When set to `record`, enforces the use of a `Record` for indexed object types, e.g. `Record<string, unknown>`. ### `"index-signature"` When set to `index-signature`, enforces the use of indexed signature types, e.g. `{ [key: string]: unknown }`. ``` For `typescript/return-await`: ```md ## Configuration This rule accepts one of the following string values: ### `"in-try-catch"` Require `await` when returning Promises inside try/catch/finally blocks. This ensures proper error handling and stack traces. ### `"always"` Require `await` before returning Promises in all cases. Example: `return await Promise.resolve()` is required. ### `"error-handling-correctness-only"` Require `await` only when it affects error handling correctness. Only flags cases where omitting await would change error handling behavior. ### `"never"` Disallow `await` before returning Promises in all cases. Example: `return Promise.resolve()` is required (no await). ``` For `vue/define-emits-declaration`: ```md ## Configuration This rule accepts one of the following string values: ### `"type-based"` Enforce type-based declaration. ### `"type-literal"` Enforce type-literal declaration. ### `"runtime"` Enforce runtime declaration. ``` For `vue/define-props-declaration`: ```md ## Configuration This rule accepts one of the following string values: ### `"type-based"` Enforce type-based declaration. ### `"runtime"` Enforce runtime declaration. ``` For `react/prefer-es6-class`: ```md ## Configuration This rule accepts one of the following string values: ### `"always"` Always prefer ES6 class-style components ### `"never"` Do not allow ES6 class-style ```
1 parent c3744c4 commit 5a66558

File tree

8 files changed

+200
-120
lines changed

8 files changed

+200
-120
lines changed

crates/oxc_linter/src/rules/react/prefer_es6_class.rs

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use oxc_ast::AstKind;
22
use oxc_diagnostics::OxcDiagnostic;
33
use oxc_macros::declare_oxc_lint;
44
use oxc_span::{GetSpan, Span};
5+
use schemars::JsonSchema;
6+
use serde::{Deserialize, Serialize};
57

68
use crate::{
79
AstNode,
810
context::{ContextHost, LintContext},
9-
rule::Rule,
11+
rule::{DefaultRuleConfig, Rule},
1012
utils::{is_es5_component, is_es6_component},
1113
};
1214

@@ -20,11 +22,19 @@ fn expected_es6_class_diagnostic(span: Span) -> OxcDiagnostic {
2022
.with_label(span)
2123
}
2224

23-
#[derive(Debug, Default, Clone)]
24-
pub struct PreferEs6Class {
25-
prefer_es6_class_option: PreferES6ClassOptionType,
25+
#[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)]
26+
#[serde(rename_all = "kebab-case")]
27+
enum PreferES6ClassOptionType {
28+
/// Always prefer ES6 class-style components
29+
#[default]
30+
Always,
31+
/// Do not allow ES6 class-style
32+
Never,
2633
}
2734

35+
#[derive(Debug, Default, Clone)]
36+
pub struct PreferEs6Class(PreferES6ClassOptionType);
37+
2838
declare_oxc_lint!(
2939
/// ### What it does
3040
///
@@ -48,30 +58,27 @@ declare_oxc_lint!(
4858
PreferEs6Class,
4959
react,
5060
style,
61+
config = PreferES6ClassOptionType,
5162
);
5263

5364
impl Rule for PreferEs6Class {
5465
fn from_configuration(value: serde_json::Value) -> Self {
55-
let obj = value.get(0);
56-
57-
Self {
58-
prefer_es6_class_option: obj
59-
.and_then(serde_json::Value::as_str)
60-
.map(PreferES6ClassOptionType::from)
61-
.unwrap_or_default(),
62-
}
66+
Self(
67+
serde_json::from_value::<DefaultRuleConfig<PreferES6ClassOptionType>>(value)
68+
.unwrap_or_default()
69+
.into_inner(),
70+
)
6371
}
6472

6573
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
6674
match node.kind() {
6775
AstKind::CallExpression(call_expr)
68-
if matches!(self.prefer_es6_class_option, PreferES6ClassOptionType::Always)
69-
&& is_es5_component(node) =>
76+
if matches!(self.0, PreferES6ClassOptionType::Always) && is_es5_component(node) =>
7077
{
7178
ctx.diagnostic(expected_es6_class_diagnostic(call_expr.callee.span()));
7279
}
7380
AstKind::Class(class_expr)
74-
if !matches!(self.prefer_es6_class_option, PreferES6ClassOptionType::Always)
81+
if !matches!(self.0, PreferES6ClassOptionType::Always)
7582
&& is_es6_component(node) =>
7683
{
7784
ctx.diagnostic(unexpected_es6_class_diagnostic(
@@ -87,22 +94,6 @@ impl Rule for PreferEs6Class {
8794
}
8895
}
8996

90-
#[derive(Debug, Default, Clone)]
91-
enum PreferES6ClassOptionType {
92-
#[default]
93-
Always,
94-
Never,
95-
}
96-
97-
impl PreferES6ClassOptionType {
98-
pub fn from(raw: &str) -> Self {
99-
match raw {
100-
"always" => Self::Always,
101-
_ => Self::Never,
102-
}
103-
}
104-
}
105-
10697
#[test]
10798
fn test() {
10899
use crate::tester::Tester;

crates/oxc_linter/src/rules/typescript/consistent_indexed_object_style.rs

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
1111
use crate::{
1212
AstNode,
1313
context::{ContextHost, LintContext},
14-
rule::Rule,
14+
rule::{DefaultRuleConfig, Rule},
1515
};
1616

1717
fn consistent_indexed_object_style_diagnostic(
@@ -32,25 +32,16 @@ fn consistent_indexed_object_style_diagnostic(
3232
OxcDiagnostic::warn(warning_message).with_help(help_message).with_label(span)
3333
}
3434

35-
#[derive(Debug, Clone, JsonSchema)]
36-
#[serde(rename_all = "camelCase", default)]
37-
pub struct ConsistentIndexedObjectStyle {
38-
/// When set to `record`, enforces the use of a `Record` for indexed object types, e.g. `Record<string, unknown>`.
39-
/// When set to `index-signature`, enforces the use of indexed signature types, e.g. `{ [key: string]: unknown }`.
40-
preferred_style: ConsistentIndexedObjectStyleConfig,
41-
}
42-
43-
impl Default for ConsistentIndexedObjectStyle {
44-
fn default() -> Self {
45-
Self { preferred_style: ConsistentIndexedObjectStyleConfig::Record }
46-
}
47-
}
35+
#[derive(Debug, Clone, Default)]
36+
pub struct ConsistentIndexedObjectStyle(ConsistentIndexedObjectStyleConfig);
4837

4938
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize, JsonSchema)]
5039
#[serde(rename_all = "kebab-case")]
5140
enum ConsistentIndexedObjectStyleConfig {
41+
/// When set to `record`, enforces the use of a `Record` for indexed object types, e.g. `Record<string, unknown>`.
5242
#[default]
5343
Record,
44+
/// When set to `index-signature`, enforces the use of indexed signature types, e.g. `{ [key: string]: unknown }`.
5445
IndexSignature,
5546
}
5647

@@ -113,25 +104,22 @@ declare_oxc_lint!(
113104
typescript,
114105
style,
115106
conditional_fix,
116-
config = ConsistentIndexedObjectStyle,
107+
config = ConsistentIndexedObjectStyleConfig,
117108
);
118109

119110
impl Rule for ConsistentIndexedObjectStyle {
120111
fn from_configuration(value: serde_json::Value) -> Self {
121-
let config = value.get(0).and_then(serde_json::Value::as_str).map_or_else(
122-
ConsistentIndexedObjectStyleConfig::default,
123-
|value| match value {
124-
"record" => ConsistentIndexedObjectStyleConfig::Record,
125-
_ => ConsistentIndexedObjectStyleConfig::IndexSignature,
126-
},
127-
);
128-
Self { preferred_style: config }
112+
Self(
113+
serde_json::from_value::<DefaultRuleConfig<ConsistentIndexedObjectStyleConfig>>(value)
114+
.unwrap_or_default()
115+
.into_inner(),
116+
)
129117
}
130118

131119
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
132-
let preferred_style = self.preferred_style;
120+
let preferred_style = self.0;
133121

134-
if self.preferred_style == ConsistentIndexedObjectStyleConfig::Record {
122+
if self.0 == ConsistentIndexedObjectStyleConfig::Record {
135123
match node.kind() {
136124
AstKind::TSInterfaceDeclaration(inf) => {
137125
if inf.body.body.len() > 1 {

crates/oxc_linter/src/rules/unicorn/switch_case_braces.rs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@ use oxc_ast::{AstKind, ast::Statement};
22
use oxc_diagnostics::OxcDiagnostic;
33
use oxc_macros::declare_oxc_lint;
44
use oxc_span::{GetSpan, Span};
5+
use schemars::JsonSchema;
6+
use serde::{Deserialize, Serialize};
57

6-
use crate::{AstNode, ast_util::get_preceding_indent_str, context::LintContext, rule::Rule};
8+
use crate::{
9+
AstNode,
10+
ast_util::get_preceding_indent_str,
11+
context::LintContext,
12+
rule::{DefaultRuleConfig, Rule},
13+
};
714

815
fn switch_case_braces_diagnostic_empty_clause(span: Span) -> OxcDiagnostic {
916
OxcDiagnostic::warn("Unexpected braces in empty case clause.")
@@ -24,16 +31,24 @@ fn switch_case_braces_diagnostic_unnecessary_braces(span: Span) -> OxcDiagnostic
2431
}
2532

2633
#[derive(Debug, Clone)]
27-
pub struct SwitchCaseBraces {
28-
always_braces: bool,
29-
}
34+
pub struct SwitchCaseBraces(Box<SwitchCaseBracesConfig>);
3035

3136
impl Default for SwitchCaseBraces {
3237
fn default() -> Self {
33-
Self { always_braces: true }
38+
Self(Box::new(SwitchCaseBracesConfig::Always))
3439
}
3540
}
3641

42+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
43+
#[serde(rename_all = "kebab-case")]
44+
pub enum SwitchCaseBracesConfig {
45+
/// Always require braces in case clauses (except empty cases).
46+
#[default]
47+
Always,
48+
/// Allow braces only when needed for scoping (e.g., variable or function declarations).
49+
Avoid,
50+
}
51+
3752
declare_oxc_lint!(
3853
/// ### What it does
3954
///
@@ -68,30 +83,24 @@ declare_oxc_lint!(
6883
/// }
6984
/// ```
7085
///
71-
/// ### Options
72-
///
73-
/// `{ type: "always" | "avoid", default: "always" }`
74-
///
75-
/// - `"always"`
76-
/// Always report when clause is not a `BlockStatement`.
77-
///
78-
/// - `"avoid"`
79-
/// Allows braces only when needed for scoping (e.g., variable or function declarations).
80-
///
81-
/// Example:
86+
/// Example config:
8287
/// ```json
8388
/// "unicorn/switch-case-braces": ["error", "avoid"]
8489
/// ```
8590
SwitchCaseBraces,
8691
unicorn,
8792
style,
88-
fix
93+
fix,
94+
config = SwitchCaseBracesConfig,
8995
);
9096

9197
impl Rule for SwitchCaseBraces {
9298
fn from_configuration(value: serde_json::Value) -> Self {
93-
let always_braces = value.get(0).is_none_or(|v| v.as_str() != Some("avoid"));
94-
Self { always_braces }
99+
Self(Box::new(
100+
serde_json::from_value::<DefaultRuleConfig<SwitchCaseBracesConfig>>(value)
101+
.unwrap_or_default()
102+
.into_inner(),
103+
))
95104
}
96105

97106
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
@@ -116,7 +125,7 @@ impl Rule for SwitchCaseBraces {
116125
);
117126
continue;
118127
}
119-
if !self.always_braces
128+
if *self.0 == SwitchCaseBracesConfig::Avoid
120129
&& !block_stmt.body.iter().any(|stmt| {
121130
matches!(
122131
stmt,
@@ -141,7 +150,7 @@ impl Rule for SwitchCaseBraces {
141150
_ => true,
142151
};
143152

144-
if self.always_braces && missing_braces {
153+
if *self.0 == SwitchCaseBracesConfig::Always && missing_braces {
145154
let test_end = case.test.as_ref().map_or(case.span.start, |t| t.span().end);
146155
let colon_offset = ctx.find_next_token_from(test_end, ":").unwrap();
147156
let colon_pos = test_end + colon_offset;

crates/oxc_linter/src/rules/vue/define_emits_declaration.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ use oxc_ast::{
55
use oxc_diagnostics::OxcDiagnostic;
66
use oxc_macros::declare_oxc_lint;
77
use oxc_span::{GetSpan, Span};
8+
use schemars::JsonSchema;
9+
use serde::{Deserialize, Serialize};
810

911
use crate::{
1012
AstNode,
1113
context::{ContextHost, LintContext},
1214
frameworks::FrameworkOptions,
13-
rule::Rule,
15+
rule::{DefaultRuleConfig, Rule},
1416
};
1517

1618
fn has_arg_diagnostic(span: Span) -> OxcDiagnostic {
@@ -30,11 +32,18 @@ fn has_type_call_diagnostic(span: Span) -> OxcDiagnostic {
3032
.with_label(span)
3133
}
3234

33-
#[derive(Debug, Default, Clone)]
35+
#[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)]
36+
#[serde(rename_all = "kebab-case")]
3437
enum DeclarationStyle {
38+
/// Enforces the use of a named TypeScript type or interface as the
39+
/// argument to `defineEmits`, e.g. `defineEmits<MyEmits>()`.
3540
#[default]
3641
TypeBased,
42+
/// Enforces the use of an inline type literal as the argument to
43+
/// `defineEmits`, e.g. `defineEmits<{ (event: string): void }>()`.
3744
TypeLiteral,
45+
/// Enforces the use of runtime declaration, where emits are declared
46+
/// using an array or object, e.g. `defineEmits(['event1', 'event2'])`.
3847
Runtime,
3948
}
4049

@@ -111,31 +120,22 @@ declare_oxc_lint!(
111120
/// });
112121
/// </script>
113122
/// ```
114-
///
115-
/// ### Options
116-
///
117-
/// ```
118-
/// "vue/define-emits-declaration": ["error", "type-based" | "type-literal" | "runtime"]
119-
/// ```
120-
///
121-
/// - `type-based` (default): Enforce type-based declaration
122-
/// - `type-literal`: Enforce type-literal declaration
123-
/// - `runtime`: Enforce runtime declaration
124123
DefineEmitsDeclaration,
125124
vue,
126125
style,
127-
pending // TODO: transform it to the other declaration (if possible)
126+
pending, // TODO: transform it to the other declaration (if possible)
127+
config = DeclarationStyle,
128128
);
129129

130130
impl Rule for DefineEmitsDeclaration {
131131
fn from_configuration(value: serde_json::Value) -> Self {
132-
let val = value.get(0).and_then(|v| v.as_str());
133-
Self(match val {
134-
Some("type-literal") => DeclarationStyle::TypeLiteral,
135-
Some("runtime") => DeclarationStyle::Runtime,
136-
_ => DeclarationStyle::TypeBased,
137-
})
132+
Self(
133+
serde_json::from_value::<DefaultRuleConfig<DeclarationStyle>>(value)
134+
.unwrap_or_default()
135+
.into_inner(),
136+
)
138137
}
138+
139139
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
140140
let AstKind::CallExpression(call_expr) = node.kind() else { return };
141141

0 commit comments

Comments
 (0)