@ttsc/lint 0.19.3 → 0.20.1
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/README.md +1 -1
- package/lib/index.js +26 -4
- package/lib/index.js.map +1 -1
- package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
- package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
- package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
- package/linthost/compile.go +9 -4
- package/linthost/config.go +18 -14
- package/linthost/contrib_adapter.go +57 -0
- package/linthost/dispatch.go +5 -1
- package/linthost/display_width.go +179 -41
- package/linthost/engine.go +120 -4
- package/linthost/flags_gen.go +4 -4
- package/linthost/hints.go +207 -0
- package/linthost/host.go +68 -0
- package/linthost/lsp.go +148 -29
- package/linthost/print_dispatch.go +24 -0
- package/linthost/print_nodes_array.go +88 -15
- package/linthost/print_nodes_call.go +38 -31
- package/linthost/print_nodes_control_flow.go +319 -0
- package/linthost/print_nodes_function.go +140 -13
- package/linthost/print_nodes_list.go +22 -10
- package/linthost/project_engine.go +18 -1
- package/linthost/project_rules.go +47 -0
- package/linthost/rule_docs.go +114 -0
- package/linthost/rules_boundaries.go +80 -2
- package/linthost/rules_format_bracket_spacing.go +18 -6
- package/linthost/rules_format_clause_join.go +11 -8
- package/linthost/rules_format_indent.go +155 -4
- package/linthost/rules_format_print_width.go +12 -35
- package/linthost/rules_format_quote_props.go +88 -27
- package/linthost/rules_format_statement_split.go +81 -0
- package/linthost/rules_format_trailing_comma.go +62 -94
- package/linthost/rules_gap.go +26 -17
- package/linthost/rules_jsdoc.go +54 -0
- package/linthost/rules_logic.go +30 -15
- package/linthost/rules_no_redeclare.go +12 -3
- package/linthost/rules_promise.go +37 -15
- package/linthost/rules_regexp.go +443 -49
- package/linthost/rules_security.go +243 -4
- package/linthost/rules_solid.go +430 -10
- package/linthost/rules_storybook.go +255 -10
- package/linthost/rules_suggestions.go +60 -39
- package/linthost/rules_ts.go +7 -0
- package/linthost/rules_ts_async.go +80 -2
- package/linthost/rules_ts_extra.go +21 -7
- package/linthost/serve.go +318 -0
- package/linthost/width_tables_gen.go +219 -0
- package/package.json +2 -2
- package/rule/hint.go +142 -0
- package/rule/rule.go +197 -1
- package/src/index.ts +35 -4
- package/src/structures/format/ITtscLintFormat.ts +14 -10
- package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
- package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
- package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
- package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
|
@@ -158,9 +158,66 @@ func (securityDetectNewBuffer) Check(ctx *Context, node *shimast.Node) {
|
|
|
158
158
|
return
|
|
159
159
|
}
|
|
160
160
|
bindings := ctx.securityBindings()
|
|
161
|
-
if
|
|
162
|
-
|
|
161
|
+
if isSecurityStaticExpression(expr.Arguments.Nodes[0], bindings, nil) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
message := "Found new Buffer with a non-literal argument."
|
|
165
|
+
replacements := securityNewBufferReplacements(ctx.File, node, expr)
|
|
166
|
+
if len(replacements) == 0 {
|
|
167
|
+
ctx.Report(node, message)
|
|
168
|
+
return
|
|
163
169
|
}
|
|
170
|
+
ctx.ReportFixSuggestions(node, message, nil, replacements...)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// securityNewBufferReplacements offers the three successors `new Buffer` was
|
|
174
|
+
// split into, and imposes none of them.
|
|
175
|
+
//
|
|
176
|
+
// Which one is correct depends on what the argument turns out to be —
|
|
177
|
+
// `Buffer.from` for a string, array, or buffer; `Buffer.alloc` for a
|
|
178
|
+
// zero-filled size; `Buffer.allocUnsafe` for an uninitialized size — and this
|
|
179
|
+
// rule fires precisely when the argument is not a literal, so the source
|
|
180
|
+
// cannot say. Picking one automatically is how the deprecated constructor's
|
|
181
|
+
// original hazard comes back: applying `Buffer.allocUnsafe` to what was really
|
|
182
|
+
// a string silently allocates uninitialized heap memory.
|
|
183
|
+
//
|
|
184
|
+
// Each edit removes `new` and replaces the callee while leaving the argument
|
|
185
|
+
// list untouched. It consumes only whitespace immediately after `new`, so a
|
|
186
|
+
// comment between `new` and `Buffer` stays in the source. Returns nil when
|
|
187
|
+
// those token ranges cannot be bounded, which downgrades the caller to a plain
|
|
188
|
+
// diagnostic.
|
|
189
|
+
func securityNewBufferReplacements(
|
|
190
|
+
file *shimast.SourceFile,
|
|
191
|
+
node *shimast.Node,
|
|
192
|
+
expr *shimast.NewExpression,
|
|
193
|
+
) []Suggestion {
|
|
194
|
+
pos, _ := tokenRange(file, node)
|
|
195
|
+
calleePos, calleeEnd := tokenRange(file, expr.Expression)
|
|
196
|
+
if pos < 0 || expr.Expression == nil || calleePos < pos+len("new") {
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
src := file.Text()
|
|
200
|
+
newEnd := pos + len("new")
|
|
201
|
+
if newEnd > len(src) || src[pos:newEnd] != "new" || calleeEnd <= calleePos {
|
|
202
|
+
return nil
|
|
203
|
+
}
|
|
204
|
+
removeEnd := newEnd
|
|
205
|
+
for removeEnd < calleePos &&
|
|
206
|
+
(src[removeEnd] == ' ' || src[removeEnd] == '\t' || src[removeEnd] == '\r' || src[removeEnd] == '\n') {
|
|
207
|
+
removeEnd++
|
|
208
|
+
}
|
|
209
|
+
successors := []string{"Buffer.from", "Buffer.alloc", "Buffer.allocUnsafe"}
|
|
210
|
+
suggestions := make([]Suggestion, 0, len(successors))
|
|
211
|
+
for _, successor := range successors {
|
|
212
|
+
suggestions = append(suggestions, Suggestion{
|
|
213
|
+
Title: "Replace with `" + successor + "`.",
|
|
214
|
+
Edits: []TextEdit{
|
|
215
|
+
{Pos: pos, End: removeEnd, Text: ""},
|
|
216
|
+
{Pos: calleePos, End: calleeEnd, Text: successor},
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
return suggestions
|
|
164
221
|
}
|
|
165
222
|
|
|
166
223
|
type securityDetectNoCSRFBeforeMethodOverride struct{}
|
|
@@ -307,14 +364,196 @@ type securityDetectPseudoRandomBytes struct{}
|
|
|
307
364
|
func (securityDetectPseudoRandomBytes) Name() string {
|
|
308
365
|
return securityRulePrefix + "detect-pseudoRandomBytes"
|
|
309
366
|
}
|
|
367
|
+
func (securityDetectPseudoRandomBytes) NeedsTypeChecker() bool {
|
|
368
|
+
return true
|
|
369
|
+
}
|
|
310
370
|
func (securityDetectPseudoRandomBytes) Visits() []shimast.Kind {
|
|
311
371
|
return []shimast.Kind{shimast.KindPropertyAccessExpression}
|
|
312
372
|
}
|
|
313
373
|
func (securityDetectPseudoRandomBytes) Check(ctx *Context, node *shimast.Node) {
|
|
314
374
|
obj, prop, ok := propertyAccessParts(node)
|
|
315
|
-
if ok
|
|
316
|
-
|
|
375
|
+
if !ok || prop != "pseudoRandomBytes" || identifierText(obj) != "crypto" {
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
message := "Found crypto.pseudoRandomBytes which is not cryptographically strong. Use `crypto.randomBytes` instead."
|
|
379
|
+
edits := securityRandomBytesEdits(ctx.File, node)
|
|
380
|
+
if len(edits) == 0 {
|
|
381
|
+
ctx.Report(node, message)
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
bindings := ctx.securityBindings()
|
|
385
|
+
module := bindings.Modules[identifierText(obj)]
|
|
386
|
+
if isNodeCryptoModule(module) && securityValueBindingModule(ctx, obj) == module {
|
|
387
|
+
ctx.ReportFix(node, message, edits...)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
ctx.ReportSuggestion(node, message, "Replace with `crypto.randomBytes`.", edits...)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// securityRandomBytesEdits renames the member being accessed to `randomBytes`
|
|
394
|
+
// and touches nothing else.
|
|
395
|
+
//
|
|
396
|
+
// The caller imposes the rewrite only when the existing security binding table
|
|
397
|
+
// and the TypeScript checker agree that this exact object reference resolves
|
|
398
|
+
// to Node's `crypto` module. The identity check matters when an imported name
|
|
399
|
+
// is shadowed by a parameter or block-local declaration. A name-only match can
|
|
400
|
+
// be a local application object, so that shape receives the same edit as an
|
|
401
|
+
// opt-in suggestion instead.
|
|
402
|
+
//
|
|
403
|
+
// Only the member name is replaced, so `crypto.pseudoRandomBytes` passed
|
|
404
|
+
// around as a value — not just called — is repaired the same way. Returns nil
|
|
405
|
+
// when the name token cannot be located, which downgrades the caller to a
|
|
406
|
+
// plain diagnostic.
|
|
407
|
+
func securityRandomBytesEdits(file *shimast.SourceFile, node *shimast.Node) []TextEdit {
|
|
408
|
+
access := node.AsPropertyAccessExpression()
|
|
409
|
+
if access == nil {
|
|
410
|
+
return nil
|
|
411
|
+
}
|
|
412
|
+
pos, end := tokenRange(file, access.Name())
|
|
413
|
+
if pos < 0 {
|
|
414
|
+
return nil
|
|
415
|
+
}
|
|
416
|
+
return []TextEdit{{Pos: pos, End: end, Text: "randomBytes"}}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
func isNodeCryptoModule(module string) bool {
|
|
420
|
+
return module == "crypto" || module == "node:crypto"
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// securityValueBindingModule resolves an identifier in value position and
|
|
424
|
+
// returns the module named by that exact binding's declaration.
|
|
425
|
+
//
|
|
426
|
+
// The security binding table is intentionally file-wide and text-keyed because
|
|
427
|
+
// most security rules are syntax-only. That table can establish that a file
|
|
428
|
+
// declares a Node module binding named `crypto`, but it cannot distinguish a
|
|
429
|
+
// same-named parameter or block-local declaration at one use site. An
|
|
430
|
+
// automatic edit needs the stronger statement, so this helper asks the
|
|
431
|
+
// checker whether the use resolves to the import or require declaration. A
|
|
432
|
+
// CommonJS script can resolve the use to lib.dom's global `crypto` even while a
|
|
433
|
+
// top-level `const crypto = require("crypto")` is the runtime binding; that
|
|
434
|
+
// shape is accepted only when the checker finds no different declaration in
|
|
435
|
+
// the current file. Missing checker data or any other declaration shape is not
|
|
436
|
+
// proof and returns the empty string.
|
|
437
|
+
func securityValueBindingModule(ctx *Context, identifier *shimast.Node) string {
|
|
438
|
+
if ctx == nil || ctx.File == nil {
|
|
439
|
+
return ""
|
|
440
|
+
}
|
|
441
|
+
target := canonicalValueSymbol(ctx, identifier)
|
|
442
|
+
if target == nil {
|
|
443
|
+
return ""
|
|
444
|
+
}
|
|
445
|
+
if module := securityRequiredModuleFromDeclaration(target.ValueDeclaration); module != "" {
|
|
446
|
+
return module
|
|
447
|
+
}
|
|
448
|
+
for _, declaration := range target.Declarations {
|
|
449
|
+
if module := securityRequiredModuleFromDeclaration(declaration); module != "" {
|
|
450
|
+
return module
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module := ""
|
|
455
|
+
topLevelRequire := ""
|
|
456
|
+
walkDescendants(ctx.File.AsNode(), func(candidate *shimast.Node) {
|
|
457
|
+
if module != "" || candidate == nil {
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
switch candidate.Kind {
|
|
461
|
+
case shimast.KindImportDeclaration:
|
|
462
|
+
imported := candidate.AsImportDeclaration()
|
|
463
|
+
if imported == nil || imported.ImportClause == nil {
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
clause := imported.ImportClause.AsImportClause()
|
|
467
|
+
if clause == nil {
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
if canonicalValueSymbol(ctx, clause.Name()) == target {
|
|
471
|
+
module = stringLiteralText(imported.ModuleSpecifier)
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
if clause.NamedBindings == nil || clause.NamedBindings.Kind != shimast.KindNamespaceImport {
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
namespace := clause.NamedBindings.AsNamespaceImport()
|
|
478
|
+
if namespace != nil && canonicalValueSymbol(ctx, namespace.Name()) == target {
|
|
479
|
+
module = stringLiteralText(imported.ModuleSpecifier)
|
|
480
|
+
}
|
|
481
|
+
case shimast.KindVariableDeclaration:
|
|
482
|
+
variable := candidate.AsVariableDeclaration()
|
|
483
|
+
if variable == nil {
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
name := variable.Name()
|
|
487
|
+
required, ok := requireExpressionModule(variable.Initializer)
|
|
488
|
+
if !ok || identifierText(name) != identifierText(identifier) {
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
if name != nil && name.Kind == shimast.KindIdentifier &&
|
|
492
|
+
canonicalValueSymbol(ctx, name) == target {
|
|
493
|
+
module = required
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
if securityTopLevelVariableDeclaration(candidate) {
|
|
497
|
+
topLevelRequire = required
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
if module == "" && topLevelRequire != "" &&
|
|
502
|
+
!securitySymbolHasDeclarationInFile(target, ctx.File) {
|
|
503
|
+
module = topLevelRequire
|
|
317
504
|
}
|
|
505
|
+
return module
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func securityRequiredModuleFromDeclaration(declaration *shimast.Node) string {
|
|
509
|
+
for current := declaration; current != nil && current.Kind != shimast.KindSourceFile; current = current.Parent {
|
|
510
|
+
if current.Kind != shimast.KindVariableDeclaration {
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
variable := current.AsVariableDeclaration()
|
|
514
|
+
if variable == nil {
|
|
515
|
+
return ""
|
|
516
|
+
}
|
|
517
|
+
module, _ := requireExpressionModule(variable.Initializer)
|
|
518
|
+
return module
|
|
519
|
+
}
|
|
520
|
+
return ""
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
func securityTopLevelVariableDeclaration(declaration *shimast.Node) bool {
|
|
524
|
+
if declaration == nil || declaration.Kind != shimast.KindVariableDeclaration {
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
list := declaration.Parent
|
|
528
|
+
if list == nil || list.Kind != shimast.KindVariableDeclarationList {
|
|
529
|
+
return false
|
|
530
|
+
}
|
|
531
|
+
statement := list.Parent
|
|
532
|
+
return statement != nil && statement.Kind == shimast.KindVariableStatement &&
|
|
533
|
+
statement.Parent != nil && statement.Parent.Kind == shimast.KindSourceFile
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
func securitySymbolHasDeclarationInFile(symbol *shimast.Symbol, file *shimast.SourceFile) bool {
|
|
537
|
+
if symbol == nil || file == nil {
|
|
538
|
+
return false
|
|
539
|
+
}
|
|
540
|
+
belongs := func(declaration *shimast.Node) bool {
|
|
541
|
+
for current := declaration; current != nil; current = current.Parent {
|
|
542
|
+
if current.Kind == shimast.KindSourceFile {
|
|
543
|
+
return current == file.AsNode()
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return false
|
|
547
|
+
}
|
|
548
|
+
if belongs(symbol.ValueDeclaration) {
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
for _, declaration := range symbol.Declarations {
|
|
552
|
+
if belongs(declaration) {
|
|
553
|
+
return true
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return false
|
|
318
557
|
}
|
|
319
558
|
|
|
320
559
|
type securityDetectUnsafeRegex struct{}
|