Skip to content

ESLint Plugin

eslint-plugin-machina surfaces machina-inspect findings inline in your editor. Three rules, one import to set up. ESLint 9 flat config only.

npm install --save-dev eslint-plugin-machina
# or
pnpm add -D eslint-plugin-machina

machina-inspect is pulled in automatically. For TypeScript files, you also need @typescript-eslint/parser:

npm install --save-dev @typescript-eslint/parser
// eslint.config.mjs
import machina from "eslint-plugin-machina";

export default [machina.configs.recommended];
// eslint.config.mjs
import tsParser from "@typescript-eslint/parser";
import machina from "eslint-plugin-machina";

export default [
    {
        files: ["src/**/*.ts"],
        languageOptions: { parser: tsParser },
    },
    machina.configs.recommended,
];
RuleDefaultTypeDescription
machina/unreachable-state"warn"problemStates with no inbound path from initialState
machina/onenter-loop"error"problemUnconditional _onEnter transition cycles
machina/missing-handler"off"suggestionStates missing handlers for inputs other states handle

Detects dead states — states that no transition leads to.

createFsm({
    id: "traffic-light",
    initialState: "green",
    states: {
        green: { timeout: "yellow" },
        yellow: { timeout: "red" },
        red: { timeout: "green" },
        broken: {}, // warning: unreachable from "green"
    },
});

Detects unconditional _onEnter transition cycles that will infinite-loop the runtime. Conditional bounces are intentional patterns and are not flagged.

createFsm({
    id: "bouncy",
    initialState: "a",
    states: {
        a: { _onEnter: () => "b" },
        b: { _onEnter: () => "a" }, // error: a -> b -> a
    },
});

Flags states that don’t handle inputs handled by other states. Off by default because many FSMs have asymmetric handlers by design. States with a * catch-all are excluded.

createFsm({
    id: "player",
    initialState: "idle",
    states: {
        idle: { start: "running" },
        running: { stop: "idle", pause: "paused" },
        paused: { resume: "running", stop: "idle" },
        // idle is missing "stop" and "pause"
    },
});

The plugin listens for createFsm() and createBehavioralFsm() call expressions, builds a StateGraph from the ESLint AST using machina-inspect’s graph IR, then runs the structural checks. Findings are reported as ESLint diagnostics at the call site.

Child FSM references are resolved when they’re inline createFsm() calls or const identifier references to such calls in the same module. Cross-module imports and let/var bindings are silently skipped — no false positives, just no analysis for those cases.