diff --git a/.changeset/remove-no-multi-comp-alias.md b/.changeset/remove-no-multi-comp-alias.md new file mode 100644 index 000000000..102b66fd8 --- /dev/null +++ b/.changeset/remove-no-multi-comp-alias.md @@ -0,0 +1,24 @@ +--- +"@react-doctor/core": patch +"oxlint-plugin-react-doctor": patch +--- + +fix: remove react/no-multi-comp alias to enable strict oxlint version + +Removes the alias from `react/no-multi-comp` to `react-doctor/no-multi-comp`. This change allows users who want strict "one component per file" enforcement to access oxlint's `react/no-multi-comp` rule via `.oxlintrc.json`, while preserving the lenient `react-doctor/no-multi-comp` behavior for users who explicitly configure it. + +The react-doctor version is intentionally more permissive, with corpus-informed exemptions for: +- Files with ≤2 components (main + helper) +- Feature modules (1-2 exports + private helpers) +- Barrel files (mostly-exported components) + +Users who previously configured `react/no-multi-comp` and want to continue using the lenient version should update their config to use `react-doctor/no-multi-comp` explicitly. Users who want the strict oxlint behavior can create an `.oxlintrc.json` file: + +```json +{ + "plugins": ["react"], + "rules": {"react/no-multi-comp": "error"} +} +``` + +Closes #1639 diff --git a/packages/core/src/rule-key-aliases.ts b/packages/core/src/rule-key-aliases.ts index abae923dc..22301878c 100644 --- a/packages/core/src/rule-key-aliases.ts +++ b/packages/core/src/rule-key-aliases.ts @@ -111,7 +111,6 @@ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY: Readonly> = { "react/no-direct-mutation-state": "react-doctor/no-direct-mutation-state", "react/no-find-dom-node": "react-doctor/no-find-dom-node", "react/no-is-mounted": "react-doctor/no-is-mounted", - "react/no-multi-comp": "react-doctor/no-multi-comp", "react/no-namespace": "react-doctor/no-namespace", "react/no-react-children": "react-doctor/no-react-children", "react/no-redundant-should-component-update": "react-doctor/no-redundant-should-component-update", diff --git a/packages/core/tests/rule-key-aliases.test.ts b/packages/core/tests/rule-key-aliases.test.ts index 851e7eeca..d0e2571a9 100644 --- a/packages/core/tests/rule-key-aliases.test.ts +++ b/packages/core/tests/rule-key-aliases.test.ts @@ -113,4 +113,22 @@ describe("rule-key-aliases", () => { expect(getEquivalentRuleKeys("toString")).toEqual(["toString"]); }); }); + + describe("removed aliases (issue #1639)", () => { + it("does not alias react/no-multi-comp to react-doctor/no-multi-comp", () => { + expect(isSameRuleKey("react/no-multi-comp", "react-doctor/no-multi-comp")).toBe(false); + }); + + it("treats react/no-multi-comp as a distinct rule from react-doctor/no-multi-comp", () => { + const reactKeys = getEquivalentRuleKeys("react/no-multi-comp"); + expect(reactKeys).toEqual(["react/no-multi-comp"]); + expect(reactKeys).not.toContain("react-doctor/no-multi-comp"); + }); + + it("treats react-doctor/no-multi-comp as its own rule", () => { + const rdKeys = getEquivalentRuleKeys("react-doctor/no-multi-comp"); + expect(rdKeys).toEqual(["react-doctor/no-multi-comp"]); + expect(rdKeys).not.toContain("react/no-multi-comp"); + }); + }); }); diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.regressions.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.regressions.test.ts index c4c9a9e3f..48f88d8ba 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.regressions.test.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.regressions.test.ts @@ -202,6 +202,21 @@ describe("react-builtins/no-multi-comp — regressions", () => { ); }); + // Regression test for issue #1639: A file with 3 components where 1 is + // exported falls under the "feature module" exemption (1-2 exports + private + // helpers). This is intentional — users who want strict "one component per + // file" enforcement should use oxlint's `react/no-multi-comp` via + // `.oxlintrc.json` instead of the lenient react-doctor version. + it("exempts a file with 1 exported component + 2 private helpers (feature module)", () => { + expectPass( + `function Row() { return
  • row
  • ; } + function Header() { return

    header

    ; } + export function List() { + return
    ; + }`, + ); + }); + it("does not trace an overwritten mutable React HoC alias", () => { expectFail( `import { memo } from "react"; diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.ts index 7564dc241..580effd19 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/no-multi-comp.ts @@ -23,6 +23,20 @@ import { REACT_HOC_NAMES, REACT_RUNTIME_MODULE_SOURCES } from "../../constants/r const MESSAGE = "This file declares several components, so each component is harder to find, test, and change."; +// React Doctor's `no-multi-comp` is intentionally more lenient than oxlint's +// `react/no-multi-comp` to accommodate common real-world React patterns: +// +// - Files with ≤2 components (1 main + 1 helper) are exempt +// - Feature modules (1-2 public exports + private helpers) are exempt +// - Barrel files (mostly-exported components) are exempt +// +// For stricter "one component per file" enforcement matching oxlint's behavior, +// create an `.oxlintrc.json` with: +// {"plugins": ["react"], "rules": {"react/no-multi-comp": "error"}} +// +// This rule was corpus-tuned to avoid false positives in design systems, +// feature modules, and icon barrels. See regression tests for examples. + interface NoMultiCompSettings { ignoreStateless?: boolean; }