@ttsc/lint 0.26.0 → 0.26.2
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 +4 -2
- package/lib/index.js +62 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/format/ITtscLintFormat.d.ts +4 -3
- package/linthost/config.go +243 -9
- package/linthost/rules_format_semi.go +500 -63
- package/package.json +2 -2
- package/src/index.ts +70 -2
- package/src/structures/format/ITtscLintFormat.ts +4 -3
|
@@ -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
|
// formatSemi controls trailing-semicolon style on ASI statements.
|
|
@@ -15,6 +16,46 @@ import (
|
|
|
15
16
|
// optional semicolon. Body-shaped declarations (functions, classes,
|
|
16
17
|
// namespaces, enums) and control-flow statements (if/for/while/try)
|
|
17
18
|
// are out of scope because they parse correctly without a terminator.
|
|
19
|
+
//
|
|
20
|
+
// Interface, type-literal, and class members do not take the statement
|
|
21
|
+
// path. Both directions route through member-specific code
|
|
22
|
+
// (stripMemberSemicolon, insertMemberSemicolon) that reads the written
|
|
23
|
+
// line structure and the list's own wrap rather than the member kind,
|
|
24
|
+
// because Prettier prints a member's `;` between two members in either
|
|
25
|
+
// layout and after the last one only where the list breaks.
|
|
26
|
+
//
|
|
27
|
+
// That makes this rule the owner of a type member's SEPARATOR, not just
|
|
28
|
+
// of a terminator, and the whole member surface follows from one rule
|
|
29
|
+
// measured against pinned Prettier 3.8.3. Prettier prints an interface or
|
|
30
|
+
// type-literal separator as `ifBreak(semi, ";")` and its trailing one
|
|
31
|
+
// inside a further `ifBreak`, which resolves to two answers:
|
|
32
|
+
//
|
|
33
|
+
// - BETWEEN two members: `;` whenever `semi` is on OR the list is laid
|
|
34
|
+
// out flat, because the flat branch of that `ifBreak` is a literal
|
|
35
|
+
// `";"` whatever `semi` says.
|
|
36
|
+
// - AFTER the last member: `;` only when `semi` is on AND the list
|
|
37
|
+
// breaks; nothing in all three other combinations.
|
|
38
|
+
//
|
|
39
|
+
// Two consequences follow that would otherwise have to be inferred.
|
|
40
|
+
//
|
|
41
|
+
// A `,` is the same separator spelled the other way. TypeScript accepts
|
|
42
|
+
// either in a type member list and the parser folds either into the
|
|
43
|
+
// member, so both answers above apply to it unchanged. Every separator
|
|
44
|
+
// Prettier keeps it spells `;`, in both layouts and both modes, so a `,`
|
|
45
|
+
// this rule does not drop it normalizes rather than leaves as written.
|
|
46
|
+
// That is a separator normalization, not a terminator insert, and neither
|
|
47
|
+
// direction needs a line-structure test to make it.
|
|
48
|
+
//
|
|
49
|
+
// A type member of a body still written on one line is never terminated,
|
|
50
|
+
// so `interface D { (a: number): void }` keeps its bare call signature.
|
|
51
|
+
// Prettier reaches its own terminator by breaking the body first, and
|
|
52
|
+
// breaking it is format/indent's decision, which today covers property,
|
|
53
|
+
// method, and index signatures but not a call or construct signature.
|
|
54
|
+
// Terminating a one-line member here instead would be a layout decision
|
|
55
|
+
// this rule does not own, and would emit a shape Prettier never does.
|
|
56
|
+
//
|
|
57
|
+
// A mapped type is neither a statement nor a member list; it takes its own
|
|
58
|
+
// path in mappedTypeSemicolon.
|
|
18
59
|
type formatSemi struct{ optionsRule }
|
|
19
60
|
|
|
20
61
|
// formatSemiOptions is the Go mirror of `TtscLintRuleOptions.Semi`. The
|
|
@@ -43,9 +84,11 @@ func (formatSemi) Visits() []shimast.Kind {
|
|
|
43
84
|
shimast.KindExportAssignment,
|
|
44
85
|
shimast.KindPropertyDeclaration,
|
|
45
86
|
shimast.KindTypeAliasDeclaration,
|
|
46
|
-
// Interface / type-literal members
|
|
47
|
-
//
|
|
48
|
-
//
|
|
87
|
+
// Interface / type-literal members, plus the class-member spellings
|
|
88
|
+
// the accessor and index-signature kinds share. Prettier's `;` here
|
|
89
|
+
// is a separator between two members and a trailing terminator only
|
|
90
|
+
// where the list breaks; see stripMemberSemicolon and
|
|
91
|
+
// insertMemberSemicolon for the per-direction rules.
|
|
49
92
|
shimast.KindPropertySignature,
|
|
50
93
|
shimast.KindMethodSignature,
|
|
51
94
|
shimast.KindIndexSignature,
|
|
@@ -53,6 +96,10 @@ func (formatSemi) Visits() []shimast.Kind {
|
|
|
53
96
|
shimast.KindConstructSignature,
|
|
54
97
|
shimast.KindGetAccessor,
|
|
55
98
|
shimast.KindSetAccessor,
|
|
99
|
+
// A mapped type spells its whole body as one clause instead of a
|
|
100
|
+
// member list, so no member node exists to carry its terminator and
|
|
101
|
+
// the kind itself has to be visited; see mappedTypeSemicolon.
|
|
102
|
+
shimast.KindMappedType,
|
|
56
103
|
}
|
|
57
104
|
}
|
|
58
105
|
|
|
@@ -69,12 +116,16 @@ func (formatSemi) Check(ctx *Context, node *shimast.Node) {
|
|
|
69
116
|
if end <= 0 || end > len(src) {
|
|
70
117
|
return
|
|
71
118
|
}
|
|
119
|
+
if node.Kind == shimast.KindMappedType {
|
|
120
|
+
mappedTypeSemicolon(ctx, src, node, preferNever)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
72
123
|
// Interface / type-literal members and class fields carry their own
|
|
73
|
-
// ASI rules, distinct from top-level statements, so
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
124
|
+
// ASI rules, distinct from top-level statements, so each direction
|
|
125
|
+
// routes through a dedicated member path. A class field keeps its
|
|
126
|
+
// existing always-direction insertion by falling through to the
|
|
127
|
+
// statement branch below: its body always breaks in Prettier, so it
|
|
128
|
+
// needs no line-structure test.
|
|
78
129
|
isClassField := node.Kind == shimast.KindPropertyDeclaration
|
|
79
130
|
isTypeMember := isTypeMemberKind(node.Kind)
|
|
80
131
|
if preferNever && (isClassField || isTypeMember) {
|
|
@@ -82,6 +133,7 @@ func (formatSemi) Check(ctx *Context, node *shimast.Node) {
|
|
|
82
133
|
return
|
|
83
134
|
}
|
|
84
135
|
if isTypeMember {
|
|
136
|
+
insertMemberSemicolon(ctx, src, node, end)
|
|
85
137
|
return
|
|
86
138
|
}
|
|
87
139
|
hasSemi := src[end-1] == ';'
|
|
@@ -282,11 +334,22 @@ func preferNeverSafeKind(kind shimast.Kind) bool {
|
|
|
282
334
|
}
|
|
283
335
|
|
|
284
336
|
// isTypeMemberKind reports whether `kind` is an interface or
|
|
285
|
-
// object-type-literal member whose trailing `;` Prettier
|
|
286
|
-
// semi:false
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
337
|
+
// object-type-literal member: the kinds whose trailing `;` Prettier
|
|
338
|
+
// strips under semi:false and inserts under semi:true.
|
|
339
|
+
//
|
|
340
|
+
// All seven take the same answer in both directions, which is measured
|
|
341
|
+
// rather than assumed from symmetry. The `format/semi` conformance cases
|
|
342
|
+
// run a property, method, index, call, and construct signature plus both
|
|
343
|
+
// accessors through pinned Prettier 3.8.3, and every one of them comes
|
|
344
|
+
// back terminated once its body is broken across lines.
|
|
345
|
+
// GetAccessor and SetAccessor also spell a class or object-literal
|
|
346
|
+
// accessor, which is not a type member at all; the context test in
|
|
347
|
+
// memberTakesSemicolonTerminator, not this predicate, separates those.
|
|
348
|
+
//
|
|
349
|
+
// Class fields (KindPropertyDeclaration) are handled separately because
|
|
350
|
+
// their initializer is an expression and so they carry the full
|
|
351
|
+
// expression-ASI hazard set, while type members only risk a
|
|
352
|
+
// call/construct-signature (`(`) or generic-call-signature (`<`)
|
|
290
353
|
// continuation.
|
|
291
354
|
func isTypeMemberKind(kind shimast.Kind) bool {
|
|
292
355
|
switch kind {
|
|
@@ -303,65 +366,108 @@ func isTypeMemberKind(kind shimast.Kind) bool {
|
|
|
303
366
|
return false
|
|
304
367
|
}
|
|
305
368
|
|
|
306
|
-
// stripMemberSemicolon
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
369
|
+
// stripMemberSemicolon settles the separator of an interface /
|
|
370
|
+
// type-literal member or a class field under semi:false.
|
|
371
|
+
//
|
|
372
|
+
// Two outcomes are possible, because semi:false silences only the
|
|
373
|
+
// separators Prettier itself would omit. A separator the oracle drops is
|
|
374
|
+
// removed; a separator it keeps is spelled `;`, so a `,` that survives is
|
|
375
|
+
// normalized instead of left as written. A `;` that survives is already
|
|
376
|
+
// in that spelling and produces no finding, which is also the
|
|
377
|
+
// idempotency guard: once removed there is nothing left to act on, and
|
|
378
|
+
// once normalized the `;` is what the next pass reads.
|
|
315
379
|
//
|
|
316
|
-
//
|
|
317
|
-
// memberSemicolonRedundant
|
|
318
|
-
// ASI-hazardous continuations keep their terminator. Idempotent: once
|
|
319
|
-
// removed, no `;` remains for the rule to act on.
|
|
380
|
+
// See findMemberSeparator for how the byte is located and
|
|
381
|
+
// memberSemicolonRedundant for the drop decision.
|
|
320
382
|
func stripMemberSemicolon(ctx *Context, src string, node *shimast.Node, isClassField bool) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
semiPos = end - 1
|
|
325
|
-
} else {
|
|
326
|
-
i := end
|
|
327
|
-
for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
|
|
328
|
-
i++
|
|
329
|
-
}
|
|
330
|
-
if i < len(src) && src[i] == ';' {
|
|
331
|
-
semiPos = i
|
|
332
|
-
}
|
|
383
|
+
sepPos := findMemberSeparator(src, node, isClassField)
|
|
384
|
+
if sepPos < 0 {
|
|
385
|
+
return
|
|
333
386
|
}
|
|
334
|
-
if
|
|
387
|
+
if memberSemicolonRedundant(src, node, sepPos+1, isClassField) {
|
|
388
|
+
message := "Unexpected trailing semicolon."
|
|
389
|
+
if src[sepPos] == ',' {
|
|
390
|
+
message = "Unexpected member separator."
|
|
391
|
+
}
|
|
392
|
+
ctx.ReportRangeFix(
|
|
393
|
+
sepPos,
|
|
394
|
+
sepPos+1,
|
|
395
|
+
message,
|
|
396
|
+
TextEdit{Pos: sepPos, End: sepPos + 1, Text: ""},
|
|
397
|
+
)
|
|
335
398
|
return
|
|
336
399
|
}
|
|
337
|
-
if
|
|
400
|
+
if src[sepPos] != ',' {
|
|
338
401
|
return
|
|
339
402
|
}
|
|
340
|
-
ctx
|
|
341
|
-
semiPos,
|
|
342
|
-
semiPos+1,
|
|
343
|
-
"Unexpected trailing semicolon.",
|
|
344
|
-
TextEdit{Pos: semiPos, End: semiPos + 1, Text: ""},
|
|
345
|
-
)
|
|
403
|
+
reportMemberSeparatorNormalization(ctx, sepPos)
|
|
346
404
|
}
|
|
347
405
|
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
406
|
+
// findMemberSeparator returns the offset of the separator byte that closes
|
|
407
|
+
// `node`, or -1 when the member carries none.
|
|
408
|
+
//
|
|
409
|
+
// The byte is located robustly. typescript-go consumes it as a separate
|
|
410
|
+
// token before closing the node (parseTypeMemberSemicolon and
|
|
411
|
+
// parseSemicolonAfterPropertyName both run ahead of finishNode), so End()
|
|
412
|
+
// normally sits just past it; an error-recovery path that returns without
|
|
413
|
+
// consuming leaves it outside instead. Accept either a separator already
|
|
414
|
+
// at End()-1 or the first one reached scanning horizontal whitespace
|
|
415
|
+
// forward from End().
|
|
416
|
+
//
|
|
417
|
+
// A `,` counts only where Prettier would print a `;`, which is what
|
|
418
|
+
// memberTakesSemicolonTerminator answers. The exclusion is not cosmetic:
|
|
419
|
+
// an object literal's accessor arrives here as the same GetAccessor /
|
|
420
|
+
// SetAccessor kind, its body's `}` leaves the following `,` sitting right
|
|
421
|
+
// where this scan looks, and removing or rewriting that comma would
|
|
422
|
+
// corrupt the literal. A class field is excluded for the same reason from
|
|
423
|
+
// the other side: `,` is not a class-member separator at all, so one found
|
|
424
|
+
// next to a field belongs to some recovered parse.
|
|
425
|
+
func findMemberSeparator(src string, node *shimast.Node, isClassField bool) int {
|
|
426
|
+
commaCounts := !isClassField && memberTakesSemicolonTerminator(node)
|
|
427
|
+
matches := func(i int) bool {
|
|
428
|
+
if i < 0 || i >= len(src) {
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
return src[i] == ';' || (commaCounts && src[i] == ',')
|
|
432
|
+
}
|
|
433
|
+
end := node.End()
|
|
434
|
+
if matches(end - 1) {
|
|
435
|
+
return end - 1
|
|
436
|
+
}
|
|
437
|
+
i := end
|
|
438
|
+
for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
|
|
439
|
+
i++
|
|
440
|
+
}
|
|
441
|
+
if matches(i) {
|
|
442
|
+
return i
|
|
443
|
+
}
|
|
444
|
+
return -1
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// memberSemicolonRedundant reports whether the member separator whose
|
|
448
|
+
// following byte is at `after` can be dropped without changing the parse.
|
|
449
|
+
// It scans past trivia (whitespace + comments, via scanPastTrivia) to the
|
|
450
|
+
// next significant byte and applies Prettier's semi:false member rules:
|
|
451
|
+
//
|
|
452
|
+
// - The closing `}` (or end of file) always makes the separator
|
|
453
|
+
// redundant: the trailing one is printed inside an `ifBreak` that
|
|
454
|
+
// resolves to nothing under semi:false, in either layout.
|
|
455
|
+
// - A next member on the SAME line (no newline crossed) keeps the
|
|
456
|
+
// separator, the rule never inserts the newline that would let ASI
|
|
457
|
+
// take over, so dropping it here would corrupt the source.
|
|
458
|
+
// - A next member in a list Prettier lays out FLAT keeps it too. Between
|
|
459
|
+
// two members the separator is `ifBreak(semi, ";")`, whose flat branch
|
|
460
|
+
// is `";"` whatever `semi` says, so only the trailing one is silenced
|
|
461
|
+
// by semi:false in a flat list. memberListBreaks is the same question
|
|
462
|
+
// insertMemberSemicolon asks from the other end.
|
|
463
|
+
// - A newline-separated next member in a broken list drops the separator
|
|
464
|
+
// unless its lead token would re-associate with the prior member: the
|
|
465
|
+
// full expression-ASI hazard set for class fields (`[ ( ` + - * / ,`),
|
|
466
|
+
// or just a call/construct/generic signature (`(` / `<`) for type
|
|
467
|
+
// members (a leading `[` is an index signature there, not a
|
|
468
|
+
// continuation). Prettier defends the same shapes, printing
|
|
469
|
+
// `a: number;` ahead of a call signature even under semi:false.
|
|
470
|
+
func memberSemicolonRedundant(src string, node *shimast.Node, after int, isClassField bool) bool {
|
|
365
471
|
i, sawNewline := scanPastTrivia(src, after)
|
|
366
472
|
if i >= len(src) {
|
|
367
473
|
return true
|
|
@@ -373,6 +479,9 @@ func memberSemicolonRedundant(src string, after int, isClassField bool) bool {
|
|
|
373
479
|
if !sawNewline {
|
|
374
480
|
return false
|
|
375
481
|
}
|
|
482
|
+
if !memberListBreaks(src, node.Parent) {
|
|
483
|
+
return false
|
|
484
|
+
}
|
|
376
485
|
if isClassField {
|
|
377
486
|
switch c {
|
|
378
487
|
case '[', '(', '`', '+', '-', '*', '/', ',':
|
|
@@ -387,6 +496,334 @@ func memberSemicolonRedundant(src string, after int, isClassField bool) bool {
|
|
|
387
496
|
return true
|
|
388
497
|
}
|
|
389
498
|
|
|
499
|
+
// reportMemberSeparatorNormalization rewrites the `,` at `pos` to the `;`
|
|
500
|
+
// Prettier spells every type-member separator it keeps. Both directions
|
|
501
|
+
// share it so the two cannot drift into different spellings.
|
|
502
|
+
func reportMemberSeparatorNormalization(ctx *Context, pos int) {
|
|
503
|
+
ctx.ReportRangeFix(
|
|
504
|
+
pos,
|
|
505
|
+
pos+1,
|
|
506
|
+
"Normalize the member separator to a semicolon.",
|
|
507
|
+
TextEdit{Pos: pos, End: pos + 1, Text: ";"},
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// insertMemberSemicolon appends the `;` Prettier prints after an
|
|
512
|
+
// interface, type-literal, or class member that ends its physical line.
|
|
513
|
+
//
|
|
514
|
+
// A member's `;` plays two roles in Prettier's object printer, and the
|
|
515
|
+
// insert answers them separately:
|
|
516
|
+
//
|
|
517
|
+
// - BETWEEN two members it is a separator, printed in both layouts. So
|
|
518
|
+
// a member with another member below it takes the `;` whether or not
|
|
519
|
+
// the list ends up broken.
|
|
520
|
+
// - AFTER the last member it is a trailing terminator, printed inside
|
|
521
|
+
// an `ifBreak` and therefore only when the list breaks. That is why
|
|
522
|
+
// `type T = { a: number }` is Prettier's own output for that input,
|
|
523
|
+
// and why memberListBreaks decides this case rather than the line
|
|
524
|
+
// structure at the member itself.
|
|
525
|
+
//
|
|
526
|
+
// Both roles need the member to end its line: a `;` the author did not
|
|
527
|
+
// write between two same-line members is one Prettier would print only
|
|
528
|
+
// after inserting the line break this rule never inserts.
|
|
529
|
+
//
|
|
530
|
+
// memberSemicolonRedundant reads the same oracle rule from the other end,
|
|
531
|
+
// which is why the two are complementary rather than opposite: a `;`
|
|
532
|
+
// before a same-line `}` closes a flat list, where Prettier prints no
|
|
533
|
+
// trailing separator at all, so the strip drops it and this never adds
|
|
534
|
+
// one back.
|
|
535
|
+
//
|
|
536
|
+
// The edit is a zero-width insertion at the member's End(), so it stays
|
|
537
|
+
// disjoint from the format/statement-split, format/indent, and
|
|
538
|
+
// format/print-width edits that may land on the same lines; the applier
|
|
539
|
+
// keeps one finding per contested range, so an overlap would cost a whole
|
|
540
|
+
// cascade pass. It cannot change the parse either: the parser already
|
|
541
|
+
// ended the member at that offset (parseTypeMemberSemicolon runs before
|
|
542
|
+
// finishNode), so the inserted `;` only spells out a boundary the parse
|
|
543
|
+
// had already made.
|
|
544
|
+
//
|
|
545
|
+
// Idempotent: a re-parse folds the inserted `;` into the member's range,
|
|
546
|
+
// so the next pass reads it at End()-1 and abstains.
|
|
547
|
+
func insertMemberSemicolon(ctx *Context, src string, node *shimast.Node, end int) {
|
|
548
|
+
if !memberTakesSemicolonTerminator(node) {
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
switch src[end-1] {
|
|
552
|
+
case ';':
|
|
553
|
+
// Already terminated. Also the idempotency guard.
|
|
554
|
+
return
|
|
555
|
+
case ',':
|
|
556
|
+
// The same separator spelled the other way, so this is a separator
|
|
557
|
+
// normalization rather than a terminator insert, and appending would
|
|
558
|
+
// emit `a: number,;`. It needs neither the line-structure test below
|
|
559
|
+
// nor memberListBreaks: under semi:"always" Prettier prints `;` for a
|
|
560
|
+
// separator in both layouts, so no layout knowledge is required to
|
|
561
|
+
// pick the spelling. The one case the oracle answers differently is a
|
|
562
|
+
// trailing `,` in a flat list, which it drops outright; this direction
|
|
563
|
+
// drops nothing, and it already leaves a written `;` standing in that
|
|
564
|
+
// same position, so normalizing lands on the shape it tolerates.
|
|
565
|
+
reportMemberSeparatorNormalization(ctx, end-1)
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
// Trivia is crossed with scanPastTrivia, so a trailing line comment and
|
|
569
|
+
// a block comment carrying a line terminator both count as the break
|
|
570
|
+
// ECMA-262 says they are, and both member scanners keep one notion of
|
|
571
|
+
// "a line was crossed". Reaching end of input means the body has no
|
|
572
|
+
// closing `}`; that source is too broken to reason about, so abstain.
|
|
573
|
+
next, sawNewline := scanPastTrivia(src, end)
|
|
574
|
+
if next >= len(src) || !sawNewline {
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
// The list's `}` is the only thing that can follow the last member, so
|
|
578
|
+
// this is the trailing-terminator case and the list has to be one
|
|
579
|
+
// Prettier breaks.
|
|
580
|
+
if src[next] == '}' && !memberListBreaks(src, node.Parent) {
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
// Anchored on the member's last character for the same reason the
|
|
584
|
+
// statement branch is: the banner underlines "the place a semicolon
|
|
585
|
+
// should follow", while the fix stays zero-width at End().
|
|
586
|
+
ctx.ReportRangeFix(
|
|
587
|
+
end-1,
|
|
588
|
+
end,
|
|
589
|
+
"Missing semicolon.",
|
|
590
|
+
TextEdit{Pos: end, End: end, Text: ";"},
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// memberTakesSemicolonTerminator reports whether Prettier terminates
|
|
595
|
+
// `node` with a `;` at all. It decides on the member's own shape and on
|
|
596
|
+
// the member list holding it, not on its kind, because one kind spells
|
|
597
|
+
// members of both a `;`-separated and a `,`-separated list:
|
|
598
|
+
//
|
|
599
|
+
// - A member carrying a body ends in `}`, and Prettier never follows a
|
|
600
|
+
// braced member with a terminator. The reachable case is an accessor:
|
|
601
|
+
// GetAccessor and SetAccessor spell both a bodiless interface
|
|
602
|
+
// accessor and a class accessor with a body.
|
|
603
|
+
// - Interface and type-literal bodies are `;`-separated. So is a class
|
|
604
|
+
// body, whose index signatures and bodiless (`declare` / `abstract`)
|
|
605
|
+
// accessors take the same terminator as their type-member spellings,
|
|
606
|
+
// and are broken onto their own lines by the same format/indent pass.
|
|
607
|
+
// - An object literal is `,`-separated. Its accessors arrive here as
|
|
608
|
+
// the same two kinds, and a `;` after one is a syntax error.
|
|
609
|
+
func memberTakesSemicolonTerminator(node *shimast.Node) bool {
|
|
610
|
+
if node.Body() != nil {
|
|
611
|
+
return false
|
|
612
|
+
}
|
|
613
|
+
parent := node.Parent
|
|
614
|
+
if parent == nil {
|
|
615
|
+
return false
|
|
616
|
+
}
|
|
617
|
+
switch parent.Kind {
|
|
618
|
+
case shimast.KindInterfaceDeclaration,
|
|
619
|
+
shimast.KindTypeLiteral,
|
|
620
|
+
shimast.KindClassDeclaration,
|
|
621
|
+
shimast.KindClassExpression:
|
|
622
|
+
return true
|
|
623
|
+
}
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// memberListBreaks reports whether Prettier lays `owner`'s braced body out
|
|
628
|
+
// across lines. Both directions ask it, and both ask about a separator
|
|
629
|
+
// whose presence is conditional on the wrap: the trailing terminator
|
|
630
|
+
// Prettier prints inside an `ifBreak`, and (under semi:false) the
|
|
631
|
+
// separator between two members, whose flat branch is a literal `";"`.
|
|
632
|
+
//
|
|
633
|
+
// `owner` is a member's parent, or a mapped type itself. An interface body
|
|
634
|
+
// and a class body always break once they hold a member, so the source's
|
|
635
|
+
// own line structure does not enter into it.
|
|
636
|
+
//
|
|
637
|
+
// An object type is the exception, and a mapped type is decided the same
|
|
638
|
+
// way: both preserve the author's wrap (Prettier's
|
|
639
|
+
// `objectWrap: "preserve"`), breaking when a line terminator separates the
|
|
640
|
+
// `{` from what follows it and otherwise staying on one line however the
|
|
641
|
+
// source placed the closing `}`. Prettier 3.8.3 returns
|
|
642
|
+
// `type T = { a: number\n};` as the one-line `type T = { a: number };` and
|
|
643
|
+
// `type M = { [K in string]: string\n};` as `type M = { [K in string]: string };`,
|
|
644
|
+
// both with nothing before the brace, so reading where the `}` landed would
|
|
645
|
+
// insert a `;` the oracle never prints.
|
|
646
|
+
//
|
|
647
|
+
// The width half of Prettier's break decision (a flat body that overflows
|
|
648
|
+
// its budget breaks) is deliberately absent: no ttsc pass reflows an
|
|
649
|
+
// object or mapped type, so a flat one stays flat and a trailing
|
|
650
|
+
// terminator would be one this formatter's own output never justifies. A
|
|
651
|
+
// pass that ever breaks them writes the line terminator this reads.
|
|
652
|
+
func memberListBreaks(src string, owner *shimast.Node) bool {
|
|
653
|
+
if owner == nil {
|
|
654
|
+
return false
|
|
655
|
+
}
|
|
656
|
+
switch owner.Kind {
|
|
657
|
+
case shimast.KindTypeLiteral, shimast.KindMappedType:
|
|
658
|
+
default:
|
|
659
|
+
return true
|
|
660
|
+
}
|
|
661
|
+
open := shimscanner.SkipTrivia(src, owner.Pos())
|
|
662
|
+
if open < 0 || open >= len(src) || src[open] != '{' {
|
|
663
|
+
// Not the shape this reads. Keep the author's bytes.
|
|
664
|
+
return false
|
|
665
|
+
}
|
|
666
|
+
_, sawNewline := scanPastTrivia(src, open+1)
|
|
667
|
+
return sawNewline
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// mappedTypeSemicolon settles the `;` Prettier prints after a mapped
|
|
671
|
+
// type's clause.
|
|
672
|
+
//
|
|
673
|
+
// A mapped type is not a member list. `{ readonly [K in T as N]?: V }`
|
|
674
|
+
// holds one clause, typescript-go hangs its parts off the MappedTypeNode
|
|
675
|
+
// itself, and parseMappedType consumes the optional `;` with
|
|
676
|
+
// parseSemicolon before finishNode, so no child's range covers it and no
|
|
677
|
+
// member node exists to carry one. Every position is therefore the
|
|
678
|
+
// trailing-terminator position, which Prettier prints as
|
|
679
|
+
// `options.semi ? ifBreak(";") : ""`: present exactly when `semi` is on
|
|
680
|
+
// and the mapped type is one it breaks, absent in every other
|
|
681
|
+
// combination. The `readonly` / `?` modifiers and their `+` and `-`
|
|
682
|
+
// variants do not change that answer, measured rather than assumed.
|
|
683
|
+
//
|
|
684
|
+
// memberListBreaks reads the mapped type's own braces because Prettier
|
|
685
|
+
// decides its wrap the way it decides an object type's. That is also why,
|
|
686
|
+
// unlike insertMemberSemicolon, this needs no "ends its line" test: a
|
|
687
|
+
// member list needs a break before Prettier will print a separator
|
|
688
|
+
// between two same-line members, while a lone terminator needs only the
|
|
689
|
+
// wrap the `{` already decided. Prettier 3.8.3 terminates
|
|
690
|
+
// `type M = {\n [K in string]: string };` and leaves
|
|
691
|
+
// `type M = { [K in string]: string\n};` bare.
|
|
692
|
+
//
|
|
693
|
+
// Idempotent in both directions: the inserted `;` is what the next pass
|
|
694
|
+
// finds ahead of the `}` and abstains on, and the stripped one leaves the
|
|
695
|
+
// `}` the strip abstains on.
|
|
696
|
+
func mappedTypeSemicolon(ctx *Context, src string, node *shimast.Node, preferNever bool) {
|
|
697
|
+
clauseEnd := mappedTypeClauseEnd(src, node)
|
|
698
|
+
if clauseEnd <= 0 {
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
next, _ := scanPastTrivia(src, clauseEnd)
|
|
702
|
+
if next >= len(src) || next >= node.End() {
|
|
703
|
+
// No closing `}` in range. That source is too broken to reason about.
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
if src[next] == ';' {
|
|
707
|
+
if !preferNever {
|
|
708
|
+
return // already terminated, and the always direction never strips
|
|
709
|
+
}
|
|
710
|
+
ctx.ReportRangeFix(
|
|
711
|
+
next,
|
|
712
|
+
next+1,
|
|
713
|
+
"Unexpected trailing semicolon.",
|
|
714
|
+
TextEdit{Pos: next, End: next + 1, Text: ""},
|
|
715
|
+
)
|
|
716
|
+
return
|
|
717
|
+
}
|
|
718
|
+
if preferNever || src[next] != '}' || !memberListBreaks(src, node) {
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
pos := mappedTypeTerminatorPos(src, clauseEnd)
|
|
722
|
+
ctx.ReportRangeFix(
|
|
723
|
+
pos-1,
|
|
724
|
+
pos,
|
|
725
|
+
"Missing semicolon.",
|
|
726
|
+
TextEdit{Pos: pos, End: pos, Text: ";"},
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// mappedTypeClauseEnd returns the offset just past the last significant
|
|
731
|
+
// byte of a mapped type's clause, or -1 when the node is not the shape
|
|
732
|
+
// this reads.
|
|
733
|
+
//
|
|
734
|
+
// With a type annotation the clause simply ends where that type does. The
|
|
735
|
+
// annotation is optional, though, and the parser's own token nodes stop
|
|
736
|
+
// short of the trailing punctuation in that case: `NameType` and
|
|
737
|
+
// `TypeParameter` both end before the `]`, and a `+`/`-` modifier is
|
|
738
|
+
// parsed as the question token with the `?` it decorates consumed by a
|
|
739
|
+
// bare parseExpected. So the remaining shapes are reached by stepping over
|
|
740
|
+
// exactly the bytes that can follow a clause child, which is why the walk
|
|
741
|
+
// runs only when there is no annotation to end at.
|
|
742
|
+
//
|
|
743
|
+
// A non-empty Members list means the parser recovered from a second member
|
|
744
|
+
// inside the braces (`{ [K in T]: V; a: number }`), which is not a shape
|
|
745
|
+
// with a single terminator position; abstain rather than guess.
|
|
746
|
+
func mappedTypeClauseEnd(src string, node *shimast.Node) int {
|
|
747
|
+
mapped := node.AsMappedTypeNode()
|
|
748
|
+
if mapped == nil {
|
|
749
|
+
return -1
|
|
750
|
+
}
|
|
751
|
+
if mapped.Members != nil && len(mapped.Members.Nodes) > 0 {
|
|
752
|
+
return -1
|
|
753
|
+
}
|
|
754
|
+
if mapped.Type != nil {
|
|
755
|
+
end := mapped.Type.End()
|
|
756
|
+
if end <= 0 || end > len(src) {
|
|
757
|
+
return -1
|
|
758
|
+
}
|
|
759
|
+
return end
|
|
760
|
+
}
|
|
761
|
+
lo := -1
|
|
762
|
+
switch {
|
|
763
|
+
case mapped.QuestionToken != nil:
|
|
764
|
+
lo = mapped.QuestionToken.End()
|
|
765
|
+
case mapped.NameType != nil:
|
|
766
|
+
lo = mapped.NameType.End()
|
|
767
|
+
case mapped.TypeParameter != nil:
|
|
768
|
+
lo = mapped.TypeParameter.End()
|
|
769
|
+
}
|
|
770
|
+
if lo <= 0 || lo > len(src) {
|
|
771
|
+
return -1
|
|
772
|
+
}
|
|
773
|
+
for {
|
|
774
|
+
next, _ := scanPastTrivia(src, lo)
|
|
775
|
+
if next >= len(src) {
|
|
776
|
+
return -1
|
|
777
|
+
}
|
|
778
|
+
switch src[next] {
|
|
779
|
+
case ']', '?', '+', '-':
|
|
780
|
+
lo = next + 1
|
|
781
|
+
default:
|
|
782
|
+
return lo
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// mappedTypeTerminatorPos returns the offset Prettier's mapped-type `;`
|
|
788
|
+
// occupies, given the clause end.
|
|
789
|
+
//
|
|
790
|
+
// It is the clause end in every ordinary shape, and the two comment cases
|
|
791
|
+
// are the reason it is a function rather than that offset. Prettier
|
|
792
|
+
// attaches a trailing block comment written on the clause's own line to
|
|
793
|
+
// the value type and prints the terminator after it
|
|
794
|
+
// (`[K in string]: string /* note */;`), while a line comment becomes the
|
|
795
|
+
// mapped type's dangling comment and is printed after the terminator
|
|
796
|
+
// (`[K in string]: string; // note`). A block comment that spans lines is
|
|
797
|
+
// a line terminator per ECMA-262 and ends the clause's line, so the walk
|
|
798
|
+
// stops there too. This is the one place a mapped type and a member
|
|
799
|
+
// disagree: a member takes its `;` at End(), ahead of the same block
|
|
800
|
+
// comment.
|
|
801
|
+
func mappedTypeTerminatorPos(src string, clauseEnd int) int {
|
|
802
|
+
pos := clauseEnd
|
|
803
|
+
i := clauseEnd
|
|
804
|
+
for i < len(src) {
|
|
805
|
+
for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
|
|
806
|
+
i++
|
|
807
|
+
}
|
|
808
|
+
if i+1 >= len(src) || src[i] != '/' || src[i+1] != '*' {
|
|
809
|
+
return pos
|
|
810
|
+
}
|
|
811
|
+
j := i + 2
|
|
812
|
+
for j+1 < len(src) && !(src[j] == '*' && src[j+1] == '/') {
|
|
813
|
+
if src[j] == '\n' || src[j] == '\r' {
|
|
814
|
+
return pos
|
|
815
|
+
}
|
|
816
|
+
j++
|
|
817
|
+
}
|
|
818
|
+
if j+1 >= len(src) {
|
|
819
|
+
return pos // unterminated block comment swallows the rest
|
|
820
|
+
}
|
|
821
|
+
i = j + 2
|
|
822
|
+
pos = i
|
|
823
|
+
}
|
|
824
|
+
return pos
|
|
825
|
+
}
|
|
826
|
+
|
|
390
827
|
func init() {
|
|
391
828
|
Register(formatSemi{})
|
|
392
829
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
4
4
|
"description": "Reference ttsc plugin: ESLint-style lint rules over the TypeScript-Go Program used by the type-check pass.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@types/node": "^25.3.0",
|
|
38
38
|
"rimraf": "^6.1.2",
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
|
-
"ttsc": "0.26.
|
|
40
|
+
"ttsc": "0.26.2"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|