Skip to content

fix(linter): accept digits after 'use' in hook names#19254

Merged
camc314 merged 1 commit intooxc-project:mainfrom
sreetamdas:fix/rules-of-hooks-digit-names
Feb 11, 2026
Merged

fix(linter): accept digits after 'use' in hook names#19254
camc314 merged 1 commit intooxc-project:mainfrom
sreetamdas:fix/rules-of-hooks-digit-names

Conversation

@sreetamdas
Copy link
Contributor

@sreetamdas sreetamdas commented Feb 11, 2026

Fix a rules-of-hooks divergence from ESLint's eslint-plugin-react-hooks: hook name validation rejects digits after use (e.g. use2FAMutation).


Details

ESLint's isHookName uses the regex /^use[A-Z0-9]/:

function isHookName(s: string): boolean {
  return s === 'use' || /^use[A-Z0-9]/.test(s);
}

Oxlint's is_react_hook_name only checked char::is_uppercase, which doesn't include digits:

name.starts_with("use") && name.chars().nth(3).is_none_or(char::is_uppercase)

This means use2FAMutation, use3DEngine etc. were not recognized as valid custom hooks — causing false positives when called inside components and false negatives when called at the top level.

The fix adds || c.is_ascii_digit() to match ESLint's behavior:

name.starts_with("use")
    && name.chars().nth(3).is_none_or(|c| c.is_uppercase() || c.is_ascii_digit())

The snapshot update confirms this is correct: Hook.use42() called at the top level now produces a diagnostic. The existing test comments already expected this (topLevelError('Hook.use42')).

Also see: #19220

ESLint's isHookName regex is /^use[A-Z0-9]/, accepting digits after 'use'
(e.g. use2FAMutation). Oxlint only accepted uppercase letters via
char::is_uppercase, causing false positives for valid custom hooks like
use2FAMutation and use3DEngine when called inside components, and false
negatives when called at the top level.

Add || c.is_ascii_digit() to is_react_hook_name to match ESLint's behavior.
@github-actions github-actions bot added A-linter Area - Linter C-bug Category - Bug labels Feb 11, 2026
sreetamdas added a commit to sreetamdas/oxc that referenced this pull request Feb 11, 2026
…ate PR

Move the is_react_hook_name digit acceptance changes to oxc-project#19254,
keeping only the exhaustive-deps module-scoped callback refs fix here.
@sreetamdas sreetamdas marked this pull request as ready for review February 11, 2026 08:03
@sreetamdas sreetamdas requested a review from camc314 as a code owner February 11, 2026 08:03
Copy link
Contributor

@camc314 camc314 left a comment

Choose a reason for hiding this comment

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

Thank you!

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 11, 2026

Merging this PR will not alter performance

✅ 47 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing sreetamdas:fix/rules-of-hooks-digit-names (d35d018) with main (aaa8967)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@camc314 camc314 merged commit 740a009 into oxc-project:main Feb 11, 2026
30 checks passed
camc314 added a commit that referenced this pull request Feb 11, 2026
…stive-deps (#19220)

Fix an `exhaustive-deps` divergence from ESLint's `eslint-plugin-react-hooks`: module-scoped function references passed as callbacks (e.g. `useMemo(getColumns, [])`) are not recognized as stable.

---

### Details

Consider this pattern:

```js
function getColumns() { return []; }       // module scope

function MyComponent() {
  const columns = useMemo(getColumns, []); // oxlint false positive
}
```

`getColumns` is defined at module scope — it never changes between
renders. ESLint correctly recognizes this and doesn't report missing
dependencies. Oxlint was reporting a false positive.

<details>
<summary>Why? (explained by Claude Opus 4.6)</summary>
When the callback to `useMemo` is an identifier rather than an inline
arrow, the rule enters [this
branch](https://github.com/oxc-project/oxc/blob/1b2f354/crates/oxc_linter/src/rules/react/exhaustive_deps.rs#L330-L400):
1. It calls `get_declaration_of_variable(ident, ...)` to resolve where
`getColumns` is defined — this finds the declaration **regardless of
scope**
2. It sees `AstKind::Function` and wraps it as `CallbackNode::Function`,
treating the body as if it were an inline callback
3. It walks the function body with `ExhaustiveDepsVisitor`, collecting
all identifier references as "found dependencies"
4. It compares those against the declared `[]` empty array and reports
mismatches
The problem: between step 1 and 2, there was no check for whether the
declaration is even inside the component. The rule already has this
exact check for the inline-callback path —
[`is_identifier_a_dependency`](https://github.com/oxc-project/oxc/blob/1b2f354/crates/oxc_linter/src/rules/react/exhaustive_deps.rs#L932-L954)
returns `false` when `declaration.scope_id() != component_scope_id`. The
function-reference-as-callback path just never had this gate.
</details>

The fix adds a scope check before analyzing the resolved function body:

```rust
if let Some(decl) = get_declaration_of_variable(ident, ctx.semantic()) {
    // NEW: module-scoped declarations are stable
    if decl.scope_id() != component_scope_id {
        return;
    }
    // ... existing body analysis logic
}
```

Also see: #19254

---------

Co-authored-by: Cameron Clark <[email protected]>
camc314 added a commit that referenced this pull request Feb 12, 2026
# Oxlint
### 🚀 Features

- ebb80b3 ast: Add `node_id` field to all AST struct nodes (#18138)
(Boshen)
- 2879fc5 linter: Implement fixer for unicorn/prefer-math-trunc (#19275)
(camc314)
- a204eda linter: Implement fixer for unicorn/no-typeof-undefined
(#19274) (camc314)
- ab46d9c linter: Implement typescript/class-literal-property-style
(#19252) (Vincent R)
- 1a61f58 linter: Implement typescript/no-invalid-void-type (#19242)
(Vincent R)

### 🐛 Bug Fixes

- 45adda2 oxlint/lsp: Use blocking stdio in Oxlint (#19292)
(overlookmotel)
- 05bc855 linter/import: Count unique module sources in max-dependencies
(#19270) (camc314)
- 8566b44 linter: Check for preceeding token in math trunc fixer
(#19277) (camc314)
- f16f2b6 linter/import-no-cycle: Avoid traversal-order false negatives
with type-only edges (#19267) (camc314)
- d4937e7 linter: Recognize module-scoped callback refs as stable in
exhaustive-deps (#19220) (Sreetam Das)
- 140c9bd linter: Detect fallthrough from `default` when it is not the
last case (#19261) (Boshen)
- 740a009 linter: Accept digits after 'use' in hook names (#19254)
(Sreetam Das)
- 31b562f linter: Update `import/no-named-as-default` to allow named
import if equivalent to the default import (#19100) (connorshea)
- 79c82cc linter: Avoid applying object-level docs to nested object
methods in require-param (#19231) (camc314)

### ⚡ Performance

- 5670291 linter/class-literal-property-style: Avoid unneeded string
allocations (#19262) (camc314)
# Oxfmt
### 🚀 Features

- ebb80b3 ast: Add `node_id` field to all AST struct nodes (#18138)
(Boshen)

### 🐛 Bug Fixes

- 1957908 formatter: Avoid unnecessary parentheses for string literal in
labeled statement (#19272) (Dunqing)

Co-authored-by: camc314 <[email protected]>
OskarLebuda pushed a commit to OskarLebuda/oxc that referenced this pull request Feb 17, 2026
Fix a `rules-of-hooks` divergence from ESLint's `eslint-plugin-react-hooks`: hook name validation rejects digits after `use` (e.g. `use2FAMutation`).

---

### Details

ESLint's
[`isHookName`](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts#L31-L33) uses the regex `/^use[A-Z0-9]/`:

```ts
function isHookName(s: string): boolean {
  return s === 'use' || /^use[A-Z0-9]/.test(s);
}
```

Oxlint's
[`is_react_hook_name`](https://github.com/oxc-project/oxc/blob/1b2f354/crates/oxc_linter/src/utils/react.rs#L489=L491)
only checked `char::is_uppercase`, which doesn't include digits:

```rust
name.starts_with("use") && name.chars().nth(3).is_none_or(char::is_uppercase)
```

This means `use2FAMutation`, `use3DEngine` etc. were not recognized as
valid custom hooks — causing false positives when called inside
components and false negatives when called at the top level.

The fix adds `|| c.is_ascii_digit()` to match ESLint's behavior:

```rust
name.starts_with("use")
    && name.chars().nth(3).is_none_or(|c| c.is_uppercase() || c.is_ascii_digit())
```

The snapshot update confirms this is correct: `Hook.use42()` called at the top level now produces a diagnostic. The existing test comments already expected this (`topLevelError('Hook.use42')`).

Also see: oxc-project#19220
OskarLebuda pushed a commit to OskarLebuda/oxc that referenced this pull request Feb 17, 2026
…stive-deps (oxc-project#19220)

Fix an `exhaustive-deps` divergence from ESLint's `eslint-plugin-react-hooks`: module-scoped function references passed as callbacks (e.g. `useMemo(getColumns, [])`) are not recognized as stable.

---

### Details

Consider this pattern:

```js
function getColumns() { return []; }       // module scope

function MyComponent() {
  const columns = useMemo(getColumns, []); // oxlint false positive
}
```

`getColumns` is defined at module scope — it never changes between
renders. ESLint correctly recognizes this and doesn't report missing
dependencies. Oxlint was reporting a false positive.

<details>
<summary>Why? (explained by Claude Opus 4.6)</summary>
When the callback to `useMemo` is an identifier rather than an inline
arrow, the rule enters [this
branch](https://github.com/oxc-project/oxc/blob/1b2f354/crates/oxc_linter/src/rules/react/exhaustive_deps.rs#L330-L400):
1. It calls `get_declaration_of_variable(ident, ...)` to resolve where
`getColumns` is defined — this finds the declaration **regardless of
scope**
2. It sees `AstKind::Function` and wraps it as `CallbackNode::Function`,
treating the body as if it were an inline callback
3. It walks the function body with `ExhaustiveDepsVisitor`, collecting
all identifier references as "found dependencies"
4. It compares those against the declared `[]` empty array and reports
mismatches
The problem: between step 1 and 2, there was no check for whether the
declaration is even inside the component. The rule already has this
exact check for the inline-callback path —
[`is_identifier_a_dependency`](https://github.com/oxc-project/oxc/blob/1b2f354/crates/oxc_linter/src/rules/react/exhaustive_deps.rs#L932-L954)
returns `false` when `declaration.scope_id() != component_scope_id`. The
function-reference-as-callback path just never had this gate.
</details>

The fix adds a scope check before analyzing the resolved function body:

```rust
if let Some(decl) = get_declaration_of_variable(ident, ctx.semantic()) {
    // NEW: module-scoped declarations are stable
    if decl.scope_id() != component_scope_id {
        return;
    }
    // ... existing body analysis logic
}
```

Also see: oxc-project#19254

---------

Co-authored-by: Cameron Clark <[email protected]>
OskarLebuda pushed a commit to OskarLebuda/oxc that referenced this pull request Feb 17, 2026
# Oxlint
### 🚀 Features

- ebb80b3 ast: Add `node_id` field to all AST struct nodes (oxc-project#18138)
(Boshen)
- 2879fc5 linter: Implement fixer for unicorn/prefer-math-trunc (oxc-project#19275)
(camc314)
- a204eda linter: Implement fixer for unicorn/no-typeof-undefined
(oxc-project#19274) (camc314)
- ab46d9c linter: Implement typescript/class-literal-property-style
(oxc-project#19252) (Vincent R)
- 1a61f58 linter: Implement typescript/no-invalid-void-type (oxc-project#19242)
(Vincent R)

### 🐛 Bug Fixes

- 45adda2 oxlint/lsp: Use blocking stdio in Oxlint (oxc-project#19292)
(overlookmotel)
- 05bc855 linter/import: Count unique module sources in max-dependencies
(oxc-project#19270) (camc314)
- 8566b44 linter: Check for preceeding token in math trunc fixer
(oxc-project#19277) (camc314)
- f16f2b6 linter/import-no-cycle: Avoid traversal-order false negatives
with type-only edges (oxc-project#19267) (camc314)
- d4937e7 linter: Recognize module-scoped callback refs as stable in
exhaustive-deps (oxc-project#19220) (Sreetam Das)
- 140c9bd linter: Detect fallthrough from `default` when it is not the
last case (oxc-project#19261) (Boshen)
- 740a009 linter: Accept digits after 'use' in hook names (oxc-project#19254)
(Sreetam Das)
- 31b562f linter: Update `import/no-named-as-default` to allow named
import if equivalent to the default import (oxc-project#19100) (connorshea)
- 79c82cc linter: Avoid applying object-level docs to nested object
methods in require-param (oxc-project#19231) (camc314)

### ⚡ Performance

- 5670291 linter/class-literal-property-style: Avoid unneeded string
allocations (oxc-project#19262) (camc314)
# Oxfmt
### 🚀 Features

- ebb80b3 ast: Add `node_id` field to all AST struct nodes (oxc-project#18138)
(Boshen)

### 🐛 Bug Fixes

- 1957908 formatter: Avoid unnecessary parentheses for string literal in
labeled statement (oxc-project#19272) (Dunqing)

Co-authored-by: camc314 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments