feat(lint): ban try/catch and non-null assertions, clean packages/ and lib/ - #687
Open
Makisuo wants to merge 1 commit into
Open
feat(lint): ban try/catch and non-null assertions, clean packages/ and lib/#687Makisuo wants to merge 1 commit into
Makisuo wants to merge 1 commit into
Conversation
…d lib/ A thrown exception is invisible to the type system, so it escapes the typed error channel the codebase relies on, and one `catch` block flattens every failure into a single branch. `maple/no-try-catch` makes that a lint error; oxlint has no `no-restricted-syntax`, so banning a statement kind takes a plugin rule. `typescript/no-non-null-assertion` is the built-in twin. Both land at `error` with three overrides: - Tests are off for both. A test asserts on a fixture it just built, and a test driving a throwing boundary needs `catch` to observe it. - `packages/browser`, `packages/browser-session`, and `packages/clickhouse-cli` are off for `no-try-catch`: they carry no Effect dependency at all (the two browser SDKs ship to customers under a bundle budget), so the primitive the rule points at does not exist there. - `apps/**`, `scripts/**`, `**/scripts/**`, and `examples/**` are off for both as a burndown list — ~460 `try`/`catch` blocks and ~500 assertions remain there. Drop entries as each is converted rather than widening the list. `packages/*` and `lib/*` source is now at zero for both: 38 `try`/`catch` blocks and ~110 assertions converted. Rather than 38 inline conversions, the repeated shapes got a seam: `trySync`/`tryPromise` in `packages/ui/src/lib/try-sync.ts` and `packages/effect-sdk/src/shared/try-sync.ts` run a throwing host call behind an `Option`; `local-storage.ts` puts the read and write of a preference behind the same guard; `guardFlush` in the SDK's `flush-core.ts` replaces the identical never-reject wrapper the client, server, and Cloudflare presets each kept. Parsing moved to `Schema.fromJsonString`, `Schema.URLFromString`, and `Schema.decodeUnknownOption` where a schema was the honest answer. Nearly every prod assertion was a `noUncheckedIndexedAccess` artifact rather than an unsafe cast, so the fixes make the invariant real instead of adding Option plumbing: `Drain` swaps two mutually-exclusive nullable stores for one union field; `pipe-dispatch`'s `int(key, def?)` is overloaded so a defaulted call is a `number`; `soleValue` and `childAt` replace the eight `length === 1` branches and nine has-then-get pairs; the LRU's `entryAt` names a broken slot instead of surfacing `undefined` two frames later. Two latent bugs closed on the way: an attribute filter over an empty key list emitted `has(…, NULL)`, and the executor's capability-aware compile now falls back to baseline capabilities instead of passing `undefined` through.
Makisuo
force-pushed
the
claude/oxlint-try-catch-ban-a83eb2
branch
from
August 30, 2026 00:17
15c9a7c to
6a0cfdf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two new lint bans, plus the burndown that makes them land green in shared code.
maple/no-try-catch— a new rule inscripts/oxlint-plugins/maple.mjs, firing onTryStatement. oxlint has nono-restricted-syntax, so banning a statement kind takes a plugin rule. The message names the primitive per case:Effect.try/tryPromise,Schema.fromJsonString,Schema.decodeUnknown{Effect,Option,Sync},Effect.catch/catchTag/catchDefect,Effect.ensuringfor afinally.typescript/no-non-null-assertion— the oxlint built-in, no plugin needed.packages/*andlib/*source is now at zero for both: 38try/catchblocks and ~110 assertions converted.Why
A thrown exception is invisible to the type system, so it escapes the typed error channel the codebase relies on, and one
catchblock flattens every failure into a single branch — discarding exactly the distinction the tagged-error convention exists to preserve.Scope, and why it is staged
I measured before wiring anything up. Repo-wide there are ~660
tryblocks and 2060!, of which 1429 assertions are in tests. Both rules are"error"with three overrides:!there documents the fixture; a test driving a throwing boundary needscatchto observe it.packages/browser,packages/browser-session,packages/clickhouse-cli—no-try-catchoff. These carry no Effect dependency at all; the two browser SDKs ship to customers under a bundle budget, sotry/catchis the only error handling available to them. Worth knowing before anyone widens the rule: checkpackage.jsonfirst.apps/**,scripts/**,**/scripts/**,examples/**,alchemy.run.tsoff for both. ~460try/catchand ~500 assertions remain there. Leaving them on would landbun run lintwith ~960 errors; the comment in the config says to shrink the list, not widen it.How the fixes were done
Rather than 38 inline conversions, the repeated shapes got a seam:
packages/ui/src/lib/try-sync.tsandpackages/effect-sdk/src/shared/try-sync.ts—Effect.try+Effect.optionbehind anOption, for host calls that throw instead of returning (JSON.parse,localStorage,execCommand,Intl,BigInt,decodeURIComponent).packages/ui/src/lib/local-storage.ts— the read and the write of a preference behind one guard.guardFlushinpackages/effect-sdk/src/shared/flush-core.ts— replaces the identical never-reject wrapper the client, server, and Cloudflare presets each kept a copy of.Schema.fromJsonString/Schema.URLFromString/Schema.decodeUnknownOptionwherever a schema was the honest answer.Almost every prod assertion turned out to be a
noUncheckedIndexedAccessartifact (arr[i]!,map.get(k)!,match[1]!) rather than an unsafe cast, so the fixes make the invariant real instead of adding Option plumbing:Drainheld two mutually-exclusive nullable stores; now one union field, which also let a test drop a cast into internals in favour of a publicclusters().pipe-dispatch'sint(key, def?)returnednumber | undefinedfor every defaulted call — overloading it removed six assertions with zero call-site edits.soleValue()inquery-helpers.tsfor the eightlength === 1 ? values[0]! : …branches;childAt()for the nine has-then-get pairs in the prefix-tree walk.entryAt()names a broken slot (LruSlotError) instead of surfacingundefinedtwo frames later.Two latent bugs closed on the way
traces-shared.ts: an attribute filter over an empty key list built its index prefilter fromkeys[0]!, emittinghas(…, NULL). It now falls back to the exact predicate.execution/executor.ts: the capability-aware compile passedcapabilities!through; it now falls back tobaselineWarehouseCapabilities(), which generates the widest SQL.Verification
bun run lintclean (both.oxlintrc.jsonand the type-aware.oxlintrc.effect.json).bun run typecheck— 39/39.Note for the reviewer
packages/domain'sproject-manifest.test.ts > keeps a stable project revision for unchanged inputsflakes roughly 1 run in 3 under the full suite and passes in isolation. It is not in this change's import graph and reproduces without it — pre-existing, and worth a separate look.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.