diff --git a/cmd/tsgolint/headless.go b/cmd/tsgolint/headless.go index 5e44055a..d5f12b43 100644 --- a/cmd/tsgolint/headless.go +++ b/cmd/tsgolint/headless.go @@ -138,8 +138,6 @@ func runHeadless(args []string) int { return 1 } - fs := bundled.WrapFS(cachedvfs.From(osvfs.FS())) - configRaw, err := io.ReadAll(os.Stdin) if err != nil { writeErrorMessage(fmt.Sprintf("error reading from stdin: %v", err)) @@ -153,6 +151,12 @@ func runHeadless(args []string) int { return 1 } + baseFS := osvfs.FS() + if len(payload.SourceOverrides) > 0 { + baseFS = newOverlayFS(baseFS, payload.SourceOverrides) + } + fs := bundled.WrapFS(cachedvfs.From(baseFS)) + workload := linter.Workload{ Programs: make(map[string][]string), UnmatchedFiles: []string{}, diff --git a/cmd/tsgolint/overlayfs.go b/cmd/tsgolint/overlayfs.go new file mode 100644 index 00000000..c575b8c2 --- /dev/null +++ b/cmd/tsgolint/overlayfs.go @@ -0,0 +1,69 @@ +package main + +import ( + "time" + + "github.com/microsoft/typescript-go/shim/vfs" +) + +type overlayFS struct { + underlying vfs.FS + overrides map[string]string +} + +func newOverlayFS(underlying vfs.FS, overrides map[string]string) vfs.FS { + return &overlayFS{ + underlying: underlying, + overrides: overrides, + } +} + +func (o *overlayFS) UseCaseSensitiveFileNames() bool { + return o.underlying.UseCaseSensitiveFileNames() +} + +func (o *overlayFS) FileExists(path string) bool { + if _, ok := o.overrides[path]; ok { + return true + } + return o.underlying.FileExists(path) +} + +func (o *overlayFS) ReadFile(path string) (string, bool) { + if content, ok := o.overrides[path]; ok { + return content, true + } + return o.underlying.ReadFile(path) +} + +func (o *overlayFS) WriteFile(path string, data string, writeByteOrderMark bool) error { + return o.underlying.WriteFile(path, data, writeByteOrderMark) +} + +func (o *overlayFS) Remove(path string) error { + return o.underlying.Remove(path) +} + +func (o *overlayFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { + return o.underlying.Chtimes(path, aTime, mTime) +} + +func (o *overlayFS) DirectoryExists(path string) bool { + return o.underlying.DirectoryExists(path) +} + +func (o *overlayFS) GetAccessibleEntries(path string) vfs.Entries { + return o.underlying.GetAccessibleEntries(path) +} + +func (o *overlayFS) Stat(path string) vfs.FileInfo { + return o.underlying.Stat(path) +} + +func (o *overlayFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + return o.underlying.WalkDir(root, walkFn) +} + +func (o *overlayFS) Realpath(path string) string { + return o.underlying.Realpath(path) +} diff --git a/cmd/tsgolint/overlayfs_test.go b/cmd/tsgolint/overlayfs_test.go new file mode 100644 index 00000000..4c0b5f2f --- /dev/null +++ b/cmd/tsgolint/overlayfs_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "testing" + + "github.com/microsoft/typescript-go/shim/vfs/osvfs" +) + +func TestOverlayFS(t *testing.T) { + baseFS := osvfs.FS() + overrides := map[string]string{ + "/tmp/test.ts": "const x: number = 42;", + } + + overlay := newOverlayFS(baseFS, overrides) + + content, ok := overlay.ReadFile("/tmp/test.ts") + if !ok { + t.Fatal("Expected to read overridden file") + } + + if content != "const x: number = 42;" { + t.Errorf("Expected 'const x: number = 42;', got %q", content) + } + + if !overlay.FileExists("/tmp/test.ts") { + t.Error("Expected file to exist") + } + + if overlay.UseCaseSensitiveFileNames() != baseFS.UseCaseSensitiveFileNames() { + t.Error("Expected UseCaseSensitiveFileNames to match base FS") + } +} + +func TestOverlayFSFallthrough(t *testing.T) { + baseFS := osvfs.FS() + overrides := map[string]string{ + "/tmp/override.ts": "overridden", + } + + overlay := newOverlayFS(baseFS, overrides) + + exists := overlay.FileExists("/nonexistent/file.ts") + if exists { + t.Error("Expected non-overridden non-existent file to not exist") + } +} diff --git a/cmd/tsgolint/payload.go b/cmd/tsgolint/payload.go index 8be8f950..35712cde 100644 --- a/cmd/tsgolint/payload.go +++ b/cmd/tsgolint/payload.go @@ -18,8 +18,9 @@ type headlessPayloadV1 struct { // V2 (current) Headless payload format type headlessPayload struct { - Version int `json:"version"` // version must be 2 - Configs []headlessConfig `json:"configs"` + Version int `json:"version"` // version must be 2 + Configs []headlessConfig `json:"configs"` + SourceOverrides map[string]string `json:"source_overrides,omitempty"` } type headlessConfig struct { diff --git a/e2e/fixtures/source-overrides/src/original.ts b/e2e/fixtures/source-overrides/src/original.ts new file mode 100644 index 00000000..25a1d6dd --- /dev/null +++ b/e2e/fixtures/source-overrides/src/original.ts @@ -0,0 +1,3 @@ +// This is the original file on disk without errors +const x: number = 42; +console.log(x); diff --git a/e2e/fixtures/source-overrides/tsconfig.json b/e2e/fixtures/source-overrides/tsconfig.json new file mode 100644 index 00000000..60e26cb1 --- /dev/null +++ b/e2e/fixtures/source-overrides/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +} diff --git a/e2e/snapshot.test.ts b/e2e/snapshot.test.ts index 3daecf16..d8b6b06c 100644 --- a/e2e/snapshot.test.ts +++ b/e2e/snapshot.test.ts @@ -292,4 +292,74 @@ describe('TSGoLint E2E Snapshot Tests', () => { expect(v1Diagnostics).toStrictEqual(v2Diagnostics); }); + + it('should use source overrides instead of reading from disk', async () => { + const testFiles = await getTestFiles('source-overrides'); + expect(testFiles.length).toBeGreaterThan(0); + const testFile = testFiles[0]; + + const overriddenContent = `const promise = new Promise((resolve, _reject) => resolve("value")); +promise; +`; + + const config = { + version: 2, + configs: [ + { + file_paths: [testFile], + rules: [{ name: 'no-floating-promises' }], + }, + ], + source_overrides: { + [testFile]: overriddenContent, + }, + }; + + const env = { ...process.env, GOMAXPROCS: '1' }; + const output = execFileSync(TSGOLINT_BIN, ['headless'], { + input: JSON.stringify(config), + env, + }); + + let diagnostics = parseHeadlessOutput(output); + diagnostics = sortDiagnostics(diagnostics); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].rule).toBe('no-floating-promises'); + expect(diagnostics[0].file_path).toContain('original.ts'); + }); + + it('should not report errors when source override is valid', async () => { + const testFiles = await getTestFiles('source-overrides'); + expect(testFiles.length).toBeGreaterThan(0); + const testFile = testFiles[0]; + + const validOverride = `// Valid code with no errors +const x: number = 42; +console.log(x); +`; + + const config = { + version: 2, + configs: [ + { + file_paths: [testFile], + rules: [{ name: 'no-floating-promises' }, { name: 'no-unsafe-assignment' }], + }, + ], + source_overrides: { + [testFile]: validOverride, + }, + }; + + const env = { ...process.env, GOMAXPROCS: '1' }; + const output = execFileSync(TSGOLINT_BIN, ['headless'], { + input: JSON.stringify(config), + env, + }); + + const diagnostics = parseHeadlessOutput(output); + + expect(diagnostics.length).toBe(0); + }); });