Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/imported-plain-class-empty-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"oxlint-plugin-react-doctor": patch
"eslint-plugin-react-doctor": patch
"react-doctor": patch
---

Avoid reporting empty object defaults passed only to imported plain classes that read their members without observing the object identity.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Component } from "react";

export default class Child extends Component {
render() {
const { config: { Slot = "span" } = {} } = this.props;
return <Slot />;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// rule: rerender-memo-with-default-value
// weakness: cross-file
// source: synthetic imported plain class member projection
// verdict: pass
// file-path: corpus/regressions/imported-plain-class-member-projection/entry.tsx
import Child from "./child.jsx";

export const App = ({ config = {} }) => <Child config={config} />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "private-imported-plain-class-fixture",
"private": true,
"type": "module",
"dependencies": {
"react": "19.2.0"
}
}
47 changes: 47 additions & 0 deletions packages/oxlint-plugin-react-doctor/src/plugin/constants/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,50 @@ export const REACT_JSX_PROVIDER_MODULE_SOURCES: ReadonlySet<string> = new Set([
"react/jsx-runtime",
"react/jsx-dev-runtime",
]);

export const IMPORTED_CLASS_PASSIVE_VALIDATOR_NAMES: ReadonlySet<string> = new Set([
"any",
"array",
"func",
"object",
]);

export const IMPORTED_CLASS_REFUSED_MEMBER_NAMES: ReadonlySet<string> = new Set([
...REACT_PROVIDER_MUTATION_CAPABILITY_NAMES,
"constructor",
"defineProperty",
"defineProperties",
"setPrototypeOf",
"__lookupGetter__",
"__lookupSetter__",
]);

export const IMPORTED_CLASS_REFUSED_MODULE_SOURCES: ReadonlySet<string> = new Set([
"module",
"node:module",
"vm",
"node:vm",
]);

export const IMPORTED_CLASS_REFUSED_GLOBAL_NAMES: ReadonlySet<string> = new Set([
"React",
"eval",
"Function",
"Reflect",
]);

export const IMPORTED_CLASS_CANONICAL_MODULE_SOURCES: ReadonlySet<string> = new Set([
...REACT_JSX_PROVIDER_MODULE_SOURCES,
"prop-types",
]);

export const IMPORTED_CLASS_PASSIVE_OBJECT_METHOD_NAMES: ReadonlySet<string> = new Set([
"keys",
"entries",
]);

export const IMPORTED_CLASS_INSTANCE_MUTATION_NAMES: ReadonlySet<string> = new Set([
"render",
"shouldComponentUpdate",
"isRequired",
]);
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,26 @@ const collectForwardedHookDependencies: CrossFileDependencyCollector = (input) =
if (hasDestructuredHookCandidate) return false;
};

const collectMemoDefaultDependencies: CrossFileDependencyCollector = (input) => {
if (collectForwardedHookDependencies(input) === false) return false;
if (
!flattenImportEntries(input.staticImports).some(
(entry) => entry.source.startsWith(".") && entry.exportedName === "default",
)
)
return;
let hasEmptyObjectDefault = false;
walkAst(input.getProgram(), (node) => {
if (
isNodeOfType(node, "AssignmentPattern") &&
isNodeOfType(node.right, "ObjectExpression") &&
node.right.properties.length === 0
)
hasEmptyObjectDefault = true;
});
if (hasEmptyObjectDefault) return false;
};

const collectCreateRefDependencies: CrossFileDependencyCollector = ({
absoluteFilePath,
sourceText,
Expand Down Expand Up @@ -732,7 +752,7 @@ export const CROSS_FILE_DEPENDENCY_COLLECTORS: ReadonlyMap<string, CrossFileDepe
["prefer-dynamic-import", collectNearestManifestDependencies],
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
["rendering-hydration-no-flicker", collectEffectValueHelperDependencies],
["rerender-memo-with-default-value", collectForwardedHookDependencies],
["rerender-memo-with-default-value", collectMemoDefaultDependencies],
["server-sequential-independent-await", collectSequentialAwaitDependencies],
["rn-no-deprecated-modules", collectReactNativeVersionDependencies],
["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],
Expand Down
Loading
Loading