diff --git a/.changeset/tall-maps-unite.md b/.changeset/tall-maps-unite.md
new file mode 100644
index 00000000..12418d42
--- /dev/null
+++ b/.changeset/tall-maps-unite.md
@@ -0,0 +1,7 @@
+---
+"@effect/language-service": patch
+---
+
+Refactor Effect context tracking to use cached node context flags and direct generator lookups.
+
+This aligns the TypeScript implementation more closely with the TSGo version and simplifies diagnostics that need to detect whether code is inside an Effect generator.
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..57adefa8
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalConsoleInEffect_skipNextLine from 207 to 226
+globalConsoleInEffect_skipFile from 207 to 226
+globalConsoleInEffect_skipNextLine from 413 to 439
+globalConsoleInEffect_skipFile from 413 to 439
+globalConsoleInEffect_skipNextLine from 289 to 308
+globalConsoleInEffect_skipFile from 289 to 308
+globalConsoleInEffect_skipNextLine from 522 to 556
+globalConsoleInEffect_skipFile from 522 to 556
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.output
new file mode 100644
index 00000000..193f5b60
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+console.log("sync")
+6:47 - 6:66 | 0 | This Effect code uses `console.log`, logging in Effect code is represented through `Effect.log or Logger`. effect(globalConsoleInEffect)
+
+console.warn("try")
+9:13 - 9:32 | 0 | This Effect code uses `console.warn`, logging in Effect code is represented through `Effect.logWarning or Logger`. effect(globalConsoleInEffect)
+
+console.log("try-promise")
+13:65 - 13:91 | 0 | This Effect code uses `console.log`, logging in Effect code is represented through `Effect.log or Logger`. effect(globalConsoleInEffect)
+
+console.warn("try-promise-object")
+16:19 - 16:53 | 0 | This Effect code uses `console.warn`, logging in Effect code is represented through `Effect.logWarning or Logger`. effect(globalConsoleInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..03b101cc
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalDateInEffect_skipNextLine from 201 to 211
+globalDateInEffect_skipFile from 201 to 211
+globalDateInEffect_skipNextLine from 386 to 396
+globalDateInEffect_skipFile from 386 to 396
+globalDateInEffect_skipNextLine from 274 to 284
+globalDateInEffect_skipFile from 274 to 284
+globalDateInEffect_skipNextLine from 476 to 486
+globalDateInEffect_skipFile from 476 to 486
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.output
new file mode 100644
index 00000000..d7e21e4d
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+Date.now()
+6:44 - 6:54 | 0 | This Effect code uses `Date.now()`, time access in Effect code is represented through `Clock` from Effect. effect(globalDateInEffect)
+
+new Date()
+9:13 - 9:23 | 0 | This Effect code constructs `new Date()`, date values in Effect code are represented through `DateTime` from Effect. effect(globalDateInEffect)
+
+Date.now()
+13:62 - 13:72 | 0 | This Effect code uses `Date.now()`, time access in Effect code is represented through `Clock` from Effect. effect(globalDateInEffect)
+
+new Date()
+16:19 - 16:29 | 0 | This Effect code constructs `new Date()`, date values in Effect code are represented through `DateTime` from Effect. effect(globalDateInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes
index a6eac123..42189401 100644
--- a/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes
@@ -1 +1,2 @@
-no codefixes
\ No newline at end of file
+globalFetchInEffect_skipNextLine from 198 to 203
+globalFetchInEffect_skipFile from 198 to 203
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output
index aa033c38..14743a2a 100644
--- a/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output
@@ -1 +1,2 @@
-// no diagnostics
\ No newline at end of file
+fetch
+6:37 - 6:42 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `@effect/platform`. effect(globalFetchInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..98e621b0
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalFetchInEffect_skipNextLine from 203 to 208
+globalFetchInEffect_skipFile from 203 to 208
+globalFetchInEffect_skipNextLine from 432 to 437
+globalFetchInEffect_skipFile from 432 to 437
+globalFetchInEffect_skipNextLine from 297 to 302
+globalFetchInEffect_skipFile from 297 to 302
+globalFetchInEffect_skipNextLine from 553 to 558
+globalFetchInEffect_skipFile from 553 to 558
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.output
new file mode 100644
index 00000000..e2119261
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+fetch
+6:45 - 6:50 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `@effect/platform`. effect(globalFetchInEffect)
+
+fetch
+9:13 - 9:18 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `@effect/platform`. effect(globalFetchInEffect)
+
+fetch
+13:63 - 13:68 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `@effect/platform`. effect(globalFetchInEffect)
+
+fetch
+16:19 - 16:24 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `@effect/platform`. effect(globalFetchInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..081f1946
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalRandomInEffect_skipNextLine from 205 to 218
+globalRandomInEffect_skipFile from 205 to 218
+globalRandomInEffect_skipNextLine from 397 to 410
+globalRandomInEffect_skipFile from 397 to 410
+globalRandomInEffect_skipNextLine from 280 to 293
+globalRandomInEffect_skipFile from 280 to 293
+globalRandomInEffect_skipNextLine from 492 to 505
+globalRandomInEffect_skipFile from 492 to 505
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.output
new file mode 100644
index 00000000..56773ceb
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+Math.random()
+6:46 - 6:59 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
+
+Math.random()
+9:13 - 9:26 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
+
+Math.random()
+13:64 - 13:77 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
+
+Math.random()
+16:19 - 16:32 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..cea72497
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalTimersInEffect_skipNextLine from 205 to 229
+globalTimersInEffect_skipFile from 205 to 229
+globalTimersInEffect_skipNextLine from 420 to 444
+globalTimersInEffect_skipFile from 420 to 444
+globalTimersInEffect_skipNextLine from 291 to 316
+globalTimersInEffect_skipFile from 291 to 316
+globalTimersInEffect_skipNextLine from 526 to 550
+globalTimersInEffect_skipFile from 526 to 550
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.output
new file mode 100644
index 00000000..1547ec24
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+setTimeout(() => {}, 10)
+6:46 - 6:70 | 0 | This Effect code uses `setTimeout`, the corresponding timer API in this context is `Effect.sleep or Schedule` from Effect. effect(globalTimersInEffect)
+
+setInterval(() => {}, 10)
+9:13 - 9:38 | 0 | This Effect code uses `setInterval`, the corresponding timer API in this context is `Schedule or Effect.repeat` from Effect. effect(globalTimersInEffect)
+
+setTimeout(() => {}, 10)
+13:64 - 13:88 | 0 | This Effect code uses `setTimeout`, the corresponding timer API in this context is `Effect.sleep or Schedule` from Effect. effect(globalTimersInEffect)
+
+setTimeout(() => {}, 10)
+16:19 - 16:43 | 0 | This Effect code uses `setTimeout`, the corresponding timer API in this context is `Effect.sleep or Schedule` from Effect. effect(globalTimersInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..430101f9
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+processEnvInEffect_skipNextLine from 231 to 254
+processEnvInEffect_skipFile from 231 to 254
+processEnvInEffect_skipNextLine from 436 to 466
+processEnvInEffect_skipFile from 436 to 466
+processEnvInEffect_skipNextLine from 313 to 335
+processEnvInEffect_skipFile from 313 to 335
+processEnvInEffect_skipNextLine from 545 to 582
+processEnvInEffect_skipFile from 545 to 582
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.output
new file mode 100644
index 00000000..fe1b56db
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+process.env.SYNC_SECRET
+7:43 - 7:66 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env.TRY_SECRET
+10:13 - 10:35 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env.TRY_PROMISE_SECRET
+14:61 - 14:91 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env.TRY_PROMISE_OBJECT_SECRET
+17:19 - 17:56 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..220c97eb
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+schemaSyncInEffect_skipNextLine from 240 to 257
+schemaSyncInEffect_skipFile from 240 to 257
+schemaSyncInEffect_skipNextLine from 510 to 527
+schemaSyncInEffect_skipFile from 510 to 527
+schemaSyncInEffect_skipNextLine from 354 to 371
+schemaSyncInEffect_skipFile from 354 to 371
+schemaSyncInEffect_skipNextLine from 643 to 667
+schemaSyncInEffect_skipFile from 643 to 667
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.output
new file mode 100644
index 00000000..7f5cb447
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+Schema.decodeSync
+10:46 - 10:63 | 2 | `Schema.decodeSync` is used inside an Effect generator. `Schema.decode` preserves the typed Effect error channel for this operation without throwing. effect(schemaSyncInEffect)
+
+Schema.decodeSync
+13:13 - 13:30 | 2 | `Schema.decodeSync` is used inside an Effect generator. `Schema.decode` preserves the typed Effect error channel for this operation without throwing. effect(schemaSyncInEffect)
+
+Schema.encodeSync
+17:64 - 17:81 | 2 | `Schema.encodeSync` is used inside an Effect generator. `Schema.encode` preserves the typed Effect error channel for this operation without throwing. effect(schemaSyncInEffect)
+
+Schema.encodeUnknownSync
+20:19 - 20:43 | 2 | `Schema.encodeUnknownSync` is used inside an Effect generator. `Schema.encodeUnknown` preserves the typed Effect error channel for this operation without throwing. effect(schemaSyncInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/examples/diagnostics/globalConsoleInEffect_thunks.ts b/packages/harness-effect-v3/examples/diagnostics/globalConsoleInEffect_thunks.ts
new file mode 100644
index 00000000..f523fe72
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/globalConsoleInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalConsoleInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const consoleInSync = Effect.sync(() => console.log("sync"))
+
+export const consoleInTryObject = Effect.try({
+ try: () => console.warn("try"),
+ catch: () => new ExampleError()
+})
+
+export const consoleInTryPromise = Effect.tryPromise(async () => console.log("try-promise"))
+
+export const consoleInTryPromiseObject = Effect.tryPromise({
+ try: async () => console.warn("try-promise-object"),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => console.log("nested"))
diff --git a/packages/harness-effect-v3/examples/diagnostics/globalDateInEffect_thunks.ts b/packages/harness-effect-v3/examples/diagnostics/globalDateInEffect_thunks.ts
new file mode 100644
index 00000000..3557bd20
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/globalDateInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalDateInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const dateInSync = Effect.sync(() => Date.now())
+
+export const newDateInTryObject = Effect.try({
+ try: () => new Date(),
+ catch: () => new ExampleError()
+})
+
+export const dateInTryPromise = Effect.tryPromise(async () => Date.now())
+
+export const dateInTryPromiseObject = Effect.tryPromise({
+ try: async () => new Date(),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => new Date())
diff --git a/packages/harness-effect-v3/examples/diagnostics/globalFetchInEffect_thunks.ts b/packages/harness-effect-v3/examples/diagnostics/globalFetchInEffect_thunks.ts
new file mode 100644
index 00000000..f2a713f4
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/globalFetchInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalFetchInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const fetchInSync = Effect.sync(() => fetch("https://example.com/sync"))
+
+export const fetchInTryObject = Effect.try({
+ try: () => fetch("https://example.com/try"),
+ catch: () => new ExampleError()
+})
+
+export const fetchInTryPromise = Effect.tryPromise(async () => fetch("https://example.com/try-promise"))
+
+export const fetchInTryPromiseObject = Effect.tryPromise({
+ try: async () => fetch("https://example.com/try-promise-object"),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => fetch("https://example.com/nested"))
diff --git a/packages/harness-effect-v3/examples/diagnostics/globalRandomInEffect_thunks.ts b/packages/harness-effect-v3/examples/diagnostics/globalRandomInEffect_thunks.ts
new file mode 100644
index 00000000..72b52b83
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/globalRandomInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalRandomInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const randomInSync = Effect.sync(() => Math.random())
+
+export const randomInTryObject = Effect.try({
+ try: () => Math.random(),
+ catch: () => new ExampleError()
+})
+
+export const randomInTryPromise = Effect.tryPromise(async () => Math.random())
+
+export const randomInTryPromiseObject = Effect.tryPromise({
+ try: async () => Math.random(),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => Math.random())
diff --git a/packages/harness-effect-v3/examples/diagnostics/globalTimersInEffect_thunks.ts b/packages/harness-effect-v3/examples/diagnostics/globalTimersInEffect_thunks.ts
new file mode 100644
index 00000000..555dc1db
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/globalTimersInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalTimersInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const timersInSync = Effect.sync(() => setTimeout(() => {}, 10))
+
+export const timersInTryObject = Effect.try({
+ try: () => setInterval(() => {}, 10),
+ catch: () => new ExampleError()
+})
+
+export const timersInTryPromise = Effect.tryPromise(async () => setTimeout(() => {}, 10))
+
+export const timersInTryPromiseObject = Effect.tryPromise({
+ try: async () => setTimeout(() => {}, 10),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => setTimeout(() => {}, 10))
diff --git a/packages/harness-effect-v3/examples/diagnostics/preferSchemaOverJson.ts b/packages/harness-effect-v3/examples/diagnostics/preferSchemaOverJson.ts
index e3e22eec..27979e18 100644
--- a/packages/harness-effect-v3/examples/diagnostics/preferSchemaOverJson.ts
+++ b/packages/harness-effect-v3/examples/diagnostics/preferSchemaOverJson.ts
@@ -57,6 +57,8 @@ export const nestedFunctionInGen = Effect.gen(function*() {
// Should NOT trigger - JSON.parse not as direct expression in Effect.try
export const notDirectExpression = Effect.try(() => {
- const parsed = JSON.parse("{\"indirect\":true}")
- return parsed.indirect
+ return () => {
+ const parsed = JSON.parse("{\"indirect\":true}")
+ return parsed.indirect
+ }
})
diff --git a/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect_thunks.ts b/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect_thunks.ts
new file mode 100644
index 00000000..a59cdecc
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect_thunks.ts
@@ -0,0 +1,21 @@
+// @effect-diagnostics processEnvInEffect:warning
+///
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const envInSync = Effect.sync(() => process.env.SYNC_SECRET)
+
+export const envInTryObject = Effect.try({
+ try: () => process.env.TRY_SECRET,
+ catch: () => new ExampleError()
+})
+
+export const envInTryPromise = Effect.tryPromise(async () => process.env.TRY_PROMISE_SECRET)
+
+export const envInTryPromiseObject = Effect.tryPromise({
+ try: async () => process.env.TRY_PROMISE_OBJECT_SECRET,
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => process.env.NESTED_SECRET)
diff --git a/packages/harness-effect-v3/examples/diagnostics/schemaSyncInEffect_thunks.ts b/packages/harness-effect-v3/examples/diagnostics/schemaSyncInEffect_thunks.ts
new file mode 100644
index 00000000..bfcbc664
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/schemaSyncInEffect_thunks.ts
@@ -0,0 +1,26 @@
+import { Data, Effect, Schema } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+const Person = Schema.Struct({
+ name: Schema.String,
+ age: Schema.Number
+})
+
+export const decodeInSync = Effect.sync(() => Schema.decodeSync(Person)({ name: "John", age: 30 }))
+
+export const decodeInTryObject = Effect.try({
+ try: () => Schema.decodeSync(Person)({ name: "Jane", age: 25 }),
+ catch: () => new ExampleError()
+})
+
+export const encodeInTryPromise = Effect.tryPromise(async () => Schema.encodeSync(Person)({ name: "Bob", age: 40 }))
+
+export const encodeInTryPromiseObject = Effect.tryPromise({
+ try: async () => Schema.encodeUnknownSync(Person)({ name: "Carol", age: 50 }),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(
+ () => () => Schema.decodeSync(Person)({ name: "Nested", age: 10 })
+)
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..3ded6a6d
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+cryptoRandomUUIDInEffect_skipNextLine from 207 to 226
+cryptoRandomUUIDInEffect_skipFile from 207 to 226
+cryptoRandomUUIDInEffect_skipNextLine from 407 to 426
+cryptoRandomUUIDInEffect_skipFile from 407 to 426
+cryptoRandomUUIDInEffect_skipNextLine from 286 to 305
+cryptoRandomUUIDInEffect_skipFile from 286 to 305
+cryptoRandomUUIDInEffect_skipNextLine from 506 to 525
+cryptoRandomUUIDInEffect_skipFile from 506 to 525
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_thunks.ts.output
new file mode 100644
index 00000000..6fab1236
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+crypto.randomUUID()
+6:44 - 6:63 | 0 | This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)
+
+crypto.randomUUID()
+9:13 - 9:32 | 0 | This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)
+
+crypto.randomUUID()
+13:62 - 13:81 | 0 | This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)
+
+crypto.randomUUID()
+16:19 - 16:38 | 0 | This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..57adefa8
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalConsoleInEffect_skipNextLine from 207 to 226
+globalConsoleInEffect_skipFile from 207 to 226
+globalConsoleInEffect_skipNextLine from 413 to 439
+globalConsoleInEffect_skipFile from 413 to 439
+globalConsoleInEffect_skipNextLine from 289 to 308
+globalConsoleInEffect_skipFile from 289 to 308
+globalConsoleInEffect_skipNextLine from 522 to 556
+globalConsoleInEffect_skipFile from 522 to 556
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.output
new file mode 100644
index 00000000..193f5b60
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalConsoleInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+console.log("sync")
+6:47 - 6:66 | 0 | This Effect code uses `console.log`, logging in Effect code is represented through `Effect.log or Logger`. effect(globalConsoleInEffect)
+
+console.warn("try")
+9:13 - 9:32 | 0 | This Effect code uses `console.warn`, logging in Effect code is represented through `Effect.logWarning or Logger`. effect(globalConsoleInEffect)
+
+console.log("try-promise")
+13:65 - 13:91 | 0 | This Effect code uses `console.log`, logging in Effect code is represented through `Effect.log or Logger`. effect(globalConsoleInEffect)
+
+console.warn("try-promise-object")
+16:19 - 16:53 | 0 | This Effect code uses `console.warn`, logging in Effect code is represented through `Effect.logWarning or Logger`. effect(globalConsoleInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..03b101cc
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalDateInEffect_skipNextLine from 201 to 211
+globalDateInEffect_skipFile from 201 to 211
+globalDateInEffect_skipNextLine from 386 to 396
+globalDateInEffect_skipFile from 386 to 396
+globalDateInEffect_skipNextLine from 274 to 284
+globalDateInEffect_skipFile from 274 to 284
+globalDateInEffect_skipNextLine from 476 to 486
+globalDateInEffect_skipFile from 476 to 486
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.output
new file mode 100644
index 00000000..d7e21e4d
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalDateInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+Date.now()
+6:44 - 6:54 | 0 | This Effect code uses `Date.now()`, time access in Effect code is represented through `Clock` from Effect. effect(globalDateInEffect)
+
+new Date()
+9:13 - 9:23 | 0 | This Effect code constructs `new Date()`, date values in Effect code are represented through `DateTime` from Effect. effect(globalDateInEffect)
+
+Date.now()
+13:62 - 13:72 | 0 | This Effect code uses `Date.now()`, time access in Effect code is represented through `Clock` from Effect. effect(globalDateInEffect)
+
+new Date()
+16:19 - 16:29 | 0 | This Effect code constructs `new Date()`, date values in Effect code are represented through `DateTime` from Effect. effect(globalDateInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes
index a6eac123..42189401 100644
--- a/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.codefixes
@@ -1 +1,2 @@
-no codefixes
\ No newline at end of file
+globalFetchInEffect_skipNextLine from 198 to 203
+globalFetchInEffect_skipFile from 198 to 203
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output
index aa033c38..6533c58c 100644
--- a/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_preview.ts.output
@@ -1 +1,2 @@
-// no diagnostics
\ No newline at end of file
+fetch
+6:37 - 6:42 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `effect/unstable/http`. effect(globalFetchInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..98e621b0
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalFetchInEffect_skipNextLine from 203 to 208
+globalFetchInEffect_skipFile from 203 to 208
+globalFetchInEffect_skipNextLine from 432 to 437
+globalFetchInEffect_skipFile from 432 to 437
+globalFetchInEffect_skipNextLine from 297 to 302
+globalFetchInEffect_skipFile from 297 to 302
+globalFetchInEffect_skipNextLine from 553 to 558
+globalFetchInEffect_skipFile from 553 to 558
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.output
new file mode 100644
index 00000000..3fc7e90d
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalFetchInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+fetch
+6:45 - 6:50 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `effect/unstable/http`. effect(globalFetchInEffect)
+
+fetch
+9:13 - 9:18 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `effect/unstable/http`. effect(globalFetchInEffect)
+
+fetch
+13:63 - 13:68 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `effect/unstable/http`. effect(globalFetchInEffect)
+
+fetch
+16:19 - 16:24 | 0 | This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `effect/unstable/http`. effect(globalFetchInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..081f1946
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalRandomInEffect_skipNextLine from 205 to 218
+globalRandomInEffect_skipFile from 205 to 218
+globalRandomInEffect_skipNextLine from 397 to 410
+globalRandomInEffect_skipFile from 397 to 410
+globalRandomInEffect_skipNextLine from 280 to 293
+globalRandomInEffect_skipFile from 280 to 293
+globalRandomInEffect_skipNextLine from 492 to 505
+globalRandomInEffect_skipFile from 492 to 505
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.output
new file mode 100644
index 00000000..56773ceb
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalRandomInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+Math.random()
+6:46 - 6:59 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
+
+Math.random()
+9:13 - 9:26 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
+
+Math.random()
+13:64 - 13:77 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
+
+Math.random()
+16:19 - 16:32 | 0 | This Effect code uses `Math.random()`, randomness is represented through the Effect `Random` service. effect(globalRandomInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..cea72497
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+globalTimersInEffect_skipNextLine from 205 to 229
+globalTimersInEffect_skipFile from 205 to 229
+globalTimersInEffect_skipNextLine from 420 to 444
+globalTimersInEffect_skipFile from 420 to 444
+globalTimersInEffect_skipNextLine from 291 to 316
+globalTimersInEffect_skipFile from 291 to 316
+globalTimersInEffect_skipNextLine from 526 to 550
+globalTimersInEffect_skipFile from 526 to 550
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.output
new file mode 100644
index 00000000..1547ec24
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/globalTimersInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+setTimeout(() => {}, 10)
+6:46 - 6:70 | 0 | This Effect code uses `setTimeout`, the corresponding timer API in this context is `Effect.sleep or Schedule` from Effect. effect(globalTimersInEffect)
+
+setInterval(() => {}, 10)
+9:13 - 9:38 | 0 | This Effect code uses `setInterval`, the corresponding timer API in this context is `Schedule or Effect.repeat` from Effect. effect(globalTimersInEffect)
+
+setTimeout(() => {}, 10)
+13:64 - 13:88 | 0 | This Effect code uses `setTimeout`, the corresponding timer API in this context is `Effect.sleep or Schedule` from Effect. effect(globalTimersInEffect)
+
+setTimeout(() => {}, 10)
+16:19 - 16:43 | 0 | This Effect code uses `setTimeout`, the corresponding timer API in this context is `Effect.sleep or Schedule` from Effect. effect(globalTimersInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..430101f9
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.codefixes
@@ -0,0 +1,8 @@
+processEnvInEffect_skipNextLine from 231 to 254
+processEnvInEffect_skipFile from 231 to 254
+processEnvInEffect_skipNextLine from 436 to 466
+processEnvInEffect_skipFile from 436 to 466
+processEnvInEffect_skipNextLine from 313 to 335
+processEnvInEffect_skipFile from 313 to 335
+processEnvInEffect_skipNextLine from 545 to 582
+processEnvInEffect_skipFile from 545 to 582
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.output
new file mode 100644
index 00000000..fe1b56db
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_thunks.ts.output
@@ -0,0 +1,11 @@
+process.env.SYNC_SECRET
+7:43 - 7:66 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env.TRY_SECRET
+10:13 - 10:35 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env.TRY_PROMISE_SECRET
+14:61 - 14:91 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env.TRY_PROMISE_OBJECT_SECRET
+17:19 - 17:56 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.codefixes
new file mode 100644
index 00000000..a6eac123
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.codefixes
@@ -0,0 +1 @@
+no codefixes
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.output
new file mode 100644
index 00000000..aa033c38
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/schemaSyncInEffect_thunks.ts.output
@@ -0,0 +1 @@
+// no diagnostics
\ No newline at end of file
diff --git a/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect_thunks.ts
new file mode 100644
index 00000000..b7021b91
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics cryptoRandomUUIDInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const uuidInSync = Effect.sync(() => crypto.randomUUID())
+
+export const uuidInTryObject = Effect.try({
+ try: () => crypto.randomUUID(),
+ catch: () => new ExampleError()
+})
+
+export const uuidInTryPromise = Effect.tryPromise(async () => crypto.randomUUID())
+
+export const uuidInTryPromiseObject = Effect.tryPromise({
+ try: async () => crypto.randomUUID(),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => crypto.randomUUID())
diff --git a/packages/harness-effect-v4/examples/diagnostics/globalConsoleInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/globalConsoleInEffect_thunks.ts
new file mode 100644
index 00000000..f523fe72
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/globalConsoleInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalConsoleInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const consoleInSync = Effect.sync(() => console.log("sync"))
+
+export const consoleInTryObject = Effect.try({
+ try: () => console.warn("try"),
+ catch: () => new ExampleError()
+})
+
+export const consoleInTryPromise = Effect.tryPromise(async () => console.log("try-promise"))
+
+export const consoleInTryPromiseObject = Effect.tryPromise({
+ try: async () => console.warn("try-promise-object"),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => console.log("nested"))
diff --git a/packages/harness-effect-v4/examples/diagnostics/globalDateInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/globalDateInEffect_thunks.ts
new file mode 100644
index 00000000..3557bd20
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/globalDateInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalDateInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const dateInSync = Effect.sync(() => Date.now())
+
+export const newDateInTryObject = Effect.try({
+ try: () => new Date(),
+ catch: () => new ExampleError()
+})
+
+export const dateInTryPromise = Effect.tryPromise(async () => Date.now())
+
+export const dateInTryPromiseObject = Effect.tryPromise({
+ try: async () => new Date(),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => new Date())
diff --git a/packages/harness-effect-v4/examples/diagnostics/globalFetchInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/globalFetchInEffect_thunks.ts
new file mode 100644
index 00000000..f2a713f4
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/globalFetchInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalFetchInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const fetchInSync = Effect.sync(() => fetch("https://example.com/sync"))
+
+export const fetchInTryObject = Effect.try({
+ try: () => fetch("https://example.com/try"),
+ catch: () => new ExampleError()
+})
+
+export const fetchInTryPromise = Effect.tryPromise(async () => fetch("https://example.com/try-promise"))
+
+export const fetchInTryPromiseObject = Effect.tryPromise({
+ try: async () => fetch("https://example.com/try-promise-object"),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => fetch("https://example.com/nested"))
diff --git a/packages/harness-effect-v4/examples/diagnostics/globalRandomInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/globalRandomInEffect_thunks.ts
new file mode 100644
index 00000000..72b52b83
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/globalRandomInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalRandomInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const randomInSync = Effect.sync(() => Math.random())
+
+export const randomInTryObject = Effect.try({
+ try: () => Math.random(),
+ catch: () => new ExampleError()
+})
+
+export const randomInTryPromise = Effect.tryPromise(async () => Math.random())
+
+export const randomInTryPromiseObject = Effect.tryPromise({
+ try: async () => Math.random(),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => Math.random())
diff --git a/packages/harness-effect-v4/examples/diagnostics/globalTimersInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/globalTimersInEffect_thunks.ts
new file mode 100644
index 00000000..555dc1db
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/globalTimersInEffect_thunks.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics globalTimersInEffect:warning
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const timersInSync = Effect.sync(() => setTimeout(() => {}, 10))
+
+export const timersInTryObject = Effect.try({
+ try: () => setInterval(() => {}, 10),
+ catch: () => new ExampleError()
+})
+
+export const timersInTryPromise = Effect.tryPromise(async () => setTimeout(() => {}, 10))
+
+export const timersInTryPromiseObject = Effect.tryPromise({
+ try: async () => setTimeout(() => {}, 10),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => setTimeout(() => {}, 10))
diff --git a/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect_thunks.ts
new file mode 100644
index 00000000..a59cdecc
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect_thunks.ts
@@ -0,0 +1,21 @@
+// @effect-diagnostics processEnvInEffect:warning
+///
+import { Data, Effect } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+export const envInSync = Effect.sync(() => process.env.SYNC_SECRET)
+
+export const envInTryObject = Effect.try({
+ try: () => process.env.TRY_SECRET,
+ catch: () => new ExampleError()
+})
+
+export const envInTryPromise = Effect.tryPromise(async () => process.env.TRY_PROMISE_SECRET)
+
+export const envInTryPromiseObject = Effect.tryPromise({
+ try: async () => process.env.TRY_PROMISE_OBJECT_SECRET,
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(() => () => process.env.NESTED_SECRET)
diff --git a/packages/harness-effect-v4/examples/diagnostics/schemaSyncInEffect_thunks.ts b/packages/harness-effect-v4/examples/diagnostics/schemaSyncInEffect_thunks.ts
new file mode 100644
index 00000000..bfcbc664
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/schemaSyncInEffect_thunks.ts
@@ -0,0 +1,26 @@
+import { Data, Effect, Schema } from "effect"
+
+class ExampleError extends Data.TaggedError("ExampleError")<{}> {}
+
+const Person = Schema.Struct({
+ name: Schema.String,
+ age: Schema.Number
+})
+
+export const decodeInSync = Effect.sync(() => Schema.decodeSync(Person)({ name: "John", age: 30 }))
+
+export const decodeInTryObject = Effect.try({
+ try: () => Schema.decodeSync(Person)({ name: "Jane", age: 25 }),
+ catch: () => new ExampleError()
+})
+
+export const encodeInTryPromise = Effect.tryPromise(async () => Schema.encodeSync(Person)({ name: "Bob", age: 40 }))
+
+export const encodeInTryPromiseObject = Effect.tryPromise({
+ try: async () => Schema.encodeUnknownSync(Person)({ name: "Carol", age: 50 }),
+ catch: () => new ExampleError()
+})
+
+export const shouldNotTriggerReturnedFunction = Effect.sync(
+ () => () => Schema.decodeSync(Person)({ name: "Nested", age: 10 })
+)
diff --git a/packages/language-service/src/core/TypeParser.ts b/packages/language-service/src/core/TypeParser.ts
index 50dee22e..c9e0cf13 100644
--- a/packages/language-service/src/core/TypeParser.ts
+++ b/packages/language-service/src/core/TypeParser.ts
@@ -55,6 +55,15 @@ export interface ParsedPipingFlowTransformation {
kind: "pipe" | "pipeable" | "call" | "effectFn" | "effectFnUntraced" // the kind of transformation
}
+export const enum EffectContextFlags {
+ None = 0,
+ CanYieldEffect = 1 << 0,
+ InEffectConstructorThunk = 1 << 1,
+ InEffect = CanYieldEffect | InEffectConstructorThunk,
+ PendingNextFunctionIsEffectThunk = 1 << 2,
+ PendingNextObjectTryPropertyIsEffectThunk = 1 << 3
+}
+
export interface TypeParser {
effectType: (
type: ts.Type,
@@ -154,22 +163,12 @@ export interface TypeParser {
},
TypeParserIssue
>
- findEnclosingScopes: (
+ getEffectContextFlags: (
node: ts.Node
- ) => Nano.Nano<
- {
- inEffect: boolean
- scopeNode: ts.FunctionLikeDeclaration | undefined
- effectGen: {
- node: ts.Node
- effectModule: ts.Node | ts.Expression
- generatorFunction: ts.FunctionExpression
- body: ts.Block
- pipeArguments?: ReadonlyArray
- } | undefined
- },
- never
- >
+ ) => Nano.Nano
+ getEffectYieldGeneratorFunction: (
+ node: ts.Node
+ ) => Nano.Nano
effectFn: (
node: ts.Node
) => Nano.Nano<
@@ -1256,95 +1255,240 @@ export function make(
(node) => node
)
- const findEnclosingScopes = Nano.fn("TypeParser.findEnclosingScopes")(function*(
- startNode: ts.Node
- ) {
- let currentParent: ts.Node | undefined = startNode.parent
- let scopeNode: ts.FunctionLikeDeclaration | undefined = undefined
- let effectGenResult: {
- node: ts.Node
- effectModule: ts.Node | ts.Expression
- generatorFunction: ts.FunctionExpression
- body: ts.Block
- pipeArguments?: ReadonlyArray
- } | undefined = undefined
+ type EffectContextAnalysis = {
+ flags: WeakMap
+ generatorFunctions: WeakMap
+ }
+
+ const effectContextAnalysis = Nano.cachedBy(
+ Nano.fn("TypeParser.effectContextAnalysis")(function*(sourceFile: ts.SourceFile) {
+ const flags = new WeakMap()
+ const generatorFunctions = new WeakMap()
+
+ const isFunctionLikeNode = (node: ts.Node): node is ts.FunctionLikeDeclaration =>
+ ts.isFunctionExpression(node) ||
+ ts.isFunctionDeclaration(node) ||
+ ts.isMethodDeclaration(node) ||
+ ts.isArrowFunction(node) ||
+ ts.isGetAccessorDeclaration(node) ||
+ ts.isSetAccessorDeclaration(node)
+
+ const isTransparentPendingNode = (node: ts.Node): node is ts.Expression =>
+ ts.isParenthesizedExpression(node) ||
+ ts.isSatisfiesExpression(node) ||
+ ts.isAsExpression(node) ||
+ ts.isNonNullExpression(node) ||
+ ts.isTypeAssertionExpression(node)
+
+ const transparentPendingExpression = (node: ts.Expression): ts.Expression | undefined => {
+ if (ts.isParenthesizedExpression(node)) return node.expression
+ if (ts.isSatisfiesExpression(node)) return node.expression
+ if (ts.isAsExpression(node)) return node.expression
+ if (ts.isNonNullExpression(node)) return node.expression
+ if (ts.isTypeAssertionExpression(node)) return node.expression
+ return undefined
+ }
+
+ const pendingEnableFlags = new WeakMap()
+ const pendingDisableFlags = new WeakMap()
+ const workQueue: Array = [sourceFile]
+
+ const pendingFlagsMask = EffectContextFlags.PendingNextFunctionIsEffectThunk |
+ EffectContextFlags.PendingNextObjectTryPropertyIsEffectThunk
+
+ const functionScopeResetFlags = EffectContextFlags.CanYieldEffect |
+ EffectContextFlags.InEffectConstructorThunk |
+ pendingFlagsMask
+
+ const setPendingEnableFlags = (node: ts.Node, nextFlags: EffectContextFlags) => {
+ pendingEnableFlags.set(node, (pendingEnableFlags.get(node) ?? EffectContextFlags.None) | nextFlags)
+ }
+
+ const setPendingDisableFlags = (node: ts.Node, nextFlags: EffectContextFlags) => {
+ pendingDisableFlags.set(node, (pendingDisableFlags.get(node) ?? EffectContextFlags.None) | nextFlags)
+ }
+
+ while (workQueue.length > 0) {
+ const current = workQueue.pop()!
+ let currentFlags = current.parent
+ ? (flags.get(current.parent) ?? EffectContextFlags.None)
+ : EffectContextFlags.None
+ const currentGeneratorFunction = generatorFunctions.has(current)
+ ? generatorFunctions.get(current)
+ : current.parent
+ ? generatorFunctions.get(current.parent)
+ : undefined
+
+ if (pendingDisableFlags.has(current)) {
+ currentFlags &= ~pendingDisableFlags.get(current)!
+ }
+
+ if (pendingEnableFlags.has(current)) {
+ currentFlags |= pendingEnableFlags.get(current)!
+ }
- while (currentParent) {
- const nodeToCheck: ts.Node = currentParent
+ flags.set(current, currentFlags)
+ generatorFunctions.set(
+ current,
+ (currentFlags & EffectContextFlags.CanYieldEffect) !== 0 ? currentGeneratorFunction : undefined
+ )
- // Check if this node introduces a function scope
- if (!scopeNode) {
if (
- ts.isFunctionExpression(nodeToCheck) ||
- ts.isFunctionDeclaration(nodeToCheck) ||
- ts.isMethodDeclaration(nodeToCheck) ||
- ts.isArrowFunction(nodeToCheck) ||
- ts.isGetAccessorDeclaration(nodeToCheck) ||
- ts.isSetAccessorDeclaration(nodeToCheck)
+ (currentFlags & EffectContextFlags.PendingNextFunctionIsEffectThunk) !== 0 &&
+ (ts.isArrowFunction(current) || ts.isFunctionExpression(current))
) {
- scopeNode = nodeToCheck
+ setPendingEnableFlags(current.body, EffectContextFlags.InEffectConstructorThunk)
+ setPendingDisableFlags(current.body, pendingFlagsMask)
+ } else if (
+ (currentFlags & EffectContextFlags.PendingNextObjectTryPropertyIsEffectThunk) !== 0 &&
+ ts.isObjectLiteralExpression(current)
+ ) {
+ ts.forEachChild(current, (child) => {
+ setPendingDisableFlags(child, pendingFlagsMask)
+ return undefined
+ })
+
+ for (const property of current.properties) {
+ if (!ts.isPropertyAssignment(property)) continue
+ if (!ts.isIdentifier(property.name) || ts.idText(property.name) !== "try") continue
+ setPendingEnableFlags(property.initializer, EffectContextFlags.PendingNextFunctionIsEffectThunk)
+ }
+ } else if (isTransparentPendingNode(current)) {
+ const expression = transparentPendingExpression(current)
+ if (expression) {
+ ts.forEachChild(current, (child) => {
+ if (child !== expression) {
+ setPendingDisableFlags(child, pendingFlagsMask)
+ }
+ return undefined
+ })
+ setPendingEnableFlags(expression, currentFlags & pendingFlagsMask)
+ }
+ } else if ((currentFlags & pendingFlagsMask) !== 0) {
+ ts.forEachChild(current, (child) => {
+ setPendingDisableFlags(child, pendingFlagsMask)
+ return undefined
+ })
}
- }
- // Try to parse as Effect.gen, Effect.fnUntraced, or Effect.fn
- if (!effectGenResult) {
- const isEffectGen = yield* pipe(
- effectGen(nodeToCheck),
+ const parsedEffectGenerator = yield* pipe(
+ effectGen(current),
Nano.map((result) => ({
- node: result.node,
- effectModule: result.effectModule,
- generatorFunction: result.generatorFunction,
- body: result.body
+ body: result.body,
+ generatorFunction: result.generatorFunction
})),
Nano.orElse(() =>
pipe(
- effectFnUntracedGen(nodeToCheck),
+ effectFnUntracedGen(current),
Nano.map((result) => ({
- node: result.node,
- effectModule: result.effectModule,
- generatorFunction: result.generatorFunction,
body: result.body,
- pipeArguments: result.pipeArguments
+ generatorFunction: result.generatorFunction
}))
)
),
Nano.orElse(() =>
pipe(
- effectFnGen(nodeToCheck),
+ effectFnGen(current),
Nano.map((result) => ({
- node: result.node,
- effectModule: result.effectModule,
- generatorFunction: result.generatorFunction,
body: result.body,
- pipeArguments: result.pipeArguments
+ generatorFunction: result.generatorFunction
}))
)
),
Nano.orUndefined
)
- if (isEffectGen) {
- effectGenResult = isEffectGen
+ if (parsedEffectGenerator) {
+ setPendingEnableFlags(
+ parsedEffectGenerator.body,
+ EffectContextFlags.CanYieldEffect
+ )
+ generatorFunctions.set(parsedEffectGenerator.body, parsedEffectGenerator.generatorFunction)
}
- }
- // If we found both, we can stop
- if (scopeNode && effectGenResult) {
- break
+ if (ts.isCallExpression(current) && current.arguments.length > 0) {
+ const effectThunkArgument = current.arguments[0]
+
+ if (yield* Nano.orUndefined(isNodeReferenceToEffectModuleApi("sync")(current.expression))) {
+ setPendingEnableFlags(effectThunkArgument, EffectContextFlags.PendingNextFunctionIsEffectThunk)
+ }
+
+ if (yield* Nano.orUndefined(isNodeReferenceToEffectModuleApi("promise")(current.expression))) {
+ setPendingEnableFlags(effectThunkArgument, EffectContextFlags.PendingNextFunctionIsEffectThunk)
+ }
+
+ if (yield* Nano.orUndefined(isNodeReferenceToEffectModuleApi("callback")(current.expression))) {
+ setPendingEnableFlags(effectThunkArgument, EffectContextFlags.PendingNextFunctionIsEffectThunk)
+ }
+
+ if (yield* Nano.orUndefined(isNodeReferenceToEffectModuleApi("suspend")(current.expression))) {
+ setPendingEnableFlags(effectThunkArgument, EffectContextFlags.PendingNextFunctionIsEffectThunk)
+ }
+
+ if (yield* Nano.orUndefined(isNodeReferenceToEffectModuleApi("try")(current.expression))) {
+ setPendingEnableFlags(
+ effectThunkArgument,
+ EffectContextFlags.PendingNextFunctionIsEffectThunk |
+ EffectContextFlags.PendingNextObjectTryPropertyIsEffectThunk
+ )
+ }
+
+ if (yield* Nano.orUndefined(isNodeReferenceToEffectModuleApi("tryPromise")(current.expression))) {
+ setPendingEnableFlags(
+ effectThunkArgument,
+ EffectContextFlags.PendingNextFunctionIsEffectThunk |
+ EffectContextFlags.PendingNextObjectTryPropertyIsEffectThunk
+ )
+ }
+ }
+
+ if (isFunctionLikeNode(current)) {
+ ts.forEachChild(current, (child) => {
+ setPendingDisableFlags(child, functionScopeResetFlags)
+ return undefined
+ })
+ }
+
+ ts.forEachChild(current, (child) => {
+ workQueue.push(child)
+ return undefined
+ })
}
- currentParent = nodeToCheck.parent
- }
+ return {
+ flags,
+ generatorFunctions
+ } satisfies EffectContextAnalysis
+ }),
+ "TypeParser.effectContextAnalysis",
+ (sourceFile) => sourceFile
+ )
- return {
- inEffect: effectGenResult !== undefined &&
- effectGenResult.body.statements.length > 0 &&
- scopeNode === effectGenResult.generatorFunction,
- scopeNode,
- effectGen: effectGenResult
+ const getEffectContextFlags = Nano.fn("TypeParser.getEffectContextFlags")(function*(node: ts.Node) {
+ const sourceFile = tsUtils.getSourceFileOfNode(node)
+
+ if (!sourceFile) {
+ return EffectContextFlags.None
}
+
+ const analysis = yield* effectContextAnalysis(sourceFile)
+ return (analysis.flags.get(node) ?? EffectContextFlags.None) &
+ EffectContextFlags.InEffect
})
+ const getEffectYieldGeneratorFunction = Nano.fn("TypeParser.getEffectYieldGeneratorFunction")(
+ function*(node: ts.Node) {
+ const sourceFile = tsUtils.getSourceFileOfNode(node)
+
+ if (!sourceFile) {
+ return undefined
+ }
+
+ const analysis = yield* effectContextAnalysis(sourceFile)
+ return analysis.generatorFunctions.get(node)
+ }
+ )
+
const effectFn = Nano.cachedBy(
function(node: ts.Node) {
// Effect.fn("name")(regularFunction, ...pipeArgs) or Effect.fn(regularFunction, ...pipeArgs)
@@ -3192,7 +3336,8 @@ export function make(
effectGen,
effectFnUntracedGen,
effectFnGen,
- findEnclosingScopes,
+ getEffectContextFlags,
+ getEffectYieldGeneratorFunction,
effectFn,
extendsCauseYieldableError,
unnecessaryEffectGen,
diff --git a/packages/language-service/src/diagnostics/cryptoRandomUUIDInEffect.ts b/packages/language-service/src/diagnostics/cryptoRandomUUIDInEffect.ts
index c3b027a8..bace6158 100644
--- a/packages/language-service/src/diagnostics/cryptoRandomUUIDInEffect.ts
+++ b/packages/language-service/src/diagnostics/cryptoRandomUUIDInEffect.ts
@@ -37,7 +37,7 @@ export const makeCryptoRandomUUIDApply = (checkInEffect: boolean) =>
if (!symbol) continue
if (typeCheckerUtils.resolveToGlobalSymbol(symbol) !== cryptoSymbol) continue
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect = ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (inEffect !== checkInEffect) continue
report({
diff --git a/packages/language-service/src/diagnostics/globalConsoleInEffect.ts b/packages/language-service/src/diagnostics/globalConsoleInEffect.ts
index bee1d63f..dead6b01 100644
--- a/packages/language-service/src/diagnostics/globalConsoleInEffect.ts
+++ b/packages/language-service/src/diagnostics/globalConsoleInEffect.ts
@@ -49,7 +49,7 @@ export const makeGlobalConsoleApply = (checkInEffect: boolean) =>
if (!symbol) continue
if (typeCheckerUtils.resolveToGlobalSymbol(symbol) !== consoleSymbol) continue
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect = ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (inEffect !== checkInEffect) continue
report({
diff --git a/packages/language-service/src/diagnostics/globalDateInEffect.ts b/packages/language-service/src/diagnostics/globalDateInEffect.ts
index abfc745b..5aedd5b9 100644
--- a/packages/language-service/src/diagnostics/globalDateInEffect.ts
+++ b/packages/language-service/src/diagnostics/globalDateInEffect.ts
@@ -51,7 +51,7 @@ export const makeGlobalDateApply = (checkInEffect: boolean) =>
if (!symbol) continue
if (typeCheckerUtils.resolveToGlobalSymbol(symbol) !== dateSymbol) continue
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect = ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (inEffect !== checkInEffect) continue
report({ location: node, messageText, fixes: [] })
diff --git a/packages/language-service/src/diagnostics/globalFetchInEffect.ts b/packages/language-service/src/diagnostics/globalFetchInEffect.ts
index 884a8514..0988cdef 100644
--- a/packages/language-service/src/diagnostics/globalFetchInEffect.ts
+++ b/packages/language-service/src/diagnostics/globalFetchInEffect.ts
@@ -35,7 +35,8 @@ export const makeGlobalFetchApply = (checkInEffect: boolean) =>
if (ts.isCallExpression(node)) {
const symbol = typeChecker.getSymbolAtLocation(node.expression)
if (symbol && typeCheckerUtils.resolveToGlobalSymbol(symbol) === fetchSymbol) {
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect =
+ ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (inEffect === checkInEffect) {
report({
location: node.expression,
diff --git a/packages/language-service/src/diagnostics/globalRandomInEffect.ts b/packages/language-service/src/diagnostics/globalRandomInEffect.ts
index 6534b676..c6e361de 100644
--- a/packages/language-service/src/diagnostics/globalRandomInEffect.ts
+++ b/packages/language-service/src/diagnostics/globalRandomInEffect.ts
@@ -37,7 +37,7 @@ export const makeGlobalRandomApply = (checkInEffect: boolean) =>
if (!symbol) continue
if (typeCheckerUtils.resolveToGlobalSymbol(symbol) !== mathSymbol) continue
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect = ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (inEffect !== checkInEffect) continue
report({
diff --git a/packages/language-service/src/diagnostics/globalTimersInEffect.ts b/packages/language-service/src/diagnostics/globalTimersInEffect.ts
index 9d365b0f..28be1462 100644
--- a/packages/language-service/src/diagnostics/globalTimersInEffect.ts
+++ b/packages/language-service/src/diagnostics/globalTimersInEffect.ts
@@ -61,7 +61,7 @@ export const makeGlobalTimersApply = (checkInEffect: boolean) =>
}
if (!messageText) continue
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect = ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (inEffect !== checkInEffect) continue
report({ location: node, messageText, fixes: [] })
diff --git a/packages/language-service/src/diagnostics/missingReturnYieldStar.ts b/packages/language-service/src/diagnostics/missingReturnYieldStar.ts
index 7ca85b4d..b3ee2d53 100644
--- a/packages/language-service/src/diagnostics/missingReturnYieldStar.ts
+++ b/packages/language-service/src/diagnostics/missingReturnYieldStar.ts
@@ -44,7 +44,8 @@ export const missingReturnYieldStar = LSP.createDiagnostic({
if (!(Option.isSome(maybeEffect) && maybeEffect.value.A.flags & ts.TypeFlags.Never)) continue
// Ensure we're in the direct body scope of an Effect.gen-like function.
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect =
+ ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.CanYieldEffect) !== 0
if (!inEffect) continue
const fix = [{
diff --git a/packages/language-service/src/diagnostics/nestedEffectGenYield.ts b/packages/language-service/src/diagnostics/nestedEffectGenYield.ts
index 3d1ab19b..aba508c1 100644
--- a/packages/language-service/src/diagnostics/nestedEffectGenYield.ts
+++ b/packages/language-service/src/diagnostics/nestedEffectGenYield.ts
@@ -29,7 +29,8 @@ export const nestedEffectGenYield = LSP.createDiagnostic({
if (!ts.isYieldExpression(node) || !node.asteriskToken || !node.expression) continue
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect =
+ ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.CanYieldEffect) !== 0
if (!inEffect) continue
const bareNestedEffectGen = yield* Nano.orUndefined(typeParser.effectGen(node.expression))
diff --git a/packages/language-service/src/diagnostics/preferSchemaOverJson.ts b/packages/language-service/src/diagnostics/preferSchemaOverJson.ts
index 2fc9df70..88ac3a05 100644
--- a/packages/language-service/src/diagnostics/preferSchemaOverJson.ts
+++ b/packages/language-service/src/diagnostics/preferSchemaOverJson.ts
@@ -135,7 +135,8 @@ export const preferSchemaOverJson = LSP.createDiagnostic({
// Match direct JSON.parse/stringify inside Effect generator
const jsonMethodInEffectGen = Nano.fn("preferSchemaOverJson.jsonMethodInEffectGen")(
function*(jsonCall: ts.CallExpression) {
- const { inEffect } = yield* typeParser.findEnclosingScopes(jsonCall)
+ const inEffect =
+ ((yield* typeParser.getEffectContextFlags(jsonCall)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (!inEffect) {
return yield* TypeParser.TypeParserIssue.issue
}
diff --git a/packages/language-service/src/diagnostics/processEnvInEffect.ts b/packages/language-service/src/diagnostics/processEnvInEffect.ts
index 963086b4..3aa1311f 100644
--- a/packages/language-service/src/diagnostics/processEnvInEffect.ts
+++ b/packages/language-service/src/diagnostics/processEnvInEffect.ts
@@ -46,7 +46,7 @@ export const makeProcessEnvApply = (checkInEffect: boolean) =>
if (!symbol) continue
if (typeCheckerUtils.resolveToGlobalSymbol(symbol) !== processSymbol) continue
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect = ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (inEffect !== checkInEffect) continue
report({
diff --git a/packages/language-service/src/diagnostics/runEffectInsideEffect.ts b/packages/language-service/src/diagnostics/runEffectInsideEffect.ts
index fb56a512..5a20c4e9 100644
--- a/packages/language-service/src/diagnostics/runEffectInsideEffect.ts
+++ b/packages/language-service/src/diagnostics/runEffectInsideEffect.ts
@@ -21,6 +21,22 @@ export const runEffectInsideEffect = LSP.createDiagnostic({
const tsUtils = yield* Nano.service(TypeScriptUtils.TypeScriptUtils)
const supportedEffect = typeParser.supportedEffect()
+ const getContainingFunctionLike = (node: ts.Node): ts.FunctionLikeDeclaration | undefined => {
+ for (let current = node.parent; current; current = current.parent) {
+ if (
+ ts.isFunctionExpression(current) ||
+ ts.isFunctionDeclaration(current) ||
+ ts.isMethodDeclaration(current) ||
+ ts.isArrowFunction(current) ||
+ ts.isGetAccessorDeclaration(current) ||
+ ts.isSetAccessorDeclaration(current)
+ ) {
+ return current
+ }
+ }
+ return undefined
+ }
+
const parseEffectMethod = (node: ts.Node, methodName: string) =>
pipe(
typeParser.isNodeReferenceToEffectModuleApi(methodName)(node),
@@ -53,8 +69,66 @@ export const runEffectInsideEffect = LSP.createDiagnostic({
if (Option.isNone(isEffectRunCall)) continue
- // Find enclosing scope and Effect generator using TypeParser helper
- const { effectGen, scopeNode } = yield* typeParser.findEnclosingScopes(node)
+ const scopeNode = getContainingFunctionLike(node)
+ let generatorFunction = yield* typeParser.getEffectYieldGeneratorFunction(node)
+
+ if (!generatorFunction) {
+ for (let current = node.parent; current; current = current.parent) {
+ const currentFlags = yield* typeParser.getEffectContextFlags(current)
+ if ((currentFlags & TypeParser.EffectContextFlags.CanYieldEffect) === 0) continue
+ generatorFunction = yield* typeParser.getEffectYieldGeneratorFunction(current)
+ if (generatorFunction) break
+ }
+ }
+
+ let effectGen: {
+ node: ts.Node
+ effectModule: ts.Node | ts.Expression
+ generatorFunction: ts.FunctionExpression
+ body: ts.Block
+ pipeArguments?: ReadonlyArray
+ } | undefined = undefined
+
+ if (generatorFunction) {
+ for (let current = generatorFunction.parent; current; current = current.parent) {
+ effectGen = yield* pipe(
+ typeParser.effectGen(current),
+ Nano.map((result) => ({
+ node: result.node,
+ effectModule: result.effectModule,
+ generatorFunction: result.generatorFunction,
+ body: result.body
+ })),
+ Nano.orElse(() =>
+ pipe(
+ typeParser.effectFnUntracedGen(current),
+ Nano.map((result) => ({
+ node: result.node,
+ effectModule: result.effectModule,
+ generatorFunction: result.generatorFunction,
+ body: result.body,
+ pipeArguments: result.pipeArguments
+ }))
+ )
+ ),
+ Nano.orElse(() =>
+ pipe(
+ typeParser.effectFnGen(current),
+ Nano.map((result) => ({
+ node: result.node,
+ effectModule: result.effectModule,
+ generatorFunction: result.generatorFunction,
+ body: result.body,
+ pipeArguments: result.pipeArguments
+ }))
+ )
+ ),
+ Nano.orUndefined
+ )
+
+ if (effectGen) break
+ }
+ }
if (effectGen && effectGen.body.statements.length > 0) {
const nodeText = sourceFile.text.substring(
diff --git a/packages/language-service/src/diagnostics/schemaSyncInEffect.ts b/packages/language-service/src/diagnostics/schemaSyncInEffect.ts
index 61a6b031..de984f29 100644
--- a/packages/language-service/src/diagnostics/schemaSyncInEffect.ts
+++ b/packages/language-service/src/diagnostics/schemaSyncInEffect.ts
@@ -64,7 +64,7 @@ export const schemaSyncInEffect = LSP.createDiagnostic({
if (Option.isNone(isSchemaSyncCall)) continue
// Find enclosing scope and Effect generator using TypeParser helper
- const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ const inEffect = ((yield* typeParser.getEffectContextFlags(node)) & TypeParser.EffectContextFlags.InEffect) !== 0
if (!inEffect) continue
diff --git a/packages/language-service/src/metadata.json b/packages/language-service/src/metadata.json
index a5b27603..f0cc6583 100644
--- a/packages/language-service/src/metadata.json
+++ b/packages/language-service/src/metadata.json
@@ -978,7 +978,13 @@
],
"preview": {
"sourceText": "import { Effect } from \"effect\"\n\nexport const preview = Effect.gen(function*() {\n return yield* Effect.promise(() => fetch(\"https://example.com\"))\n})\n",
- "diagnostics": []
+ "diagnostics": [
+ {
+ "start": 118,
+ "end": 123,
+ "text": "This Effect code calls the global `fetch` function, HTTP requests in Effect code are represented through `HttpClient` from `effect/unstable/http`. effect(globalFetchInEffect)"
+ }
+ ]
}
},
{