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`
This commit is contained in:
Mikhail Baev authored and GitHub committed 2026-08-05 20:21:36 +01:00
1 parent 6be314f226
commit 9573937df3
3 files changed
+61 -62

No files matched your search

@@ -250,12 +250,7 @@ export type ImportKind = "none" | "all" | "multiple" | "single";
export type SortOrder = "desc" | "asc";
export type ArrayOption = "array" | "array-simple" | "generic";
export type ReadonlyArrayOption = "array" | "array-simple" | "generic";
export type DirectiveConfigSchema =
| boolean
| RequireDescription
| {
descriptionFormat?: string;
};
export type DirectiveConfigSchema = boolean | RequireDescription | DescriptionFormatConfig;
export type RequireDescription = "allow-with-description";
export type ClassLiteralPropertyStyleOption = "fields" | "getters";
export type PreferGenericType = "constructor" | "type-annotation";
@@ -5277,6 +5272,9 @@ export interface BanTsCommentConfig {
*/
"ts-nocheck"?: DirectiveConfigSchema;
}
export interface DescriptionFormatConfig {
descriptionFormat?: string;
}
export interface ConsistentReturnConfig {
/**
* Treat explicit `return undefined` as equivalent to an unspecified return.
@@ -4,10 +4,12 @@ use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{
context::{ContextHost, LintContext},
rule::Rule,
rule::{DefaultRuleConfig, Rule},
utils::deserialize_required_regex_option,
};
fn comment(ts_comment_name: &str, span: Span) -> OxcDiagnostic {
@@ -45,10 +47,10 @@ fn comment_description_not_match_pattern(
.with_label(span)
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Deserialize)]
pub struct BanTsComment(Box<BanTsCommentConfig>);
#[derive(Debug, Clone, JsonSchema)]
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
/// This rule allows you to specify how different TypeScript directive comments
/// should be handled.
@@ -122,10 +124,7 @@ pub enum DirectiveConfig {
enum DirectiveConfigSchema {
Boolean(bool),
RequireDescription(RequireDescription),
DescriptionFormat {
#[serde(rename = "descriptionFormat")]
description_format: Option<Regex>,
},
DescriptionFormat(DescriptionFormatConfig),
}
#[derive(Debug, JsonSchema)]
@@ -135,25 +134,35 @@ enum RequireDescription {
AllowWithDescription,
}
impl DirectiveConfig {
fn from_json(value: &serde_json::Value) -> Option<Self> {
match value {
serde_json::Value::Bool(b) => Some(Self::Boolean(*b)),
serde_json::Value::String(s) => {
if s == "allow-with-description" {
Some(Self::RequireDescription)
} else {
None
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct DescriptionFormatConfig {
#[serde(
default,
rename = "descriptionFormat",
deserialize_with = "deserialize_required_regex_option"
)]
description_format: Option<Regex>,
}
impl<'de> Deserialize<'de> for DirectiveConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
match serde_json::Value::deserialize(deserializer)? {
serde_json::Value::Bool(value) => Ok(Self::Boolean(value)),
serde_json::Value::String(value) if value == "allow-with-description" => {
Ok(Self::RequireDescription)
}
serde_json::Value::Object(o) => {
let re = o
.get("descriptionFormat")
.and_then(serde_json::Value::as_str)
.and_then(|pattern| Regex::new(pattern).ok());
Some(Self::DescriptionFormat(re))
}
_ => None,
value @ serde_json::Value::Object(_) => DescriptionFormatConfig::deserialize(value)
.map(|config| Self::DescriptionFormat(config.description_format))
.map_err(D::Error::custom),
_ => Err(D::Error::custom(
"expected a boolean, `allow-with-description`, or a descriptionFormat object",
)),
}
}
}
@@ -188,30 +197,7 @@ declare_oxc_lint!(
impl Rule for BanTsComment {
fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
let config = value.get(0).unwrap_or_default();
Ok(Self(Box::new(BanTsCommentConfig {
ts_expect_error: config
.get("ts-expect-error")
.and_then(DirectiveConfig::from_json)
.unwrap_or(DirectiveConfig::RequireDescription),
ts_ignore: config
.get("ts-ignore")
.and_then(DirectiveConfig::from_json)
.unwrap_or(DirectiveConfig::Boolean(true)),
ts_nocheck: config
.get("ts-nocheck")
.and_then(DirectiveConfig::from_json)
.unwrap_or(DirectiveConfig::Boolean(true)),
ts_check: config
.get("ts-check")
.and_then(DirectiveConfig::from_json)
.unwrap_or(DirectiveConfig::Boolean(false)),
minimum_description_length: config
.get("minimumDescriptionLength")
.and_then(serde_json::Value::as_u64)
.unwrap_or(3),
})))
serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
}
fn run_once(&self, ctx: &LintContext) {
@@ -1047,3 +1033,15 @@ if (false) {
.expect_fix(fix)
.test_and_snapshot();
}
#[test]
fn invalid_description_format_is_rejected() {
let result = BanTsComment::from_configuration(serde_json::json!([{
"ts-expect-error": {
"descriptionFormat": "^(unclosed",
},
}]));
let error = result.expect_err("invalid descriptionFormat should be rejected");
assert!(error.to_string().contains("regex parse error"), "unexpected error: {error}");
}
+10 -7
View File
@@ -2049,6 +2049,15 @@
},
"additionalProperties": false
},
"DescriptionFormatConfig": {
"type": "object",
"properties": {
"descriptionFormat": {
"type": "string"
}
},
"additionalProperties": false
},
"Destructure": {
"oneOf": [
{
@@ -2106,13 +2115,7 @@
"$ref": "#/definitions/RequireDescription"
},
{
"type": "object",
"properties": {
"descriptionFormat": {
"type": "string"
}
},
"additionalProperties": false
"$ref": "#/definitions/DescriptionFormatConfig"
}
]
},