Skip to content

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.

ESLint≥ 9.0.0
OxlintJS plugins (jsPlugins)
Config formatFlat config only (eslint.config.js) — legacy .eslintrc is not supported
NodeWhatever 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.

Terminal window
npm install --save-dev @umpire/eslint-plugin

Add 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',
},
},
]

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.

RuleSeverityWhat it catches
no-unknown-fieldswarnField names in rules that aren’t declared in fields
no-inline-umpire-initwarnumpire() called inside a component or hook body without useMemo
no-self-disableerrorA field listed as both source and target of disables()
no-contradicting-ruleserrorrequires/disables pairs that make a field permanently unavailable
no-circular-requireserrorCircular 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.


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 fields
const ump = umpire({
fields: { mode: {}, details: {}, submit: {} },
rules: [
requires('submitt', 'mode'), // typo
],
})
// ✓
const ump = umpire({
fields: { mode: {}, details: {}, submit: {} },
rules: [
requires('submit', 'mode'),
],
})

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 render
function CheckoutForm() {
const ump = umpire({
fields: { card: {}, billing: {}, submit: {} },
rules: [requires('submit', 'card')],
})
const { check } = useUmpire(ump, values)
// ...
}
// ✓ — defined once at module scope
const ump = umpire({
fields: { card: {}, billing: {}, submit: {} },
rules: [requires('submit', 'card')],
})
function CheckoutForm() {
const { check } = useUmpire(ump, values)
// ...
}
// ✓ — or memoized if it depends on props
function CheckoutForm({ requireBilling }: Props) {
const ump = useMemo(() => umpire({
fields: { card: {}, billing: {}, submit: {} },
rules: [
requires('submit', 'card'),
...(requireBilling ? [requires('submit', 'billing')] : []),
],
}), [requireBilling])
// ...
}

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 itself
const ump = umpire({
fields: { promo: {}, discount: {} },
rules: [
disables('promo', ['promo', 'discount']),
],
})
// ✓
const ump = umpire({
fields: { promo: {}, discount: {} },
rules: [
disables('promo', ['discount']),
],
})

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 dependency
const ump = umpire({
fields: { plan: {}, annual: {}, discount: {} },
rules: [
requires('annual', 'plan'),
disables('plan', ['discount']),
],
})

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 other
const ump = umpire({
fields: { a: {}, b: {}, c: {} },
rules: [
requires('a', 'b'),
requires('b', 'a'),
],
})
// ✗ — three-node cycle: a → b → c → a
const ump = umpire({
fields: { a: {}, b: {}, c: {} },
rules: [
requires('a', 'b'),
requires('b', 'c'),
requires('c', 'a'),
],
})
// ✓ — linear dependency, no cycle
const ump = umpire({
fields: { a: {}, b: {}, c: {} },
rules: [
requires('a', 'b'),
requires('b', 'c'),
],
})