Skip to content

feat(architecture): add no-prop-drilling rule#700

Open
NisargIO wants to merge 1 commit into
mainfrom
feat/no-prop-drilling-rule
Open

feat(architecture): add no-prop-drilling rule#700
NisargIO wants to merge 1 commit into
mainfrom
feat/no-prop-drilling-rule

Conversation

@NisargIO
Copy link
Copy Markdown
Member

@NisargIO NisargIO commented Jun 6, 2026

Why

Catches a prop forwarded untouched through 3+ same-file components — each one a pure pass-through that never reads the prop — before it's finally used. That's prop drilling: the intermediate components exist only to relay a value, which couples them to a shape they don't care about and makes refactors noisy. The fix is to lift the value into a Context/Provider (or compose with children), per the vercel-labs composition-patterns (compound-components) skill.

This angle had no prior coverage in the plugin — the only existing passthrough logic is zod's .passthrough() and a single-component wrapper check in no-multi-comp; there was no cross-component drilling analysis.

Before (flagged):

function Page({ user })    { return <Sidebar user={user} />; }   // forwards only
function Sidebar({ user }) { return <Profile user={user} />; }   // forwards only
function Profile({ user }) { return <Avatar user={user} />; }    // forwards only
function Avatar({ user })  { return <img alt={user.name} />; }   // finally uses it
// ⚠ Prop "user" is forwarded untouched through 3 components (Page → Sidebar → Profile) before it's used.

After (quiet):

const UserContext = createContext(null);
function Page({ user }) {
  return <UserContext value={user}><Sidebar /></UserContext>;
}
function Avatar() {
  const user = use(UserContext);
  return <img alt={user.name} />;
}

What changed

  • Added no-prop-drilling (architecture/, category Maintainability, severity warn, tags test-noise + react-jsx-only).
  • Detects a destructured named prop passed as a bare JSX attribute value through a chain of same-file components.
  • Reports once per drilled prop, at the outermost forwarder's attribute, when the chain of pure forwarders reaches PROP_DRILL_CHAIN_THRESHOLD (= 3) and terminates in a real consumer.
  • Scope-aware (uses context.scopes): resolves each JSX tag to a same-file component symbol and each forwarded value to its prop parameter binding, so it's robust to shadowing and renamed destructures rather than name-matching.
  • Allows (stays quiet on): chains below the threshold, any intermediate that reads the prop, transformed/derived values (user.name, fn(user), ternaries), {...spread} forwarding, non-destructured props.x, memo()/forwardRef()-wrapped components, member/namespaced JSX tags, and hand-offs to DOM elements or imported components (treated as consuming the prop — the chain ends there).
  • Cycle-safe: a pure-forwarding loop never consumes the prop, so it produces no diagnostic and cannot recurse infinitely.
  • Adds PROP_DRILL_CHAIN_THRESHOLD to constants/thresholds.ts; regenerates rule-registry.ts via pnpm gen.

v1 non-goals (intentional)

Cross-file drilling, {...spread} chains, non-destructured props, and HOC/wrapper indirection are out of scope — documented in-code. The rule errs conservative: false positives are treated as correctness bugs.

Adversarial review

This rule was stress-tested with a 4-lens multi-agent review that empirically ran 173 candidate fixtures through the real harness (false-positives: 32, false-negatives: 61, AST edge-cases: 55, robustness: 25). Result: 0 false negatives, no crashes/hangs/parse errors, and one genuine false positive — a pure-forwarding cycle with no consumer (A→B→C→B) manufacturing a phantom chain. That's fixed (chains now only count when they terminate in a consumer) and locked in with 2 regression tests.

Test plan

  • pnpm exec vp test run src/plugin/rules/architecture/no-prop-drilling.test.ts16 pass (12 valid + 4 invalid adversarial cases incl. renamed pass-through, TS-cast hop, shadowing, imported terminus, cycles).
  • pnpm --filter oxlint-plugin-react-doctor typecheck — clean.
  • pnpm exec vp lint + vp fmt --check on changed files — clean.
  • src/plugin/rules/architecture/ suite — 109 pass.

Note: a broad RDE/OSS eval pass (HOW_TO_WRITE_A_RULE workflow two) has not been run yet — recommend doing so before merge to confirm low noise on real repos and to validate/tune the threshold of 3.

🤖 Generated with Claude Code

Flags a prop forwarded untouched through 3+ same-file components — each a
pure pass-through that never reads it — before it's finally consumed, and
recommends lifting the value into a Context/Provider (or composing with
`children`). Sourced from the vercel-labs composition-patterns
(compound-components) skill; this drilling angle had no prior coverage.

The detector is scope-aware (uses context.scopes): it resolves each JSX
tag to a same-file component and each forwarded attribute value to a prop
parameter binding, so shadowed names, transformed values (user.name,
fn(user)), conditionals, {...spread}, and hand-offs to DOM or imported
components do not count as untouched forwarding. The chain is only
counted when it terminates in a real consumer, so pure-forwarding cycles
never produce a phantom diagnostic.

Conservative v1 (threshold 3, same-file only); 16 adversarial unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 6, 2026

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@700
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@700
npm i https://pkg.pr.new/react-doctor@700

commit: f3e3fac

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 6, 2026

No React Doctor issues found. 🎉

Reviewed by React Doctor for commit f3e3fac.

@NisargIO NisargIO self-assigned this Jun 6, 2026
@NisargIO NisargIO requested a review from rayhanadev June 6, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant