Skip to content

Commit eef77bc

Browse files
committed
feat(overlayfs): implement overlay filesystem with source overrides support
1 parent b9d7afd commit eef77bc

File tree

7 files changed

+209
-4
lines changed

7 files changed

+209
-4
lines changed

cmd/tsgolint/headless.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,6 @@ func runHeadless(args []string) int {
138138
return 1
139139
}
140140

141-
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
142-
143141
configRaw, err := io.ReadAll(os.Stdin)
144142
if err != nil {
145143
writeErrorMessage(fmt.Sprintf("error reading from stdin: %v", err))
@@ -153,6 +151,12 @@ func runHeadless(args []string) int {
153151
return 1
154152
}
155153

154+
baseFS := osvfs.FS()
155+
if len(payload.SourceOverrides) > 0 {
156+
baseFS = newOverlayFS(baseFS, payload.SourceOverrides)
157+
}
158+
fs := bundled.WrapFS(cachedvfs.From(baseFS))
159+
156160
workload := linter.Workload{
157161
Programs: make(map[string][]string),
158162
UnmatchedFiles: []string{},

cmd/tsgolint/overlayfs.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package main
2+
3+
import (
4+
"time"
5+
6+
"github.com/microsoft/typescript-go/shim/vfs"
7+
)
8+
9+
type overlayFS struct {
10+
underlying vfs.FS
11+
overrides map[string]string
12+
}
13+
14+
func newOverlayFS(underlying vfs.FS, overrides map[string]string) vfs.FS {
15+
return &overlayFS{
16+
underlying: underlying,
17+
overrides: overrides,
18+
}
19+
}
20+
21+
func (o *overlayFS) UseCaseSensitiveFileNames() bool {
22+
return o.underlying.UseCaseSensitiveFileNames()
23+
}
24+
25+
func (o *overlayFS) FileExists(path string) bool {
26+
if _, ok := o.overrides[path]; ok {
27+
return true
28+
}
29+
return o.underlying.FileExists(path)
30+
}
31+
32+
func (o *overlayFS) ReadFile(path string) (string, bool) {
33+
if content, ok := o.overrides[path]; ok {
34+
return content, true
35+
}
36+
return o.underlying.ReadFile(path)
37+
}
38+
39+
func (o *overlayFS) WriteFile(path string, data string, writeByteOrderMark bool) error {
40+
return o.underlying.WriteFile(path, data, writeByteOrderMark)
41+
}
42+
43+
func (o *overlayFS) Remove(path string) error {
44+
return o.underlying.Remove(path)
45+
}
46+
47+
func (o *overlayFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
48+
return o.underlying.Chtimes(path, aTime, mTime)
49+
}
50+
51+
func (o *overlayFS) DirectoryExists(path string) bool {
52+
return o.underlying.DirectoryExists(path)
53+
}
54+
55+
func (o *overlayFS) GetAccessibleEntries(path string) vfs.Entries {
56+
return o.underlying.GetAccessibleEntries(path)
57+
}
58+
59+
func (o *overlayFS) Stat(path string) vfs.FileInfo {
60+
return o.underlying.Stat(path)
61+
}
62+
63+
func (o *overlayFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
64+
return o.underlying.WalkDir(root, walkFn)
65+
}
66+
67+
func (o *overlayFS) Realpath(path string) string {
68+
return o.underlying.Realpath(path)
69+
}

cmd/tsgolint/overlayfs_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
7+
)
8+
9+
func TestOverlayFS(t *testing.T) {
10+
baseFS := osvfs.FS()
11+
overrides := map[string]string{
12+
"/tmp/test.ts": "const x: number = 42;",
13+
}
14+
15+
overlay := newOverlayFS(baseFS, overrides)
16+
17+
content, ok := overlay.ReadFile("/tmp/test.ts")
18+
if !ok {
19+
t.Fatal("Expected to read overridden file")
20+
}
21+
22+
if content != "const x: number = 42;" {
23+
t.Errorf("Expected 'const x: number = 42;', got %q", content)
24+
}
25+
26+
if !overlay.FileExists("/tmp/test.ts") {
27+
t.Error("Expected file to exist")
28+
}
29+
30+
if overlay.UseCaseSensitiveFileNames() != baseFS.UseCaseSensitiveFileNames() {
31+
t.Error("Expected UseCaseSensitiveFileNames to match base FS")
32+
}
33+
}
34+
35+
func TestOverlayFSFallthrough(t *testing.T) {
36+
baseFS := osvfs.FS()
37+
overrides := map[string]string{
38+
"/tmp/override.ts": "overridden",
39+
}
40+
41+
overlay := newOverlayFS(baseFS, overrides)
42+
43+
exists := overlay.FileExists("/nonexistent/file.ts")
44+
if exists {
45+
t.Error("Expected non-overridden non-existent file to not exist")
46+
}
47+
}

cmd/tsgolint/payload.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ type headlessPayloadV1 struct {
1818

1919
// V2 (current) Headless payload format
2020
type headlessPayload struct {
21-
Version int `json:"version"` // version must be 2
22-
Configs []headlessConfig `json:"configs"`
21+
Version int `json:"version"` // version must be 2
22+
Configs []headlessConfig `json:"configs"`
23+
SourceOverrides map[string]string `json:"source_overrides,omitempty"`
2324
}
2425

2526
type headlessConfig struct {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This is the original file on disk without errors
2+
const x: number = 42;
3+
console.log(x);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "commonjs",
5+
"strict": true,
6+
"esModuleInterop": true,
7+
"skipLibCheck": true,
8+
"forceConsistentCasingInFileNames": true
9+
},
10+
"include": ["src/**/*"]
11+
}

e2e/snapshot.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,74 @@ describe('TSGoLint E2E Snapshot Tests', () => {
292292

293293
expect(v1Diagnostics).toStrictEqual(v2Diagnostics);
294294
});
295+
296+
it('should use source overrides instead of reading from disk', async () => {
297+
const testFiles = await getTestFiles('source-overrides');
298+
expect(testFiles.length).toBeGreaterThan(0);
299+
const testFile = testFiles[0];
300+
301+
const overriddenContent = `const promise = new Promise((resolve, _reject) => resolve("value"));
302+
promise;
303+
`;
304+
305+
const config = {
306+
version: 2,
307+
configs: [
308+
{
309+
file_paths: [testFile],
310+
rules: [{ name: 'no-floating-promises' }],
311+
},
312+
],
313+
source_overrides: {
314+
[testFile]: overriddenContent,
315+
},
316+
};
317+
318+
const env = { ...process.env, GOMAXPROCS: '1' };
319+
const output = execFileSync(TSGOLINT_BIN, ['headless'], {
320+
input: JSON.stringify(config),
321+
env,
322+
});
323+
324+
let diagnostics = parseHeadlessOutput(output);
325+
diagnostics = sortDiagnostics(diagnostics);
326+
327+
expect(diagnostics.length).toBe(1);
328+
expect(diagnostics[0].rule).toBe('no-floating-promises');
329+
expect(diagnostics[0].file_path).toContain('original.ts');
330+
});
331+
332+
it('should not report errors when source override is valid', async () => {
333+
const testFiles = await getTestFiles('source-overrides');
334+
expect(testFiles.length).toBeGreaterThan(0);
335+
const testFile = testFiles[0];
336+
337+
const validOverride = `// Valid code with no errors
338+
const x: number = 42;
339+
console.log(x);
340+
`;
341+
342+
const config = {
343+
version: 2,
344+
configs: [
345+
{
346+
file_paths: [testFile],
347+
rules: [{ name: 'no-floating-promises' }, { name: 'no-unsafe-assignment' }],
348+
},
349+
],
350+
source_overrides: {
351+
[testFile]: validOverride,
352+
},
353+
};
354+
355+
const env = { ...process.env, GOMAXPROCS: '1' };
356+
const output = execFileSync(TSGOLINT_BIN, ['headless'], {
357+
input: JSON.stringify(config),
358+
env,
359+
});
360+
361+
const diagnostics = parseHeadlessOutput(output);
362+
363+
expect(diagnostics.length).toBe(0);
364+
});
295365
});

0 commit comments

Comments
 (0)