Skip to content

Commit 707c5e7

Browse files
committed
refactor: update headless payload to be config-based instead of file-based (#182)
- based on #125 (cc @camc314) I took @camc314's branch and updated based on the latest code. I'm not familiar with Go, so it's very possible I've made some mistakes here, please review carefully 🙏 The goal of this PR is to ultimately enable doing custom configurations for rules without needing to specify redudant rule information. The new config format emphasizes configs over files, where each config defines a set of files it should apply to, and the rules that should be run.
1 parent 8933042 commit 707c5e7

File tree

4 files changed

+184
-47
lines changed

4 files changed

+184
-47
lines changed

cmd/tsgolint/headless.go

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@ import (
2525
"github.com/typescript-eslint/tsgolint/internal/utils"
2626
)
2727

28-
type headlessConfigForFile struct {
29-
FilePath string `json:"file_path"`
30-
Rules []string `json:"rules"`
31-
}
32-
type headlessConfig struct {
33-
Files []headlessConfigForFile `json:"files"`
34-
}
35-
3628
type headlessRange struct {
3729
Pos int `json:"pos"`
3830
End int `json:"end"`
@@ -154,51 +146,56 @@ func runHeadless(args []string) int {
154146
return 1
155147
}
156148

157-
var config headlessConfig
149+
payload, err := deserializePayload(configRaw)
158150

159-
if err := json.Unmarshal(configRaw, &config); err != nil {
151+
if err != nil {
160152
writeErrorMessage(fmt.Sprintf("error parsing config: %v", err))
161153
return 1
162154
}
163-
if len(config.Files) == 0 {
164-
writeErrorMessage("no files specified in config")
165-
return 1
166-
}
167155

168-
fileConfigs := make(map[string]headlessConfigForFile, len(config.Files))
169156
workload := linter.Workload{
170157
Programs: make(map[string][]string),
171158
UnmatchedFiles: []string{},
172159
}
173160

161+
totalFileCount := 0
162+
for _, config := range payload.Configs {
163+
totalFileCount += len(config.FilePaths)
164+
}
174165
if logLevel == utils.LogLevelDebug {
175-
log.Printf("Starting to assign files to programs. Total files: %d", len(config.Files))
166+
log.Printf("Starting to assign files to programs. Total files: %d", totalFileCount)
176167
}
177168

178169
tsConfigResolver := utils.NewTsConfigResolver(fs, cwd)
179170

180-
for idx, fileConfig := range config.Files {
181-
if logLevel == utils.LogLevelDebug {
182-
log.Printf("[%d/%d] Processing file: %s", idx+1, len(config.Files), fileConfig.FilePath)
183-
}
171+
fileConfigs := make(map[string][]headlessRule, totalFileCount)
172+
173+
idx := 0
174+
for _, config := range payload.Configs {
175+
for _, filePath := range config.FilePaths {
176+
if logLevel == utils.LogLevelDebug {
177+
log.Printf("[%d/%d] Processing file: %s", idx+1, totalFileCount, filePath)
178+
}
184179

185-
normalizedFilePath := tspath.NormalizeSlashes(fileConfig.FilePath)
180+
normalizedFilePath := tspath.NormalizeSlashes(filePath)
186181

187-
tsconfig, found := tsConfigResolver.FindTsconfigForFile(normalizedFilePath, false)
188-
if logLevel == utils.LogLevelDebug {
189-
tsconfigStr := "<none>"
190-
if found {
191-
tsconfigStr = tsconfig
182+
tsconfig, found := tsConfigResolver.FindTsconfigForFile(normalizedFilePath, false)
183+
if logLevel == utils.LogLevelDebug {
184+
tsconfigStr := "<none>"
185+
if found {
186+
tsconfigStr = tsconfig
187+
}
188+
log.Printf("Got tsconfig for file %s: %s", normalizedFilePath, tsconfigStr)
192189
}
193-
log.Printf("Got tsconfig for file %s: %s", normalizedFilePath, tsconfigStr)
194-
}
195190

196-
if !found {
197-
workload.UnmatchedFiles = append(workload.UnmatchedFiles, normalizedFilePath)
198-
} else {
199-
workload.Programs[tsconfig] = append(workload.Programs[tsconfig], normalizedFilePath)
191+
if !found {
192+
workload.UnmatchedFiles = append(workload.UnmatchedFiles, normalizedFilePath)
193+
} else {
194+
workload.Programs[tsconfig] = append(workload.Programs[tsconfig], normalizedFilePath)
195+
}
196+
fileConfigs[normalizedFilePath] = config.Rules
197+
idx++
200198
}
201-
fileConfigs[normalizedFilePath] = fileConfig
202199
}
203200

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

284-
for i, ruleName := range cfg.Rules {
285-
r, ok := allRulesByName[ruleName]
281+
for i, headlessRule := range cfg {
282+
r, ok := allRulesByName[headlessRule.Name]
286283
if !ok {
287-
panic(fmt.Sprintf("unknown rule: %v", ruleName))
284+
panic(fmt.Sprintf("unknown rule: %v", headlessRule.Name))
288285
}
289286
rules[i] = linter.ConfiguredRule{
290287
Name: r.Name,

cmd/tsgolint/payload.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/go-json-experiment/json"
8+
)
9+
10+
// V1 Headless payload format
11+
type headlessConfigForFileV1 struct {
12+
FilePath string `json:"file_path"`
13+
Rules []string `json:"rules"`
14+
}
15+
type headlessPayloadV1 struct {
16+
Files []headlessConfigForFileV1 `json:"files"`
17+
}
18+
19+
// V2 (current) Headless payload format
20+
type headlessPayload struct {
21+
Version int `json:"version"` // version must be 2
22+
Configs []headlessConfig `json:"configs"`
23+
}
24+
25+
type headlessConfig struct {
26+
FilePaths []string `json:"file_paths"`
27+
Rules []headlessRule `json:"rules"`
28+
}
29+
30+
type headlessRule struct {
31+
Name string `json:"name"`
32+
}
33+
34+
func deserializePayload(data []byte) (*headlessPayload, error) {
35+
version, err := getPayloadVersion(data)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
if version == 2 {
41+
var payload headlessPayload
42+
if err := json.Unmarshal(data, &payload); err != nil {
43+
return nil, errors.New("failed to deserialize V2 payload: " + err.Error())
44+
}
45+
return &payload, nil
46+
}
47+
48+
// Version 0 or unset indicates V1 payload
49+
if version != 0 {
50+
return nil, fmt.Errorf("unsupported version `%d`: expected `unset` or `2`", version)
51+
}
52+
53+
var payloadV1 headlessPayloadV1
54+
if err := json.Unmarshal(data, &payloadV1); err != nil {
55+
return nil, errors.New("failed to deserialize V1 payload: " + err.Error())
56+
}
57+
58+
// Validate V1 payload
59+
if len(payloadV1.Files) == 0 {
60+
return nil, errors.New("V1 payload has no files")
61+
}
62+
63+
// Convert V1 to V2
64+
payloadV2 := &headlessPayload{
65+
Version: 2,
66+
Configs: make([]headlessConfig, len(payloadV1.Files)),
67+
}
68+
for i, fileV1 := range payloadV1.Files {
69+
config := headlessConfig{
70+
FilePaths: []string{fileV1.FilePath}, // V1 has single file, V2 supports multiple
71+
Rules: make([]headlessRule, len(fileV1.Rules)),
72+
}
73+
for j, rule := range fileV1.Rules {
74+
config.Rules[j] = headlessRule{Name: rule} // V1 rules are just strings
75+
}
76+
payloadV2.Configs[i] = config
77+
}
78+
79+
return payloadV2, nil
80+
}
81+
82+
func getPayloadVersion(data []byte) (int, error) {
83+
var versionCheck struct {
84+
Version int `json:"version"`
85+
}
86+
if err := json.Unmarshal(data, &versionCheck); err != nil {
87+
return 0, err
88+
}
89+
return versionCheck.Version, nil
90+
}

e2e/snapshot.test.ts

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,26 @@ async function getTestFiles(testPath: string): Promise<string[]> {
146146
}
147147

148148
function generateConfig(files: string[], rules: readonly (typeof ALL_RULES)[number][] = ALL_RULES): string {
149+
// Headless payload format:
150+
// ```json
151+
// {
152+
// "configs": [
153+
// {
154+
// "file_paths": ["/abs/path/a.ts", ...],
155+
// "rules": [ { "name": "rule-a" }, { "name": "rule-b" } ]
156+
// }
157+
// ]
158+
// }
159+
// ```
149160
const config = {
150-
files: files.map((filePath) => ({
151-
file_path: filePath,
152-
rules,
153-
})),
154-
};
161+
version: 2,
162+
configs: [
163+
{
164+
file_paths: files,
165+
rules: rules.map((r) => ({ name: r })),
166+
},
167+
],
168+
} as const;
155169
return JSON.stringify(config);
156170
}
157171

@@ -194,13 +208,13 @@ describe('TSGoLint E2E Snapshot Tests', () => {
194208
const rustStylePath = testFile.replace(/\//g, '\\');
195209

196210
const config = {
197-
files: [
211+
configs: [
198212
{
199-
file_path: rustStylePath,
200-
rules: ['no-floating-promises'],
213+
file_paths: [rustStylePath],
214+
rules: [{ name: 'no-floating-promises' }],
201215
},
202216
],
203-
};
217+
} as const;
204218

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

@@ -252,4 +266,41 @@ describe('TSGoLint E2E Snapshot Tests', () => {
252266

253267
expect(diagnostics).toMatchSnapshot();
254268
});
269+
270+
it('should work with the old version of the headless payload', async () => {
271+
function generateV1HeadlessPayload(
272+
files: string[],
273+
rules: readonly (typeof ALL_RULES)[number][] = ALL_RULES,
274+
): string {
275+
const config = {
276+
files: files.map((filePath) => ({
277+
file_path: filePath,
278+
rules,
279+
})),
280+
};
281+
return JSON.stringify(config);
282+
}
283+
284+
function getDiagnostics(config: string): Diagnostic[] {
285+
let output: Buffer;
286+
output = execFileSync(TSGOLINT_BIN, ['headless'], {
287+
input: config,
288+
env: { ...process.env, GOMAXPROCS: '1' },
289+
});
290+
291+
const diagnostics = parseHeadlessOutput(output);
292+
return sortDiagnostics(diagnostics);
293+
}
294+
295+
const testFiles = await getTestFiles('basic');
296+
expect(testFiles.length).toBeGreaterThan(0);
297+
298+
const v1Config = generateV1HeadlessPayload(testFiles);
299+
const v1Diagnostics = getDiagnostics(v1Config);
300+
301+
const v2Config = generateConfig(testFiles);
302+
const v2Diagnostics = getDiagnostics(v2Config);
303+
304+
expect(v1Diagnostics).toStrictEqual(v2Diagnostics);
305+
});
255306
});

internal/linter/linter.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ func RunLinter(logLevel utils.LogLevel, currentDirectory string, workload Worklo
9494
return err
9595
}
9696

97-
9897
files := make([]*ast.SourceFile, 0, len(workload.UnmatchedFiles))
9998
for _, f := range workload.UnmatchedFiles {
10099
sf := program.GetSourceFile(f)

0 commit comments

Comments
 (0)