Program 与 Checker
本页讲 driver 如何用 tsgo 建一个 Program、为什么把 checker 池钉到 1、诊断怎么过滤与渲染、以及转发的 tsgo flag 怎么变成 CompilerOptions 覆盖层。核心文件是 packages/ttsc/driver/program.go 和 packages/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 解析器(
ParseTSConfig,program.go:243),自动处理注释、尾逗号、extends链。绝对路径在任何 VFS 查找前先对 cwd 解析,因为 tsgo 的文件系统 API 要求绝对路径。
单 checker 约束(最重要的设计取舍)
CreateProgramFromConfig(program.go:294)在建 Program 前调 forceSingleChecker,把 checker 池钉到 1:
为什么(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 数量,仍然并行。所以这是"类型解析正确性"换"类型检查并行度",而不是全盘串行。
applyThreadingOptions(program.go:433)先记录用户的 --singleThreaded / --checkers N,但 forceSingleChecker 随后会把 --checkers N>1 夹回 1;--singleThreaded 仍然完整生效(它进一步串行化 parse/emit)。这是一个微妙的优先级:用户能要求"更串行",但不能要求"transform 路径用多 checker"。
tsgo flag 覆盖层
ttsc 把自己没识别的 tsgo flag 经 --tsgo-args=<JSON> 转发给原生宿主。parseTsgoArgs(program.go:264)用 tsgo 自己的命令行解析器把它们解析成 CompilerOptions:
ParseTSConfig 把这个覆盖层的非零字段合并到 tsconfig 之上(CLI 胜),与 tsgo 自己 CLI 的优先级一致。这就是 ttsc --strict 能在"in-process 建 Program 的插件构建"里生效的原因——插件构建不 shell 出 tsgo,而是自己建 Program。
ForceEmit / ForceNoEmit / OutDir 通过 forceEmit(清 NoEmit 与 EmitDeclarationOnly)、forceNoEmit、overrideOutDir 直接改 CompilerOptions。ForceEmit 给 ttsc --emit 和运行时编译用,让默认 noEmit 的项目仍能 emit。
诊断模型
Diagnostic(program.go:29)刻意做成无 shim 依赖,但带两个内部锚点:
raw 与 lint 至多一个非 nil;两者都 nil 时退回单行纯文本形式。这让三类诊断(tsgo 类型错误、lint 违规、手工组装的 host 错误)走同一渲染管线 WritePrettyDiagnostics(program.go:127):富诊断走 FormatMixedDiagnostics(带颜色、源码片段、错误统计),纯诊断走 - path:line:col: message。
诊断过滤:抑制重载签名误报
filterDiagnostics(program.go:554)移除一类 ttsc 编译模型里的误报:isUnusedOverloadSignatureTypeParameterDiagnostic(program.go:572)识别 TS6196/TS6205("未使用声明"/"所有类型参数未使用")落在无函数体的函数声明(即重载签名)上的情况。tsgo 在那些只在实现签名里用到类型参数的重载上误报这两条;ttsc 把它们过滤掉,因为重载签名对收窄是必需的、其类型参数实际转发给了实现签名。
错误计数决定退出码
CountErrors(program.go:161)决定哪些诊断翻退出码:lint 诊断按 lint.IsError(),tsgo 诊断按 Severity != Warning,纯文本诊断一律当 error(这样 "tsconfig not found" 这类失败也能翻退出码)。
Diagnostics() 怎么收集
(*Program).Diagnostics()(program.go:534):
- 调
shimcompiler.GetDiagnosticsOfAnyProgram收 bind + semantic 诊断; - 过
filterDiagnostics(去重载误报); - 排序去重(
SortAndDeduplicateDiagnostics); - 经
convertDiagnostics(program.go:594)翻成Diagnostic,用 tsgo 的GetECMALineAndByteOffsetOfPosition填行列(与 tsc 的 banner 同一套坐标)。
source preamble 注入
当链接插件提供 source preamble(如 @ttsc/banner),sourcePreambleFS(program.go:447)包住 VFS,在 tsgo 解析器读每个源文件时前置 preamble 文本。声明文件(.d.ts/.d.mts/.d.cts)被排除,避免注入代码出现在类型定义里(isSourcePreambleTarget,program.go:463)。ApplySourcePreamble(program.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()是不触发的内部版本,避免重入。