feat(linter): Implement react/no-multi-comp rule.#18794
feat(linter): Implement react/no-multi-comp rule.#18794graphite-app[bot] merged 1 commit intomainfrom
react/no-multi-comp rule.#18794Conversation
CodSpeed Performance ReportMerging this PR will not alter performanceComparing Summary
Footnotes
|
|
Here's a fun thing I discovered about how eslint-plugin-react determines whether something is a React component, after trying to figure out why oxlint wasn't detecting a specific file in the mastodon repo as violating the rule. // This is a React component (according to eslint-plugin-react).
const TabsBarPortal = () => {
return <div />;
};
// This is a React component, apparently.
export default class ColumnsArea extends ImmutablePureComponent {
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
};
}
// This is NOT a React component (according to eslint-plugin-react).
export default class ColumnsArea extends ImmutablePureComponent {
render() {
const { isModalOpen } = this.props;
return (
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}></div>
);
}
}The detection in the original plugin is borked and catches this, inside the class, as a React component: renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
};Amazing. |
There was a problem hiding this comment.
Pull request overview
This PR implements the react/no-multi-comp ESLint rule, which prevents developers from defining multiple React components in a single file. The implementation uses a visitor pattern to detect various forms of React components including:
- Function components (named functions and arrow functions)
- Class components (ES6 and ES5 createReactClass)
- HOC-wrapped components (memo, forwardRef)
- Components assigned to exports or object properties
Changes:
- Added new rule
no_multi_compwith support forignoreStatelessconfiguration option - Implemented comprehensive component detection using AST visitor pattern
- Added depth tracking to avoid counting nested components
- Includes extensive test coverage matching 84/85 cases from the original ESLint rule
Reviewed changes
Copilot reviewed 3 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
crates/oxc_linter/src/rules/react/no_multi_comp.rs |
Main rule implementation with visitor pattern and component detection logic |
crates/oxc_linter/src/snapshots/react_no_multi_comp.snap |
Test snapshots showing expected diagnostic output |
crates/oxc_linter/src/rules.rs |
Module registration for the new rule |
crates/oxc_linter/src/generated/rules_enum.rs |
Generated enum registration code |
crates/oxc_linter/src/generated/rule_runner_impls.rs |
Generated rule runner implementation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 5 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Merge activity
|
Implements the no-multi-comp rule from eslint-plugin-react. - [Original code](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/lib/rules/no-multi-comp.js) - [Original tests](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/tests/lib/rules/no-multi-comp.js) - [Original docs](https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-multi-comp.md) This rule prevents users from defining more than one React component in a single file, for example: ```jsx export function Foo({ name }) { return <div>Hello {name}</div>; } export function Bar({ name }) { return <div>Hello again {name}</div>; } ``` This can be undesirable because we generally want to have one component per file, never more than that. AI Disclosure: Implemented with help from Claude Code + Opus 4.5. Reviewed by me and guided by me, but I will admit I'm uncertain on the performance of the rule as-written. I did testing with the rule on the mastodon codebase, and got 84 / 85 violations matching the original ESLint rule. The only difference is described in a comment on this PR, and is an invalid case IMO. I _had_ noticed the initial implementation going through every AST node in the given file and doing potentially-expensive checks on them, so I had it refactor the code to use a visitor pattern instead, which should be better for the rule's performance. But I assume there may be other performance issues that need to be addressed as well. A lot of the work the rule has to do right now is in service of `createReactClass` and class components support, which feels stupid but is technically necessary for compatibility with the original rule. I think removing all support for `createReactClass` may be worth considering in the future. I can't really think of a good way to optimize that away, other than adding a config option like `check_for_create_react_class` on the rule that defaults to false?
13421a4 to
17ca42d
Compare
# Oxlint ### 💥 BREAKING CHANGES - b34a155 linter/plugins: [**BREAKING**] `RuleTester` set `context.filename` to absolute path (#18702) (overlookmotel) ### 🚀 Features - 1753209 linter/vscode: Run extension when JS configs are detected (#18832) (camc314) - c962dd2 linter/lsp: Implement support for oxlint.config.ts (#18826) (camc314) - da32203 linter: Auto generate oxlint.config.ts types (#18597) (camc314) - 19b4df7 oxlint: Introduce `defineConfig` helper (#18596) (camc314) - ea97231 linter: Implement `oxlint.config.ts` support (#17563) (camc314) - 17ca42d linter: Implement `react/no-multi-comp` rule. (#18794) (connorshea) - 88f30e0 linter/plugins: Move eslint compatible plugin conversion to `eslintCompatPlugin` function (#18791) (overlookmotel) - 2a72794 linter/plugins: `RuleTester` take `cwd` property (#18756) (overlookmotel) - 9f533db linter: Add `find_prev_token_within` method for token search (#18769) (camc314) - 772ea70 linter: Introduce `load_js_configs` napi callback (#18767) (camc314) - e9690c1 linter: Introduce `DiscoveredConfig` in preparation for JS configs (#18674) (camc314) - 558b588 linter/prefer-namespace-keyword: Move to correctness (#18733) (camc314) - 7a5c268 oxlint/lsp: Support `jsPlugins` (#17840) (Sysix) - c07497c linter/prefer-modern-dom-apis: Implement suggestion (#17965) (Mikhail Baev) - 8531bc9 linter: Implement `prefer-const` (#18687) (camchenry) - 8670b18 parser: Error on ambient class accessor implementations (#18592) (camc314) - 6b8a5ae linter: Add `eslint-plugin-import/no-nodejs-modules` rule (#18006) (Mikhail Baev) - 04f400d linter/no-duplicates: Add support for `considerQueryString` option (#18657) (camc314) - 3b7f260 linter/consistent-generic-constructor: Implement fixer (#18616) (camc314) - 794f9e4 linter/prefer-exponentation-operator: Implement suggestion (#18602) (camc314) - 773d916 linter: `eslint/sort_keys` ignore leading and trailing spreads in auto-fix (#18485) (Lonami) - 20d4ede linter: Implement `import/no-relative-parent-imports` rule (#18513) (Valentin Maerten) - 0da45ef vscode: Fallback to globally installed oxlint/oxfmt packages (#18007) (Sysix) ### 🐛 Bug Fixes - a3417b1 linter/plugins: Clear state when reloading workspace (#18837) (overlookmotel) - c879992 linter: Error on arrays passed in as config (#18822) (camc314) - 5c80422 linter/tsdown: Ensure relative path for globals import starts with `./` (#18820) (camc314) - 7419dfb linter: Remove invalid debug assersion, add test (#18819) (camc314) - 0ca6269 ci: Fix the repo path normalization logic for tests on Windows. (#18815) (connorshea) - c7b0a65 linter: Fix config option docs for `react/jsx-boolean-value` rule. (#18811) (connorshea) - cce374e linter/prefer-const: Replace entire declaration over just the `let` kw (#18814) (camc314) - 41f92d1 linter: Error when given config options for a lint rule that has no config options defined. (#18809) (connorshea) - 0867a36 linter/consistent-index-object-style: False positive with mapped + generic types (#18801) (camc314) - 1d34b42 linter: Fix 32 bit build (#18783) (camc314) - 95df577 linter/plugins: Handle error from `destroyWorkspace` (#18763) (overlookmotel) - b3261dc linter: Fix the curly rule config to enforce the shape of the config and emit correct docs (#18743) (connorshea) - d981978 linter/plugins: Use non-blocking mode when calling `destroyWorkspace` (#18762) (overlookmotel) - 3f43d4c linter: Accept bools as valid values for `fixable` (#18772) (camc314) - 005910a linter/plugins: Support plugins outside of workspace (#18755) (overlookmotel) - fd92711 vscode: Use `fsPath` for workspace mapping (#18728) (Sysix) - 358b2c1 linter/consistent-generic-constructors: Check bounds when searching for `:` token (#18745) (connorshea) - abd0c28 linter/capitalized-comments: Fix generated rule option docs (#18748) (connorshea) - d90a9f6 linter: Add more tests for `prefer-const`'s fixer and fix its invalid behavior. (#18747) (connorshea) - f82011b oxlint/lsp: Disable JS plugins support in LSP except in tests (#18727) (overlookmotel) - 94505c8 linter/jest: Change `prefer-spy-on` autofix to suggestion (#18152) (Ben Lowery) - 6ec1112 linter: Mark unused disable directive fix as suggestion (#18703) (ddmoney420) - 49609ec linter/no-useless-constructor: Consider argument transformation as used (#18706) (ddmoney420) - 40218de linter: Fix behavior of jsx-a11y/no-static-element-interactions rule. (#17817) (connorshea) - db9751d linter/no-html-link-for-pages: Handle `target=_blank` correctly (#18693) (camc314) - e440b78 linter/plugins: Pass all args to CFG event handlers when 2 rules use same handler (#18683) (overlookmotel) - b393430 linter/curly: Fix multi-or-nest and consistent conflict (#18660) (camc314) - 2e1fbc2 linter/plugins: Implement `context.parserPath` (#18644) (overlookmotel) - 34951ed linter/plugins: `filename` option takes precedence over `parserOptions.lang` in `RuleTester` (#18643) (overlookmotel) - 28df160 linter/plugins: Allow line number passed to `report` to be 0 (#18642) (overlookmotel) - 14fabec vscode: Use built-in `getWorkspaceFolder` for detecting the right workspace of a given uri (#18583) (Sysix) - 0ff4cea oxlint/cli: Report error when nested config could not be parsed (#18504) (Sysix) ### ⚡ Performance - 9862224 linter/plugins: Reduce cost of workspaces (#18758) (overlookmotel) - 6bc0bde linter: Remove string allocation (#18725) (overlookmotel) - 3a6b41e linter/plugins: Replace ESLint Traverser with lightweight traverseNode (#18529) (Rintaro Itokawa) ### 📚 Documentation - dd1a653 linter: Fix doc comment for ignoreStateless config option. (#18808) (connorshea) - 5909085 linter/plugins: Add doc comments (#18753) (overlookmotel) - ffe53a3 linter: Update lint function docs (#18766) (camc314) - b82faec linter: Glob for any css module for no-unassigned-import (#18713) (Ben Stickley) - cd86347 linter: Mark some react rules as unsupported, misc docs improvements (#18617) (connorshea) - 23401d8 linter: Update fixes and suggestions status for tsgolint rules (#18619) (camchenry) # Oxfmt ### 🚀 Features - ee30de9 oxfmt: Add config migration from biome (#18638) (Luca Fischer) ### 🐛 Bug Fixes - e754b18 oxfmt/migrate-prettier: Set `experimentalSortPackagejson: false` by default (#18831) (leaysgur) - a83c266 formatter: Keep decorated function pattern hugged when params break (#18830) (Dunqing) - 0c8efe1 formatter: Quote numeric property keys with `quoteProps: consistent` (#18803) (Dunqing) - 9c14c3e formatter: Ignore comment does not work for sequence expressions in arrow function body (#18799) (Dunqing) - 54984ae formatter: Handle leading comments in arrow function sequence expressions (#18798) (Dunqing) - 61bb2b5 formatter: Correctly expand JSX returned from arrow callbacks in JSX expression containers (#18797) (Dunqing) - 34ee194 formatter: Tailwindcss sorting doesn't work for object property keys (#18773) (Dunqing) - 48f1e35 oxfmt: Prevent ThreadsafeFunction crash on Node.js exit (#18723) (Boshen) - e96adca formatter: Follow Prettier's approach for for-in initializer parentheses (#18695) (Dunqing) - 1215a6f formatter: Preserve quote for class property key in TypeScript (#18692) (Dunqing) - 059acae formatter: Incorrect comments placement for union type in `TSTypeIntersection` (#18690) (Dunqing) - c3d05c1 formatter,oxfmt: Handle CRLF with embedded formatting (#18686) (leaysgur) - 7cb3085 formatter: Preserve comments on rest elements (#18649) (Dunqing) - 21984dd formatter: Preserve type cast comments on rest parameters (#18648) (Dunqing) - 2f70254 formatter: Don't add extra semicolon on suppressed class properties (#18631) (Dunqing) - ac1ff4e oxfmt: Use `empty_line` IR for empty xxx-in-js line (#18623) (leaysgur) - 8f76900 oxfmt: Dedent xxx-in-js templates before calling prettier (#18622) (leaysgur) - 6b726ef oxfmt: Trim whitespace only xxx-in-js templates (#18621) (leaysgur) Co-authored-by: camc314 <[email protected]>
Implements the no-multi-comp rule from eslint-plugin-react.
This rule prevents users from defining more than one React component in a single file, for example:
This can be undesirable because we generally want to have one component per file, never more than that.
AI Disclosure: Implemented with help from Claude Code + Opus 4.5. Reviewed by me and guided by me, but I will admit I'm uncertain on the performance of the rule as-written. I did testing with the rule on the mastodon codebase, and got 84 / 85 violations matching the original ESLint rule. The only difference is described in a comment on this PR, and is an invalid case IMO.
I had noticed the initial implementation going through every AST node in the given file and doing potentially-expensive checks on them, so I had it refactor the code to use a visitor pattern instead, which should be better for the rule's performance. But I assume there may be other performance issues that need to be addressed as well.
A lot of the work the rule has to do right now is in service of
createReactClassand class components support, which feels stupid but is technically necessary for compatibility with the original rule. I think removing all support forcreateReactClassmay be worth considering in the future. I can't really think of a good way to optimize that away, other than adding a config option likecheck_for_create_react_classon the rule that defaults to false?