chore: enforce effect simplifications (#43979)

This commit is contained in:
Kit Langton 2026-08-21 15:48:27 -04:00 committed by GitHub
parent fa1b4ef7ec
commit e945ddf80e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 861 additions and 66 deletions

View file

@ -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"

View file

@ -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",

View file

@ -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)}` : ""}`
})

View file

@ -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(

View file

@ -258,7 +258,7 @@ export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record<strin
const readJson = Effect.fnUntraced(function* (target: string) {
const fs = yield* FileSystem.FileSystem
const text = yield* fs.readFileString(target).pipe(Effect.catch(() => 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 })

View file

@ -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),
)
})

View file

@ -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: "" })),
)
})

View file

@ -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(() =>

View file

@ -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(() => []),
)
}

View file

@ -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) =>

View file

@ -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),
)
})
})

View file

@ -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(

View file

@ -622,7 +622,7 @@ export const layer = (options?: Options) =>
const loadFromFile = options?.file
? fs.readJson(options.file).pipe(
Effect.map((input) => input as Record<string, SourceProvider>),
Effect.catch(() => Effect.succeed(undefined)),
Effect.orElseSucceed(() => undefined),
)
: Effect.succeed(undefined)

View file

@ -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

View file

@ -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(

View file

@ -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" &&

View file

@ -92,7 +92,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
const load = Effect.fn("OpencodePlugin.load")(function* () {
const connection = yield* ctx.integration.connection.active("opencode")
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
connected = connection !== undefined
providers = credential

View file

@ -144,7 +144,7 @@ function poll(device: typeof Device.Type, app: App.Info): Effect.Effect<Token, u
if (response.ok) return yield* decode(response, Token)
const error = yield* Effect.promise(() => 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)))

View file

@ -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,

View file

@ -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,

View file

@ -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
})

View file

@ -319,7 +319,7 @@ export const makeLayer = (connector: WebSocketConnector) =>
}).pipe(
Effect.andThen(metric("connect_failure")),
Effect.andThen(metric("fallback")),
Effect.asVoid,
Effect.as(undefined),
),
),
)

View file

@ -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 },
)

View file

@ -36,7 +36,7 @@ export class Service extends Context.Service<Service, Interface>()("@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,

View file

@ -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

View file

@ -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)

View file

@ -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 }) {

View file

@ -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)]))),

View file

@ -0,0 +1,12 @@
import { expect, test } from "bun:test"
import { Effect } from "effect"
const source = Effect.succeed(1)
const exactUndefined: Effect.Effect<undefined> = source.pipe(Effect.as(undefined))
// @ts-expect-error Effect.asVoid widens the success type to void.
const voidSuccess: Effect.Effect<undefined> = source.pipe(Effect.asVoid)
test("Effect.as preserves the exact undefined success type", () => {
expect(Effect.runSync(exactUndefined)).toBeUndefined()
expect(Effect.runSync(voidSuccess)).toBeUndefined()
})

View file

@ -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))
}

View file

@ -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

View file

@ -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) {

View file

@ -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 })

View file

@ -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

View file

@ -98,12 +98,12 @@ export const fileSystem = (
const pathFor = (name: string) => cassettePath(directory, name)
const walk = (current: string): Effect.Effect<ReadonlyArray<string>> =>
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) =>

View file

@ -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

View file

@ -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<string>()
// Caller picked a specific bin (e.g. pyright exposes both `pyright` and

View file

@ -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`.

View file

@ -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

View file

@ -0,0 +1,32 @@
id: no-effect-and-then-succeed-undefined-tsx
snapshots:
? |
const view = (
<Widget
value={effect.pipe(
Effect.andThen(
Effect.succeed(undefined),
),
)}
/>
)
: labels:
- source: |-
Effect.andThen(
Effect.succeed(undefined),
)
style: primary
start: 55
end: 113
const view = <Widget value={Effect.andThen(effect, Effect.succeed(undefined))} />:
labels:
- source: Effect.andThen(effect, Effect.succeed(undefined))
style: primary
start: 28
end: 77
const view = <Widget value={effect.pipe(Effect.andThen(Effect.succeed(undefined)))} />:
labels:
- source: Effect.andThen(Effect.succeed(undefined))
style: primary
start: 40
end: 81

View file

@ -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

View file

@ -0,0 +1,38 @@
id: no-effect-catch-succeed-tsx
snapshots:
const view = <Widget value={Effect.catch(effect, () => Effect.succeed(fallback))} />:
labels:
- source: Effect.catch(effect, () => Effect.succeed(fallback))
style: primary
start: 28
end: 80
const view = <Widget value={effect.pipe(Effect.catch(() => Effect.succeed(fallback)))} />:
labels:
- source: Effect.catch(() => Effect.succeed(fallback))
style: primary
start: 40
end: 84
const view = <Widget value={effect.pipe(Effect.catch((_error) => Effect.succeed(fallback)))} />:
labels:
- source: Effect.catch((_error) => Effect.succeed(fallback))
style: primary
start: 40
end: 90
"const view = <Widget value={effect.pipe(Effect.catch((_error: Error) => Effect.succeed(fallback)))} />":
labels:
- source: "Effect.catch((_error: Error) => Effect.succeed(fallback))"
style: primary
start: 40
end: 97
const view = <Widget value={effect.pipe(Effect.catch(_error => { return Effect.succeed(fallback) }))} />:
labels:
- source: Effect.catch(_error => { return Effect.succeed(fallback) })
style: primary
start: 40
end: 99
const view = <Widget value={effect.pipe(Effect.catch(function (_error) { return Effect.succeed(fallback) }))} />:
labels:
- source: Effect.catch(function (_error) { return Effect.succeed(fallback) })
style: primary
start: 40
end: 107

View file

@ -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

View file

@ -0,0 +1,38 @@
id: no-effect-flat-map-suspend-tsx
snapshots:
const view = <Widget value={Effect.flatMap(effect, () => Effect.suspend(() => next))} />:
labels:
- source: Effect.flatMap(effect, () => Effect.suspend(() => next))
style: primary
start: 28
end: 84
const view = <Widget value={effect.pipe(Effect.flatMap(() => Effect.suspend(() => next)))} />:
labels:
- source: Effect.flatMap(() => Effect.suspend(() => next))
style: primary
start: 40
end: 88
const view = <Widget value={effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => next)))} />:
labels:
- source: Effect.flatMap((_value) => Effect.suspend(() => next))
style: primary
start: 40
end: 94
"const view = <Widget value={effect.pipe(Effect.flatMap((_value: Value) => Effect.suspend(() => next)))} />":
labels:
- source: "Effect.flatMap((_value: Value) => Effect.suspend(() => next))"
style: primary
start: 40
end: 101
const view = <Widget value={effect.pipe(Effect.flatMap(_value => { return Effect.suspend(() => next) }))} />:
labels:
- source: Effect.flatMap(_value => { return Effect.suspend(() => next) })
style: primary
start: 40
end: 103
const view = <Widget value={effect.pipe(Effect.flatMap(function (_value) { return Effect.suspend(() => next) }))} />:
labels:
- source: Effect.flatMap(function (_value) { return Effect.suspend(() => next) })
style: primary
start: 40
end: 111

View file

@ -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),
),
)

View file

@ -0,0 +1,21 @@
id: no-effect-and-then-succeed-undefined-tsx
valid:
- const view = <Widget value={effect.pipe(Effect.as(undefined))} />
- const view = <Widget value={Effect.as(effect, undefined)} />
- const view = <Widget value={effect.pipe(Effect.asVoid)} />
- const view = <Widget value={effect.pipe(Effect.andThen(Effect.succeed(null)))} />
- const view = <Widget value={Effect.andThen(effect, options, Effect.succeed(undefined))} />
- const view = <Widget value={Stream.andThen(effect, Effect.succeed(undefined))} />
invalid:
- const view = <Widget value={effect.pipe(Effect.andThen(Effect.succeed(undefined)))} />
- const view = <Widget value={Effect.andThen(effect, Effect.succeed(undefined))} />
- |
const view = (
<Widget
value={effect.pipe(
Effect.andThen(
Effect.succeed(undefined),
),
)}
/>
)

View file

@ -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[])),
)

View file

@ -0,0 +1,21 @@
id: no-effect-catch-succeed-tsx
valid:
- const view = <Widget value={effect.pipe(Effect.orElseSucceed(() => fallback))} />
- const view = <Widget value={effect.pipe(Effect.catch((error) => Effect.succeed(error)))} />
- const view = <Widget value={effect.pipe(Effect.catch((error) => Effect.succeed(fallback)))} />
- const view = <Widget value={effect.pipe(Effect.catch((_error) => Effect.succeed(_error)))} />
- const view = <Widget value={effect.pipe(Effect.catch((_error) => Effect.succeed({ _error })))} />
- "const view = <Widget value={effect.pipe(Effect.catch((_error: Error) => Effect.succeed(use(_error))))} />"
- const view = <Widget value={effect.pipe(Effect.catch((_error) => Effect.succeed(values.map((_error) => _error))))} />
- const view = <Widget value={effect.pipe(Effect.catch(({ message }) => Effect.succeed(fallback)))} />
- const view = <Widget value={Effect.catch(effect, options, () => Effect.succeed(fallback))} />
- const view = <Widget value={effect.pipe(Effect.catch(() => { log(); return Effect.succeed(fallback) }))} />
- const view = <Widget value={effect.pipe(Effect.catch(function () { return Effect.succeed(new.target) }))} />
- const view = <Widget value={effect.pipe(Effect.catch(function () { return Effect.succeed(import.meta) }))} />
invalid:
- const view = <Widget value={effect.pipe(Effect.catch(() => Effect.succeed(fallback)))} />
- const view = <Widget value={Effect.catch(effect, () => Effect.succeed(fallback))} />
- const view = <Widget value={effect.pipe(Effect.catch((_error) => Effect.succeed(fallback)))} />
- const view = <Widget value={effect.pipe(Effect.catch(_error => { return Effect.succeed(fallback) }))} />
- "const view = <Widget value={effect.pipe(Effect.catch((_error: Error) => Effect.succeed(fallback)))} />"
- const view = <Widget value={effect.pipe(Effect.catch(function (_error) { return Effect.succeed(fallback) }))} />

View file

@ -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),
),
)

View file

@ -0,0 +1,21 @@
id: no-effect-flat-map-suspend-tsx
valid:
- const view = <Widget value={effect.pipe(Effect.andThen(Effect.suspend(() => next)))} />
- const view = <Widget value={effect.pipe(Effect.flatMap((value) => Effect.suspend(() => use(value))))} />
- const view = <Widget value={effect.pipe(Effect.flatMap((value) => Effect.suspend(() => next)))} />
- const view = <Widget value={effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => use(_value))))} />
- const view = <Widget value={effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => ({ _value }))))} />
- "const view = <Widget value={effect.pipe(Effect.flatMap((_value: Value) => Effect.suspend(() => use(_value))))} />"
- const view = <Widget value={effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => values.map((_value) => use(_value)))))} />
- const view = <Widget value={effect.pipe(Effect.flatMap(({ value }) => Effect.suspend(() => next)))} />
- const view = <Widget value={Effect.flatMap(effect, options, () => Effect.suspend(() => next))} />
- const view = <Widget value={effect.pipe(Effect.flatMap(() => Effect.suspend(thunk)))} />
- const view = <Widget value={effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => new.target) }))} />
- const view = <Widget value={effect.pipe(Effect.flatMap(function () { return Effect.suspend(() => import.meta) }))} />
invalid:
- const view = <Widget value={effect.pipe(Effect.flatMap(() => Effect.suspend(() => next)))} />
- const view = <Widget value={Effect.flatMap(effect, () => Effect.suspend(() => next))} />
- const view = <Widget value={effect.pipe(Effect.flatMap((_value) => Effect.suspend(() => next)))} />
- const view = <Widget value={effect.pipe(Effect.flatMap(_value => { return Effect.suspend(() => next) }))} />
- "const view = <Widget value={effect.pipe(Effect.flatMap((_value: Value) => Effect.suspend(() => next)))} />"
- const view = <Widget value={effect.pipe(Effect.flatMap(function (_value) { return Effect.suspend(() => next) }))} />

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,4 @@
ruleDirs:
- rules
testConfigs:
- testDir: rule-tests