@ttsc/lint 0.12.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +227 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +68 -16
- package/linthost/compile.go +91 -124
- package/linthost/config.go +518 -707
- package/linthost/config_format.go +16 -4
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +44 -0
- package/linthost/engine.go +152 -43
- package/linthost/fix.go +24 -33
- package/linthost/flags_gen.go +31 -0
- package/linthost/format.go +96 -3
- package/linthost/host.go +104 -5
- package/linthost/print_dispatch.go +121 -23
- package/linthost/print_doc.go +19 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +129 -20
- package/linthost/print_nodes_function.go +353 -0
- package/linthost/print_nodes_imports.go +46 -29
- package/linthost/print_nodes_list.go +86 -5
- package/linthost/print_nodes_object.go +56 -11
- package/linthost/rules_escape.go +20 -3
- package/linthost/rules_format_print_width.go +267 -15
- package/linthost/rules_gap.go +55 -3
- package/linthost/rules_logic.go +64 -5
- package/linthost/rules_problems.go +65 -21
- package/linthost/rules_promise.go +3 -0
- package/linthost/rules_suggestions.go +160 -5
- package/linthost/rules_var.go +1 -1
- package/package.json +3 -3
- package/src/index.ts +245 -168
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -351
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
package linthost
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"fmt"
|
|
6
|
-
"os"
|
|
7
|
-
"os/exec"
|
|
8
|
-
"path/filepath"
|
|
9
|
-
"strings"
|
|
10
|
-
"unicode/utf8"
|
|
11
|
-
|
|
12
|
-
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
13
|
-
shimdw "github.com/microsoft/typescript-go/shim/diagnosticwriter"
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
// eslintRuntimeProvider is the optional interface that a RuleResolver
|
|
17
|
-
// implementation may satisfy to enable the external ESLint subprocess path.
|
|
18
|
-
// ConfigStore implements all three methods; other resolvers that do not
|
|
19
|
-
// implement this interface silently bypass the ESLint runtime.
|
|
20
|
-
type eslintRuntimeProvider interface {
|
|
21
|
-
ExternalConfigPath() string
|
|
22
|
-
WantsESLintRuntime() bool
|
|
23
|
-
RequiresESLintRuntime() bool
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// eslintRuntimeOutput is the top-level JSON object written to stdout by
|
|
27
|
-
// the embedded externalESLintRunnerScript.
|
|
28
|
-
type eslintRuntimeOutput struct {
|
|
29
|
-
Missing bool `json:"missing"`
|
|
30
|
-
Fixed int `json:"fixed"`
|
|
31
|
-
Results []eslintRuntimeFile `json:"results"`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// eslintRuntimeFile mirrors one entry from ESLint's LintResult array.
|
|
35
|
-
type eslintRuntimeFile struct {
|
|
36
|
-
FilePath string `json:"filePath"`
|
|
37
|
-
Messages []eslintRuntimeMessage `json:"messages"`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// eslintRuntimeMessage mirrors one entry from ESLint's LintMessage array.
|
|
41
|
-
type eslintRuntimeMessage struct {
|
|
42
|
-
RuleID string `json:"ruleId"`
|
|
43
|
-
Severity int `json:"severity"`
|
|
44
|
-
Message string `json:"message"`
|
|
45
|
-
Line int `json:"line"`
|
|
46
|
-
Column int `json:"column"`
|
|
47
|
-
EndLine int `json:"endLine"`
|
|
48
|
-
EndColumn int `json:"endColumn"`
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// runExternalESLintDiagnostics delegates to the project's installed ESLint
|
|
52
|
-
// binary (via the embedded JS runner) and converts the JSON output into
|
|
53
|
-
// LintDiagnostic values anchored to their source positions. Returns
|
|
54
|
-
// (nil, false, nil) when the resolver does not want the ESLint runtime,
|
|
55
|
-
// (nil, true, nil) when no source files qualify, and (diags, true, nil)
|
|
56
|
-
// on success. The bool return is "ran ESLint" — callers use it to skip
|
|
57
|
-
// native-rule diagnostics when ESLint was the sole configured source.
|
|
58
|
-
func runExternalESLintDiagnostics(
|
|
59
|
-
resolver RuleResolver,
|
|
60
|
-
cwd string,
|
|
61
|
-
files []*shimast.SourceFile,
|
|
62
|
-
) ([]*shimdw.LintDiagnostic, bool, error) {
|
|
63
|
-
provider, ok := resolver.(eslintRuntimeProvider)
|
|
64
|
-
if !ok || !provider.WantsESLintRuntime() {
|
|
65
|
-
return nil, false, nil
|
|
66
|
-
}
|
|
67
|
-
configPath := provider.ExternalConfigPath()
|
|
68
|
-
if configPath == "" {
|
|
69
|
-
return nil, false, nil
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
fileNames := make([]string, 0, len(files))
|
|
73
|
-
byPath := make(map[string]*shimast.SourceFile, len(files))
|
|
74
|
-
for _, file := range files {
|
|
75
|
-
if file == nil || file.IsDeclarationFile {
|
|
76
|
-
continue
|
|
77
|
-
}
|
|
78
|
-
name := file.FileName()
|
|
79
|
-
if !filepath.IsAbs(name) {
|
|
80
|
-
name = filepath.Join(cwd, name)
|
|
81
|
-
}
|
|
82
|
-
if abs, err := filepath.Abs(name); err == nil {
|
|
83
|
-
name = abs
|
|
84
|
-
}
|
|
85
|
-
fileNames = append(fileNames, name)
|
|
86
|
-
byPath[filepath.ToSlash(name)] = file
|
|
87
|
-
}
|
|
88
|
-
if len(fileNames) == 0 {
|
|
89
|
-
return nil, true, nil
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
payload, err := json.Marshal(fileNames)
|
|
93
|
-
if err != nil {
|
|
94
|
-
return nil, false, fmt.Errorf("@ttsc/lint: encode ESLint file list: %w", err)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
output, err := runExternalESLint(cwd, configPath, string(payload))
|
|
98
|
-
if err != nil {
|
|
99
|
-
return nil, false, err
|
|
100
|
-
}
|
|
101
|
-
if output.Missing {
|
|
102
|
-
if provider.RequiresESLintRuntime() {
|
|
103
|
-
return nil, false, fmt.Errorf("@ttsc/lint: ESLint runtime is required by %s; install eslint in the project or replace runtime-only config features", configPath)
|
|
104
|
-
}
|
|
105
|
-
return nil, false, nil
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
diagnostics := make([]*shimdw.LintDiagnostic, 0)
|
|
109
|
-
for _, result := range output.Results {
|
|
110
|
-
file := byPath[filepath.ToSlash(result.FilePath)]
|
|
111
|
-
if file == nil {
|
|
112
|
-
continue
|
|
113
|
-
}
|
|
114
|
-
for _, msg := range result.Messages {
|
|
115
|
-
if msg.Severity == 0 {
|
|
116
|
-
// ESLint severity 0 means "off" — skip silently.
|
|
117
|
-
continue
|
|
118
|
-
}
|
|
119
|
-
ruleID := strings.TrimSpace(msg.RuleID)
|
|
120
|
-
if ruleID == "" {
|
|
121
|
-
// ESLint can emit messages without a ruleId for parse errors.
|
|
122
|
-
ruleID = "eslint"
|
|
123
|
-
}
|
|
124
|
-
category := shimdw.LintCategoryWarning
|
|
125
|
-
if msg.Severity >= 2 {
|
|
126
|
-
// severity 2 = error; values above 2 are treated as error too.
|
|
127
|
-
category = shimdw.LintCategoryError
|
|
128
|
-
}
|
|
129
|
-
pos := positionOfESLintLocation(file.Text(), msg.Line, msg.Column)
|
|
130
|
-
end := positionOfESLintLocation(file.Text(), msg.EndLine, msg.EndColumn)
|
|
131
|
-
if end <= pos {
|
|
132
|
-
end = pos + 1
|
|
133
|
-
}
|
|
134
|
-
diagnostics = append(diagnostics, shimdw.NewLintDiagnostic(
|
|
135
|
-
file,
|
|
136
|
-
pos,
|
|
137
|
-
end,
|
|
138
|
-
ruleCode(ruleID),
|
|
139
|
-
category,
|
|
140
|
-
fmt.Sprintf("[%s] %s", ruleID, msg.Message),
|
|
141
|
-
))
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return diagnostics, true, nil
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// runExternalESLint invokes ESLint in check (read-only) mode.
|
|
148
|
-
func runExternalESLint(cwd, configPath, fileListJSON string) (*eslintRuntimeOutput, error) {
|
|
149
|
-
return runExternalESLintWithMode(cwd, configPath, fileListJSON, false)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// runExternalESLintFixes invokes ESLint in fix mode and returns the number of
|
|
153
|
-
// files that were actually modified. The fix runner calls ESLint.outputFixes
|
|
154
|
-
// inside the JS subprocess, which writes changes directly to disk.
|
|
155
|
-
func runExternalESLintFixes(
|
|
156
|
-
resolver RuleResolver,
|
|
157
|
-
cwd string,
|
|
158
|
-
files []*shimast.SourceFile,
|
|
159
|
-
) (int, error) {
|
|
160
|
-
provider, ok := resolver.(eslintRuntimeProvider)
|
|
161
|
-
if !ok || !provider.WantsESLintRuntime() {
|
|
162
|
-
return 0, nil
|
|
163
|
-
}
|
|
164
|
-
configPath := provider.ExternalConfigPath()
|
|
165
|
-
if configPath == "" {
|
|
166
|
-
return 0, nil
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
fileNames := make([]string, 0, len(files))
|
|
170
|
-
for _, file := range files {
|
|
171
|
-
if file == nil || file.IsDeclarationFile {
|
|
172
|
-
continue
|
|
173
|
-
}
|
|
174
|
-
name := file.FileName()
|
|
175
|
-
if !filepath.IsAbs(name) {
|
|
176
|
-
name = filepath.Join(cwd, name)
|
|
177
|
-
}
|
|
178
|
-
if abs, err := filepath.Abs(name); err == nil {
|
|
179
|
-
name = abs
|
|
180
|
-
}
|
|
181
|
-
fileNames = append(fileNames, name)
|
|
182
|
-
}
|
|
183
|
-
if len(fileNames) == 0 {
|
|
184
|
-
return 0, nil
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
payload, err := json.Marshal(fileNames)
|
|
188
|
-
if err != nil {
|
|
189
|
-
return 0, fmt.Errorf("@ttsc/lint: encode ESLint file list: %w", err)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
output, err := runExternalESLintWithMode(cwd, configPath, string(payload), true)
|
|
193
|
-
if err != nil {
|
|
194
|
-
return 0, err
|
|
195
|
-
}
|
|
196
|
-
if output.Missing {
|
|
197
|
-
if provider.RequiresESLintRuntime() {
|
|
198
|
-
return 0, fmt.Errorf("@ttsc/lint: ESLint runtime is required by %s; install eslint in the project or replace runtime-only config features", configPath)
|
|
199
|
-
}
|
|
200
|
-
return 0, nil
|
|
201
|
-
}
|
|
202
|
-
return output.Fixed, nil
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// runExternalESLintWithMode spawns a Node.js subprocess that runs the embedded
|
|
206
|
-
// externalESLintRunnerScript. When fix is true the runner calls
|
|
207
|
-
// ESLint.outputFixes and returns the count of modified files; when false it
|
|
208
|
-
// returns diagnostics only and leaves files untouched.
|
|
209
|
-
func runExternalESLintWithMode(cwd, configPath, fileListJSON string, fix bool) (*eslintRuntimeOutput, error) {
|
|
210
|
-
node := os.Getenv("TTSC_NODE_BINARY")
|
|
211
|
-
if node == "" {
|
|
212
|
-
node = "node"
|
|
213
|
-
}
|
|
214
|
-
mode := "check"
|
|
215
|
-
if fix {
|
|
216
|
-
mode = "fix"
|
|
217
|
-
}
|
|
218
|
-
cmd := exec.Command(node, "-e", externalESLintRunnerScript, cwd, configPath, fileListJSON, mode)
|
|
219
|
-
cmd.Env = nodeConfigLoaderEnv(configPath)
|
|
220
|
-
cmd.Dir = cwd
|
|
221
|
-
raw, err := cmd.Output()
|
|
222
|
-
if err != nil {
|
|
223
|
-
stderr := ""
|
|
224
|
-
if exit, ok := err.(*exec.ExitError); ok {
|
|
225
|
-
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
226
|
-
}
|
|
227
|
-
if stderr != "" {
|
|
228
|
-
return nil, fmt.Errorf("@ttsc/lint: run ESLint config %s: %s", configPath, stderr)
|
|
229
|
-
}
|
|
230
|
-
return nil, fmt.Errorf("@ttsc/lint: run ESLint config %s: %w", configPath, err)
|
|
231
|
-
}
|
|
232
|
-
var output eslintRuntimeOutput
|
|
233
|
-
if err := json.Unmarshal(raw, &output); err != nil {
|
|
234
|
-
return nil, fmt.Errorf("@ttsc/lint: parse ESLint output for %s: %w", configPath, err)
|
|
235
|
-
}
|
|
236
|
-
return &output, nil
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// positionOfESLintLocation converts a 1-based (line, column) position from
|
|
240
|
-
// ESLint's output — where column is a UTF-16 code-unit offset — into a
|
|
241
|
-
// zero-based byte offset into text. This matches how tsgo positions source
|
|
242
|
-
// spans: lines are 1-based, columns are UTF-16 units (so a supplementary
|
|
243
|
-
// codepoint counts as 2). Returns len(text) when the position is past EOF.
|
|
244
|
-
func positionOfESLintLocation(text string, line, column int) int {
|
|
245
|
-
if line <= 0 {
|
|
246
|
-
line = 1
|
|
247
|
-
}
|
|
248
|
-
if column <= 0 {
|
|
249
|
-
column = 1
|
|
250
|
-
}
|
|
251
|
-
lineStart := 0
|
|
252
|
-
currentLine := 1
|
|
253
|
-
for i := 0; i < len(text) && currentLine < line; {
|
|
254
|
-
r, size := utf8.DecodeRuneInString(text[i:])
|
|
255
|
-
if r == '\n' {
|
|
256
|
-
currentLine++
|
|
257
|
-
lineStart = i + size
|
|
258
|
-
}
|
|
259
|
-
i += size
|
|
260
|
-
}
|
|
261
|
-
targetUTF16 := column - 1
|
|
262
|
-
seenUTF16 := 0
|
|
263
|
-
for i := lineStart; i < len(text); {
|
|
264
|
-
if seenUTF16 >= targetUTF16 {
|
|
265
|
-
return i
|
|
266
|
-
}
|
|
267
|
-
r, size := utf8.DecodeRuneInString(text[i:])
|
|
268
|
-
if r == '\n' || r == '\r' {
|
|
269
|
-
return i
|
|
270
|
-
}
|
|
271
|
-
if r > 0xFFFF {
|
|
272
|
-
seenUTF16 += 2
|
|
273
|
-
} else {
|
|
274
|
-
seenUTF16++
|
|
275
|
-
}
|
|
276
|
-
i += size
|
|
277
|
-
}
|
|
278
|
-
return len(text)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const externalESLintRunnerScript = `
|
|
282
|
-
const { createRequire } = require("node:module");
|
|
283
|
-
const path = require("node:path");
|
|
284
|
-
|
|
285
|
-
(async () => {
|
|
286
|
-
const cwd = process.argv[1];
|
|
287
|
-
const configPath = process.argv[2];
|
|
288
|
-
const files = JSON.parse(process.argv[3]);
|
|
289
|
-
const shouldFix = process.argv[4] === "fix";
|
|
290
|
-
const requireFromProject = createRequire(path.join(cwd, "package.json"));
|
|
291
|
-
let eslintPath;
|
|
292
|
-
try {
|
|
293
|
-
eslintPath = requireFromProject.resolve("eslint");
|
|
294
|
-
} catch (error) {
|
|
295
|
-
if (error && error.code === "MODULE_NOT_FOUND") {
|
|
296
|
-
process.stdout.write(JSON.stringify({ missing: true, results: [] }));
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
const eslintModule = requireFromProject(eslintPath);
|
|
302
|
-
|
|
303
|
-
const ESLintCtor = typeof eslintModule.loadESLint === "function"
|
|
304
|
-
? await eslintModule.loadESLint({ useFlatConfig: true })
|
|
305
|
-
: eslintModule.ESLint ?? eslintModule.default?.ESLint ?? eslintModule.default;
|
|
306
|
-
|
|
307
|
-
if (typeof ESLintCtor !== "function") {
|
|
308
|
-
throw new Error("installed eslint package does not export ESLint or loadESLint");
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const eslint = new ESLintCtor({
|
|
312
|
-
cwd,
|
|
313
|
-
overrideConfigFile: configPath,
|
|
314
|
-
fix: shouldFix,
|
|
315
|
-
ignore: true,
|
|
316
|
-
warnIgnored: false,
|
|
317
|
-
});
|
|
318
|
-
const results = await eslint.lintFiles(files);
|
|
319
|
-
if (shouldFix) {
|
|
320
|
-
const outputFixes =
|
|
321
|
-
ESLintCtor.outputFixes ||
|
|
322
|
-
eslintModule.ESLint?.outputFixes ||
|
|
323
|
-
eslintModule.default?.ESLint?.outputFixes;
|
|
324
|
-
if (typeof outputFixes !== "function") {
|
|
325
|
-
throw new Error("installed eslint package does not expose ESLint.outputFixes");
|
|
326
|
-
}
|
|
327
|
-
await outputFixes.call(ESLintCtor, results);
|
|
328
|
-
}
|
|
329
|
-
process.stdout.write(JSON.stringify({
|
|
330
|
-
missing: false,
|
|
331
|
-
fixed: shouldFix
|
|
332
|
-
? results.filter((result) => typeof result.output === "string").length
|
|
333
|
-
: 0,
|
|
334
|
-
results: results.map((result) => ({
|
|
335
|
-
filePath: result.filePath,
|
|
336
|
-
messages: result.messages.map((message) => ({
|
|
337
|
-
ruleId: message.ruleId || "eslint",
|
|
338
|
-
severity: message.severity,
|
|
339
|
-
message: message.message,
|
|
340
|
-
line: message.line || 1,
|
|
341
|
-
column: message.column || 1,
|
|
342
|
-
endLine: message.endLine || message.line || 1,
|
|
343
|
-
endColumn: message.endColumn || message.column || 1,
|
|
344
|
-
})),
|
|
345
|
-
})),
|
|
346
|
-
}));
|
|
347
|
-
})().catch((error) => {
|
|
348
|
-
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
349
|
-
process.exit(1);
|
|
350
|
-
});
|
|
351
|
-
`
|