Program 与 Checker

本页讲 driver 如何用 tsgo 建一个 Program、为什么把 checker 池钉到 1、诊断怎么过滤与渲染、以及转发的 tsgo flag 怎么变成 CompilerOptions 覆盖层。核心文件是 packages/ttsc/driver/program.gopackages/ttsc/driver/host.go

LoadProgram 全流程

LoadProgram(cwd, tsconfigPath, options)program.go:331)是 ttsc 用的一站式入口,步骤如下:

几个关键点:

  • 文件系统是分层的DefaultFS()host.go:23)把 OS 文件系统先包 cachedvfs 再包 bundled.WrapFS,让内置 lib.es*.d.ts、DOM 等定义无需联网即可解析。如果有 source preamble,再包一层 sourcePreambleFS
  • CompilerHost 锚定在 cwd,通过 bundled.LibPath() 找到 tsgo 自带的 lib 文件(host.go:29)。
  • tsconfig 解析用 tsgo 原生 JSONC 解析器ParseTSConfigprogram.go:243),自动处理注释、尾逗号、extends 链。绝对路径在任何 VFS 查找前先对 cwd 解析,因为 tsgo 的文件系统 API 要求绝对路径。

单 checker 约束(最重要的设计取舍)

CreateProgramFromConfigprogram.go:294)在建 Program 前调 forceSingleChecker,把 checker 池钉到 1:

func forceSingleChecker(parsed *tsoptions.ParsedCommandLine) {
  options := parsed.ParsedConfig.CompilerOptions
  if options.SingleThreaded == core.TSTrue {
    return
  }
  one := 1
  options.Checkers = &one
}

为什么program.go:308 注释):ttsc 在 tsgo 之上叠加的每个阶段——插件 transform、输出重写——都串行走整个 program,并向 Program.GetTypeChecker 返回的那一个 checker 查询类型,且会要求它解析来自所有源文件的节点的类型。而 tsgo 的多 checker 池会把每个文件 affinitize 到不同 checker,并禁止跨 checker 混用类型;一个声明跨越不同 checker 文件的循环类型(如循环 indexed-access 别名)在借来的 checker 上会解析成 any。把池钉到 1 保证 prog.Checker 与"每个文件被怎么检查"一致。

代价与缓解:parse 和 emit 受影响——它们不咨询 checker 数量,仍然并行。所以这是"类型解析正确性"换"类型检查并行度",而不是全盘串行。

applyThreadingOptionsprogram.go:433)先记录用户的 --singleThreaded / --checkers N,但 forceSingleChecker 随后会把 --checkers N>1 夹回 1;--singleThreaded 仍然完整生效(它进一步串行化 parse/emit)。这是一个微妙的优先级:用户能要求"更串行",但不能要求"transform 路径用多 checker"。

tsgo flag 覆盖层

ttsc 把自己没识别的 tsgo flag 经 --tsgo-args=<JSON> 转发给原生宿主。parseTsgoArgsprogram.go:264)用 tsgo 自己的命令行解析器把它们解析成 CompilerOptions

cli := tsoptions.ParseCommandLine(args, host)
return cli.CompilerOptions(), nil, nil

ParseTSConfig 把这个覆盖层的非零字段合并到 tsconfig 之上(CLI 胜),与 tsgo 自己 CLI 的优先级一致。这就是 ttsc --strict 能在"in-process 建 Program 的插件构建"里生效的原因——插件构建不 shell 出 tsgo,而是自己建 Program。

ForceEmit / ForceNoEmit / OutDir 通过 forceEmit(清 NoEmitEmitDeclarationOnly)、forceNoEmitoverrideOutDir 直接改 CompilerOptionsForceEmitttsc --emit 和运行时编译用,让默认 noEmit 的项目仍能 emit。

诊断模型

Diagnosticprogram.go:29)刻意做成无 shim 依赖,但带两个内部锚点:

type Diagnostic struct {
  File, Message string
  Line, Column  int
  Code          int32
  Start, Length *int
  Severity      Severity
  raw  *ast.Diagnostic                          // tsgo 原始诊断, 富渲染
  lint *shimdiagnosticwriter.LintDiagnostic     // 插件 lint 诊断
}

rawlint 至多一个非 nil;两者都 nil 时退回单行纯文本形式。这让三类诊断(tsgo 类型错误、lint 违规、手工组装的 host 错误)走同一渲染管线 WritePrettyDiagnosticsprogram.go:127):富诊断走 FormatMixedDiagnostics(带颜色、源码片段、错误统计),纯诊断走 - path:line:col: message

诊断过滤:抑制重载签名误报

filterDiagnosticsprogram.go:554)移除一类 ttsc 编译模型里的误报:isUnusedOverloadSignatureTypeParameterDiagnosticprogram.go:572)识别 TS6196/TS6205("未使用声明"/"所有类型参数未使用")落在无函数体的函数声明(即重载签名)上的情况。tsgo 在那些只在实现签名里用到类型参数的重载上误报这两条;ttsc 把它们过滤掉,因为重载签名对收窄是必需的、其类型参数实际转发给了实现签名。

错误计数决定退出码

CountErrorsprogram.go:161)决定哪些诊断翻退出码:lint 诊断按 lint.IsError(),tsgo 诊断按 Severity != Warning,纯文本诊断一律当 error(这样 "tsconfig not found" 这类失败也能翻退出码)。

Diagnostics() 怎么收集

(*Program).Diagnostics()program.go:534):

  1. shimcompiler.GetDiagnosticsOfAnyProgram 收 bind + semantic 诊断;
  2. filterDiagnostics(去重载误报);
  3. 排序去重(SortAndDeduplicateDiagnostics);
  4. convertDiagnosticsprogram.go:594)翻成 Diagnostic,用 tsgo 的 GetECMALineAndByteOffsetOfPosition 填行列(与 tsc 的 banner 同一套坐标)。

source preamble 注入

当链接插件提供 source preamble(如 @ttsc/banner),sourcePreambleFSprogram.go:447)包住 VFS,在 tsgo 解析器读每个文件时前置 preamble 文本。声明文件(.d.ts/.d.mts/.d.cts)被排除,避免注入代码出现在类型定义里(isSourcePreambleTargetprogram.go:463)。ApplySourcePreambleprogram.go:480)小心处理 BOM 与 hashbang:preamble 插在它们之后,让指令仍是第一条语句。

preamble 引入的行偏移会移动 sourcemap 坐标,emit 阶段会做相应修正(见 Emit 与重写sourcemap_preamble.go)。

不变量

  • cwd 必须绝对(LoadProgram 开头会 filepath.Abs + ResolvePath)。
  • Close() 必须被调用以释放 checker 租约(cmd/ttsc 各命令都 defer prog.Close())。
  • 链接插件的 ApplyLinkedPlugins 至多跑一次(pluginsApplied 守卫,program.go:524);SourceFiles() 会触发它,sourceFilesRaw() 是不触发的内部版本,避免重入。

失败模式

失败表现处理位置
tsconfig 不存在tsconfig not found: <path>ParseTSConfig:245
tsconfig 解析错误诊断数组非空,返回退出码 2ParseTSConfig 返回 convertDiagnostics(allDiags)
转发 flag 非法cli.Errors 翻成诊断parseTsgoArgs:272
nil programdriver: nil program 诊断Diagnostics():536

接下来