@ttsc/lint 0.26.1 → 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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE) [![NPM Version](https://img.shields.io/npm/v/@ttsc/lint.svg)](https://www.npmjs.com/package/@ttsc/lint) [![NPM Downloads](https://img.shields.io/npm/dm/@ttsc/lint.svg)](https://www.npmjs.com/package/@ttsc/lint) [![Build Status](https://github.com/samchon/ttsc/workflows/test/badge.svg)](https://github.com/samchon/ttsc/actions?query=workflow%3Atest) [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://ttsc.dev/docs) [![Discord Badge](https://img.shields.io/badge/discord-samchon-d91965?style=flat&labelColor=5866f2&logo=discord&logoColor=white&link=https://discord.gg/E94XhzrUCZ)](https://discord.gg/E94XhzrUCZ)
6
6
 
7
- A linter and formatter. Co-protagonist of the [`ttsc`](https://ttsc.dev) toolchain, paired with `ttsc`, it replaces `eslint` and `prettier`.
7
+ A linter and formatter. Co-protagonist of the [`ttsc`](https://ttsc.dev) toolchain, paired with `ttsc`, it replaces `eslint` and covers most of `prettier`.
8
8
 
9
9
  720+ rules across 21 families. Lint violations surface as `error TSxxxxx` from a single compile pass; the formatter applies via `ttsc format`.
10
10
 
@@ -102,6 +102,8 @@ npx ttsc format
102
102
 
103
103
  Configure the formatter through the `format` block in `lint.config.ts`. Keys mirror `.prettierrc`; the presence of the block, even empty `format: {}`, enables the always-on format rules at Prettier defaults so `ttsc format` rewrites your source to match.
104
104
 
105
+ One boundary to know before you drop `prettier`: no pass normalizes the whitespace between two tokens, so `a=1`, `if(x){`, and `const i : number` are left as written. See [Format → Scope](https://ttsc.dev/docs/lint/format#scope).
106
+
105
107
  ```ts
106
108
  // lint.config.ts
107
109
  import type { ITtscLintConfig } from "@ttsc/lint";
@@ -128,7 +130,7 @@ Each `format` key controls one behavior:
128
130
  | Config key | Effect |
129
131
  | --- | --- |
130
132
  | `severity` (default `"off"`) | Check-time diagnostic level for formatting. Does not gate `ttsc format`. |
131
- | `semi` | Insert trailing semicolons on ASI-terminated statements. |
133
+ | `semi` | Insert trailing semicolons on ASI-terminated statements, and own the member separator in interface, type-literal, mapped-type, and class bodies. |
132
134
  | `singleQuote` | Convert quoted strings to the preferred quote style. |
133
135
  | `arrowParens` | Add or remove parens around a single arrow parameter. |
134
136
  | `bracketSpacing` | Spaces inside object and named-import/export braces. |
@@ -28,9 +28,10 @@ export interface ITtscLintFormat {
28
28
  */
29
29
  severity?: TtscLintSeverity;
30
30
  /**
31
- * Insert trailing semicolons on ASI-terminated statements. Mirrors Prettier's
32
- * `semi`. `false` flips the rule to require _no_ trailing semicolon (rare;
33
- * matches prettier's `semi: false`).
31
+ * Insert trailing semicolons on ASI-terminated statements, and on the
32
+ * interface, type-literal, and class members that carry no body. Mirrors
33
+ * Prettier's `semi`. `false` flips the rule to require _no_ trailing
34
+ * semicolon (rare; matches prettier's `semi: false`).
34
35
  *
35
36
  * @default true
36
37
  */
@@ -3028,14 +3028,15 @@ func loadTypeScriptConfigEvaluationWithin(
3028
3028
  // ttsx build hermetic.
3029
3029
  "--no-plugins",
3030
3030
  }
3031
- if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
3031
+ anchors := configToolAnchors(location, resolutionRoot)
3032
+ if tsgo := resolveConfigTsgo(anchors); tsgo != "" {
3032
3033
  args = append(args, "--binary", tsgo)
3033
3034
  }
3034
3035
  args = append(args, loader)
3035
3036
 
3036
3037
  ctx, cancel := context.WithCancel(context.Background())
3037
3038
  defer cancel()
3038
- cmd := ttsxCommandContext(ctx, args...)
3039
+ cmd := ttsxCommandContext(ctx, anchors, args...)
3039
3040
  cmd.Env = nodeConfigLoaderEnv(location)
3040
3041
  return runConfigLoaderCommand(cmd, location, "TypeScript config file", outputPath)
3041
3042
  }
@@ -4276,6 +4277,23 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
4276
4277
  return string(body)
4277
4278
  }
4278
4279
 
4280
+ // ttsc:config-loader-shared begin
4281
+ //
4282
+ // One policy in three Go copies: everything between these markers is
4283
+ // duplicated verbatim in packages/lint/linthost/config.go,
4284
+ // packages/banner/driver/banner.go and packages/strip/driver/config.go. #1169
4285
+ // decided against extracting it — the only home the three modules could share
4286
+ // is the public `packages/ttsc/driver` seam, and packages/lint's go.mod
4287
+ // deliberately requires no in-tree ttsc module — and replaced the checklist
4288
+ // with a gate: `scripts/ci/config-loader-copies.cjs` compares every function
4289
+ // between these markers across all three copies on every pull request, so
4290
+ // editing one and not the others fails by name. That file's header carries the
4291
+ // full decision and the rules for changing this block.
4292
+ //
4293
+ // The code between the markers must stay identical. Comments may differ, the
4294
+ // `@ttsc/<pkg>:` error prefix may differ, and @ttsc/strip spells each name with
4295
+ // a `strip` prefix. Anything package-specific belongs outside the markers.
4296
+
4279
4297
  // configModuleOption returns the loader tsconfig's "module" for a config file:
4280
4298
  // the module kind Node itself would give that file.
4281
4299
  //
@@ -4406,21 +4424,235 @@ func resolveDirLink(dir string) string {
4406
4424
  return dir
4407
4425
  }
4408
4426
 
4427
+ // Both tools the TypeScript config evaluator needs — the `ttsx` launcher it
4428
+ // spawns and the native compiler it hands that launcher — are resolved from the
4429
+ // project being linted, with an explicit environment variable winning and a
4430
+ // last resort that invents no path.
4431
+ //
4432
+ // The three Go copies are held identical by the gate named at the top of this
4433
+ // block. The JS original — `resolveConfigTsgo` / `resolveTtsxLauncher` in
4434
+ // packages/lint/src/index.ts — is a fourth copy in another language that no Go
4435
+ // gate can reach; the two evaluators must keep one policy, because it was a
4436
+ // divergence between them that made a TypeScript lint config unevaluable
4437
+ // outside a `ttsx`-launched host.
4438
+ //
4439
+ // The environment alone is the wrong place to ask. `ttsx` exports
4440
+ // TTSC_TSGO_BINARY and TTSC_TTSX_BINARY to its own descendants, so a host
4441
+ // launched under `ttsx` inherited both and a host launched any other way
4442
+ // inherited neither. The shipped `ttscserver` binary invoked with its
4443
+ // documented `--tsgo <path>` flag keeps that path in a local and exports
4444
+ // nothing, and an embedder of the driver package exports nothing either. For
4445
+ // those the evaluator spawned a bare `ttsx` that only a global install puts on
4446
+ // PATH, and, past that, a compiler-less child that aborted with
4447
+ // `ttsc: typescript is required` before a line of the config was read.
4448
+ //
4449
+ // configToolAnchors lists the file paths those resolutions walk upward from,
4450
+ // in order: the config file being evaluated, then the resolution root's
4451
+ // manifest. The config comes first because it is the file whose own
4452
+ // installation decides which toolchain the config's imports were written
4453
+ // against; the resolution root answers for a config that lives outside the
4454
+ // project tree (an `extends` target, or a `configFile` pointed at a shared
4455
+ // package).
4456
+ //
4457
+ // The JS evaluator carries a third anchor, the loaded descriptor's own
4458
+ // directory. It has no counterpart here: this host is a compiled binary rather
4459
+ // than a module some `node_modules` copy of `@ttsc/lint` was loaded from, so
4460
+ // there is no third installation to ask.
4461
+ func configToolAnchors(configPath, resolutionRoot string) []string {
4462
+ anchors := make([]string, 0, 2)
4463
+ if strings.TrimSpace(configPath) != "" {
4464
+ anchors = append(anchors, configPath)
4465
+ }
4466
+ if strings.TrimSpace(resolutionRoot) != "" {
4467
+ anchors = append(anchors, filepath.Join(resolutionRoot, "package.json"))
4468
+ }
4469
+ return anchors
4470
+ }
4471
+
4472
+ // resolveConfigTsgo returns the native TypeScript compiler the evaluator hands
4473
+ // its ttsx child through `--binary`, or "" to leave the child resolving for
4474
+ // itself.
4475
+ //
4476
+ // The child runs with `--cwd <ephemeral loader dir>`, so it cannot discover
4477
+ // `typescript` the way an ordinary invocation does: linkNearestNodeModules is
4478
+ // the only thing that puts the project's modules within its reach, and it links
4479
+ // nothing when the config's ancestry carries no node_modules. An explicit
4480
+ // TTSC_TSGO_BINARY still wins, so an embedder that pins a compiler keeps
4481
+ // pinning it. "" is the unchanged last resort: a project that cannot answer
4482
+ // here could not answer inside the child either, and the child's own diagnostic
4483
+ // is the one that names the missing package.
4484
+ func resolveConfigTsgo(anchors []string) string {
4485
+ if explicit := strings.TrimSpace(os.Getenv("TTSC_TSGO_BINARY")); explicit != "" {
4486
+ return explicit
4487
+ }
4488
+ for _, anchor := range anchors {
4489
+ if binary := tsgoBinaryFrom(anchor); binary != "" {
4490
+ return binary
4491
+ }
4492
+ }
4493
+ return ""
4494
+ }
4495
+
4496
+ // tsgoBinaryFrom returns the platform compiler executable of the `typescript`
4497
+ // install `anchor` can see, or "" when this anchor reaches neither the package
4498
+ // nor its platform dependency.
4499
+ //
4500
+ // Mirrors resolveTsgo.ts so the Go host and the JS launcher name one file: the
4501
+ // `typescript` manifest, then `@typescript/typescript-<platform>-<arch>`
4502
+ // resolved from that manifest, then `lib/tsc` inside it.
4503
+ //
4504
+ // The install is chased to its real directory before the second hop, because
4505
+ // Node resolves a module's own dependencies from its real location. pnpm keeps
4506
+ // the real `typescript` directory in its content-addressed store with the
4507
+ // platform package beside it and leaves a link in the project's node_modules,
4508
+ // so a walk that started at the link would climb straight past the platform
4509
+ // package. NTFS junctions defeat filepath.EvalSymlinks, so the link component
4510
+ // is chased by hand first, the same order loaderTempBase uses.
4511
+ func tsgoBinaryFrom(anchor string) string {
4512
+ manifest := nodePackageManifestFrom(anchor, "typescript")
4513
+ if manifest == "" {
4514
+ return ""
4515
+ }
4516
+ packageDir := realpathIfPossible(resolveDirLink(filepath.Dir(manifest)))
4517
+ platform, arch := nodePlatformPair()
4518
+ platformManifest := nodePackageManifestFrom(
4519
+ filepath.Join(packageDir, "package.json"),
4520
+ "@typescript/typescript-"+platform+"-"+arch,
4521
+ )
4522
+ if platformManifest == "" {
4523
+ return ""
4524
+ }
4525
+ name := "tsc"
4526
+ if runtime.GOOS == "windows" {
4527
+ name = "tsc.exe"
4528
+ }
4529
+ binary := filepath.Join(filepath.Dir(platformManifest), "lib", name)
4530
+ if stat, err := os.Stat(binary); err != nil || stat.IsDir() {
4531
+ return ""
4532
+ }
4533
+ return binary
4534
+ }
4535
+
4536
+ // resolveTtsxLauncher returns the launcher ttsxCommandContext spawns.
4537
+ //
4538
+ // An explicit TTSC_TTSX_BINARY wins. Otherwise the launcher is derived from the
4539
+ // `ttsc` installation one of the anchors can see, because a bare command name
4540
+ // only works when a bin link happens to be on PATH — which it is for a global
4541
+ // install and is not for the ordinary project-local one. The bare `"ttsx"` name
4542
+ // remains the unchanged last resort for an installation no anchor reaches.
4543
+ func resolveTtsxLauncher(anchors []string) string {
4544
+ if explicit := strings.TrimSpace(os.Getenv("TTSC_TTSX_BINARY")); explicit != "" {
4545
+ return explicit
4546
+ }
4547
+ for _, anchor := range anchors {
4548
+ if launcher := ttsxLauncherFrom(anchor); launcher != "" {
4549
+ return launcher
4550
+ }
4551
+ }
4552
+ return "ttsx"
4553
+ }
4554
+
4555
+ // ttsxLauncherFrom returns `lib/launcher/ttsx.js` of the `ttsc` install
4556
+ // `anchor` can see, or "" when this anchor reaches no such install. Only the
4557
+ // manifest is an exported subpath, so the launcher is derived from where the
4558
+ // manifest resolved rather than requested as a subpath of its own.
4559
+ func ttsxLauncherFrom(anchor string) string {
4560
+ manifest := nodePackageManifestFrom(anchor, "ttsc")
4561
+ if manifest == "" {
4562
+ return ""
4563
+ }
4564
+ launcher := filepath.Join(filepath.Dir(manifest), "lib", "launcher", "ttsx.js")
4565
+ if stat, err := os.Stat(launcher); err != nil || stat.IsDir() {
4566
+ return ""
4567
+ }
4568
+ return launcher
4569
+ }
4570
+
4571
+ // nodePackageManifestFrom resolves `<pkg>/package.json` the way Node's
4572
+ // require.resolve does from the FILE `anchor`: walk upward from the anchor's
4573
+ // directory and return the first `<dir>/node_modules/<pkg>/package.json` that
4574
+ // exists. The anchor is treated as a file path, so its own directory is the
4575
+ // first candidate's parent, and it need not exist — Node derives the search
4576
+ // paths from the string alone.
4577
+ //
4578
+ // A directory already named `node_modules` contributes no candidate of its own,
4579
+ // matching Module._nodeModulePaths, so nothing ever resolves through
4580
+ // `node_modules/node_modules`.
4581
+ //
4582
+ // A relative anchor is resolved against the process directory before the walk,
4583
+ // again matching Node. Walking a relative path instead would terminate at "."
4584
+ // after one step and silently answer nothing for a config named relatively.
4585
+ func nodePackageManifestFrom(anchor, pkg string) string {
4586
+ if strings.TrimSpace(anchor) == "" || pkg == "" {
4587
+ return ""
4588
+ }
4589
+ if absolute, err := filepath.Abs(anchor); err == nil {
4590
+ anchor = absolute
4591
+ }
4592
+ dir := filepath.Dir(filepath.Clean(anchor))
4593
+ for {
4594
+ if filepath.Base(dir) != "node_modules" {
4595
+ candidate := filepath.Join(dir, "node_modules", filepath.FromSlash(pkg), "package.json")
4596
+ if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
4597
+ return candidate
4598
+ }
4599
+ }
4600
+ parent := filepath.Dir(dir)
4601
+ if parent == dir {
4602
+ return ""
4603
+ }
4604
+ dir = parent
4605
+ }
4606
+ }
4607
+
4608
+ // nodePlatformPair is nodePlatformPairFor applied to this build's own target.
4609
+ func nodePlatformPair() (string, string) {
4610
+ return nodePlatformPairFor(runtime.GOOS, runtime.GOARCH)
4611
+ }
4612
+
4613
+ // nodePlatformPairFor maps a Go build target onto the `process.platform` and
4614
+ // `process.arch` pair npm spells a platform package with, so the package name
4615
+ // this host resolves is the same one the JS launcher resolves.
4616
+ //
4617
+ // Only the members whose two vocabularies disagree are mapped. Every other
4618
+ // value is identical on both sides and passes through, which keeps a target
4619
+ // neither side publishes yet resolvable rather than silently wrong, and keeps
4620
+ // this from becoming a list that has to grow with every new port.
4621
+ func nodePlatformPairFor(goos, goarch string) (string, string) {
4622
+ platform := goos
4623
+ switch platform {
4624
+ case "windows":
4625
+ platform = "win32"
4626
+ case "solaris":
4627
+ platform = "sunos"
4628
+ }
4629
+ arch := goarch
4630
+ switch arch {
4631
+ case "amd64":
4632
+ arch = "x64"
4633
+ case "386":
4634
+ arch = "ia32"
4635
+ case "ppc64le":
4636
+ arch = "ppc64"
4637
+ }
4638
+ return platform, arch
4639
+ }
4640
+
4409
4641
  // ttsxCommand returns a ttsx exec.Cmd bound to a background context. Use
4410
4642
  // ttsxCommandContext when the caller owns a cancellable context.
4411
- func ttsxCommand(args ...string) *exec.Cmd {
4412
- return ttsxCommandContext(context.Background(), args...)
4643
+ func ttsxCommand(anchors []string, args ...string) *exec.Cmd {
4644
+ return ttsxCommandContext(context.Background(), anchors, args...)
4413
4645
  }
4414
4646
 
4415
4647
  // ttsxCommandContext is the cancellable variant, used by the config loaders so
4416
4648
  // their subprocess is torn down with the call that started it. It carries no
4417
4649
  // deadline: evaluating a user config is the user's own code running, and how
4418
4650
  // long that is allowed to take is not this binary's decision.
4419
- func ttsxCommandContext(ctx context.Context, args ...string) *exec.Cmd {
4420
- ttsx := os.Getenv("TTSC_TTSX_BINARY")
4421
- if ttsx == "" {
4422
- ttsx = "ttsx"
4423
- }
4651
+ //
4652
+ // `anchors` are the file paths the launcher is resolved from; see
4653
+ // resolveTtsxLauncher.
4654
+ func ttsxCommandContext(ctx context.Context, anchors []string, args ...string) *exec.Cmd {
4655
+ ttsx := resolveTtsxLauncher(anchors)
4424
4656
  if shouldRunTtsxThroughNode(ttsx) {
4425
4657
  node := os.Getenv("TTSC_NODE_BINARY")
4426
4658
  if node == "" {
@@ -4531,6 +4763,8 @@ func setEnv(env []string, key, value string) []string {
4531
4763
  return append(env, prefix+value)
4532
4764
  }
4533
4765
 
4766
+ // ttsc:config-loader-shared end
4767
+
4534
4768
  // parseExternalRuleEntry delegates to parseRuleEntry. It is kept under this
4535
4769
  // name because test files in the same package call it directly.
4536
4770
  func parseExternalRuleEntry(v any) (Severity, json.RawMessage, error) {
@@ -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.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.1"
40
+ "ttsc": "0.26.2"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
@@ -30,9 +30,10 @@ export interface ITtscLintFormat {
30
30
  severity?: TtscLintSeverity;
31
31
 
32
32
  /**
33
- * Insert trailing semicolons on ASI-terminated statements. Mirrors Prettier's
34
- * `semi`. `false` flips the rule to require _no_ trailing semicolon (rare;
35
- * matches prettier's `semi: false`).
33
+ * Insert trailing semicolons on ASI-terminated statements, and on the
34
+ * interface, type-literal, and class members that carry no body. Mirrors
35
+ * Prettier's `semi`. `false` flips the rule to require _no_ trailing
36
+ * semicolon (rare; matches prettier's `semi: false`).
36
37
  *
37
38
  * @default true
38
39
  */