Skip to content

Commit 9573937

Browse files
authored
fix(linter/typescript): validate ban-ts-comment description_format (#25320)
adds validation for the `descriptionFormat` regex option from #23594 and replaces manual config parsing with `DefaultRuleConfig`
1 parent 6be314f commit 9573937

3 files changed

Lines changed: 61 additions & 62 deletions

File tree

‎apps/oxlint/src-js/package/config.generated.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,7 @@ export type ImportKind = "none" | "all" | "multiple" | "single";
250250
export type SortOrder = "desc" | "asc";
251251
export type ArrayOption = "array" | "array-simple" | "generic";
252252
export type ReadonlyArrayOption = "array" | "array-simple" | "generic";
253-
export type DirectiveConfigSchema =
254-
| boolean
255-
| RequireDescription
256-
| {
257-
descriptionFormat?: string;
258-
};
253+
export type DirectiveConfigSchema = boolean | RequireDescription | DescriptionFormatConfig;
259254
export type RequireDescription = "allow-with-description";
260255
export type ClassLiteralPropertyStyleOption = "fields" | "getters";
261256
export type PreferGenericType = "constructor" | "type-annotation";
@@ -5277,6 +5272,9 @@ export interface BanTsCommentConfig {
52775272
*/
52785273
"ts-nocheck"?: DirectiveConfigSchema;
52795274
}
5275+
export interface DescriptionFormatConfig {
5276+
descriptionFormat?: string;
5277+
}
52805278
export interface ConsistentReturnConfig {
52815279
/**
52825280
* Treat explicit `return undefined` as equivalent to an unspecified return.

‎crates/oxc_linter/src/rules/typescript/ban_ts_comment.rs‎

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ use oxc_diagnostics::OxcDiagnostic;
44
use oxc_macros::declare_oxc_lint;
55
use oxc_span::Span;
66
use schemars::JsonSchema;
7+
use serde::Deserialize;
78

89
use crate::{
910
context::{ContextHost, LintContext},
10-
rule::Rule,
11+
rule::{DefaultRuleConfig, Rule},
12+
utils::deserialize_required_regex_option,
1113
};
1214

1315
fn comment(ts_comment_name: &str, span: Span) -> OxcDiagnostic {
@@ -45,10 +47,10 @@ fn comment_description_not_match_pattern(
4547
.with_label(span)
4648
}
4749

48-
#[derive(Debug, Default, Clone)]
50+
#[derive(Debug, Default, Clone, Deserialize)]
4951
pub struct BanTsComment(Box<BanTsCommentConfig>);
5052

51-
#[derive(Debug, Clone, JsonSchema)]
53+
#[derive(Debug, Clone, Deserialize, JsonSchema)]
5254
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
5355
/// This rule allows you to specify how different TypeScript directive comments
5456
/// should be handled.
@@ -122,10 +124,7 @@ pub enum DirectiveConfig {
122124
enum DirectiveConfigSchema {
123125
Boolean(bool),
124126
RequireDescription(RequireDescription),
125-
DescriptionFormat {
126-
#[serde(rename = "descriptionFormat")]
127-
description_format: Option<Regex>,
128-
},
127+
DescriptionFormat(DescriptionFormatConfig),
129128
}
130129

131130
#[derive(Debug, JsonSchema)]
@@ -135,25 +134,35 @@ enum RequireDescription {
135134
AllowWithDescription,
136135
}
137136

138-
impl DirectiveConfig {
139-
fn from_json(value: &serde_json::Value) -> Option<Self> {
140-
match value {
141-
serde_json::Value::Bool(b) => Some(Self::Boolean(*b)),
142-
serde_json::Value::String(s) => {
143-
if s == "allow-with-description" {
144-
Some(Self::RequireDescription)
145-
} else {
146-
None
147-
}
148-
}
149-
serde_json::Value::Object(o) => {
150-
let re = o
151-
.get("descriptionFormat")
152-
.and_then(serde_json::Value::as_str)
153-
.and_then(|pattern| Regex::new(pattern).ok());
154-
Some(Self::DescriptionFormat(re))
137+
#[derive(Debug, Deserialize, JsonSchema)]
138+
#[serde(deny_unknown_fields)]
139+
struct DescriptionFormatConfig {
140+
#[serde(
141+
default,
142+
rename = "descriptionFormat",
143+
deserialize_with = "deserialize_required_regex_option"
144+
)]
145+
description_format: Option<Regex>,
146+
}
147+
148+
impl<'de> Deserialize<'de> for DirectiveConfig {
149+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150+
where
151+
D: serde::Deserializer<'de>,
152+
{
153+
use serde::de::Error;
154+
155+
match serde_json::Value::deserialize(deserializer)? {
156+
serde_json::Value::Bool(value) => Ok(Self::Boolean(value)),
157+
serde_json::Value::String(value) if value == "allow-with-description" => {
158+
Ok(Self::RequireDescription)
155159
}
156-
_ => None,
160+
value @ serde_json::Value::Object(_) => DescriptionFormatConfig::deserialize(value)
161+
.map(|config| Self::DescriptionFormat(config.description_format))
162+
.map_err(D::Error::custom),
163+
_ => Err(D::Error::custom(
164+
"expected a boolean, `allow-with-description`, or a descriptionFormat object",
165+
)),
157166
}
158167
}
159168
}
@@ -188,30 +197,7 @@ declare_oxc_lint!(
188197

189198
impl Rule for BanTsComment {
190199
fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
191-
let config = value.get(0).unwrap_or_default();
192-
193-
Ok(Self(Box::new(BanTsCommentConfig {
194-
ts_expect_error: config
195-
.get("ts-expect-error")
196-
.and_then(DirectiveConfig::from_json)
197-
.unwrap_or(DirectiveConfig::RequireDescription),
198-
ts_ignore: config
199-
.get("ts-ignore")
200-
.and_then(DirectiveConfig::from_json)
201-
.unwrap_or(DirectiveConfig::Boolean(true)),
202-
ts_nocheck: config
203-
.get("ts-nocheck")
204-
.and_then(DirectiveConfig::from_json)
205-
.unwrap_or(DirectiveConfig::Boolean(true)),
206-
ts_check: config
207-
.get("ts-check")
208-
.and_then(DirectiveConfig::from_json)
209-
.unwrap_or(DirectiveConfig::Boolean(false)),
210-
minimum_description_length: config
211-
.get("minimumDescriptionLength")
212-
.and_then(serde_json::Value::as_u64)
213-
.unwrap_or(3),
214-
})))
200+
serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
215201
}
216202

217203
fn run_once(&self, ctx: &LintContext) {
@@ -1047,3 +1033,15 @@ if (false) {
10471033
.expect_fix(fix)
10481034
.test_and_snapshot();
10491035
}
1036+
1037+
#[test]
1038+
fn invalid_description_format_is_rejected() {
1039+
let result = BanTsComment::from_configuration(serde_json::json!([{
1040+
"ts-expect-error": {
1041+
"descriptionFormat": "^(unclosed",
1042+
},
1043+
}]));
1044+
1045+
let error = result.expect_err("invalid descriptionFormat should be rejected");
1046+
assert!(error.to_string().contains("regex parse error"), "unexpected error: {error}");
1047+
}

‎npm/oxlint/configuration_schema.json‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,15 @@
20492049
},
20502050
"additionalProperties": false
20512051
},
2052+
"DescriptionFormatConfig": {
2053+
"type": "object",
2054+
"properties": {
2055+
"descriptionFormat": {
2056+
"type": "string"
2057+
}
2058+
},
2059+
"additionalProperties": false
2060+
},
20522061
"Destructure": {
20532062
"oneOf": [
20542063
{
@@ -2106,13 +2115,7 @@
21062115
"$ref": "#/definitions/RequireDescription"
21072116
},
21082117
{
2109-
"type": "object",
2110-
"properties": {
2111-
"descriptionFormat": {
2112-
"type": "string"
2113-
}
2114-
},
2115-
"additionalProperties": false
2118+
"$ref": "#/definitions/DescriptionFormatConfig"
21162119
}
21172120
]
21182121
},

0 commit comments

Comments
 (0)