Skip to content
Merged
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
71 changes: 34 additions & 37 deletions cmd/tsgolint/headless.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ import (
"github.com/typescript-eslint/tsgolint/internal/utils"
)

type headlessConfigForFile struct {
FilePath string `json:"file_path"`
Rules []string `json:"rules"`
}
type headlessConfig struct {
Files []headlessConfigForFile `json:"files"`
}

type headlessRange struct {
Pos int `json:"pos"`
End int `json:"end"`
Expand Down Expand Up @@ -154,51 +146,56 @@ func runHeadless(args []string) int {
return 1
}

var config headlessConfig
payload, err := deserializePayload(configRaw)

if err := json.Unmarshal(configRaw, &config); err != nil {
if err != nil {
writeErrorMessage(fmt.Sprintf("error parsing config: %v", err))
return 1
}
if len(config.Files) == 0 {
writeErrorMessage("no files specified in config")
return 1
}

fileConfigs := make(map[string]headlessConfigForFile, len(config.Files))
workload := linter.Workload{
Programs: make(map[string][]string),
UnmatchedFiles: []string{},
}

totalFileCount := 0
for _, config := range payload.Configs {
totalFileCount += len(config.FilePaths)
}
if logLevel == utils.LogLevelDebug {
log.Printf("Starting to assign files to programs. Total files: %d", len(config.Files))
log.Printf("Starting to assign files to programs. Total files: %d", totalFileCount)
}

tsConfigResolver := utils.NewTsConfigResolver(fs, cwd)

for idx, fileConfig := range config.Files {
if logLevel == utils.LogLevelDebug {
log.Printf("[%d/%d] Processing file: %s", idx+1, len(config.Files), fileConfig.FilePath)
}
fileConfigs := make(map[string][]headlessRule, totalFileCount)

idx := 0
for _, config := range payload.Configs {
for _, filePath := range config.FilePaths {
if logLevel == utils.LogLevelDebug {
log.Printf("[%d/%d] Processing file: %s", idx+1, totalFileCount, filePath)
}

normalizedFilePath := tspath.NormalizeSlashes(fileConfig.FilePath)
normalizedFilePath := tspath.NormalizeSlashes(filePath)

tsconfig, found := tsConfigResolver.FindTsconfigForFile(normalizedFilePath, false)
if logLevel == utils.LogLevelDebug {
tsconfigStr := "<none>"
if found {
tsconfigStr = tsconfig
tsconfig, found := tsConfigResolver.FindTsconfigForFile(normalizedFilePath, false)
if logLevel == utils.LogLevelDebug {
tsconfigStr := "<none>"
if found {
tsconfigStr = tsconfig
}
log.Printf("Got tsconfig for file %s: %s", normalizedFilePath, tsconfigStr)
}
log.Printf("Got tsconfig for file %s: %s", normalizedFilePath, tsconfigStr)
}

if !found {
workload.UnmatchedFiles = append(workload.UnmatchedFiles, normalizedFilePath)
} else {
workload.Programs[tsconfig] = append(workload.Programs[tsconfig], normalizedFilePath)
if !found {
workload.UnmatchedFiles = append(workload.UnmatchedFiles, normalizedFilePath)
} else {
workload.Programs[tsconfig] = append(workload.Programs[tsconfig], normalizedFilePath)
}
fileConfigs[normalizedFilePath] = config.Rules
idx++
}
fileConfigs[normalizedFilePath] = fileConfig
}

if logLevel == utils.LogLevelDebug {
Expand Down Expand Up @@ -279,12 +276,12 @@ func runHeadless(args []string) int {
runtime.GOMAXPROCS(0),
func(sourceFile *ast.SourceFile) []linter.ConfiguredRule {
cfg := fileConfigs[sourceFile.FileName()]
rules := make([]linter.ConfiguredRule, len(cfg.Rules))
rules := make([]linter.ConfiguredRule, len(cfg))

for i, ruleName := range cfg.Rules {
r, ok := allRulesByName[ruleName]
for i, headlessRule := range cfg {
r, ok := allRulesByName[headlessRule.Name]
if !ok {
panic(fmt.Sprintf("unknown rule: %v", ruleName))
panic(fmt.Sprintf("unknown rule: %v", headlessRule.Name))
}
rules[i] = linter.ConfiguredRule{
Name: r.Name,
Expand Down
90 changes: 90 additions & 0 deletions cmd/tsgolint/payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"errors"
"fmt"

"github.com/go-json-experiment/json"
)

// V1 Headless payload format
type headlessConfigForFileV1 struct {
FilePath string `json:"file_path"`
Rules []string `json:"rules"`
}
type headlessPayloadV1 struct {
Files []headlessConfigForFileV1 `json:"files"`
}

// V2 (current) Headless payload format
type headlessPayload struct {
Version int `json:"version"` // version must be 2
Configs []headlessConfig `json:"configs"`
}

type headlessConfig struct {
FilePaths []string `json:"file_paths"`
Rules []headlessRule `json:"rules"`
}

type headlessRule struct {
Name string `json:"name"`
}

func deserializePayload(data []byte) (*headlessPayload, error) {
version, err := getPayloadVersion(data)
if err != nil {
return nil, err
}

if version == 2 {
var payload headlessPayload
if err := json.Unmarshal(data, &payload); err != nil {
return nil, errors.New("failed to deserialize V2 payload: " + err.Error())
}
return &payload, nil
}

// Version 0 or unset indicates V1 payload
if version != 0 {
return nil, fmt.Errorf("unsupported version `%d`: expected `unset` or `2`", version)
}

var payloadV1 headlessPayloadV1
if err := json.Unmarshal(data, &payloadV1); err != nil {
return nil, errors.New("failed to deserialize V1 payload: " + err.Error())
}

// Validate V1 payload
if len(payloadV1.Files) == 0 {
return nil, errors.New("V1 payload has no files")
}

// Convert V1 to V2
payloadV2 := &headlessPayload{
Version: 2,
Configs: make([]headlessConfig, len(payloadV1.Files)),
}
for i, fileV1 := range payloadV1.Files {
config := headlessConfig{
FilePaths: []string{fileV1.FilePath}, // V1 has single file, V2 supports multiple
Rules: make([]headlessRule, len(fileV1.Rules)),
}
for j, rule := range fileV1.Rules {
config.Rules[j] = headlessRule{Name: rule} // V1 rules are just strings
}
payloadV2.Configs[i] = config
}

return payloadV2, nil
}

func getPayloadVersion(data []byte) (int, error) {
var versionCheck struct {
Version int `json:"version"`
}
if err := json.Unmarshal(data, &versionCheck); err != nil {
return 0, err
}
return versionCheck.Version, nil
}
69 changes: 60 additions & 9 deletions e2e/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,26 @@
}

function generateConfig(files: string[], rules: readonly (typeof ALL_RULES)[number][] = ALL_RULES): string {
// Headless payload format:
// ```json
// {
// "configs": [
// {
// "file_paths": ["/abs/path/a.ts", ...],
// "rules": [ { "name": "rule-a" }, { "name": "rule-b" } ]
// }
// ]
// }
// ```
const config = {
files: files.map((filePath) => ({
file_path: filePath,
rules,
})),
};
version: 2,
configs: [
{
file_paths: files,
rules: rules.map((r) => ({ name: r })),
},
],
} as const;
return JSON.stringify(config);
}

Expand Down Expand Up @@ -194,13 +208,13 @@
const rustStylePath = testFile.replace(/\//g, '\\');

const config = {
files: [
configs: [
{
file_path: rustStylePath,
rules: ['no-floating-promises'],
file_paths: [rustStylePath],
rules: [{ name: 'no-floating-promises' }],
},
],
};
} as const;

const env = { ...process.env, GOMAXPROCS: '1' };

Expand All @@ -209,7 +223,7 @@
input: JSON.stringify(config),
env,
});
}).not.toThrow();

Check failure on line 226 in e2e/snapshot.test.ts

View workflow job for this annotation

GitHub Actions / test-windows

snapshot.test.ts > TSGoLint E2E Snapshot Tests > should not panic with mixed forward/backslash paths from Rust (issue #143)

AssertionError: expected [Function] to not throw an error but 'Error: Command failed: D:\a\tsgolint\…' was thrown - Expected: undefined + Received: "Error: Command failed: D:\\a\\tsgolint\\tsgolint\\tsgolint.exe headless" ❯ snapshot.test.ts:226:14
},
);

Expand Down Expand Up @@ -252,4 +266,41 @@

expect(diagnostics).toMatchSnapshot();
});

it('should work with the old version of the headless payload', async () => {
function generateV1HeadlessPayload(
files: string[],
rules: readonly (typeof ALL_RULES)[number][] = ALL_RULES,
): string {
const config = {
files: files.map((filePath) => ({
file_path: filePath,
rules,
})),
};
return JSON.stringify(config);
}

function getDiagnostics(config: string): Diagnostic[] {
let output: Buffer;
output = execFileSync(TSGOLINT_BIN, ['headless'], {
input: config,
env: { ...process.env, GOMAXPROCS: '1' },
});

const diagnostics = parseHeadlessOutput(output);
return sortDiagnostics(diagnostics);
}

const testFiles = await getTestFiles('basic');
expect(testFiles.length).toBeGreaterThan(0);

const v1Config = generateV1HeadlessPayload(testFiles);
const v1Diagnostics = getDiagnostics(v1Config);

const v2Config = generateConfig(testFiles);
const v2Diagnostics = getDiagnostics(v2Config);

expect(v1Diagnostics).toStrictEqual(v2Diagnostics);
});
});
1 change: 0 additions & 1 deletion internal/linter/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ func RunLinter(logLevel utils.LogLevel, currentDirectory string, workload Worklo
return err
}


files := make([]*ast.SourceFile, 0, len(workload.UnmatchedFiles))
for _, f := range workload.UnmatchedFiles {
sf := program.GetSourceFile(f)
Expand Down
Loading