shim 子系统

packages/ttsc/shim/* 是 ttsc 把 typescript-go 暴露给自己和插件作者的唯一通道。它的存在源于一个 Go 语言约束,它的完整性是 ttsc 的核心职责,并由一套机械闸门强制。

本子系统页面:

  • 设计:为什么需要 shim、模块结构、三种再导出机制、go.work 编排。
  • 审计与同步shim_audit 三层闸门、gen_shims 生成器、版本 bump、遍历完整性探针。

为什么需要 shim

ttsc 建在 typescript-go(tsgo,TypeScript 编译器的 Go 移植版,模块 github.com/microsoft/typescript-go)之上。它几乎所有真正的编译器表面都活在那个模块的 internal/* 包里,而 Go 禁止跨模块 import 别的模块的 internal/

于是 ttsc 把它需要的部分通过 packages/ttsc/shim/<name> 逐个再导出。每个 shim 子模块(astcheckercompilercoreprinterscannerparsertsoptionstspathvfsbundleddiagnosticwriterlsptransformers……)是自己独立的 Go 模块,包装对应的 internal/<name> 包。

核心理念:缺失再导出是 ttsc 的 bug

来自 .codex/skills/typescript-go-sync/SKILL.md 的核心判断:

保持 shim 同步且完整是 ttsc 的核心目的,不是杂活。shim 是源插件作者(typia、nestia、第三方规则)唯一能触碰的 typescript-go 表面。任务是跟踪 tsgo 源码变更,暴露插件需要的每一个 AST、transform、printer、emit API,让插件永远不必自己伸进 internal/一个缺失的再导出是 ttsc 的 bug,不是插件的 bug。

这把"缺失再导出"从一类反复出现的 bug,变成了一个可被 CI 强制的不变量(见 审计与同步)。

模块结构概览

packages/ttsc/shim/
├── ast/          # AST 节点、工厂、遍历
│   ├── surface.go      # 生成: 类型别名 (DO NOT EDIT)
│   ├── enums_gen.go    # 生成: 枚举家族补全
│   ├── shim.go         # 手写: gen_shims:hand-maintained
│   ├── parent.go       # 手写: SetParentInChildren 等
│   ├── extra-shim.json # 喂生成器的额外符号
│   ├── go.mod / go.sum # 独立模块
│   └── test/
├── checker/  compiler/  core/  printer/  scanner/
├── parser/   tsoptions/ tspath/  vfs/  bundled/
├── diagnosticwriter/  lsp/  transformers/

每个 shim/<name>/ 目录有:

  • surface.go——生成、勿手改。首行 // Code generated ... DO NOT EDIT.。是该包导出 API 表面的纯类型别名(type Foo = innerast.Foo),由 go run ./tools/gen_shims 产出,重新生成会覆盖。
  • shim.go——手写。首行 // gen_shims:hand-maintained,生成器检测此标记并跳过。包装函数与 //go:linkname 声明住这里。像 ast/parent.go 这样的额外文件也是手写。
  • extra-shim.json——喂生成器它无法自己推导的符号:ExtraFunctions(要 linkname 的未导出函数)、ExtraMethodsExtraFieldsIgnoreFunctions(生成器应跳过、已有手写变体的导出函数)。
  • 独立的 go.mod/go.sum,版本固定到同一个 tsgo 伪版本。

三种再导出机制

按符号性质选机制:

符号性质机制例子
导出类型surface.go 里的类型别名(让生成器加)type Node = innerast.Node
生成器跳过的导出函数(签名含未导出类型)shim.go 里手写包装函数func SetParentInChildren(node *Node) { innerast.SetParentInChildren(node) }
未导出符号//go:linknameGetSourceFileOfNode / GetNodeAtPositionast/shim.go

//go:linkname 形式需要 _ "unsafe" import 并声明无函数体的函数。

go.work 编排让插件能 import shim

shim 子模块各自独立,靠同级 go.work 串联。但插件是在用户项目里构建的——它们怎么能 import github.com/microsoft/typescript-go/shim/ast

答案在 go build 缓存buildSourcePlugin 构建插件时,findTtscOverlayDirs 收集 ttsc 包根 go.mod + 所有 shim/* 子模块 go.mod 作为 overlay 目录,writeGoWork 把它们 wire 进插件构建的临时 go.work,并对 ttsc 管理模块加 replace 指令。所以插件源 import shim 时,go.work 把它解析到 ttsc 仓库里的真实 shim 模块。

唯一的 driver 例外

注意:shim 是给"跑在 Go 进程里的代码"用的,包括 ttsc 自己的 driver 与插件作者。但在 ttsc 自己的 Go 代码里,只有 packages/ttsc/driver import shimdriver/host.go:1)。其余 ttsc Go 代码(cmdinternalutility)消费 *driver.Program,看不到 shim 类型。这把 typescript-go 类型的泄漏面收敛到一个包。

接下来