From e945ddf80e89fa2d5cd382402736af05055e5715 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 21 Aug 2026 15:48:27 -0400 Subject: [PATCH] chore: enforce effect simplifications (#43979) --- .github/workflows/test.yml | 8 ++ package.json | 2 + packages/ai/script/setup-recording-env.ts | 2 +- packages/cli/src/config/config.ts | 4 +- packages/cli/src/config/migrate.ts | 2 +- packages/cli/src/services/service-config.ts | 2 +- packages/cli/src/services/updater.ts | 4 +- packages/core/src/config.ts | 2 +- packages/core/src/config/plugin/agent.ts | 4 +- packages/core/src/config/plugin/command.ts | 4 +- packages/core/src/config/plugin/source.ts | 2 +- .../core/src/filesystem/location-watcher.ts | 6 +- packages/core/src/models-dev.ts | 2 +- .../src/plugin/provider/github-copilot.ts | 4 +- packages/core/src/plugin/provider/ollama.ts | 2 +- packages/core/src/plugin/provider/openai.ts | 2 +- packages/core/src/plugin/provider/opencode.ts | 2 +- packages/core/src/plugin/provider/xai.ts | 2 +- packages/core/src/project.ts | 10 +- packages/core/src/session.ts | 4 +- packages/core/src/session/history.ts | 2 +- packages/core/src/session/model-transport.ts | 2 +- packages/core/src/shell.ts | 4 +- packages/core/src/tool-output.ts | 2 +- packages/core/src/tool/plugin/read.ts | 2 +- packages/core/src/vcs.ts | 2 +- packages/core/src/vcs/git.ts | 2 +- packages/core/test/config/websearch.test.ts | 2 +- .../test/effect-simplification-types.test.ts | 12 +++ packages/desktop/src/main/files/apps.ts | 11 +- packages/desktop/src/main/files/index.ts | 5 +- packages/desktop/src/main/native/logging.ts | 2 +- .../desktop/src/main/service/desktop-cli.ts | 2 +- packages/desktop/src/main/storage/cleanup.ts | 8 +- packages/http-recorder/src/cassette/store.ts | 10 +- packages/util/src/fs-util.ts | 2 +- packages/util/src/npm.ts | 2 +- .../ast-grep/effect-simplifications/README.md | 17 +++ ...ct-and-then-succeed-undefined-snapshot.yml | 28 +++++ ...nd-then-succeed-undefined-tsx-snapshot.yml | 32 ++++++ .../no-effect-catch-succeed-snapshot.yml | 95 +++++++++++++++++ .../no-effect-catch-succeed-tsx-snapshot.yml | 38 +++++++ .../no-effect-flat-map-suspend-snapshot.yml | 100 ++++++++++++++++++ ...o-effect-flat-map-suspend-tsx-snapshot.yml | 38 +++++++ ...effect-and-then-succeed-undefined-test.yml | 21 ++++ ...ct-and-then-succeed-undefined-tsx-test.yml | 21 ++++ .../no-effect-catch-succeed-test.yml | 49 +++++++++ .../no-effect-catch-succeed-tsx-test.yml | 21 ++++ .../no-effect-flat-map-suspend-test.yml | 52 +++++++++ .../no-effect-flat-map-suspend-tsx-test.yml | 21 ++++ ...-effect-and-then-succeed-undefined-tsx.yml | 9 ++ .../no-effect-and-then-succeed-undefined.yml | 9 ++ .../rules/no-effect-catch-succeed-tsx.yml | 61 +++++++++++ .../rules/no-effect-catch-succeed.yml | 61 +++++++++++ .../rules/no-effect-flat-map-suspend-tsx.yml | 54 ++++++++++ .../rules/no-effect-flat-map-suspend.yml | 54 ++++++++++ .../effect-simplifications/sgconfig.yml | 4 + 57 files changed, 861 insertions(+), 66 deletions(-) create mode 100644 packages/core/test/effect-simplification-types.test.ts create mode 100644 script/ast-grep/effect-simplifications/README.md create mode 100644 script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-snapshot.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-tsx-snapshot.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-snapshot.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-tsx-snapshot.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-snapshot.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-tsx-snapshot.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-test.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-tsx-test.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-test.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-tsx-test.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-test.yml create mode 100644 script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-tsx-test.yml create mode 100644 script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined-tsx.yml create mode 100644 script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined.yml create mode 100644 script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed-tsx.yml create mode 100644 script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed.yml create mode 100644 script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend-tsx.yml create mode 100644 script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend.yml create mode 100644 script/ast-grep/effect-simplifications/sgconfig.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e278e7af353..24f3119a4aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,6 +50,14 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun + - name: Test Effect simplification rules + if: runner.os == 'Linux' + run: bun run test:effect-simplification-rules + + - name: Check Effect simplifications + if: runner.os == 'Linux' + run: bun run lint:effect-simplifications + - name: Configure git identity run: | git config --global user.email "bot@opencode.ai" diff --git a/package.json b/package.json index e0efca555ec..951409bafd1 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "bench:devex": "bun run --cwd packages/app test:bench:devex", "lint": "oxlint", "lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/util/src packages/core/src packages/server/src packages/protocol/src packages/cli/src", + "lint:effect-simplifications": "ast-grep scan -c script/ast-grep/effect-simplifications/sgconfig.yml --off=unused-suppression packages", "test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml", + "test:effect-simplification-rules": "ast-grep test -c script/ast-grep/effect-simplifications/sgconfig.yml", "typecheck": "bun turbo typecheck --concurrency=3", "typecheck:profile": "bun script/profile-typecheck.ts", "typecheck:profile:packages": "bun script/profile-typecheck-packages.ts", diff --git a/packages/ai/script/setup-recording-env.ts b/packages/ai/script/setup-recording-env.ts index 281c9b8a8de..3ee8189141e 100644 --- a/packages/ai/script/setup-recording-env.ts +++ b/packages/ai/script/setup-recording-env.ts @@ -370,7 +370,7 @@ const responseError = Effect.fn("RecordingEnv.responseError")(function* ( response: HttpClientResponse.HttpClientResponse, ) { if (response.status >= 200 && response.status < 300) return undefined - const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed(""))) + const body = yield* response.text.pipe(Effect.orElseSucceed(() => "")) return `${response.status}${body ? `: ${body.slice(0, 180)}` : ""}` }) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 6ee62f2d5a6..04bf7b90fae 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -31,7 +31,7 @@ export const layer = Layer.effect( const file = path.join(global.config, "cli.json") const readJson = Effect.fnUntraced(function* () { - const text = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(undefined))) + const text = yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => undefined)) if (text === undefined) return undefined const errors: ParseError[] = [] const value: any = parse(text, errors, { allowTrailingComma: true }) @@ -87,7 +87,7 @@ export const layer = Layer.effect( const next = produce(current, update) const edits = changes(current, next) if (!edits.length) return current - const text = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed("{}"))) + const text = yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => "{}")) const updated = edits.reduce( (text, edit) => applyEdits( diff --git a/packages/cli/src/config/migrate.ts b/packages/cli/src/config/migrate.ts index 782beefce22..2a4c33a718f 100644 --- a/packages/cli/src/config/migrate.ts +++ b/packages/cli/src/config/migrate.ts @@ -258,7 +258,7 @@ export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record Effect.succeed(undefined))) + const text = yield* fs.readFileString(target).pipe(Effect.orElseSucceed(() => undefined)) if (text === undefined) return undefined const errors: ParseError[] = [] const value: any = parse(text, errors, { allowTrailingComma: true }) diff --git a/packages/cli/src/services/service-config.ts b/packages/cli/src/services/service-config.ts index 5b459777e4d..df723295db1 100644 --- a/packages/cli/src/services/service-config.ts +++ b/packages/cli/src/services/service-config.ts @@ -119,7 +119,7 @@ export const read = Effect.fn("cli.service-config.read")(function* () { if (legacyConfigFile) yield* migrateConfig(legacyConfigFile, configFile) return yield* fs.readFileString(configFile).pipe( Effect.flatMap(decodeInfo), - Effect.catch(() => Effect.succeed({} as Info)), + Effect.orElseSucceed(() => ({}) as Info), ) }) diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts index 66e1a0aa9d1..ff767e5be3d 100644 --- a/packages/cli/src/services/updater.ts +++ b/packages/cli/src/services/updater.ts @@ -44,7 +44,7 @@ export const layer = Layer.effect( const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) => fs.readFileString(path.join(global.config, name)).pipe( Effect.map(decodePolicy), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ), ) return values.findLast((value) => value !== undefined) ?? true @@ -63,7 +63,7 @@ export const layer = Layer.effect( stdout: result.stdout.toString("utf8"), stderr: result.stderr.toString("utf8"), })), - Effect.catch(() => Effect.succeed({ code: 1, stdout: "", stderr: "" })), + Effect.orElseSucceed(() => ({ code: 1, stdout: "", stderr: "" })), ) }) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 472d81b8ccd..00253ca7057 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -347,7 +347,7 @@ export const layer = (options?: Options) => Stream.filterEffect((event) => wellknown.entries().pipe( Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ), ), Stream.runForEach(() => diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index e2f3fe32161..78445e68b98 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -59,7 +59,7 @@ export const Plugin = define({ return yield* Effect.forEach(files, (file) => fs.readFileStringSafe(file.filepath).pipe( Effect.map((content) => (content ? decode(file, content) : undefined)), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ), ).pipe(Effect.map((documents) => documents.filter((document): document is Document => document !== undefined))) }) @@ -176,7 +176,7 @@ function discover(fs: FSUtil.Interface, directory: string) { ), ).pipe( Effect.map((files) => files.flat()), - Effect.catch(() => Effect.succeed([])), + Effect.orElseSucceed(() => []), ) } diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts index 65b21836d80..27a5c451ada 100644 --- a/packages/core/src/config/plugin/command.ts +++ b/packages/core/src/config/plugin/command.ts @@ -86,11 +86,11 @@ function loadDirectory(fs: FSUtil.Interface, directory: string) { return Effect.gen(function* () { const files = yield* fs .scan("{command,commands}/**/*.md", { cwd: directory, absolute: true, dot: true, symlink: true }) - .pipe(Effect.catch(() => Effect.succeed([] as string[]))) + .pipe(Effect.orElseSucceed(() => [] as string[])) return yield* Effect.forEach(files.toSorted(), (filepath) => fs.readFileStringSafe(filepath).pipe( Effect.map((content) => (content === undefined ? undefined : decode(directory, filepath, content))), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ), ).pipe( Effect.map((commands) => diff --git a/packages/core/src/config/plugin/source.ts b/packages/core/src/config/plugin/source.ts index d91b2d259aa..0ea62434e92 100644 --- a/packages/core/src/config/plugin/source.ts +++ b/packages/core/src/config/plugin/source.ts @@ -148,7 +148,7 @@ const scan = Effect.fn("ConfigPluginSource.scan")(function* ( ...operation, mtime: Option.getOrElse(info.mtime, () => new Date(0)).getTime(), })), - Effect.catch(() => Effect.succeed(operation)), + Effect.orElseSucceed(() => operation), ) }) }) diff --git a/packages/core/src/filesystem/location-watcher.ts b/packages/core/src/filesystem/location-watcher.ts index 5c01d29fe93..40a76c4de30 100644 --- a/packages/core/src/filesystem/location-watcher.ts +++ b/packages/core/src/filesystem/location-watcher.ts @@ -35,14 +35,12 @@ const layer = Layer.effect( Effect.gen(function* () { if (location.vcs?.type === "git") { const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory - const vcs = resolved - ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) - : undefined + const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)) : undefined if (vcs) return { path: path.join(vcs, "HEAD"), aliases: [".git", vcs, ...(resolved ? [resolved] : [])] } } if (location.vcs?.type === "hg") { const store = location.vcs.store - const vcs = yield* fs.realPath(store).pipe(Effect.catch(() => Effect.succeed(store))) + const vcs = yield* fs.realPath(store).pipe(Effect.orElseSucceed(() => store)) return { path: path.join(vcs, "branch"), aliases: [".hg", vcs] } } }).pipe( diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 126489c96e9..f04eea732e0 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -622,7 +622,7 @@ export const layer = (options?: Options) => const loadFromFile = options?.file ? fs.readJson(options.file).pipe( Effect.map((input) => input as Record), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ) : Effect.succeed(undefined) diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index 927bd82ba3b..cb534c60a82 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -108,7 +108,7 @@ const oauth = (app: App.Info) => }, ).pipe( Effect.map((user) => Option.getOrUndefined(decodeUser(user))?.endpoints?.api?.replace(/\/+$/, "")), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), Effect.map((apiEndpoint) => Credential.OAuth.make({ type: "oauth", @@ -159,7 +159,7 @@ export const GithubCopilotPlugin = define({ const load = Effect.fn("GithubCopilotPlugin.load")(function* () { const connection = yield* ctx.integration.connection.active("github-copilot") const credential = connection - ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined))) + ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.orElseSucceed(() => undefined)) : undefined if (credential?.type !== "oauth") { loaded.baseURL = undefined diff --git a/packages/core/src/plugin/provider/ollama.ts b/packages/core/src/plugin/provider/ollama.ts index 3e6bc8648ad..f6074a6c63c 100644 --- a/packages/core/src/plugin/provider/ollama.ts +++ b/packages/core/src/plugin/provider/ollama.ts @@ -148,7 +148,7 @@ export function make(origin = "http://127.0.0.1:11434", interval: Duration.Input ) shows.set(model.model, { digest: model.digest, info }) return { ...model, show: info } - }).pipe(Effect.catch(() => Effect.succeed(undefined))), + }).pipe(Effect.orElseSucceed(() => undefined)), { concurrency: 4 }, ) const filtered = models.filter( diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 49af4b8669f..8fa5354bb0c 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -175,7 +175,7 @@ export const OpenAIPlugin = define({ const load = Effect.fn("OpenAIPlugin.load")(function* () { const connection = yield* ctx.integration.connection.active("openai") const credential = connection - ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined))) + ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.orElseSucceed(() => undefined)) : undefined chatgpt = credential?.type === "oauth" && diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 2ee3555250e..b7425c0fd51 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -92,7 +92,7 @@ export const OpencodePlugin = define Effect.succeed(undefined))) + ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.orElseSucceed(() => undefined)) : undefined connected = connection !== undefined providers = credential diff --git a/packages/core/src/plugin/provider/xai.ts b/packages/core/src/plugin/provider/xai.ts index 9ed1168f3a2..8e9b137a761 100644 --- a/packages/core/src/plugin/provider/xai.ts +++ b/packages/core/src/plugin/provider/xai.ts @@ -144,7 +144,7 @@ function poll(device: typeof Device.Type, app: App.Info): Effect.Effect response.text()).pipe( Effect.map((body) => Option.getOrUndefined(decodeDeviceError(body))), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ) if (error?.error === "authorization_pending") { return yield* Effect.sleep(interval + pollingSafetyMargin).pipe(Effect.andThen(loop(interval))) diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index df7fa870fba..05285718743 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -41,7 +41,7 @@ export interface Resolved { export const root = Effect.fn("Project.root")(function* (fs: FSUtil.Interface, input: AbsolutePath) { return yield* fs.up({ targets: [".git", ".hg"], start: input, mode: "first" }).pipe( Effect.map((matches) => (matches[0] ? AbsolutePath.make(path.dirname(matches[0])) : undefined)), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ) }) @@ -149,7 +149,7 @@ const layer = Layer.effect( return yield* fs.readFileString(path.join(dir, "opencode")).pipe( Effect.map((value) => value.trim()), Effect.map((value) => (value ? ID.make(value) : undefined)), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ) }) @@ -202,7 +202,7 @@ const layer = Layer.effect( stdin: "ignore", }), ) - .pipe(Effect.catch(() => Effect.succeed(undefined))) + .pipe(Effect.orElseSucceed(() => undefined)) if (!result || result.exitCode !== 0) return undefined const node = result.stdout .toString("utf8") @@ -216,7 +216,7 @@ const layer = Layer.effect( const hgDiscover = Effect.fnUntraced(function* (input: AbsolutePath) { const dotHg = yield* fs.up({ targets: [".hg"], start: input, mode: "first" }).pipe( Effect.map((matches) => matches[0]), - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ) if (!dotHg) return undefined const worktree = AbsolutePath.make(path.dirname(dotHg)) @@ -241,7 +241,7 @@ const layer = Layer.effect( ? repo.worktree : yield* git.worktree.list(repo).pipe( Effect.map((items) => items.find((item) => item.kind === "main")?.directory ?? repo.worktree), - Effect.catch(() => Effect.succeed(repo.worktree)), + Effect.orElseSucceed(() => repo.worktree), ) return yield* persist({ previous, diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 037261b9e4d..4432691718e 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -768,7 +768,7 @@ const layer = Layer.effect( const expanded = value === "~" ? global.home : value.startsWith("~/") ? path.join(global.home, value.slice(2)) : value const directory = AbsolutePath.make(path.resolve(current.location.directory, expanded)) - const info = yield* fs.stat(directory).pipe(Effect.catch(() => Effect.succeed(undefined))) + const info = yield* fs.stat(directory).pipe(Effect.orElseSucceed(() => undefined)) if (!info) return yield* new DestinationNotFoundError({ directory }) if (info.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory }) const project = yield* projects.resolve(directory) @@ -787,7 +787,7 @@ const layer = Layer.effect( input.sessionID, Effect.gen(function* () { const latest = yield* result.get(input.sessionID) - const source = yield* fs.stat(latest.location.directory).pipe(Effect.catch(() => Effect.succeed(undefined))) + const source = yield* fs.stat(latest.location.directory).pipe(Effect.orElseSucceed(() => undefined)) if (!source || source.type !== "Directory") { const cancellations = (yield* SessionInbox.moveIDs(db, input.sessionID)).map( (item) => [SessionEvent.InboxCancelled, { sessionID: input.sessionID, inboxID: item.id }] as const, diff --git a/packages/core/src/session/history.ts b/packages/core/src/session/history.ts index 19383000d61..b3df7847160 100644 --- a/packages/core/src/session/history.ts +++ b/packages/core/src/session/history.ts @@ -120,7 +120,7 @@ export const firstUserMessage = Effect.fn("SessionHistory.firstUserMessage")(fun .get() .pipe(Effect.orDie) if (!row) return undefined - const message = yield* decodeMessageRow(row).pipe(Effect.catch(() => Effect.succeed(undefined))) + const message = yield* decodeMessageRow(row).pipe(Effect.orElseSucceed(() => undefined)) return message?.type === "user" ? message : undefined }) diff --git a/packages/core/src/session/model-transport.ts b/packages/core/src/session/model-transport.ts index 600f067256b..2a052f41e86 100644 --- a/packages/core/src/session/model-transport.ts +++ b/packages/core/src/session/model-transport.ts @@ -319,7 +319,7 @@ export const makeLayer = (connector: WebSocketConnector) => }).pipe( Effect.andThen(metric("connect_failure")), Effect.andThen(metric("fallback")), - Effect.asVoid, + Effect.as(undefined), ), ), ) diff --git a/packages/core/src/shell.ts b/packages/core/src/shell.ts index 924ba90cb54..40d9b51c4d1 100644 --- a/packages/core/src/shell.ts +++ b/packages/core/src/shell.ts @@ -77,7 +77,7 @@ export const cleanup = Effect.fn("Shell.cleanup")(function* () { const directory = path.join(global.data, DIRECTORY) const projects = yield* fs.readDirectoryEntries(directory).pipe( Effect.map((entries) => entries.filter((entry) => entry.type === "directory")), - Effect.catch(() => Effect.succeed([])), + Effect.orElseSucceed(() => []), ) const files = yield* Effect.forEach( projects, @@ -90,7 +90,7 @@ export const cleanup = Effect.fn("Shell.cleanup")(function* () { : [], ), ), - Effect.catch(() => Effect.succeed([])), + Effect.orElseSucceed(() => []), ), { concurrency: 8 }, ) diff --git a/packages/core/src/tool-output.ts b/packages/core/src/tool-output.ts index b2a29c2431f..db29cbe3560 100644 --- a/packages/core/src/tool-output.ts +++ b/packages/core/src/tool-output.ts @@ -36,7 +36,7 @@ export class Service extends Context.Service()("@opencode/To const cleanup = Effect.fn("ToolOutput.cleanup")(function* (fs: FSUtil.Interface, directory: string) { const entries = yield* fs.readDirectory(directory).pipe( Effect.map((entries) => entries.filter((entry) => /^tool_[0-9a-f]{12}/.test(entry))), - Effect.catch(() => Effect.succeed([])), + Effect.orElseSucceed(() => []), ) yield* FileRetention.cleanup( fs, diff --git a/packages/core/src/tool/plugin/read.ts b/packages/core/src/tool/plugin/read.ts index 829a6776289..33ad8d1e884 100644 --- a/packages/core/src/tool/plugin/read.ts +++ b/packages/core/src/tool/plugin/read.ts @@ -142,7 +142,7 @@ export const Plugin = { .map((entry) => join(dirname(input), entry)) .slice(0, 3), ), - Effect.catch(() => Effect.succeed([] as string[])), + Effect.orElseSucceed(() => [] as string[]), ) const message = suggestions.length === 0 diff --git a/packages/core/src/vcs.ts b/packages/core/src/vcs.ts index 1f0bcdf31ce..4f5603313ba 100644 --- a/packages/core/src/vcs.ts +++ b/packages/core/src/vcs.ts @@ -49,7 +49,7 @@ const layer = Layer.effect( const state = { info: impl ? yield* impl.info() : ({ branch: {} } satisfies Info) } if (vcs && impl) { - const store = yield* fs.realPath(vcs.store).pipe(Effect.catch(() => Effect.succeed(vcs.store))) + const store = yield* fs.realPath(vcs.store).pipe(Effect.orElseSucceed(() => vcs.store)) const isBranchMetadata = vcs.type === "git" ? (file: string) => path.basename(file) === "HEAD" && FSUtil.contains(store, file) diff --git a/packages/core/src/vcs/git.ts b/packages/core/src/vcs/git.ts index b180d595964..e09fff14a4f 100644 --- a/packages/core/src/vcs/git.ts +++ b/packages/core/src/vcs/git.ts @@ -141,7 +141,7 @@ function makeGit(proc: AppProcess.Interface) { truncated: result.stdoutTruncated || result.stderrTruncated, } }, - Effect.catch(() => Effect.succeed({ exitCode: 1, text: () => "", truncated: false })), + Effect.orElseSucceed(() => ({ exitCode: 1, text: () => "", truncated: false })), ) const text = Effect.fnUntraced(function* (args: string[], opts: { cwd: string }) { diff --git a/packages/core/test/config/websearch.test.ts b/packages/core/test/config/websearch.test.ts index ddd21e7c01f..98431fa481d 100644 --- a/packages/core/test/config/websearch.test.ts +++ b/packages/core/test/config/websearch.test.ts @@ -32,7 +32,7 @@ describe("ConfigWebSearchPlugin.Plugin", () => { yield* waitUntil( websearch.default().pipe( Effect.map((provider) => provider?.id === WebSearch.ID.make("test")), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ), ) }).pipe(Effect.provide(Config.testLayer([configured(false)]))), diff --git a/packages/core/test/effect-simplification-types.test.ts b/packages/core/test/effect-simplification-types.test.ts new file mode 100644 index 00000000000..c2368efed49 --- /dev/null +++ b/packages/core/test/effect-simplification-types.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "bun:test" +import { Effect } from "effect" + +const source = Effect.succeed(1) +const exactUndefined: Effect.Effect = source.pipe(Effect.as(undefined)) +// @ts-expect-error Effect.asVoid widens the success type to void. +const voidSuccess: Effect.Effect = source.pipe(Effect.asVoid) + +test("Effect.as preserves the exact undefined success type", () => { + expect(Effect.runSync(exactUndefined)).toBeUndefined() + expect(Effect.runSync(voidSuccess)).toBeUndefined() +}) diff --git a/packages/desktop/src/main/files/apps.ts b/packages/desktop/src/main/files/apps.ts index 8981f2838dd..a1e222c29fc 100644 --- a/packages/desktop/src/main/files/apps.ts +++ b/packages/desktop/src/main/files/apps.ts @@ -28,7 +28,7 @@ const checkMacosApp = Effect.fn("DesktopFiles.checkMacosApp")(function* (appName return yield* Effect.tryPromise(() => execFilePromise("which", [appName])).pipe( Effect.as(true), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false), ) }) @@ -36,7 +36,7 @@ const resolveWindowsAppPath = Effect.fn("DesktopFiles.resolveWindowsAppPath")(fu const fs = yield* FileSystem.FileSystem const path = yield* Path.Path const result = yield* Effect.tryPromise(() => execFilePromise("where", [appName])).pipe( - Effect.catch(() => Effect.succeed(undefined)), + Effect.orElseSucceed(() => undefined), ) if (!result) return null @@ -110,7 +110,7 @@ const resolveWindowsAppPath = Effect.fn("DesktopFiles.resolveWindowsAppPath")(fu for (const file of paths) { const dirs = [path.dirname(file), path.dirname(path.dirname(file)), path.dirname(path.dirname(path.dirname(file)))] for (const dir of dirs) { - const entries = yield* fs.readDirectory(dir).pipe(Effect.catch(() => Effect.succeed([]))) + const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => [])) for (const entry of entries) { const candidate = path.join(dir, entry) if (!hasExt(candidate, "exe")) continue @@ -130,8 +130,5 @@ const resolveWindowsAppPath = Effect.fn("DesktopFiles.resolveWindowsAppPath")(fu }) function exists(fs: FileSystem.FileSystem, path: string) { - return fs.access(path).pipe( - Effect.as(true), - Effect.catch(() => Effect.succeed(false)), - ) + return fs.exists(path).pipe(Effect.orElseSucceed(() => false)) } diff --git a/packages/desktop/src/main/files/index.ts b/packages/desktop/src/main/files/index.ts index b73a4f2ee04..8334acbd618 100644 --- a/packages/desktop/src/main/files/index.ts +++ b/packages/desktop/src/main/files/index.ts @@ -85,10 +85,7 @@ function make(fs: FileSystem.FileSystem, path: Path.Path) { ) }), revealPath: Effect.fn("DesktopFiles.revealPath")(function* (target: string) { - const exists = yield* fs.stat(target).pipe( - Effect.as(true), - Effect.catch(() => Effect.succeed(false)), - ) + const exists = yield* fs.exists(target).pipe(Effect.orElseSucceed(() => false)) if (!exists) return false shell.showItemInFolder(target) return true diff --git a/packages/desktop/src/main/native/logging.ts b/packages/desktop/src/main/native/logging.ts index 963da1546bd..e2de7c3300b 100644 --- a/packages/desktop/src/main/native/logging.ts +++ b/packages/desktop/src/main/native/logging.ts @@ -163,7 +163,7 @@ export const tail = Effect.fn("DesktopLogging.tail")(function* () { const contents = yield* fs.readFileString(path) const lines = contents.split("\n") return lines.slice(Math.max(0, lines.length - TAIL_LINES)).join("\n") - }).pipe(Effect.catch(() => Effect.succeed(""))) + }).pipe(Effect.orElseSucceed(() => "")) }) function initRunDirectory(fs: FileSystem.FileSystem, path: Path.Path) { diff --git a/packages/desktop/src/main/service/desktop-cli.ts b/packages/desktop/src/main/service/desktop-cli.ts index ad032520241..2e02a56445b 100644 --- a/packages/desktop/src/main/service/desktop-cli.ts +++ b/packages/desktop/src/main/service/desktop-cli.ts @@ -95,7 +95,7 @@ export const cleanStages = Effect.fn("DesktopCli.cleanStages")(function* (binary Effect.fnUntraced(function* (entry) { const target = path.join(root, entry) if (target === current) return - const stat = yield* fs.stat(target).pipe(Effect.catch(() => Effect.succeed(undefined))) + const stat = yield* fs.stat(target).pipe(Effect.orElseSucceed(() => undefined)) if (stat?.type !== "Directory") return yield* fs .remove(target, { recursive: true, force: true }) diff --git a/packages/desktop/src/main/storage/cleanup.ts b/packages/desktop/src/main/storage/cleanup.ts index 64fa091cfa4..ed7dc200383 100644 --- a/packages/desktop/src/main/storage/cleanup.ts +++ b/packages/desktop/src/main/storage/cleanup.ts @@ -19,7 +19,7 @@ export const cleanupStoreFiles = Effect.fn("Storage.cleanupStoreFiles")(function ) { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path - const entries = yield* fs.readDirectory(userDataPath).pipe(Effect.catch(() => Effect.succeed([]))) + const entries = yield* fs.readDirectory(userDataPath).pipe(Effect.orElseSucceed(() => [])) const candidates = (yield* Effect.forEach( entries, Effect.fnUntraced(function* (entry) { @@ -27,7 +27,7 @@ export const cleanupStoreFiles = Effect.fn("Storage.cleanupStoreFiles")(function if (!kind) return const file = path.join(userDataPath, entry) - const stats = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined))) + const stats = yield* fs.stat(file).pipe(Effect.orElseSucceed(() => undefined)) if (stats?.type !== "File") return return { @@ -74,7 +74,7 @@ export const deleteStoreFileIfEmpty = Effect.fn("Storage.deleteStoreFileIfEmpty" const fs = yield* FileSystem.FileSystem const path = yield* Path.Path const file = path.join(userDataPath, name) - const stats = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined))) + const stats = yield* fs.stat(file).pipe(Effect.orElseSucceed(() => undefined)) if (stats?.type !== "File") return false if (!(yield* isEmptyStore(file, stats.size))) return false @@ -91,7 +91,7 @@ const isEmptyStore = Effect.fn("Storage.isEmptyStore")(function* (file: string, if (size > FileSystem.Size(EMPTY_STORE_MAX_BYTES)) return false const fs = yield* FileSystem.FileSystem - const raw = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(undefined))) + const raw = yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => undefined)) if (raw === undefined) return false if (raw.trim() === "") return true diff --git a/packages/http-recorder/src/cassette/store.ts b/packages/http-recorder/src/cassette/store.ts index e37f797d5af..e1fba898877 100644 --- a/packages/http-recorder/src/cassette/store.ts +++ b/packages/http-recorder/src/cassette/store.ts @@ -98,12 +98,12 @@ export const fileSystem = ( const pathFor = (name: string) => cassettePath(directory, name) const walk = (current: string): Effect.Effect> => Effect.gen(function* () { - const entries = yield* fs.readDirectory(current).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + const entries = yield* fs.readDirectory(current).pipe(Effect.orElseSucceed(() => [] as string[])) const nested = yield* Effect.forEach(entries, (entry) => { const full = path.join(current, entry) return fs.stat(full).pipe( Effect.flatMap((stat) => (stat.type === "Directory" ? walk(full) : Effect.succeed([full]))), - Effect.catch(() => Effect.succeed([] as string[])), + Effect.orElseSucceed(() => [] as string[]), ) }) return nested.flat() @@ -144,11 +144,7 @@ export const fileSystem = ( recorded.set(name, { interactions, findings: interactionFindings }) }), ), - exists: (name) => - fs.access(pathFor(name)).pipe( - Effect.as(true), - Effect.catch(() => Effect.succeed(false)), - ), + exists: (name) => fs.exists(pathFor(name)).pipe(Effect.orElseSucceed(() => false)), list: () => walk(directory).pipe( Effect.map((files) => diff --git a/packages/util/src/fs-util.ts b/packages/util/src/fs-util.ts index 1bbd451d7af..b82a5d8e66c 100644 --- a/packages/util/src/fs-util.ts +++ b/packages/util/src/fs-util.ts @@ -188,7 +188,7 @@ export namespace FSUtil { let current = start while (true) { const matches = yield* scan(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe( - Effect.catch(() => Effect.succeed([] as string[])), + Effect.orElseSucceed(() => [] as string[]), ) result.push(...matches) if (stop === current) break diff --git a/packages/util/src/npm.ts b/packages/util/src/npm.ts index 934a04f7197..8cf027be6da 100644 --- a/packages/util/src/npm.ts +++ b/packages/util/src/npm.ts @@ -167,7 +167,7 @@ const layer = Layer.effect( const binDir = path.join(dir, "node_modules", ".bin") const pick = Effect.fnUntraced(function* () { - const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + const files = yield* fs.readDirectory(binDir).pipe(Effect.orElseSucceed(() => [] as string[])) if (files.length === 0) return Option.none() // Caller picked a specific bin (e.g. pyright exposes both `pyright` and diff --git a/script/ast-grep/effect-simplifications/README.md b/script/ast-grep/effect-simplifications/README.md new file mode 100644 index 00000000000..10aba3fb4c1 --- /dev/null +++ b/script/ast-grep/effect-simplifications/README.md @@ -0,0 +1,17 @@ +# Effect simplification rules + +The dedicated config scans both TypeScript and TSX with equivalent rules. The +generic ast-grep config intentionally does not load these rules. + +Ignored callback parameters are supported only when they are simple ASCII +underscore-prefixed identifiers such as `_`, `_error`, or `_value`, optionally +with a type annotation. A rule reports the callback only when the exact target +subtree contains no underscore-prefixed identifier-like node, proving the +binding is unused without relying on unavailable TypeScript scope resolution. + +This proof is intentionally conservative. Destructured, defaulted, rest, +optional, non-underscore, and Unicode parameters are excluded. A nested binding +or property whose name starts with `_` also suppresses the diagnostic, even when +it does not reference the callback parameter. Regular-function callbacks are +excluded when their target captures `this`, `arguments`, or a meta-property such +as `new.target` or `import.meta`. diff --git a/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-snapshot.yml b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-snapshot.yml new file mode 100644 index 00000000000..0c6db7d65cd --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-snapshot.yml @@ -0,0 +1,28 @@ +id: no-effect-and-then-succeed-undefined +snapshots: + Effect.andThen(effect, Effect.succeed(undefined)): + labels: + - source: Effect.andThen(effect, Effect.succeed(undefined)) + style: primary + start: 0 + end: 49 + ? | + effect.pipe( + Effect.andThen( + Effect.succeed(undefined), + ), + ) + : labels: + - source: |- + Effect.andThen( + Effect.succeed(undefined), + ) + style: primary + start: 15 + end: 65 + effect.pipe(Effect.andThen(Effect.succeed(undefined))): + labels: + - source: Effect.andThen(Effect.succeed(undefined)) + style: primary + start: 12 + end: 53 diff --git a/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-tsx-snapshot.yml b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-tsx-snapshot.yml new file mode 100644 index 00000000000..9c0cfcba9ff --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-and-then-succeed-undefined-tsx-snapshot.yml @@ -0,0 +1,32 @@ +id: no-effect-and-then-succeed-undefined-tsx +snapshots: + ? | + const view = ( + + ) + : labels: + - source: |- + Effect.andThen( + Effect.succeed(undefined), + ) + style: primary + start: 55 + end: 113 + const view = : + labels: + - source: Effect.andThen(effect, Effect.succeed(undefined)) + style: primary + start: 28 + end: 77 + const view = : + labels: + - source: Effect.andThen(Effect.succeed(undefined)) + style: primary + start: 40 + end: 81 diff --git a/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-snapshot.yml b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-snapshot.yml new file mode 100644 index 00000000000..8e78d0f56ce --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-snapshot.yml @@ -0,0 +1,95 @@ +id: no-effect-catch-succeed +snapshots: + 'Effect.catch(effect, () => Effect.succeed({ reason: "fallback" }))': + labels: + - source: 'Effect.catch(effect, () => Effect.succeed({ reason: "fallback" }))' + style: primary + start: 0 + end: 66 + ? | + effect.pipe( + Effect.catch(() => Effect.succeed([] as string[])), + ) + : labels: + - source: Effect.catch(() => Effect.succeed([] as string[])) + style: primary + start: 15 + end: 65 + effect.pipe(Effect.catch(() => Effect.succeed(fallback))): + labels: + - source: Effect.catch(() => Effect.succeed(fallback)) + style: primary + start: 12 + end: 56 + effect.pipe(Effect.catch(() => { return Effect.succeed(fallback) })): + labels: + - source: Effect.catch(() => { return Effect.succeed(fallback) }) + style: primary + start: 12 + end: 67 + effect.pipe(Effect.catch((_) => Effect.succeed(fallback))): + labels: + - source: Effect.catch((_) => Effect.succeed(fallback)) + style: primary + start: 12 + end: 57 + "effect.pipe(Effect.catch((_cause: Cause) => Effect.succeed(fallback)))": + labels: + - source: "Effect.catch((_cause: Cause) => Effect.succeed(fallback))" + style: primary + start: 12 + end: 69 + effect.pipe(Effect.catch((_error) => Effect.succeed(fallback))): + labels: + - source: Effect.catch((_error) => Effect.succeed(fallback)) + style: primary + start: 12 + end: 62 + "effect.pipe(Effect.catch((_error: Error) => { return Effect.succeed(fallback) }))": + labels: + - source: "Effect.catch((_error: Error) => { return Effect.succeed(fallback) })" + style: primary + start: 12 + end: 80 + effect.pipe(Effect.catch(_ => Effect.succeed(fallback))): + labels: + - source: Effect.catch(_ => Effect.succeed(fallback)) + style: primary + start: 12 + end: 55 + effect.pipe(Effect.catch(_ => { return Effect.succeed(fallback) })): + labels: + - source: Effect.catch(_ => { return Effect.succeed(fallback) }) + style: primary + start: 12 + end: 66 + effect.pipe(Effect.catch(_error => { return Effect.succeed(fallback) })): + labels: + - source: Effect.catch(_error => { return Effect.succeed(fallback) }) + style: primary + start: 12 + end: 71 + effect.pipe(Effect.catch(function () { return Effect.succeed(fallback) })): + labels: + - source: Effect.catch(function () { return Effect.succeed(fallback) }) + style: primary + start: 12 + end: 73 + effect.pipe(Effect.catch(function (_) { return Effect.succeed(fallback) })): + labels: + - source: Effect.catch(function (_) { return Effect.succeed(fallback) }) + style: primary + start: 12 + end: 74 + effect.pipe(Effect.catch(function (_error) { return Effect.succeed(fallback) })): + labels: + - source: Effect.catch(function (_error) { return Effect.succeed(fallback) }) + style: primary + start: 12 + end: 79 + "effect.pipe(Effect.catch(function (_error: Error) { return Effect.succeed(fallback) }))": + labels: + - source: "Effect.catch(function (_error: Error) { return Effect.succeed(fallback) })" + style: primary + start: 12 + end: 86 diff --git a/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-tsx-snapshot.yml b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-tsx-snapshot.yml new file mode 100644 index 00000000000..48b09b57001 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-catch-succeed-tsx-snapshot.yml @@ -0,0 +1,38 @@ +id: no-effect-catch-succeed-tsx +snapshots: + const view = Effect.succeed(fallback))} />: + labels: + - source: Effect.catch(effect, () => Effect.succeed(fallback)) + style: primary + start: 28 + end: 80 + const view = Effect.succeed(fallback)))} />: + labels: + - source: Effect.catch(() => Effect.succeed(fallback)) + style: primary + start: 40 + end: 84 + const view = Effect.succeed(fallback)))} />: + labels: + - source: Effect.catch((_error) => Effect.succeed(fallback)) + style: primary + start: 40 + end: 90 + "const view = Effect.succeed(fallback)))} />": + labels: + - source: "Effect.catch((_error: Error) => Effect.succeed(fallback))" + style: primary + start: 40 + end: 97 + const view = { return Effect.succeed(fallback) }))} />: + labels: + - source: Effect.catch(_error => { return Effect.succeed(fallback) }) + style: primary + start: 40 + end: 99 + const view = : + labels: + - source: Effect.catch(function (_error) { return Effect.succeed(fallback) }) + style: primary + start: 40 + end: 107 diff --git a/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-snapshot.yml b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-snapshot.yml new file mode 100644 index 00000000000..47ee1c15db2 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-snapshot.yml @@ -0,0 +1,100 @@ +id: no-effect-flat-map-suspend +snapshots: + Effect.flatMap(effect, () => Effect.suspend(() => next)): + labels: + - source: Effect.flatMap(effect, () => Effect.suspend(() => next)) + style: primary + start: 0 + end: 56 + ? | + effect.pipe( + Effect.flatMap(() => + Effect.suspend(() => next), + ), + ) + : labels: + - source: |- + Effect.flatMap(() => + Effect.suspend(() => next), + ) + style: primary + start: 15 + end: 71 + effect.pipe(Effect.flatMap(() => Effect.suspend(() => next))): + labels: + - source: Effect.flatMap(() => Effect.suspend(() => next)) + style: primary + start: 12 + end: 60 + effect.pipe(Effect.flatMap(() => { return Effect.suspend(() => next) })): + labels: + - source: Effect.flatMap(() => { return Effect.suspend(() => next) }) + style: primary + start: 12 + end: 71 + effect.pipe(Effect.flatMap((_) => Effect.suspend(() => next))): + labels: + - source: Effect.flatMap((_) => Effect.suspend(() => next)) + style: primary + start: 12 + end: 61 + effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => next))): + labels: + - source: Effect.flatMap((_value) => Effect.suspend(() => next)) + style: primary + start: 12 + end: 66 + "effect.pipe(Effect.flatMap((_value: Value) => Effect.suspend(() => next)))": + labels: + - source: "Effect.flatMap((_value: Value) => Effect.suspend(() => next))" + style: primary + start: 12 + end: 73 + "effect.pipe(Effect.flatMap((_value: Value) => { return Effect.suspend(() => next) }))": + labels: + - source: "Effect.flatMap((_value: Value) => { return Effect.suspend(() => next) })" + style: primary + start: 12 + end: 84 + effect.pipe(Effect.flatMap(_ => Effect.suspend(() => next))): + labels: + - source: Effect.flatMap(_ => Effect.suspend(() => next)) + style: primary + start: 12 + end: 59 + effect.pipe(Effect.flatMap(_ => { return Effect.suspend(() => next) })): + labels: + - source: Effect.flatMap(_ => { return Effect.suspend(() => next) }) + style: primary + start: 12 + end: 70 + effect.pipe(Effect.flatMap(_value => { return Effect.suspend(() => next) })): + labels: + - source: Effect.flatMap(_value => { return Effect.suspend(() => next) }) + style: primary + start: 12 + end: 75 + effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => next) })): + labels: + - source: Effect.flatMap(function () { return Effect.suspend(() => next) }) + style: primary + start: 12 + end: 77 + effect.pipe(Effect.flatMap(function (_) { return Effect.suspend(() => next) })): + labels: + - source: Effect.flatMap(function (_) { return Effect.suspend(() => next) }) + style: primary + start: 12 + end: 78 + effect.pipe(Effect.flatMap(function (_value) { return Effect.suspend(() => next) })): + labels: + - source: Effect.flatMap(function (_value) { return Effect.suspend(() => next) }) + style: primary + start: 12 + end: 83 + "effect.pipe(Effect.flatMap(function (_value: Value) { return Effect.suspend(() => next) }))": + labels: + - source: "Effect.flatMap(function (_value: Value) { return Effect.suspend(() => next) })" + style: primary + start: 12 + end: 90 diff --git a/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-tsx-snapshot.yml b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-tsx-snapshot.yml new file mode 100644 index 00000000000..2f9784187c0 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/__snapshots__/no-effect-flat-map-suspend-tsx-snapshot.yml @@ -0,0 +1,38 @@ +id: no-effect-flat-map-suspend-tsx +snapshots: + const view = Effect.suspend(() => next))} />: + labels: + - source: Effect.flatMap(effect, () => Effect.suspend(() => next)) + style: primary + start: 28 + end: 84 + const view = Effect.suspend(() => next)))} />: + labels: + - source: Effect.flatMap(() => Effect.suspend(() => next)) + style: primary + start: 40 + end: 88 + const view = Effect.suspend(() => next)))} />: + labels: + - source: Effect.flatMap((_value) => Effect.suspend(() => next)) + style: primary + start: 40 + end: 94 + "const view = Effect.suspend(() => next)))} />": + labels: + - source: "Effect.flatMap((_value: Value) => Effect.suspend(() => next))" + style: primary + start: 40 + end: 101 + const view = { return Effect.suspend(() => next) }))} />: + labels: + - source: Effect.flatMap(_value => { return Effect.suspend(() => next) }) + style: primary + start: 40 + end: 103 + const view = next) }))} />: + labels: + - source: Effect.flatMap(function (_value) { return Effect.suspend(() => next) }) + style: primary + start: 40 + end: 111 diff --git a/script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-test.yml b/script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-test.yml new file mode 100644 index 00000000000..3c28503200e --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-test.yml @@ -0,0 +1,21 @@ +id: no-effect-and-then-succeed-undefined +valid: + - effect.pipe(Effect.as(undefined)) + - Effect.as(effect, undefined) + - effect.pipe(Effect.asVoid) + - effect.pipe(Effect.andThen(Effect.succeed(null))) + - effect.pipe(Effect.andThen(() => Effect.succeed(undefined))) + - effect.pipe(Effect.andThen(Effect.sync(() => undefined))) + - effect.pipe(Stream.andThen(Effect.succeed(undefined))) + - Effect.andThen(effect, options, Effect.succeed(undefined)) + - Effect.andThen(first, second, third, Effect.succeed(undefined)) + - Effect.succeed(undefined) +invalid: + - effect.pipe(Effect.andThen(Effect.succeed(undefined))) + - Effect.andThen(effect, Effect.succeed(undefined)) + - | + effect.pipe( + Effect.andThen( + Effect.succeed(undefined), + ), + ) diff --git a/script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-tsx-test.yml b/script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-tsx-test.yml new file mode 100644 index 00000000000..163f0a19b03 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/no-effect-and-then-succeed-undefined-tsx-test.yml @@ -0,0 +1,21 @@ +id: no-effect-and-then-succeed-undefined-tsx +valid: + - const view = + - const view = + - const view = + - const view = + - const view = + - const view = +invalid: + - const view = + - const view = + - | + const view = ( + + ) diff --git a/script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-test.yml b/script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-test.yml new file mode 100644 index 00000000000..b3c61a09c74 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-test.yml @@ -0,0 +1,49 @@ +id: no-effect-catch-succeed +valid: + - effect.pipe(Effect.orElseSucceed(() => fallback)) + - Effect.orElseSucceed(effect, () => fallback) + - effect.pipe(Effect.catch((error) => Effect.succeed(error))) + - effect.pipe(Effect.catch((error) => Effect.succeed(fallback))) + - effect.pipe(Effect.catch((_) => Effect.succeed(_))) + - effect.pipe(Effect.catch((_) => Effect.succeed(use(_)))) + - effect.pipe(Effect.catch((_) => { return Effect.succeed(_) })) + - effect.pipe(Effect.catch((_error) => Effect.succeed(_error))) + - effect.pipe(Effect.catch((_error) => Effect.succeed({ _error }))) + - "effect.pipe(Effect.catch((_error: Error) => Effect.succeed(use(_error))))" + - effect.pipe(Effect.catch((_error) => Effect.succeed(values.map((_error) => _error)))) + - effect.pipe(Effect.catch(({ message }) => Effect.succeed(fallback))) + - effect.pipe(Effect.catch((_error = fallbackError) => Effect.succeed(fallback))) + - effect.pipe(Effect.catch((..._errors) => Effect.succeed(fallback))) + - "effect.pipe(Effect.catch((_error?: Error) => Effect.succeed(fallback)))" + - effect.pipe(Effect.catch(() => Effect.void)) + - effect.pipe(Effect.catch(() => Effect.sync(fallback))) + - effect.pipe(Other.catch(() => Effect.succeed(fallback))) + - Effect.catch(effect, options, () => Effect.succeed(fallback)) + - Effect.catch(first, second, third, () => Effect.succeed(fallback)) + - effect.pipe(Effect.catch(() => { log(); return Effect.succeed(fallback) })) + - effect.pipe(Effect.catch(function () { return Effect.succeed(this.fallback) })) + - effect.pipe(Effect.catch(function () { return Effect.succeed(arguments[0]) })) + - effect.pipe(Effect.catch(function () { return Effect.succeed(new.target) })) + - effect.pipe(Effect.catch(function () { return Effect.succeed(import.meta) })) + - effect.pipe(Effect.catch(function (_) { return Effect.succeed(_) })) + - "effect.pipe(Effect.catch(function (_error: Error) { return Effect.succeed(_error) }))" + - effect.pipe(Effect.catch(function (_error) { return Effect.succeed(new.target) })) +invalid: + - effect.pipe(Effect.catch(() => Effect.succeed(fallback))) + - 'Effect.catch(effect, () => Effect.succeed({ reason: "fallback" }))' + - effect.pipe(Effect.catch((_) => Effect.succeed(fallback))) + - effect.pipe(Effect.catch(_ => Effect.succeed(fallback))) + - effect.pipe(Effect.catch(_ => { return Effect.succeed(fallback) })) + - effect.pipe(Effect.catch(() => { return Effect.succeed(fallback) })) + - effect.pipe(Effect.catch(function () { return Effect.succeed(fallback) })) + - effect.pipe(Effect.catch(function (_) { return Effect.succeed(fallback) })) + - effect.pipe(Effect.catch((_error) => Effect.succeed(fallback))) + - effect.pipe(Effect.catch(_error => { return Effect.succeed(fallback) })) + - "effect.pipe(Effect.catch((_cause: Cause) => Effect.succeed(fallback)))" + - "effect.pipe(Effect.catch((_error: Error) => { return Effect.succeed(fallback) }))" + - effect.pipe(Effect.catch(function (_error) { return Effect.succeed(fallback) })) + - "effect.pipe(Effect.catch(function (_error: Error) { return Effect.succeed(fallback) }))" + - | + effect.pipe( + Effect.catch(() => Effect.succeed([] as string[])), + ) diff --git a/script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-tsx-test.yml b/script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-tsx-test.yml new file mode 100644 index 00000000000..1d3c7e118ea --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/no-effect-catch-succeed-tsx-test.yml @@ -0,0 +1,21 @@ +id: no-effect-catch-succeed-tsx +valid: + - const view = fallback))} /> + - const view = Effect.succeed(error)))} /> + - const view = Effect.succeed(fallback)))} /> + - const view = Effect.succeed(_error)))} /> + - const view = Effect.succeed({ _error })))} /> + - "const view = Effect.succeed(use(_error))))} />" + - const view = Effect.succeed(values.map((_error) => _error))))} /> + - const view = Effect.succeed(fallback)))} /> + - const view = Effect.succeed(fallback))} /> + - const view = { log(); return Effect.succeed(fallback) }))} /> + - const view = + - const view = +invalid: + - const view = Effect.succeed(fallback)))} /> + - const view = Effect.succeed(fallback))} /> + - const view = Effect.succeed(fallback)))} /> + - const view = { return Effect.succeed(fallback) }))} /> + - "const view = Effect.succeed(fallback)))} />" + - const view = diff --git a/script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-test.yml b/script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-test.yml new file mode 100644 index 00000000000..3d187b6c38b --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-test.yml @@ -0,0 +1,52 @@ +id: no-effect-flat-map-suspend +valid: + - effect.pipe(Effect.andThen(Effect.suspend(() => next))) + - Effect.andThen(effect, Effect.suspend(() => next)) + - effect.pipe(Effect.flatMap((value) => Effect.suspend(() => use(value)))) + - effect.pipe(Effect.flatMap((value) => Effect.suspend(() => next))) + - effect.pipe(Effect.flatMap((_) => Effect.suspend(() => use(_)))) + - effect.pipe(Effect.flatMap((_) => { return Effect.suspend(() => use(_)) })) + - effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => use(_value)))) + - effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => ({ _value })))) + - "effect.pipe(Effect.flatMap((_value: Value) => Effect.suspend(() => use(_value))))" + - effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => values.map((_value) => use(_value))))) + - effect.pipe(Effect.flatMap(({ value }) => Effect.suspend(() => next))) + - effect.pipe(Effect.flatMap((_value = fallback) => Effect.suspend(() => next))) + - effect.pipe(Effect.flatMap((..._values) => Effect.suspend(() => next))) + - "effect.pipe(Effect.flatMap((_value?: Value) => Effect.suspend(() => next)))" + - effect.pipe(Effect.flatMap(() => Effect.suspend(thunk))) + - effect.pipe(Effect.flatMap(() => Effect.suspend(getThunk()))) + - effect.pipe(Effect.flatMap(() => Effect.suspend(function thunk() { return next }))) + - effect.pipe(Effect.flatMap(() => Effect.sync(thunk))) + - effect.pipe(Stream.flatMap(() => Effect.suspend(() => next))) + - Effect.flatMap(effect, options, () => Effect.suspend(() => next)) + - Effect.flatMap(first, second, third, () => Effect.suspend(() => next)) + - effect.pipe(Effect.flatMap(() => { log(); return Effect.suspend(() => next) })) + - effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => this.next) })) + - effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => arguments[0]) })) + - effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => new.target) })) + - effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => import.meta) })) + - effect.pipe(Effect.flatMap(function (_) { return Effect.suspend(() => use(_)) })) + - "effect.pipe(Effect.flatMap(function (_value: Value) { return Effect.suspend(() => use(_value)) }))" + - effect.pipe(Effect.flatMap(function (_value) { return Effect.suspend(() => new.target) })) +invalid: + - effect.pipe(Effect.flatMap(() => Effect.suspend(() => next))) + - Effect.flatMap(effect, () => Effect.suspend(() => next)) + - effect.pipe(Effect.flatMap((_) => Effect.suspend(() => next))) + - effect.pipe(Effect.flatMap(_ => Effect.suspend(() => next))) + - effect.pipe(Effect.flatMap(_ => { return Effect.suspend(() => next) })) + - effect.pipe(Effect.flatMap(() => { return Effect.suspend(() => next) })) + - effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => next) })) + - effect.pipe(Effect.flatMap(function (_) { return Effect.suspend(() => next) })) + - effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => next))) + - effect.pipe(Effect.flatMap(_value => { return Effect.suspend(() => next) })) + - "effect.pipe(Effect.flatMap((_value: Value) => Effect.suspend(() => next)))" + - "effect.pipe(Effect.flatMap((_value: Value) => { return Effect.suspend(() => next) }))" + - effect.pipe(Effect.flatMap(function (_value) { return Effect.suspend(() => next) })) + - "effect.pipe(Effect.flatMap(function (_value: Value) { return Effect.suspend(() => next) }))" + - | + effect.pipe( + Effect.flatMap(() => + Effect.suspend(() => next), + ), + ) diff --git a/script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-tsx-test.yml b/script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-tsx-test.yml new file mode 100644 index 00000000000..5ec0ecd67c6 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rule-tests/no-effect-flat-map-suspend-tsx-test.yml @@ -0,0 +1,21 @@ +id: no-effect-flat-map-suspend-tsx +valid: + - const view = next)))} /> + - const view = Effect.suspend(() => use(value))))} /> + - const view = Effect.suspend(() => next)))} /> + - const view = Effect.suspend(() => use(_value))))} /> + - const view = Effect.suspend(() => ({ _value }))))} /> + - "const view = Effect.suspend(() => use(_value))))} />" + - const view = Effect.suspend(() => values.map((_value) => use(_value)))))} /> + - const view = Effect.suspend(() => next)))} /> + - const view = Effect.suspend(() => next))} /> + - const view = Effect.suspend(thunk)))} /> + - const view = new.target) }))} /> + - const view = import.meta) }))} /> +invalid: + - const view = Effect.suspend(() => next)))} /> + - const view = Effect.suspend(() => next))} /> + - const view = Effect.suspend(() => next)))} /> + - const view = { return Effect.suspend(() => next) }))} /> + - "const view = Effect.suspend(() => next)))} />" + - const view = next) }))} /> diff --git a/script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined-tsx.yml b/script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined-tsx.yml new file mode 100644 index 00000000000..e4b60ea4102 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined-tsx.yml @@ -0,0 +1,9 @@ +id: no-effect-and-then-succeed-undefined-tsx +language: Tsx +message: Use Effect.as(undefined) to preserve the exact undefined success type. +severity: error +rule: + all: + - pattern: Effect.andThen($$$SELF, Effect.succeed(undefined)) + - not: + pattern: Effect.andThen($_FIRST, $_SECOND, $$$REST) diff --git a/script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined.yml b/script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined.yml new file mode 100644 index 00000000000..3bd3486b91c --- /dev/null +++ b/script/ast-grep/effect-simplifications/rules/no-effect-and-then-succeed-undefined.yml @@ -0,0 +1,9 @@ +id: no-effect-and-then-succeed-undefined +language: TypeScript +message: Use Effect.as(undefined) to preserve the exact undefined success type. +severity: error +rule: + all: + - pattern: Effect.andThen($$$SELF, Effect.succeed(undefined)) + - not: + pattern: Effect.andThen($_FIRST, $_SECOND, $$$REST) diff --git a/script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed-tsx.yml b/script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed-tsx.yml new file mode 100644 index 00000000000..cd3c7be8ae2 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed-tsx.yml @@ -0,0 +1,61 @@ +id: no-effect-catch-succeed-tsx +language: Tsx +message: Use Effect.orElseSucceed for constant failure recovery. +severity: error +rule: + all: + - any: + - pattern: Effect.catch($$$SELF, () => Effect.succeed($VALUE)) + - pattern: Effect.catch($$$SELF, () => { return Effect.succeed($VALUE) }) + - pattern: Effect.catch($$$SELF, ($IGNORED) => Effect.succeed($IGNORED_VALUE)) + - pattern: Effect.catch($$$SELF, ($IGNORED) => { return Effect.succeed($IGNORED_VALUE) }) + - pattern: Effect.catch($$$SELF, $IGNORED => Effect.succeed($IGNORED_VALUE)) + - pattern: Effect.catch($$$SELF, $IGNORED => { return Effect.succeed($IGNORED_VALUE) }) + - pattern: "Effect.catch($$$SELF, ($IGNORED: $_TYPE) => Effect.succeed($IGNORED_VALUE))" + - pattern: "Effect.catch($$$SELF, ($IGNORED: $_TYPE) => { return Effect.succeed($IGNORED_VALUE) })" + - pattern: Effect.catch($$$SELF, function () { return Effect.succeed($FUNCTION_VALUE) }) + - pattern: Effect.catch($$$SELF, function ($FUNCTION_IGNORED) { return Effect.succeed($FUNCTION_IGNORED_VALUE) }) + - pattern: "Effect.catch($$$SELF, function ($FUNCTION_IGNORED: $_TYPE) { return Effect.succeed($FUNCTION_IGNORED_VALUE) })" + - not: + pattern: Effect.catch($_FIRST, $_SECOND, $$$REST) +constraints: + IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + IGNORED_VALUE: + all: + - not: + regex: ^_[A-Za-z0-9_$]*$ + - not: + has: + regex: ^_[A-Za-z0-9_$]*$ + stopBy: end + FUNCTION_VALUE: + all: + - not: + any: + - regex: ^arguments$ + - kind: this + - kind: meta_property + - not: + has: + any: + - regex: ^arguments$ + - kind: this + - kind: meta_property + stopBy: end + FUNCTION_IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + FUNCTION_IGNORED_VALUE: + all: + - not: + any: + - regex: ^(_[A-Za-z0-9_$]*|arguments)$ + - kind: this + - kind: meta_property + - not: + has: + any: + - regex: ^(_[A-Za-z0-9_$]*|arguments)$ + - kind: this + - kind: meta_property + stopBy: end diff --git a/script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed.yml b/script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed.yml new file mode 100644 index 00000000000..44f6a02b80f --- /dev/null +++ b/script/ast-grep/effect-simplifications/rules/no-effect-catch-succeed.yml @@ -0,0 +1,61 @@ +id: no-effect-catch-succeed +language: TypeScript +message: Use Effect.orElseSucceed for constant failure recovery. +severity: error +rule: + all: + - any: + - pattern: Effect.catch($$$SELF, () => Effect.succeed($VALUE)) + - pattern: Effect.catch($$$SELF, () => { return Effect.succeed($VALUE) }) + - pattern: Effect.catch($$$SELF, ($IGNORED) => Effect.succeed($IGNORED_VALUE)) + - pattern: Effect.catch($$$SELF, ($IGNORED) => { return Effect.succeed($IGNORED_VALUE) }) + - pattern: Effect.catch($$$SELF, $IGNORED => Effect.succeed($IGNORED_VALUE)) + - pattern: Effect.catch($$$SELF, $IGNORED => { return Effect.succeed($IGNORED_VALUE) }) + - pattern: "Effect.catch($$$SELF, ($IGNORED: $_TYPE) => Effect.succeed($IGNORED_VALUE))" + - pattern: "Effect.catch($$$SELF, ($IGNORED: $_TYPE) => { return Effect.succeed($IGNORED_VALUE) })" + - pattern: Effect.catch($$$SELF, function () { return Effect.succeed($FUNCTION_VALUE) }) + - pattern: Effect.catch($$$SELF, function ($FUNCTION_IGNORED) { return Effect.succeed($FUNCTION_IGNORED_VALUE) }) + - pattern: "Effect.catch($$$SELF, function ($FUNCTION_IGNORED: $_TYPE) { return Effect.succeed($FUNCTION_IGNORED_VALUE) })" + - not: + pattern: Effect.catch($_FIRST, $_SECOND, $$$REST) +constraints: + IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + IGNORED_VALUE: + all: + - not: + regex: ^_[A-Za-z0-9_$]*$ + - not: + has: + regex: ^_[A-Za-z0-9_$]*$ + stopBy: end + FUNCTION_VALUE: + all: + - not: + any: + - regex: ^arguments$ + - kind: this + - kind: meta_property + - not: + has: + any: + - regex: ^arguments$ + - kind: this + - kind: meta_property + stopBy: end + FUNCTION_IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + FUNCTION_IGNORED_VALUE: + all: + - not: + any: + - regex: ^(_[A-Za-z0-9_$]*|arguments)$ + - kind: this + - kind: meta_property + - not: + has: + any: + - regex: ^(_[A-Za-z0-9_$]*|arguments)$ + - kind: this + - kind: meta_property + stopBy: end diff --git a/script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend-tsx.yml b/script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend-tsx.yml new file mode 100644 index 00000000000..5a3dbe46ff8 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend-tsx.yml @@ -0,0 +1,54 @@ +id: no-effect-flat-map-suspend-tsx +language: Tsx +message: Use effect-form Effect.andThen when the previous value is ignored. +severity: error +rule: + all: + - any: + - pattern: Effect.flatMap($$$SELF, () => Effect.suspend($THUNK)) + - pattern: Effect.flatMap($$$SELF, () => { return Effect.suspend($THUNK) }) + - pattern: Effect.flatMap($$$SELF, ($IGNORED) => Effect.suspend($IGNORED_THUNK)) + - pattern: Effect.flatMap($$$SELF, ($IGNORED) => { return Effect.suspend($IGNORED_THUNK) }) + - pattern: Effect.flatMap($$$SELF, $IGNORED => Effect.suspend($IGNORED_THUNK)) + - pattern: Effect.flatMap($$$SELF, $IGNORED => { return Effect.suspend($IGNORED_THUNK) }) + - pattern: "Effect.flatMap($$$SELF, ($IGNORED: $_TYPE) => Effect.suspend($IGNORED_THUNK))" + - pattern: "Effect.flatMap($$$SELF, ($IGNORED: $_TYPE) => { return Effect.suspend($IGNORED_THUNK) })" + - pattern: Effect.flatMap($$$SELF, function () { return Effect.suspend($FUNCTION_THUNK) }) + - pattern: Effect.flatMap($$$SELF, function ($FUNCTION_IGNORED) { return Effect.suspend($FUNCTION_IGNORED_THUNK) }) + - pattern: "Effect.flatMap($$$SELF, function ($FUNCTION_IGNORED: $_TYPE) { return Effect.suspend($FUNCTION_IGNORED_THUNK) })" + - not: + pattern: Effect.flatMap($_FIRST, $_SECOND, $$$REST) +constraints: + THUNK: + kind: arrow_function + IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + IGNORED_THUNK: + all: + - kind: arrow_function + - not: + has: + regex: ^_[A-Za-z0-9_$]*$ + stopBy: end + FUNCTION_THUNK: + all: + - kind: arrow_function + - not: + has: + any: + - regex: ^arguments$ + - kind: this + - kind: meta_property + stopBy: end + FUNCTION_IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + FUNCTION_IGNORED_THUNK: + all: + - kind: arrow_function + - not: + has: + any: + - regex: ^(_[A-Za-z0-9_$]*|arguments)$ + - kind: this + - kind: meta_property + stopBy: end diff --git a/script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend.yml b/script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend.yml new file mode 100644 index 00000000000..bf2f6710694 --- /dev/null +++ b/script/ast-grep/effect-simplifications/rules/no-effect-flat-map-suspend.yml @@ -0,0 +1,54 @@ +id: no-effect-flat-map-suspend +language: TypeScript +message: Use effect-form Effect.andThen when the previous value is ignored. +severity: error +rule: + all: + - any: + - pattern: Effect.flatMap($$$SELF, () => Effect.suspend($THUNK)) + - pattern: Effect.flatMap($$$SELF, () => { return Effect.suspend($THUNK) }) + - pattern: Effect.flatMap($$$SELF, ($IGNORED) => Effect.suspend($IGNORED_THUNK)) + - pattern: Effect.flatMap($$$SELF, ($IGNORED) => { return Effect.suspend($IGNORED_THUNK) }) + - pattern: Effect.flatMap($$$SELF, $IGNORED => Effect.suspend($IGNORED_THUNK)) + - pattern: Effect.flatMap($$$SELF, $IGNORED => { return Effect.suspend($IGNORED_THUNK) }) + - pattern: "Effect.flatMap($$$SELF, ($IGNORED: $_TYPE) => Effect.suspend($IGNORED_THUNK))" + - pattern: "Effect.flatMap($$$SELF, ($IGNORED: $_TYPE) => { return Effect.suspend($IGNORED_THUNK) })" + - pattern: Effect.flatMap($$$SELF, function () { return Effect.suspend($FUNCTION_THUNK) }) + - pattern: Effect.flatMap($$$SELF, function ($FUNCTION_IGNORED) { return Effect.suspend($FUNCTION_IGNORED_THUNK) }) + - pattern: "Effect.flatMap($$$SELF, function ($FUNCTION_IGNORED: $_TYPE) { return Effect.suspend($FUNCTION_IGNORED_THUNK) })" + - not: + pattern: Effect.flatMap($_FIRST, $_SECOND, $$$REST) +constraints: + THUNK: + kind: arrow_function + IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + IGNORED_THUNK: + all: + - kind: arrow_function + - not: + has: + regex: ^_[A-Za-z0-9_$]*$ + stopBy: end + FUNCTION_THUNK: + all: + - kind: arrow_function + - not: + has: + any: + - regex: ^arguments$ + - kind: this + - kind: meta_property + stopBy: end + FUNCTION_IGNORED: + regex: ^_[A-Za-z0-9_$]*(?:\s*:\s*.+)?$ + FUNCTION_IGNORED_THUNK: + all: + - kind: arrow_function + - not: + has: + any: + - regex: ^(_[A-Za-z0-9_$]*|arguments)$ + - kind: this + - kind: meta_property + stopBy: end diff --git a/script/ast-grep/effect-simplifications/sgconfig.yml b/script/ast-grep/effect-simplifications/sgconfig.yml new file mode 100644 index 00000000000..098ecb2af48 --- /dev/null +++ b/script/ast-grep/effect-simplifications/sgconfig.yml @@ -0,0 +1,4 @@ +ruleDirs: + - rules +testConfigs: + - testDir: rule-tests