lintcn 0.2.0 → 0.4.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/dist/codegen.js CHANGED
@@ -1,14 +1,11 @@
1
1
  // Generate Go workspace files for building a custom tsgolint binary.
2
- // Creates:
3
- // .lintcn/go.work workspace for gopls (editor support)
4
- // .lintcn/go.mod — module declaration
5
- // build/go.work — build workspace in cache dir
6
- // build/wrapper/go.mod — wrapper module
7
- // build/wrapper/main.go — entry point with all rules
2
+ // With the fork (remorses/tsgolint) exposing pkg/runner.Run(), codegen is
3
+ // minimal: a 10-line main.go template + go.work with shim replaces.
4
+ // No regex surgery, no file copying, no fragile string manipulation.
8
5
  import fs from 'node:fs';
9
6
  import path from 'node:path';
10
- // All replace directives needed from tsgolint's go.mod.
11
- // These redirect shim module paths to local directories inside the tsgolint source.
7
+ // Shim modules that need replace directives in go.work.
8
+ // These redirect module paths to local directories inside the tsgolint source.
12
9
  const SHIM_MODULES = [
13
10
  'ast',
14
11
  'bundled',
@@ -25,87 +22,12 @@ const SHIM_MODULES = [
25
22
  'vfs/cachedvfs',
26
23
  'vfs/osvfs',
27
24
  ];
28
- // Built-in tsgolint rules — all 50+ from cmd/tsgolint/main.go.
29
- // Each entry: [package_path_suffix, exported_var_name]
30
- const BUILTIN_RULES = [
31
- ['await_thenable', 'AwaitThenableRule'],
32
- ['consistent_return', 'ConsistentReturnRule'],
33
- ['consistent_type_exports', 'ConsistentTypeExportsRule'],
34
- ['dot_notation', 'DotNotationRule'],
35
- ['no_array_delete', 'NoArrayDeleteRule'],
36
- ['no_base_to_string', 'NoBaseToStringRule'],
37
- ['no_confusing_void_expression', 'NoConfusingVoidExpressionRule'],
38
- ['no_deprecated', 'NoDeprecatedRule'],
39
- ['no_duplicate_type_constituents', 'NoDuplicateTypeConstituentsRule'],
40
- ['no_floating_promises', 'NoFloatingPromisesRule'],
41
- ['no_for_in_array', 'NoForInArrayRule'],
42
- ['no_implied_eval', 'NoImpliedEvalRule'],
43
- ['no_meaningless_void_operator', 'NoMeaninglessVoidOperatorRule'],
44
- ['no_misused_promises', 'NoMisusedPromisesRule'],
45
- ['no_misused_spread', 'NoMisusedSpreadRule'],
46
- ['no_mixed_enums', 'NoMixedEnumsRule'],
47
- ['no_redundant_type_constituents', 'NoRedundantTypeConstituentsRule'],
48
- ['no_unnecessary_boolean_literal_compare', 'NoUnnecessaryBooleanLiteralCompareRule'],
49
- ['no_unnecessary_condition', 'NoUnnecessaryConditionRule'],
50
- ['no_unnecessary_qualifier', 'NoUnnecessaryQualifierRule'],
51
- ['no_unnecessary_template_expression', 'NoUnnecessaryTemplateExpressionRule'],
52
- ['no_unnecessary_type_conversion', 'NoUnnecessaryTypeConversionRule'],
53
- ['no_unnecessary_type_arguments', 'NoUnnecessaryTypeArgumentsRule'],
54
- ['no_unnecessary_type_parameters', 'NoUnnecessaryTypeParametersRule'],
55
- ['no_unnecessary_type_assertion', 'NoUnnecessaryTypeAssertionRule'],
56
- ['no_useless_default_assignment', 'NoUselessDefaultAssignmentRule'],
57
- ['no_unsafe_argument', 'NoUnsafeArgumentRule'],
58
- ['no_unsafe_assignment', 'NoUnsafeAssignmentRule'],
59
- ['no_unsafe_call', 'NoUnsafeCallRule'],
60
- ['no_unsafe_enum_comparison', 'NoUnsafeEnumComparisonRule'],
61
- ['no_unsafe_member_access', 'NoUnsafeMemberAccessRule'],
62
- ['no_unsafe_return', 'NoUnsafeReturnRule'],
63
- ['no_unsafe_type_assertion', 'NoUnsafeTypeAssertionRule'],
64
- ['no_unsafe_unary_minus', 'NoUnsafeUnaryMinusRule'],
65
- ['non_nullable_type_assertion_style', 'NonNullableTypeAssertionStyleRule'],
66
- ['only_throw_error', 'OnlyThrowErrorRule'],
67
- ['prefer_find', 'PreferFindRule'],
68
- ['prefer_includes', 'PreferIncludesRule'],
69
- ['prefer_optional_chain', 'PreferOptionalChainRule'],
70
- ['prefer_nullish_coalescing', 'PreferNullishCoalescingRule'],
71
- ['prefer_promise_reject_errors', 'PreferPromiseRejectErrorsRule'],
72
- ['prefer_readonly_parameter_types', 'PreferReadonlyParameterTypesRule'],
73
- ['prefer_regexp_exec', 'PreferRegexpExecRule'],
74
- ['prefer_readonly', 'PreferReadonlyRule'],
75
- ['prefer_reduce_type_parameter', 'PreferReduceTypeParameterRule'],
76
- ['prefer_return_this_type', 'PreferReturnThisTypeRule'],
77
- ['prefer_string_starts_ends_with', 'PreferStringStartsEndsWithRule'],
78
- ['promise_function_async', 'PromiseFunctionAsyncRule'],
79
- ['related_getter_setter_pairs', 'RelatedGetterSetterPairsRule'],
80
- ['require_array_sort_compare', 'RequireArraySortCompareRule'],
81
- ['require_await', 'RequireAwaitRule'],
82
- ['restrict_plus_operands', 'RestrictPlusOperandsRule'],
83
- ['restrict_template_expressions', 'RestrictTemplateExpressionsRule'],
84
- ['return_await', 'ReturnAwaitRule'],
85
- ['strict_boolean_expressions', 'StrictBooleanExpressionsRule'],
86
- ['strict_void_return', 'StrictVoidReturnRule'],
87
- ['switch_exhaustiveness_check', 'SwitchExhaustivenessCheckRule'],
88
- ['unbound_method', 'UnboundMethodRule'],
89
- ['use_unknown_in_catch_callback_variable', 'UseUnknownInCatchCallbackVariableRule'],
90
- ];
91
25
  function generateReplaceDirectives(tsgolintRelPath) {
92
26
  return SHIM_MODULES.map((mod) => {
93
27
  return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`;
94
28
  }).join('\n');
95
29
  }
96
- function generateShimRequires() {
97
- return SHIM_MODULES.map((mod) => {
98
- return `\tgithub.com/microsoft/typescript-go/shim/${mod} v0.0.0`;
99
- }).join('\n');
100
- }
101
- /** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support.
102
- *
103
- * Key learnings from testing:
104
- * - Module name MUST be a child path of github.com/typescript-eslint/tsgolint
105
- * so Go allows importing internal/ packages across the module boundary.
106
- * - go.work must `use` both .tsgolint AND .tsgolint/typescript-go since
107
- * tsgolint's own go.work (which does this) is ignored by the outer workspace.
108
- * - go.mod should be minimal (no requires) — the workspace resolves everything. */
30
+ /** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
109
31
  export function generateEditorGoFiles(lintcnDir) {
110
32
  const goWork = `go 1.26
111
33
 
@@ -119,7 +41,8 @@ replace (
119
41
  ${generateReplaceDirectives('./.tsgolint')}
120
42
  )
121
43
  `;
122
- const goMod = `module github.com/typescript-eslint/tsgolint/lintcn-rules
44
+ // No child-path hack needed — pkg/ is public, any module name works
45
+ const goMod = `module lintcn-rules
123
46
 
124
47
  go 1.26
125
48
  `;
@@ -136,7 +59,9 @@ go.sum
136
59
  fs.writeFileSync(gitignorePath, gitignore);
137
60
  }
138
61
  }
139
- /** Generate build workspace in cache dir for compiling the custom binary */
62
+ /** Generate build workspace for compiling the custom binary.
63
+ * With pkg/runner.Run(), the generated main.go is a static template —
64
+ * no regex surgery or file copying needed. */
140
65
  export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }) {
141
66
  fs.mkdirSync(path.join(buildDir, 'wrapper'), { recursive: true });
142
67
  // symlink tsgolint source
@@ -151,7 +76,7 @@ export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules
151
76
  fs.rmSync(rulesLink, { recursive: true });
152
77
  }
153
78
  fs.symlinkSync(path.resolve(lintcnDir), rulesLink);
154
- // go.work — must include typescript-go submodule and use child module paths
79
+ // go.work
155
80
  const goWork = `go 1.26
156
81
 
157
82
  use (
@@ -166,442 +91,38 @@ ${generateReplaceDirectives('./tsgolint')}
166
91
  )
167
92
  `;
168
93
  fs.writeFileSync(path.join(buildDir, 'go.work'), goWork);
169
- // wrapper/go.mod — must be child path of tsgolint for internal/ access
170
- const wrapperGoMod = `module github.com/typescript-eslint/tsgolint/lintcn-wrapper
94
+ // wrapper/go.mod — simple module name, no child-path hack needed
95
+ const wrapperGoMod = `module lintcn-wrapper
171
96
 
172
97
  go 1.26
173
-
174
- require (
175
- \tgithub.com/typescript-eslint/tsgolint v0.0.0
176
- \tgithub.com/typescript-eslint/tsgolint/lintcn-rules v0.0.0
177
- )
178
98
  `;
179
99
  fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod);
180
- // wrapper/main.go
100
+ // wrapper/main.go — simple template, no regex or string surgery
181
101
  const mainGo = generateMainGo(rules);
182
102
  fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo);
183
103
  }
184
- /** Generate the main.go that imports all built-in + custom rules.
185
- * This is essentially tsgolint's cmd/tsgolint/main.go with custom rules appended
186
- * to allRules. We import tsgolint's internal packages directly since go.work
187
- * allows cross-module internal imports. */
188
- function generateMainGo(customRules) {
189
- const tsgolintPkg = 'github.com/typescript-eslint/tsgolint';
190
- // built-in rule imports
191
- const builtinImports = BUILTIN_RULES.map(([pkg]) => {
192
- return `\t"${tsgolintPkg}/internal/rules/${pkg}"`;
193
- }).join('\n');
194
- // built-in rule entries
195
- const builtinEntries = BUILTIN_RULES.map(([pkg, varName]) => {
196
- return `\t${pkg}.${varName},`;
104
+ /** Generate a minimal main.go that imports user rules and calls runner.Run().
105
+ * This is a static template no copying or patching of tsgolint source. */
106
+ function generateMainGo(rules) {
107
+ const ruleEntries = rules.map((r) => {
108
+ return `\t\tlintcn.${r.varName},`;
197
109
  }).join('\n');
198
- // custom rule entries (all in package lintcn via single import)
199
- const customEntries = customRules.map((r) => {
200
- return `\tlintcn.${r.varName},`;
201
- }).join('\n');
202
- // only add lintcn import if there are custom rules
203
- const lintcnImport = customRules.length > 0 ? '\n\tlintcn "github.com/typescript-eslint/tsgolint/lintcn-rules"' : '';
204
110
  return `// Code generated by lintcn. DO NOT EDIT.
205
111
  package main
206
112
 
207
113
  import (
208
- \t"bufio"
209
- \t"flag"
210
- \t"fmt"
211
- \t"math"
212
114
  \t"os"
213
- \t"runtime"
214
- \t"runtime/pprof"
215
- \t"runtime/trace"
216
- \t"slices"
217
- \t"strconv"
218
- \t"strings"
219
- \t"sync"
220
- \t"time"
221
- \t"unicode"
222
-
223
- \t"${tsgolintPkg}/internal/diagnostic"
224
- \t"${tsgolintPkg}/internal/linter"
225
- \t"${tsgolintPkg}/internal/rule"
226
- \t"${tsgolintPkg}/internal/utils"
227
115
 
228
- ${builtinImports}${lintcnImport}
229
-
230
- \t"github.com/microsoft/typescript-go/shim/ast"
231
- \t"github.com/microsoft/typescript-go/shim/bundled"
232
- \t"github.com/microsoft/typescript-go/shim/core"
233
- \t"github.com/microsoft/typescript-go/shim/scanner"
234
- \t"github.com/microsoft/typescript-go/shim/tspath"
235
- \t"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
236
- \t"github.com/microsoft/typescript-go/shim/vfs/osvfs"
116
+ \t"github.com/typescript-eslint/tsgolint/pkg/rule"
117
+ \t"github.com/typescript-eslint/tsgolint/pkg/runner"
118
+ \tlintcn "lintcn-rules"
237
119
  )
238
120
 
239
- var allRules = []rule.Rule{
240
- ${builtinEntries}
241
- ${customEntries}
242
- }
243
-
244
- var allRulesByName = make(map[string]rule.Rule, len(allRules))
245
-
246
- func init() {
247
- \tfor _, rule := range allRules {
248
- \t\tallRulesByName[rule.Name] = rule
249
- \t}
250
- }
251
-
252
- // Below is copied from tsgolint's cmd/tsgolint/main.go.
253
- // We cannot import it directly because main packages are not importable in Go.
254
-
255
- func recordTrace(traceOut string) (func(), error) {
256
- \tif traceOut != "" {
257
- \t\tf, err := os.Create(traceOut)
258
- \t\tif err != nil {
259
- \t\t\treturn nil, fmt.Errorf("error creating trace file: %w", err)
260
- \t\t}
261
- \t\ttrace.Start(f)
262
- \t\treturn func() {
263
- \t\t\ttrace.Stop()
264
- \t\t\tf.Close()
265
- \t\t}, nil
266
- \t}
267
- \treturn func() {}, nil
268
- }
269
-
270
- func recordCpuprof(cpuprofOut string) (func(), error) {
271
- \tif cpuprofOut != "" {
272
- \t\tf, err := os.Create(cpuprofOut)
273
- \t\tif err != nil {
274
- \t\t\treturn nil, fmt.Errorf("error creating cpuprof file: %w", err)
275
- \t\t}
276
- \t\terr = pprof.StartCPUProfile(f)
277
- \t\tif err != nil {
278
- \t\t\treturn nil, fmt.Errorf("error starting cpu profiling: %w", err)
279
- \t\t}
280
- \t\treturn func() {
281
- \t\t\tpprof.StopCPUProfile()
282
- \t\t\tf.Close()
283
- \t\t}, nil
284
- \t}
285
- \treturn func() {}, nil
286
- }
287
-
288
- const spaces = " "
289
-
290
- func printDiagnostic(d rule.RuleDiagnostic, w *bufio.Writer, comparePathOptions tspath.ComparePathsOptions) {
291
- \tdiagnosticStart := d.Range.Pos()
292
- \tdiagnosticEnd := d.Range.End()
293
- \tdiagnosticStartLine, diagnosticStartColumn := scanner.GetECMALineAndUTF16CharacterOfPosition(d.SourceFile, diagnosticStart)
294
- \tdiagnosticEndline, _ := scanner.GetECMALineAndUTF16CharacterOfPosition(d.SourceFile, diagnosticEnd)
295
- \tlineMap := d.SourceFile.ECMALineMap()
296
- \ttext := d.SourceFile.Text()
297
- \tcodeboxStartLine := max(diagnosticStartLine-1, 0)
298
- \tcodeboxEndLine := min(diagnosticEndline+1, len(lineMap)-1)
299
- \tcodeboxStart := scanner.GetECMAPositionOfLineAndUTF16Character(d.SourceFile, codeboxStartLine, 0)
300
- \tvar codeboxEndColumn int
301
- \tif codeboxEndLine == len(lineMap)-1 {
302
- \t\tcodeboxEndColumn = len(text) - int(lineMap[len(lineMap)-1])
303
- \t} else {
304
- \t\tcodeboxEndColumn = int(lineMap[codeboxEndLine+1]-lineMap[codeboxEndLine]) - 1
305
- \t}
306
- \tcodeboxEnd := scanner.GetECMAPositionOfLineAndUTF16Character(d.SourceFile, codeboxEndLine, core.UTF16Offset(codeboxEndColumn))
307
- \tw.Write([]byte{' ', 0x1b, '[', '7', 'm', 0x1b, '[', '1', 'm', 0x1b, '[', '3', '8', ';', '5', ';', '3', '7', 'm', ' '})
308
- \tw.WriteString(d.RuleName)
309
- \tw.WriteString(" \\x1b[0m — ")
310
- \tmessageLineStart := 0
311
- \tfor i, char := range d.Message.Description {
312
- \t\tif char == '\\n' {
313
- \t\t\tw.WriteString(d.Message.Description[messageLineStart : i+1])
314
- \t\t\tmessageLineStart = i + 1
315
- \t\t\tw.WriteString(" \\x1b[2m│\\x1b[0m")
316
- \t\t\tw.WriteString(spaces[:len(d.RuleName)+1])
317
- \t\t}
318
- \t}
319
- \tif messageLineStart <= len(d.Message.Description) {
320
- \t\tw.WriteString(d.Message.Description[messageLineStart:len(d.Message.Description)])
321
- \t}
322
- \tw.WriteString("\\n \\x1b[2m╭─┴──────────(\\x1b[0m \\x1b[3m\\x1b[38;5;117m")
323
- \tw.WriteString(tspath.ConvertToRelativePath(d.SourceFile.FileName(), comparePathOptions))
324
- \tw.WriteByte(':')
325
- \tw.WriteString(strconv.Itoa(diagnosticStartLine + 1))
326
- \tw.WriteByte(':')
327
- \tw.WriteString(strconv.Itoa(int(diagnosticStartColumn) + 1))
328
- \tw.WriteString("\\x1b[0m \\x1b[2m)─────\\x1b[0m\\n")
329
- \tindentSize := math.MaxInt
330
- \tline := codeboxStartLine
331
- \tlineIndentCalculated := false
332
- \tlastNonSpaceIndex := -1
333
- \tlineStarts := make([]int, 13)
334
- \tlineEnds := make([]int, 13)
335
- \tif codeboxEndLine-codeboxStartLine >= len(lineEnds) {
336
- \t\tw.WriteString(" \\x1b[2m│\\x1b[0m Error range is too big. Skipping code block printing.\\n \\x1b[2m╰────────────────────────────────\\x1b[0m\\n\\n")
337
- \t\treturn
338
- \t}
339
- \tfor i, char := range text[codeboxStart:codeboxEnd] {
340
- \t\tif char == '\\n' {
341
- \t\t\tif line != codeboxEndLine {
342
- \t\t\t\tlineIndentCalculated = false
343
- \t\t\t\tlineEnds[line-codeboxStartLine] = lastNonSpaceIndex - int(lineMap[line]) + codeboxStart
344
- \t\t\t\tlastNonSpaceIndex = -1
345
- \t\t\t\tline++
346
- \t\t\t}
347
- \t\t\tcontinue
348
- \t\t}
349
- \t\tif !lineIndentCalculated && !unicode.IsSpace(char) {
350
- \t\t\tlineIndentCalculated = true
351
- \t\t\tlineStarts[line-codeboxStartLine] = i - int(lineMap[line]) + codeboxStart
352
- \t\t\tindentSize = min(indentSize, lineStarts[line-codeboxStartLine])
353
- \t\t}
354
- \t\tif lineIndentCalculated && !unicode.IsSpace(char) {
355
- \t\t\tlastNonSpaceIndex = i + 1
356
- \t\t}
357
- \t}
358
- \tif line == codeboxEndLine {
359
- \t\tlineEnds[line-codeboxStartLine] = lastNonSpaceIndex - int(lineMap[line]) + codeboxStart
360
- \t}
361
- \tdiagnosticHighlightActive := false
362
- \tlastLineNumber := strconv.Itoa(codeboxEndLine + 1)
363
- \tfor line := codeboxStartLine; line <= codeboxEndLine; line++ {
364
- \t\tw.WriteString(" \\x1b[2m│ ")
365
- \t\tif line == codeboxEndLine {
366
- \t\t\tw.WriteString(lastLineNumber)
367
- \t\t} else {
368
- \t\t\tnumber := strconv.Itoa(line + 1)
369
- \t\t\tif len(number) < len(lastLineNumber) {
370
- \t\t\t\tw.WriteByte(' ')
371
- \t\t\t}
372
- \t\t\tw.WriteString(number)
373
- \t\t}
374
- \t\tw.WriteString(" │\\x1b[0m ")
375
- \t\tlineTextStart := int(lineMap[line]) + indentSize
376
- \t\tunderlineStart := max(lineTextStart, int(lineMap[line])+lineStarts[line-codeboxStartLine])
377
- \t\tunderlineEnd := underlineStart
378
- \t\tlineTextEnd := max(int(lineMap[line])+lineEnds[line-codeboxStartLine], lineTextStart)
379
- \t\tif diagnosticHighlightActive {
380
- \t\t\tunderlineEnd = lineTextEnd
381
- \t\t} else if int(lineMap[line]) <= diagnosticStart && (line == len(lineMap) || diagnosticStart < int(lineMap[line+1])) {
382
- \t\t\tunderlineStart = min(max(lineTextStart, diagnosticStart), lineTextEnd)
383
- \t\t\tunderlineEnd = lineTextEnd
384
- \t\t\tdiagnosticHighlightActive = true
385
- \t\t}
386
- \t\tif int(lineMap[line]) <= diagnosticEnd && (line == len(lineMap) || diagnosticEnd < int(lineMap[line+1])) {
387
- \t\t\tunderlineEnd = min(max(underlineStart, diagnosticEnd), lineTextEnd)
388
- \t\t\tdiagnosticHighlightActive = false
389
- \t\t}
390
- \t\tif underlineStart != underlineEnd {
391
- \t\t\tw.WriteString(text[lineTextStart:underlineStart])
392
- \t\t\tw.Write([]byte{
393
- \t\t\t\t0x1b, '[', '4', 'm',
394
- \t\t\t\t0x1b, '[', '4', ':', '3', 'm',
395
- \t\t\t\t0x1b, '[', '5', '8', ':', '5', ':', '1', '6', '0', 'm',
396
- \t\t\t\t0x1b, '[', '3', '8', ';', '5', ';', '1', '6', '0', 'm',
397
- \t\t\t\t0x1b, '[', '2', '2', ';', '4', '9', 'm',
398
- \t\t\t})
399
- \t\t\tw.WriteString(text[underlineStart:underlineEnd])
400
- \t\t\tw.Write([]byte{0x1b, '[', '0', 'm'})
401
- \t\t\tw.WriteString(text[underlineEnd:lineTextEnd])
402
- \t\t} else if lineTextStart != lineTextEnd {
403
- \t\t\tw.WriteString(text[lineTextStart:lineTextEnd])
404
- \t\t}
405
- \t\tw.WriteByte('\\n')
406
- \t}
407
- \tw.WriteString(" \\x1b[2m╰────────────────────────────────\\x1b[0m\\n\\n")
408
- }
409
-
410
- const usage = \` lintcn - type-aware TypeScript linter with custom rules
411
-
412
- Usage:
413
- lintcn-binary [OPTIONS]
414
-
415
- Options:
416
- --tsconfig PATH Which tsconfig to use. Defaults to tsconfig.json.
417
- --list-files List matched files
418
- -h, --help Show help
419
- \`
420
-
421
- func runMain() int {
422
- \tflag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
423
- \tvar (
424
- \t\thelp bool
425
- \t\ttsconfig string
426
- \t\tlistFiles bool
427
- \t\ttraceOut string
428
- \t\tcpuprofOut string
429
- \t\tsingleThreaded bool
430
- \t)
431
- \tflag.StringVar(&tsconfig, "tsconfig", "", "which tsconfig to use")
432
- \tflag.BoolVar(&listFiles, "list-files", false, "list matched files")
433
- \tflag.BoolVar(&help, "help", false, "show help")
434
- \tflag.BoolVar(&help, "h", false, "show help")
435
- \tflag.StringVar(&traceOut, "trace", "", "file to put trace to")
436
- \tflag.StringVar(&cpuprofOut, "cpuprof", "", "file to put cpu profiling to")
437
- \tflag.BoolVar(&singleThreaded, "singleThreaded", false, "run in single threaded mode")
438
- \tflag.Parse()
439
- \tif help {
440
- \t\tflag.Usage()
441
- \t\treturn 0
442
- \t}
443
- \ttimeBefore := time.Now()
444
- \tif done, err := recordTrace(traceOut); err != nil {
445
- \t\tos.Stderr.WriteString(err.Error())
446
- \t\treturn 1
447
- \t} else {
448
- \t\tdefer done()
449
- \t}
450
- \tif done, err := recordCpuprof(cpuprofOut); err != nil {
451
- \t\tos.Stderr.WriteString(err.Error())
452
- \t\treturn 1
453
- \t} else {
454
- \t\tdefer done()
455
- \t}
456
- \tcurrentDirectory, err := os.Getwd()
457
- \tif err != nil {
458
- \t\tfmt.Fprintf(os.Stderr, "error getting current directory: %v\\n", err)
459
- \t\treturn 1
460
- \t}
461
- \tcurrentDirectory = tspath.NormalizePath(currentDirectory)
462
- \tfs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
463
- \tvar configFileName string
464
- \tif tsconfig == "" {
465
- \t\tconfigFileName = tspath.ResolvePath(currentDirectory, "tsconfig.json")
466
- \t\tif !fs.FileExists(configFileName) {
467
- \t\t\tfs = utils.NewOverlayVFS(fs, map[string]string{
468
- \t\t\t\tconfigFileName: "{}",
469
- \t\t\t})
470
- \t\t}
471
- \t} else {
472
- \t\tconfigFileName = tspath.ResolvePath(currentDirectory, tsconfig)
473
- \t\tif !fs.FileExists(configFileName) {
474
- \t\t\tfmt.Fprintf(os.Stderr, "error: tsconfig %q doesn't exist", tsconfig)
475
- \t\t\treturn 1
476
- \t\t}
477
- \t}
478
- \tcurrentDirectory = tspath.GetDirectoryPath(configFileName)
479
- \thost := utils.CreateCompilerHost(currentDirectory, fs)
480
- \tcomparePathOptions := tspath.ComparePathsOptions{
481
- \t\tCurrentDirectory: host.GetCurrentDirectory(),
482
- \t\tUseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
483
- \t}
484
- \tprogram, _, err := utils.CreateProgram(singleThreaded, fs, currentDirectory, configFileName, host, false)
485
- \tif err != nil {
486
- \t\tfmt.Fprintf(os.Stderr, "error creating TS program: %v", err)
487
- \t\treturn 1
488
- \t}
489
- \tif program == nil {
490
- \t\tfmt.Fprintf(os.Stderr, "error creating TS program")
491
- \t\treturn 1
492
- \t}
493
- \tfiles := []*ast.SourceFile{}
494
- \tcwdPath := string(tspath.ToPath("", currentDirectory, program.Host().FS().UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator())
495
- \tvar matchedFiles strings.Builder
496
- \tfor _, file := range program.SourceFiles() {
497
- \t\tp := string(file.Path())
498
- \t\tif strings.Contains(p, "/node_modules/") {
499
- \t\t\tcontinue
500
- \t\t}
501
- \t\tif fileName, matched := strings.CutPrefix(p, cwdPath); matched {
502
- \t\t\tif listFiles {
503
- \t\t\t\tmatchedFiles.WriteString("Found file: ")
504
- \t\t\t\tmatchedFiles.WriteString(fileName)
505
- \t\t\t\tmatchedFiles.WriteByte('\\n')
506
- \t\t\t}
507
- \t\t\tfiles = append(files, file)
508
- \t\t}
509
- \t}
510
- \tif listFiles {
511
- \t\tos.Stdout.WriteString(matchedFiles.String())
512
- \t}
513
- \tslices.SortFunc(files, func(a *ast.SourceFile, b *ast.SourceFile) int {
514
- \t\treturn len(b.Text()) - len(a.Text())
515
- \t})
516
- \tvar wg sync.WaitGroup
517
- \tdiagnosticsChan := make(chan rule.RuleDiagnostic, 4096)
518
- \terrorsCount := 0
519
- \twg.Go(func() {
520
- \t\tw := bufio.NewWriterSize(os.Stdout, 4096*100)
521
- \t\tdefer w.Flush()
522
- \t\tfor d := range diagnosticsChan {
523
- \t\t\terrorsCount++
524
- \t\t\tif errorsCount == 1 {
525
- \t\t\t\tw.WriteByte('\\n')
526
- \t\t\t}
527
- \t\t\tprintDiagnostic(d, w, comparePathOptions)
528
- \t\t\tif w.Available() < 4096 {
529
- \t\t\t\tw.Flush()
530
- \t\t\t}
531
- \t\t}
532
- \t})
533
- \terr = linter.RunLinterOnProgram(
534
- \t\tutils.GetLogLevel(),
535
- \t\tprogram,
536
- \t\tfiles,
537
- \t\truntime.GOMAXPROCS(0),
538
- \t\tfunc(sourceFile *ast.SourceFile) []linter.ConfiguredRule {
539
- \t\t\treturn utils.Map(allRules, func(r rule.Rule) linter.ConfiguredRule {
540
- \t\t\t\treturn linter.ConfiguredRule{
541
- \t\t\t\t\tName: r.Name,
542
- \t\t\t\t\tRun: func(ctx rule.RuleContext) rule.RuleListeners {
543
- \t\t\t\t\t\treturn r.Run(ctx, nil)
544
- \t\t\t\t\t},
545
- \t\t\t\t}
546
- \t\t\t})
547
- \t\t},
548
- \t\tfunc(d rule.RuleDiagnostic) {
549
- \t\t\tdiagnosticsChan <- d
550
- \t\t},
551
- \t\tfunc(d diagnostic.Internal) {},
552
- \t\tlinter.Fixes{
553
- \t\t\tFix: true,
554
- \t\t\tFixSuggestions: true,
555
- \t\t},
556
- \t\tlinter.TypeErrors{
557
- \t\t\tReportSyntactic: false,
558
- \t\t\tReportSemantic: false,
559
- \t\t},
560
- \t)
561
- \tclose(diagnosticsChan)
562
- \tif err != nil {
563
- \t\tfmt.Fprintf(os.Stderr, "error running linter: %v\\n", err)
564
- \t\treturn 1
565
- \t}
566
- \twg.Wait()
567
- \terrorsColor := "\\x1b[1m"
568
- \tif errorsCount == 0 {
569
- \t\terrorsColor = "\\x1b[1;32m"
570
- \t}
571
- \terrorsText := "errors"
572
- \tif errorsCount == 1 {
573
- \t\terrorsText = "error"
574
- \t}
575
- \tfilesText := "files"
576
- \tif len(files) == 1 {
577
- \t\tfilesText = "file"
578
- \t}
579
- \trulesText := "rules"
580
- \tif len(allRules) == 1 {
581
- \t\trulesText = "rule"
582
- \t}
583
- \tthreadsCount := 1
584
- \tif !singleThreaded {
585
- \t\tthreadsCount = runtime.GOMAXPROCS(0)
586
- \t}
587
- \tfmt.Fprintf(
588
- \t\tos.Stdout,
589
- \t\t"Found %v%v\\x1b[0m %v \\x1b[2m(linted \\x1b[1m%v\\x1b[22m\\x1b[2m %v with \\x1b[1m%v\\x1b[22m\\x1b[2m %v in \\x1b[1m%v\\x1b[22m\\x1b[2m using \\x1b[1m%v\\x1b[22m\\x1b[2m threads)\\n",
590
- \t\terrorsColor,
591
- \t\terrorsCount,
592
- \t\terrorsText,
593
- \t\tlen(files),
594
- \t\tfilesText,
595
- \t\tlen(allRules),
596
- \t\trulesText,
597
- \t\ttime.Since(timeBefore).Round(time.Millisecond),
598
- \t\tthreadsCount,
599
- \t)
600
- \treturn 0
601
- }
602
-
603
121
  func main() {
604
- \tos.Exit(runMain())
122
+ \trules := []rule.Rule{
123
+ ${ruleEntries}
124
+ \t}
125
+ \tos.Exit(runner.Run(rules, os.Args[1:]))
605
126
  }
606
127
  `;
607
128
  }
@@ -1 +1 @@
1
- {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AA6DA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDxD"}
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AA+EA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDxD"}
@@ -6,32 +6,48 @@ import path from 'node:path';
6
6
  import { getLintcnDir } from "../paths.js";
7
7
  import { generateEditorGoFiles } from "../codegen.js";
8
8
  import { ensureTsgolintSource, DEFAULT_TSGOLINT_VERSION } from "../cache.js";
9
+ /** Convert GitHub blob URLs to raw.githubusercontent.com.
10
+ * Handles branch names containing slashes (e.g. feature/x) by splitting
11
+ * on /blob/ then finding the file path from the end (must end in .go). */
9
12
  function normalizeGithubUrl(url) {
10
- // Convert github.com/user/repo/blob/branch/path to raw.githubusercontent.com
11
- const blobMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
12
- if (blobMatch) {
13
- const [, owner, repo, branch, filePath] = blobMatch;
14
- return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`;
13
+ const blobSplit = url.match(/^(https?:\/\/github\.com\/[^/]+\/[^/]+)\/blob\/(.+)$/);
14
+ if (!blobSplit) {
15
+ return url;
15
16
  }
16
- return url;
17
+ const [, repoUrl, refAndPath] = blobSplit;
18
+ // repoUrl = "https://github.com/owner/repo"
19
+ // refAndPath = "feature/x/rules/my_rule.go" or "main/rules/my_rule.go"
20
+ // Extract owner/repo from repoUrl
21
+ const repoMatch = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)$/);
22
+ if (!repoMatch) {
23
+ return url;
24
+ }
25
+ const [, owner, repo] = repoMatch;
26
+ // For raw.githubusercontent.com, the format is owner/repo/ref/path.
27
+ // We can pass refAndPath directly since GitHub resolves it.
28
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${refAndPath}`;
17
29
  }
18
30
  function deriveTestUrl(rawUrl) {
19
31
  return rawUrl.replace(/\.go$/, '_test.go');
20
32
  }
21
33
  async function fetchFile(url) {
34
+ const response = await fetch(url);
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
37
+ }
38
+ return response.text();
39
+ }
40
+ async function tryFetchFile(url) {
22
41
  try {
23
- const response = await fetch(url);
24
- if (!response.ok) {
25
- return null;
26
- }
27
- return await response.text();
42
+ return await fetchFile(url);
28
43
  }
29
44
  catch {
30
45
  return null;
31
46
  }
32
47
  }
33
48
  function rewritePackageName(content) {
34
- // Rewrite first package declaration to package lintcn
49
+ // Rewrite first package declaration to package lintcn.
50
+ // Only matches before the first import or func to avoid touching comments.
35
51
  return content.replace(/^package\s+\w+/m, 'package lintcn');
36
52
  }
37
53
  function ensureSourceComment(content, sourceUrl) {
@@ -56,9 +72,6 @@ export async function addRule(url) {
56
72
  const rawUrl = normalizeGithubUrl(url);
57
73
  console.log(`Fetching ${rawUrl}...`);
58
74
  const content = await fetchFile(rawUrl);
59
- if (!content) {
60
- throw new Error(`Could not fetch rule from ${rawUrl}`);
61
- }
62
75
  // validate it looks like a Go file with a rule
63
76
  if (!content.includes('rule.Rule')) {
64
77
  console.warn('Warning: no rule.Rule reference found in this file. Are you sure this is a tsgolint rule?');
@@ -82,7 +95,7 @@ export async function addRule(url) {
82
95
  console.log(`Added ${fileName}`);
83
96
  // try to fetch matching test file
84
97
  const testUrl = deriveTestUrl(rawUrl);
85
- const testContent = await fetchFile(testUrl);
98
+ const testContent = await tryFetchFile(testUrl);
86
99
  if (testContent) {
87
100
  const testFileName = fileName.replace(/\.go$/, '_test.go');
88
101
  const testProcessed = rewritePackageName(testContent);
@@ -1 +1 @@
1
- {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAuBA,wBAAsB,WAAW,CAAC,EAChC,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoDlB;AAED,wBAAsB,IAAI,CAAC,EACzB,OAAO,EACP,eAAe,EACf,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBlB"}
1
+ {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAuBA,wBAAsB,WAAW,CAAC,EAChC,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiDlB;AAED,wBAAsB,IAAI,CAAC,EACzB,OAAO,EACP,eAAe,EACf,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBlB"}