Skip to content

feat(linter): implement react/forbid-component-props rule#20005

Open
baevm wants to merge 6 commits intooxc-project:mainfrom
baevm:react/forbid-component-props
Open

feat(linter): implement react/forbid-component-props rule#20005
baevm wants to merge 6 commits intooxc-project:mainfrom
baevm:react/forbid-component-props

Conversation

@baevm
Copy link
Contributor

@baevm baevm commented Mar 4, 2026

this PR adds react/forbid-component-props rule

issue #1022

@baevm baevm requested a review from camc314 as a code owner March 4, 2026 17:30
Copilot AI review requested due to automatic review settings March 4, 2026 17:30
@github-actions github-actions bot added A-linter Area - Linter C-enhancement Category - New feature or request labels Mar 4, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-props with support for exact and glob-pattern prop names and per-component allow/disallow lists.
  • Introduce get_jsx_element_name utility 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",
),
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
),
),
("const App = () => <App.Foo.Bar.Baz />", "App.Foo.Bar.Baz"),

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added test and fixed

Comment on lines +169 to +173
let is_pattern = obj.prop_name_pattern.is_some();
let key = obj
.prop_name
.or(obj.prop_name_pattern)
.unwrap_or_else(|| CompactStr::new(""));
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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`"
);
}
};

Copilot uses AI. Check for mistakes.
/// ```
ForbidComponentProps,
react,
style,
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
style,
restriction,

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 444 to 446
}
JSXElementName::MemberExpression(jsx_mem_expr) => get_jsx_mem_expr_name(jsx_mem_expr),
JSXElementName::ThisExpression(_) => Cow::Borrowed("this"),
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow 😮 , fixed

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 4, 2026

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 52 skipped benchmarks1


Comparing baevm:react/forbid-component-props (c634519) with main (ee26215)2

Open in CodSpeed

Footnotes

  1. 52 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (92f4490) during the generation of this report, so ee26215 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Comment on lines +86 to +112
#[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>,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[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>,
}

Copy link
Contributor Author

@baevm baevm Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. generated config documentation after #20018:

image

@baevm baevm force-pushed the react/forbid-component-props branch from fceecf7 to 9d87800 Compare March 6, 2026 06:23
@baevm baevm force-pushed the react/forbid-component-props branch from 9d87800 to d7f8a66 Compare March 6, 2026 06:24
@baevm baevm force-pushed the react/forbid-component-props branch from 1daefad to c634519 Compare March 6, 2026 06:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants