Skip to content

Verify API mode support for compilerOptions overrides implementation #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion cmd/rslint/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func (h *IPCHandler) HandleLint(req api.LintRequest) (*api.LintResponse, error)
// Create programs from all tsconfig files found in rslint config
programs := []*compiler.Program{}
for _, configFileName := range tsConfigs {
program, err := utils.CreateProgram(false, fs, configDirectory, configFileName, host)
program, err := utils.CreateProgramWithOverrides(false, fs, configDirectory, configFileName, host, req.CompilerOptions)
if err != nil {
return nil, fmt.Errorf("error creating TS program for %s: %w", configFileName, err)
}
Expand Down
86 changes: 86 additions & 0 deletions docs/api-compiler-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Compiler Options Override Support

The API mode now supports passing `compilerOptions` to override settings from the TypeScript configuration file.

## Usage

When making a lint request via the API, you can now include a `compilerOptions` field that will override any corresponding options in your `tsconfig.json`:

```json
{
"kind": "lint",
"id": 1,
"data": {
"files": ["src/**/*.ts"],
"config": "./rslint.json",
"compilerOptions": {
"strict": true,
"target": 7,
"noImplicitAny": true,
"skipLibCheck": true
},
"ruleOptions": {
"no-unused-vars": "error"
}
}
}
```

## Notes

- The `compilerOptions` field accepts a map of option name to value
- These options will override any corresponding options from your `tsconfig.json`
- Numeric options (like `target`) should be passed as numbers corresponding to the TypeScript enum values
- Boolean options can be passed as `true`/`false`
- String options should be passed as strings
- Array options should be passed as arrays

## TypeScript Target Values

Common target values:

- `1` = ES3
- `2` = ES5
- `3` = ES2015 (ES6)
- `4` = ES2016
- `5` = ES2017
- `6` = ES2018
- `7` = ES2019
- `8` = ES2020
- `9` = ES2021
- `10` = ES2022
- `99` = ESNext

## Module Values

Common module values:

- `0` = None
- `1` = CommonJS
- `2` = AMD
- `3` = UMD
- `4` = System
- `5` = ES2015 (ES6)
- `6` = ES2020
- `7` = ES2022
- `99` = ESNext
- `100` = Node16
- `199` = NodeNext

## Example

```json
{
"compilerOptions": {
"target": 8,
"module": 1,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ES2020", "DOM"]
}
}
```

This will override the corresponding settings in your `tsconfig.json` for the duration of the linting session.
5 changes: 3 additions & 2 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ type LintRequest struct {
Format string `json:"format,omitempty"`
WorkingDirectory string `json:"workingDirectory,omitempty"`
// Supports both string level and array [level, options] format
RuleOptions map[string]interface{} `json:"ruleOptions,omitempty"`
FileContents map[string]string `json:"fileContents,omitempty"` // Map of file paths to their contents for VFS
RuleOptions map[string]interface{} `json:"ruleOptions,omitempty"`
FileContents map[string]string `json:"fileContents,omitempty"` // Map of file paths to their contents for VFS
CompilerOptions map[string]interface{} `json:"compilerOptions,omitempty"` // TypeScript compiler options to override config file settings
}

// LintResponse represents a lint response from Go to JS
Expand Down
16 changes: 15 additions & 1 deletion internal/utils/create_program.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,26 @@
}

func CreateProgram(singleThreaded bool, fs vfs.FS, cwd string, tsconfigPath string, host compiler.CompilerHost) (*compiler.Program, error) {
return CreateProgramWithOverrides(singleThreaded, fs, cwd, tsconfigPath, host, nil)
}

func CreateProgramWithOverrides(singleThreaded bool, fs vfs.FS, cwd string, tsconfigPath string, host compiler.CompilerHost, compilerOptionsOverrides map[string]interface{}) (*compiler.Program, error) {
resolvedConfigPath := tspath.ResolvePath(cwd, tsconfigPath)
if !fs.FileExists(resolvedConfigPath) {
return nil, fmt.Errorf("couldn't read tsconfig at %v", resolvedConfigPath)
}

configParseResult, _ := tsoptions.GetParsedCommandLineOfConfigFile(tsconfigPath, &core.CompilerOptions{}, host, nil)
// Start with empty existing options, we'll apply overrides later
existingOptions := &core.CompilerOptions{}

// Apply compiler options overrides if provided
if compilerOptionsOverrides != nil {

Check failure on line 37 in internal/utils/create_program.go

View workflow job for this annotation

GitHub Actions / Test Go (1.24.1)

S1031: unnecessary nil check around range (staticcheck)
for key, value := range compilerOptionsOverrides {
tsoptions.ParseCompilerOptions(key, value, existingOptions)
}
}

configParseResult, _ := tsoptions.GetParsedCommandLineOfConfigFile(tsconfigPath, existingOptions, host, nil)

opts := compiler.ProgramOptions{
Config: configParseResult,
Expand Down
84 changes: 84 additions & 0 deletions internal/utils/create_program_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package utils

import (
"os"
"path/filepath"
"testing"

"github.com/microsoft/typescript-go/shim/bundled"
"github.com/microsoft/typescript-go/shim/core"
"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
)

func TestCreateProgramWithOverrides(t *testing.T) {
// Create a temporary directory for our test
tempDir, err := os.MkdirTemp("", "rslint-test")

Check failure on line 16 in internal/utils/create_program_test.go

View workflow job for this annotation

GitHub Actions / Test Go (1.24.1)

os.MkdirTemp() could be replaced by t.TempDir() in TestCreateProgramWithOverrides (usetesting)
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)

// Create a basic tsconfig.json
tsconfigContent := `{
"compilerOptions": {
"target": "ES2015",
"strict": false,
"noImplicitAny": false
}
}`
tsconfigPath := filepath.Join(tempDir, "tsconfig.json")
if err := os.WriteFile(tsconfigPath, []byte(tsconfigContent), 0644); err != nil {
t.Fatalf("Failed to write tsconfig.json: %v", err)
}

// Create a simple TypeScript file
tsFileContent := `const x: any = 42;`
tsFilePath := filepath.Join(tempDir, "test.ts")
if err := os.WriteFile(tsFilePath, []byte(tsFileContent), 0644); err != nil {
t.Fatalf("Failed to write test.ts: %v", err)
}

// Create compiler host with proper bundled FS for TypeScript libs
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
host := CreateCompilerHost(tempDir, fs)

// Test 1: Create program without overrides
program1, err := CreateProgram(false, fs, tempDir, tsconfigPath, host)
if err != nil {
t.Fatalf("Failed to create program without overrides: %v", err)
}

// Verify original config settings
options1 := program1.Options()
if options1.Target != core.ScriptTargetES2015 {
t.Errorf("Expected target ES2015, got %v", options1.Target)
}
if options1.Strict != core.TSFalse {
t.Errorf("Expected strict false, got %v", options1.Strict)
}

// Test 2: Create program with overrides
overrides := map[string]interface{}{
"target": float64(core.ScriptTargetES2020), // TypeScript expects numeric enum values
"strict": true,
"noImplicitAny": true,
}

program2, err := CreateProgramWithOverrides(false, fs, tempDir, tsconfigPath, host, overrides)
if err != nil {
t.Fatalf("Failed to create program with overrides: %v", err)
}

// Verify overrides applied
options2 := program2.Options()
if options2.Target != core.ScriptTargetES2020 {
t.Errorf("Expected target ES2020 after override, got %v", options2.Target)
}
if options2.Strict != core.TSTrue {
t.Errorf("Expected strict true after override, got %v", options2.Strict)
}
if options2.NoImplicitAny != core.TSTrue {
t.Errorf("Expected noImplicitAny true after override, got %v", options2.NoImplicitAny)
}
}
Loading