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
5 changes: 5 additions & 0 deletions .changeset/fourslash-effect-mount-markers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/tsgo": patch
---

Mount real Effect packages in fourslash tests when `@effect-v3` or `@effect-v4` markers are present, and cover Effect auto-import namespace behavior against the real mounted package.
77 changes: 52 additions & 25 deletions _patches/018-ls-autoimport-stylepolicy.patch
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
diff --git a/internal/ls/autoimport/aliasresolver.go b/internal/ls/autoimport/aliasresolver.go
index e34b62822..b733cd122 100644
--- a/internal/ls/autoimport/aliasresolver.go
+++ b/internal/ls/autoimport/aliasresolver.go
@@ -219,6 +219,11 @@ func (r *aliasResolver) IsSourceFileDefaultLibrary(path tspath.Path) bool {
panic("unimplemented")
}

+// IsSourceFileFromExternalLibrary implements checker.Program.
+func (r *aliasResolver) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool {
+ return false
+}
+
// IsSourceFromProjectReference implements checker.Program.
func (r *aliasResolver) IsSourceFromProjectReference(path tspath.Path) bool {
panic("unimplemented")
diff --git a/internal/ls/autoimport/fix.go b/internal/ls/autoimport/fix.go
index b2107443f..bf5161047 100644
index 991bea0db..42a0745ae 100644
--- a/internal/ls/autoimport/fix.go
+++ b/internal/ls/autoimport/fix.go
@@ -74,14 +74,19 @@ func (f *Fix) Edits(
@@ -74,14 +74,18 @@ func (f *Fix) Edits(
case lsproto.AutoImportFixKindAddNew:
var declarations []*ast.Statement
defaultImport := core.IfElse(f.ImportKind == lsproto.ImportKindDefault, &newImportBinding{name: f.Name, addAsTypeOnly: f.AddAsTypeOnly}, nil)
Expand All @@ -13,7 +29,7 @@ index b2107443f..bf5161047 100644
+ }
+ namedImports := core.IfElse(f.ImportKind == lsproto.ImportKindNamed, []*newImportBinding{{name: namedBindingName, addAsTypeOnly: f.AddAsTypeOnly}}, nil)
var namespaceLikeImport *newImportBinding
// qualification := f.qualification()
- // qualification := f.qualification()
if f.ImportKind == lsproto.ImportKindNamespace || f.ImportKind == lsproto.ImportKindCommonJS {
- namespaceLikeImport = &newImportBinding{kind: f.ImportKind, name: f.Name}
- // if qualification != nil && qualification.namespacePref != "" {
Expand All @@ -27,7 +43,7 @@ index b2107443f..bf5161047 100644
}

quotePreference := lsutil.GetQuotePreference(file, preferences)
@@ -98,9 +103,11 @@ func (f *Fix) Edits(
@@ -98,9 +102,11 @@ func (f *Fix) Edits(
/*blankLineBetween*/ true,
preferences,
)
Expand All @@ -42,16 +58,18 @@ index b2107443f..bf5161047 100644
return tracker.GetChanges()[file.FileName()], diagnostics.Add_import_from_0.Localize(locale, f.ModuleSpecifier)
case lsproto.AutoImportFixKindPromoteTypeOnly:
promotedDeclaration := promoteFromTypeOnly(tracker, f.TypeOnlyAliasDeclaration, compilerOptions, file, preferences)
@@ -541,7 +548,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
@@ -540,8 +546,8 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
fixes = append(fixes, namespaceFix)
}

if fix := v.tryAddToExistingImport(ctx, export, isValidTypeOnlyUseSite); fix != nil {
- if fix := v.tryAddToExistingImport(ctx, export, isValidTypeOnlyUseSite); fix != nil {
- return append(fixes, fix)
+ if fix := v.tryAddToExistingImport(ctx, export, isValidTypeOnlyUseSite, usagePosition); fix != nil {
+ return v.applyStylePolicy(export, append(fixes, fix))
}

// !!! getNewImportFromExistingSpecifier - even worth it?
@@ -549,7 +556,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
@@ -549,7 +555,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
moduleSpecifier, moduleSpecifierKind := v.GetModuleSpecifier(export, v.preferences)
if moduleSpecifier == "" {
if len(fixes) > 0 {
Expand All @@ -60,7 +78,7 @@ index b2107443f..bf5161047 100644
}
return nil
}
@@ -559,7 +566,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
@@ -559,7 +565,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
importedSymbolHasValueMeaning := export.Flags&ast.SymbolFlagsValue != 0 || export.IsUnresolvedAlias()
if !importedSymbolHasValueMeaning && isJs && usagePosition != nil {
// For pure types in JS files, use JSDoc import type syntax
Expand All @@ -69,7 +87,7 @@ index b2107443f..bf5161047 100644
{
AutoImportFix: &lsproto.AutoImportFix{
Kind: lsproto.AutoImportFixKindJsdocTypeImport,
@@ -571,7 +578,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
@@ -571,7 +577,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
IsReExport: export.Target.ModuleID != export.ModuleID,
ModuleFileName: export.ModuleFileName,
},
Expand All @@ -78,7 +96,7 @@ index b2107443f..bf5161047 100644
}

importKind := getImportKind(v.importingFile, export, v.program)
@@ -587,7 +594,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
@@ -587,7 +593,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
}
}

Expand All @@ -87,7 +105,7 @@ index b2107443f..bf5161047 100644
AutoImportFix: &lsproto.AutoImportFix{
Kind: lsproto.AutoImportFixKindAddNew,
ImportKind: importKind,
@@ -595,11 +602,37 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
@@ -595,11 +601,37 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali
Name: name,
UseRequire: v.shouldUseRequire(),
AddAsTypeOnly: addAsTypeOnly,
Expand Down Expand Up @@ -126,18 +144,27 @@ index b2107443f..bf5161047 100644
}

// getAddAsTypeOnly determines if an import should be type-only based on usage context
diff --git a/internal/ls/autoimport/aliasresolver.go b/internal/ls/autoimport/aliasresolver.go
--- a/internal/ls/autoimport/aliasresolver.go
+++ b/internal/ls/autoimport/aliasresolver.go
@@ -220,6 +220,11 @@ func (r *aliasResolver) IsSourceFileDefaultLibrary(path tspath.Path) bool {
panic("unimplemented")
}
@@ -674,6 +706,7 @@ func (v *View) tryAddToExistingImport(
ctx context.Context,
export *Export,
isValidTypeOnlyUseSite bool,
+ usagePosition *lsproto.Position,
) *Fix {
existingImports := v.getExistingImports(ctx)
matchingDeclarations := existingImports.Get(export.ModuleID)
@@ -711,6 +744,7 @@ func (v *View) tryAddToExistingImport(
ImportIndex: int32(existingImport.index),
ModuleSpecifier: existingImport.moduleSpecifier,
AddAsTypeOnly: addAsTypeOnly,
+ UsagePosition: usagePosition,
},
}
// Variable declarations are never type-only.
@@ -759,6 +793,7 @@ func (v *View) tryAddToExistingImport(
ImportIndex: int32(existingImport.index),
ModuleSpecifier: existingImport.moduleSpecifier,
AddAsTypeOnly: addAsTypeOnly,
+ UsagePosition: usagePosition,
},
}

+// IsSourceFileFromExternalLibrary implements checker.Program.
+func (r *aliasResolver) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool {
+ return false
+}
+
// IsSourceFromProjectReference implements checker.Program.
func (r *aliasResolver) IsSourceFromProjectReference(path tspath.Path) bool {
panic("unimplemented")
10 changes: 7 additions & 3 deletions etstesthooks/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ func init() {
fourslash.RegisterPrepareTestFSCallback(prepareTestFS)
}

// prepareTestFS detects Effect imports in test files and mounts real Effect packages.
// It checks for a // @effect-v3 marker at the start of any file to choose the library version.
// prepareTestFS detects Effect imports or explicit version markers in test files
// and mounts real Effect packages into the fourslash VFS.
func prepareTestFS(testfs map[string]any) {
hasEffectImport := false
hasV3Marker := false
hasV4Marker := false
for _, v := range testfs {
content, ok := v.(string)
if !ok {
Expand All @@ -27,8 +28,11 @@ func prepareTestFS(testfs map[string]any) {
if strings.HasPrefix(content, "// @effect-v3") || strings.Contains(content, "\n// @effect-v3") {
hasV3Marker = true
}
if strings.HasPrefix(content, "// @effect-v4") || strings.Contains(content, "\n// @effect-v4") {
hasV4Marker = true
}
}
if !hasEffectImport {
if !hasEffectImport && !hasV3Marker && !hasV4Marker {
return
}
version := bundledeffect.EffectV4
Expand Down
71 changes: 71 additions & 0 deletions etstesthooks/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package etstesthooks

import (
"strings"
"testing"
"testing/fstest"
)

func TestPrepareTestFSMountsEffectForV4MarkerWithoutImport(t *testing.T) {
t.Parallel()

testfs := map[string]any{
"/main.ts": "// @effect-v4\nconst value = 1\n",
}

prepareTestFS(testfs)

assertMountedEffect(t, testfs)
assertSourcePackageName(t, testfs, "effect-v4-tests")
}

func TestPrepareTestFSMountsEffectForV3MarkerWithoutImport(t *testing.T) {
t.Parallel()

testfs := map[string]any{
"/main.ts": "// @effect-v3\nconst value = 1\n",
}

prepareTestFS(testfs)

assertMountedEffect(t, testfs)
assertSourcePackageName(t, testfs, "effect-v3-tests")
}

func TestPrepareTestFSDoesNotMountWithoutImportOrMarker(t *testing.T) {
t.Parallel()

testfs := map[string]any{
"/main.ts": "const value = 1\n",
}

prepareTestFS(testfs)

if _, ok := testfs["/.src/package.json"]; ok {
t.Fatal("expected no mounted Effect package without import or marker")
}
if _, ok := testfs["/node_modules/effect/package.json"]; ok {
t.Fatal("expected no mounted effect package without import or marker")
}
}

func assertMountedEffect(t *testing.T, testfs map[string]any) {
t.Helper()
if _, ok := testfs["/.src/package.json"]; !ok {
t.Fatal("expected mounted /.src/package.json")
}
if _, ok := testfs["/node_modules/effect/package.json"]; !ok {
t.Fatal("expected mounted /node_modules/effect/package.json")
}
}

func assertSourcePackageName(t *testing.T, testfs map[string]any, want string) {
t.Helper()
file, ok := testfs["/.src/package.json"].(*fstest.MapFile)
if !ok {
t.Fatal("expected /.src/package.json to be a fstest.MapFile")
}
if !strings.Contains(string(file.Data), want) {
t.Fatalf("expected /.src/package.json to contain %q, got %q", want, string(file.Data))
}
}
25 changes: 23 additions & 2 deletions internal/autoimportstyle/stylepolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,40 @@ func NewFixTransformer(resolved *etscore.ResolvedEffectPluginOptions) autoimport
}
return func(export *autoimport.Export, fixes []*autoimport.Fix) []*autoimport.Fix {
rewritten := make([]*autoimport.Fix, 0, len(fixes))
hasUseNamespace := make(map[string]bool)
for _, fix := range fixes {
adjusted := sp.Apply(export, fix)
if adjusted != nil {
if adjusted.Kind == lsproto.AutoImportFixKindUseNamespace {
hasUseNamespace[namespaceFixKey(adjusted)] = true
}
rewritten = append(rewritten, adjusted)
}
}
if len(hasUseNamespace) != 0 {
filtered := rewritten[:0]
for _, fix := range rewritten {
if fix.Kind == lsproto.AutoImportFixKindAddNew && fix.ImportKind == lsproto.ImportKindNamespace && hasUseNamespace[namespaceFixKey(fix)] {
continue
}
filtered = append(filtered, fix)
}
rewritten = filtered
}
if len(rewritten) == 0 {
return nil
}
return rewritten
}
}

func namespaceFixKey(fix *autoimport.Fix) string {
if fix == nil {
return ""
}
return fix.ModuleSpecifier + "\x00" + fix.NamespacePrefix
}

// newStylePolicy creates a stylePolicy from the given resolved options.
// Package names are lowercased for case-insensitive matching.
func newStylePolicy(resolved *etscore.ResolvedEffectPluginOptions) *stylePolicy {
Expand Down Expand Up @@ -75,8 +96,8 @@ func (sp *stylePolicy) Apply(export *autoimport.Export, fix *autoimport.Fix) *au
return fix
}

// Only rewrite AddNew fixes
if fix.Kind != lsproto.AutoImportFixKindAddNew {
// Only rewrite new imports or additions to existing imports.
if fix.Kind != lsproto.AutoImportFixKindAddNew && fix.Kind != lsproto.AutoImportFixKindAddToExisting {
return fix
}

Expand Down
57 changes: 57 additions & 0 deletions internal/autoimportstyle/stylepolicy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,34 @@ func TestApplyNamespaceRewrite(t *testing.T) {
}
}

func TestApplyNamespaceRewriteFromAddToExisting(t *testing.T) {
t.Parallel()
sp := newStylePolicy(&etscore.ResolvedEffectPluginOptions{
NamespaceImportPackages: []string{"effect"},
})

export := makeExport("effect", "effect/testing/TestClock", "")
fix := makeAddNewFix(lsproto.ImportKindNamed, "effect/testing/TestClock", "testClockWith")
fix.Kind = lsproto.AutoImportFixKindAddToExisting

result := sp.Apply(export, fix)
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Kind != lsproto.AutoImportFixKindAddNew {
t.Errorf("expected rewrite to AddNew, got %v", result.Kind)
}
if result.ImportKind != lsproto.ImportKindNamespace {
t.Errorf("expected ImportKindNamespace, got %v", result.ImportKind)
}
if result.ModuleSpecifier != "effect/testing/TestClock" {
t.Errorf("expected module specifier 'effect/testing/TestClock', got %q", result.ModuleSpecifier)
}
if result.NamespacePrefix != "TestClock" {
t.Errorf("expected namespace prefix 'TestClock', got %q", result.NamespacePrefix)
}
}

func TestApplyNamespaceRewriteWithAlias(t *testing.T) {
t.Parallel()
sp := newStylePolicy(&etscore.ResolvedEffectPluginOptions{
Expand Down Expand Up @@ -403,3 +431,32 @@ func TestNewFixTransformerAppliesPolicy(t *testing.T) {
t.Errorf("expected ImportKindNamespace, got %v", result[0].ImportKind)
}
}

func TestNewFixTransformerPrefersExistingNamespaceUse(t *testing.T) {
t.Parallel()
transformer := NewFixTransformer(&etscore.ResolvedEffectPluginOptions{
NamespaceImportPackages: []string{"effect"},
})
if transformer == nil {
t.Fatal("expected non-nil transformer")
}

export := makeExport("effect", "effect/testing/TestClock", "")
useNamespace := &autoimport.Fix{AutoImportFix: &lsproto.AutoImportFix{
Kind: lsproto.AutoImportFixKindUseNamespace,
ImportKind: lsproto.ImportKindNamespace,
ModuleSpecifier: "effect/testing/TestClock",
Name: "testClockWith",
NamespacePrefix: "TestClock",
}}
addToExisting := makeAddNewFix(lsproto.ImportKindNamed, "effect/testing/TestClock", "testClockWith")
addToExisting.Kind = lsproto.AutoImportFixKindAddToExisting

result := transformer(export, []*autoimport.Fix{useNamespace, addToExisting})
if len(result) != 1 {
t.Fatalf("expected 1 fix, got %d", len(result))
}
if result[0].Kind != lsproto.AutoImportFixKindUseNamespace {
t.Fatalf("expected UseNamespace fix, got %v", result[0].Kind)
}
}
Loading
Loading