@ttsc/lint 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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. Prettier drops their trailing
47
- // `;` under semi:false when they are newline-separated; see
48
- // stripMemberSemicolon for the per-context hazard rules.
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 the never
74
- // direction routes through a dedicated stripper. Class fields keep
75
- // their existing always-direction insertion (falling through below);
76
- // inserting a missing interface/type member terminator is out of scope
77
- // for this strip fix, so type members short-circuit in always mode.
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 strips under
286
- // semi:false. Class fields (KindPropertyDeclaration) are handled
287
- // separately because their initializer is an expression and so they
288
- // carry the full expression-ASI hazard set, while type members only
289
- // risk a call/construct-signature (`(`) or generic-call-signature (`<`)
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 removes a redundant trailing `;` from an
307
- // interface / type-literal member or a class field under semi:false.
308
- //
309
- // The member-terminating `;` is located robustly: typescript-go parses
310
- // the terminator as a separate token (parseTypeMemberSemicolon /
311
- // parseSemicolonAfterPropertyName run after finishNode), so a member
312
- // node's End() may sit before the `;`. Accept either a `;` already at
313
- // End()-1 or the first `;` reached scanning horizontal whitespace
314
- // forward from End().
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
- // The `;` is dropped only when it is redundant, see
317
- // memberSemicolonRedundant, so single-line separators stay intact and
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
- end := node.End()
322
- semiPos := -1
323
- if end-1 >= 0 && src[end-1] == ';' {
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 semiPos < 0 {
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 !memberSemicolonRedundant(src, semiPos+1, isClassField) {
400
+ if src[sepPos] != ',' {
338
401
  return
339
402
  }
340
- ctx.ReportRangeFix(
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
- // memberSemicolonRedundant reports whether the member terminator `;`
349
- // whose following byte is at `after` can be dropped without changing the
350
- // parse. It scans past trivia (whitespace + comments, via
351
- // scanPastTrivia) to the next significant byte and applies Prettier's
352
- // semi:false member rules:
353
- //
354
- // - The closing `}` (or end of file) always makes the `;` redundant.
355
- // - A next member on the SAME line (no newline crossed) keeps the `;`
356
- // as a required separator, the rule never inserts the newline that
357
- // would let ASI take over, so dropping it here would corrupt the
358
- // source.
359
- // - A newline-separated next member drops the `;` unless its lead token
360
- // would re-associate with the prior member: the full expression-ASI
361
- // hazard set for class fields (`[ ( ` + - * / ,`), or just a
362
- // call/construct/generic signature (`(` / `<`) for type members
363
- // (a leading `[` is an index signature there, not a continuation).
364
- func memberSemicolonRedundant(src string, after int, isClassField bool) bool {
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.1",
3
+ "version": "0.27.0",
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.1"
40
+ "ttsc": "0.27.0"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",