Skip to content

Commit 343ebcf

Browse files
fi3eworkCopilot
andauthored
fix(lsp): should load jsonc config as well (#222)
Co-authored-by: Copilot <[email protected]>
1 parent a56bf09 commit 343ebcf

File tree

2 files changed

+271
-30
lines changed

2 files changed

+271
-30
lines changed

cmd/rslint/lsp.go

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/microsoft/typescript-go/shim/compiler"
1717
"github.com/microsoft/typescript-go/shim/lsp/lsproto"
1818
"github.com/microsoft/typescript-go/shim/scanner"
19+
"github.com/microsoft/typescript-go/shim/vfs"
1920
"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
2021
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
2122
"github.com/sourcegraph/jsonrpc2"
@@ -272,39 +273,12 @@ func (s *LSPServer) runDiagnostics(ctx context.Context, uri lsproto.DocumentUri,
272273

273274
host := utils.CreateCompilerHost(workingDir, vfs)
274275

275-
// Try to find rslint.json with multiple strategies
276+
// Try to find rslint configuration files with multiple strategies
276277
var rslintConfigPath string
277278
var configFound bool
278279

279-
// Strategy 1: Try in the working directory
280-
rslintConfigPath = workingDir + "/rslint.json"
281-
if vfs.FileExists(rslintConfigPath) {
282-
configFound = true
283-
}
284-
285-
// Strategy 2: If not found, walk up the directory tree
286-
if !configFound && filePath != "" {
287-
dir := filePath
288-
if idx := strings.LastIndex(dir, "/"); idx != -1 {
289-
dir = dir[:idx]
290-
}
291-
292-
for i := 0; i < 5 && dir != "/" && dir != ""; i++ { // Limit search depth
293-
testPath := dir + "/rslint.json"
294-
if vfs.FileExists(testPath) {
295-
rslintConfigPath = testPath
296-
workingDir = dir
297-
configFound = true
298-
break
299-
}
300-
// Move up one directory
301-
if idx := strings.LastIndex(dir, "/"); idx != -1 {
302-
dir = dir[:idx]
303-
} else {
304-
break
305-
}
306-
}
307-
}
280+
// Use helper function to find config
281+
rslintConfigPath, workingDir, configFound = findRslintConfig(vfs, workingDir, filePath)
308282

309283
if !configFound {
310284
return
@@ -441,6 +415,44 @@ func uriToPath(uri string) string {
441415
return uri
442416
}
443417

418+
// findRslintConfig searches for rslint configuration files using multiple strategies
419+
func findRslintConfig(fs vfs.FS, workingDir, filePath string) (string, string, bool) {
420+
defaultConfigs := []string{"rslint.json", "rslint.jsonc"}
421+
422+
// Strategy 1: Try in the working directory
423+
for _, configName := range defaultConfigs {
424+
configPath := workingDir + "/" + configName
425+
if fs.FileExists(configPath) {
426+
return configPath, workingDir, true
427+
}
428+
}
429+
430+
// Strategy 2: If not found, walk up the directory tree
431+
if filePath != "" {
432+
dir := filePath
433+
if idx := strings.LastIndex(dir, "/"); idx != -1 {
434+
dir = dir[:idx]
435+
}
436+
437+
for i := 0; i < 5 && dir != "/" && dir != ""; i++ { // Limit search depth
438+
for _, configName := range defaultConfigs {
439+
testPath := dir + "/" + configName
440+
if fs.FileExists(testPath) {
441+
return testPath, dir, true
442+
}
443+
}
444+
// Move up one directory
445+
if idx := strings.LastIndex(dir, "/"); idx != -1 {
446+
dir = dir[:idx]
447+
} else {
448+
break
449+
}
450+
}
451+
}
452+
453+
return "", workingDir, false
454+
}
455+
444456
func runLSP() int {
445457
log.SetOutput(os.Stderr) // Send logs to stderr so they don't interfere with LSP communication
446458

cmd/rslint/lsp_test.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/shim/vfs"
7+
)
8+
9+
// mockFS is a mock implementation of vfs.FS for testing. Only FileExists is properly implemented;
10+
// other methods are stubbed with default implementations.
11+
type mockFS struct {
12+
files map[string]bool // path -> exists
13+
}
14+
15+
func (m *mockFS) FileExists(path string) bool {
16+
exists, found := m.files[path]
17+
if !found {
18+
return false
19+
}
20+
return exists
21+
}
22+
23+
// Stubbed implementations of other vfs.FS interface methods for testing purposes
24+
func (m *mockFS) UseCaseSensitiveFileNames() bool { return true }
25+
func (m *mockFS) ReadFile(path string) (string, bool) { return "", false }
26+
func (m *mockFS) WriteFile(path string, data string, writeByteOrderMark bool) error { return nil }
27+
func (m *mockFS) Remove(path string) error { return nil }
28+
func (m *mockFS) DirectoryExists(path string) bool { return false }
29+
func (m *mockFS) GetAccessibleEntries(path string) vfs.Entries { return vfs.Entries{} }
30+
func (m *mockFS) Stat(path string) vfs.FileInfo { return nil }
31+
func (m *mockFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { return nil }
32+
func (m *mockFS) Realpath(path string) string { return path }
33+
34+
func TestFindRslintConfig(t *testing.T) {
35+
tests := []struct {
36+
name string
37+
workingDir string
38+
filePath string
39+
fileSystemMap map[string]bool // path -> exists
40+
expectedPath string
41+
expectedDir string
42+
expectedFound bool
43+
}{
44+
{
45+
name: "config found in working directory - rslint.json",
46+
workingDir: "/project",
47+
filePath: "/project/src/file.ts",
48+
fileSystemMap: map[string]bool{
49+
"/project/rslint.json": true,
50+
},
51+
expectedPath: "/project/rslint.json",
52+
expectedDir: "/project",
53+
expectedFound: true,
54+
},
55+
{
56+
name: "config found in working directory - rslint.jsonc",
57+
workingDir: "/project",
58+
filePath: "/project/src/file.ts",
59+
fileSystemMap: map[string]bool{
60+
"/project/rslint.json": false,
61+
"/project/rslint.jsonc": true,
62+
},
63+
expectedPath: "/project/rslint.jsonc",
64+
expectedDir: "/project",
65+
expectedFound: true,
66+
},
67+
{
68+
name: "config not found in working directory, found in parent",
69+
workingDir: "/project/src",
70+
filePath: "/project/src/components/file.ts",
71+
fileSystemMap: map[string]bool{
72+
"/project/src/rslint.json": false,
73+
"/project/src/rslint.jsonc": false,
74+
"/project/src/components/rslint.json": false,
75+
"/project/src/components/rslint.jsonc": false,
76+
"/project/rslint.json": true,
77+
},
78+
expectedPath: "/project/rslint.json",
79+
expectedDir: "/project",
80+
expectedFound: true,
81+
},
82+
{
83+
name: "config found by walking up from file path",
84+
workingDir: "/wrong/path",
85+
filePath: "/project/src/components/file.ts",
86+
fileSystemMap: map[string]bool{
87+
"/wrong/path/rslint.json": false,
88+
"/wrong/path/rslint.jsonc": false,
89+
"/project/src/components/rslint.json": false,
90+
"/project/src/components/rslint.jsonc": false,
91+
"/project/src/rslint.json": false,
92+
"/project/src/rslint.jsonc": false,
93+
"/project/rslint.json": true,
94+
},
95+
expectedPath: "/project/rslint.json",
96+
expectedDir: "/project",
97+
expectedFound: true,
98+
},
99+
{
100+
name: "config found multiple levels up",
101+
workingDir: "/project/src/components/nested",
102+
filePath: "/project/src/components/nested/deep/file.ts",
103+
fileSystemMap: map[string]bool{
104+
"/project/src/components/nested/rslint.json": false,
105+
"/project/src/components/nested/rslint.jsonc": false,
106+
"/project/src/components/nested/deep/rslint.json": false,
107+
"/project/src/components/nested/deep/rslint.jsonc": false,
108+
"/project/src/components/rslint.json": false,
109+
"/project/src/components/rslint.jsonc": false,
110+
"/project/src/rslint.json": false,
111+
"/project/src/rslint.jsonc": false,
112+
"/project/rslint.jsonc": true,
113+
},
114+
expectedPath: "/project/rslint.jsonc",
115+
expectedDir: "/project",
116+
expectedFound: true,
117+
},
118+
{
119+
name: "no config found anywhere",
120+
workingDir: "/project",
121+
filePath: "/project/src/file.ts",
122+
fileSystemMap: map[string]bool{
123+
"/project/rslint.json": false,
124+
"/project/rslint.jsonc": false,
125+
"/project/src/rslint.json": false,
126+
"/project/src/rslint.jsonc": false,
127+
"/rslint.json": false,
128+
"/rslint.jsonc": false,
129+
},
130+
expectedPath: "",
131+
expectedDir: "/project",
132+
expectedFound: false,
133+
},
134+
{
135+
name: "empty file path - only check working directory",
136+
workingDir: "/project",
137+
filePath: "",
138+
fileSystemMap: map[string]bool{
139+
"/project/rslint.json": false,
140+
"/project/rslint.jsonc": true,
141+
},
142+
expectedPath: "/project/rslint.jsonc",
143+
expectedDir: "/project",
144+
expectedFound: true,
145+
},
146+
{
147+
name: "empty file path - no config in working directory",
148+
workingDir: "/project",
149+
filePath: "",
150+
fileSystemMap: map[string]bool{
151+
"/project/rslint.json": false,
152+
"/project/rslint.jsonc": false,
153+
},
154+
expectedPath: "",
155+
expectedDir: "/project",
156+
expectedFound: false,
157+
},
158+
{
159+
name: "prefer rslint.json over rslint.jsonc when both exist",
160+
workingDir: "/project",
161+
filePath: "/project/src/file.ts",
162+
fileSystemMap: map[string]bool{
163+
"/project/rslint.json": true,
164+
"/project/rslint.jsonc": true,
165+
},
166+
expectedPath: "/project/rslint.json",
167+
expectedDir: "/project",
168+
expectedFound: true,
169+
},
170+
{
171+
name: "search depth limit - stop after 5 levels",
172+
workingDir: "/very/deep/nested/structure/level5",
173+
filePath: "/very/deep/nested/structure/level5/level6/level7/file.ts",
174+
fileSystemMap: map[string]bool{
175+
"/very/deep/nested/structure/level5/rslint.json": false,
176+
"/very/deep/nested/structure/level5/rslint.jsonc": false,
177+
"/very/deep/nested/structure/level5/level6/rslint.json": false,
178+
"/very/deep/nested/structure/level5/level6/rslint.jsonc": false,
179+
"/very/deep/nested/structure/level5/level6/level7/rslint.json": false,
180+
"/very/deep/nested/structure/level5/level6/level7/rslint.jsonc": false,
181+
"/very/deep/nested/structure/rslint.json": false,
182+
"/very/deep/nested/structure/rslint.jsonc": false,
183+
"/very/deep/nested/rslint.json": false,
184+
"/very/deep/nested/rslint.jsonc": false,
185+
"/very/deep/rslint.json": false,
186+
"/very/deep/rslint.jsonc": false,
187+
// This should not be reached due to 5-level limit
188+
"/very/rslint.json": true,
189+
},
190+
expectedPath: "",
191+
expectedDir: "/very/deep/nested/structure/level5",
192+
expectedFound: false,
193+
},
194+
{
195+
name: "windows-style paths (for cross-platform compatibility)",
196+
workingDir: "/c/project",
197+
filePath: "/c/project/src/file.ts",
198+
fileSystemMap: map[string]bool{
199+
"/c/project/rslint.json": true,
200+
},
201+
expectedPath: "/c/project/rslint.json",
202+
expectedDir: "/c/project",
203+
expectedFound: true,
204+
},
205+
}
206+
207+
for _, tt := range tests {
208+
t.Run(tt.name, func(t *testing.T) {
209+
// Create mock VFS
210+
mockFS := &mockFS{
211+
files: tt.fileSystemMap,
212+
}
213+
214+
// Call the function under test
215+
gotPath, gotDir, gotFound := findRslintConfig(mockFS, tt.workingDir, tt.filePath)
216+
217+
// Assert results
218+
if gotPath != tt.expectedPath {
219+
t.Errorf("findRslintConfig() gotPath = %v, want %v", gotPath, tt.expectedPath)
220+
}
221+
if gotDir != tt.expectedDir {
222+
t.Errorf("findRslintConfig() gotDir = %v, want %v", gotDir, tt.expectedDir)
223+
}
224+
if gotFound != tt.expectedFound {
225+
t.Errorf("findRslintConfig() gotFound = %v, want %v", gotFound, tt.expectedFound)
226+
}
227+
})
228+
}
229+
}

0 commit comments

Comments
 (0)