| author | |
| committer | |
| log | ff397a29523d3c63e5844b11608c0189640c7561 |
| tree | bd5179ac2bcd68f267b03c130c2adc7a8b6336d8 |
| parent | fad673225650193fad2a85487ecffc903f8ff30a |
| signature | Signed by PGP key B5690EEEBB952194 |
21 files changed, 220 insertions(+), 85 deletions(-)
apps/desktop/src/backend/DesktopBackendManager.ts+1-1| ... | @@ -423,7 +423,7 @@ function drainBackendOutput( | ... | @@ -423,7 +423,7 @@ function drainBackendOutput( |
| 423 | cause, | 423 | cause, |
| 424 | }), | 424 | }), |
| 425 | ), | 425 | ), |
| 426 | Effect.catchTag("BackendProcessOutputHandlingError", onOutputFailure), | 426 | Effect.catchTags({ BackendProcessOutputHandlingError: onOutputFailure }), |
| 427 | ), | 427 | ), |
| 428 | ), | 428 | ), |
| 429 | Effect.catchTags({ | 429 | Effect.catchTags({ |
apps/server/src/auth/http.ts+5-4| ... | @@ -223,7 +223,7 @@ export const layerAuthenticatedAuth = Layer.effect( | ... | @@ -223,7 +223,7 @@ export const layerAuthenticatedAuth = Layer.effect( |
| 223 | }), | 223 | }), |
| 224 | session.subject === "cloud-connect" ? traceAuthenticatedRelayRequest : identity, | 224 | session.subject === "cloud-connect" ? traceAuthenticatedRelayRequest : identity, |
| 225 | ); | 225 | ); |
| 226 | }).pipe(Effect.catchTag("EnvironmentAuthInvalidError", appendDpopChallengeOnUnauthorized)); | 226 | }).pipe(Effect.catchTags({ EnvironmentAuthInvalidError: appendDpopChallengeOnUnauthorized })); |
| 227 | }), | 227 | }), |
| 228 | ); | 228 | ); |
| 229 | 229 | ||
| ... | @@ -474,9 +474,10 @@ export const layer = HttpApiBuilder.group( | ... | @@ -474,9 +474,10 @@ export const layer = HttpApiBuilder.group( |
| 474 | ); | 474 | ); |
| 475 | return { revoked }; | 475 | return { revoked }; |
| 476 | }, | 476 | }, |
| 477 | Effect.catchTag("ServerAuthForbiddenOperationError", () => | 477 | Effect.catchTags({ |
| 478 | failEnvironmentOperationForbidden("current_session_revoke_not_allowed"), | 478 | ServerAuthForbiddenOperationError: () => |
| 479 | ), | 479 | failEnvironmentOperationForbidden("current_session_revoke_not_allowed"), |
| 480 | }), | ||
| 480 | Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => | 481 | Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => |
| 481 | failEnvironmentInternal("client_session_revoke_failed", error), | 482 | failEnvironmentInternal("client_session_revoke_failed", error), |
| 482 | ), | 483 | ), |
apps/server/src/cli/connect.ts+6-5| ... | @@ -105,11 +105,12 @@ const authorizeCli = Effect.fn("cloud.cli.authorize")(function* (options: { | ... | @@ -105,11 +105,12 @@ const authorizeCli = Effect.fn("cloud.cli.authorize")(function* (options: { |
| 105 | // A stored credential whose refresh fails (revoked, expired grant) must | 105 | // A stored credential whose refresh fails (revoked, expired grant) must |
| 106 | // fall through to a fresh device authorization, not dead-end the command. | 106 | // fall through to a fresh device authorization, not dead-end the command. |
| 107 | const existing = yield* tokens.getExisting.pipe( | 107 | const existing = yield* tokens.getExisting.pipe( |
| 108 | Effect.catchTag("CloudCliCredentialRefreshError", () => | 108 | Effect.catchTags({ |
| 109 | Console.log( | 109 | CloudCliCredentialRefreshError: () => |
| 110 | "The stored T3 Connect credential could not be refreshed; signing in again.", | 110 | Console.log( |
| 111 | ).pipe(Effect.as(Option.none())), | 111 | "The stored T3 Connect credential could not be refreshed; signing in again.", |
| 112 | ), | 112 | ).pipe(Effect.as(Option.none())), |
| 113 | }), | ||
| 113 | ); | 114 | ); |
| 114 | if (Option.isSome(existing)) { | 115 | if (Option.isSome(existing)) { |
| 115 | return existing.value.identity ?? null; | 116 | return existing.value.identity ?? null; |
apps/server/src/cli/uninstall.ts+1-1| ... | @@ -162,7 +162,7 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: { | ... | @@ -162,7 +162,7 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: { |
| 162 | } | 162 | } |
| 163 | const confirmed = yield* Prompt.run( | 163 | const confirmed = yield* Prompt.run( |
| 164 | Prompt.Confirm({ message: "Remove t3 from this machine?", initial: false }), | 164 | Prompt.Confirm({ message: "Remove t3 from this machine?", initial: false }), |
| 165 | ).pipe(Effect.catchTag("QuitError", () => Effect.succeed(false))); | 165 | ).pipe(Effect.catchTags({ QuitError: () => Effect.succeed(false) })); |
| 166 | if (!confirmed) { | 166 | if (!confirmed) { |
| 167 | yield* Console.log("Left as is."); | 167 | yield* Console.log("Left as is."); |
| 168 | return; | 168 | return; |
apps/server/src/cli/update.ts+2-2| ... | @@ -384,7 +384,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: { | ... | @@ -384,7 +384,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: { |
| 384 | } | 384 | } |
| 385 | const confirmed = yield* Prompt.run( | 385 | const confirmed = yield* Prompt.run( |
| 386 | Prompt.Confirm({ message: "Install the preview build anyway?", initial: false }), | 386 | Prompt.Confirm({ message: "Install the preview build anyway?", initial: false }), |
| 387 | ).pipe(Effect.catchTag("QuitError", () => Effect.succeed(false))); | 387 | ).pipe(Effect.catchTags({ QuitError: () => Effect.succeed(false) })); |
| 388 | if (!confirmed) { | 388 | if (!confirmed) { |
| 389 | yield* Console.log("Left as is."); | 389 | yield* Console.log("Left as is."); |
| 390 | return; | 390 | return; |
| ... | @@ -473,7 +473,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: { | ... | @@ -473,7 +473,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: { |
| 473 | message: "Restart the background service once the download is verified?", | 473 | message: "Restart the background service once the download is verified?", |
| 474 | initial: true, | 474 | initial: true, |
| 475 | }), | 475 | }), |
| 476 | ).pipe(Effect.catchTag("QuitError", () => Effect.succeed(false))); | 476 | ).pipe(Effect.catchTags({ QuitError: () => Effect.succeed(false) })); |
| 477 | } else { | 477 | } else { |
| 478 | yield* Console.log( | 478 | yield* Console.log( |
| 479 | " Not a terminal, so the service keeps running its current version. Rerun with --yes to restart it now, or run `t3 service restart` later.", | 479 | " Not a terminal, so the service keeps running its current version. Rerun with --yes to restart it now, or run `t3 service restart` later.", |
apps/server/src/cloud/CliTokenManager.ts+6-6| ... | @@ -408,9 +408,9 @@ export const deviceAuthorizationLogin = Effect.fn("cloud.cli_token.device_author | ... | @@ -408,9 +408,9 @@ export const deviceAuthorizationLogin = Effect.fn("cloud.cli_token.device_author |
| 408 | }); | 408 | }); |
| 409 | return yield* pollDeviceToken(metadata, authorization.device_code, interval).pipe( | 409 | return yield* pollDeviceToken(metadata, authorization.device_code, interval).pipe( |
| 410 | Effect.timeout(expiresIn), | 410 | Effect.timeout(expiresIn), |
| 411 | Effect.catchTag("TimeoutError", (cause) => | 411 | Effect.catchTags({ |
| 412 | Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })), | 412 | TimeoutError: (cause) => Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })), |
| 413 | ), | 413 | }), |
| 414 | ); | 414 | ); |
| 415 | }, | 415 | }, |
| 416 | ); | 416 | ); |
| ... | @@ -505,9 +505,9 @@ export const make = Effect.gen(function* () { | ... | @@ -505,9 +505,9 @@ export const make = Effect.gen(function* () { |
| 505 | authorizationUrl, | 505 | authorizationUrl, |
| 506 | callback: Deferred.await(callback).pipe( | 506 | callback: Deferred.await(callback).pipe( |
| 507 | Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT), | 507 | Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT), |
| 508 | Effect.catchTag("TimeoutError", (cause) => | 508 | Effect.catchTags({ |
| 509 | Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })), | 509 | TimeoutError: (cause) => Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })), |
| 510 | ), | 510 | }), |
| 511 | ), | 511 | ), |
| 512 | terminal, | 512 | terminal, |
| 513 | launchBrowser: externalLauncher.launchBrowser, | 513 | launchBrowser: externalLauncher.launchBrowser, |
apps/server/src/cloud/CloudLink.ts+3-3| ... | @@ -681,7 +681,7 @@ const make = Effect.gen(function* () { | ... | @@ -681,7 +681,7 @@ const make = Effect.gen(function* () { |
| 681 | return proof satisfies RelayEnvironmentLinkProof; | 681 | return proof satisfies RelayEnvironmentLinkProof; |
| 682 | }, | 682 | }, |
| 683 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("generate-link-proof")), | 683 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("generate-link-proof")), |
| 684 | Effect.catchTag("PlatformError", internalError("generate-link-proof")), | 684 | Effect.catchTags({ PlatformError: internalError("generate-link-proof") }), |
| 685 | ); | 685 | ); |
| 686 | 686 | ||
| 687 | const activateManagedTunnel = Effect.fn("environment.cloud.activateManagedTunnel")( | 687 | const activateManagedTunnel = Effect.fn("environment.cloud.activateManagedTunnel")( |
| ... | @@ -1546,7 +1546,7 @@ const make = Effect.gen(function* () { | ... | @@ -1546,7 +1546,7 @@ const make = Effect.gen(function* () { |
| 1546 | } satisfies RelayEnvironmentHealthResponse; | 1546 | } satisfies RelayEnvironmentHealthResponse; |
| 1547 | }, | 1547 | }, |
| 1548 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("answer-health")), | 1548 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("answer-health")), |
| 1549 | Effect.catchTag("PlatformError", internalError("answer-health")), | 1549 | Effect.catchTags({ PlatformError: internalError("answer-health") }), |
| 1550 | ); | 1550 | ); |
| 1551 | 1551 | ||
| 1552 | const mintCredential = Effect.fn("environment.cloud.mintCredential")( | 1552 | const mintCredential = Effect.fn("environment.cloud.mintCredential")( |
| ... | @@ -1653,7 +1653,7 @@ const make = Effect.gen(function* () { | ... | @@ -1653,7 +1653,7 @@ const make = Effect.gen(function* () { |
| 1653 | } satisfies RelayEnvironmentMintResponse; | 1653 | } satisfies RelayEnvironmentMintResponse; |
| 1654 | }, | 1654 | }, |
| 1655 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("issue-credential")), | 1655 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("issue-credential")), |
| 1656 | Effect.catchTag("PlatformError", internalError("issue-credential")), | 1656 | Effect.catchTags({ PlatformError: internalError("issue-credential") }), |
| 1657 | ); | 1657 | ); |
| 1658 | 1658 | ||
| 1659 | return CloudLink.of({ | 1659 | return CloudLink.of({ |
apps/server/src/htmlRender/HtmlRender.ts+20-18| ... | @@ -351,26 +351,28 @@ const make = Effect.gen(function* () { | ... | @@ -351,26 +351,28 @@ const make = Effect.gen(function* () { |
| 351 | browsers.withPermits(1)( | 351 | browsers.withPermits(1)( |
| 352 | // Read once a permit is held, so a queued call sees a fallback learned meanwhile. | 352 | // Read once a permit is held, so a queued call sees a fallback learned meanwhile. |
| 353 | Effect.suspend(() => run(noSandbox)).pipe( | 353 | Effect.suspend(() => run(noSandbox)).pipe( |
| 354 | Effect.catchTag("HtmlRenderSandboxUnavailableError", () => | 354 | Effect.catchTags({ |
| 355 | Effect.logInfo( | 355 | HtmlRenderSandboxUnavailableError: () => |
| 356 | "Chrome's sandbox is unavailable on this host; launching it without one.", | 356 | Effect.logInfo( |
| 357 | ).pipe( | 357 | "Chrome's sandbox is unavailable on this host; launching it without one.", |
| 358 | Effect.andThen( | 358 | ).pipe( |
| 359 | Effect.sync(() => { | 359 | Effect.andThen( |
| 360 | noSandbox = true; | 360 | Effect.sync(() => { |
| 361 | noSandbox = true; | ||
| 362 | }), | ||
| 363 | ), | ||
| 364 | Effect.andThen(run(true)), | ||
| 365 | ), | ||
| 366 | }), | ||
| 367 | Effect.catchTags({ | ||
| 368 | HtmlRenderSandboxUnavailableError: (cause) => | ||
| 369 | Effect.fail( | ||
| 370 | new HeadlessChrome.HtmlRenderBrowserError({ | ||
| 371 | reason: "the browser has no sandbox", | ||
| 372 | cause, | ||
| 361 | }), | 373 | }), |
| 362 | ), | 374 | ), |
| 363 | Effect.andThen(run(true)), | 375 | }), |
| 364 | ), | ||
| 365 | ), | ||
| 366 | Effect.catchTag("HtmlRenderSandboxUnavailableError", (cause) => | ||
| 367 | Effect.fail( | ||
| 368 | new HeadlessChrome.HtmlRenderBrowserError({ | ||
| 369 | reason: "the browser has no sandbox", | ||
| 370 | cause, | ||
| 371 | }), | ||
| 372 | ), | ||
| 373 | ), | ||
| 374 | ), | 376 | ), |
| 375 | ); | 377 | ); |
| 376 | 378 |
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts+12-10| ... | @@ -3011,16 +3011,18 @@ export function makeOpenCodeAdapterV2( | ... | @@ -3011,16 +3011,18 @@ export function makeOpenCodeAdapterV2( |
| 3011 | ); | 3011 | ); |
| 3012 | yield* Effect.raceFirst(Fiber.join(request), Deferred.await(receipt)).pipe( | 3012 | yield* Effect.raceFirst(Fiber.join(request), Deferred.await(receipt)).pipe( |
| 3013 | Effect.timeout("10 seconds"), | 3013 | Effect.timeout("10 seconds"), |
| 3014 | Effect.catchTag("TimeoutError", (cause) => { | 3014 | Effect.catchTags({ |
| 3015 | const error = new OpenCodeRuntime.OpenCodeRuntimeError({ | 3015 | TimeoutError: (cause) => { |
| 3016 | operation: "session.command", | 3016 | const error = new OpenCodeRuntime.OpenCodeRuntimeError({ |
| 3017 | detail: "OpenCode command admission did not complete within 10 seconds.", | 3017 | operation: "session.command", |
| 3018 | cause, | 3018 | detail: "OpenCode command admission did not complete within 10 seconds.", |
| 3019 | }); | 3019 | cause, |
| 3020 | abortController.abort(); | 3020 | }); |
| 3021 | return finalizeTurn(state, turn, "failed", { | 3021 | abortController.abort(); |
| 3022 | failure: makeProviderFailure({ cause: error, class: "provider_error" }), | 3022 | return finalizeTurn(state, turn, "failed", { |
| 3023 | }).pipe(Effect.andThen(Effect.fail(error))); | 3023 | failure: makeProviderFailure({ cause: error, class: "provider_error" }), |
| 3024 | }).pipe(Effect.andThen(Effect.fail(error))); | ||
| 3025 | }, | ||
| 3024 | }), | 3026 | }), |
| 3025 | ); | 3027 | ); |
| 3026 | }); | 3028 | }); |
apps/server/src/provider/acp/AcpRegistrySupport.ts+3-4| ... | @@ -1916,16 +1916,15 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct | ... | @@ -1916,16 +1916,15 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct |
| 1916 | ), | 1916 | ), |
| 1917 | ) | 1917 | ) |
| 1918 | .pipe( | 1918 | .pipe( |
| 1919 | Effect.catchTag( | 1919 | Effect.catchTags({ |
| 1920 | "ServerSettingsError", | 1920 | ServerSettingsError: (cause) => |
| 1921 | (cause) => | ||
| 1922 | new AcpRegistryError({ | 1921 | new AcpRegistryError({ |
| 1923 | reason: "install_failed", | 1922 | reason: "install_failed", |
| 1924 | detail: | 1923 | detail: |
| 1925 | "Could not read provider settings while checking managed ACP binary references.", | 1924 | "Could not read provider settings while checking managed ACP binary references.", |
| 1926 | cause, | 1925 | cause, |
| 1927 | }), | 1926 | }), |
| 1928 | ), | 1927 | }), |
| 1929 | ); | 1928 | ); |
| 1930 | 1929 | ||
| 1931 | return AcpRegistryCatalog.of({ | 1930 | return AcpRegistryCatalog.of({ |
apps/server/src/pullRequest/PullRequestService.ts+1-1| ... | @@ -1578,7 +1578,7 @@ export const make = Effect.gen(function* () { | ... | @@ -1578,7 +1578,7 @@ export const make = Effect.gen(function* () { |
| 1578 | ? operation.pipe(Effect.provideService(routingCredential, identity), Effect.result) | 1578 | ? operation.pipe(Effect.provideService(routingCredential, identity), Effect.result) |
| 1579 | : Effect.fail(rejected()), | 1579 | : Effect.fail(rejected()), |
| 1580 | ) | 1580 | ) |
| 1581 | .pipe(Effect.catchTag("PullRequestProviderError", () => Effect.fail(rejected()))); | 1581 | .pipe(Effect.catchTags({ PullRequestProviderError: () => Effect.fail(rejected()) })); |
| 1582 | return yield* Effect.fromResult(result); | 1582 | return yield* Effect.fromResult(result); |
| 1583 | }); | 1583 | }); |
| 1584 | 1584 |
apps/server/src/workspace/WorkspaceSearchIndex.ts+8-7| ... | @@ -376,13 +376,14 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( | ... | @@ -376,13 +376,14 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( |
| 376 | cause, | 376 | cause, |
| 377 | }), | 377 | }), |
| 378 | ).pipe( | 378 | ).pipe( |
| 379 | Effect.catchTag("WorkspaceSearchIndexScanTimedOut", (error) => | 379 | Effect.catchTags({ |
| 380 | variant === "paths" | 380 | WorkspaceSearchIndexScanTimedOut: (error) => |
| 381 | ? Effect.sync(() => { | 381 | variant === "paths" |
| 382 | initialScanTimedOut = true; | 382 | ? Effect.sync(() => { |
| 383 | }) | 383 | initialScanTimedOut = true; |
| 384 | : Effect.fail(error), | 384 | }) |
| 385 | ), | 385 | : Effect.fail(error), |
| 386 | }), | ||
| 386 | ); | 387 | ); |
| 387 | 388 | ||
| 388 | const hasIncompleteInitialScan = () => initialScanTimedOut && finder.isScanning(); | 389 | const hasIncompleteInitialScan = () => initialScanTimedOut && finder.isScanning(); |
apps/server/src/ws.ts+10-9| ... | @@ -2697,15 +2697,16 @@ const layerWsRpc = ( | ... | @@ -2697,15 +2697,16 @@ const layerWsRpc = ( |
| 2697 | status, | 2697 | status, |
| 2698 | }), | 2698 | }), |
| 2699 | ), | 2699 | ), |
| 2700 | Effect.catchTag("RelayClientInstallError", (error) => | 2700 | Effect.catchTags({ |
| 2701 | Queue.fail( | 2701 | RelayClientInstallError: (error) => |
| 2702 | queue, | 2702 | Queue.fail( |
| 2703 | new RelayClientInstallFailedError({ | 2703 | queue, |
| 2704 | reason: error.reason, | 2704 | new RelayClientInstallFailedError({ |
| 2705 | message: error.message, | 2705 | reason: error.reason, |
| 2706 | }), | 2706 | message: error.message, |
| 2707 | ), | 2707 | }), |
| 2708 | ), | 2708 | ), |
| 2709 | }), | ||
| 2709 | Effect.andThen(Queue.end(queue)), | 2710 | Effect.andThen(Queue.end(queue)), |
| 2710 | Effect.forkScoped, | 2711 | Effect.forkScoped, |
| 2711 | ), | 2712 | ), |
oxlint-plugin-t3code/index.ts+2| ... | @@ -9,6 +9,7 @@ import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-e | ... | @@ -9,6 +9,7 @@ import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-e |
| 9 | import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts"; | 9 | import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts"; |
| 10 | import noTestInLoop from "./rules/no-test-in-loop.ts"; | 10 | import noTestInLoop from "./rules/no-test-in-loop.ts"; |
| 11 | import noUnscopedHas from "./rules/no-unscoped-has.ts"; | 11 | import noUnscopedHas from "./rules/no-unscoped-has.ts"; |
| 12 | import preferCatchTags from "./rules/prefer-catch-tags.ts"; | ||
| 12 | import requireSuppressionReason from "./rules/require-suppression-reason.ts"; | 13 | import requireSuppressionReason from "./rules/require-suppression-reason.ts"; |
| 13 | 14 | ||
| 14 | export default definePlugin({ | 15 | export default definePlugin({ |
| ... | @@ -25,6 +26,7 @@ export default definePlugin({ | ... | @@ -25,6 +26,7 @@ export default definePlugin({ |
| 25 | "no-native-title-tooltip": noNativeTitleTooltip, | 26 | "no-native-title-tooltip": noNativeTitleTooltip, |
| 26 | "no-test-in-loop": noTestInLoop, | 27 | "no-test-in-loop": noTestInLoop, |
| 27 | "no-unscoped-has": noUnscopedHas, | 28 | "no-unscoped-has": noUnscopedHas, |
| 29 | "prefer-catch-tags": preferCatchTags, | ||
| 28 | "require-suppression-reason": requireSuppressionReason, | 30 | "require-suppression-reason": requireSuppressionReason, |
| 29 | }, | 31 | }, |
| 30 | }); | 32 | }); |
oxlint-plugin-t3code/rules/prefer-catch-tags.test.ts created+64| ... | @@ -0,0 +1,64 @@ | ||
| 1 | import { assert, describe } from "@effect/vitest"; | ||
| 2 | |||
| 3 | import { createOxlintRuleHarness } from "../test/utils.ts"; | ||
| 4 | |||
| 5 | const rule = createOxlintRuleHarness("t3code/prefer-catch-tags"); | ||
| 6 | |||
| 7 | describe("t3code/prefer-catch-tags", () => { | ||
| 8 | rule.valid( | ||
| 9 | "allows Effect.catchTags", | ||
| 10 | ` | ||
| 11 | import * as Effect from "effect/Effect"; | ||
| 12 | |||
| 13 | export const program = Effect.fail({ _tag: "A" } as const).pipe( | ||
| 14 | Effect.catchTags({ A: () => Effect.void }), | ||
| 15 | ); | ||
| 16 | `, | ||
| 17 | ); | ||
| 18 | |||
| 19 | rule.valid( | ||
| 20 | "ignores catchTag on other modules", | ||
| 21 | ` | ||
| 22 | import * as Stream from "effect/Stream"; | ||
| 23 | |||
| 24 | export const recover = Stream.catchTag("A", () => Stream.empty); | ||
| 25 | `, | ||
| 26 | ); | ||
| 27 | |||
| 28 | rule.valid( | ||
| 29 | "ignores a local that shadows the namespace", | ||
| 30 | ` | ||
| 31 | import * as Effect from "effect/Effect"; | ||
| 32 | |||
| 33 | export const program = Effect.void; | ||
| 34 | export function recover(Effect: { catchTag: (tag: string) => void }) { | ||
| 35 | Effect.catchTag("A"); | ||
| 36 | } | ||
| 37 | `, | ||
| 38 | ); | ||
| 39 | |||
| 40 | rule.invalid( | ||
| 41 | "reports Effect.catchTag", | ||
| 42 | ` | ||
| 43 | import * as Effect from "effect/Effect"; | ||
| 44 | |||
| 45 | export const program = Effect.fail({ _tag: "A" } as const).pipe( | ||
| 46 | Effect.catchTag("A", () => Effect.void), | ||
| 47 | ); | ||
| 48 | `, | ||
| 49 | (output) => { | ||
| 50 | assert.match(output, /Effect\.catchTags\(\{ Tag: handler \}\)/); | ||
| 51 | }, | ||
| 52 | ); | ||
| 53 | |||
| 54 | rule.invalid( | ||
| 55 | "reports catchTag through an aliased namespace", | ||
| 56 | ` | ||
| 57 | import * as Eff from "effect/Effect"; | ||
| 58 | |||
| 59 | export const recover = Eff.catchTag("A", () => Eff.void); | ||
| 60 | `, | ||
| 61 | ); | ||
| 62 | |||
| 63 | rule.invalid("reports a named catchTag import", `import { catchTag } from "effect/Effect";`); | ||
| 64 | }); | ||
oxlint-plugin-t3code/rules/prefer-catch-tags.ts created+60| ... | @@ -0,0 +1,60 @@ | ||
| 1 | import { defineRule, type ESTree } from "@oxlint/plugins"; | ||
| 2 | |||
| 3 | const MESSAGE = "Catch known tags with `Effect.catchTags({ Tag: handler })`, even for one tag."; | ||
| 4 | |||
| 5 | /** Reports `Effect.catchTag`, through any namespace import of `effect/Effect` or a named import. */ | ||
| 6 | export default defineRule({ | ||
| 7 | meta: { | ||
| 8 | type: "suggestion", | ||
| 9 | docs: { | ||
| 10 | description: "Require `Effect.catchTags` over `Effect.catchTag`.", | ||
| 11 | }, | ||
| 12 | }, | ||
| 13 | create(context) { | ||
| 14 | // Whether `identifier` is bound by `import * as X from "effect/Effect"`, not a shadowing local. | ||
| 15 | const isEffectNamespace = (identifier: ESTree.IdentifierReference) => { | ||
| 16 | let scope = context.sourceCode.getScope(identifier); | ||
| 17 | while (true) { | ||
| 18 | const variable = scope.set.get(identifier.name); | ||
| 19 | if (variable !== undefined) { | ||
| 20 | return variable.defs.some( | ||
| 21 | (def) => | ||
| 22 | def.type === "ImportBinding" && | ||
| 23 | def.node.type === "ImportNamespaceSpecifier" && | ||
| 24 | def.parent?.type === "ImportDeclaration" && | ||
| 25 | def.parent.source.value === "effect/Effect", | ||
| 26 | ); | ||
| 27 | } | ||
| 28 | if (scope.upper === null) return false; | ||
| 29 | scope = scope.upper; | ||
| 30 | } | ||
| 31 | }; | ||
| 32 | |||
| 33 | return { | ||
| 34 | ImportDeclaration(node) { | ||
| 35 | if (node.source.value !== "effect/Effect") return; | ||
| 36 | for (const specifier of node.specifiers) { | ||
| 37 | if ( | ||
| 38 | specifier.type === "ImportSpecifier" && | ||
| 39 | (specifier.imported.type === "Identifier" | ||
| 40 | ? specifier.imported.name | ||
| 41 | : specifier.imported.value) === "catchTag" | ||
| 42 | ) { | ||
| 43 | context.report({ node: specifier, message: MESSAGE }); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | }, | ||
| 47 | MemberExpression(node) { | ||
| 48 | if ( | ||
| 49 | !node.computed && | ||
| 50 | node.object.type === "Identifier" && | ||
| 51 | node.property.type === "Identifier" && | ||
| 52 | node.property.name === "catchTag" && | ||
| 53 | isEffectNamespace(node.object) | ||
| 54 | ) { | ||
| 55 | context.report({ node, message: MESSAGE }); | ||
| 56 | } | ||
| 57 | }, | ||
| 58 | }; | ||
| 59 | }, | ||
| 60 | }); | ||
packages/client-runtime/src/connection/registry.ts+3-3| ... | @@ -482,7 +482,7 @@ export const make = Effect.gen(function* () { | ... | @@ -482,7 +482,7 @@ export const make = Effect.gen(function* () { |
| 482 | persistedRoutesByEnvironment.keys(), | 482 | persistedRoutesByEnvironment.keys(), |
| 483 | (environmentId) => | 483 | (environmentId) => |
| 484 | acquireSupervisor(environmentId).pipe( | 484 | acquireSupervisor(environmentId).pipe( |
| 485 | Effect.catchTag("EnvironmentNotRegisteredError", () => Effect.void), | 485 | Effect.catchTags({ EnvironmentNotRegisteredError: () => Effect.void }), |
| 486 | ), | 486 | ), |
| 487 | { | 487 | { |
| 488 | concurrency: "unbounded", | 488 | concurrency: "unbounded", |
| ... | @@ -999,7 +999,7 @@ export const make = Effect.gen(function* () { | ... | @@ -999,7 +999,7 @@ export const make = Effect.gen(function* () { |
| 999 | relayEnvironmentIds, | 999 | relayEnvironmentIds, |
| 1000 | (environmentId) => | 1000 | (environmentId) => |
| 1001 | removeRoute(environmentId, RELAY_ROUTE_ID).pipe( | 1001 | removeRoute(environmentId, RELAY_ROUTE_ID).pipe( |
| 1002 | Effect.catchTag("EnvironmentNotRegisteredError", () => Effect.void), | 1002 | Effect.catchTags({ EnvironmentNotRegisteredError: () => Effect.void }), |
| 1003 | ), | 1003 | ), |
| 1004 | { | 1004 | { |
| 1005 | concurrency: "unbounded", | 1005 | concurrency: "unbounded", |
| ... | @@ -1012,7 +1012,7 @@ export const make = Effect.gen(function* () { | ... | @@ -1012,7 +1012,7 @@ export const make = Effect.gen(function* () { |
| 1012 | const retryNow = (environmentId: EnvironmentId) => | 1012 | const retryNow = (environmentId: EnvironmentId) => |
| 1013 | acquireSupervisor(environmentId).pipe( | 1013 | acquireSupervisor(environmentId).pipe( |
| 1014 | Effect.flatMap((supervisor) => supervisor.retryNow), | 1014 | Effect.flatMap((supervisor) => supervisor.retryNow), |
| 1015 | Effect.catchTag("EnvironmentNotRegisteredError", () => Effect.void), | 1015 | Effect.catchTags({ EnvironmentNotRegisteredError: () => Effect.void }), |
| 1016 | Effect.withSpan("EnvironmentRegistry.retryNow"), | 1016 | Effect.withSpan("EnvironmentRegistry.retryNow"), |
| 1017 | ); | 1017 | ); |
| 1018 | const setEnabled = Effect.fn("EnvironmentRegistry.setEnabled")(function* ( | 1018 | const setEnabled = Effect.fn("EnvironmentRegistry.setEnabled")(function* ( |
packages/shared/src/relayClient.ts+10-9| ... | @@ -379,15 +379,16 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function | ... | @@ -379,15 +379,16 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function |
| 379 | ); | 379 | ); |
| 380 | yield* report("waiting_for_lock"); | 380 | yield* report("waiting_for_lock"); |
| 381 | yield* acquireInstallLock(lockPath).pipe( | 381 | yield* acquireInstallLock(lockPath).pipe( |
| 382 | Effect.catchTag("PlatformError", (cause) => | 382 | Effect.catchTags({ |
| 383 | Effect.fail( | 383 | PlatformError: (cause) => |
| 384 | new RelayClientInstallError({ | 384 | Effect.fail( |
| 385 | reason: "write_failed", | 385 | new RelayClientInstallError({ |
| 386 | message: "Could not acquire the relay client installation lock.", | 386 | reason: "write_failed", |
| 387 | cause, | 387 | message: "Could not acquire the relay client installation lock.", |
| 388 | }), | 388 | cause, |
| 389 | ), | 389 | }), |
| 390 | ), | 390 | ), |
| 391 | }), | ||
| 391 | ); | 392 | ); |
| 392 | return yield* Effect.gen(function* () { | 393 | return yield* Effect.gen(function* () { |
| 393 | const afterLock = yield* resolve; | 394 | const afterLock = yield* resolve; |
packages/shared/src/shell.ts+1-1| ... | @@ -756,7 +756,7 @@ export const isCommandAvailable = Effect.fn("shell.isCommandAvailable")(function | ... | @@ -756,7 +756,7 @@ export const isCommandAvailable = Effect.fn("shell.isCommandAvailable")(function |
| 756 | ) { | 756 | ) { |
| 757 | return yield* resolveCommandPath(command, options).pipe( | 757 | return yield* resolveCommandPath(command, options).pipe( |
| 758 | Effect.as(true), | 758 | Effect.as(true), |
| 759 | Effect.catchTag("CommandResolutionError", () => Effect.succeed(false)), | 759 | Effect.catchTags({ CommandResolutionError: () => Effect.succeed(false) }), |
| 760 | ); | 760 | ); |
| 761 | }); | 761 | }); |
| 762 | 762 |
scripts/mobile-native-client.ts+1-1| ... | @@ -316,7 +316,7 @@ const main = Command.make( | ... | @@ -316,7 +316,7 @@ const main = Command.make( |
| 316 | installedBinary: installedBinary(platform, device), | 316 | installedBinary: installedBinary(platform, device), |
| 317 | readRecord: fs.readFileString(recordPath).pipe( | 317 | readRecord: fs.readFileString(recordPath).pipe( |
| 318 | Effect.flatMap(decodeRecord), | 318 | Effect.flatMap(decodeRecord), |
| 319 | Effect.catchTag("SchemaError", () => Effect.succeed(null)), | 319 | Effect.catchTags({ SchemaError: () => Effect.succeed(null) }), |
| 320 | Effect.catchIf( | 320 | Effect.catchIf( |
| 321 | (error) => error.reason._tag === "NotFound", | 321 | (error) => error.reason._tag === "NotFound", |
| 322 | () => Effect.succeed(null), | 322 | () => Effect.succeed(null), |
vite.config.ts+1| ... | @@ -170,6 +170,7 @@ export default defineConfig({ | ... | @@ -170,6 +170,7 @@ export default defineConfig({ |
| 170 | "t3code/no-test-in-loop": "error", | 170 | "t3code/no-test-in-loop": "error", |
| 171 | "t3code/no-unscoped-has": "error", | 171 | "t3code/no-unscoped-has": "error", |
| 172 | "t3code/namespace-node-imports": "error", | 172 | "t3code/namespace-node-imports": "error", |
| 173 | "t3code/prefer-catch-tags": "error", | ||
| 173 | "t3code/require-suppression-reason": "error", | 174 | "t3code/require-suppression-reason": "error", |
| 174 | }, | 175 | }, |
| 175 | overrides: [ | 176 | overrides: [ |