diff --git a/.changeset/autoimport-style-effect-fixtures.md b/.changeset/autoimport-style-effect-fixtures.md new file mode 100644 index 00000000..9fcb881b --- /dev/null +++ b/.changeset/autoimport-style-effect-fixtures.md @@ -0,0 +1,5 @@ +--- +"@effect/tsgo": patch +--- + +Update the auto-import style consistency tests to use the mounted Effect fixtures and assert the full rewritten fix shapes for barrel and namespace import behavior. diff --git a/internal/effecttest/autoimport_style_consistency_test.go b/internal/effecttest/autoimport_style_consistency_test.go index 6991f087..b3be4a0b 100644 --- a/internal/effecttest/autoimport_style_consistency_test.go +++ b/internal/effecttest/autoimport_style_consistency_test.go @@ -1,11 +1,15 @@ package effecttest_test import ( + "cmp" + "slices" "testing" "github.com/microsoft/typescript-go/shim/core" "github.com/microsoft/typescript-go/shim/fourslash" + "github.com/microsoft/typescript-go/shim/ls/lsconv" "github.com/microsoft/typescript-go/shim/ls/lsutil" + "github.com/microsoft/typescript-go/shim/lsp/lsproto" _ "github.com/effect-ts/tsgo/etslshooks" _ "github.com/effect-ts/tsgo/etstesthooks" @@ -65,24 +69,16 @@ func TestAutoImportEffectStyleConsistency_barrel(t *testing.T) { "plugins": [ { "name": "@effect/language-service", - "barrelImportPackages": ["@EFFECT/PLATFORM"] + "barrelImportPackages": ["EFFECT"] } ] } } -// @Filename: /node_modules/@effect/platform/package.json -{ - "name": "@effect/platform", - "version": "0.0.0" -} -// @Filename: /node_modules/@effect/platform/HttpClient.ts -export const request = (url: string): string => url; -// @Filename: /node_modules/@effect/platform/index.ts -export * as HttpClient from "./HttpClient"; +// @effect-v4 // @Filename: /mainCompletion.ts -request/*completion*/("/"); +succeed/*completion*/(1); // @Filename: /mainFix.ts -request/*fix*/("/"); +succeed/*fix*/(1); ` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) @@ -92,24 +88,139 @@ request/*fix*/("/"); IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, } - completion := "completion" + f.GoToMarker(t, "fix") + // Barrel imports use the top-level effect package while preserving the + // namespace-qualified call shape from the direct module imports. + verifyImportFixContentsUnordered(t, f, "fix", "succeed(1);\n", []string{ + "import { Channel } from \"effect\";\n\nChannel.succeed(1);\n", + "import { Config } from \"effect\";\n\nConfig.succeed(1);\n", + "import { Deferred } from \"effect\";\n\nDeferred.succeed(1);\n", + "import { DurableDeferred } from \"effect\";\n\nDurableDeferred.succeed(1);\n", + "import { Effect } from \"effect\";\n\nEffect.succeed(1);\n", + "import { Exit } from \"effect\";\n\nExit.succeed(1);\n", + "import { Layer } from \"effect\";\n\nLayer.succeed(1);\n", + "import { Prompt } from \"effect\";\n\nPrompt.succeed(1);\n", + "import { Request } from \"effect\";\n\nRequest.succeed(1);\n", + "import { Result } from \"effect\";\n\nResult.succeed(1);\n", + "import { SchemaGetter } from \"effect\";\n\nSchemaGetter.succeed(1);\n", + "import { Sink } from \"effect\";\n\nSink.succeed(1);\n", + "import { Stream } from \"effect\";\n\nStream.succeed(1);\n", + "import { TxDeferred } from \"effect\";\n\nTxDeferred.succeed(1);\n", + }, preferences) +} - // After barrel rewrite, the module specifier changes to the barrel package - f.VerifyApplyCodeActionFromCompletion(t, &completion, &fourslash.ApplyCodeActionFromCompletionOptions{ - Name: "request", - Source: "@effect/platform", - Description: "Add import from \"@effect/platform\"", - NewFileContent: new(`import { HttpClient } from "@effect/platform"; +func verifyImportFixContentsUnordered(t *testing.T, f *fourslash.FourslashTest, markerName string, originalContent string, expected []string, preferences *lsutil.UserPreferences) { + t.Helper() + f.GoToMarker(t, markerName) + if preferences != nil { + reset := f.ConfigureWithReset(t, *preferences) + defer reset() + } -HttpClient.request("/");`), - UserPreferences: preferences, + marker := f.MarkerByName(t, markerName) + uri := lsconv.FileNameToDocumentURI(marker.FileName()) + client := fourslash.FourslashTest_client(f) + + diagIDValue := client.NextID() + diagID := lsproto.NewID(lsproto.IntegerOrString{Integer: &diagIDValue}) + diagReq := lsproto.TextDocumentDiagnosticInfo.NewRequestMessage(diagID, &lsproto.DocumentDiagnosticParams{ + TextDocument: lsproto.TextDocumentIdentifier{Uri: uri}, }) + diagResp, ok := client.SendRequestWorker(t, diagReq, diagID) + if !ok { + t.Fatal("diagnostic request failed") + } + diagResult, ok := diagResp.Result.(lsproto.DocumentDiagnosticResponse) + if !ok { + t.Fatal("unexpected diagnostic response type") + } - f.GoToMarker(t, "fix") - f.VerifyImportFixAtPosition(t, []string{`import { HttpClient } from "@effect/platform"; + var diagnostics []*lsproto.Diagnostic + if diagResult.FullDocumentDiagnosticReport != nil && diagResult.FullDocumentDiagnosticReport.Items != nil { + diagnostics = diagResult.FullDocumentDiagnosticReport.Items + } -HttpClient.request("/"); -`}, preferences) + actionIDValue := client.NextID() + actionID := lsproto.NewID(lsproto.IntegerOrString{Integer: &actionIDValue}) + actionReq := lsproto.TextDocumentCodeActionInfo.NewRequestMessage(actionID, &lsproto.CodeActionParams{ + TextDocument: lsproto.TextDocumentIdentifier{Uri: uri}, + Range: lsproto.Range{Start: marker.LSPos(), End: marker.LSPos()}, + Context: &lsproto.CodeActionContext{Diagnostics: diagnostics}, + }) + actionResp, ok := client.SendRequestWorker(t, actionReq, actionID) + if !ok { + t.Fatal("code action request failed") + } + actionResult, ok := actionResp.Result.(lsproto.CodeActionResponse) + if !ok { + t.Fatal("unexpected code action response type") + } + + var actual []string + lineMap := lsconv.ComputeLSPLineStarts(originalContent) + if actionResult.CommandOrCodeActionArray != nil { + for _, item := range *actionResult.CommandOrCodeActionArray { + if item.CodeAction == nil || item.CodeAction.Kind == nil || *item.CodeAction.Kind != lsproto.CodeActionKindQuickFix { + continue + } + if item.CodeAction.Edit == nil || item.CodeAction.Edit.Changes == nil { + continue + } + for _, edits := range *item.CodeAction.Edit.Changes { + actual = append(actual, applyTextEdits(originalContent, lineMap, edits)) + } + } + } + + slices.Sort(actual) + expected = slices.Clone(expected) + slices.Sort(expected) + if !slices.Equal(actual, expected) { + t.Fatalf("Unexpected import fix contents.\nExpected: %v\nActual: %v", expected, actual) + } +} + +func applyTextEdits(original string, lineMap *lsconv.LSPLineMap, edits []*lsproto.TextEdit) string { + sortedEdits := slices.Clone(edits) + slices.SortFunc(sortedEdits, func(a, b *lsproto.TextEdit) int { + if a.Range.Start.Line != b.Range.Start.Line { + return cmp.Compare(int(b.Range.Start.Line), int(a.Range.Start.Line)) + } + if a.Range.Start.Character != b.Range.Start.Character { + return cmp.Compare(int(b.Range.Start.Character), int(a.Range.Start.Character)) + } + if a.Range.End.Line != b.Range.End.Line { + return cmp.Compare(int(b.Range.End.Line), int(a.Range.End.Line)) + } + if a.Range.End.Character != b.Range.End.Character { + return cmp.Compare(int(b.Range.End.Character), int(a.Range.End.Character)) + } + return cmp.Compare(len(a.NewText), len(b.NewText)) + }) + + content := original + for _, edit := range sortedEdits { + start := lspPositionToOffset(content, lineMap, edit.Range.Start) + end := lspPositionToOffset(content, lineMap, edit.Range.End) + content = content[:start] + edit.NewText + content[end:] + lineMap = lsconv.ComputeLSPLineStarts(content) + } + return content +} + +func lspPositionToOffset(text string, lineMap *lsconv.LSPLineMap, pos lsproto.Position) int { + lineStart := int(lineMap.LineStarts[pos.Line]) + lineText := text[lineStart:] + for i, r := range lineText { + if pos.Character == 0 { + return lineStart + i + } + if r == '\n' || r == '\r' { + return lineStart + i + } + pos.Character-- + } + return len(text) } func TestAutoImportEffectStyleConsistency_topLevelNamedReexportsIgnore(t *testing.T) { @@ -126,15 +237,7 @@ func TestAutoImportEffectStyleConsistency_topLevelNamedReexportsIgnore(t *testin ] } } -// @Filename: /node_modules/effect/package.json -{ - "name": "effect", - "version": "0.0.0" -} -// @Filename: /node_modules/effect/Effect.ts -export const succeed = (value: A): A => value; -// @Filename: /node_modules/effect/index.ts -export { succeed } from "./Effect"; +// @effect-v4 // @Filename: /mainCompletion.ts succeed/*completion*/(1); // @Filename: /mainFix.ts @@ -152,16 +255,23 @@ succeed/*fix*/(1); _ = completion f.GoToMarker(t, "fix") - // Two import fixes available: named from "effect" (reexport kept) and namespace from "effect/Effect" + // With the real effect-v4 package mounted, succeed is surfaced from multiple + // namespace modules rather than as a direct named import from "effect". f.VerifyImportFixAtPosition(t, []string{ - `import { succeed } from "effect"; - -succeed(1); -`, - `import * as Effect from "effect/Effect"; - -Effect.succeed(1); -`, + "import * as Channel from \"effect/Channel\";\n\nChannel.succeed(1);\n", + "import * as Config from \"effect/Config\";\n\nConfig.succeed(1);\n", + "import * as Deferred from \"effect/Deferred\";\n\nDeferred.succeed(1);\n", + "import * as Effect from \"effect/Effect\";\n\nEffect.succeed(1);\n", + "import * as Exit from \"effect/Exit\";\n\nExit.succeed(1);\n", + "import * as Layer from \"effect/Layer\";\n\nLayer.succeed(1);\n", + "import * as Request from \"effect/Request\";\n\nRequest.succeed(1);\n", + "import * as Result from \"effect/Result\";\n\nResult.succeed(1);\n", + "import * as SchemaGetter from \"effect/SchemaGetter\";\n\nSchemaGetter.succeed(1);\n", + "import * as Sink from \"effect/Sink\";\n\nSink.succeed(1);\n", + "import * as Stream from \"effect/Stream\";\n\nStream.succeed(1);\n", + "import * as TxDeferred from \"effect/TxDeferred\";\n\nTxDeferred.succeed(1);\n", + "import * as Prompt from \"effect/unstable/cli/Prompt\";\n\nPrompt.succeed(1);\n", + "import * as DurableDeferred from \"effect/unstable/workflow/DurableDeferred\";\n\nDurableDeferred.succeed(1);\n", }, preferences) }