Skip to content

Commit eb83c1c

Browse files
CopilotDanielRosenwasserRyanCavanaugh
authored
Fix invalid URI for untitled references/definitions (#1344)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: DanielRosenwasser <[email protected]> Co-authored-by: Daniel Rosenwasser <[email protected]> Co-authored-by: RyanCavanaugh <[email protected]>
1 parent 256441c commit eb83c1c

File tree

4 files changed

+226
-1
lines changed

4 files changed

+226
-1
lines changed

internal/ast/ast.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10164,7 +10164,7 @@ type SourceFile struct {
1016410164
}
1016510165

1016610166
func (f *NodeFactory) NewSourceFile(opts SourceFileParseOptions, text string, statements *NodeList, endOfFileToken *TokenNode) *Node {
10167-
if (tspath.GetEncodedRootLength(opts.FileName) == 0 && !strings.HasPrefix(opts.FileName, "^/")) || opts.FileName != tspath.NormalizePath(opts.FileName) {
10167+
if tspath.GetEncodedRootLength(opts.FileName) == 0 || opts.FileName != tspath.NormalizePath(opts.FileName) {
1016810168
panic(fmt.Sprintf("fileName should be normalized and absolute: %q", opts.FileName))
1016910169
}
1017010170
data := &SourceFile{}

internal/ls/untitled_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package ls_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/microsoft/typescript-go/internal/bundled"
8+
"github.com/microsoft/typescript-go/internal/ls"
9+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
10+
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
11+
"github.com/microsoft/typescript-go/internal/tspath"
12+
"gotest.tools/v3/assert"
13+
)
14+
15+
func TestUntitledReferences(t *testing.T) {
16+
t.Parallel()
17+
if !bundled.Embedded {
18+
t.Skip("bundled files are not embedded")
19+
}
20+
21+
// First test the URI conversion functions to understand the issue
22+
untitledURI := lsproto.DocumentUri("untitled:Untitled-2")
23+
convertedFileName := ls.DocumentURIToFileName(untitledURI)
24+
t.Logf("URI '%s' converts to filename '%s'", untitledURI, convertedFileName)
25+
26+
backToURI := ls.FileNameToDocumentURI(convertedFileName)
27+
t.Logf("Filename '%s' converts back to URI '%s'", convertedFileName, backToURI)
28+
29+
if string(backToURI) != string(untitledURI) {
30+
t.Errorf("Round-trip conversion failed: '%s' -> '%s' -> '%s'", untitledURI, convertedFileName, backToURI)
31+
}
32+
33+
// Create a test case that simulates how untitled files should work
34+
testContent := `let x = 42;
35+
36+
x
37+
38+
x++;`
39+
40+
// Use the converted filename that DocumentURIToFileName would produce
41+
untitledFileName := convertedFileName // "^/untitled/ts-nul-authority/Untitled-2"
42+
t.Logf("Would use untitled filename: %s", untitledFileName)
43+
44+
// Set up the file system with an untitled file -
45+
// But use a regular file first to see the current behavior
46+
files := map[string]string{
47+
"/Untitled-2.ts": testContent,
48+
}
49+
50+
ctx := projecttestutil.WithRequestID(t.Context())
51+
service, done := createLanguageService(ctx, "/Untitled-2.ts", files)
52+
defer done()
53+
54+
// Test the filename that the source file reports
55+
program := service.GetProgram()
56+
sourceFile := program.GetSourceFile("/Untitled-2.ts")
57+
t.Logf("SourceFile.FileName() returns: '%s'", sourceFile.FileName())
58+
59+
// Calculate position of 'x' on line 3 (zero-indexed line 2, character 0)
60+
position := 13 // After "let x = 42;\n\n"
61+
62+
// Call ProvideReferences using the test method
63+
refs := service.TestProvideReferences("/Untitled-2.ts", position)
64+
65+
// Log the results
66+
t.Logf("Input file name: %s", "/Untitled-2.ts")
67+
t.Logf("Number of references found: %d", len(refs))
68+
for i, ref := range refs {
69+
t.Logf("Reference %d: URI=%s, Range=%+v", i+1, ref.Uri, ref.Range)
70+
}
71+
72+
// We expect to find 3 references
73+
assert.Assert(t, len(refs) == 3, "Expected 3 references, got %d", len(refs))
74+
75+
// Also test definition using ProvideDefinition
76+
uri := ls.FileNameToDocumentURI("/Untitled-2.ts")
77+
lspPosition := lsproto.Position{Line: 2, Character: 0}
78+
definition, err := service.ProvideDefinition(t.Context(), uri, lspPosition)
79+
assert.NilError(t, err)
80+
if definition != nil && definition.Locations != nil {
81+
t.Logf("Definition found: %d locations", len(*definition.Locations))
82+
for i, loc := range *definition.Locations {
83+
t.Logf("Definition %d: URI=%s, Range=%+v", i+1, loc.Uri, loc.Range)
84+
}
85+
}
86+
}
87+
88+
func TestUntitledFileNameDebugging(t *testing.T) {
89+
t.Parallel()
90+
if !bundled.Embedded {
91+
t.Skip("bundled files are not embedded")
92+
}
93+
94+
// Test the URI conversion flow
95+
untitledURI := lsproto.DocumentUri("untitled:Untitled-2")
96+
convertedFileName := ls.DocumentURIToFileName(untitledURI)
97+
t.Logf("1. URI '%s' converts to filename '%s'", untitledURI, convertedFileName)
98+
99+
// Test the path handling
100+
currentDir := "/home/daniel/TypeScript"
101+
path := tspath.ToPath(convertedFileName, currentDir, true)
102+
t.Logf("2. ToPath('%s', '%s') returns: '%s'", convertedFileName, currentDir, string(path))
103+
104+
// Verify the path is NOT resolved against current directory
105+
if strings.HasPrefix(string(path), currentDir) {
106+
t.Errorf("Path was incorrectly resolved against current directory: %s", string(path))
107+
}
108+
109+
// Test converting back to URI
110+
backToURI := ls.FileNameToDocumentURI(string(path))
111+
t.Logf("3. Path '%s' converts back to URI '%s'", string(path), backToURI)
112+
113+
if string(backToURI) != string(untitledURI) {
114+
t.Errorf("Round-trip conversion failed: '%s' -> '%s' -> '%s'", untitledURI, string(path), backToURI)
115+
}
116+
117+
t.Logf("✅ Fix working: untitled paths are not resolved against current directory")
118+
}
119+
120+
func TestUntitledFileIntegration(t *testing.T) {
121+
t.Parallel()
122+
if !bundled.Embedded {
123+
t.Skip("bundled files are not embedded")
124+
}
125+
126+
// This test simulates the exact scenario from the issue:
127+
// 1. VS Code sends untitled:Untitled-2 URI
128+
// 2. References/definitions should return untitled:Untitled-2 URIs, not file:// URIs
129+
130+
// Simulate exactly what happens in the LSP flow
131+
originalURI := lsproto.DocumentUri("untitled:Untitled-2")
132+
133+
// Step 1: URI gets converted to filename when file is opened
134+
fileName := ls.DocumentURIToFileName(originalURI)
135+
t.Logf("1. Opening file: URI '%s' -> fileName '%s'", originalURI, fileName)
136+
137+
// Step 2: fileName gets processed through ToPath in project service
138+
currentDir := "/home/daniel/TypeScript" // Current directory from the original issue
139+
path := tspath.ToPath(fileName, currentDir, true)
140+
t.Logf("2. Project service processes: fileName '%s' -> path '%s'", fileName, string(path))
141+
142+
// Step 3: Verify path is NOT corrupted by current directory resolution
143+
if strings.HasPrefix(string(path), currentDir) {
144+
t.Fatalf("❌ BUG: Path was incorrectly resolved against current directory: %s", string(path))
145+
}
146+
147+
// Step 4: When references are found, the path gets converted back to URI
148+
resultURI := ls.FileNameToDocumentURI(string(path))
149+
t.Logf("3. References return: path '%s' -> URI '%s'", string(path), resultURI)
150+
151+
// Step 5: Verify the round-trip conversion works
152+
if string(resultURI) != string(originalURI) {
153+
t.Fatalf("❌ Round-trip failed: %s != %s", originalURI, resultURI)
154+
}
155+
156+
t.Logf("✅ SUCCESS: Untitled file URIs are preserved correctly")
157+
t.Logf(" Original URI: %s", originalURI)
158+
t.Logf(" Final URI: %s", resultURI)
159+
}

internal/tspath/path.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ func GetEncodedRootLength(path string) int {
190190
}
191191
}
192192

193+
// Untitled paths (e.g., "^/untitled/ts-nul-authority/Untitled-1")
194+
if ch0 == '^' && ln > 1 && path[1] == '/' {
195+
return 2 // Untitled: "^/"
196+
}
197+
193198
// URL
194199
schemeEnd := strings.Index(path, urlSchemeSeparator)
195200
if schemeEnd != -1 {

internal/tspath/untitled_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package tspath_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/tspath"
7+
"gotest.tools/v3/assert"
8+
)
9+
10+
func TestUntitledPathHandling(t *testing.T) {
11+
t.Parallel()
12+
// Test that untitled paths are treated as rooted
13+
untitledPath := "^/untitled/ts-nul-authority/Untitled-2"
14+
15+
// GetEncodedRootLength should return 2 for "^/"
16+
rootLength := tspath.GetEncodedRootLength(untitledPath)
17+
assert.Equal(t, rootLength, 2, "GetEncodedRootLength should return 2 for untitled paths")
18+
19+
// IsRootedDiskPath should return true
20+
isRooted := tspath.IsRootedDiskPath(untitledPath)
21+
assert.Assert(t, isRooted, "IsRootedDiskPath should return true for untitled paths")
22+
23+
// ToPath should not resolve untitled paths against current directory
24+
currentDir := "/home/user/project"
25+
path := tspath.ToPath(untitledPath, currentDir, true)
26+
// The path should be the original untitled path
27+
assert.Equal(t, string(path), "^/untitled/ts-nul-authority/Untitled-2", "ToPath should not resolve untitled paths against current directory")
28+
29+
// Test GetNormalizedAbsolutePath doesn't resolve untitled paths
30+
normalized := tspath.GetNormalizedAbsolutePath(untitledPath, currentDir)
31+
assert.Equal(t, normalized, "^/untitled/ts-nul-authority/Untitled-2", "GetNormalizedAbsolutePath should not resolve untitled paths")
32+
}
33+
34+
func TestUntitledPathEdgeCases(t *testing.T) {
35+
t.Parallel()
36+
// Test edge cases
37+
testCases := []struct {
38+
path string
39+
expected int
40+
isRooted bool
41+
}{
42+
{"^/", 2, true}, // Minimal untitled path
43+
{"^/untitled/ts-nul-authority/test", 2, true}, // Normal untitled path
44+
{"^", 0, false}, // Just ^ is not rooted
45+
{"^x", 0, false}, // ^x is not untitled
46+
{"^^/", 0, false}, // ^^/ is not untitled
47+
{"x^/", 0, false}, // x^/ is not untitled (doesn't start with ^)
48+
{"^/untitled/ts-nul-authority/path/with/deeper/structure", 2, true}, // Deeper path
49+
}
50+
51+
for _, tc := range testCases {
52+
t.Run(tc.path, func(t *testing.T) {
53+
t.Parallel()
54+
rootLength := tspath.GetEncodedRootLength(tc.path)
55+
assert.Equal(t, rootLength, tc.expected, "GetEncodedRootLength for path %s", tc.path)
56+
57+
isRooted := tspath.IsRootedDiskPath(tc.path)
58+
assert.Equal(t, isRooted, tc.isRooted, "IsRootedDiskPath for path %s", tc.path)
59+
})
60+
}
61+
}

0 commit comments

Comments
 (0)