feat(oxlint-plugin): add client-prefer-keybind-library rule#475
feat(oxlint-plugin): add client-prefer-keybind-library rule#475devin-ai-integration[bot] wants to merge 3 commits into
client-prefer-keybind-library rule#475Conversation
Flags manual addEventListener('keydown'|'keyup'|'keypress', …) calls on
window/document or inside useEffect/useLayoutEffect callbacks, and
recommends using a keybind library (react-hotkeys-hook, tinykeys,
mousetrap, etc.) instead.
The rule:
- Detects global keyboard event listeners (window/document)
- Detects keyboard listeners inside React effect hooks
- Skips files that already import from a known keybind library
- Skips non-keyboard events (click, scroll, resize, etc.)
- Skips element-scoped listeners outside effects
- Tagged test-noise to auto-skip test/story/spec files
Includes 35 tests covering:
- Positive cases (all keyboard event types, global/document/effect)
- False positive avoidance (non-keyboard events, existing imports)
- Open-source repo patterns (Cmd+K search, Escape-to-close, arrow nav)
- Keybind library detection (react-hotkeys-hook, tinykeys, mousetrap, etc.)
- Test file skipping
Co-Authored-By: nisarg@million.dev <awesomenisarg@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: nisarg@million.dev <awesomenisarg@gmail.com>
| export const clientPreferKeybindLibrary = defineRule<Rule>({ | ||
| id: "client-prefer-keybind-library", | ||
| tags: ["test-noise"], | ||
| severity: "warn", | ||
| recommendation: | ||
| 'Use a keybind library like react-hotkeys-hook (`useHotkeys("mod+k", handler)`) instead of manual addEventListener("keydown", …) — it handles focus scoping, modifier normalization, and cleanup automatically', |
There was a problem hiding this comment.
🚩 Rule categorized as "Performance" but has no performance implications
The client bucket defaults to category: "Performance" (see codegen script at packages/oxlint-plugin-react-doctor/scripts/generate-rule-registry.mjs:69). The existing client-localstorage-no-version rule overrides this to "Correctness" in its defineRule call (client-localstorage-no-version.ts:36) because localStorage versioning is about data correctness, not performance. This new rule similarly doesn't fit "Performance" — its recommendation message explicitly lists benefits as focus scoping, modifier normalization, and cleanup automation, which are correctness/architecture concerns. Adding category: "Correctness" or category: "Architecture" to the defineRule call would make the categorization accurate and follow the precedent set by the other client rule.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
should be in architecture imo, feel free to push back if you think otherwise but i don't find it inherently in performance
There was a problem hiding this comment.
Good call — changed the category to "Architecture" in the defineRule call. The codegen now emits category: "Architecture" for this rule.
There was a problem hiding this comment.
Already addressed in ed5221b — category is now "Architecture".
| export const KEYBIND_LIBRARY_PACKAGES: ReadonlyArray<string> = [ | ||
| "react-hotkeys-hook", | ||
| "react-hotkeys", | ||
| "@mantine/hooks", | ||
| "tinykeys", | ||
| "hotkeys-js", | ||
| "mousetrap", | ||
| "@react-hook/hotkey", | ||
| "use-hotkeys", | ||
| "@invoke-ai/ui-library", | ||
| ]; |
There was a problem hiding this comment.
📝 Info: Broad package names in KEYBIND_LIBRARY_PACKAGES may cause per-file false negatives
@mantine/hooks and @invoke-ai/ui-library are general-purpose packages that happen to include keybind functionality. If a file imports any hook/component from these packages (e.g., import { useDebouncedValue } from '@mantine/hooks'), the rule suppresses all keyboard listener warnings for that file, even if the import is unrelated to keybinds. This is a per-file check (not per-project), so the impact is limited to files that both import from one of these broad packages AND have manual keyboard listeners. The design choice is defensible — the user already has the keybind tool available — but it does mean some real cases will be silently skipped.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
this will produce edge cases we should detect. imo this list isn''t even needed if the purpose is to tell people not use manual keyboard listneerr,
There was a problem hiding this comment.
just flag all manual impls, better to have false positives than false negatives
There was a problem hiding this comment.
Agreed — removed KEYBIND_LIBRARY_PACKAGES entirely and the library-detection skip. The rule now flags all manual keyboard listener implementations unconditionally. Better to have false positives than false negatives here.
There was a problem hiding this comment.
Already addressed in ed5221b — KEYBIND_LIBRARY_PACKAGES and the library-detection skip were removed entirely. The rule now flags all manual keyboard listeners unconditionally, so this false-negative scenario no longer applies.
| export const clientPreferKeybindLibrary = defineRule<Rule>({ | ||
| id: "client-prefer-keybind-library", | ||
| tags: ["test-noise"], | ||
| severity: "warn", | ||
| recommendation: | ||
| 'Use a keybind library like react-hotkeys-hook (`useHotkeys("mod+k", handler)`) instead of manual addEventListener("keydown", …) — it handles focus scoping, modifier normalization, and cleanup automatically', |
There was a problem hiding this comment.
should be in architecture imo, feel free to push back if you think otherwise but i don't find it inherently in performance
| export const KEYBIND_LIBRARY_PACKAGES: ReadonlyArray<string> = [ | ||
| "react-hotkeys-hook", | ||
| "react-hotkeys", | ||
| "@mantine/hooks", | ||
| "tinykeys", | ||
| "hotkeys-js", | ||
| "mousetrap", | ||
| "@react-hook/hotkey", | ||
| "use-hotkeys", | ||
| "@invoke-ai/ui-library", | ||
| ]; |
There was a problem hiding this comment.
this will produce edge cases we should detect. imo this list isn''t even needed if the purpose is to tell people not use manual keyboard listneerr,
| export const KEYBIND_LIBRARY_PACKAGES: ReadonlyArray<string> = [ | ||
| "react-hotkeys-hook", | ||
| "react-hotkeys", | ||
| "@mantine/hooks", | ||
| "tinykeys", | ||
| "hotkeys-js", | ||
| "mousetrap", | ||
| "@react-hook/hotkey", | ||
| "use-hotkeys", | ||
| "@invoke-ai/ui-library", | ||
| ]; |
There was a problem hiding this comment.
just flag all manual impls, better to have false positives than false negatives
- Change category from Performance to Architecture per reviewer feedback - Remove KEYBIND_LIBRARY_PACKAGES constant and library-detection logic — flag all manual keyboard listeners unconditionally to prefer false positives over false negatives - Update tests accordingly (30 tests, all pass) Co-Authored-By: nisarg@million.dev <awesomenisarg@gmail.com>
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ed5221b. Configure here.
| (current.callee.name === "useEffect" || current.callee.name === "useLayoutEffect") | ||
| ) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Effect detection misses React.useEffect(...) member expression calls
Medium Severity
isInsideUseEffectCallback only matches callees that are Identifier nodes (e.g. useEffect(…)), so it silently misses React.useEffect(…) or any namespaced call where the callee is a MemberExpression. The codebase already provides getCalleeName (which extracts the method name from both patterns) and the EFFECT_HOOK_NAMES constant — other rules like nextjs-no-client-fetch-for-server-data use isHookCall(node, EFFECT_HOOK_NAMES) for exactly this purpose. This creates a false negative that contradicts the PR's stated preference for false positives over false negatives.
Reviewed by Cursor Bugbot for commit ed5221b. Configure here.


Summary
Adds
client-prefer-keybind-library, a new draft rule that flags manualaddEventListener("keydown" | "keyup" | "keypress", …)calls onwindow/documentor insideuseEffect/useLayoutEffect, recommending a keybind library likereact-hotkeys-hookinstead for consistent, declarative, and accessible keyboard shortcut management.Category: Architecture (not Performance — the benefits are focus scoping, modifier normalization, and cleanup automation)
Detection scope:
window.addEventListener("keydown", …)/document.addEventListener("keydown", …)anywhere.addEventListener("keydown", …)insideuseEffectoruseLayoutEffectcallbackskeydown,keyup,keypressDoes NOT flag:
click,scroll,resize,mousedown, etc.)ref.current.addEventListener)test-noisetag)Flags unconditionally — even if a keybind library is already imported in the file. Prefers false positives over false negatives.
30 tests covering positive cases, false positive avoidance, real-world open-source patterns (Cmd+K search, Escape-to-close, arrow nav, focus trap), and test file skipping.
Review & Testing Checklist for Human
react-doctoragainst a project that useswindow.addEventListener("keydown", …))Notes
dom.ts:KEYBOARD_EVENT_NAMESKEYBIND_LIBRARY_PACKAGESwas removed per review feedback — the rule flags all manual implementations unconditionallyLink to Devin session: https://app.devin.ai/sessions/0b43ca64311c427394bd367288cc28cb
Requested by: @NisargIO
Note
Low Risk
Adds a new warn-only lint rule and constants with no runtime or security impact; main risk is extra warnings on codebases that intentionally use manual keyboard listeners.
Overview
Adds a new Architecture lint rule
client-prefer-keybind-librarythat warns when apps register manual keyboard shortcuts viaaddEventListenerwith literalkeydown,keyup, orkeypressonwindow/document(anywhere) or on any target insideuseEffect/useLayoutEffect. Diagnostics recommend a library such as react-hotkeys-hook and still fire even if a keybind package is already imported.Shared DOM constants gain
KEYBOARD_EVENT_NAMES. The rule is wired into the generatedrule-registryas a global warn-level entry and ships with a large test suite (including real-world patterns andtest-noiseskipping for test/spec/story files).Reviewed by Cursor Bugbot for commit ed5221b. Bugbot is set up for automated code reviews on this repo. Configure here.