1. 为什么你的 TypeScript 项目总在 if/else 上翻车
写 TypeScript 的人大多有过这种经历:本地跑得好好的,上线后某个边界值进来,代码走进了一条从没被显式处理过的分支,然后就是半夜爬起来看日志。问题不在于类型系统不够强,而在于类型检查只回答“这个值是不是 string”,它不回答“这条逻辑路径有没有被代码覆盖过”。
传统做法是等tsc --noEmit或者跑一遍覆盖率报告,但覆盖率是事后统计,等你看到报告时 bug 可能已经发出去了。真正想要的是:在编辑器里敲代码的当下,光标停在某个if上,就能知道这个分支的“另一条路”有没有被显式处理。这就是 AST 静态分析能做的事——它不依赖运行时,直接读语法树结构,判断if有没有else、switch有没有default、try有没有catch。
这篇面向的是正在做 VSCode 插件开发、或者想给自己团队做一套编码期质量门禁的 TypeScript 工程师。我会从零拆一个叫 BranchGuard 的插件:用 TypeScript Compiler API 解析 AST,遍历节点找出未覆盖分支,再通过 HoverProvider 在编辑器里实时告警。全程可复制,配置片段和报错排查都会给全。如果你只是想快速验证模型对 AST 代码的理解,可以先用 TaoToken 模型对话 把分析逻辑跑通,再落到插件工程里。
核心检索词先明确:AST 实时检测 TypeScript 未覆盖分支,指的是在编码阶段用抽象语法树静态识别缺失的 else/default 路径,而不是等测试覆盖率报告。适合谁?适合手里有中大型 TS 项目、分支逻辑多、又不想引入重型静态分析工具链的团队。
2. TaoToken 前置准备:把 AST 分析逻辑先跑通
在动手写插件之前,我建议先把“AST 遍历找未覆盖分支”这段纯逻辑单独验证一遍。原因很简单:插件工程涉及package.json声明、激活事件、Provider 注册,一旦报错你很难判断是分析逻辑错了还是插件配置错了。把分析函数抽出来,用 Node 脚本直接跑,能省掉大量排查时间。
这一步需要两样东西:一个能执行 TypeScript 的 Node 环境,以及一个稳定的模型接口来帮你快速生成/校对 AST 遍历代码。TaoToken 在这里的角色是提供兼容 OpenAI 协议的 API 入口,你可以把它当成一个统一的模型调用层,用来生成ts.Visitor的遍历骨架、解释ts.isIfStatement这类 API 的返回值。它的 Base URL 是https://taotoken.net/api,Key 在控制台创建。
具体操作:先到 TaoToken API Keys 生成一个 Key,然后在项目根目录建一个.env文件,把 Key 写进去。注意不要提交到 git。接着用 curl 验证一下接口通不通:
curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [ {"role": "user", "content": "用 TypeScript Compiler API 写一个函数,遍历 AST 找出没有 else 的 if 语句,返回位置数组"} ] }'如果返回里能看到choices[0].message.content带代码,说明链路通了。这一步的意义是:后面插件里findUncoveredBranches的遍历逻辑,你可以先让模型生成一版,再自己改。模型对ts.visitEachChild、ts.isSwitchStatement这些 API 的写法比较熟,能帮你少查文档。
需要提醒的是,TaoToken 只是模型调用入口,它不替代你的编辑器,也不直接连你的生产数据库。插件工程本身还是跑在本地 VSCode 里。如果你打算长期做这类编码辅助工具,可以考虑 TaoToken Coding Plan,把模型调用额度固定下来,避免调试时频繁换 Key。
环境准备好后,建一个最小验证脚本scripts/analyze.ts,把分析函数先写出来跑通,再进插件工程。这样第 3 章的配置你复制过去就能直接用。
3. 可复制配置:AST 遍历 + 插件激活 + 检测规则
这一章是全文的技术核心,分三块:分析器代码、插件入口、规则配置。每一块都给完整可复制的片段,路径和原文保持一致。
先建目录结构:
vscode-branchguard/ ├── src/ │ ├── analyzer/ │ │ └── branchAnalyzer.ts │ └── extension.ts ├── package.json ├── tsconfig.json └── .vscodeignore第一块,src/analyzer/branchAnalyzer.ts,这是 AST 遍历的核心。它用 TypeScript Compiler API 创建 SourceFile,然后用 Visitor 模式递归遍历,识别两类问题:没有 else 的 if、没有 default 的 switch。
import * as ts from 'typescript'; export interface BranchLocation { start: number; end: number; type: 'missing-else' | 'missing-default'; message: string; } export function findUncoveredBranches(sourceFile: ts.SourceFile): BranchLocation[] { const uncovered: BranchLocation[] = []; const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<ts.Node> => { if (ts.isIfStatement(node) && !node.elseStatement) { uncovered.push({ start: node.getStart(), end: node.getEnd(), type: 'missing-else', message: '缺少 else 分支:此 if 的否定路径未被显式处理', }); } if (ts.isSwitchStatement(node)) { const hasDefault = node.caseBlock?.clauses.some((c) => ts.isDefaultClause(c) ); if (!hasDefault) { uncovered.push({ start: node.getStart(), end: node.getEnd(), type: 'missing-default', message: '缺少 default 分支:所有 case 之外的值未被处理', }); } } return ts.visitEachChild(node, visitor, ts.getProgram()!); }; ts.visitNode(sourceFile, visitor); return uncovered; }注意ts.getProgram()在纯脚本环境里可能为 null,插件里因为有语言服务所以有值。如果你在独立脚本里跑,把ts.visitEachChild的第三个参数换成undefined也能工作,只是少了类型上下文。
第二块,src/extension.ts,插件入口。它注册一个 HoverProvider,当光标停在 TypeScript 文件里时,实时解析当前文档并返回告警。
import * as vscode from 'vscode'; import * as ts from 'typescript'; import { findUncoveredBranches } from './analyzer/branchAnalyzer'; export function activate(context: vscode.ExtensionContext) { const provider: vscode.HoverProvider = { provideHover(document, position) { if (document.languageId !== 'typescript') return null; const text = document.getText(); const sourceFile = ts.createSourceFile( document.fileName, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS ); const branches = findUncoveredBranches(sourceFile); const offset = document.offsetAt(position); for (const branch of branches) { if (offset >= branch.start && offset <= branch.end) { const md = new vscode.MarkdownString(); md.appendMarkdown(`**分支检测告警**\n\n${branch.message}\n\n`); md.appendMarkdown('建议:补充 else 或 default 显式处理边界路径'); return new vscode.Hover(md); } } return null; }, }; context.subscriptions.push( vscode.languages.registerHoverProvider('typescript', provider) ); } export function deactivate() {}第三块,package.json的 contributes 声明。这里要写全三件套:激活事件、语言支持、配置项。
{ "name": "vscode-branchguard", "displayName": "BranchGuard", "version": "0.0.1", "engines": { "vscode": "^1.85.0" }, "activationEvents": ["onLanguage:typescript"], "main": "./out/extension.js", "contributes": { "configuration": { "title": "BranchGuard", "properties": { "branchguard.rules.requireElseForIf": { "type": "boolean", "default": true, "description": "要求 if 语句必须有 else 分支" }, "branchguard.rules.requireDefaultForSwitch": { "type": "boolean", "default": true, "description": "要求 switch 语句必须有 default 分支" }, "branchguard.ignorePatterns": { "type": "array", "default": ["\\.test\\.ts$", "node_modules/"], "description": "忽略的文件路径模式" } } } } }tsconfig.json用标准配置即可,关键是outDir指向out,和main字段对应:
{ "compilerOptions": { "module": "commonjs", "target": "ES2020", "outDir": "out", "lib": ["ES2020"], "sourceMap": true, "rootDir": "src", "strict": true }, "exclude": ["node_modules", ".vscode-test"] }配置片段里出现的 Base URL、Key、Model ID 三件套,在插件场景下指的是:如果你想让插件调用模型做更复杂的语义判断,可以在settings.json里配branchguard.api.baseUrl、branchguard.api.key、branchguard.api.model。Base URL 填https://taotoken.net/api,Model ID 填你控制台里可用的模型名。这三件套缺一不可,否则请求会 401。
4. 验证请求:在编辑器里触发告警并确认分支被标记
配置写完,编译并加载插件。先跑编译:
npm install npm run compile然后在 VSCode 里按Ctrl+Shift+P,输入Developer: Install Extension from VSIX,或者直接用 F5 启动扩展开发宿主窗口。在宿主窗口里打开一个.ts文件,粘贴下面这段测试代码:
function getStatus(code: number): string { if (code === 200) return 'OK'; if (code === 404) return 'Not Found'; return 'Unknown'; } function handleEvent(type: 'click' | 'hover' | 'scroll') { switch (type) { case 'click': return 'clicked'; case 'hover': return 'hovered'; } }把光标停在第一个if的if关键字上,悬停。你应该看到黄色告警气泡,内容是“缺少 else 分支:此 if 的否定路径未被显式处理”。再把光标移到switch关键字上,悬停,看到“缺少 default 分支”。
这里有个细节:getStatus里虽然两个if都没写else,但最后有return 'Unknown'兜底,逻辑上其实是覆盖了的。BranchGuard 当前版本只做结构判断,不追踪控制流,所以它会误报。这是静态分析的边界,后面第 5 章会讲怎么用配置缓解。真正要验证的是handleEvent的switch——它确实没有default,当type传入未列举的值时函数返回undefined,这是真实风险。
验证成功的标志有三个:悬停出现告警、告警位置准确落在对应节点、修改代码后告警实时消失。你可以在handleEvent里加一行default: return 'unknown';,保存,再悬停switch,告警应该立刻不见。这个“改完即消失”的反馈,就是 AST 实时分析相比跑覆盖率报告的价值。
如果你在验证时想确认模型对这段 AST 的理解是否准确,可以把branchAnalyzer.ts贴到 TaoToken 模型对话 里,让它解释ts.isIfStatement和node.elseStatement的判断逻辑,对照自己的实现查漏。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
插件开发过程中最容易卡住的不是 AST 逻辑,而是环境配置。下面按真实报错逐条排查。
401 Unauthorized:如果你在插件里集成了模型调用,请求返回 401,先检查三件套是否齐全。Base URL 必须是https://taotoken.net/api,Key 必须是从控制台创建的、未过期的,Model ID 必须是账号下可用的。三者缺一,或者 Key 里混入了空格、换行,都会 401。排查方法:用第 2 章的 curl 命令单独测,curl 通了再查插件代码。
local proxy failed:这个报错通常出现在你本地配了网络代理,但代理进程没启动或端口不对。VSCode 插件进程继承的是系统环境变量,如果HTTP_PROXY指向一个不存在的端口,请求就会失败。排查:在终端echo $HTTP_PROXY看有没有值,有的话临时unset再启动 VSCode。注意,这里说的是本地开发环境的代理配置问题,不涉及任何网络访问方式的选择。
reading 'choices':这个报错说明你拿到的响应体里没有choices字段,代码却直接读了response.choices[0]。常见原因是接口返回了错误对象(比如{"error": {...}}),或者返回的是流式响应但你没按流解析。排查:先把原始响应console.log(JSON.stringify(response))打出来,确认结构再取值。如果是流式,要用for await逐块拼接。
OAuth 相关报错:如果你用的是需要 OAuth 授权的模型服务,插件里没走完授权流程就会报 token 无效。TaoToken 的 API Key 方式是 Bearer Token,不需要 OAuth 跳转,所以直接用 Key 即可。如果你在别处见过 OAuth 报错,检查是不是把两种鉴权方式混用了。
另外,如果你用了 CC Switch、Cline MCP 或 Codex 的auth.json,记住它们都遵循同一套三件套逻辑:Base URL、Key、Model ID。以auth.json为例:
{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "claude-sonnet-4-20250514" }三个字段任何一个写错,都会导致请求失败。Cline MCP 的配置同理,在 MCP server 的 env 里把这三个值传进去。CC Switch 切换配置时,确认切换后的 Base URL 没有多余斜杠。
6. 从告警到修复:把 BranchGuard 接进你的日常编码流
插件跑通之后,真正决定它有没有用的,是你愿不愿意每天开着它。我的做法是把它和保存动作绑定:在extension.ts里监听onDidSaveTextDocument,保存时对当前文件跑一遍分析,把未覆盖分支数量输出到状态栏。这样你不用主动悬停,保存时扫一眼状态栏就知道这个文件有没有漏分支。
状态栏代码片段:
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); context.subscriptions.push(statusBar); vscode.workspace.onDidSaveTextDocument((doc) => { if (doc.languageId !== 'typescript') return; const sourceFile = ts.createSourceFile( doc.fileName, doc.getText(), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS ); const branches = findUncoveredBranches(sourceFile); statusBar.text = branches.length > 0 ? `BranchGuard: ${branches.length} 处未覆盖` : 'BranchGuard: 通过'; statusBar.show(); });再进一步,把ignorePatterns用起来,跳过测试文件和node_modules,避免噪音。配置读取用vscode.workspace.getConfiguration('branchguard'),在分析前先判断当前文件路径是否匹配忽略模式。
最后说一个我踩过的坑:AST 分析在超大文件上会卡。一个 3000 行的.ts文件,每次悬停都重新createSourceFile会明显延迟。解决办法是加缓存——用WeakMap<TextDocument, { version: number; branches: BranchLocation[] }>,文档版本号没变就复用上次结果。VSCode 的TextDocument.version会在每次编辑后自增,用它做缓存键最准。
这套东西做完,你的编辑器就不只是语法检查器了,它开始理解代码的“意图完整性”。下一步可以扩展规则:检测try没有catch、检测Promise没有.catch、检测联合类型switch是否穷尽。每加一条规则,都是在把编码期的质量门禁往前推一步。如果你要把这套逻辑做成团队共享的插件,记得把模型调用的 Key 走环境变量,别硬编码进package.json。