Skip to content

Lint the repository with Oxlint and dogfood the machine rules - #260

Merged
SandroMaglione merged 1 commit into
mainfrom
repository-oxlint
Nov 1, 2026
Merged

SandroMaglione merged 1 commit into
mainfrom
repository-oxlint

Conversation

@SandroMaglione

@SandroMaglione SandroMaglione commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Add Oxlint to the repository and run the repository's own Effect Machine plugin on its machines.

Formatter check. Effect-TS/effect main (pushed 2026-10-31) formats with dprint (^0.63.0) and lints with oxlint (^1.88.0): "lint": "oxlint -f unix && dprint check", with dprint.json and .oxlintrc.json at the root and no .oxfmtrc*, biome, or prettier config. Effect-TS/effect-smol and the local beta.98 mirror do the same with dprint 0.55 and oxlint 1.42. So formatting stays on dprint. This PR does not touch the dprint version; the dependency PR bumps it.

Lint setup

  • .oxlintrc.json, modeled on the ritmo config: the correctness category plus type-aware rules through oxlint-tsgolint (no-unnecessary-type-assertion, switch-exhaustiveness-check, no-floating-promises, no-misused-promises) and no-unused-vars with _ ignore patterns. Pins oxlint 1.90.0 to match the dependency-bump PR, and oxlint-tsgolint 7.0.2005.
  • pnpm lint builds @typeonce/oxlint-plugin-effect-machine (workspace:*, loaded as a jsPlugin) and then runs oxlint, so it works in a fresh clone. It runs in pnpm check right after format:check, so the CI check and latest-effect jobs both run it. I did not add a separate ci.yml step because it would run the same lint twice.
  • The plugin's current recommended rules apply to the devtools example machines (packages/devtools/src/internal/browser/{example-machine,*-example}.ts) and to packages/effect-machine/test/{fixtures,examples}. Docs snippets live in Markdown, which Oxlint cannot lint.
  • The root tsconfig.json now references tsconfig.tests.json. Without it, tsgolint (and editors) type-checked tests in an inferred project without noUncheckedIndexedAccess, which produced about 180 false "unnecessary !" reports. tsc -b and every script that reads a tsconfig are unaffected.

Rules left out or turned off, and why

  • Type tests, type-performance fixtures, and scripts/** belong to no tsconfig, so type-aware rules are off there. Type tests and perf fixtures also skip no-unused-vars: type tests bind values only to check their types, and the perf fixtures stay byte-stable so type-performance comparisons stay valid.
  • no-unused-vars is off for packages/*/src/**. Public and internal types keep phantom type parameters for variance and inference, and noUnusedLocals already checks the rest.
  • no-unnecessary-type-arguments (in ritmo's set) is left out: Effect-style code spells out Effect.Effect<A, E, never> on purpose.
  • no-redundant-type-constituents is off: unions such as unknown | StoppedError and string | "dynamic" document the intended values.
  • unicorn/no-new-array is off, as in Effect's config: the execution plan preallocates arrays.
  • no-this-alias allows self, the capture idiom for Effect.gen bodies.

Fixes (no behavior change)

  • Removed about 100 unnecessary type assertions and non-null assertions in core, devtools, React, and tests. Casts only; the emitted JavaScript stays the same apart from the items below.
  • Five assertions stay with a reasoned oxlint-disable-next-line. TypeScript 6.0.3 needs them, but the TypeScript 7 checker in tsgolint does not:
    • the TargetSelectionTypeId key,
    • two places in finiteModel.ts,
    • the ELK CommonJS constructor,
    • one testing-library query.
      The as never in Paths.test.ts also stays: TypeScript 6 rejects the reserved root state name there, but TypeScript 7 accepts it. That may be a type-level gap under TS 7 worth checking in the TS 7 lane.
  • Other fixes:
    • ({}) => to () =>, and { input: input } to { input }.
    • Removed ...{ literal } and array spreads that did nothing.
    • ...(values ?? {}) to ...values.
    • Two useless regex escapes in the site generator.
    • String(src) in an invocation error message (a symbol in a template literal would throw).
    • An explicit default in the schema-arbitrary visitor, and a missing active case in a test switch.
    • await act(async () => …) in the React tests.
    • Explicit fields instead of spreading a class instance.
    • An optional parameter instead of = undefined.
  • One plugin finding is suppressed: invoke-outcomes-example.ts selects a titled none branch from an onSnapshot resolver. Machine.can does not query snapshot outcomes, so this is a false positive. Check no-conditional-none-branch only on event handlers #257 fixes it by limiting no-conditional-none-branch to on handlers. The suppression can go once Check no-conditional-none-branch only on event handlers #257 merges.

Coordination

Changeset

  • Added an empty changeset. The root manifest changed, which the changeset policy requires, but no package release is needed: the source edits only remove type assertions and do not change behavior.
  • Not required because this PR does not change a publishable package

Validation

  • pnpm check: format, lint, architecture, CI scripts, API docs, site docs, typecheck, build, 901 tests, type tests (246 tests and 1,299 assertions in core, 35 assertions in React), consumer, and all four pack checks passed.
  • Fresh clone (git clone + pnpm install --frozen-lockfile + pnpm lint): builds the plugin, then lints 318 files with 119 rules, exit 0. pnpm lint takes about 8–9 s end to end (oxlint alone 5.5–15 s, depending on machine load).
  • latest-effect simulation (scripts/prepare-effect-compatibility.mjs, Effect 4.1.1): oxlint and pnpm typecheck both pass.
  • Automated type-performance measurement passed or was not required
  • Automated runtime- and memory-performance measurement passed or was not required (no runtime change; the casts are erased)

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Type performance

Measured with TypeScript 6.0.3 and skipLibCheck=true.

Scenario Base PR Difference
Effect only 55 55 0 (0.0%)
Import effect-machine 55 55 0 (0.0%)
Machine.state (3 children) 2,664 2,664 0 (0.0%)
Machine.make (3 states, 2 events) 8,932 8,932 0 (0.0%)
machine.handle (3 states, 2 transitions) 39,449 39,449 0 (0.0%)
declared transition (10 named branches) 60,402 60,402 0 (0.0%)
atomic construction, guards, and reentry 85,710 85,710 0 (0.0%)
registered invocation (input-taking Effect) 39,819 39,819 0 (0.0%)
machine.handle (depth 24) 194,531 194,531 0 (0.0%)
machine.handle (wide depth 16) 218,238 218,238 0 (0.0%)
machine.handle (parallel/history/choice) 91,642 91,642 0 (0.0%)
machine definition (3 independent implementations) 99,173 99,173 0 (0.0%)
machine exact input/output/error/services 82,941 82,941 0 (0.0%)
execution adapter readiness 71,428 71,428 0 (0.0%)

Marginal instantiations are measured against the matching setup without that API call:

Scenario Base PR Difference
Import effect-machine 0 0 0
Machine.state (3 children) 2,609 2,609 0 (0.0%)
Machine.make (3 states, 2 events) 6,260 6,260 0 (0.0%)
machine.handle (3 states, 2 transitions) 30,517 30,517 0 (0.0%)
declared transition (10 named branches) 51,849 51,849 0 (0.0%)
atomic construction, guards, and reentry 70,370 70,370 0 (0.0%)
registered invocation (input-taking Effect) 32,682 32,682 0 (0.0%)
machine.handle (depth 24) 177,222 177,222 0 (0.0%)
machine.handle (wide depth 16) 201,707 201,707 0 (0.0%)
machine.handle (parallel/history/choice) 79,144 79,144 0 (0.0%)
machine definition (3 independent implementations) 88,492 88,492 0 (0.0%)
machine exact input/output/error/services 70,732 70,732 0 (0.0%)
execution adapter readiness 31,922 31,922 0 (0.0%)
Check times (informational)
Scenario Base PR
Effect only 0.03s 0.03s
Import effect-machine 0.03s 0.03s
Machine.state (3 children) 0.09s 0.10s
Machine.make (3 states, 2 events) 0.17s 0.16s
machine.handle (3 states, 2 transitions) 0.31s 0.31s
declared transition (10 named branches) 0.43s 0.41s
atomic construction, guards, and reentry 0.56s 0.52s
registered invocation (input-taking Effect) 0.33s 0.31s
machine.handle (depth 24) 0.72s 0.65s
machine.handle (wide depth 16) 0.72s 0.72s
machine.handle (parallel/history/choice) 0.54s 0.53s
machine definition (3 independent implementations) 0.57s 0.56s
machine exact input/output/error/services 0.52s 0.51s
execution adapter readiness 0.45s 0.44s

Type instantiations are the comparison metric. Check time varies with runner load and is informational only.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Runtime performance

Median of 5 independent benchmark processes on Intel(R) Xeon(R) 6973P-C with Node v24.22.1.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 170,455 transitions/s
Drain burst with terminal fence 612,732 increments/s
Drain burst with a change observer 557,123 increments/s
Lookup and send to one child 502,568 increments/s
Start and stop a machine 175,871 machines/s
Start and stop a parent with one child 52,010 families/s
Complete an Effect invocation with tracing enabled 18,619 invocations/s
Complete an Effect invocation with tracing disabled 21,253 invocations/s
Plan transitions through a compound state 149,808 transitions/s
Plan transitions through parallel regions 120,598 transitions/s
Drain burst through a compound state 520,877 events/s
Drain burst through two parallel regions 467,734 events/s
Drain a compound-state burst with a change observer 472,369 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 45,612 processes/s
Start and stop a raw compiled process 105,363 processes/s
Memory profile Effect Machine
Idle machine 1.9 KiB
Raw generic managed process 11.4 KiB
Raw compiled process 3.1 KiB
Two independent idle machines 3.6 KiB
Idle parent with one child 6.0 KiB
Parent with observed child registry 9.6 KiB
Parent with observed invoked child snapshots 6.9 KiB

Effect Machine change from base

Metric Base Base variability PR PR variability Difference
Plan counter transitions 169,066 transitions/s 0.3% MAD 170,455 transitions/s 0.5% MAD +0.8%
Drain burst with terminal fence 631,867 increments/s 1.4% MAD 612,732 increments/s 0.9% MAD -3.0%
Drain burst with a change observer 579,900 increments/s 0.9% MAD 557,123 increments/s 1.3% MAD -3.9%
Lookup and send to one child 515,278 increments/s 0.1% MAD 502,568 increments/s 2.4% MAD -2.5%
Start and stop a machine 177,085 machines/s 0.4% MAD 175,871 machines/s 0.2% MAD -0.7%
Start and stop a parent with one child 52,832 families/s 0.8% MAD 52,010 families/s 1.3% MAD -1.6%
Complete an Effect invocation with tracing enabled 18,839 invocations/s 0.6% MAD 18,619 invocations/s 1.2% MAD -1.2%
Complete an Effect invocation with tracing disabled 21,396 invocations/s 0.6% MAD 21,253 invocations/s 0.9% MAD -0.7%
Plan transitions through a compound state 149,769 transitions/s 0.4% MAD 149,808 transitions/s 0.7% MAD +0.0%
Plan transitions through parallel regions 119,024 transitions/s 1.0% MAD 120,598 transitions/s 1.4% MAD +1.3%
Drain burst through a compound state 507,521 events/s 0.4% MAD 520,877 events/s 0.7% MAD +2.6%
Drain burst through two parallel regions 470,740 events/s 0.6% MAD 467,734 events/s 0.5% MAD -0.6%
Drain a compound-state burst with a change observer 468,749 events/s 0.9% MAD 472,369 events/s 2.3% MAD +0.8%
Idle machine heap per unit 1.9 KiB 0.6% MAD 1.9 KiB 0.2% MAD -0.3%
Raw generic managed process heap per unit 11.4 KiB 0.0% MAD 11.4 KiB 0.0% MAD +0.0%
Raw compiled process heap per unit 3.2 KiB 0.1% MAD 3.1 KiB 0.1% MAD -0.9%
Two independent idle machines heap per unit 3.6 KiB 0.0% MAD 3.6 KiB 0.0% MAD -0.0%
Idle parent with one child heap per unit 6.0 KiB 0.0% MAD 6.0 KiB 0.1% MAD +0.1%
Parent with observed child registry heap per unit 9.6 KiB 0.0% MAD 9.6 KiB 0.0% MAD +0.0%
Parent with observed invoked child snapshots heap per unit 6.9 KiB 0.1% MAD 6.9 KiB 0.2% MAD -0.0%

Process runtime reference change from base

Metric Base Base variability PR PR variability Difference
Start and stop a raw generic process 45,455 processes/s 1.1% MAD 45,612 processes/s 0.5% MAD +0.3%
Start and stop a raw compiled process 106,236 processes/s 0.6% MAD 105,363 processes/s 0.4% MAD -0.8%

Regression guard

No large, noise-adjusted throughput or heap regressions detected.

Versions and interpretation
  • Effect Machine: 0.46.0

Higher throughput is better; lower heap is better. Variability is the median absolute deviation across independent processes, relative to their median. Small differences on shared GitHub-hosted hardware remain informational; the required guard rejects only large changes beyond the measured noise allowance.

Add `pnpm lint` to `pnpm check`. It builds the local
@typeonce/oxlint-plugin-effect-machine package and runs Oxlint 1.90.0
with type-aware rules through oxlint-tsgolint. The plugin's rules check
the devtools example machines and the machine test fixtures. The root
tsconfig references the test project so type-aware rules and editors use
the repository's compiler options for tests.

Fix the findings: remove unnecessary type assertions, empty parameter
patterns, redundant renames and spreads, useless regex escapes, and add
the missing switch cases. The few assertions that TypeScript 6 still
requires keep a justified suppression. Formatting stays on dprint, which
Effect also uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SandroMaglione
SandroMaglione merged commit 96d7382 into main Nov 1, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the repository-oxlint branch November 1, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant