Skip to content

feat(eslint-rules): add base-hook-signature rule#36252

Open
Hotell wants to merge 6 commits into
microsoft:masterfrom
Hotell:tools/ws-lint/rule-base-hook-signature
Open

feat(eslint-rules): add base-hook-signature rule#36252
Hotell wants to merge 6 commits into
microsoft:masterfrom
Hotell:tools/ws-lint/rule-base-hook-signature

Conversation

@Hotell
Copy link
Copy Markdown
Contributor

@Hotell Hotell commented May 26, 2026

Summary

Introduces a new workspace ESLint rule @nx/workspace-base-hook-signature that enforces the consumed signature for useFooBase / useFooBase_unstable hook implementations:

  • Hook must accept 1 or 2 positional parameters (props, optional ref).
  • props must be a plain Identifier named props with an explicit type annotation — untyped props would be inferred as any and fail under noImplicitAny. The shape of the type is intentionally not validated; only its presence.
  • Optional ref must be an Identifier named ref typed as React.Ref<...> (resolved through scope so a locally-shadowed Ref/React does not pass).
  • Hook must be a FunctionDeclaration or const useFooBase = (...) => {} — or a re-export identifier (const useFooBase_unstable = useFooBase;). Literals, object expressions, and arrays are rejected with invalidBaseHookInit.
  • Also applies to paired wrapping state hooks useFoo_unstable when their sibling useFooBase_unstable exists (same file or useFooBase.ts(x) next to useFoo.ts(x)).

Validation order (stops at the first failure):

  1. Param count must be 1 or 2 (invalidParamCount)
  2. props name must be correct; then ref name if present (invalidParamName, stops at first wrong name)
  3. props must have a type annotation (missingPropsType, short-circuits before the ref check)
  4. ref type must be React.Ref<...> (invalidRefType)

Diagnostic message IDs: invalidParamCount, invalidParamName, missingPropsType, invalidRefType, invalidBaseHookInit.

The rule is fully inert in this PR — it is registered with @nx/workspace but not enabled in any project config. Wiring happens in a follow-up PR.

Plan reference

Part of the split of #36251 — see prd/eslint-rules-base-hook-pr-split.spec.md (PR1).

Fixtures

Self-contained fixture tree under tools/eslint-rules/rules/__fixtures__/base-hook-signature/:

  • src/components/Sibling/useSiblingBase.ts — MUST exist; presence triggers pair detection for the sibling-file test.
  • src/components/Sibling/useSibling.ts and src/components/Orphan/useOrphan.ts — docs-only stubs (export {}; + header comment). The rule never reads them; they exist purely so the fixture tree mirrors a real component folder layout. The Orphan/ folder intentionally has no useOrphanContextValuesBase.ts(x) — the absence drives the "non-paired hook is exempt" test.

The spec file contains a NOTE on fixture filenames block explaining RuleTester semantics and which fixture files actually drive behavior.

Verification

nx run eslint-rules:test --skip-nx-cache

→ 4 suites / 65 tests pass.

ESLint CLI perf (TIMING=200)

Measured on a combined branch (PR1 + PR2 + PR3 wiring) so the rule actually runs.

Project @nx/workspace-base-hook-signature % of total
react-button (no *Base hook) 1.74 ms 0.1%
react-combobox (1 *Base hook) 2.31 ms 0.1%
react-tags (1 *Base hook) 2.24 ms 0.1%

Negligible — well below @typescript-eslint/no-deprecated (≈1000 ms) and react-hooks/static-components (≈400 ms) in the same runs.

Out of scope

  • Wiring into @fluentui/eslint-plugin (separate PR).
  • Beachball change file (declined — tooling-only change).

Registers @nx/workspace-base-hook-signature. Rule is not yet enabled in any project config and is fully inert until wired up in a follow-up PR.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 26, 2026

📊 Bundle size report

✅ No changes found

@github-actions
Copy link
Copy Markdown

Pull request demo site: URL

Hotell added 2 commits May 26, 2026 16:59
…nting

Add docs-only Sibling/useSibling.ts and Orphan/useOrphan.ts stubs so the fixture folder layout mirrors the situations the tests assert against, and document in the spec why useSiblingBase.ts (must exist) and the absent useOrphanContextValuesBase.ts(x) (must NOT exist) are the only files that actually drive rule behavior.
…ops` param

Untyped `props` would be inferred as `any` and fail under `noImplicitAny`. The rule now reports `missingPropsType` when a base/paired hook's first parameter has no type annotation. The shape of the type is intentionally not validated \u2014 just its presence.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new internal Nx workspace ESLint rule, @nx/workspace-base-hook-signature, to enforce the parameter contract for v9 base hooks (use<Name>Base_unstable) and (when paired) their wrapping state hooks (use<Name>_unstable). It’s implemented in tools/eslint-rules/ and is registered but not enabled in any lint config yet.

Changes:

  • Added the base-hook-signature rule implementation, including same-file + sibling-file pair detection for enforcing state-hook signatures only when a base hook exists.
  • Added a comprehensive RuleTester test suite plus a fixture directory tree to validate sibling-file detection behavior.
  • Registered the rule in the workspace rules index and excluded __fixtures__ from the lint tsconfig compilation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tools/eslint-rules/tsconfig.lint.json Excludes __fixtures__ from lint TS compilation.
tools/eslint-rules/rules/base-hook-signature.ts New ESLint rule enforcing (props, ref?) signature and React.Ref<...> typing with pairing detection.
tools/eslint-rules/rules/base-hook-signature.spec.ts Unit tests covering valid/invalid signatures and sibling-file pairing behavior.
tools/eslint-rules/rules/fixtures/base-hook-signature/src/components/Sibling/useSiblingBase.ts Fixture file used for sibling-file existence detection.
tools/eslint-rules/rules/fixtures/base-hook-signature/src/components/Sibling/useSibling.ts Docs-only stub fixture mirroring component layout.
tools/eslint-rules/rules/fixtures/base-hook-signature/src/components/Orphan/useOrphan.ts Docs-only stub fixture to validate “no pair ⇒ no enforcement”.
tools/eslint-rules/index.ts Registers the new rule for Nx workspace ESLint rule exposure.

Comment thread tools/eslint-rules/rules/base-hook-signature.ts Outdated
Comment thread tools/eslint-rules/rules/base-hook-signature.ts
Hotell added 3 commits May 26, 2026 17:51
- Fix short-circuiting: use for loop instead of forEach to allow early returns after missingPropsType check, preventing simultaneous reporting of missingPropsType and invalidRefType
- Add invalidBaseHookInit check: report errors for non-function base hook initializers (literals like 42, {}, []), while allowing valid re-exports (identifiers)
- Distinguish between param-name errors (all reported) and type-annotation errors (short-circuit for fundamental issues)
- Add describeInitializer() helper for user-facing error messages
- Add test cases for invalid initializers (4 new invalid cases) and valid re-exports (2 new valid cases)
- All 65 tests passing
Parameters with wrong names now stop validation immediately (don't check subsequent params or types). Update tests to expect only first param-name error.
…hecks

Since the max param count is 2, loops add no value. Destructure [propsParam, refParam]
directly and check each in sequence for clearer linear flow.
@Hotell Hotell marked this pull request as ready for review May 26, 2026 16:48
@Hotell Hotell requested a review from a team as a code owner May 26, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants