@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
@@ -63,6 +63,21 @@ import "strings"
63
63
  // makes the close brace of a reflowed list land at `BaseIndent`
64
64
  // while its children sit at `BaseIndent + indentUnit`.
65
65
  //
66
+ // TrailingComma mirrors Prettier's `trailingComma` setting and controls
67
+ // which broken-list shapes the printer emits a trailing comma on:
68
+ //
69
+ // - "all" (default) every multi-line list gets one.
70
+ // - "es5" arrays, objects, named imports / exports get one;
71
+ // call arguments, parameter lists, and type-level
72
+ // lists do not — those positions accepted trailing
73
+ // commas only in ES2017+, so es5 mode skips them
74
+ // to match Prettier and avoid oscillating against
75
+ // the formatter on every cascade pass.
76
+ // - "none" no list gets one.
77
+ //
78
+ // An empty string is treated as "all", which keeps `DefaultPrintOptions()`
79
+ // callers and tests that pre-date this field on their original behavior.
80
+ //
66
81
  // Defaults of 0 keep the engine usable for top-of-file reflow without
67
82
  // a wrapper.
68
83
  type PrintOptions struct {
@@ -70,14 +85,17 @@ type PrintOptions struct {
70
85
  TabWidth int
71
86
  UseTabs bool
72
87
  EndOfLine string
88
+ TrailingComma string
73
89
  StartingColumn int
74
90
  BaseIndent int
75
91
  }
76
92
 
77
93
  // DefaultPrintOptions returns the Prettier defaults: 80-column lines,
78
- // 2-space indentation, LF line terminators.
94
+ // 2-space indentation, LF line terminators, trailing commas on every
95
+ // multi-line list (the `trailingComma: "all"` default Prettier adopted
96
+ // in v2).
79
97
  func DefaultPrintOptions() PrintOptions {
80
- return PrintOptions{PrintWidth: 80, TabWidth: 2, UseTabs: false, EndOfLine: "lf"}
98
+ return PrintOptions{PrintWidth: 80, TabWidth: 2, UseTabs: false, EndOfLine: "lf", TrailingComma: "all"}
81
99
  }
82
100
 
83
101
  // printMode is the per-group choice made by the fit measurement.
@@ -191,13 +209,30 @@ func Print(doc Doc, opts PrintOptions) string {
191
209
  child := Concat(top.doc.Children...)
192
210
  stack = append(stack, printFrame{indent: col, mode: top.mode, doc: child})
193
211
  case docGroup:
194
- // Try flat unless the group contains a hardline.
212
+ // Try flat unless the group is forced broken or its flat form
213
+ // would overflow the remaining width.
195
214
  child := Concat(top.doc.Children...)
196
- if fits(child, opts.PrintWidth-col, top.indent) {
215
+ if !top.doc.Break && fits(child, opts.PrintWidth-col, top.indent) {
197
216
  stack = append(stack, printFrame{indent: top.indent, mode: modeFlat, doc: child})
198
217
  } else {
199
218
  stack = append(stack, printFrame{indent: top.indent, mode: modeBreak, doc: child})
200
219
  }
220
+ case docConditionalGroup:
221
+ // Render the first option whose first line fits the remaining
222
+ // width; fall back to the last option when none do. fitsFirstLine
223
+ // measures only up to an option's first break, so an option whose
224
+ // later lines wrap — a hugged callback body — is still eligible.
225
+ options := top.doc.Children
226
+ if len(options) > 0 {
227
+ chosen := options[len(options)-1]
228
+ for i := 0; i < len(options)-1; i++ {
229
+ if fitsFirstLine(options[i], opts.PrintWidth-col) {
230
+ chosen = options[i]
231
+ break
232
+ }
233
+ }
234
+ stack = append(stack, printFrame{indent: top.indent, mode: top.mode, doc: chosen})
235
+ }
201
236
  case docIfBreak:
202
237
  pick := top.doc.Children[1] // flat
203
238
  if top.mode == modeBreak {
@@ -306,6 +341,11 @@ func fits(doc Doc, remaining int, indent int) bool {
306
341
  stack = append(stack, frame{mode: top.mode, doc: top.doc.Children[i]})
307
342
  }
308
343
  case docGroup:
344
+ // A forced-broken group cannot contribute a flat layout; treat it
345
+ // like a hard line break for the enclosing measurement.
346
+ if top.doc.Break {
347
+ return false
348
+ }
309
349
  // Measure nested groups in flat mode too — that is the
310
350
  // standard Wadler choice: the outer group's "does my
311
351
  // flat form fit" question is answered by treating every
@@ -313,6 +353,11 @@ func fits(doc Doc, remaining int, indent int) bool {
313
353
  for i := len(top.doc.Children) - 1; i >= 0; i-- {
314
354
  stack = append(stack, frame{mode: modeFlat, doc: top.doc.Children[i]})
315
355
  }
356
+ case docConditionalGroup:
357
+ // A conditional group's flat form is its first (flattest) option.
358
+ if len(top.doc.Children) > 0 {
359
+ stack = append(stack, frame{mode: top.mode, doc: top.doc.Children[0]})
360
+ }
316
361
  case docIfBreak:
317
362
  pick := top.doc.Children[1]
318
363
  if top.mode == modeBreak {
@@ -341,3 +386,122 @@ func fits(doc Doc, remaining int, indent int) bool {
341
386
  }
342
387
  return true
343
388
  }
389
+
390
+ // fitsFirstLine reports whether the first line of `doc` — every column
391
+ // up to its first line break — renders within `remaining` columns. It
392
+ // drives ConditionalGroup option selection: an option is eligible when
393
+ // its opening line fits, even if its later lines wrap.
394
+ //
395
+ // The walk treats every top-level break point as broken — an IfBreak
396
+ // takes its break branch, and a Line / Softline / Hardline ends the
397
+ // measurement — so it counts exactly the columns the option would place
398
+ // on the line the group starts on. A nested ConditionalGroup
399
+ // contributes its own first option.
400
+ //
401
+ // A nested Group is the exception: a Group with no hard break renders
402
+ // flat when it fits, so its Line separators collapse to spaces and stay
403
+ // on the first line. The walk measures such a Group's flattened width
404
+ // rather than stopping at its first Line; only a Group that carries a
405
+ // Hardline (flatten reports it cannot render flat) ends the first line
406
+ // at its break.
407
+ func fitsFirstLine(doc Doc, remaining int) bool {
408
+ if remaining < 0 {
409
+ return false
410
+ }
411
+ stack := []Doc{doc}
412
+ for len(stack) > 0 {
413
+ top := stack[len(stack)-1]
414
+ stack = stack[:len(stack)-1]
415
+ switch top.Kind {
416
+ case docText:
417
+ if idx := strings.IndexByte(top.Text, '\n'); idx >= 0 {
418
+ // A multi-line Text ends the first line at its first newline.
419
+ return remaining-idx >= 0
420
+ }
421
+ remaining -= len(top.Text)
422
+ if remaining < 0 {
423
+ return false
424
+ }
425
+ case docGroup:
426
+ // A Group that can render flat keeps its Lines on the first line
427
+ // as spaces — measure the flattened form. One that cannot (a
428
+ // Hardline or forced break inside) breaks, so descend and let the
429
+ // Line/Hardline case end the first line at that break.
430
+ if flat, ok := flatten(top); ok {
431
+ stack = append(stack, flat)
432
+ } else {
433
+ for i := len(top.Children) - 1; i >= 0; i-- {
434
+ stack = append(stack, top.Children[i])
435
+ }
436
+ }
437
+ case docConcat, docIndent, docAlign:
438
+ for i := len(top.Children) - 1; i >= 0; i-- {
439
+ stack = append(stack, top.Children[i])
440
+ }
441
+ case docConditionalGroup:
442
+ if len(top.Children) > 0 {
443
+ stack = append(stack, top.Children[0])
444
+ }
445
+ case docIfBreak:
446
+ // Measuring in break mode: take the broken branch.
447
+ stack = append(stack, top.Children[0])
448
+ case docLine, docSoftline, docHardline, docLiteralline:
449
+ // The first break ends the first line; what fit so far fits.
450
+ return true
451
+ case docNil, docLineSuffix:
452
+ // No first-line width contribution.
453
+ }
454
+ }
455
+ return true
456
+ }
457
+
458
+ // flatten returns the all-flat rendering of `doc`: every Group rendered
459
+ // flat, every IfBreak resolved to its flat branch, every Line collapsed
460
+ // to a single space and every Softline to nothing. The second result is
461
+ // false when the doc cannot render flat at all — it carries a Hardline,
462
+ // a Literalline, a forced-broken Group, a multi-line Text or a queued
463
+ // LineSuffix — and the caller must then drop the flat layout option.
464
+ func flatten(doc Doc) (Doc, bool) {
465
+ switch doc.Kind {
466
+ case docText:
467
+ if strings.Contains(doc.Text, "\n") {
468
+ return Doc{}, false
469
+ }
470
+ return doc, true
471
+ case docLine:
472
+ return Text(" "), true
473
+ case docNil, docSoftline:
474
+ return Doc{Kind: docNil}, true
475
+ case docHardline, docLiteralline, docLineSuffix:
476
+ return Doc{}, false
477
+ case docIfBreak:
478
+ return flatten(doc.Children[1])
479
+ case docConditionalGroup:
480
+ if len(doc.Children) == 0 {
481
+ return Doc{Kind: docNil}, true
482
+ }
483
+ return flatten(doc.Children[0])
484
+ case docGroup:
485
+ if doc.Break {
486
+ return Doc{}, false
487
+ }
488
+ return flattenChildren(doc.Children)
489
+ case docConcat, docIndent, docAlign:
490
+ return flattenChildren(doc.Children)
491
+ }
492
+ return doc, true
493
+ }
494
+
495
+ // flattenChildren flattens each child and concatenates the results,
496
+ // short-circuiting to (zero, false) when any child cannot render flat.
497
+ func flattenChildren(children []Doc) (Doc, bool) {
498
+ out := make([]Doc, 0, len(children))
499
+ for _, c := range children {
500
+ fc, ok := flatten(c)
501
+ if !ok {
502
+ return Doc{}, false
503
+ }
504
+ out = append(out, fc)
505
+ }
506
+ return Concat(out...), true
507
+ }
@@ -20,27 +20,37 @@ import (
20
20
  // Array literals do NOT carry a leading/trailing space inside the
21
21
  // brackets in flat mode (`[a, b]`, not `[ a, b ]`), matching every
22
22
  // JavaScript formatter. Empty arrays collapse to `[]`.
23
- func printArrayLiteral(ctx *PrintContext, node *shimast.Node) Doc {
23
+ //
24
+ // The second return value is the `covered` flag: see PrintNode. It is
25
+ // the AND of every element's coverage.
26
+ func printArrayLiteral(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
24
27
  if node == nil {
25
- return Doc{}
28
+ return Doc{}, true
26
29
  }
27
30
  arr := node.AsArrayLiteralExpression()
28
31
  if arr == nil || arr.Elements == nil {
29
- return verbatim(ctx, node)
32
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
30
33
  }
31
34
  items := make([]Doc, 0, len(arr.Elements.Nodes))
35
+ covered := true
32
36
  for _, elem := range arr.Elements.Nodes {
33
37
  if elem == nil {
34
- return verbatim(ctx, node)
38
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
35
39
  }
36
- doc, _ := PrintNode(ctx, elem)
40
+ doc, childCovered := PrintNode(ctx, elem)
41
+ covered = covered && childCovered
37
42
  items = append(items, doc)
38
43
  }
44
+ // AddComma honors `format.trailingComma`: arrays accept trailing
45
+ // commas in ES5 so both "all" and "es5" keep them; only "none"
46
+ // suppresses. Hardcoding `true` would oscillate against Prettier on
47
+ // every `none` project (the trailing-comma rule wouldn't insert one
48
+ // and the printer would put one back).
39
49
  return printList(ctx, listShape{
40
50
  OpenTok: "[",
41
51
  CloseTok: "]",
42
52
  Items: items,
43
53
  Space: false,
44
- AddComma: true,
45
- })
54
+ AddComma: ctx.allowsEs5TrailingComma(),
55
+ }), covered
46
56
  }
@@ -20,17 +20,23 @@ import (
20
20
  // Type arguments (`foo<A, B>(x)`) are preserved verbatim. Trailing
21
21
  // commas on type arguments are intentionally avoided — Prettier omits
22
22
  // them too (see prettier#10353).
23
- func printCallExpression(ctx *PrintContext, node *shimast.Node) Doc {
23
+ //
24
+ // The second return value is the `covered` flag: see PrintNode. The
25
+ // callee, optional `?.` token and type arguments are verbatim, so a
26
+ // multi-line callee taints coverage just as a multi-line argument does.
27
+ func printCallExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
24
28
  if node == nil {
25
- return Doc{}
29
+ return Doc{}, true
26
30
  }
27
31
  call := node.AsCallExpression()
28
32
  if call == nil {
29
- return verbatim(ctx, node)
33
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
30
34
  }
31
35
  parts := []Doc{}
36
+ covered := true
32
37
  if call.Expression != nil {
33
38
  parts = append(parts, verbatim(ctx, call.Expression))
39
+ covered = covered && !nodeSpansMultipleLines(ctx, call.Expression)
34
40
  }
35
41
  // Question-dot for optional call: `foo?.(x)`. The token byte range
36
42
  // lives between Expression.End() and the open paren; copy
@@ -43,38 +49,45 @@ func printCallExpression(ctx *PrintContext, node *shimast.Node) Doc {
43
49
  parts = append(parts, verbatimRange(ctx.Source, callTypeArgsStart(ctx, call), callTypeArgsEnd(ctx, call)))
44
50
  }
45
51
  if hasNilEntry(call.Arguments) {
46
- return verbatim(ctx, node)
52
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
47
53
  }
48
- parts = append(parts, printArgList(ctx, call.Arguments))
49
- return Concat(parts...)
54
+ argDoc, argCovered := printArgList(ctx, call.Arguments)
55
+ parts = append(parts, argDoc)
56
+ return Concat(parts...), covered && argCovered
50
57
  }
51
58
 
52
59
  // printNewExpression renders a NewExpression. It mirrors the call
53
60
  // expression printer; the only difference is the leading `new ` keyword
54
61
  // and the optional argument list (NewExpression may omit args entirely,
55
62
  // e.g. `new Foo`).
56
- func printNewExpression(ctx *PrintContext, node *shimast.Node) Doc {
63
+ //
64
+ // The second return value is the `covered` flag: see PrintNode.
65
+ func printNewExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
57
66
  if node == nil {
58
- return Doc{}
67
+ return Doc{}, true
59
68
  }
60
69
  ne := node.AsNewExpression()
61
70
  if ne == nil {
62
- return verbatim(ctx, node)
71
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
63
72
  }
64
73
  parts := []Doc{Text("new ")}
74
+ covered := true
65
75
  if ne.Expression != nil {
66
76
  parts = append(parts, verbatim(ctx, ne.Expression))
77
+ covered = covered && !nodeSpansMultipleLines(ctx, ne.Expression)
67
78
  }
68
79
  if ne.TypeArguments != nil {
69
80
  parts = append(parts, verbatimRange(ctx.Source, newTypeArgsStart(ctx, ne), newTypeArgsEnd(ctx, ne)))
70
81
  }
71
82
  if ne.Arguments != nil {
72
83
  if hasNilEntry(ne.Arguments) {
73
- return verbatim(ctx, node)
84
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
74
85
  }
75
- parts = append(parts, printArgList(ctx, ne.Arguments))
86
+ argDoc, argCovered := printArgList(ctx, ne.Arguments)
87
+ parts = append(parts, argDoc)
88
+ covered = covered && argCovered
76
89
  }
77
- return Concat(parts...)
90
+ return Concat(parts...), covered
78
91
  }
79
92
 
80
93
  // hasNilEntry reports whether any entry of `list` is a nil pointer.
@@ -94,24 +107,120 @@ func hasNilEntry(list *shimast.NodeList) bool {
94
107
  }
95
108
 
96
109
  // printArgList renders an argument node list. The shared printList
97
- // handles the open-comma-close shape; this helper just gathers the
98
- // per-argument docs.
99
- func printArgList(ctx *PrintContext, list *shimast.NodeList) Doc {
110
+ // handles the open-comma-close shape; this helper gathers the
111
+ // per-argument docs and threads each argument's `covered` flag up.
112
+ //
113
+ // When the final argument is a block-bodied callback or object literal,
114
+ // the list renders in the "last-argument hugging" shape (see
115
+ // printListHuggingLast): the callback's own body carries the multi-line
116
+ // layout, so the parens stay attached and the preceding arguments are
117
+ // not exploded onto their own lines. This is the Prettier behavior for
118
+ // `foo(x, () => { … })`.
119
+ func printArgList(ctx *PrintContext, list *shimast.NodeList) (Doc, bool) {
100
120
  if list == nil {
101
- return Text("()")
121
+ return Text("()"), true
102
122
  }
103
123
  items := make([]Doc, 0, len(list.Nodes))
124
+ covered := true
104
125
  for _, arg := range list.Nodes {
105
- doc, _ := PrintNode(ctx, arg)
126
+ doc, childCovered := PrintNode(ctx, arg)
127
+ covered = covered && childCovered
106
128
  items = append(items, doc)
107
129
  }
108
- return printList(ctx, listShape{
130
+ // AddComma honors `format.trailingComma`: call / new argument lists
131
+ // accepted trailing commas only in ES2017+, so Prettier's "es5" and
132
+ // "none" modes skip them here. Hardcoding `true` would oscillate
133
+ // against Prettier on every cascade pass on any project configured
134
+ // with "es5" (rxjs hit this on ajax.ts and several operators / testing
135
+ // helpers — the rule said "no comma needed" while the printer added
136
+ // one back on its own reflow).
137
+ shape := listShape{
109
138
  OpenTok: "(",
110
139
  CloseTok: ")",
111
140
  Items: items,
112
141
  Space: false,
113
- AddComma: true,
114
- })
142
+ AddComma: ctx.allowsCallArgumentTrailingComma(),
143
+ HugLast: shouldHugLastArgument(list.Nodes),
144
+ }
145
+ return printList(ctx, shape), covered
146
+ }
147
+
148
+ // shouldHugLastArgument reports whether the final entry of `args` is a
149
+ // shape Prettier keeps hugging the closing paren: an object or array
150
+ // literal, a function expression, or an arrow function whose body is a
151
+ // block, an object literal, or an array literal. Hugging only applies
152
+ // when that argument is genuinely the last one; a callback in the
153
+ // middle of the list does not trigger the shape.
154
+ //
155
+ // An arrow with any other expression body (`(x) => x.id`) is
156
+ // deliberately excluded. Such a body carries no internal break point,
157
+ // so the hugging shape — a flat `Concat` with no Group — would pin the
158
+ // whole call to one line even when that line overflows printWidth.
159
+ // Routing it through the normal list shape instead lets the argument
160
+ // list explode onto its own line when the call does not fit, which is
161
+ // what Prettier does.
162
+ func shouldHugLastArgument(args []*shimast.Node) bool {
163
+ if len(args) == 0 {
164
+ return false
165
+ }
166
+ last := args[len(args)-1]
167
+ if last == nil {
168
+ return false
169
+ }
170
+ switch last.Kind {
171
+ case shimast.KindFunctionExpression,
172
+ shimast.KindObjectLiteralExpression,
173
+ shimast.KindArrayLiteralExpression:
174
+ return true
175
+ case shimast.KindArrowFunction:
176
+ arrow := last.AsArrowFunction()
177
+ if arrow == nil || arrow.Body == nil {
178
+ return false
179
+ }
180
+ body := arrow.Body
181
+ // `(x) => ({ … })` parenthesizes its object body; hug on the inner
182
+ // expression, mirroring Prettier's couldExpandArg.
183
+ if body.Kind == shimast.KindParenthesizedExpression {
184
+ if p := body.AsParenthesizedExpression(); p != nil && p.Expression != nil {
185
+ body = p.Expression
186
+ }
187
+ }
188
+ switch body.Kind {
189
+ case shimast.KindBlock,
190
+ shimast.KindObjectLiteralExpression,
191
+ shimast.KindArrayLiteralExpression:
192
+ return true
193
+ }
194
+ }
195
+ return false
196
+ }
197
+
198
+ // forceBreakFirstGroup returns `doc` with the first Group found in a
199
+ // left-to-right walk of its subtree forced broken, and reports whether
200
+ // one was found. printListHuggingLast uses it to commit a hugged
201
+ // argument — an object or array literal, possibly nested inside an
202
+ // arrow body (`(x) => ({ … })`) — to its multi-line shape. The caller
203
+ // guards the walk with flatten: it is only run on an item that has no
204
+ // hard line breaks of its own, so the first Group reached is the
205
+ // hugged literal itself, never an unrelated Group inside a block body.
206
+ func forceBreakFirstGroup(doc Doc) (Doc, bool) {
207
+ switch doc.Kind {
208
+ case docGroup:
209
+ doc.Break = true
210
+ return doc, true
211
+ case docConcat, docIndent, docAlign:
212
+ children := make([]Doc, len(doc.Children))
213
+ copy(children, doc.Children)
214
+ for i, child := range children {
215
+ broken, done := forceBreakFirstGroup(child)
216
+ if done {
217
+ children[i] = broken
218
+ doc.Children = children
219
+ return doc, true
220
+ }
221
+ }
222
+ }
223
+ return doc, false
115
224
  }
116
225
 
117
226
  // Type-argument byte-range helpers. The shim's NodeList.End() points