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/shiny-seals-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/tsgo": patch
---

Fix the toggle-pipe-style refactor to avoid formatter panics on nested callback bodies such as SQL effects using `.pipe(Effect.flatMap(...))`.

This adds a regression test and updates the affected refactor baselines to match the new text-preserving rewrite output.
56 changes: 33 additions & 23 deletions internal/refactors/toggle_pipe_style.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package refactors

import (
"strings"

"github.com/effect-ts/tsgo/internal/refactor"
"github.com/effect-ts/tsgo/internal/typeparser"
"github.com/microsoft/typescript-go/shim/ast"
"github.com/microsoft/typescript-go/shim/astnav"
"github.com/microsoft/typescript-go/shim/ls"
"github.com/microsoft/typescript-go/shim/ls/change"
"github.com/microsoft/typescript-go/shim/lsp/lsproto"
"github.com/microsoft/typescript-go/shim/scanner"
)

var TogglePipeStyle = refactor.Refactor{
Expand Down Expand Up @@ -45,17 +49,12 @@ func runTogglePipeStyle(ctx *refactor.Context) []ls.CodeAction {
action := ctx.NewRefactorAction(refactor.RefactorAction{
Description: "Rewrite as X.pipe(Y, Z, ...)",
Run: func(tracker *change.Tracker) {
clonedSubject := tracker.DeepCloneNode(pipeCall.Subject)
pipeAccess := tracker.NewPropertyAccessExpression(clonedSubject, nil, tracker.NewIdentifier("pipe"), ast.NodeFlagsNone)

var clonedArgs []*ast.Node
for _, arg := range pipeCall.Args {
clonedArgs = append(clonedArgs, tracker.DeepCloneNode(arg))
}

callExpr := tracker.NewCallExpression(pipeAccess, nil, nil, tracker.NewNodeList(clonedArgs), ast.NodeFlagsNone)
ast.SetParentInChildren(callExpr)
tracker.ReplaceNode(ctx.SourceFile, node, callExpr, nil)
start := astnav.GetStartOfNode(node, ctx.SourceFile, false)
rewritten := togglePipeToMethodText(ctx.SourceFile, pipeCall.Subject, pipeCall.Args)
tracker.ReplaceRangeWithText(ctx.SourceFile, lsproto.Range{
Start: ctx.BytePosToLSPPosition(start),
End: ctx.BytePosToLSPPosition(node.End()),
}, rewritten)
},
})
if action == nil {
Expand All @@ -69,18 +68,12 @@ func runTogglePipeStyle(ctx *refactor.Context) []ls.CodeAction {
action := ctx.NewRefactorAction(refactor.RefactorAction{
Description: "Rewrite as pipe(X, Y, Z, ...)",
Run: func(tracker *change.Tracker) {
clonedSubject := tracker.DeepCloneNode(pipeCall.Subject)

allArgs := make([]*ast.Node, 0, 1+len(pipeCall.Args))
allArgs = append(allArgs, clonedSubject)
for _, arg := range pipeCall.Args {
allArgs = append(allArgs, tracker.DeepCloneNode(arg))
}

pipeId := tracker.NewIdentifier("pipe")
callExpr := tracker.NewCallExpression(pipeId, nil, nil, tracker.NewNodeList(allArgs), ast.NodeFlagsNone)
ast.SetParentInChildren(callExpr)
tracker.ReplaceNode(ctx.SourceFile, node, callExpr, nil)
start := astnav.GetStartOfNode(node, ctx.SourceFile, false)
rewritten := togglePipeToFunctionText(ctx.SourceFile, pipeCall.Subject, pipeCall.Args)
tracker.ReplaceRangeWithText(ctx.SourceFile, lsproto.Range{
Start: ctx.BytePosToLSPPosition(start),
End: ctx.BytePosToLSPPosition(node.End()),
}, rewritten)
},
})
if action == nil {
Expand All @@ -93,3 +86,20 @@ func runTogglePipeStyle(ctx *refactor.Context) []ls.CodeAction {

return nil
}

func togglePipeToMethodText(sf *ast.SourceFile, subject *ast.Node, args []*ast.Node) string {
parts := make([]string, 0, len(args))
for _, arg := range args {
parts = append(parts, scanner.GetSourceTextOfNodeFromSourceFile(sf, arg, false))
}
return scanner.GetSourceTextOfNodeFromSourceFile(sf, subject, false) + ".pipe(" + strings.Join(parts, ", ") + ")"
}

func togglePipeToFunctionText(sf *ast.SourceFile, subject *ast.Node, args []*ast.Node) string {
parts := make([]string, 0, len(args)+1)
parts = append(parts, scanner.GetSourceTextOfNodeFromSourceFile(sf, subject, false))
for _, arg := range args {
parts = append(parts, scanner.GetSourceTextOfNodeFromSourceFile(sf, arg, false))
}
return "pipe(" + strings.Join(parts, ", ") + ")"
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ export const program = () =>

export const programWithPipes = (fa: number, fb: number) =>
pipe(Eff.gen(function*() {
const a = yield* Eff.succeed(fa)
const b = yield* Eff.succeed(fb)
return a + b
}), Eff.map(a => a + 1))
const a = yield* Eff.succeed(fa)
const b = yield* Eff.succeed(fb)
return a + b
}), Eff.map((a) => a + 1))

export function sampleReturns<A extends number, B extends number>(arg1: A, arg2: B) {
return Eff.gen(function*() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { pipe } from "effect/Function"
const test = () =>
pipe(
pipe(Effect.gen(function*() {
const test = "test"
return yield* Effect.succeed(test)
}), Effect.asVoid),
const test = "test"
return yield* Effect.succeed(test)
}), Effect.asVoid),
Effect.tapError(Effect.logError)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import { pipe } from "effect/Function"

const test = () =>
Effect.gen(function*() {
const test = "test"
return yield* Effect.succeed(test)
const test = "test"
return yield* Effect.succeed(test)
}).pipe(Effect.tapError(Effect.logError))


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const test3 = pipe(
import * as T from "effect/Effect"
import { pipe } from "effect/Function"

const test = T.succeed("Hello").pipe(T.flatMap(_ => T.log(_)), T.zipRight(T.succeed(42)), T.map(_ => _ * 2))
const test = T.succeed("Hello").pipe(T.flatMap((_) => T.log(_)), T.zipRight(T.succeed(42)), T.map((_) => _ * 2))

const noDataFirst = (value: string) => <A, E, R>(eff: T.Effect<A, E, R>) => pipe(eff, T.zipLeft(T.log(value)))

Expand Down Expand Up @@ -190,7 +190,7 @@ const test = pipe(

const noDataFirst = (value: string) => <A, E, R>(eff: T.Effect<A, E, R>) => pipe(eff, T.zipLeft(T.log(value)))

const test2 = T.succeed("Hello").pipe(T.flatMap(_ => T.log(_)), noDataFirst("42"))
const test2 = T.succeed("Hello").pipe(T.flatMap((_) => T.log(_)), noDataFirst("42"))

const test3 = pipe(
T.succeed("Hello"),
Expand Down Expand Up @@ -316,7 +316,7 @@ const test2 = pipe(
noDataFirst("42")
)

const test3 = T.succeed("Hello").pipe(T.flatMap(_ => T.log(_)), noDataFirst("a"), noDataFirst("b"), noDataFirst("c"))
const test3 = T.succeed("Hello").pipe(T.flatMap((_) => T.log(_)), noDataFirst("a"), noDataFirst("b"), noDataFirst("c"))


=== [R3] Refactor 2: "Rewrite to datafirst" ===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const toPipeable = pipe(Effect.succeed(42), Effect.map((x) => x * 2))
import * as Effect from "effect/Effect"
import { pipe } from "effect/Function"

export const toRegularPipe = pipe(Effect.succeed(42), Effect.map(x => x * 2))
export const toRegularPipe = pipe(Effect.succeed(42), Effect.map((x) => x * 2))

export const toPipeable = pipe(Effect.succeed(42), Effect.map((x) => x * 2))

Expand Down Expand Up @@ -77,7 +77,7 @@ import { pipe } from "effect/Function"

export const toRegularPipe = Effect.succeed(42).pipe(Effect.map((x) => x * 2))

export const toPipeable = Effect.succeed(42).pipe(Effect.map(x => x * 2))
export const toPipeable = Effect.succeed(42).pipe(Effect.map((x) => x * 2))


=== [R2] Refactor 2: "Wrap with Effect.gen" ===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const test1 = Effect.succeed(42)

export const test3 = test1

export const test2 = pipe(Effect.succeed(42), Effect.map(n => n + 1), Effect.map(n => n - 1))
export const test2 = pipe(Effect.succeed(42), Effect.map((n) => n + 1), Effect.map((n) => n - 1))

export const test4 = pipe(
Effect.succeed(42),
Expand Down Expand Up @@ -235,7 +235,7 @@ export const test1 = Effect.succeed(42)

export const test3 = test1

export const test2 = pipe(Effect.succeed(42), Effect.map(n => n + 1), Effect.map(n => n - 1))
export const test2 = pipe(Effect.succeed(42), Effect.map((n) => n + 1), Effect.map((n) => n - 1))

export const test4 = pipe(
Effect.succeed(42),
Expand Down Expand Up @@ -309,7 +309,7 @@ export const test2 = Effect.succeed(42).pipe(
Effect.map((n) => n - 1)
)

export const test4 = Effect.succeed(42).pipe(Effect.map(n => n + 1), Effect.map(n => n - 1))
export const test4 = Effect.succeed(42).pipe(Effect.map((n) => n + 1), Effect.map((n) => n - 1))


=== [R5] Refactor 2: "Rewrite to datafirst" ===
Expand Down Expand Up @@ -397,7 +397,7 @@ export const test2 = Effect.succeed(42).pipe(
Effect.map((n) => n - 1)
)

export const test4 = Effect.succeed(42).pipe(Effect.map(n => n + 1), Effect.map(n => n - 1))
export const test4 = Effect.succeed(42).pipe(Effect.map((n) => n + 1), Effect.map((n) => n - 1))


=== [R6] Refactor 2: "Wrap with Effect.gen" ===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ export const program = () =>

export const programWithPipes = (fa: number, fb: number) =>
pipe(Eff.gen(function*() {
const a = yield* Eff.succeed(fa)
const b = yield* Eff.succeed(fb)
return a + b
}), Eff.map(a => a + 1))
const a = yield* Eff.succeed(fa)
const b = yield* Eff.succeed(fb)
return a + b
}), Eff.map((a) => a + 1))

export function sampleReturns<A extends number, B extends number>(arg1: A, arg2: B) {
return Eff.gen(function*() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { pipe } from "effect/Function"
const test = () =>
pipe(
pipe(Effect.gen(function*() {
const test = "test"
return yield* Effect.succeed(test)
}), Effect.asVoid),
const test = "test"
return yield* Effect.succeed(test)
}), Effect.asVoid),
Effect.tapError(Effect.logError)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import { pipe } from "effect/Function"

const test = () =>
Effect.gen(function*() {
const test = "test"
return yield* Effect.succeed(test)
const test = "test"
return yield* Effect.succeed(test)
}).pipe(Effect.tapError(Effect.logError))


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const test3 = pipe(
import * as T from "effect/Effect"
import { pipe } from "effect/Function"

const test = T.succeed("Hello").pipe(T.flatMap(_ => T.log(_)), T.andThen(T.succeed(42)), T.map(_ => _ * 2))
const test = T.succeed("Hello").pipe(T.flatMap((_) => T.log(_)), T.andThen(T.succeed(42)), T.map((_) => _ * 2))

const noDataFirst = (value: string) => <A, E, R>(eff: T.Effect<A, E, R>) => pipe(eff, T.andThen(T.log(value)))

Expand Down Expand Up @@ -184,7 +184,7 @@ const test = pipe(

const noDataFirst = (value: string) => <A, E, R>(eff: T.Effect<A, E, R>) => pipe(eff, T.andThen(T.log(value)))

const test2 = T.succeed("Hello").pipe(T.flatMap(_ => T.log(_)), noDataFirst("42"))
const test2 = T.succeed("Hello").pipe(T.flatMap((_) => T.log(_)), noDataFirst("42"))

const test3 = pipe(
T.succeed("Hello"),
Expand Down Expand Up @@ -306,7 +306,7 @@ const test2 = pipe(
noDataFirst("42")
)

const test3 = T.succeed("Hello").pipe(T.flatMap(_ => T.log(_)), noDataFirst("a"), noDataFirst("b"), noDataFirst("c"))
const test3 = T.succeed("Hello").pipe(T.flatMap((_) => T.log(_)), noDataFirst("a"), noDataFirst("b"), noDataFirst("c"))


=== [R3] Refactor 2: "Rewrite to datafirst" ===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const toPipeable = pipe(Effect.succeed(42), Effect.map((x) => x * 2))
import * as Effect from "effect/Effect"
import { pipe } from "effect/Function"

export const toRegularPipe = pipe(Effect.succeed(42), Effect.map(x => x * 2))
export const toRegularPipe = pipe(Effect.succeed(42), Effect.map((x) => x * 2))

export const toPipeable = pipe(Effect.succeed(42), Effect.map((x) => x * 2))

Expand Down Expand Up @@ -72,7 +72,7 @@ import { pipe } from "effect/Function"

export const toRegularPipe = Effect.succeed(42).pipe(Effect.map((x) => x * 2))

export const toPipeable = Effect.succeed(42).pipe(Effect.map(x => x * 2))
export const toPipeable = Effect.succeed(42).pipe(Effect.map((x) => x * 2))


=== [R2] Refactor 2: "Wrap with Effect.gen" ===
Expand Down
Loading
Loading