feat(linter): add schema for react/jsx-props-no-spreading (#23276)

part of  #22955

also replaced `get_tag_name` in rule with shared react helper `get_jsx_element_name`
This commit is contained in:
Mikhail Baev authored and GitHub committed 2026-06-11 12:10:16 +01:00
1 parent 08d68a5e20
commit 44806094fc
5 files changed
+174 -43

No files matched your search

+25 -1
View File
@@ -176,6 +176,7 @@ export type ForbidItem2 =
};
export type EnforceBooleanAttribute = "always" | "never";
export type FragmentMode = "syntax" | "element";
export type IgnoreEnforceOption = "ignore" | "enforce";
export type NoDidMountSetStateConfig = "allowed" | "disallow-in-func";
export type NoWillUpdateSetStateConfig = "allowed" | "disallow-in-func";
export type RequireFlag = "u" | "v";
@@ -1179,7 +1180,7 @@ export interface DummyRuleMap {
"react/jsx-no-useless-fragment"?: AllowWarnDeny | [AllowWarnDeny] | [AllowWarnDeny, JsxNoUselessFragment];
"react/jsx-pascal-case"?: AllowWarnDeny | [AllowWarnDeny] | [AllowWarnDeny, JsxPascalCaseConfig];
"react/jsx-props-no-spread-multi"?: RuleNoConfig;
"react/jsx-props-no-spreading"?: DummyRule;
"react/jsx-props-no-spreading"?: AllowWarnDeny | [AllowWarnDeny] | [AllowWarnDeny, JsxPropsNoSpreadingConfig];
"react/no-array-index-key"?: RuleNoConfig;
"react/no-children-prop"?: RuleNoConfig;
"react/no-clone-element"?: RuleNoConfig;
@@ -4166,6 +4167,29 @@ export interface JsxPascalCaseConfig {
*/
ignore?: string[];
}
export interface JsxPropsNoSpreadingConfig {
/**
* `custom` set to `ignore` will ignore all custom jsx tags like `App`, `MyCustomComponent` etc. Default is set to `enforce`.
*/
custom?: IgnoreEnforceOption;
/**
* Exceptions flip the enforcement behavior for specific components.
* For example:
* - If `html` is set to `ignore`, an exception for `div` will enforce the rule on `<div>` elements.
* - If `custom` is set to `enforce`, an exception for `Foo` will ignore the rule on `<Foo>` components.
*
* This allows you to override the general setting for individual components.
*/
exceptions?: string[];
/**
* `explicitSpread` set to `ignore` will ignore spread operators that are explicitly listing all object properties within that spread. Default is set to `enforce`.
*/
explicitSpread?: IgnoreEnforceOption;
/**
* `html` set to `ignore` will ignore all html jsx tags like `div`, `img` etc. Default is set to `enforce`.
*/
html?: IgnoreEnforceOption;
}
export interface NoMultiCompConfig {
/**
* When `true`, the rule will ignore stateless components and will allow you to have multiple
@@ -1,10 +1,7 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use oxc_ast::{
AstKind,
ast::{Expression, JSXElementName, JSXMemberExpression, JSXMemberExpressionObject},
};
use oxc_ast::{AstKind, ast::Expression};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
@@ -14,7 +11,7 @@ use crate::{
AstNode,
context::LintContext,
rule::{DefaultRuleConfig, Rule},
utils::is_react_component_name,
utils::{get_jsx_element_name, is_react_component_name},
};
fn jsx_props_no_spreading_diagnostic(span: Span) -> OxcDiagnostic {
@@ -107,14 +104,15 @@ impl Rule for JsxPropsNoSpreading {
return;
};
let tag_name = get_tag_name(&jsx_opening_element.name);
let tag_name = get_jsx_element_name(&jsx_opening_element.name);
let tag_name = tag_name.as_ref();
// Check if first character is lowercase (HTML tag convention)
let is_html_tag = !is_react_component_name(&tag_name);
let is_html_tag = !is_react_component_name(tag_name);
// Custom tags: uppercase first char OR contains '.' (member expressions like Nav.Item)
let is_custom_tag = !is_html_tag || tag_name.contains('.');
let is_exception = is_exception(&tag_name, &self.exceptions);
let is_exception = is_exception(tag_name, &self.exceptions);
let ignore_html_tags = self.html == IgnoreEnforceOption::Ignore;
let ignore_custom_tags = self.custom == IgnoreEnforceOption::Ignore;
@@ -140,36 +138,8 @@ impl Rule for JsxPropsNoSpreading {
}
}
fn is_exception(tag: &CompactStr, exceptions: &[CompactStr]) -> bool {
exceptions.contains(tag)
}
fn get_tag_name(name: &JSXElementName<'_>) -> CompactStr {
match name {
JSXElementName::Identifier(ident) => ident.name.as_str().into(),
JSXElementName::IdentifierReference(ident) => ident.name.as_str().into(),
JSXElementName::MemberExpression(member_expr) => get_member_expr_tag_name(member_expr),
JSXElementName::NamespacedName(namespaced_name) => format!(
"{}:{}",
namespaced_name.namespace.name.as_str(),
namespaced_name.name.name.as_str()
)
.into(),
JSXElementName::ThisExpression(_) => "this".into(),
}
}
/// gets full component name, e.g. "components.Group" in <components.Group />
fn get_member_expr_tag_name(member_expr: &JSXMemberExpression) -> CompactStr {
let object_name = match &member_expr.object {
JSXMemberExpressionObject::IdentifierReference(ident) => ident.name.as_str(),
JSXMemberExpressionObject::ThisExpression(_) => "this",
JSXMemberExpressionObject::MemberExpression(next_expr) => {
&get_member_expr_tag_name(next_expr)
}
};
format!("{}.{}", object_name, member_expr.property.name.as_str()).into()
fn is_exception(tag: &str, exceptions: &[CompactStr]) -> bool {
exceptions.iter().any(|exception| exception.as_str() == tag)
}
#[test]
+1 -2
View File
@@ -13,7 +13,7 @@ use crate::rules::RuleEnum;
/// These should return the DummyRule struct/schema instead of the actual rule struct/schema,
/// until we verify that the rule's schema is valid and can be generated by schemars.
#[cfg(feature = "ruledocs")]
const NO_VERIFIED_VALID_SCHEMA: [&str; 59] = [
const NO_VERIFIED_VALID_SCHEMA: [&str; 58] = [
"eslint/func-name-matching",
"eslint/no-restricted-globals",
"eslint/no-restricted-imports",
@@ -43,7 +43,6 @@ const NO_VERIFIED_VALID_SCHEMA: [&str; 59] = [
"react/jsx-handler-names",
"react/jsx-no-script-url",
"react/jsx-no-target-blank",
"react/jsx-props-no-spreading",
"react/no-did-update-set-state",
"react/no-object-type-as-default-prop",
"react/no-unstable-nested-components",
+70 -1
View File
@@ -5388,7 +5388,24 @@
"$ref": "#/definitions/RuleNoConfig"
},
"react/jsx-props-no-spreading": {
"$ref": "#/definitions/DummyRule"
"anyOf": [
{
"$ref": "#/definitions/AllowWarnDeny"
},
{
"type": "array",
"items": [
{
"$ref": "#/definitions/AllowWarnDeny"
},
{
"$ref": "#/definitions/JsxPropsNoSpreadingConfig"
}
],
"maxItems": 2,
"minItems": 1
}
]
},
"react/no-array-index-key": {
"$ref": "#/definitions/RuleNoConfig"
@@ -9398,6 +9415,13 @@
}
]
},
"IgnoreEnforceOption": {
"type": "string",
"enum": [
"ignore",
"enforce"
]
},
"IgnorePattern_for_String": {
"anyOf": [
{
@@ -9788,6 +9812,51 @@
},
"additionalProperties": false
},
"JsxPropsNoSpreadingConfig": {
"type": "object",
"properties": {
"custom": {
"description": "`custom` set to `ignore` will ignore all custom jsx tags like `App`, `MyCustomComponent` etc. Default is set to `enforce`.",
"default": "enforce",
"allOf": [
{
"$ref": "#/definitions/IgnoreEnforceOption"
}
],
"markdownDescription": "`custom` set to `ignore` will ignore all custom jsx tags like `App`, `MyCustomComponent` etc. Default is set to `enforce`."
},
"exceptions": {
"description": "Exceptions flip the enforcement behavior for specific components.\nFor example:\n- If `html` is set to `ignore`, an exception for `div` will enforce the rule on `<div>` elements.\n- If `custom` is set to `enforce`, an exception for `Foo` will ignore the rule on `<Foo>` components.\n\nThis allows you to override the general setting for individual components.",
"default": [],
"type": "array",
"items": {
"type": "string"
},
"markdownDescription": "Exceptions flip the enforcement behavior for specific components.\nFor example:\n- If `html` is set to `ignore`, an exception for `div` will enforce the rule on `<div>` elements.\n- If `custom` is set to `enforce`, an exception for `Foo` will ignore the rule on `<Foo>` components.\n\nThis allows you to override the general setting for individual components."
},
"explicitSpread": {
"description": "`explicitSpread` set to `ignore` will ignore spread operators that are explicitly listing all object properties within that spread. Default is set to `enforce`.",
"default": "enforce",
"allOf": [
{
"$ref": "#/definitions/IgnoreEnforceOption"
}
],
"markdownDescription": "`explicitSpread` set to `ignore` will ignore spread operators that are explicitly listing all object properties within that spread. Default is set to `enforce`."
},
"html": {
"description": "`html` set to `ignore` will ignore all html jsx tags like `div`, `img` etc. Default is set to `enforce`.",
"default": "enforce",
"allOf": [
{
"$ref": "#/definitions/IgnoreEnforceOption"
}
],
"markdownDescription": "`html` set to `ignore` will ignore all html jsx tags like `div`, `img` etc. Default is set to `enforce`."
}
},
"additionalProperties": false
},
"LibFrom": {
"type": "string",
"enum": [
@@ -5392,7 +5392,24 @@ expression: json
"$ref": "#/definitions/RuleNoConfig"
},
"react/jsx-props-no-spreading": {
"$ref": "#/definitions/DummyRule"
"anyOf": [
{
"$ref": "#/definitions/AllowWarnDeny"
},
{
"type": "array",
"items": [
{
"$ref": "#/definitions/AllowWarnDeny"
},
{
"$ref": "#/definitions/JsxPropsNoSpreadingConfig"
}
],
"maxItems": 2,
"minItems": 1
}
]
},
"react/no-array-index-key": {
"$ref": "#/definitions/RuleNoConfig"
@@ -9402,6 +9419,13 @@ expression: json
}
]
},
"IgnoreEnforceOption": {
"type": "string",
"enum": [
"ignore",
"enforce"
]
},
"IgnorePattern_for_String": {
"anyOf": [
{
@@ -9792,6 +9816,51 @@ expression: json
},
"additionalProperties": false
},
"JsxPropsNoSpreadingConfig": {
"type": "object",
"properties": {
"custom": {
"description": "`custom` set to `ignore` will ignore all custom jsx tags like `App`, `MyCustomComponent` etc. Default is set to `enforce`.",
"default": "enforce",
"allOf": [
{
"$ref": "#/definitions/IgnoreEnforceOption"
}
],
"markdownDescription": "`custom` set to `ignore` will ignore all custom jsx tags like `App`, `MyCustomComponent` etc. Default is set to `enforce`."
},
"exceptions": {
"description": "Exceptions flip the enforcement behavior for specific components.\nFor example:\n- If `html` is set to `ignore`, an exception for `div` will enforce the rule on `<div>` elements.\n- If `custom` is set to `enforce`, an exception for `Foo` will ignore the rule on `<Foo>` components.\n\nThis allows you to override the general setting for individual components.",
"default": [],
"type": "array",
"items": {
"type": "string"
},
"markdownDescription": "Exceptions flip the enforcement behavior for specific components.\nFor example:\n- If `html` is set to `ignore`, an exception for `div` will enforce the rule on `<div>` elements.\n- If `custom` is set to `enforce`, an exception for `Foo` will ignore the rule on `<Foo>` components.\n\nThis allows you to override the general setting for individual components."
},
"explicitSpread": {
"description": "`explicitSpread` set to `ignore` will ignore spread operators that are explicitly listing all object properties within that spread. Default is set to `enforce`.",
"default": "enforce",
"allOf": [
{
"$ref": "#/definitions/IgnoreEnforceOption"
}
],
"markdownDescription": "`explicitSpread` set to `ignore` will ignore spread operators that are explicitly listing all object properties within that spread. Default is set to `enforce`."
},
"html": {
"description": "`html` set to `ignore` will ignore all html jsx tags like `div`, `img` etc. Default is set to `enforce`.",
"default": "enforce",
"allOf": [
{
"$ref": "#/definitions/IgnoreEnforceOption"
}
],
"markdownDescription": "`html` set to `ignore` will ignore all html jsx tags like `div`, `img` etc. Default is set to `enforce`."
}
},
"additionalProperties": false
},
"LibFrom": {
"type": "string",
"enum": [