@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.
Files changed (37) hide show
  1. package/lib/index.js +227 -135
  2. package/lib/index.js.map +1 -1
  3. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  4. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  5. package/linthost/ast_helpers.go +68 -16
  6. package/linthost/compile.go +91 -124
  7. package/linthost/config.go +518 -707
  8. package/linthost/config_format.go +16 -4
  9. package/linthost/contrib_adapter.go +7 -0
  10. package/linthost/directives.go +44 -0
  11. package/linthost/engine.go +152 -43
  12. package/linthost/fix.go +24 -33
  13. package/linthost/flags_gen.go +31 -0
  14. package/linthost/format.go +96 -3
  15. package/linthost/host.go +104 -5
  16. package/linthost/print_dispatch.go +121 -23
  17. package/linthost/print_doc.go +19 -0
  18. package/linthost/print_engine.go +168 -4
  19. package/linthost/print_nodes_array.go +17 -7
  20. package/linthost/print_nodes_call.go +129 -20
  21. package/linthost/print_nodes_function.go +353 -0
  22. package/linthost/print_nodes_imports.go +46 -29
  23. package/linthost/print_nodes_list.go +86 -5
  24. package/linthost/print_nodes_object.go +56 -11
  25. package/linthost/rules_escape.go +20 -3
  26. package/linthost/rules_format_print_width.go +267 -15
  27. package/linthost/rules_gap.go +55 -3
  28. package/linthost/rules_logic.go +64 -5
  29. package/linthost/rules_problems.go +65 -21
  30. package/linthost/rules_promise.go +3 -0
  31. package/linthost/rules_suggestions.go +160 -5
  32. package/linthost/rules_var.go +1 -1
  33. package/package.json +3 -3
  34. package/src/index.ts +245 -168
  35. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  36. package/src/structures/TtscLintRuleOptions.ts +15 -0
  37. 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
- `