ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入理解 AST 与 TSESTree:typescript-eslint 如何让 ESLint 读懂 TypeScript 代码

深入理解 AST 与 TSESTree:typescript-eslint 如何让 ESLint 读懂 TypeScript 代码 深入理解 AST 与 TSESTreetypescript-eslint 如何让 ESLint 读懂 TypeScript 代码【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint导读本文以 typescript-eslint 官方博客《ASTs and typescript-eslint》为核心系统讲解 Abstract Syntax TreeAST抽象语法树是什么、ESTree 与 TypeScript AST 两种格式为何不兼容以及 typescript-eslint 如何通过typescript-eslint/parser的三步流水线把 TypeScript 源码转换为 ESLint 可识别的 TSESTree 结构并打通 TypeScript 类型检查 API。读完本文你将理解 ESLint 规则遍历 AST 的底层原理掌握 parser 的双 AST 节点映射机制并能借助 AST Playground 亲手验证任意代码的树形结构。什么是 AST静态分析Static Analysis工具是指那些只读代码、不运行代码的工具——典型代表就是 ESLint代码检查与 Prettier代码格式化。这类工具通常会先解析parse代码把一段字符串形式的源码转换成一个它们能够推理的标准结构化格式这个格式就是Abstract Syntax Tree抽象语法树AST。之所以叫抽象语法树是因为虽然 AST 中会记录代码片段在源文件中的位置信息如行列号loc、字符区间range但它本质上是关于代码语义结构的抽象描述会忽略空白、换行等与语义无关的细节。一句话概括AST 是对你代码语法的一种对象化描述。一个 AST 示例以这行代码为例1 2;ESLint 使用的 AST 规范是[ESTree]它会把这行代码描述成如下对象{ type: ExpressionStatement, expression: { type: BinaryExpression, left: { type: Literal, value: 1, raw: 1 }, operator: , right: { type: Literal, value: 2, raw: 2 } } }AST 描述中的每一段代码被称为一个节点node每个节点都有一个节点类型node type来表明它代表哪种语法结构。上面这段代码共包含四个节点ExpressionStatement1 2;整条表达式语句BinaryExpression1 2二元运算Literal1字面量Literal2字面量注意这里嵌套关系的妙处外层节点持有指向内层节点的引用expression.left/expression.right从而构成一棵真正的树。ESLint 和 Prettier 等静态分析工具正是基于这样的对象化表示来遍历、检查和改写代码的。在 typescript-eslint 仓库中所有 ESTree 节点类型被集中定义为一个枚举常量AST_NODE_TYPES见 packages/ast-spec/src/ast-node-types.ts例如BinaryExpression、Literal、ExpressionStatement都名列其中。规则作者可以借助这些类型常量精确地定位感兴趣的语法结构。两种 AST 格式ESTree 与 TypeScript ASTESTree 并不仅仅是 ESLint 私有的格式它是一套被广泛采用的社区标准。ESLint 内置的解析器 Espree 输出的正是 ESTree 形状的 AST。而 TypeScript 有自己独立的 AST 格式通常被称为 TypeScript AST。由于 TypeScript 是独立开发的与 ESLint、ESTree、Espree 的目标不同它的 AST 在许多情况下对节点的表示方式也不一样。两者的核心差异在于TypeScript AST 是为解析不完整代码 类型检查优化的TypeScript 编译器需要容忍半成品代码、需要携带丰富的类型信息因此其 AST 设计更贴近编译器的内部需求。ESTree 是不做优化、面向通用遍历场景设计的它的目标就是让各类工具能简单、统一地遍历语法树。这一差异带来了一个直接后果ESLint 规则默认只能拿到 ESTree 格式的 AST 节点——而 ESTree 本身完全不了解 interface、type 关键字、泛型等 TypeScript 特有语法另一方面TypeScript 的类型检查 API 却只接受 TypeScript AST 格式的节点。两个世界互相不认识对方的数据结构这正是 typescript-eslint 存在的根本原因。走进 TSESTree为了解决 ESTree 与 TypeScript AST 之间的不兼容typescript-eslint 提供了自己的解析器包typescript-eslint/parser其工作流程包含三步先把 TypeScript 语法解析成TypeScript AST再基于这个 TypeScript AST创建一份ESTree AST持续跟踪两份 AST 中彼此等价的节点之间的对应关系。通过同时产出 ESTree AST 和 TypeScript AST该解析器让 ESLint 规则能够处理 TypeScript 代码同时让需要类型信息的规则能够拿到 TypeScript AST 去调用类型检查 API。这就是为什么 typescript-eslint 的快速上手文档会让你在 ESLint 配置中显式指定parser: typescript-eslint/parser——因为只有它才能产出 ESLint 需要的 ESTree 结构。我们把包含 TypeScript 特有语法的 ESTree 格式统称为TSESTree。它本质上是对 ESTree 的扩展在标准 ESTree 节点之外新增了大量TS前缀的节点类型——从源码中的AST_NODE_TYPES枚举可以直观看到这份扩展清单例如TSInterfaceDeclaration接口声明、TSTypeAliasDeclaration类型别名、TSAsExpressionas断言、TSUnionType联合类型、TSEnumDeclaration枚举声明等packages/ast-spec/src/ast-node-types.ts 中列出了完整的 TS 前缀节点族。源码视角parser 的三步流水线是如何落地的上面三步流程并非抽象概念在仓库源码中有非常具体的实现。我们可以沿调用链逐层印证第一站typescript-eslint/parser的parseForESLint。在 packages/parser/src/parser.ts 中parseForESLint会先归一化parserOptions例如默认把sourceType设为script、默认开启comment/loc/range/tokens以满足 ESLint 对 AST 的硬性要求然后调用来自typescript-eslint/typescript-estree的parseAndGenerateServices最终返回{ ast, scopeManager, services, visitorKeys }——其中ast就是 ESLint 可直接消费的 ESTree/TSESTree 树。第二站typescript-eslint/typescript-estree的解析入口。在 packages/typescript-estree/src/parser.ts 中parseAndGenerateServices会依据配置决定获取 TypeScript AST 的方式如果启用了projecttsconfig 工程程序、programs外部传入的 Program或projectService项目服务则创建带完整类型信息的ts.Program否则走createNoProgram的轻量路径只创建一个孤立的ts.SourceFile。第三站AST 转换的核心——astConverter。在 packages/typescript-estree/src/ast-converter.ts 中astConverter实例化Converter并调用convertProgram()把 TypeScript AST递归转换成 ESTree 兼容结构随后按配置可选地附加tokens词法令牌与comments注释。整个转换的逐节点映射逻辑位于庞大的 packages/typescript-estree/src/convert.ts其中Converter类维护两张 WeakMap 来记录节点对应关系esTreeNodeToTSNodeMap与tsNodeToESTreeNodeMap。双 AST 的节点映射类型检查的桥梁第三步跟踪等价节点是 typescript-eslint 最精妙的设计它的实现体现在 packages/typescript-estree/src/createParserServices.ts当存在ts.Program时createParserServices通过program.getTypeChecker()拿到类型检查器并对外暴露getTypeAtLocation、getSymbolAtLocation、getContextualType、getResolvedSignature等类型查询 API这些 API 的入参都是 ESTree/TSESTree 节点函数内部先用astMaps.esTreeNodeToTSNodeMap.get(node)把该节点映射回 TypeScript AST 节点再交给 checker 处理。换句话说ESLint 规则作者写的services.getTypeAtLocation(node)拿到的类型信息背后正是这条ESTree 节点 → TypeScript AST 节点 → 类型检查器的映射链路。这就是类型化 lint 规则能够获得完整类型上下文的原因也是typescript-eslint/parser之所以强大的核心所在。落地到配置为什么必须指定 parser在 快速上手文档 的配置示例中你需要这样声明// ts-check import js from eslint/js; import { defineConfig } from eslint/config; import tseslint from typescript-eslint; export default defineConfig({ files: [**/*.{js,ts}], extends: [js.configs.recommended, tseslint.configs.recommended], });关键点在于typescript-eslint包的默认配置内部已把parser设置为typescript-eslint/parser。若你手动配置而非使用共享配置则需显式写出export default { files: [**/*.{ts,tsx}], languageOptions: { parser: await import(typescript-eslint/parser), parserOptions: { sourceType: module, }, }, };typescript-eslint/parser本身还提供了丰富的parserOptions完整清单见 docs/packages/Parser.mdx例如project指定 tsconfig 路径以启用类型信息、projectService基于 TypeScript 语言服务的项目模式、tsconfigRootDir、jsx、lib、extraFileExtensions等。这些选项最终都会透传进上述parseAndGenerateServices流程决定程序创建方式与类型信息的有无。底层转换能力由typescript-eslint/typescript-estree提供该包的 API 文档见 docs/packages/TypeScript_ESTree.mdx。在 AST Playground 中亲手验证纸上得来终觉浅。typescript-eslint 官方提供了一个内置 AST 浏览器Playground可以针对任意输入的代码实时生成可交互的 AST打开 typescript-eslint 官网的 Playgroundhttps://typescript-eslint.io/play路径参数#showASTes可直接展开 AST 面板在左侧边栏的Options选项下找到AST ExplorerAST 浏览器将AST ViewerAST 视图的值切换为对应选项即可查看 ESTree/TSESTree 形态的实时 AST。你可以尝试输入示例中的1 2;观察它如何被拆分为ExpressionStatement → BinaryExpression → Literal/Literal的嵌套结构再试试interface Foo {}或let x: number 1;看看 TS 前缀节点如TSInterfaceDeclaration、TSTypeAnnotation是如何挂在标准 ESTree 结构上的——这正是 TSESTree扩展 ESTree的最佳直观演示。术语表把本文引入的所有关键术语汇总如下ASTAbstract Syntax Tree抽象语法树对代码语法的一种对象化树形描述。Node节点AST 中代表一段连续语法结构的单元。Node Type节点类型一个 AST 节点所对应的语法种类例如BinaryExpression、Literal。Parser解析器读取字符串并输出 AST 的工具。ESTreeESLint 及其他常见 JavaScript 工具使用的 AST 规范。EspreeESLint 内置的、输出 ESTree 形状 AST 的解析器。TypeScript ASTTypeScript 编译器自己使用的 AST 格式为解析不完整代码与类型检查而优化。TSESTreetypescript-eslint 对 ESTree 格式的扩展额外包含 TypeScript 特有语法节点。进一步探索想进一步了解不同 AST 形态可以在 astexplorer.net 上体验包括 CSS、HTML 等其他语言的 AST。想了解 AST 的更多历史与背景可以参考维基百科的 Abstract Syntax Tree 词条。想深入了解 ESLint 规则如何借助 AST 分析代码、以及如何通过typescript-eslint/parser调用 TypeScript 类型检查 API可继续阅读 typescript-eslint 仓库中的自定义规则开发文档与类型化 lint 指南。完整 AST 节点规格每个节点的字段定义、父子关系见 packages/ast-spec/src 下的源码以及 docs/packages/typescript-estree/AST_Spec.mdx。总而言之AST 是静态分析工具的通用语言ESTree 是 ESLint 世界的通用语言TypeScript AST 是类型系统的语言而 TSESTree 则是 typescript-eslint 让这两种语言握手的翻译层——理解了这条链路你就掌握了阅读任何 ESLint 规则源码和编写自定义规则的底层地图。【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表