feat(linter): implement react/forbid-component-props rule#20005
feat(linter): implement react/forbid-component-props rule#20005baevm wants to merge 6 commits intooxc-project:mainfrom
react/forbid-component-props rule#20005Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds the react/forbid-component-props lint rule to oxc_linter, enabling configurable restrictions on passing certain props (defaulting to className and style) to React components while ignoring DOM nodes.
Changes:
- Implement
react/forbid-component-propswith support for exact and glob-pattern prop names and per-component allow/disallow lists. - Introduce
get_jsx_element_nameutility and corresponding tests to stringify JSX element names consistently (identifiers, member expressions, namespaced names,this.*). - Wire the new rule into the rule registry, runner, and add snapshot-based tests.
Reviewed changes
Copilot reviewed 4 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/oxc_linter/src/utils/react.rs | Adds get_jsx_element_name and tests; refactors get_element_type to reuse the new helper. |
| crates/oxc_linter/src/rules/react/forbid_component_props.rs | New rule implementation + config schema + tests. |
| crates/oxc_linter/src/snapshots/react_forbid_component_props.snap | Snapshot output for the new rule’s diagnostics. |
| crates/oxc_linter/src/rules.rs | Registers the new React rule module. |
| crates/oxc_linter/src/generated/rules_enum.rs | Adds the rule to the generated rule enum + metadata plumbing. |
| crates/oxc_linter/src/generated/rule_runner_impls.rs | Adds runner implementation for the new rule’s AST node type. |
| ( | ||
| "class App extends React.Component { render() { return <this.Modal />; } }", | ||
| "this.Modal", | ||
| ), |
There was a problem hiding this comment.
test_get_jsx_element_name doesn’t cover nested member expressions (e.g. <App.Foo.Bar.Baz />). Adding a multi-segment case would catch regressions in member-expression name formatting (and would currently fail due to the duplicated-segment issue).
| ), | |
| ), | |
| ("const App = () => <App.Foo.Bar.Baz />", "App.Foo.Bar.Baz"), |
| let is_pattern = obj.prop_name_pattern.is_some(); | ||
| let key = obj | ||
| .prop_name | ||
| .or(obj.prop_name_pattern) | ||
| .unwrap_or_else(|| CompactStr::new("")); |
There was a problem hiding this comment.
When a ForbidItem::Object omits both propName and propNamePattern, the code silently falls back to an empty-string key, effectively ignoring that config entry. Also, if both are provided, is_pattern becomes true but key prefers propName, which is inconsistent. This should be treated as an invalid configuration (require exactly one of propName / propNamePattern) and surfaced as a configuration error rather than being silently accepted.
| let is_pattern = obj.prop_name_pattern.is_some(); | |
| let key = obj | |
| .prop_name | |
| .or(obj.prop_name_pattern) | |
| .unwrap_or_else(|| CompactStr::new("")); | |
| let has_name = obj.prop_name.is_some(); | |
| let has_pattern = obj.prop_name_pattern.is_some(); | |
| let (key, is_pattern) = match (has_name, has_pattern) { | |
| (true, false) => ( | |
| obj.prop_name | |
| .expect("ForbidComponentProps: `prop_name` unexpectedly missing"), | |
| false, | |
| ), | |
| (false, true) => ( | |
| obj.prop_name_pattern | |
| .expect("ForbidComponentProps: `prop_name_pattern` unexpectedly missing"), | |
| true, | |
| ), | |
| (true, true) => { | |
| panic!( | |
| "ForbidComponentProps: invalid configuration: exactly one of `propName` or `propNamePattern` must be provided" | |
| ); | |
| } | |
| (false, false) => { | |
| panic!( | |
| "ForbidComponentProps: invalid configuration: missing both `propName` and `propNamePattern`" | |
| ); | |
| } | |
| }; |
| /// ``` | ||
| ForbidComponentProps, | ||
| react, | ||
| style, |
There was a problem hiding this comment.
The lint category passed to declare_oxc_lint! is style, but similar React “forbid” rules are categorized as restriction (e.g. react/forbid-dom-props in crates/oxc_linter/src/rules/react/forbid_dom_props.rs:140-144 and react/forbid-elements in crates/oxc_linter/src/rules/react/forbid_elements.rs:117-121). To keep rule categorization consistent (and avoid surprising grouping/filtering behavior), this should likely be restriction as well.
| style, | |
| restriction, |
| } | ||
| JSXElementName::MemberExpression(jsx_mem_expr) => get_jsx_mem_expr_name(jsx_mem_expr), | ||
| JSXElementName::ThisExpression(_) => Cow::Borrowed("this"), |
There was a problem hiding this comment.
get_jsx_element_name delegates member-expression formatting to get_jsx_mem_expr_name, but get_jsx_mem_expr_name currently duplicates the last segment when the object is itself a MemberExpression (e.g. <A.B.C> would become A.B.B.C). This will break matching against configured component names/patterns for nested member expressions and can affect other callers like get_element_type. Fix the recursion to avoid appending mem_expr.property twice for nested member expressions.
Merging this PR will not alter performance
Comparing Footnotes
|
| #[derive(Debug, Clone, Deserialize, JsonSchema)] | ||
| #[serde(rename_all = "camelCase", deny_unknown_fields)] | ||
| pub struct ForbidItemObject { | ||
| /// Exact prop name to forbid. | ||
| #[serde(default)] | ||
| prop_name: Option<CompactStr>, | ||
| /// Glob pattern to match prop names against. | ||
| #[serde(default)] | ||
| prop_name_pattern: Option<CompactStr>, | ||
| /// Component names for which this prop is **allowed** (all others are | ||
| /// forbidden). | ||
| #[serde(default)] | ||
| allowed_for: Vec<CompactStr>, | ||
| /// Glob patterns for component names where the prop is **allowed**. | ||
| #[serde(default)] | ||
| allowed_for_patterns: Vec<CompactStr>, | ||
| /// Component names for which this prop is **disallowed** (all others are | ||
| /// allowed). | ||
| #[serde(default)] | ||
| disallowed_for: Vec<CompactStr>, | ||
| /// Glob patterns for component names where the prop is **disallowed**. | ||
| #[serde(default)] | ||
| disallowed_for_patterns: Vec<CompactStr>, | ||
| /// Custom message to display. | ||
| #[serde(default)] | ||
| message: Option<String>, | ||
| } |
There was a problem hiding this comment.
| #[derive(Debug, Clone, Deserialize, JsonSchema)] | |
| #[serde(rename_all = "camelCase", deny_unknown_fields)] | |
| pub struct ForbidItemObject { | |
| /// Exact prop name to forbid. | |
| #[serde(default)] | |
| prop_name: Option<CompactStr>, | |
| /// Glob pattern to match prop names against. | |
| #[serde(default)] | |
| prop_name_pattern: Option<CompactStr>, | |
| /// Component names for which this prop is **allowed** (all others are | |
| /// forbidden). | |
| #[serde(default)] | |
| allowed_for: Vec<CompactStr>, | |
| /// Glob patterns for component names where the prop is **allowed**. | |
| #[serde(default)] | |
| allowed_for_patterns: Vec<CompactStr>, | |
| /// Component names for which this prop is **disallowed** (all others are | |
| /// allowed). | |
| #[serde(default)] | |
| disallowed_for: Vec<CompactStr>, | |
| /// Glob patterns for component names where the prop is **disallowed**. | |
| #[serde(default)] | |
| disallowed_for_patterns: Vec<CompactStr>, | |
| /// Custom message to display. | |
| #[serde(default)] | |
| message: Option<String>, | |
| } | |
| #[derive(Debug, Clone, Deserialize, JsonSchema)] | |
| #[serde(rename_all = "camelCase", default, deny_unknown_fields)] | |
| pub struct ForbidItemObject { | |
| /// Exact prop name to forbid. | |
| prop_name: Option<CompactStr>, | |
| /// Glob pattern to match prop names against. | |
| prop_name_pattern: Option<CompactStr>, | |
| /// Component names for which this prop is **allowed** (all others are | |
| /// forbidden). | |
| allowed_for: Vec<CompactStr>, | |
| /// Glob patterns for component names where the prop is **allowed**. | |
| allowed_for_patterns: Vec<CompactStr>, | |
| /// Component names for which this prop is **disallowed** (all others are | |
| /// allowed). | |
| disallowed_for: Vec<CompactStr>, | |
| /// Glob patterns for component names where the prop is **disallowed**. | |
| disallowed_for_patterns: Vec<CompactStr>, | |
| /// Custom message to display. | |
| message: Option<String>, | |
| } |
There was a problem hiding this comment.
fixed. generated config documentation after #20018:
fceecf7 to
9d87800
Compare
9d87800 to
d7f8a66
Compare
1daefad to
c634519
Compare
this PR adds
react/forbid-component-propsruleissue #1022