lintcn 0.2.0 → 0.3.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/CHANGELOG.md +21 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +82 -41
- package/dist/codegen.d.ts +7 -1
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +64 -498
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +29 -16
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +2 -5
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +3 -4
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +2 -5
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +8 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +6 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +23 -1
- package/package.json +2 -1
- package/src/cache.ts +93 -46
- package/src/codegen.ts +74 -498
- package/src/commands/add.ts +34 -19
- package/src/commands/lint.ts +2 -5
- package/src/commands/list.ts +3 -4
- package/src/commands/remove.ts +2 -6
- package/src/discover.ts +10 -1
- package/src/hash.ts +7 -2
- package/src/index.ts +1 -0
- package/src/paths.ts +25 -1
package/src/codegen.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// .lintcn/go.mod — module declaration
|
|
5
5
|
// build/go.work — build workspace in cache dir
|
|
6
6
|
// build/wrapper/go.mod — wrapper module
|
|
7
|
-
// build/wrapper/main.go —
|
|
7
|
+
// build/wrapper/main.go — tsgolint main.go with custom rules appended
|
|
8
8
|
|
|
9
9
|
import fs from 'node:fs'
|
|
10
10
|
import path from 'node:path'
|
|
@@ -29,82 +29,12 @@ const SHIM_MODULES = [
|
|
|
29
29
|
'vfs/osvfs',
|
|
30
30
|
] as const
|
|
31
31
|
|
|
32
|
-
// Built-in tsgolint rules — all 50+ from cmd/tsgolint/main.go.
|
|
33
|
-
// Each entry: [package_path_suffix, exported_var_name]
|
|
34
|
-
const BUILTIN_RULES: [string, string][] = [
|
|
35
|
-
['await_thenable', 'AwaitThenableRule'],
|
|
36
|
-
['consistent_return', 'ConsistentReturnRule'],
|
|
37
|
-
['consistent_type_exports', 'ConsistentTypeExportsRule'],
|
|
38
|
-
['dot_notation', 'DotNotationRule'],
|
|
39
|
-
['no_array_delete', 'NoArrayDeleteRule'],
|
|
40
|
-
['no_base_to_string', 'NoBaseToStringRule'],
|
|
41
|
-
['no_confusing_void_expression', 'NoConfusingVoidExpressionRule'],
|
|
42
|
-
['no_deprecated', 'NoDeprecatedRule'],
|
|
43
|
-
['no_duplicate_type_constituents', 'NoDuplicateTypeConstituentsRule'],
|
|
44
|
-
['no_floating_promises', 'NoFloatingPromisesRule'],
|
|
45
|
-
['no_for_in_array', 'NoForInArrayRule'],
|
|
46
|
-
['no_implied_eval', 'NoImpliedEvalRule'],
|
|
47
|
-
['no_meaningless_void_operator', 'NoMeaninglessVoidOperatorRule'],
|
|
48
|
-
['no_misused_promises', 'NoMisusedPromisesRule'],
|
|
49
|
-
['no_misused_spread', 'NoMisusedSpreadRule'],
|
|
50
|
-
['no_mixed_enums', 'NoMixedEnumsRule'],
|
|
51
|
-
['no_redundant_type_constituents', 'NoRedundantTypeConstituentsRule'],
|
|
52
|
-
['no_unnecessary_boolean_literal_compare', 'NoUnnecessaryBooleanLiteralCompareRule'],
|
|
53
|
-
['no_unnecessary_condition', 'NoUnnecessaryConditionRule'],
|
|
54
|
-
['no_unnecessary_qualifier', 'NoUnnecessaryQualifierRule'],
|
|
55
|
-
['no_unnecessary_template_expression', 'NoUnnecessaryTemplateExpressionRule'],
|
|
56
|
-
['no_unnecessary_type_conversion', 'NoUnnecessaryTypeConversionRule'],
|
|
57
|
-
['no_unnecessary_type_arguments', 'NoUnnecessaryTypeArgumentsRule'],
|
|
58
|
-
['no_unnecessary_type_parameters', 'NoUnnecessaryTypeParametersRule'],
|
|
59
|
-
['no_unnecessary_type_assertion', 'NoUnnecessaryTypeAssertionRule'],
|
|
60
|
-
['no_useless_default_assignment', 'NoUselessDefaultAssignmentRule'],
|
|
61
|
-
['no_unsafe_argument', 'NoUnsafeArgumentRule'],
|
|
62
|
-
['no_unsafe_assignment', 'NoUnsafeAssignmentRule'],
|
|
63
|
-
['no_unsafe_call', 'NoUnsafeCallRule'],
|
|
64
|
-
['no_unsafe_enum_comparison', 'NoUnsafeEnumComparisonRule'],
|
|
65
|
-
['no_unsafe_member_access', 'NoUnsafeMemberAccessRule'],
|
|
66
|
-
['no_unsafe_return', 'NoUnsafeReturnRule'],
|
|
67
|
-
['no_unsafe_type_assertion', 'NoUnsafeTypeAssertionRule'],
|
|
68
|
-
['no_unsafe_unary_minus', 'NoUnsafeUnaryMinusRule'],
|
|
69
|
-
['non_nullable_type_assertion_style', 'NonNullableTypeAssertionStyleRule'],
|
|
70
|
-
['only_throw_error', 'OnlyThrowErrorRule'],
|
|
71
|
-
['prefer_find', 'PreferFindRule'],
|
|
72
|
-
['prefer_includes', 'PreferIncludesRule'],
|
|
73
|
-
['prefer_optional_chain', 'PreferOptionalChainRule'],
|
|
74
|
-
['prefer_nullish_coalescing', 'PreferNullishCoalescingRule'],
|
|
75
|
-
['prefer_promise_reject_errors', 'PreferPromiseRejectErrorsRule'],
|
|
76
|
-
['prefer_readonly_parameter_types', 'PreferReadonlyParameterTypesRule'],
|
|
77
|
-
['prefer_regexp_exec', 'PreferRegexpExecRule'],
|
|
78
|
-
['prefer_readonly', 'PreferReadonlyRule'],
|
|
79
|
-
['prefer_reduce_type_parameter', 'PreferReduceTypeParameterRule'],
|
|
80
|
-
['prefer_return_this_type', 'PreferReturnThisTypeRule'],
|
|
81
|
-
['prefer_string_starts_ends_with', 'PreferStringStartsEndsWithRule'],
|
|
82
|
-
['promise_function_async', 'PromiseFunctionAsyncRule'],
|
|
83
|
-
['related_getter_setter_pairs', 'RelatedGetterSetterPairsRule'],
|
|
84
|
-
['require_array_sort_compare', 'RequireArraySortCompareRule'],
|
|
85
|
-
['require_await', 'RequireAwaitRule'],
|
|
86
|
-
['restrict_plus_operands', 'RestrictPlusOperandsRule'],
|
|
87
|
-
['restrict_template_expressions', 'RestrictTemplateExpressionsRule'],
|
|
88
|
-
['return_await', 'ReturnAwaitRule'],
|
|
89
|
-
['strict_boolean_expressions', 'StrictBooleanExpressionsRule'],
|
|
90
|
-
['strict_void_return', 'StrictVoidReturnRule'],
|
|
91
|
-
['switch_exhaustiveness_check', 'SwitchExhaustivenessCheckRule'],
|
|
92
|
-
['unbound_method', 'UnboundMethodRule'],
|
|
93
|
-
['use_unknown_in_catch_callback_variable', 'UseUnknownInCatchCallbackVariableRule'],
|
|
94
|
-
]
|
|
95
|
-
|
|
96
32
|
function generateReplaceDirectives(tsgolintRelPath: string): string {
|
|
97
33
|
return SHIM_MODULES.map((mod) => {
|
|
98
34
|
return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`
|
|
99
35
|
}).join('\n')
|
|
100
36
|
}
|
|
101
37
|
|
|
102
|
-
function generateShimRequires(): string {
|
|
103
|
-
return SHIM_MODULES.map((mod) => {
|
|
104
|
-
return `\tgithub.com/microsoft/typescript-go/shim/${mod} v0.0.0`
|
|
105
|
-
}).join('\n')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
38
|
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support.
|
|
109
39
|
*
|
|
110
40
|
* Key learnings from testing:
|
|
@@ -148,7 +78,10 @@ go.sum
|
|
|
148
78
|
}
|
|
149
79
|
}
|
|
150
80
|
|
|
151
|
-
/** Generate build workspace in cache dir for compiling the custom binary
|
|
81
|
+
/** Generate build workspace in cache dir for compiling the custom binary.
|
|
82
|
+
* Instead of hardcoding the built-in rule list, we copy tsgolint's actual
|
|
83
|
+
* main.go and inject custom rule imports + entries. This way the generated
|
|
84
|
+
* code always matches the pinned tsgolint version. */
|
|
152
85
|
export function generateBuildWorkspace({
|
|
153
86
|
buildDir,
|
|
154
87
|
tsgolintDir,
|
|
@@ -192,449 +125,92 @@ ${generateReplaceDirectives('./tsgolint')}
|
|
|
192
125
|
`
|
|
193
126
|
fs.writeFileSync(path.join(buildDir, 'go.work'), goWork)
|
|
194
127
|
|
|
195
|
-
// wrapper/go.mod — must be child path of tsgolint for internal/ access
|
|
128
|
+
// wrapper/go.mod — must be child path of tsgolint for internal/ access.
|
|
129
|
+
// Minimal: no require block. The workspace resolves all dependencies.
|
|
130
|
+
// Adding explicit requires with v0.0.0 triggers Go proxy lookups that fail.
|
|
196
131
|
const wrapperGoMod = `module github.com/typescript-eslint/tsgolint/lintcn-wrapper
|
|
197
132
|
|
|
198
133
|
go 1.26
|
|
199
|
-
|
|
200
|
-
require (
|
|
201
|
-
\tgithub.com/typescript-eslint/tsgolint v0.0.0
|
|
202
|
-
\tgithub.com/typescript-eslint/tsgolint/lintcn-rules v0.0.0
|
|
203
|
-
)
|
|
204
134
|
`
|
|
205
135
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod)
|
|
206
136
|
|
|
207
|
-
//
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
}
|
|
137
|
+
// copy all supporting .go files from cmd/tsgolint/ (headless, payload, etc.)
|
|
138
|
+
const wrapperDir = path.join(buildDir, 'wrapper')
|
|
139
|
+
copyTsgolintCmdFiles(tsgolintDir, wrapperDir)
|
|
211
140
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
function generateMainGo(customRules: RuleMetadata[]): string {
|
|
217
|
-
const tsgolintPkg = 'github.com/typescript-eslint/tsgolint'
|
|
141
|
+
// wrapper/main.go — copy from tsgolint and inject custom rules
|
|
142
|
+
const mainGo = generateMainGoFromSource(tsgolintDir, rules)
|
|
143
|
+
fs.writeFileSync(path.join(wrapperDir, 'main.go'), mainGo)
|
|
144
|
+
}
|
|
218
145
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
146
|
+
/** Copy tsgolint's main.go and transform it to only include custom rules.
|
|
147
|
+
* Two targeted string operations on the copied source:
|
|
148
|
+
* 1. Remove all /internal/rules/ import lines (built-in rule packages)
|
|
149
|
+
* 2. Replace allRules body with only custom lintcn.* entries
|
|
150
|
+
* Everything else (printDiagnostic, runMain, headless) stays untouched. */
|
|
151
|
+
function generateMainGoFromSource(tsgolintDir: string, customRules: RuleMetadata[]): string {
|
|
152
|
+
const mainGoPath = path.join(tsgolintDir, 'cmd', 'tsgolint', 'main.go')
|
|
153
|
+
const original = fs.readFileSync(mainGoPath, 'utf-8')
|
|
154
|
+
|
|
155
|
+
// 1. Remove built-in rule import lines, add lintcn import
|
|
156
|
+
const lines = original.split('\n')
|
|
157
|
+
const filtered = lines.filter((line) => {
|
|
158
|
+
return !line.includes('/internal/rules/')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Insert lintcn import before the first shim import (microsoft/typescript-go)
|
|
162
|
+
const lintcnImport = `\tlintcn "github.com/typescript-eslint/tsgolint/lintcn-rules"`
|
|
163
|
+
let shimImportIndex = -1
|
|
164
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
165
|
+
if (filtered[i].includes('microsoft/typescript-go/shim')) {
|
|
166
|
+
shimImportIndex = i
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (shimImportIndex === -1) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'Failed to find shim import in tsgolint main.go. The source layout may have changed.',
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
if (customRules.length > 0) {
|
|
176
|
+
filtered.splice(shimImportIndex, 0, lintcnImport, '')
|
|
177
|
+
}
|
|
223
178
|
|
|
224
|
-
|
|
225
|
-
const builtinEntries = BUILTIN_RULES.map(([pkg, varName]) => {
|
|
226
|
-
return `\t${pkg}.${varName},`
|
|
227
|
-
}).join('\n')
|
|
179
|
+
let mainGo = filtered.join('\n')
|
|
228
180
|
|
|
229
|
-
//
|
|
181
|
+
// 2. Replace allRules body with only custom entries
|
|
230
182
|
const customEntries = customRules.map((r) => {
|
|
231
183
|
return `\tlintcn.${r.varName},`
|
|
232
184
|
}).join('\n')
|
|
233
185
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
import (
|
|
241
|
-
\t"bufio"
|
|
242
|
-
\t"flag"
|
|
243
|
-
\t"fmt"
|
|
244
|
-
\t"math"
|
|
245
|
-
\t"os"
|
|
246
|
-
\t"runtime"
|
|
247
|
-
\t"runtime/pprof"
|
|
248
|
-
\t"runtime/trace"
|
|
249
|
-
\t"slices"
|
|
250
|
-
\t"strconv"
|
|
251
|
-
\t"strings"
|
|
252
|
-
\t"sync"
|
|
253
|
-
\t"time"
|
|
254
|
-
\t"unicode"
|
|
255
|
-
|
|
256
|
-
\t"${tsgolintPkg}/internal/diagnostic"
|
|
257
|
-
\t"${tsgolintPkg}/internal/linter"
|
|
258
|
-
\t"${tsgolintPkg}/internal/rule"
|
|
259
|
-
\t"${tsgolintPkg}/internal/utils"
|
|
260
|
-
|
|
261
|
-
${builtinImports}${lintcnImport}
|
|
262
|
-
|
|
263
|
-
\t"github.com/microsoft/typescript-go/shim/ast"
|
|
264
|
-
\t"github.com/microsoft/typescript-go/shim/bundled"
|
|
265
|
-
\t"github.com/microsoft/typescript-go/shim/core"
|
|
266
|
-
\t"github.com/microsoft/typescript-go/shim/scanner"
|
|
267
|
-
\t"github.com/microsoft/typescript-go/shim/tspath"
|
|
268
|
-
\t"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
|
|
269
|
-
\t"github.com/microsoft/typescript-go/shim/vfs/osvfs"
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
var allRules = []rule.Rule{
|
|
273
|
-
${builtinEntries}
|
|
274
|
-
${customEntries}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
var allRulesByName = make(map[string]rule.Rule, len(allRules))
|
|
278
|
-
|
|
279
|
-
func init() {
|
|
280
|
-
\tfor _, rule := range allRules {
|
|
281
|
-
\t\tallRulesByName[rule.Name] = rule
|
|
282
|
-
\t}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Below is copied from tsgolint's cmd/tsgolint/main.go.
|
|
286
|
-
// We cannot import it directly because main packages are not importable in Go.
|
|
287
|
-
|
|
288
|
-
func recordTrace(traceOut string) (func(), error) {
|
|
289
|
-
\tif traceOut != "" {
|
|
290
|
-
\t\tf, err := os.Create(traceOut)
|
|
291
|
-
\t\tif err != nil {
|
|
292
|
-
\t\t\treturn nil, fmt.Errorf("error creating trace file: %w", err)
|
|
293
|
-
\t\t}
|
|
294
|
-
\t\ttrace.Start(f)
|
|
295
|
-
\t\treturn func() {
|
|
296
|
-
\t\t\ttrace.Stop()
|
|
297
|
-
\t\t\tf.Close()
|
|
298
|
-
\t\t}, nil
|
|
299
|
-
\t}
|
|
300
|
-
\treturn func() {}, nil
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
func recordCpuprof(cpuprofOut string) (func(), error) {
|
|
304
|
-
\tif cpuprofOut != "" {
|
|
305
|
-
\t\tf, err := os.Create(cpuprofOut)
|
|
306
|
-
\t\tif err != nil {
|
|
307
|
-
\t\t\treturn nil, fmt.Errorf("error creating cpuprof file: %w", err)
|
|
308
|
-
\t\t}
|
|
309
|
-
\t\terr = pprof.StartCPUProfile(f)
|
|
310
|
-
\t\tif err != nil {
|
|
311
|
-
\t\t\treturn nil, fmt.Errorf("error starting cpu profiling: %w", err)
|
|
312
|
-
\t\t}
|
|
313
|
-
\t\treturn func() {
|
|
314
|
-
\t\t\tpprof.StopCPUProfile()
|
|
315
|
-
\t\t\tf.Close()
|
|
316
|
-
\t\t}, nil
|
|
317
|
-
\t}
|
|
318
|
-
\treturn func() {}, nil
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const spaces = " "
|
|
322
|
-
|
|
323
|
-
func printDiagnostic(d rule.RuleDiagnostic, w *bufio.Writer, comparePathOptions tspath.ComparePathsOptions) {
|
|
324
|
-
\tdiagnosticStart := d.Range.Pos()
|
|
325
|
-
\tdiagnosticEnd := d.Range.End()
|
|
326
|
-
\tdiagnosticStartLine, diagnosticStartColumn := scanner.GetECMALineAndUTF16CharacterOfPosition(d.SourceFile, diagnosticStart)
|
|
327
|
-
\tdiagnosticEndline, _ := scanner.GetECMALineAndUTF16CharacterOfPosition(d.SourceFile, diagnosticEnd)
|
|
328
|
-
\tlineMap := d.SourceFile.ECMALineMap()
|
|
329
|
-
\ttext := d.SourceFile.Text()
|
|
330
|
-
\tcodeboxStartLine := max(diagnosticStartLine-1, 0)
|
|
331
|
-
\tcodeboxEndLine := min(diagnosticEndline+1, len(lineMap)-1)
|
|
332
|
-
\tcodeboxStart := scanner.GetECMAPositionOfLineAndUTF16Character(d.SourceFile, codeboxStartLine, 0)
|
|
333
|
-
\tvar codeboxEndColumn int
|
|
334
|
-
\tif codeboxEndLine == len(lineMap)-1 {
|
|
335
|
-
\t\tcodeboxEndColumn = len(text) - int(lineMap[len(lineMap)-1])
|
|
336
|
-
\t} else {
|
|
337
|
-
\t\tcodeboxEndColumn = int(lineMap[codeboxEndLine+1]-lineMap[codeboxEndLine]) - 1
|
|
338
|
-
\t}
|
|
339
|
-
\tcodeboxEnd := scanner.GetECMAPositionOfLineAndUTF16Character(d.SourceFile, codeboxEndLine, core.UTF16Offset(codeboxEndColumn))
|
|
340
|
-
\tw.Write([]byte{' ', 0x1b, '[', '7', 'm', 0x1b, '[', '1', 'm', 0x1b, '[', '3', '8', ';', '5', ';', '3', '7', 'm', ' '})
|
|
341
|
-
\tw.WriteString(d.RuleName)
|
|
342
|
-
\tw.WriteString(" \\x1b[0m — ")
|
|
343
|
-
\tmessageLineStart := 0
|
|
344
|
-
\tfor i, char := range d.Message.Description {
|
|
345
|
-
\t\tif char == '\\n' {
|
|
346
|
-
\t\t\tw.WriteString(d.Message.Description[messageLineStart : i+1])
|
|
347
|
-
\t\t\tmessageLineStart = i + 1
|
|
348
|
-
\t\t\tw.WriteString(" \\x1b[2m│\\x1b[0m")
|
|
349
|
-
\t\t\tw.WriteString(spaces[:len(d.RuleName)+1])
|
|
350
|
-
\t\t}
|
|
351
|
-
\t}
|
|
352
|
-
\tif messageLineStart <= len(d.Message.Description) {
|
|
353
|
-
\t\tw.WriteString(d.Message.Description[messageLineStart:len(d.Message.Description)])
|
|
354
|
-
\t}
|
|
355
|
-
\tw.WriteString("\\n \\x1b[2m╭─┴──────────(\\x1b[0m \\x1b[3m\\x1b[38;5;117m")
|
|
356
|
-
\tw.WriteString(tspath.ConvertToRelativePath(d.SourceFile.FileName(), comparePathOptions))
|
|
357
|
-
\tw.WriteByte(':')
|
|
358
|
-
\tw.WriteString(strconv.Itoa(diagnosticStartLine + 1))
|
|
359
|
-
\tw.WriteByte(':')
|
|
360
|
-
\tw.WriteString(strconv.Itoa(int(diagnosticStartColumn) + 1))
|
|
361
|
-
\tw.WriteString("\\x1b[0m \\x1b[2m)─────\\x1b[0m\\n")
|
|
362
|
-
\tindentSize := math.MaxInt
|
|
363
|
-
\tline := codeboxStartLine
|
|
364
|
-
\tlineIndentCalculated := false
|
|
365
|
-
\tlastNonSpaceIndex := -1
|
|
366
|
-
\tlineStarts := make([]int, 13)
|
|
367
|
-
\tlineEnds := make([]int, 13)
|
|
368
|
-
\tif codeboxEndLine-codeboxStartLine >= len(lineEnds) {
|
|
369
|
-
\t\tw.WriteString(" \\x1b[2m│\\x1b[0m Error range is too big. Skipping code block printing.\\n \\x1b[2m╰────────────────────────────────\\x1b[0m\\n\\n")
|
|
370
|
-
\t\treturn
|
|
371
|
-
\t}
|
|
372
|
-
\tfor i, char := range text[codeboxStart:codeboxEnd] {
|
|
373
|
-
\t\tif char == '\\n' {
|
|
374
|
-
\t\t\tif line != codeboxEndLine {
|
|
375
|
-
\t\t\t\tlineIndentCalculated = false
|
|
376
|
-
\t\t\t\tlineEnds[line-codeboxStartLine] = lastNonSpaceIndex - int(lineMap[line]) + codeboxStart
|
|
377
|
-
\t\t\t\tlastNonSpaceIndex = -1
|
|
378
|
-
\t\t\t\tline++
|
|
379
|
-
\t\t\t}
|
|
380
|
-
\t\t\tcontinue
|
|
381
|
-
\t\t}
|
|
382
|
-
\t\tif !lineIndentCalculated && !unicode.IsSpace(char) {
|
|
383
|
-
\t\t\tlineIndentCalculated = true
|
|
384
|
-
\t\t\tlineStarts[line-codeboxStartLine] = i - int(lineMap[line]) + codeboxStart
|
|
385
|
-
\t\t\tindentSize = min(indentSize, lineStarts[line-codeboxStartLine])
|
|
386
|
-
\t\t}
|
|
387
|
-
\t\tif lineIndentCalculated && !unicode.IsSpace(char) {
|
|
388
|
-
\t\t\tlastNonSpaceIndex = i + 1
|
|
389
|
-
\t\t}
|
|
390
|
-
\t}
|
|
391
|
-
\tif line == codeboxEndLine {
|
|
392
|
-
\t\tlineEnds[line-codeboxStartLine] = lastNonSpaceIndex - int(lineMap[line]) + codeboxStart
|
|
393
|
-
\t}
|
|
394
|
-
\tdiagnosticHighlightActive := false
|
|
395
|
-
\tlastLineNumber := strconv.Itoa(codeboxEndLine + 1)
|
|
396
|
-
\tfor line := codeboxStartLine; line <= codeboxEndLine; line++ {
|
|
397
|
-
\t\tw.WriteString(" \\x1b[2m│ ")
|
|
398
|
-
\t\tif line == codeboxEndLine {
|
|
399
|
-
\t\t\tw.WriteString(lastLineNumber)
|
|
400
|
-
\t\t} else {
|
|
401
|
-
\t\t\tnumber := strconv.Itoa(line + 1)
|
|
402
|
-
\t\t\tif len(number) < len(lastLineNumber) {
|
|
403
|
-
\t\t\t\tw.WriteByte(' ')
|
|
404
|
-
\t\t\t}
|
|
405
|
-
\t\t\tw.WriteString(number)
|
|
406
|
-
\t\t}
|
|
407
|
-
\t\tw.WriteString(" │\\x1b[0m ")
|
|
408
|
-
\t\tlineTextStart := int(lineMap[line]) + indentSize
|
|
409
|
-
\t\tunderlineStart := max(lineTextStart, int(lineMap[line])+lineStarts[line-codeboxStartLine])
|
|
410
|
-
\t\tunderlineEnd := underlineStart
|
|
411
|
-
\t\tlineTextEnd := max(int(lineMap[line])+lineEnds[line-codeboxStartLine], lineTextStart)
|
|
412
|
-
\t\tif diagnosticHighlightActive {
|
|
413
|
-
\t\t\tunderlineEnd = lineTextEnd
|
|
414
|
-
\t\t} else if int(lineMap[line]) <= diagnosticStart && (line == len(lineMap) || diagnosticStart < int(lineMap[line+1])) {
|
|
415
|
-
\t\t\tunderlineStart = min(max(lineTextStart, diagnosticStart), lineTextEnd)
|
|
416
|
-
\t\t\tunderlineEnd = lineTextEnd
|
|
417
|
-
\t\t\tdiagnosticHighlightActive = true
|
|
418
|
-
\t\t}
|
|
419
|
-
\t\tif int(lineMap[line]) <= diagnosticEnd && (line == len(lineMap) || diagnosticEnd < int(lineMap[line+1])) {
|
|
420
|
-
\t\t\tunderlineEnd = min(max(underlineStart, diagnosticEnd), lineTextEnd)
|
|
421
|
-
\t\t\tdiagnosticHighlightActive = false
|
|
422
|
-
\t\t}
|
|
423
|
-
\t\tif underlineStart != underlineEnd {
|
|
424
|
-
\t\t\tw.WriteString(text[lineTextStart:underlineStart])
|
|
425
|
-
\t\t\tw.Write([]byte{
|
|
426
|
-
\t\t\t\t0x1b, '[', '4', 'm',
|
|
427
|
-
\t\t\t\t0x1b, '[', '4', ':', '3', 'm',
|
|
428
|
-
\t\t\t\t0x1b, '[', '5', '8', ':', '5', ':', '1', '6', '0', 'm',
|
|
429
|
-
\t\t\t\t0x1b, '[', '3', '8', ';', '5', ';', '1', '6', '0', 'm',
|
|
430
|
-
\t\t\t\t0x1b, '[', '2', '2', ';', '4', '9', 'm',
|
|
431
|
-
\t\t\t})
|
|
432
|
-
\t\t\tw.WriteString(text[underlineStart:underlineEnd])
|
|
433
|
-
\t\t\tw.Write([]byte{0x1b, '[', '0', 'm'})
|
|
434
|
-
\t\t\tw.WriteString(text[underlineEnd:lineTextEnd])
|
|
435
|
-
\t\t} else if lineTextStart != lineTextEnd {
|
|
436
|
-
\t\t\tw.WriteString(text[lineTextStart:lineTextEnd])
|
|
437
|
-
\t\t}
|
|
438
|
-
\t\tw.WriteByte('\\n')
|
|
439
|
-
\t}
|
|
440
|
-
\tw.WriteString(" \\x1b[2m╰────────────────────────────────\\x1b[0m\\n\\n")
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const usage = \` lintcn - type-aware TypeScript linter with custom rules
|
|
186
|
+
const allRulesPattern = /var allRules = \[]rule\.Rule\{[^}]*\}/s
|
|
187
|
+
if (!allRulesPattern.test(mainGo)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
'Failed to find allRules slice in tsgolint main.go. The source layout may have changed.',
|
|
190
|
+
)
|
|
191
|
+
}
|
|
444
192
|
|
|
445
|
-
|
|
446
|
-
|
|
193
|
+
mainGo = mainGo.replace(
|
|
194
|
+
allRulesPattern,
|
|
195
|
+
`var allRules = []rule.Rule{\n${customEntries}\n}`,
|
|
196
|
+
)
|
|
447
197
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
\`
|
|
198
|
+
// assertion: verify custom rules are present
|
|
199
|
+
if (customRules.length > 0 && !mainGo.includes(`lintcn.${customRules[0].varName}`)) {
|
|
200
|
+
throw new Error('Custom rule injection verification failed.')
|
|
201
|
+
}
|
|
453
202
|
|
|
454
|
-
|
|
455
|
-
\tflag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
|
|
456
|
-
\tvar (
|
|
457
|
-
\t\thelp bool
|
|
458
|
-
\t\ttsconfig string
|
|
459
|
-
\t\tlistFiles bool
|
|
460
|
-
\t\ttraceOut string
|
|
461
|
-
\t\tcpuprofOut string
|
|
462
|
-
\t\tsingleThreaded bool
|
|
463
|
-
\t)
|
|
464
|
-
\tflag.StringVar(&tsconfig, "tsconfig", "", "which tsconfig to use")
|
|
465
|
-
\tflag.BoolVar(&listFiles, "list-files", false, "list matched files")
|
|
466
|
-
\tflag.BoolVar(&help, "help", false, "show help")
|
|
467
|
-
\tflag.BoolVar(&help, "h", false, "show help")
|
|
468
|
-
\tflag.StringVar(&traceOut, "trace", "", "file to put trace to")
|
|
469
|
-
\tflag.StringVar(&cpuprofOut, "cpuprof", "", "file to put cpu profiling to")
|
|
470
|
-
\tflag.BoolVar(&singleThreaded, "singleThreaded", false, "run in single threaded mode")
|
|
471
|
-
\tflag.Parse()
|
|
472
|
-
\tif help {
|
|
473
|
-
\t\tflag.Usage()
|
|
474
|
-
\t\treturn 0
|
|
475
|
-
\t}
|
|
476
|
-
\ttimeBefore := time.Now()
|
|
477
|
-
\tif done, err := recordTrace(traceOut); err != nil {
|
|
478
|
-
\t\tos.Stderr.WriteString(err.Error())
|
|
479
|
-
\t\treturn 1
|
|
480
|
-
\t} else {
|
|
481
|
-
\t\tdefer done()
|
|
482
|
-
\t}
|
|
483
|
-
\tif done, err := recordCpuprof(cpuprofOut); err != nil {
|
|
484
|
-
\t\tos.Stderr.WriteString(err.Error())
|
|
485
|
-
\t\treturn 1
|
|
486
|
-
\t} else {
|
|
487
|
-
\t\tdefer done()
|
|
488
|
-
\t}
|
|
489
|
-
\tcurrentDirectory, err := os.Getwd()
|
|
490
|
-
\tif err != nil {
|
|
491
|
-
\t\tfmt.Fprintf(os.Stderr, "error getting current directory: %v\\n", err)
|
|
492
|
-
\t\treturn 1
|
|
493
|
-
\t}
|
|
494
|
-
\tcurrentDirectory = tspath.NormalizePath(currentDirectory)
|
|
495
|
-
\tfs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
|
|
496
|
-
\tvar configFileName string
|
|
497
|
-
\tif tsconfig == "" {
|
|
498
|
-
\t\tconfigFileName = tspath.ResolvePath(currentDirectory, "tsconfig.json")
|
|
499
|
-
\t\tif !fs.FileExists(configFileName) {
|
|
500
|
-
\t\t\tfs = utils.NewOverlayVFS(fs, map[string]string{
|
|
501
|
-
\t\t\t\tconfigFileName: "{}",
|
|
502
|
-
\t\t\t})
|
|
503
|
-
\t\t}
|
|
504
|
-
\t} else {
|
|
505
|
-
\t\tconfigFileName = tspath.ResolvePath(currentDirectory, tsconfig)
|
|
506
|
-
\t\tif !fs.FileExists(configFileName) {
|
|
507
|
-
\t\t\tfmt.Fprintf(os.Stderr, "error: tsconfig %q doesn't exist", tsconfig)
|
|
508
|
-
\t\t\treturn 1
|
|
509
|
-
\t\t}
|
|
510
|
-
\t}
|
|
511
|
-
\tcurrentDirectory = tspath.GetDirectoryPath(configFileName)
|
|
512
|
-
\thost := utils.CreateCompilerHost(currentDirectory, fs)
|
|
513
|
-
\tcomparePathOptions := tspath.ComparePathsOptions{
|
|
514
|
-
\t\tCurrentDirectory: host.GetCurrentDirectory(),
|
|
515
|
-
\t\tUseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
|
|
516
|
-
\t}
|
|
517
|
-
\tprogram, _, err := utils.CreateProgram(singleThreaded, fs, currentDirectory, configFileName, host, false)
|
|
518
|
-
\tif err != nil {
|
|
519
|
-
\t\tfmt.Fprintf(os.Stderr, "error creating TS program: %v", err)
|
|
520
|
-
\t\treturn 1
|
|
521
|
-
\t}
|
|
522
|
-
\tif program == nil {
|
|
523
|
-
\t\tfmt.Fprintf(os.Stderr, "error creating TS program")
|
|
524
|
-
\t\treturn 1
|
|
525
|
-
\t}
|
|
526
|
-
\tfiles := []*ast.SourceFile{}
|
|
527
|
-
\tcwdPath := string(tspath.ToPath("", currentDirectory, program.Host().FS().UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator())
|
|
528
|
-
\tvar matchedFiles strings.Builder
|
|
529
|
-
\tfor _, file := range program.SourceFiles() {
|
|
530
|
-
\t\tp := string(file.Path())
|
|
531
|
-
\t\tif strings.Contains(p, "/node_modules/") {
|
|
532
|
-
\t\t\tcontinue
|
|
533
|
-
\t\t}
|
|
534
|
-
\t\tif fileName, matched := strings.CutPrefix(p, cwdPath); matched {
|
|
535
|
-
\t\t\tif listFiles {
|
|
536
|
-
\t\t\t\tmatchedFiles.WriteString("Found file: ")
|
|
537
|
-
\t\t\t\tmatchedFiles.WriteString(fileName)
|
|
538
|
-
\t\t\t\tmatchedFiles.WriteByte('\\n')
|
|
539
|
-
\t\t\t}
|
|
540
|
-
\t\t\tfiles = append(files, file)
|
|
541
|
-
\t\t}
|
|
542
|
-
\t}
|
|
543
|
-
\tif listFiles {
|
|
544
|
-
\t\tos.Stdout.WriteString(matchedFiles.String())
|
|
545
|
-
\t}
|
|
546
|
-
\tslices.SortFunc(files, func(a *ast.SourceFile, b *ast.SourceFile) int {
|
|
547
|
-
\t\treturn len(b.Text()) - len(a.Text())
|
|
548
|
-
\t})
|
|
549
|
-
\tvar wg sync.WaitGroup
|
|
550
|
-
\tdiagnosticsChan := make(chan rule.RuleDiagnostic, 4096)
|
|
551
|
-
\terrorsCount := 0
|
|
552
|
-
\twg.Go(func() {
|
|
553
|
-
\t\tw := bufio.NewWriterSize(os.Stdout, 4096*100)
|
|
554
|
-
\t\tdefer w.Flush()
|
|
555
|
-
\t\tfor d := range diagnosticsChan {
|
|
556
|
-
\t\t\terrorsCount++
|
|
557
|
-
\t\t\tif errorsCount == 1 {
|
|
558
|
-
\t\t\t\tw.WriteByte('\\n')
|
|
559
|
-
\t\t\t}
|
|
560
|
-
\t\t\tprintDiagnostic(d, w, comparePathOptions)
|
|
561
|
-
\t\t\tif w.Available() < 4096 {
|
|
562
|
-
\t\t\t\tw.Flush()
|
|
563
|
-
\t\t\t}
|
|
564
|
-
\t\t}
|
|
565
|
-
\t})
|
|
566
|
-
\terr = linter.RunLinterOnProgram(
|
|
567
|
-
\t\tutils.GetLogLevel(),
|
|
568
|
-
\t\tprogram,
|
|
569
|
-
\t\tfiles,
|
|
570
|
-
\t\truntime.GOMAXPROCS(0),
|
|
571
|
-
\t\tfunc(sourceFile *ast.SourceFile) []linter.ConfiguredRule {
|
|
572
|
-
\t\t\treturn utils.Map(allRules, func(r rule.Rule) linter.ConfiguredRule {
|
|
573
|
-
\t\t\t\treturn linter.ConfiguredRule{
|
|
574
|
-
\t\t\t\t\tName: r.Name,
|
|
575
|
-
\t\t\t\t\tRun: func(ctx rule.RuleContext) rule.RuleListeners {
|
|
576
|
-
\t\t\t\t\t\treturn r.Run(ctx, nil)
|
|
577
|
-
\t\t\t\t\t},
|
|
578
|
-
\t\t\t\t}
|
|
579
|
-
\t\t\t})
|
|
580
|
-
\t\t},
|
|
581
|
-
\t\tfunc(d rule.RuleDiagnostic) {
|
|
582
|
-
\t\t\tdiagnosticsChan <- d
|
|
583
|
-
\t\t},
|
|
584
|
-
\t\tfunc(d diagnostic.Internal) {},
|
|
585
|
-
\t\tlinter.Fixes{
|
|
586
|
-
\t\t\tFix: true,
|
|
587
|
-
\t\t\tFixSuggestions: true,
|
|
588
|
-
\t\t},
|
|
589
|
-
\t\tlinter.TypeErrors{
|
|
590
|
-
\t\t\tReportSyntactic: false,
|
|
591
|
-
\t\t\tReportSemantic: false,
|
|
592
|
-
\t\t},
|
|
593
|
-
\t)
|
|
594
|
-
\tclose(diagnosticsChan)
|
|
595
|
-
\tif err != nil {
|
|
596
|
-
\t\tfmt.Fprintf(os.Stderr, "error running linter: %v\\n", err)
|
|
597
|
-
\t\treturn 1
|
|
598
|
-
\t}
|
|
599
|
-
\twg.Wait()
|
|
600
|
-
\terrorsColor := "\\x1b[1m"
|
|
601
|
-
\tif errorsCount == 0 {
|
|
602
|
-
\t\terrorsColor = "\\x1b[1;32m"
|
|
603
|
-
\t}
|
|
604
|
-
\terrorsText := "errors"
|
|
605
|
-
\tif errorsCount == 1 {
|
|
606
|
-
\t\terrorsText = "error"
|
|
607
|
-
\t}
|
|
608
|
-
\tfilesText := "files"
|
|
609
|
-
\tif len(files) == 1 {
|
|
610
|
-
\t\tfilesText = "file"
|
|
611
|
-
\t}
|
|
612
|
-
\trulesText := "rules"
|
|
613
|
-
\tif len(allRules) == 1 {
|
|
614
|
-
\t\trulesText = "rule"
|
|
615
|
-
\t}
|
|
616
|
-
\tthreadsCount := 1
|
|
617
|
-
\tif !singleThreaded {
|
|
618
|
-
\t\tthreadsCount = runtime.GOMAXPROCS(0)
|
|
619
|
-
\t}
|
|
620
|
-
\tfmt.Fprintf(
|
|
621
|
-
\t\tos.Stdout,
|
|
622
|
-
\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",
|
|
623
|
-
\t\terrorsColor,
|
|
624
|
-
\t\terrorsCount,
|
|
625
|
-
\t\terrorsText,
|
|
626
|
-
\t\tlen(files),
|
|
627
|
-
\t\tfilesText,
|
|
628
|
-
\t\tlen(allRules),
|
|
629
|
-
\t\trulesText,
|
|
630
|
-
\t\ttime.Since(timeBefore).Round(time.Millisecond),
|
|
631
|
-
\t\tthreadsCount,
|
|
632
|
-
\t)
|
|
633
|
-
\treturn 0
|
|
203
|
+
return mainGo
|
|
634
204
|
}
|
|
635
205
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
206
|
+
/** Copy all supporting .go files from cmd/tsgolint/ into the wrapper dir.
|
|
207
|
+
* main.go is generated separately with custom rules injected. */
|
|
208
|
+
export function copyTsgolintCmdFiles(tsgolintDir: string, wrapperDir: string): void {
|
|
209
|
+
const cmdDir = path.join(tsgolintDir, 'cmd', 'tsgolint')
|
|
210
|
+
const files = fs.readdirSync(cmdDir).filter((f) => {
|
|
211
|
+
return f.endsWith('.go') && f !== 'main.go' && !f.endsWith('_test.go')
|
|
212
|
+
})
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
fs.copyFileSync(path.join(cmdDir, file), path.join(wrapperDir, file))
|
|
215
|
+
}
|
|
640
216
|
}
|