@ttsc/lint 0.14.0 → 0.14.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 +2 -2
- package/linthost/print_nodes_array.go +67 -8
- package/linthost/print_nodes_call.go +350 -82
- package/linthost/print_nodes_function.go +18 -0
- package/linthost/print_nodes_imports.go +17 -0
- package/linthost/print_nodes_list.go +30 -7
- package/linthost/print_nodes_object.go +4 -2
- package/linthost/print_nodes_ternary.go +48 -7
- package/linthost/rules_format_arrow_parens.go +46 -0
- package/linthost/rules_format_clause_join.go +13 -6
- package/linthost/rules_format_print_width.go +9 -14
- package/linthost/rules_format_sort_imports.go +65 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -66,8 +66,8 @@ export default {
|
|
|
66
66
|
rules: {
|
|
67
67
|
"no-var": "error",
|
|
68
68
|
"prefer-const": "error",
|
|
69
|
-
"typescript/no-explicit-any": "
|
|
70
|
-
"no-
|
|
69
|
+
"typescript/no-explicit-any": "error",
|
|
70
|
+
"typescript/no-floating-promises": "error",
|
|
71
71
|
},
|
|
72
72
|
} satisfies ITtscLintConfig;
|
|
73
73
|
```
|
|
@@ -32,9 +32,11 @@ func printArrayLiteral(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
32
32
|
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
33
33
|
}
|
|
34
34
|
// A comment between elements (or after `[`) would be dropped by the fresh
|
|
35
|
-
// separators; bail to verbatim
|
|
35
|
+
// separators; bail to verbatim and report UNCOVERED (hard `false`, not
|
|
36
|
+
// `!nodeSpansMultipleLines`) so an enclosing reflow abstains instead of
|
|
37
|
+
// breaking around this single-line verbatim array and moving it off its line.
|
|
36
38
|
if listHasInterItemComments(ctx, node) {
|
|
37
|
-
return verbatim(ctx, node),
|
|
39
|
+
return verbatim(ctx, node), false
|
|
38
40
|
}
|
|
39
41
|
items := make([]Doc, 0, len(arr.Elements.Nodes))
|
|
40
42
|
covered := true
|
|
@@ -119,12 +121,68 @@ func arrayForcesBreak(node *shimast.Node) bool {
|
|
|
119
121
|
return arrayShouldForceBreak(arr.Elements.Nodes)
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// fastPathForcesBreak reports whether `node`, OR a force-breaking node nested
|
|
125
|
+
// within the subtree a reflow of `node` would print (its call/new arguments and
|
|
126
|
+
// array elements, recursively), must explode even though it fits flat. Prettier
|
|
127
|
+
// breaks such a descendant — an array `shouldBreak` or a function-composition
|
|
128
|
+
// call/new — when the enclosing node reflows; ttsc's print-width fast path
|
|
129
|
+
// returns first while the descendant abstains via hasReflowAncestor, leaving
|
|
130
|
+
// both flat (`new Map([["a", 1], ["b", 2]])`, `foo([[1, 2], [3, 4]])`). So the
|
|
131
|
+
// fast path must consult this, not only the visited node itself.
|
|
132
|
+
func fastPathForcesBreak(node *shimast.Node) bool {
|
|
133
|
+
if node == nil {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
if callForcesFunctionBreak(node) || arrayForcesBreak(node) {
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
var children []*shimast.Node
|
|
140
|
+
switch node.Kind {
|
|
141
|
+
case shimast.KindCallExpression:
|
|
142
|
+
if c := node.AsCallExpression(); c != nil && c.Arguments != nil {
|
|
143
|
+
children = c.Arguments.Nodes
|
|
144
|
+
}
|
|
145
|
+
case shimast.KindNewExpression:
|
|
146
|
+
if n := node.AsNewExpression(); n != nil && n.Arguments != nil {
|
|
147
|
+
children = n.Arguments.Nodes
|
|
148
|
+
}
|
|
149
|
+
case shimast.KindArrayLiteralExpression:
|
|
150
|
+
if a := node.AsArrayLiteralExpression(); a != nil && a.Elements != nil {
|
|
151
|
+
children = a.Elements.Nodes
|
|
152
|
+
}
|
|
153
|
+
case shimast.KindObjectLiteralExpression:
|
|
154
|
+
// A force-breaking array/object nested in an object PROPERTY value
|
|
155
|
+
// (`{ m: [[1, 2], [3, 4]] }`) must also deny the fast path: the value
|
|
156
|
+
// abstains to its object ancestor via hasReflowAncestor, and the object
|
|
157
|
+
// itself fits flat, so without descending into property initializers both
|
|
158
|
+
// would stay flat where Prettier breaks them.
|
|
159
|
+
if o := node.AsObjectLiteralExpression(); o != nil && o.Properties != nil {
|
|
160
|
+
for _, p := range o.Properties.Nodes {
|
|
161
|
+
if p != nil && p.Kind == shimast.KindPropertyAssignment {
|
|
162
|
+
if pa := p.AsPropertyAssignment(); pa != nil && pa.Initializer != nil {
|
|
163
|
+
children = append(children, pa.Initializer)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for _, ch := range children {
|
|
170
|
+
if fastPathForcesBreak(ch) {
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
|
|
122
177
|
// isConciselyPrintedArray reports whether an array should use Prettier's
|
|
123
|
-
// concise "fill" layout:
|
|
178
|
+
// concise "fill" layout: at least one element and every element a numeric
|
|
124
179
|
// literal (optionally a `+`/`-` signed numeric). Prettier packs such arrays
|
|
125
180
|
// several per line; mixed / string / identifier arrays stay one-per-line.
|
|
181
|
+
// Prettier's predicate gates on `elements.length > 0` (array.js), so a
|
|
182
|
+
// single-element numeric array counts — which also makes a `[42]` last
|
|
183
|
+
// argument decline last-argument hugging, matching shouldExpandLastArg.
|
|
126
184
|
func isConciselyPrintedArray(elems []*shimast.Node) bool {
|
|
127
|
-
if len(elems) <
|
|
185
|
+
if len(elems) < 1 {
|
|
128
186
|
return false
|
|
129
187
|
}
|
|
130
188
|
for _, e := range elems {
|
|
@@ -136,13 +194,15 @@ func isConciselyPrintedArray(elems []*shimast.Node) bool {
|
|
|
136
194
|
}
|
|
137
195
|
|
|
138
196
|
// isNumericArrayElement reports whether `node` is a numeric literal or a
|
|
139
|
-
// `+`/`-` prefix applied to one.
|
|
197
|
+
// `+`/`-` prefix applied to one. A BigInt literal is NOT numeric here: Prettier's
|
|
198
|
+
// isNumericLiteral matches only a number-valued literal, so `[1n, 2n]` prints
|
|
199
|
+
// one element per line rather than filling.
|
|
140
200
|
func isNumericArrayElement(node *shimast.Node) bool {
|
|
141
201
|
if node == nil {
|
|
142
202
|
return false
|
|
143
203
|
}
|
|
144
204
|
switch node.Kind {
|
|
145
|
-
case shimast.KindNumericLiteral
|
|
205
|
+
case shimast.KindNumericLiteral:
|
|
146
206
|
return true
|
|
147
207
|
case shimast.KindPrefixUnaryExpression:
|
|
148
208
|
u := node.AsPrefixUnaryExpression()
|
|
@@ -152,8 +212,7 @@ func isNumericArrayElement(node *shimast.Node) bool {
|
|
|
152
212
|
if u.Operator != shimast.KindPlusToken && u.Operator != shimast.KindMinusToken {
|
|
153
213
|
return false
|
|
154
214
|
}
|
|
155
|
-
return u.Operand.Kind == shimast.KindNumericLiteral
|
|
156
|
-
u.Operand.Kind == shimast.KindBigIntLiteral
|
|
215
|
+
return u.Operand.Kind == shimast.KindNumericLiteral
|
|
157
216
|
}
|
|
158
217
|
return false
|
|
159
218
|
}
|
|
@@ -2,7 +2,6 @@ package linthost
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
-
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
6
5
|
)
|
|
7
6
|
|
|
8
7
|
// printCallExpression renders a CallExpression with width-aware
|
|
@@ -52,6 +51,18 @@ func printCallExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
52
51
|
if hasNilEntry(call.Arguments) {
|
|
53
52
|
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
54
53
|
}
|
|
54
|
+
// A comment in an argument gap (`foo(a, /* c */ b)`) would be dropped by the
|
|
55
|
+
// freshly minted `, ` separators in printArgList. The top-level print-width
|
|
56
|
+
// scan only masks a *direct* child's comments, so a comment inside a NESTED
|
|
57
|
+
// call's argument list slips through and is lost on reflow. Self-guard the
|
|
58
|
+
// way the object/array printers do: bail to verbatim and report UNCOVERED so
|
|
59
|
+
// an enclosing reflow abstains too. Uncovered must be hard `false`, not
|
|
60
|
+
// `!nodeSpansMultipleLines`: a single-line comment-bearing call would
|
|
61
|
+
// otherwise report covered and let the outer reflow break around it, moving
|
|
62
|
+
// the verbatim node off its line (the assertFormatUnchanged contract).
|
|
63
|
+
if listHasInterItemComments(ctx, node) {
|
|
64
|
+
return verbatim(ctx, node), false
|
|
65
|
+
}
|
|
55
66
|
// Prettier never appends a trailing comma inside a dynamic
|
|
56
67
|
// `import(...)`, so the printer's reflow must agree with
|
|
57
68
|
// format/trailing-comma's same exception (see isDynamicImportCall).
|
|
@@ -90,6 +101,16 @@ func printNewExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
90
101
|
if ne.TypeArguments != nil {
|
|
91
102
|
parts = append(parts, verbatimRange(ctx.Source, typeArgsStart(ctx.Source, ne.TypeArguments), typeArgsEnd(ctx.Source, ne.TypeArguments)))
|
|
92
103
|
}
|
|
104
|
+
// A comment in the `new`->constructee gap (`new /* c */ Foo`) or an argument
|
|
105
|
+
// gap would be dropped by the minted `Text("new ")` / fresh separators — those
|
|
106
|
+
// gaps are not AST children, so a nested new-expression's comment slips past
|
|
107
|
+
// the top-level scan. Guard unconditionally (the no-args `new Foo` path mints
|
|
108
|
+
// `new ` too), mirroring printCallExpression's unconditional guard. Report
|
|
109
|
+
// UNCOVERED (hard `false`, not `!nodeSpansMultipleLines`) so an enclosing
|
|
110
|
+
// reflow abstains rather than breaking around this single-line verbatim node.
|
|
111
|
+
if listHasInterItemComments(ctx, node) {
|
|
112
|
+
return verbatim(ctx, node), false
|
|
113
|
+
}
|
|
93
114
|
if ne.Arguments != nil {
|
|
94
115
|
if hasNilEntry(ne.Arguments) {
|
|
95
116
|
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
@@ -155,13 +176,17 @@ func printArgList(ctx *PrintContext, list *shimast.NodeList, addComma bool, deco
|
|
|
155
176
|
// doc carries hard breaks (a block-bodied callback). A leading object or
|
|
156
177
|
// array that merely fits flat does NOT decline — Prettier still hugs the
|
|
157
178
|
// last argument there (`doConfigure({ … }, () => { … })`).
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
179
|
+
// The useMemo/useEffect deps shape (`f(() => x, [deps])`) explodes its 2-arg
|
|
180
|
+
// arrow+array one-per-line even under a decorator: Prettier's shouldExpandLastArg
|
|
181
|
+
// declines `args.length === 2 && penultimate is ArrowFunction && last is Array`
|
|
182
|
+
// unconditionally (it is NOT decorator-gated). The genuine decorator hug
|
|
183
|
+
// (`@OneToMany(() => P, (p) => p.c, { … })`) is 3-arg, so isUseMemoArrowArrayShape
|
|
184
|
+
// already returns false for it; a 2-or-more-callback composition is handled by
|
|
185
|
+
// argListForcesFunctionBreak, and a block-bodied leading callback by
|
|
186
|
+
// anyLeadingItemBreaks.
|
|
162
187
|
hugLast := shouldHugLastArgument(list.Nodes) &&
|
|
163
188
|
!anyLeadingItemBreaks(items) &&
|
|
164
|
-
|
|
189
|
+
!isUseMemoArrowArrayShape(list.Nodes)
|
|
165
190
|
// Two or more function/arrow arguments force the list to explode, one per
|
|
166
191
|
// line, even when it would fit flat: Prettier always breaks a call carrying
|
|
167
192
|
// multiple callbacks (`promise.then(() => a, () => b)`), the
|
|
@@ -180,6 +205,7 @@ func printArgList(ctx *PrintContext, list *shimast.NodeList, addComma bool, deco
|
|
|
180
205
|
hugLast = true
|
|
181
206
|
forceFnBreak = false
|
|
182
207
|
}
|
|
208
|
+
hugFirst := !hugLast && !forceFnBreak && shouldHugFirstArgument(ctx, list.Nodes)
|
|
183
209
|
shape := listShape{
|
|
184
210
|
OpenTok: "(",
|
|
185
211
|
CloseTok: ")",
|
|
@@ -188,9 +214,16 @@ func printArgList(ctx *PrintContext, list *shimast.NodeList, addComma bool, deco
|
|
|
188
214
|
AddComma: addComma,
|
|
189
215
|
HugLast: hugLast,
|
|
190
216
|
HugLastForce: forceHugLast,
|
|
191
|
-
HugFirst:
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
HugFirst: hugFirst,
|
|
218
|
+
// A non-empty array second argument reaches HugFirst only via the
|
|
219
|
+
// React-hook deps branch of shouldHugFirstArgument; let that deps array
|
|
220
|
+
// break one-per-line instead of being pinned flat on the close line.
|
|
221
|
+
HugFirstTrailingBreaks: hugFirst && len(list.Nodes) == 2 &&
|
|
222
|
+
list.Nodes[1] != nil &&
|
|
223
|
+
list.Nodes[1].Kind == shimast.KindArrayLiteralExpression,
|
|
224
|
+
HugFirstForce: hugFirst && isReactHookDepsCall(list.Nodes),
|
|
225
|
+
ForceBreak: forceFnBreak,
|
|
226
|
+
BlankBefore: blankBeforeItems(ctx.Source, list.Nodes),
|
|
194
227
|
}
|
|
195
228
|
return printList(ctx, shape), covered
|
|
196
229
|
}
|
|
@@ -379,40 +412,78 @@ func argListForcesFunctionBreak(args []*shimast.Node, decoratorCall bool) bool {
|
|
|
379
412
|
return isFunctionCompositionArgs(args)
|
|
380
413
|
}
|
|
381
414
|
|
|
382
|
-
// callForcesFunctionBreak reports whether a node is a
|
|
383
|
-
// multiple-callback rule forces to explode.
|
|
384
|
-
//
|
|
415
|
+
// callForcesFunctionBreak reports whether a node is a call OR new expression
|
|
416
|
+
// that the multiple-callback rule forces to explode. Prettier's printCallArguments
|
|
417
|
+
// is shared by NewExpression (its function-composition break is gated only by
|
|
418
|
+
// `path.parent.type !== "Decorator"`, not by call-vs-new), so `new Foo(() => a,
|
|
419
|
+
// () => b)` explodes the same as a call. The print-width rule consults this so
|
|
420
|
+
// its flat-fit fast path does not leave such a call/new inline when the source
|
|
385
421
|
// wrote it on one line.
|
|
386
422
|
func callForcesFunctionBreak(node *shimast.Node) bool {
|
|
387
|
-
if node == nil
|
|
423
|
+
if node == nil {
|
|
388
424
|
return false
|
|
389
425
|
}
|
|
390
|
-
|
|
391
|
-
|
|
426
|
+
var args *shimast.NodeList
|
|
427
|
+
switch node.Kind {
|
|
428
|
+
case shimast.KindCallExpression:
|
|
429
|
+
if c := node.AsCallExpression(); c != nil {
|
|
430
|
+
args = c.Arguments
|
|
431
|
+
}
|
|
432
|
+
case shimast.KindNewExpression:
|
|
433
|
+
if n := node.AsNewExpression(); n != nil {
|
|
434
|
+
args = n.Arguments
|
|
435
|
+
}
|
|
436
|
+
default:
|
|
392
437
|
return false
|
|
393
438
|
}
|
|
394
|
-
|
|
395
|
-
|
|
439
|
+
if args == nil {
|
|
440
|
+
return false
|
|
441
|
+
}
|
|
442
|
+
// A new expression is never a decorator's call.
|
|
443
|
+
decoratorCall := node.Kind == shimast.KindCallExpression &&
|
|
444
|
+
node.Parent != nil && node.Parent.Kind == shimast.KindDecorator
|
|
445
|
+
return argListForcesFunctionBreak(args.Nodes, decoratorCall)
|
|
396
446
|
}
|
|
397
447
|
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return true
|
|
413
|
-
}
|
|
448
|
+
// isUseMemoArrowArrayShape reports the ONE leading-argument shape Prettier
|
|
449
|
+
// declines last-argument hugging for: exactly two arguments where the first is
|
|
450
|
+
// an arrow function and the last is an array literal — the
|
|
451
|
+
// `useMemo(() => x, [deps])` / `useEffect(() => { … }, [deps])` shape.
|
|
452
|
+
// Prettier's shouldExpandLastArg (call-arguments.js:260-262) has NO general
|
|
453
|
+
// "a leading function declines hugging" rule; its only leading-argument clause
|
|
454
|
+
// is `args.length === 2 && penultimate is ArrowFunctionExpression && last is
|
|
455
|
+
// ArrayExpression`. A block-bodied leading callback is handled independently by
|
|
456
|
+
// anyLeadingItemBreaks (it `willBreak`), and two-or-more callbacks by
|
|
457
|
+
// argListForcesFunctionBreak, so a leading expression-bodied arrow before a
|
|
458
|
+
// non-array huggable last argument (`f((x) => g(x), { a: 1 })`) must still hug.
|
|
459
|
+
func isUseMemoArrowArrayShape(args []*shimast.Node) bool {
|
|
460
|
+
if len(args) != 2 || args[0] == nil || args[1] == nil {
|
|
461
|
+
return false
|
|
414
462
|
}
|
|
415
|
-
return
|
|
463
|
+
return args[0].Kind == shimast.KindArrowFunction &&
|
|
464
|
+
args[1].Kind == shimast.KindArrayLiteralExpression
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// isReactHookDepsCall reports the React-hook deps shape Prettier hugs WITHOUT an
|
|
468
|
+
// exploded fallback: exactly two arguments, the first a ZERO-parameter
|
|
469
|
+
// block-bodied arrow, the second an array literal (`useEffect(() => { … },
|
|
470
|
+
// [deps])`). Prettier's isReactHookCallWithDepsArray / isValidHookCallbackAndDepsFormat
|
|
471
|
+
// keys on this and never explodes the args — it keeps the callback hugged and
|
|
472
|
+
// lets the open line overflow. A parameterized callback (`subscribe((e) => { … },
|
|
473
|
+
// [])`) is NOT this shape and keeps the fallback, so the zero-parameter +
|
|
474
|
+
// block-body gate is load-bearing (distinct from isUseMemoArrowArrayShape and
|
|
475
|
+
// from the HugFirstTrailingBreaks array check).
|
|
476
|
+
func isReactHookDepsCall(args []*shimast.Node) bool {
|
|
477
|
+
if len(args) != 2 || args[0] == nil || args[1] == nil {
|
|
478
|
+
return false
|
|
479
|
+
}
|
|
480
|
+
if args[1].Kind != shimast.KindArrayLiteralExpression {
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
arrow := args[0].AsArrowFunction()
|
|
484
|
+
return arrow != nil && arrow.Body != nil &&
|
|
485
|
+
arrow.Body.Kind == shimast.KindBlock &&
|
|
486
|
+
len(args[0].Parameters()) == 0
|
|
416
487
|
}
|
|
417
488
|
|
|
418
489
|
// anyLeadingItemBreaks reports whether any item before the last carries a
|
|
@@ -460,6 +531,15 @@ func shouldHugLastArgument(args []*shimast.Node) bool {
|
|
|
460
531
|
if pen := args[len(args)-2]; pen != nil && pen.Kind == last.Kind {
|
|
461
532
|
return false
|
|
462
533
|
}
|
|
534
|
+
// Prettier's shouldExpandLastArg also declines a CONCISELY-PRINTED numeric
|
|
535
|
+
// array as the last of two-plus arguments: it fills on its own line, so
|
|
536
|
+
// `drawPolygon(ctx, [1, 2, 3, …])` explodes rather than hugging.
|
|
537
|
+
if last.Kind == shimast.KindArrayLiteralExpression {
|
|
538
|
+
if arr := last.AsArrayLiteralExpression(); arr != nil && arr.Elements != nil &&
|
|
539
|
+
isConciselyPrintedArray(arr.Elements.Nodes) {
|
|
540
|
+
return false
|
|
541
|
+
}
|
|
542
|
+
}
|
|
463
543
|
}
|
|
464
544
|
return true
|
|
465
545
|
}
|
|
@@ -471,10 +551,23 @@ func shouldHugLastArgument(args []*shimast.Node) bool {
|
|
|
471
551
|
// flat hugging Concat would pin an overflowing call to one line.
|
|
472
552
|
func lastArgHuggableShape(last *shimast.Node) bool {
|
|
473
553
|
switch last.Kind {
|
|
474
|
-
case shimast.KindFunctionExpression
|
|
475
|
-
shimast.KindObjectLiteralExpression,
|
|
476
|
-
shimast.KindArrayLiteralExpression:
|
|
554
|
+
case shimast.KindFunctionExpression:
|
|
477
555
|
return true
|
|
556
|
+
case shimast.KindObjectLiteralExpression:
|
|
557
|
+
// Prettier's couldExpandArg requires a NON-EMPTY object (`properties.length
|
|
558
|
+
// > 0`); an empty `{}` is not expandable, so `foo(a, b, {})` explodes the
|
|
559
|
+
// list rather than hugging. (Inter-item comments route to verbatim, so the
|
|
560
|
+
// hasComment branch is moot here.)
|
|
561
|
+
if obj := last.AsObjectLiteralExpression(); obj != nil {
|
|
562
|
+
return obj.Properties != nil && len(obj.Properties.Nodes) > 0
|
|
563
|
+
}
|
|
564
|
+
return false
|
|
565
|
+
case shimast.KindArrayLiteralExpression:
|
|
566
|
+
// Likewise a non-empty array; an empty `[]` is not expandable.
|
|
567
|
+
if arr := last.AsArrayLiteralExpression(); arr != nil {
|
|
568
|
+
return arr.Elements != nil && len(arr.Elements.Nodes) > 0
|
|
569
|
+
}
|
|
570
|
+
return false
|
|
478
571
|
case shimast.KindArrowFunction:
|
|
479
572
|
arrow := last.AsArrowFunction()
|
|
480
573
|
if arrow == nil || arrow.Body == nil {
|
|
@@ -494,13 +587,26 @@ func lastArgHuggableShape(last *shimast.Node) bool {
|
|
|
494
587
|
return true
|
|
495
588
|
case shimast.KindObjectLiteralExpression,
|
|
496
589
|
shimast.KindArrayLiteralExpression:
|
|
497
|
-
// An
|
|
498
|
-
// Prettier's
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
return
|
|
590
|
+
// An object/array-bodied arrow hugs unless its return type is a type
|
|
591
|
+
// REFERENCE. Prettier's couldExpandArg gates only on
|
|
592
|
+
// `returnType.typeAnnotation.type === "TSTypeReference"`, so a keyword,
|
|
593
|
+
// union, array, or literal return type (`map((r): void => ({ … }))`)
|
|
594
|
+
// still hugs while a named-reference return (`map((r): Foo => ({ … })))`)
|
|
595
|
+
// explodes the whole list. A block body hugs regardless (handled above).
|
|
596
|
+
return arrow.Type == nil || arrow.Type.Kind != shimast.KindTypeReference
|
|
502
597
|
}
|
|
503
598
|
}
|
|
599
|
+
// DEFERRED (architectural, not a one-line predicate fix; do not re-add to the
|
|
600
|
+
// switch above without the supporting layout work):
|
|
601
|
+
// - A call/conditional/JSX-expression arrow body. Prettier's couldExpandArg
|
|
602
|
+
// hugs it, but ttsc emits the arrow signature (`=> `) verbatim with no
|
|
603
|
+
// break point after `=>`, so hugging here would force the FIRST group
|
|
604
|
+
// inside the call instead of breaking after `=>` — a different shape, not
|
|
605
|
+
// parity. Needs a break-after-`=>` arrow-body layout variant first.
|
|
606
|
+
// - A trailing `as`/`satisfies` cast wrapping a huggable last argument.
|
|
607
|
+
// Prettier's couldExpandArg recurses into the cast, but ttsc does not
|
|
608
|
+
// dispatch KindAsExpression (it prints verbatim), so forceBreakFirstGroup
|
|
609
|
+
// finds no group and the hug is inert. Needs a cast printer first.
|
|
504
610
|
return false
|
|
505
611
|
}
|
|
506
612
|
|
|
@@ -523,7 +629,22 @@ func shouldHugFirstArgument(ctx *PrintContext, args []*shimast.Node) bool {
|
|
|
523
629
|
if first == nil || second == nil {
|
|
524
630
|
return false
|
|
525
631
|
}
|
|
526
|
-
|
|
632
|
+
if !isFirstArgHuggableCallback(first) {
|
|
633
|
+
return false
|
|
634
|
+
}
|
|
635
|
+
// A NON-EMPTY array trailing arg hugs only in Prettier's React-hook deps
|
|
636
|
+
// shape (isReactHookCallWithDepsArray): a ZERO-parameter arrow callback plus
|
|
637
|
+
// an array, e.g. `useEffect(() => { … }, [a, b])`. With a parameter the call
|
|
638
|
+
// explodes (`subscribe((event) => { … }, [a, b])`), and a function expression
|
|
639
|
+
// (not an arrow) never qualifies. An EMPTY array falls through to
|
|
640
|
+
// isSimpleTrailingArg.
|
|
641
|
+
if second.Kind == shimast.KindArrayLiteralExpression {
|
|
642
|
+
if arr := second.AsArrayLiteralExpression(); arr != nil &&
|
|
643
|
+
arr.Elements != nil && len(arr.Elements.Nodes) > 0 {
|
|
644
|
+
return first.Kind == shimast.KindArrowFunction && len(first.Parameters()) == 0
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return isSimpleTrailingArg(ctx, second)
|
|
527
648
|
}
|
|
528
649
|
|
|
529
650
|
// isFirstArgHuggableCallback reports whether `node` is the callback shape
|
|
@@ -544,18 +665,18 @@ func isFirstArgHuggableCallback(node *shimast.Node) bool {
|
|
|
544
665
|
// isSimpleTrailingArg reports whether `node` is a value that may trail a
|
|
545
666
|
// hugged first-argument callback. Prettier hugs the leading callback when
|
|
546
667
|
// the trailing argument is an identifier, member access, literal, `this`,
|
|
547
|
-
// or an array literal
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
668
|
+
// or an EMPTY array/object literal. It also accepts a short call/new (at
|
|
669
|
+
// most one value argument) and a short arithmetic/logical expression
|
|
670
|
+
// (`setTimeout(fn, 1000 - x)`). A function, arrow, conditional, a non-empty
|
|
671
|
+
// array, or a non-empty object literal is excluded, so first-argument hugging
|
|
672
|
+
// declines and the whole list explodes. (A non-empty array after a
|
|
673
|
+
// zero-parameter arrow is the React-hook deps shape, hugged earlier in
|
|
674
|
+
// shouldHugFirstArgument and never routed here.)
|
|
552
675
|
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// that limitation in principle, but dependency arrays are short in
|
|
558
|
-
// practice, the same way the identifier/member cases already are.
|
|
676
|
+
// The call/new case rides the close line: the conditional-group fit check
|
|
677
|
+
// only measures an option's first line, so a hugged-first option whose
|
|
678
|
+
// trailing call overflows the closing line is still selected. Prettier
|
|
679
|
+
// accepts the same trade for a short trailing call.
|
|
559
680
|
func isSimpleTrailingArg(ctx *PrintContext, node *shimast.Node) bool {
|
|
560
681
|
switch node.Kind {
|
|
561
682
|
case shimast.KindIdentifier,
|
|
@@ -567,26 +688,46 @@ func isSimpleTrailingArg(ctx *PrintContext, node *shimast.Node) bool {
|
|
|
567
688
|
shimast.KindNullKeyword,
|
|
568
689
|
shimast.KindThisKeyword,
|
|
569
690
|
shimast.KindPropertyAccessExpression,
|
|
570
|
-
shimast.KindElementAccessExpression
|
|
571
|
-
shimast.KindArrayLiteralExpression:
|
|
691
|
+
shimast.KindElementAccessExpression:
|
|
572
692
|
return true
|
|
693
|
+
case shimast.KindArrayLiteralExpression:
|
|
694
|
+
// Only an EMPTY array is simple-trailing (the `[] as T[]` cast idiom).
|
|
695
|
+
// Prettier's couldExpandArg treats an array with elements as expandable,
|
|
696
|
+
// so a non-empty array cast (`[x] as number[]`) explodes the list rather
|
|
697
|
+
// than hugging. The non-empty bare-array deps shape (zero-param arrow) is
|
|
698
|
+
// handled in shouldHugFirstArgument before reaching here.
|
|
699
|
+
if arr := node.AsArrayLiteralExpression(); arr != nil {
|
|
700
|
+
return arr.Elements == nil || len(arr.Elements.Nodes) == 0
|
|
701
|
+
}
|
|
702
|
+
return false
|
|
573
703
|
case shimast.KindAsExpression:
|
|
574
704
|
// `[] as string[]`: the `reduce(fn, [] as T[])` idiom. Hug when the cast
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
705
|
+
// target is a simple type AND its inner expression is simple at depth 1 —
|
|
706
|
+
// Prettier's isHopefullyShortCallArgument cast branch is exactly
|
|
707
|
+
// `isSimpleType(typeAnnotation) && isSimpleCallArgument(node.expression, 1)`.
|
|
708
|
+
// Depth 1 (not the general isSimpleTrailingArg) is deliberate: it has no
|
|
709
|
+
// binaryish branch and bottoms a nested call's argument out at depth 0, so
|
|
710
|
+
// `(a - b) as T` and `makeInit(x) as T[]` are NOT simple and explode.
|
|
579
711
|
if as := node.AsAsExpression(); as != nil && as.Expression != nil {
|
|
580
|
-
return
|
|
712
|
+
return isSimpleCallArg(as.Expression, 1) && isSimpleCastType(as.Type)
|
|
713
|
+
}
|
|
714
|
+
case shimast.KindSatisfiesExpression:
|
|
715
|
+
// `[] satisfies T[]`: Prettier's isBinaryCastExpression covers
|
|
716
|
+
// TSSatisfiesExpression identically to TSAsExpression, so a satisfies cast
|
|
717
|
+
// takes the same simple-type + depth-1 simple-expression path as `as`.
|
|
718
|
+
if s := node.AsSatisfiesExpression(); s != nil && s.Expression != nil {
|
|
719
|
+
return isSimpleCallArg(s.Expression, 1) && isSimpleCastType(s.Type)
|
|
581
720
|
}
|
|
582
721
|
case shimast.KindCallExpression, shimast.KindNewExpression:
|
|
583
722
|
// `reduce(fn, Object.create(null))` / `reduce(fn, new Map<…>())`: Prettier
|
|
584
|
-
// hugs the first arg
|
|
585
|
-
// argument
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
|
|
723
|
+
// hugs the first arg over a trailing call/new only when it has at most one
|
|
724
|
+
// value argument AND that argument is itself structurally simple. Its
|
|
725
|
+
// isHopefullyShortCallArgument early-rejects >1 args, then falls through to
|
|
726
|
+
// isSimpleCallArgument (depth 2), so a 1-arg call whose sole argument is a
|
|
727
|
+
// non-trivial nested call/object/array (`reduce(fn, makeInit(deep(arg)))`)
|
|
728
|
+
// is NOT simple and the whole list explodes. Type arguments and long names
|
|
729
|
+
// still do not matter; the close line is allowed to overflow.
|
|
730
|
+
return callValueArgCount(node) <= 1 && isSimpleCallArg(node, 2)
|
|
590
731
|
case shimast.KindObjectLiteralExpression:
|
|
591
732
|
// `reduce(fn, {})`: an EMPTY object literal hugs (Prettier's couldGroupArg
|
|
592
733
|
// excludes a property-less object); an object with properties expands and
|
|
@@ -612,6 +753,109 @@ func isSimpleTrailingArg(ctx *PrintContext, node *shimast.Node) bool {
|
|
|
612
753
|
return false
|
|
613
754
|
}
|
|
614
755
|
|
|
756
|
+
// isSimpleCallArg ports Prettier's isSimpleCallArgument (utilities/index.js): a
|
|
757
|
+
// depth-bounded structural simplicity check. A node is simple when it is a leaf
|
|
758
|
+
// (identifier, literal, this, member access) or a shallow composite whose parts
|
|
759
|
+
// are simple at a reduced depth — a call/new is simple only when its callee is
|
|
760
|
+
// simple, it has at most `depth` arguments, and every argument is simple at
|
|
761
|
+
// `depth-1`. The recursion floor (`depth <= 0`) is what makes a nested call
|
|
762
|
+
// like `makeInit(deep(arg))` non-simple, so first-argument hugging declines and
|
|
763
|
+
// the list explodes, matching Prettier. A non-empty object is treated as
|
|
764
|
+
// non-simple (its values bottom out below the depth floor anyway — the safe
|
|
765
|
+
// under-match direction). Element access is treated as a simple leaf, matching
|
|
766
|
+
// ttsc's existing member-access handling, rather than recursing into its
|
|
767
|
+
// computed key (a pre-existing, rare residual over-match left out of scope).
|
|
768
|
+
func isSimpleCallArg(node *shimast.Node, depth int) bool {
|
|
769
|
+
if node == nil || depth <= 0 {
|
|
770
|
+
return false
|
|
771
|
+
}
|
|
772
|
+
switch node.Kind {
|
|
773
|
+
case shimast.KindIdentifier,
|
|
774
|
+
shimast.KindStringLiteral,
|
|
775
|
+
shimast.KindNumericLiteral,
|
|
776
|
+
shimast.KindBigIntLiteral,
|
|
777
|
+
shimast.KindTrueKeyword,
|
|
778
|
+
shimast.KindFalseKeyword,
|
|
779
|
+
shimast.KindNullKeyword,
|
|
780
|
+
shimast.KindThisKeyword,
|
|
781
|
+
shimast.KindElementAccessExpression:
|
|
782
|
+
return true
|
|
783
|
+
case shimast.KindParenthesizedExpression:
|
|
784
|
+
if p := node.AsParenthesizedExpression(); p != nil {
|
|
785
|
+
return isSimpleCallArg(p.Expression, depth)
|
|
786
|
+
}
|
|
787
|
+
case shimast.KindNonNullExpression:
|
|
788
|
+
// Prettier's isSimpleCallArgument unwraps TSNonNullExpression at the same
|
|
789
|
+
// depth, so `foo!()` / `target!` recurse into the asserted expression.
|
|
790
|
+
if n := node.AsNonNullExpression(); n != nil {
|
|
791
|
+
return isSimpleCallArg(n.Expression, depth)
|
|
792
|
+
}
|
|
793
|
+
case shimast.KindPrefixUnaryExpression:
|
|
794
|
+
if u := node.AsPrefixUnaryExpression(); u != nil {
|
|
795
|
+
return isSimpleCallArg(u.Operand, depth)
|
|
796
|
+
}
|
|
797
|
+
case shimast.KindPropertyAccessExpression:
|
|
798
|
+
// The `.name` half is an identifier (trivially simple); recurse the object
|
|
799
|
+
// so `a.b(x, y).c` (a member of a multi-argument call) is correctly not
|
|
800
|
+
// simple, matching Prettier's member recursion.
|
|
801
|
+
if m := node.AsPropertyAccessExpression(); m != nil {
|
|
802
|
+
return isSimpleCallArg(m.Expression, depth)
|
|
803
|
+
}
|
|
804
|
+
case shimast.KindArrayLiteralExpression:
|
|
805
|
+
if arr := node.AsArrayLiteralExpression(); arr != nil {
|
|
806
|
+
if arr.Elements == nil {
|
|
807
|
+
return true
|
|
808
|
+
}
|
|
809
|
+
for _, el := range arr.Elements.Nodes {
|
|
810
|
+
if !isSimpleCallArg(el, depth-1) {
|
|
811
|
+
return false
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return true
|
|
815
|
+
}
|
|
816
|
+
case shimast.KindObjectLiteralExpression:
|
|
817
|
+
// Only an empty object is treated as simple. A non-empty object's values
|
|
818
|
+
// are checked at depth-1, which bottoms out to false at every depth that
|
|
819
|
+
// occurs as a trailing-call argument, so declining one never over-matches.
|
|
820
|
+
if obj := node.AsObjectLiteralExpression(); obj != nil {
|
|
821
|
+
return obj.Properties == nil || len(obj.Properties.Nodes) == 0
|
|
822
|
+
}
|
|
823
|
+
case shimast.KindCallExpression:
|
|
824
|
+
if c := node.AsCallExpression(); c != nil {
|
|
825
|
+
var args []*shimast.Node
|
|
826
|
+
if c.Arguments != nil {
|
|
827
|
+
args = c.Arguments.Nodes
|
|
828
|
+
}
|
|
829
|
+
if !isSimpleCallArg(c.Expression, depth) || len(args) > depth {
|
|
830
|
+
return false
|
|
831
|
+
}
|
|
832
|
+
for _, a := range args {
|
|
833
|
+
if !isSimpleCallArg(a, depth-1) {
|
|
834
|
+
return false
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return true
|
|
838
|
+
}
|
|
839
|
+
case shimast.KindNewExpression:
|
|
840
|
+
if n := node.AsNewExpression(); n != nil {
|
|
841
|
+
var args []*shimast.Node
|
|
842
|
+
if n.Arguments != nil {
|
|
843
|
+
args = n.Arguments.Nodes
|
|
844
|
+
}
|
|
845
|
+
if !isSimpleCallArg(n.Expression, depth) || len(args) > depth {
|
|
846
|
+
return false
|
|
847
|
+
}
|
|
848
|
+
for _, a := range args {
|
|
849
|
+
if !isSimpleCallArg(a, depth-1) {
|
|
850
|
+
return false
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return true
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return false
|
|
857
|
+
}
|
|
858
|
+
|
|
615
859
|
// isSimpleBinaryOperand reports whether a node is a short, non-recursive
|
|
616
860
|
// operand, an identifier, a literal, `this`, or a member access. A binary,
|
|
617
861
|
// call, conditional, or other compound expression is not, so a binary built
|
|
@@ -636,24 +880,48 @@ func isSimpleBinaryOperand(node *shimast.Node) bool {
|
|
|
636
880
|
return false
|
|
637
881
|
}
|
|
638
882
|
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
883
|
+
// isSimpleCastType ports the type half of Prettier's isHopefullyShortCallArgument
|
|
884
|
+
// cast branch: unwrap an array type up to two levels (`T[]`, `T[][]` -> `T`) and
|
|
885
|
+
// a single-type-argument reference (`Ref<X>` -> `X`), then require a SIMPLE type
|
|
886
|
+
// (isSimpleTypeNode). A reference still carrying type arguments after the unwrap
|
|
887
|
+
// (`Foo<A, B>`, `Array<Array<string>>`), or a union / intersection / function /
|
|
888
|
+
// tuple / object type, is not simple, so the cast declines the first-argument
|
|
889
|
+
// hug and the list explodes, matching Prettier.
|
|
890
|
+
func isSimpleCastType(typeNode *shimast.Node) bool {
|
|
891
|
+
node := typeNode
|
|
892
|
+
for i := 0; i < 2 && node != nil && node.Kind == shimast.KindArrayType; i++ {
|
|
893
|
+
at := node.AsArrayTypeNode()
|
|
894
|
+
if at == nil || at.ElementType == nil {
|
|
895
|
+
return false
|
|
896
|
+
}
|
|
897
|
+
node = at.ElementType
|
|
651
898
|
}
|
|
652
|
-
|
|
653
|
-
if
|
|
654
|
-
|
|
899
|
+
if node != nil && node.Kind == shimast.KindTypeReference {
|
|
900
|
+
if ref := node.AsTypeReferenceNode(); ref != nil && ref.TypeArguments != nil &&
|
|
901
|
+
len(ref.TypeArguments.Nodes) == 1 {
|
|
902
|
+
node = ref.TypeArguments.Nodes[0]
|
|
655
903
|
}
|
|
656
904
|
}
|
|
905
|
+
return isSimpleTypeNode(node)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// isSimpleTypeNode mirrors Prettier's isSimpleType: a keyword/primitive type, a
|
|
909
|
+
// literal type, `this`, or a bare type reference with NO type arguments.
|
|
910
|
+
func isSimpleTypeNode(node *shimast.Node) bool {
|
|
911
|
+
if node == nil {
|
|
912
|
+
return false
|
|
913
|
+
}
|
|
914
|
+
switch node.Kind {
|
|
915
|
+
case shimast.KindStringKeyword, shimast.KindNumberKeyword, shimast.KindBooleanKeyword,
|
|
916
|
+
shimast.KindAnyKeyword, shimast.KindUnknownKeyword, shimast.KindVoidKeyword,
|
|
917
|
+
shimast.KindNeverKeyword, shimast.KindUndefinedKeyword, shimast.KindNullKeyword,
|
|
918
|
+
shimast.KindObjectKeyword, shimast.KindSymbolKeyword, shimast.KindBigIntKeyword,
|
|
919
|
+
shimast.KindThisType, shimast.KindLiteralType, shimast.KindTemplateLiteralType:
|
|
920
|
+
return true
|
|
921
|
+
case shimast.KindTypeReference:
|
|
922
|
+
ref := node.AsTypeReferenceNode()
|
|
923
|
+
return ref != nil && (ref.TypeArguments == nil || len(ref.TypeArguments.Nodes) == 0)
|
|
924
|
+
}
|
|
657
925
|
return false
|
|
658
926
|
}
|
|
659
927
|
|
|
@@ -113,6 +113,16 @@ func printParenthesizedExpression(ctx *PrintContext, node *shimast.Node) (Doc, b
|
|
|
113
113
|
if paren == nil || paren.Expression == nil {
|
|
114
114
|
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
115
115
|
}
|
|
116
|
+
// A comment around the parens (`(/* c */ x)`, `(x /* c */)`) would be dropped
|
|
117
|
+
// by the minted `(`/`)` — the parens are not AST children, so a nested
|
|
118
|
+
// ParenthesizedExpression's gap comment slips past the top-level print-width
|
|
119
|
+
// scan and is lost on reflow. Bail to verbatim and report UNCOVERED (hard
|
|
120
|
+
// `false`, not `!nodeSpansMultipleLines`) so an enclosing reflow abstains
|
|
121
|
+
// instead of breaking around this single-line verbatim paren, like the
|
|
122
|
+
// object/array/call printers.
|
|
123
|
+
if listHasInterItemComments(ctx, node) {
|
|
124
|
+
return verbatim(ctx, node), false
|
|
125
|
+
}
|
|
116
126
|
inner, covered := PrintNode(ctx, paren.Expression)
|
|
117
127
|
return Concat(Text("("), inner, Text(")")), covered
|
|
118
128
|
}
|
|
@@ -344,6 +354,14 @@ func printReturnStatement(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
344
354
|
if !tailIsCleanTerminator(ctx.Source, stmt.Expression.End(), node.End()) {
|
|
345
355
|
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
346
356
|
}
|
|
357
|
+
// A comment between `return` and its argument (`return /* c */ x`) lives in
|
|
358
|
+
// the argument's leading trivia and would be dropped by the minted
|
|
359
|
+
// `Text("return ")` — the gap is not an AST child, and a return statement is
|
|
360
|
+
// only reached as a nested child of a block, so the outer print-width and
|
|
361
|
+
// block scans mask it. Bail to verbatim (uncovered) like the round-7 printers.
|
|
362
|
+
if gapHasComment(ctx.Source, stmt.Expression.Pos(), shimscanner.SkipTrivia(ctx.Source, stmt.Expression.Pos())) {
|
|
363
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
364
|
+
}
|
|
347
365
|
exprDoc, covered := PrintNode(ctx, stmt.Expression)
|
|
348
366
|
parts := []Doc{Text("return "), exprDoc}
|
|
349
367
|
if sourceHasStatementTerminator(ctx.Source, node.End()) {
|
|
@@ -2,6 +2,7 @@ package linthost
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
5
6
|
)
|
|
6
7
|
|
|
7
8
|
// printNamedImports renders `{ a, b, c }` inside an import declaration.
|
|
@@ -142,9 +143,25 @@ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
142
143
|
// prefix forms are exclusive.
|
|
143
144
|
prefix := "import "
|
|
144
145
|
if clause.IsTypeOnly() {
|
|
146
|
+
// A comment in the `type`->`{` gap (`import type /* c */ { … }`) would be
|
|
147
|
+
// dropped by the minted prefix. The `type` keyword is a clause modifier
|
|
148
|
+
// flag, not a child node, so the top-level scan masks it (it sits inside the
|
|
149
|
+
// ImportClause span) and the `nb`-scoped guard starts at `{`. Bail to
|
|
150
|
+
// verbatim, like the default-binding gap below.
|
|
151
|
+
if gapHasComment(ctx.Source, shimscanner.SkipTrivia(ctx.Source, clause.Pos()), shimscanner.SkipTrivia(ctx.Source, nb.Pos())) {
|
|
152
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
153
|
+
}
|
|
145
154
|
prefix = "import type "
|
|
146
155
|
}
|
|
147
156
|
if name := clause.Name(); name != nil {
|
|
157
|
+
// A comment in the default-binding-to-brace gap (`import D /* c */, { … }`)
|
|
158
|
+
// would be dropped by the minted `, ` prefix. That gap is not a direct child
|
|
159
|
+
// of the import node, so the top-level scan masks it and the `nb`-scoped
|
|
160
|
+
// guard above starts at `{` — same class as printReturnStatement's leading
|
|
161
|
+
// gap. Bail to verbatim.
|
|
162
|
+
if gapHasComment(ctx.Source, name.End(), shimscanner.SkipTrivia(ctx.Source, nb.Pos())) {
|
|
163
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
164
|
+
}
|
|
148
165
|
prefix = "import " + identifierText(name) + ", "
|
|
149
166
|
}
|
|
150
167
|
|
|
@@ -66,6 +66,22 @@ type listShape struct {
|
|
|
66
66
|
// `callback, simpleArg` shape Prettier hugs (`foo(() => { … }, x)`).
|
|
67
67
|
// HugLast and HugFirst are mutually exclusive.
|
|
68
68
|
HugFirst bool
|
|
69
|
+
// HugFirstTrailingBreaks tells printListHuggingFirst NOT to flatten the
|
|
70
|
+
// trailing item, letting it render through its own breakable group. The
|
|
71
|
+
// call-argument printer sets it for the React-hook deps shape
|
|
72
|
+
// (`useEffect(() => { … }, [deps])`), whose deps array Prettier breaks
|
|
73
|
+
// one-element-per-line when it overflows rather than pinning it flat on the
|
|
74
|
+
// close line. The short leaf/call trailing args of every other first-arg-hug
|
|
75
|
+
// case still flatten (they ride the close line flat, per Prettier).
|
|
76
|
+
HugFirstTrailingBreaks bool
|
|
77
|
+
// HugFirstForce drops the exploded fallback from a HugFirst list, the mirror
|
|
78
|
+
// of HugLastForce. The call-argument printer sets it for a genuine React-hook
|
|
79
|
+
// deps call (`useEffect(() => { … }, [deps])`: a zero-parameter block-bodied
|
|
80
|
+
// arrow plus an array literal), which Prettier's isReactHookCallWithDepsArray
|
|
81
|
+
// path never explodes — it keeps the callback hugged and lets the open line
|
|
82
|
+
// overflow. Distinct from HugFirstTrailingBreaks, which also fires for a
|
|
83
|
+
// parameterized callback whose empty-array second arg DOES keep the fallback.
|
|
84
|
+
HugFirstForce bool
|
|
69
85
|
// HugLastForce drops the exploded fallback from a HugLast list: the hugged
|
|
70
86
|
// shape is chosen even when its opening line overflows printWidth. The
|
|
71
87
|
// call-argument printer sets it for a test-framework call
|
|
@@ -153,10 +169,12 @@ func printList(ctx *PrintContext, shape listShape) Doc {
|
|
|
153
169
|
if shape.HugFirst {
|
|
154
170
|
hugged = printListHuggingFirst(ctx, shape)
|
|
155
171
|
}
|
|
156
|
-
// A test-framework call
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
172
|
+
// A test-framework call (HugLastForce) and a genuine React-hook deps call
|
|
173
|
+
// (HugFirstForce, `useEffect(() => { … }, [deps])`) hug their callback
|
|
174
|
+
// unconditionally — Prettier never explodes their arguments, it lets the open
|
|
175
|
+
// line overflow — so drop the exploded fallback. The all-flat option still
|
|
176
|
+
// wins when the whole call fits (an empty-body callback).
|
|
177
|
+
if shape.HugLastForce || shape.HugFirstForce {
|
|
160
178
|
if allFlat, ok := flatten(plain); ok {
|
|
161
179
|
return ConditionalGroup(allFlat, hugged)
|
|
162
180
|
}
|
|
@@ -317,9 +335,14 @@ func printListHuggingFirst(ctx *PrintContext, shape listShape) Doc {
|
|
|
317
335
|
// The trailing simple arguments ride the close line flat — Prettier keeps
|
|
318
336
|
// them on one line even when that line overflows (a long zero/one-argument
|
|
319
337
|
// trailing call `}, makeAccumulator(single))` is not broken). Force each
|
|
320
|
-
// flat so its own Group does not break against the close-line width.
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
// flat so its own Group does not break against the close-line width. The
|
|
339
|
+
// React-hook deps array is the exception (HugFirstTrailingBreaks): Prettier
|
|
340
|
+
// renders it through a breakable group, so leave it unflattened to break
|
|
341
|
+
// one-element-per-line when the close line overflows.
|
|
342
|
+
if !shape.HugFirstTrailingBreaks {
|
|
343
|
+
if flat, ok := flatten(item); ok {
|
|
344
|
+
item = flat
|
|
345
|
+
}
|
|
323
346
|
}
|
|
324
347
|
parts = append(parts, Text(", "), item)
|
|
325
348
|
}
|
|
@@ -36,9 +36,11 @@ func printObjectLiteral(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
36
36
|
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
37
37
|
}
|
|
38
38
|
// A comment between properties (or after `{`) would be dropped by the fresh
|
|
39
|
-
// separators; bail to verbatim
|
|
39
|
+
// separators; bail to verbatim and report UNCOVERED (hard `false`, not
|
|
40
|
+
// `!nodeSpansMultipleLines`) so an enclosing reflow abstains instead of
|
|
41
|
+
// breaking around this single-line verbatim object and moving it off its line.
|
|
40
42
|
if listHasInterItemComments(ctx, node) {
|
|
41
|
-
return verbatim(ctx, node),
|
|
43
|
+
return verbatim(ctx, node), false
|
|
42
44
|
}
|
|
43
45
|
items := make([]Doc, 0, len(obj.Properties.Nodes))
|
|
44
46
|
covered := true
|
|
@@ -20,9 +20,11 @@ import (
|
|
|
20
20
|
// (`a ? b : c ? d : e`) or every rung breaks onto its own line. Nesting
|
|
21
21
|
// is expressed by recursing the chain builder without wrapping the
|
|
22
22
|
// nested conditional in its own Group. The outermost chain indents its arms
|
|
23
|
-
// by `tabWidth
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// by `tabWidth`. A nested chain in the ALTERNATE (`: `) position indents by a
|
|
24
|
+
// fixed 2 columns; a nested chain in the CONSEQUENT (`? `) position indents by
|
|
25
|
+
// `max(2, tabWidth)` (Prettier's extra `align(tabWidth-2)` on the consequent).
|
|
26
|
+
// So at tabWidth 4 the outer arms sit at column 4, an alternate-nested arm at
|
|
27
|
+
// column 6, and a consequent-nested arm at column 8; they coincide at tabWidth 2.
|
|
26
28
|
//
|
|
27
29
|
// The second return value is the coverage flag (see PrintNode): the AND
|
|
28
30
|
// of the test and both branches, so a multi-line verbatim node anywhere
|
|
@@ -46,6 +48,16 @@ func buildConditionalChain(ctx *PrintContext, node *shimast.Node, indentCols int
|
|
|
46
48
|
if cond == nil || cond.Condition == nil || cond.WhenTrue == nil || cond.WhenFalse == nil {
|
|
47
49
|
return Doc{}, true
|
|
48
50
|
}
|
|
51
|
+
// A comment around the `?`/`:` markers (`a ? /* c */ b : c`) would be dropped
|
|
52
|
+
// by the minted "? "/": " text — the markers are not AST children, so a
|
|
53
|
+
// nested conditional's gap comment slips past the top-level print-width scan
|
|
54
|
+
// and is lost on reflow. Bail to verbatim and report UNCOVERED (hard `false`,
|
|
55
|
+
// not `!nodeSpansMultipleLines`) so an enclosing reflow abstains instead of
|
|
56
|
+
// breaking around this single-line verbatim conditional and moving it off its
|
|
57
|
+
// line; the bytes survive.
|
|
58
|
+
if listHasInterItemComments(ctx, node) {
|
|
59
|
+
return verbatim(ctx, node), false
|
|
60
|
+
}
|
|
49
61
|
testDoc, c1 := PrintNode(ctx, cond.Condition)
|
|
50
62
|
consDoc, c2 := ternaryArm(ctx, cond.WhenTrue, true)
|
|
51
63
|
altDoc, c3 := ternaryArm(ctx, cond.WhenFalse, false)
|
|
@@ -87,10 +99,39 @@ func buildConditionalChain(ctx *PrintContext, node *shimast.Node, indentCols int
|
|
|
87
99
|
// the broken staircase. A nested conditional in the ALTERNATE (`: `) position
|
|
88
100
|
// is never wrapped — it chains. `isConsequent` selects between the two.
|
|
89
101
|
func ternaryArm(ctx *PrintContext, node *shimast.Node, isConsequent bool) (Doc, bool) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
inner := node
|
|
103
|
+
// A nested ternary written with explicit source parentheses — `a ? (b ? c : d)
|
|
104
|
+
// : e` — is the same chain link as a bare nested ternary: Prettier's AST has no
|
|
105
|
+
// ParenthesizedExpression node, so its `printTernary` sees the consequent as a
|
|
106
|
+
// ConditionalExpression and joins the staircase (re-adding the parens only in
|
|
107
|
+
// flat mode via ifBreak). Unwrap a parenthesized conditional so it chains too,
|
|
108
|
+
// instead of printing flat inside kept parens. Only a ConditionalExpression
|
|
109
|
+
// inner is unwrapped; `(a ?? b)` and other parenthesized expressions keep their
|
|
110
|
+
// parens through the normal printer.
|
|
111
|
+
// Do NOT unwrap when a comment sits between the parens and the inner
|
|
112
|
+
// conditional (`(/* keep */ b ? c : d)`): dropping the ParenExpr wrapper would
|
|
113
|
+
// delete that comment. Leaving `inner` as the ParenExpr routes it through the
|
|
114
|
+
// normal printer, whose own self-guard bails to verbatim and preserves it.
|
|
115
|
+
if inner != nil && inner.Kind == shimast.KindParenthesizedExpression &&
|
|
116
|
+
!listHasInterItemComments(ctx, inner) {
|
|
117
|
+
if p := inner.AsParenthesizedExpression(); p != nil && p.Expression != nil &&
|
|
118
|
+
p.Expression.Kind == shimast.KindConditionalExpression {
|
|
119
|
+
inner = p.Expression
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if inner != nil && inner.Kind == shimast.KindConditionalExpression {
|
|
123
|
+
// A nested chain's arms align at 2 past their parent rung. Prettier's
|
|
124
|
+
// ternary-old.js uses that as-is for an ALTERNATE-position nested chain
|
|
125
|
+
// (`align(2)`), but a CONSEQUENT-position one gets an extra
|
|
126
|
+
// `align(Math.max(0, tabWidth - 2))`, for a total of `max(2, tabWidth)`.
|
|
127
|
+
// The two coincide at the default tabWidth 2; they diverge at tabWidth > 2.
|
|
128
|
+
indentCols := 2
|
|
129
|
+
if isConsequent {
|
|
130
|
+
if u := ctx.indentUnit(); u > indentCols {
|
|
131
|
+
indentCols = u
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
chain, covered := buildConditionalChain(ctx, inner, indentCols)
|
|
94
135
|
if isConsequent {
|
|
95
136
|
// `ifBreak("", "(")` … `ifBreak("", ")")`: parens in flat mode only.
|
|
96
137
|
chain = Concat(
|
|
@@ -77,6 +77,17 @@ func (formatArrowParens) Check(ctx *Context, node *shimast.Node) {
|
|
|
77
77
|
return
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// A comment in the parameter region (leading trivia, or between the name and
|
|
81
|
+
// its `)`/`=>`) defeats the whitespace-only paren scan below: the scan stops
|
|
82
|
+
// at the comment byte and reports "not wrapped", so the "always" branch would
|
|
83
|
+
// wrap an already-parenthesized name a second time and emit invalid
|
|
84
|
+
// `(/* c */ (x)) => x`. Prettier leaves such an arrow alone
|
|
85
|
+
// (canPrintParamsWithoutParens requires `!hasComment(parameters[0])`), so
|
|
86
|
+
// abstain rather than corrupt.
|
|
87
|
+
if arrowParamRegionHasComment(src, param.Pos(), nameStart, nameEnd) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
// Is the parameter already wrapped in `(` … `)`? Scan over whitespace on
|
|
81
92
|
// each side; the sole parameter of an arrow is delimited by the
|
|
82
93
|
// parameter-list parens when present.
|
|
@@ -162,6 +173,41 @@ func scanForwardForByte(src string, from int, target byte) int {
|
|
|
162
173
|
return -1
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
// arrowParamRegionHasComment reports whether a `//` or `/*` comment sits in the
|
|
177
|
+
// single parameter's region — its leading trivia (`[paramPos, nameStart)`) or
|
|
178
|
+
// the trailing trivia after the name up to the first real token (`)` or `=>`).
|
|
179
|
+
// The byte scan is safe here because a bare-identifier parameter region holds
|
|
180
|
+
// only comments, whitespace, the identifier, and parentheses (no strings:
|
|
181
|
+
// typed/defaulted params are already excluded by isBareIdentifierParam).
|
|
182
|
+
func arrowParamRegionHasComment(src string, paramPos, nameStart, nameEnd int) bool {
|
|
183
|
+
for i := paramPos; i+1 < nameStart && i+1 < len(src); i++ {
|
|
184
|
+
if src[i] == '/' && (src[i+1] == '*' || src[i+1] == '/') {
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Trailing trivia after the identifier, up to `=>`. Tolerate one closing
|
|
189
|
+
// paren so a comment between `)` and `=>` (`(x) /* c */ => x`) — which in
|
|
190
|
+
// "avoid" mode would otherwise be stranded when the parens are stripped — is
|
|
191
|
+
// also detected (Prettier keeps the parens of an arrow with such a dangling
|
|
192
|
+
// comment).
|
|
193
|
+
sawParen := false
|
|
194
|
+
for i := nameEnd; i+1 < len(src); i++ {
|
|
195
|
+
c := src[i]
|
|
196
|
+
if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
if c == '/' && (src[i+1] == '*' || src[i+1] == '/') {
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
if c == ')' && !sawParen {
|
|
203
|
+
sawParen = true
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
|
|
165
211
|
func init() {
|
|
166
212
|
Register(formatArrowParens{})
|
|
167
213
|
}
|
|
@@ -96,6 +96,14 @@ func joinClauseBody(
|
|
|
96
96
|
if body == nil || body.Kind == shimast.KindBlock {
|
|
97
97
|
return
|
|
98
98
|
}
|
|
99
|
+
// An empty-statement body (`while (x)\n;`) glues directly to the header with
|
|
100
|
+
// NO space: Prettier's adjustClause special-cases EmptyStatement and returns
|
|
101
|
+
// the bare `;` (`while (x);`), only prepending a space when the empty
|
|
102
|
+
// statement carries a leading comment. This rule's gap->" " rewrite cannot
|
|
103
|
+
// produce the spaceless `);` glue, so abstain and leave the source shape.
|
|
104
|
+
if body.Kind == shimast.KindEmptyStatement {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
99
107
|
bodyStart := shimscanner.SkipTrivia(src, body.Pos())
|
|
100
108
|
bodyEnd := body.End()
|
|
101
109
|
if bodyStart < 0 || bodyEnd < bodyStart || bodyEnd > len(src) {
|
|
@@ -149,12 +157,11 @@ func isClauseGapByte(c byte) bool {
|
|
|
149
157
|
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
// visualWidth returns the column width of `s
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// approximation never changes a real join decision.
|
|
160
|
+
// visualWidth returns the display-column width of `s`: a tab expands to a flat
|
|
161
|
+
// `tabWidth` columns and every other rune is charged its display width via
|
|
162
|
+
// runeWidth (combining marks 0, wide East-Asian/emoji 2), matching displayWidth.
|
|
163
|
+
// The only approximation left is the flat tab expansion (no tab-stop rounding),
|
|
164
|
+
// which never changes a real clause-join decision.
|
|
158
165
|
func visualWidth(s string, tabWidth int) int {
|
|
159
166
|
width := 0
|
|
160
167
|
for _, r := range s {
|
|
@@ -93,11 +93,6 @@ func (formatPrintWidth) Visits() []shimast.Kind {
|
|
|
93
93
|
// re-render still over-breaks some of these; the next slice should
|
|
94
94
|
// measure fitsFirstLine against `pw - col - trailingNonComment`
|
|
95
95
|
// before committing to the broken layout.
|
|
96
|
-
// - Default-combined imports (`import D, { X } from "x"`) are kept
|
|
97
|
-
// verbatim by printImportDeclaration (its `clause.Name() != nil`
|
|
98
|
-
// guard), so a default import whose named clause overflows is not
|
|
99
|
-
// broken; Prettier breaks it. Lifting the guard, threading the default
|
|
100
|
-
// name into the printed prefix, is a future default-import reflow slice.
|
|
101
96
|
//
|
|
102
97
|
// (A single-specifier `import/export { X } from "long-path"` that overflows
|
|
103
98
|
// only because of the `from "..."` tail is now kept inline by the
|
|
@@ -162,15 +157,15 @@ func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
|
162
157
|
// the per-byte comment scan in hasNonChildComments. Charging that
|
|
163
158
|
// cost only on nodes that actually overflow keeps the hot path on
|
|
164
159
|
// well-formatted code allocation- and scan-free.
|
|
165
|
-
// A call with two or more
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
160
|
+
// A call/new with two or more callback arguments explodes regardless of width
|
|
161
|
+
// (Prettier's multiple-callback rule), and an array of same-kind multi-child
|
|
162
|
+
// arrays/objects explodes under Prettier's shouldBreak heuristic — even when
|
|
163
|
+
// such a node is nested inside an otherwise-fitting call/new/array. So a flat
|
|
164
|
+
// one-line node containing either shape still needs a reflow. fastPathForcesBreak
|
|
165
|
+
// walks the reflow subtree; skip the fast path for it so the printer's
|
|
166
|
+
// ForceBreak produces the exploded shape. Everything else that fits flat is
|
|
167
|
+
// byte-identical after reflow, so the fast path stands.
|
|
168
|
+
if !fastPathForcesBreak(node) &&
|
|
174
169
|
!sliceContainsNewline(src, start, end) &&
|
|
175
170
|
printOpts.StartingColumn+displayWidth(src[start:end])+trailingWidth <= printOpts.PrintWidth {
|
|
176
171
|
return
|
|
@@ -359,6 +359,12 @@ type siSpec struct {
|
|
|
359
359
|
sortKey string
|
|
360
360
|
text string
|
|
361
361
|
fromTypeOnlyDecl bool
|
|
362
|
+
// inlineType is the specifier's own AST `type` modifier (`import { type Foo }`),
|
|
363
|
+
// distinct from fromTypeOnlyDecl (the whole `import type { … }` declaration).
|
|
364
|
+
// It classifies a specifier without re-parsing its text, so a binding literally
|
|
365
|
+
// named `type` (`import { type as bar }` -> name `type`, alias `bar`, NOT a type
|
|
366
|
+
// specifier) is not mistaken for a type modifier by a `"type "` string prefix.
|
|
367
|
+
inlineType bool
|
|
362
368
|
}
|
|
363
369
|
|
|
364
370
|
// siDecl is the parsed shape of one import declaration used by the block
|
|
@@ -371,8 +377,13 @@ type siDecl struct {
|
|
|
371
377
|
defaultName string
|
|
372
378
|
named []siSpec
|
|
373
379
|
namespace bool
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
// hasSpecComment marks a declaration whose named-import braces carry a comment.
|
|
381
|
+
// The merge rebuilder joins specifier texts with ", " and would drop such a
|
|
382
|
+
// comment, so a flagged declaration is made unmergeable (like a namespace
|
|
383
|
+
// binding) and re-emitted from its original text. See mergeKey.
|
|
384
|
+
hasSpecComment bool
|
|
385
|
+
original string
|
|
386
|
+
semicolon bool
|
|
376
387
|
}
|
|
377
388
|
|
|
378
389
|
// siEntry is a (possibly merged) import declaration ready to be grouped,
|
|
@@ -451,6 +462,42 @@ func parseImportDecl(src string, decl *shimast.Node) siDecl {
|
|
|
451
462
|
}
|
|
452
463
|
out.typeOnly = clause.PhaseModifier == shimast.KindTypeKeyword
|
|
453
464
|
out.defaultName = identifierText(clause.Name())
|
|
465
|
+
// A comment anywhere in the rebuilt import prefix (`import [type] [D] [, { … }]
|
|
466
|
+
// from `) would be lost when renderMergedDecl reconstructs the statement
|
|
467
|
+
// field-by-field. Flag it so mergeKey keeps the declaration unmergeable and
|
|
468
|
+
// its original text is preserved. This runs BEFORE the no-named-bindings /
|
|
469
|
+
// namespace early returns so a default-only import (`import a /* c */ from
|
|
470
|
+
// "m"`) is flagged too; it depends only on decl.Pos() and the module
|
|
471
|
+
// specifier, not on the named bindings. The prefix holds no module-path
|
|
472
|
+
// string, so a `//` or `/*` there is unambiguously a comment (a leading
|
|
473
|
+
// comment before `import` is excluded: SkipTrivia advances to `import`).
|
|
474
|
+
if imp.ModuleSpecifier != nil {
|
|
475
|
+
pStart := shimscanner.SkipTrivia(src, decl.Pos())
|
|
476
|
+
pEnd := shimscanner.SkipTrivia(src, imp.ModuleSpecifier.Pos())
|
|
477
|
+
if pStart >= 0 && pEnd <= len(src) && pStart < pEnd {
|
|
478
|
+
if span := src[pStart:pEnd]; strings.Contains(span, "//") || strings.Contains(span, "/*") {
|
|
479
|
+
out.hasSpecComment = true
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Also the module-specifier -> `;` tail (`from "m" /* keep */;`): a block
|
|
483
|
+
// comment there is interior to the declaration but outside the
|
|
484
|
+
// inter-declaration gap that leadingTriviaIsAllWhitespace scans, so the
|
|
485
|
+
// merge rebuilder would drop it. The module string itself is excluded (the
|
|
486
|
+
// span starts at ModuleSpecifier.End()), so no `//`-in-a-path false positive.
|
|
487
|
+
if tStart, tEnd := imp.ModuleSpecifier.End(), decl.End(); tStart >= 0 && tEnd <= len(src) && tStart < tEnd {
|
|
488
|
+
if span := src[tStart:tEnd]; strings.Contains(span, "//") || strings.Contains(span, "/*") {
|
|
489
|
+
out.hasSpecComment = true
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// An import-attributes clause (`with { type: "json" }` / `assert { … }`) is not
|
|
494
|
+
// reconstructed by renderMergedDecl, so merging would silently drop those
|
|
495
|
+
// bytes. Flag the declaration unmergeable (its original text is then re-emitted
|
|
496
|
+
// verbatim), mirroring the print-width import printer, which bails to verbatim
|
|
497
|
+
// on `imp.Attributes != nil` for the same reason.
|
|
498
|
+
if imp.Attributes != nil {
|
|
499
|
+
out.hasSpecComment = true
|
|
500
|
+
}
|
|
454
501
|
if clause.NamedBindings == nil {
|
|
455
502
|
return out
|
|
456
503
|
}
|
|
@@ -475,6 +522,7 @@ func parseImportDecl(src string, decl *shimast.Node) siDecl {
|
|
|
475
522
|
sortKey: identifierText(s.Name()),
|
|
476
523
|
text: src[start:spec.End()],
|
|
477
524
|
fromTypeOnlyDecl: out.typeOnly,
|
|
525
|
+
inlineType: s.IsTypeOnly,
|
|
478
526
|
})
|
|
479
527
|
}
|
|
480
528
|
return out
|
|
@@ -509,6 +557,11 @@ func mergeKey(d siDecl, combine bool) string {
|
|
|
509
557
|
if d.namespace {
|
|
510
558
|
return "\x00ns\x00" + d.original
|
|
511
559
|
}
|
|
560
|
+
// A named-import list carrying a comment is unmergeable so the comment
|
|
561
|
+
// survives in the declaration's original text (a per-declaration key).
|
|
562
|
+
if d.hasSpecComment {
|
|
563
|
+
return "\x00cmt\x00" + d.original
|
|
564
|
+
}
|
|
512
565
|
if combine || !d.typeOnly {
|
|
513
566
|
return "v\x00" + d.specifier
|
|
514
567
|
}
|
|
@@ -613,10 +666,18 @@ func collectMergedSpecs(group []siDecl, mergedTypeOnly, caseSensitive bool) []st
|
|
|
613
666
|
for _, d := range group {
|
|
614
667
|
for _, s := range d.named {
|
|
615
668
|
text := s.text
|
|
616
|
-
|
|
669
|
+
// Classify by AST flags, not a `"type "` text prefix. A binding literally
|
|
670
|
+
// named `type` (`import { type as bar }`) has inlineType=false and must stay
|
|
671
|
+
// a value specifier; a string-prefix check would mis-promote it to a type
|
|
672
|
+
// specifier and skip the modifier when folding a type-only declaration.
|
|
673
|
+
isType := s.inlineType
|
|
674
|
+
if s.fromTypeOnlyDecl && !mergedTypeOnly {
|
|
675
|
+
// A specifier from `import type { … }` carries no inline `type` modifier
|
|
676
|
+
// (TS forbids it there), so folding into a mixed value import must add one.
|
|
617
677
|
text = "type " + text
|
|
678
|
+
isType = true
|
|
618
679
|
}
|
|
619
|
-
cur := spec{name: s.sortKey, isType:
|
|
680
|
+
cur := spec{name: s.sortKey, isType: isType, text: text}
|
|
620
681
|
if at, dup := index[s.sortKey]; dup {
|
|
621
682
|
if items[at].isType && !cur.isType {
|
|
622
683
|
items[at] = cur
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Reference ttsc plugin: ESLint-style lint rules hosted in the same Program/Checker as the type-check pass.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@typescript/native-preview": "7.0.0-dev.20260519.1",
|
|
37
37
|
"@types/node": "^25.3.0",
|
|
38
38
|
"rimraf": "^6.1.2",
|
|
39
|
-
"ttsc": "0.14.
|
|
39
|
+
"ttsc": "0.14.1"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|