Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-issue179-effect-gen-this.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/tsgo": patch
---

Fix false-positive `TS2683` diagnostics for `Effect.gen({ self: this }, ...)` by avoiding eager call-signature analysis in affected Effect contexts.

This includes nested `Effect.gen` generic calls plus related cases such as `this` in callees, `Effect.sync`/`Effect.tryPromise` callbacks, `.pipe()` chains, and curried wrappers.
41 changes: 38 additions & 3 deletions internal/typeparser/data_first_signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ func (tp *TypeParser) DataFirstOrLastCall(node *ast.Node) *ParsedDataFirstOrLast
}
}

if tp.GetEffectContextFlags(node) != 0 {
if callHasNonBareThisArgument(call.Arguments.Nodes) ||
containsThisKeyword(call.Expression) ||
callHasArrowFunctionArgument(call.Arguments.Nodes) ||
callHasGenericCallee(tp, call) {
return nil
}
}

c := tp.checker
resolved := c.GetResolvedSignature(node)
if resolved == nil || resolved.Declaration() == nil {
Expand All @@ -39,9 +48,6 @@ func (tp *TypeParser) DataFirstOrLastCall(node *ast.Node) *ParsedDataFirstOrLast
if len(resolved.Parameters()) != len(call.Arguments.Nodes) {
return nil
}
if tp.GetEffectContextFlags(node)&EffectContextFlagCanYieldEffect != 0 && callHasNonBareThisArgument(call.Arguments.Nodes) {
return nil
}

resolvedSymbol := checker.Checker_getSymbolOfDeclaration(c, resolved.Declaration())
if resolvedSymbol == nil {
Expand Down Expand Up @@ -121,6 +127,35 @@ func callHasNonBareThisArgument(args []*ast.Node) bool {
return false
}

func callHasArrowFunctionArgument(args []*ast.Node) bool {
for _, arg := range args {
if arg != nil && arg.Kind == ast.KindArrowFunction {
return true
}
}
return false
}

func callHasGenericCallee(tp *TypeParser, call *ast.CallExpression) bool {
if call == nil || call.Expression == nil {
return false
}
sym := tp.ReferenceSymbolAtNode(call.Expression)
if sym == nil {
return false
}
for _, decl := range sym.Declarations {
if decl == nil {
continue
}
typeParams := GetFunctionLikeTypeParameters(decl)
if typeParams != nil && len(typeParams.Nodes) > 0 {
return true
}
}
return false
}

func containsThisKeyword(node *ast.Node) bool {
if node == nil {
return false
Expand Down
52 changes: 12 additions & 40 deletions internal/typeparser/effect_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,16 @@ package typeparser

import (
"github.com/microsoft/typescript-go/shim/ast"
"github.com/microsoft/typescript-go/shim/checker"
)

// EffectGenCallResult represents a parsed Effect.gen(...) call.
type EffectGenCallResult struct {
Call *ast.CallExpression
EffectModule *ast.Expression
OptionsNode *ast.Node
GeneratorFunction *ast.FunctionExpression
Body *ast.BlockOrExpression
FunctionReturnType *checker.Type
PipeArguments []*ast.Node
}

func (tp *TypeParser) buildEffectGenFunctionReturnType(call *ast.CallExpression, trailingStartIndex int, pipeArgs []*ast.Node) *checker.Type {
if tp == nil || tp.checker == nil || call == nil {
return nil
}

if len(pipeArgs) == 0 {
return tp.GetTypeAtLocation(call.AsNode())
}

firstPipeParamType := tp.checker.GetContextualTypeForArgumentAtIndex(call.AsNode(), trailingStartIndex)
if firstPipeParamType == nil {
return nil
}
firstPipeCallSigs := tp.checker.GetSignaturesOfType(firstPipeParamType, checker.SignatureKindCall)
if len(firstPipeCallSigs) == 0 {
return nil
}
pipeInputParams := firstPipeCallSigs[0].Parameters()
if len(pipeInputParams) == 0 {
return nil
}
return tp.checker.GetTypeOfSymbolAtLocation(pipeInputParams[0], pipeArgs[0])
Call *ast.CallExpression
EffectModule *ast.Expression
OptionsNode *ast.Node
GeneratorFunction *ast.FunctionExpression
Body *ast.BlockOrExpression
PipeArguments []*ast.Node
}

// EffectGenCall parses a node as Effect.gen(<generator>).
Expand All @@ -59,7 +33,6 @@ func (tp *TypeParser) EffectGenCall(node *ast.Node) *EffectGenCallResult {
return nil
}
genFn := bodyArg.AsFunctionExpression()
trailingStartIndex := len(call.Arguments.Nodes) - len(pipeArgs)

expr := call.Expression
if expr == nil || expr.Kind != ast.KindPropertyAccessExpression {
Expand All @@ -76,13 +49,12 @@ func (tp *TypeParser) EffectGenCall(node *ast.Node) *EffectGenCallResult {
}

return &EffectGenCallResult{
Call: call,
EffectModule: propertyAccess.Expression,
OptionsNode: optionsNode,
GeneratorFunction: genFn,
Body: genFn.Body,
FunctionReturnType: tp.buildEffectGenFunctionReturnType(call, trailingStartIndex, pipeArgs),
PipeArguments: pipeArgs,
Call: call,
EffectModule: propertyAccess.Expression,
OptionsNode: optionsNode,
GeneratorFunction: genFn,
Body: genFn.Body,
PipeArguments: pipeArgs,
}
})
}
7 changes: 4 additions & 3 deletions internal/typeparser/effect_gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ func assertEffectGenFunctionReturnType(t *testing.T, version bundledeffect.Effec
defer done()

parsed := findFirstEffectGenCall(t, tp, sf)
if parsed.FunctionReturnType == nil {
t.Fatal("expected FunctionReturnType")
returnType := tp.GetTypeAtLocation(parsed.Call.AsNode())
if returnType == nil {
t.Fatal("expected return type")
}
if got := c.TypeToString(parsed.FunctionReturnType); got != want {
if got := c.TypeToString(returnType); got != want {
t.Fatalf("FunctionReturnType = %q", got)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
=== Metadata ===
Effect version: 4.0.0



==== /.src/tsconfig.json (0 errors) ====
{
"compilerOptions": {
"strict": true,
"plugins": [
{
"name": "@effect/language-service"
}
]
}
}


==== /.src/issue179_nestedEffectGenThis.ts (0 errors) ====
import { Effect } from "effect"

function combine<T>(a: T, b: T): T { return a }

export class Repro {
run(): Effect.Effect<number> {
return Effect.gen({ self: this }, function* () {
return yield* Effect.gen({ self: this }, function* () {
return combine(1, 2)
})
})
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
flowchart TB
0[["type: #lt;T#gt;#40;a: T, b: T#41; =#gt; T<br/>node: function combine#lt;T#gt;#40;a: T, b: T#41;: T #123; return a #125;"]]
1[/"type: T<br/>node: a"/]
2[["type: #40;#41; =#gt; Effect#lt;number, never, never#gt;<br/>node: run#40;#41;: Effect.Effect#lt;number#gt; #123;#92;n return Effect.gen#40;#123; self: this #125;, function* #40;#41; #123;#92;n return yield* Effect.gen#40;#123; self: this #125;, function* #40;#41; #123;#92;n return combine#40;1, 2#41;#92;n #125;#41;#92;n #125;#41;#92;n #125;"]]
3((("type: Effect#lt;1 #124; 2, never, never#gt;<br/>node: Effect.gen#40;#123; self: this #125;, function* #40;#41; #123;#92;n return yield* Effect.gen#40;#123; self: this #125;, function* #40;#41; #123;#92;n return combine#40;1, 2#41;#92;n #125;#41;#92;n #125;#41;")))
4[/"type: 1 #124; 2<br/>node: yield* Effect.gen#40;#123; self: this #125;, function* #40;#41; #123;#92;n return combine#40;1, 2#41;#92;n #125;#41;"/]
5((("type: Effect#lt;1 #124; 2, never, never#gt;<br/>node: Effect.gen#40;#123; self: this #125;, function* #40;#41; #123;#92;n return combine#40;1, 2#41;#92;n #125;#41;")))
6[/"type: 1 #124; 2<br/>node: combine#40;1, 2#41;"/]
1 -->|"kind: potentialReturn"| 0
1 -->|"kind: usedBy"| 0
6 -->|"kind: potentialReturn"| 5
6 -->|"kind: usedBy"| 5
5 -->|"kind: usedBy"| 4
4 -->|"kind: potentialReturn"| 3
5 -->|"kind: yieldable"| 3
3 -->|"kind: potentialReturn"| 2
3 -->|"kind: usedBy"| 2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/.src/issue179_nestedEffectGenThis.ts -> issue179_nestedEffectGenThis.flows.issue179_nestedEffectGenThis.mermaid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
==== /.src/issue179_nestedEffectGenThis.ts (0 layer exports) ====
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
==== /.src/issue179_nestedEffectGenThis.ts (0 flows) ====
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
=== Quick Fix Inventory ===

[D1] (3:27-3:28) TS6133: 'b' is declared but its value is never read.
(no quick fixes)

=== Quick Fix Application Results ===
(no quick fixes to apply)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
=== Metadata ===
Effect version: 4.0.0



==== /.src/tsconfig.json (0 errors) ====
{
"compilerOptions": {
"strict": true,
"plugins": [
{
"name": "@effect/language-service"
}
]
}
}


==== /.src/issue179_remainingCases.ts (0 errors) ====
import { Effect } from "effect"

function runMcpRequest<T>(
name: string,
label: string,
request: () => PromiseLike<T>
): Effect.Effect<T> {
return Effect.tryPromise(request)
}

export class Repro {
readonly #value = 42

#innerEffect(a: number, b: number): Effect.Effect<number> {
return Effect.succeed(a + b)
}

#innerSync(a: number, b: number): number {
return a + b
}

#innerOne(a: number): Effect.Effect<number> {
return Effect.succeed(a)
}

#getWrapper(key: string): (effect: Effect.Effect<void>) => Effect.Effect<void> {
return (effect) => effect.pipe(Effect.annotateLogs("key", key))
}

calleeExpression(): Effect.Effect<number> {
return Effect.gen({ self: this }, function* () {
return yield* this.#innerEffect(1, 2)
})
}

tryPromiseCallback(): Effect.Effect<number, unknown> {
return Effect.gen({ self: this }, function* () {
return yield* Effect.tryPromise(() => Promise.resolve(this.#value))
})
}

syncCallbackMultiArg(): Effect.Effect<number> {
return Effect.gen({ self: this }, function* () {
return yield* Effect.sync(() => this.#innerSync(1, 2))
})
}

calleeWithPipe(): Effect.Effect<number> {
return Effect.gen({ self: this }, function* () {
return yield* this.#innerOne(1).pipe(Effect.map((n) => n + 1))
})
}

curriedWrapper(key: string): Effect.Effect<void> {
return this.#getWrapper(key)(
Effect.gen({ self: this }, function* () {
return
})
)
}

nestedGenericCallback(server: { readonly name: string }): Effect.Effect<void> {
return Effect.gen({ self: this }, function* () {
return yield* Effect.gen({ self: this }, function* () {
yield* runMcpRequest(server.name, "connect", () => Promise.resolve(42))
})
})
}
}

Loading
Loading