@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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +26 -4
  3. package/lib/index.js.map +1 -1
  4. package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
  5. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
  6. package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
  7. package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
  8. package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
  9. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
  10. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
  11. package/linthost/compile.go +9 -4
  12. package/linthost/config.go +18 -14
  13. package/linthost/contrib_adapter.go +57 -0
  14. package/linthost/dispatch.go +5 -1
  15. package/linthost/display_width.go +179 -41
  16. package/linthost/engine.go +120 -4
  17. package/linthost/flags_gen.go +4 -4
  18. package/linthost/hints.go +207 -0
  19. package/linthost/host.go +68 -0
  20. package/linthost/lsp.go +148 -29
  21. package/linthost/print_dispatch.go +24 -0
  22. package/linthost/print_nodes_array.go +88 -15
  23. package/linthost/print_nodes_call.go +38 -31
  24. package/linthost/print_nodes_control_flow.go +319 -0
  25. package/linthost/print_nodes_function.go +140 -13
  26. package/linthost/print_nodes_list.go +22 -10
  27. package/linthost/project_engine.go +18 -1
  28. package/linthost/project_rules.go +47 -0
  29. package/linthost/rule_docs.go +114 -0
  30. package/linthost/rules_boundaries.go +80 -2
  31. package/linthost/rules_format_bracket_spacing.go +18 -6
  32. package/linthost/rules_format_clause_join.go +11 -8
  33. package/linthost/rules_format_indent.go +155 -4
  34. package/linthost/rules_format_print_width.go +12 -35
  35. package/linthost/rules_format_quote_props.go +88 -27
  36. package/linthost/rules_format_statement_split.go +81 -0
  37. package/linthost/rules_format_trailing_comma.go +62 -94
  38. package/linthost/rules_gap.go +26 -17
  39. package/linthost/rules_jsdoc.go +54 -0
  40. package/linthost/rules_logic.go +30 -15
  41. package/linthost/rules_no_redeclare.go +12 -3
  42. package/linthost/rules_promise.go +37 -15
  43. package/linthost/rules_regexp.go +443 -49
  44. package/linthost/rules_security.go +243 -4
  45. package/linthost/rules_solid.go +430 -10
  46. package/linthost/rules_storybook.go +255 -10
  47. package/linthost/rules_suggestions.go +60 -39
  48. package/linthost/rules_ts.go +7 -0
  49. package/linthost/rules_ts_async.go +80 -2
  50. package/linthost/rules_ts_extra.go +21 -7
  51. package/linthost/serve.go +318 -0
  52. package/linthost/width_tables_gen.go +219 -0
  53. package/package.json +2 -2
  54. package/rule/hint.go +142 -0
  55. package/rule/rule.go +197 -1
  56. package/src/index.ts +35 -4
  57. package/src/structures/format/ITtscLintFormat.ts +14 -10
  58. package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
  59. package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
  60. package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
  61. package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
  62. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
  63. 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 !isSecurityStaticExpression(expr.Arguments.Nodes[0], bindings, nil) {
162
- ctx.Report(node, "Found new Buffer with a non-literal argument.")
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 && prop == "pseudoRandomBytes" && identifierText(obj) == "crypto" {
316
- ctx.Report(node, "Found crypto.pseudoRandomBytes which is not cryptographically strong.")
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{}