@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
|
[](https://github.com/samchon/ttsc/blob/master/LICENSE) [](https://www.npmjs.com/package/@ttsc/lint) [](https://www.npmjs.com/package/@ttsc/lint) [](https://github.com/samchon/ttsc/actions?query=workflow%3Atest) [](https://ttsc.dev/docs) [](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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
*/
|
package/linthost/config.go
CHANGED
|
@@ -3028,14 +3028,15 @@ func loadTypeScriptConfigEvaluationWithin(
|
|
|
3028
3028
|
// ttsx build hermetic.
|
|
3029
3029
|
"--no-plugins",
|
|
3030
3030
|
}
|
|
3031
|
-
|
|
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
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
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
|
|
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",
|
|
@@ -30,9 +30,10 @@ export interface ITtscLintFormat {
|
|
|
30
30
|
severity?: TtscLintSeverity;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Insert trailing semicolons on ASI-terminated statements
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
*/
|