From 6adc7b193738555819607bc68747eb48d49384b9 Mon Sep 17 00:00:00 2001 From: Akshay More Date: Wed, 18 Feb 2026 08:41:20 +0000 Subject: [PATCH] feat: add no-inline-object-props performance rule Detects inline object/array literals passed directly as JSX props which create new references on every render causing unnecessary re-renders. - Added rule visitor in performance.ts - Registered in index.ts - Added severity (warn) in oxlint-config.ts - Added category mapping and help text in run-oxlint.ts - Added test coverage in run-oxlint.test.ts --- packages/react-doctor/src/oxlint-config.ts | 1 + packages/react-doctor/src/plugin/index.ts | 2 ++ .../src/plugin/rules/performance.ts | 25 +++++++++++++++++++ packages/react-doctor/src/utils/run-oxlint.ts | 3 +++ .../react-doctor/tests/run-oxlint.test.ts | 6 +++++ 5 files changed, 37 insertions(+) diff --git a/packages/react-doctor/src/oxlint-config.ts b/packages/react-doctor/src/oxlint-config.ts index 6976b8b..e5d2aa8 100644 --- a/packages/react-doctor/src/oxlint-config.ts +++ b/packages/react-doctor/src/oxlint-config.ts @@ -119,6 +119,7 @@ export const createOxlintConfig = ({ "react-doctor/rerender-memo-with-default-value": "warn", "react-doctor/rendering-animate-svg-wrapper": "warn", "react-doctor/no-inline-prop-on-memo-component": "warn", + "react-doctor/no-inline-object-props": "warn", "react-doctor/rendering-hydration-no-flicker": "warn", "react-doctor/no-transition-all": "warn", diff --git a/packages/react-doctor/src/plugin/index.ts b/packages/react-doctor/src/plugin/index.ts index 76220bf..67b874f 100644 --- a/packages/react-doctor/src/plugin/index.ts +++ b/packages/react-doctor/src/plugin/index.ts @@ -49,6 +49,7 @@ import { } from "./rules/nextjs.js"; import { noGlobalCssVariableAnimation, + noInlineObjectProps, noLargeAnimatedBlur, noLayoutPropertyAnimation, noPermanentWillChange, @@ -107,6 +108,7 @@ const plugin: RulePlugin = { "rerender-memo-with-default-value": rerenderMemoWithDefaultValue, "rendering-animate-svg-wrapper": renderingAnimateSvgWrapper, "no-inline-prop-on-memo-component": noInlinePropOnMemoComponent, + "no-inline-object-props": noInlineObjectProps, "rendering-hydration-no-flicker": renderingHydrationNoFlicker, "no-transition-all": noTransitionAll, diff --git a/packages/react-doctor/src/plugin/rules/performance.ts b/packages/react-doctor/src/plugin/rules/performance.ts index 2f015c2..822cb55 100644 --- a/packages/react-doctor/src/plugin/rules/performance.ts +++ b/packages/react-doctor/src/plugin/rules/performance.ts @@ -50,6 +50,31 @@ const isInlineReference = (node: EsTreeNode): string | null => { return null; }; +const getInlineLiteralKind = (node: EsTreeNode): "object" | "array" | null => { + if (node.type === "ObjectExpression") return "object"; + if (node.type === "ArrayExpression") return "array"; + return null; +}; + +export const noInlineObjectProps: Rule = { + create: (context: RuleContext) => ({ + JSXAttribute(node: EsTreeNode) { + if (!node.value || node.value.type !== "JSXExpressionContainer") return; + + const inlineLiteralKind = getInlineLiteralKind(node.value.expression); + if (!inlineLiteralKind) return; + + const propName = + node.name?.type === "JSXIdentifier" ? node.name.name : "this prop"; + + context.report({ + node: node.value.expression, + message: `Inline ${inlineLiteralKind} literal passed to "${propName}" creates a new reference on every render — extract it to a stable constant`, + }); + }, + }), +}; + export const noInlinePropOnMemoComponent: Rule = { create: (context: RuleContext) => { const memoizedComponentNames = new Set(); diff --git a/packages/react-doctor/src/utils/run-oxlint.ts b/packages/react-doctor/src/utils/run-oxlint.ts index a738383..7000e26 100644 --- a/packages/react-doctor/src/utils/run-oxlint.ts +++ b/packages/react-doctor/src/utils/run-oxlint.ts @@ -40,6 +40,7 @@ const RULE_CATEGORY_MAP: Record = { "react-doctor/rerender-memo-with-default-value": "Performance", "react-doctor/rendering-animate-svg-wrapper": "Performance", "react-doctor/rendering-usetransition-loading": "Performance", + "react-doctor/no-inline-object-props": "Performance", "react-doctor/rendering-hydration-no-flicker": "Performance", "react-doctor/no-transition-all": "Performance", @@ -124,6 +125,8 @@ const RULE_HELP_MAP: Record = { "Wrap the SVG: `...`", "rendering-usetransition-loading": "Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading state", + "no-inline-object-props": + "Extract object/array literals to stable references with `useMemo` or module-level constants, then pass those variables as props", "rendering-hydration-no-flicker": "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element", diff --git a/packages/react-doctor/tests/run-oxlint.test.ts b/packages/react-doctor/tests/run-oxlint.test.ts index bce8b4c..64410cb 100644 --- a/packages/react-doctor/tests/run-oxlint.test.ts +++ b/packages/react-doctor/tests/run-oxlint.test.ts @@ -149,6 +149,12 @@ describe("runOxlint", () => { fixture: "performance-issues.tsx", ruleSource: "rules/performance.ts", }, + "no-inline-object-props": { + fixture: "performance-issues.tsx", + ruleSource: "rules/performance.ts", + severity: "warning", + category: "Performance", + }, "no-usememo-simple-expression": { fixture: "performance-issues.tsx", ruleSource: "rules/performance.ts",