ESLint/Prettier插件不够用,因无法满足团队硬性规范(如console.log需带模块前缀、.ts禁any但.d.ts允许),且VSCode仅提示不执行AST校验;自定义规则需发包维护,协作成本高,难以深度绑定保存修复与提交拦截。

为什么直接用 ESLint / Prettier 插件不够用
因为团队内部有硬性规范:比如所有 console.log 必须带模块前缀(console.log('[auth]', 'login success')),或禁止在 .ts 文件里出现 any 类型但允许在 .d.ts 中使用——这类规则 ESLint 默认不覆盖,插件市场也找不到现成匹配项。
VSCode 自身只做编辑器层提示,不执行自定义 AST 校验逻辑;而直接改 ESLint 规则需要发布独立配置包、维护共享依赖,协作成本高且无法与 VSCode 编辑行为深度绑定(比如保存时自动修复 + 禁止提交)。
- ESLint 的
context.report()无法控制 VSCode 的「问题面板」分类(如标为「错误」还是「警告」) - 第三方插件如
eslint-plugin-unicorn不支持按文件后缀动态启用/禁用规则 - VSCode 的
workspace.onDidSaveTextDocument事件触发时机早于 ESLint 的缓存解析,容易漏检刚键入的代码
用 vscode-languageclient + ts-morph 实现轻量级实时校验
不用重写语言服务器,也不用 fork TypeScript Server。核心思路是:监听文件保存 → 用 ts-morph 解析 AST → 遍历节点执行自定义断言 → 调用 vscode.languages.createDiagnosticCollection 注入问题。
关键点在于绕过 ESLint 的配置加载和规则注册流程,把校验逻辑收归到插件内部,便于条件化控制:
- 对
console.log检查:匹配CallExpression节点,判断expression.getFullText()是否以'console.log'开头,再检查第一个参数是否为字符串字面量且含方括号 - 对
any类型检查:遍历TypeReferenceNode,取type.getText(),排除node.getSourceFile().fileName.endsWith('.d.ts')的情况 - 性能优化:仅对
document.languageId === 'typescript'或'javascript'的文件触发,跳过node_modules和dist目录路径
如何让违规代码无法被 Git 提交
VSCode 插件本身无权拦截 git commit,但可通过 git.hooks 配合本地 pre-commit 脚本联动。插件需暴露一个 CLI 入口供脚本调用,例如 npx vscode-custom-linter --files src/**/*.{ts,js}。
这个 CLI 不依赖 VSCode 环境,底层复用同一套 ts-morph 校验逻辑,返回标准 JSON 格式错误列表(含 file、line、message)。pre-commit 脚本捕获非零退出码并打印错误,阻断提交:
#!/bin/sh npx vscode-custom-linter --files $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|js)$') || exit 1
- 必须用
git diff --cached而非git status,否则会误检未暂存的修改 - CLI 中需设置
project.addSourceFilesAtPaths()时忽略node_modules,否则首次运行极慢 - VSCode 插件 UI 层显示的错误行号要和 CLI 输出严格一致,否则开发者无法定位——这要求两者共用同一版
ts-morph和相同CompilerOptions
调试时 AST 节点类型总对不上怎么办
常见现象:写了 node.isKind(ts.SyntaxKind.CallExpression) 却始终不进分支,实际是节点类型为 ts.SyntaxKind.ExpressionStatement 包裹了 CallExpression。TS 的 AST 结构比直觉深一层。
正确做法永远先打印节点全貌:
console.log(node.getFullText().substring(0, 100)); console.log(node.getKindName()); // 不要用 node.kind,它返回数字
- 不要依赖
node.getText()判断内容,它可能包含换行或空格;优先用node.getKindName()+node.getFullText().trim() -
node.getParent()可能返回undefined,需判空;深层嵌套建议用node.forEachDescendant()替代递归遍历 - 测试用例必须覆盖单行/多行字符串、模板字面量、带注释的语句——这些场景下
getFullText()返回值差异极大
最易被忽略的是:ts-morph 默认使用项目根目录下的 tsconfig.json,但如果工作区打开的是子文件夹,需显式传入 tsConfigFilePath: path.join(workspaceFolder, 'tsconfig.json'),否则类型解析失败导致节点缺失。


















