ESLint Plugin
@umpire/eslint-plugin adds five ESLint rules that catch umpire mistakes before your tests run. Two catch likely bugs (unknown fields, inline instantiation), three catch logical impossibilities that would silently produce fields that can never be enabled.
Compatibility
Section titled “Compatibility”| ESLint | ≥ 9.0.0 |
| Oxlint | JS plugins (jsPlugins) |
| Config format | Flat config only (eslint.config.js) — legacy .eslintrc is not supported |
| Node | Whatever ESLint 9 requires (≥ 18.18) |
eslint is an optional peer dependency — it’s only needed when using ESLint directly. Oxlint users don’t need it installed.
Install
Section titled “Install”npm install --save-dev @umpire/eslint-pluginAdd the recommended config to your eslint.config.js:
import umpire from '@umpire/eslint-plugin'
export default [ umpire.configs.recommended, // ... rest of your config]This enables all five rules with the severities shown in the table below. To customize individual rules, spread the config and override:
import umpire from '@umpire/eslint-plugin'
export default [ { ...umpire.configs.recommended, rules: { ...umpire.configs.recommended.rules, '@umpire/no-unknown-fields': 'error', // promote to error '@umpire/no-inline-umpire-init': 'off', // disable if you prefer useMemo discipline elsewhere }, },]Or register the plugin manually and pick individual rules:
import umpire from '@umpire/eslint-plugin'
export default [ { plugins: { '@umpire': umpire }, rules: { '@umpire/no-self-disable': 'error', '@umpire/no-circular-requires': 'error', }, },]Oxlint setup
Section titled “Oxlint setup”Add the plugin to jsPlugins, then enable rules by their full name:
{ "jsPlugins": ["@umpire/eslint-plugin"], "rules": { "@umpire/eslint-plugin/no-self-disable": "error" }}Rule names use the @umpire/eslint-plugin/ prefix in Oxlint, unlike ESLint’s @umpire/ namespace.
| Rule | Severity | What it catches |
|---|---|---|
no-unknown-fields | warn | Field names in rules that aren’t declared in fields |
no-inline-umpire-init | warn | umpire() called inside a component or hook body without useMemo |
no-self-disable | error | A field listed as both source and target of disables() |
no-contradicting-rules | error | requires/disables pairs that make a field permanently unavailable |
no-circular-requires | error | Circular requires chains where fields mutually depend on each other |
warn rules are style/perf issues you should fix. error rules are logical impossibilities — the field they affect can never be enabled, ever.
no-unknown-fields
Section titled “no-unknown-fields”Catches field names used inside rules that don’t match any key in the fields config. Typos here fail silently at runtime — check() just ignores the unknown dependency.
// ✗ — 'submitt' is not declared in fieldsconst ump = umpire({ fields: { mode: {}, details: {}, submit: {} }, rules: [ requires('submitt', 'mode'), // typo ],})
// ✓const ump = umpire({ fields: { mode: {}, details: {}, submit: {} }, rules: [ requires('submit', 'mode'), ],})no-inline-umpire-init
Section titled “no-inline-umpire-init”umpire() builds a dependency graph when called. Calling it inside a React component or hook body without useMemo rebuilds that graph on every render.
// ✗ — umpire() runs on every renderfunction CheckoutForm() { const ump = umpire({ fields: { card: {}, billing: {}, submit: {} }, rules: [requires('submit', 'card')], }) const { check } = useUmpire(ump, values) // ...}
// ✓ — defined once at module scopeconst ump = umpire({ fields: { card: {}, billing: {}, submit: {} }, rules: [requires('submit', 'card')],})
function CheckoutForm() { const { check } = useUmpire(ump, values) // ...}
// ✓ — or memoized if it depends on propsfunction CheckoutForm({ requireBilling }: Props) { const ump = useMemo(() => umpire({ fields: { card: {}, billing: {}, submit: {} }, rules: [ requires('submit', 'card'), ...(requireBilling ? [requires('submit', 'billing')] : []), ], }), [requireBilling]) // ...}no-self-disable
Section titled “no-self-disable”A field listed as both the source and a target of disables() would disable itself when it has a value — making it immediately unavailable.
// ✗ — 'promo' disables itselfconst ump = umpire({ fields: { promo: {}, discount: {} }, rules: [ disables('promo', ['promo', 'discount']), ],})
// ✓const ump = umpire({ fields: { promo: {}, discount: {} }, rules: [ disables('promo', ['discount']), ],})no-contradicting-rules
Section titled “no-contradicting-rules”Detects requires/disables pairs that make a field’s requirement permanently unsatisfiable. Two patterns are caught:
Case A — the dependency disables the requiring field: requires(A, B) + disables(B, [A]). A needs B to be available, but when B is satisfied it disables A — so A can never hold a value.
Case B — the field disables its own dependency: requires(A, B) + disables(A, [B]). A needs B, but when A is satisfied it disables B — so A’s own requirement can never hold while A has a value.
// ✗ — Case A: 'annual' requires 'plan', but 'plan' disables 'annual'const ump = umpire({ fields: { plan: {}, annual: {}, discount: {} }, rules: [ requires('annual', 'plan'), disables('plan', ['annual']), ],})
// ✓ — remove the contradicting disables, or restructure the dependencyconst ump = umpire({ fields: { plan: {}, annual: {}, discount: {} }, rules: [ requires('annual', 'plan'), disables('plan', ['discount']), ],})no-circular-requires
Section titled “no-circular-requires”Detects cycles in the requires dependency graph. If A requires B and B requires A (or a longer chain), neither field can ever satisfy its own requirement.
// ✗ — A and B mutually require each otherconst ump = umpire({ fields: { a: {}, b: {}, c: {} }, rules: [ requires('a', 'b'), requires('b', 'a'), ],})
// ✗ — three-node cycle: a → b → c → aconst ump = umpire({ fields: { a: {}, b: {}, c: {} }, rules: [ requires('a', 'b'), requires('b', 'c'), requires('c', 'a'), ],})
// ✓ — linear dependency, no cycleconst ump = umpire({ fields: { a: {}, b: {}, c: {} }, rules: [ requires('a', 'b'), requires('b', 'c'), ],})See also
Section titled “See also”umpire()construction-time checks — runtime validation that runs when the instance is created- Testing —
monkeyTest()for invariant testing across exhaustive input combinations