type
status
date
slug
summary
tags
category
icon
password
fullWidth
fullWidth

前言

TypeScript提供了一套完整的编译器API,可见微软官方文档:Using the Compiler API · microsoft/TypeScript Wiki
模块
用途
ts.CompilerHost
自定义编译器文件系统交互
ts.Program
表示整个TypeScript项目
ts.SourceFile
单个文件的AST表示
ts.TransformerFactory
代码转换功能
ts.TypeChecker
类型系统访问
允许开发者以编程方式分析、检查和生成代码,有以下用途:
  1. 静态代码分析:在运行前发现潜在问题,如API过时
    1. 自动代码转换:用于批量升级代码库
      1. 自定义检查规则:强制执行特定编码规范
        用代码处理代码是一件比较抽象的工作,需要用机器视角阅读代码文本,借助可视化辅助工具AST Viewer帮助理解语法树,本文以实际业务场景为入口,给出自定义检查规则的实现方案和细节。

        抽象语法树

        抽象语法树(Abstract Syntax Tree, AST)是源代码语法结构的一种抽象表示,它以树状形式表现编程语言的语法结构。
        TypeScript AST是TypeScript代码的树状结构表示:
        • 去掉了源代码中的无关细节(如分号、括号等)
        • 保留了代码的语法结构
        • 每个节点(Node)表示源代码中的一个构造
        🐈‍⬛
        这个网站可以查看TypeScript代码对应的语法树:TypeScript AST Viewer
        notion image
        更复杂的语法也可以解析出来:
        notion image

        节点类型

        SyntaxKind是TypeScript编译器API中的枚举类型,它定义了所有可能的语法节点类型;每个 AST节点都有一个kind属性,其值是SyntaxKind的枚举成员。
        以下是一些常见的SyntaxKind值的含义:
        1. CallExpression
          1. AsExpression
            1. Identifier
              1. StringLiteral
                1. ObjectLiteralExpression
                  1. PropertyAccessExpression
                    1. VariableDeclaration
                      1. PropertyAssignment
                        1. TrueKeyword & FalseKeyword
                          1. TypeReference
                            1. SpreadAssignment

                              自定义检查实现

                              以一个具体的业务需求为例,说明自定义检查的实现思路:用接口作为泛型构造Schema,检查Schema中的Fields属性中的各子Scheme的Optional字段是否和接口中的可选性一致
                              例如,positionRollbackConfigScheme是用接口IPositionRollbackConfig声明的Schema:
                              • 接口中Range?Position?是可选字段
                              • Schema的Fields中的RangePosition对应的Schema中Optional必须为true
                              以下代码是符合规则的示例:

                              构建工程

                              从检查的入口构建TypeScript工程:
                              • 基于项目tsconfig.json配置初始化工程
                              • 通配符匹配interfaceDirschemeDir所有.ts文件加入分析范围
                              • SourceFile.getInterfaces()得到所有接口声明并处理
                              • 处理Action、Component、Var开头的子目录下的所有Schema文件
                                • 通过getVariableDeclarations()得到所有变量声明
                                • 调用scanObjectSchemeNode()解析每个变量声明节点
                              • 在检查之前调用ResolveAllUnresolved()处理Schema依赖关系
                              • 最后调用CheckAllIsValid()验证所有Schema的必填字段和类型约束

                              处理接口

                              处理接口的重点是找到接口的继承关系,可以用AST Viewer进行查看一些复杂接口的语法结构:
                              notion image
                              对于接口对象的定义如下:

                              遍历接口对象

                              • 遍历每个接口声明
                              • 提取接口基本信息(名称、属性、可选标记)
                              • 记录继承关系并标记需要后续解决的接口
                              • 最后统一解决所有继承关系

                              递归合并父接口属性

                              • SolveUnsolved()深度优先遍历继承树
                              • mergeParentProperties()自底向上合并属性
                              • 最后要将HeritageList设置成undefined
                                • 避免循环继承造成无限递归
                                • 避免菱形继承造成重复合并的开销

                              处理Schema

                              遍历Schema对象

                              已知项目中所有的Schema对象都是通过函数调用来创建的,有两种方式:
                              • 调用函数创建对象
                                • 调用函数后类型转换
                                  在抽象语法树中,如果调用了函数之后进行类型转换,CallExpression会被AsExpression包裹,作为子节点而存在:
                                  notion image
                                  因此需要特殊处理,如果获取不到CallExpression,尝试获取AsExpression的Child:
                                  如果识别到为函数表达式,则要继续通过resolveObjectSchemeCall()解析:
                                  • 解析调用泛型函数时的接口名称interfaceName,只识别I开头的接口作为Schema的关联接口
                                    • 比如,ITrackLocationslocationsScheme的关联接口
                                    • 比如,虽然buffIdScheme在创建时用了<number>作为接口类型,但这里不认为它是接口,需要设置成undefined
                                    • 比如,以下Schema没有关联接口,得到的接口名是undefined
                                  • 得到作为对象字面量的参数objectArgument
                                  • 遍历每个参数(objectArgument的子节点)解析各属性
                                  • 最后将生成的SchemeInfo加入缓存(可能会有多个Schema同名,因此需要用Map<string, SchemeInfo[]>存储)
                                  • 对于Fields属性对应的节点,要进一步解析
                                  解析Fields属性对应的节点,从属性中读取每个子字段的可选性,生成ObjectSchemeState
                                  • 尝试寻找函数调用节点(同样地,对于函数转型节点要提取出它的子节点函数调用节点)
                                    • 找到,说明是以下属性赋值形式
                                      • 得到所有属性赋值节点,尝试寻找Optional属性
                                        • 存在,提取Optional的布尔值,如果为true则加入到optionalKeys
                                        • 不存在,如果里面有展开表达式,则解析展开表达式(内部也是加入unResolvedMap
                                    • 未找到,说明是以下属性赋值形式
                                      • 生成segment加入unResolvedMap
                                  resolveIdentifierAssignment()resolveSpreadAssignment()处理对象属性直接引用其他Schema的语法(如field: otherSchemefield: createScheme({...otherScheme})),提取被引用Schema的定义路径,并记录依赖关系到unResolvedMap
                                  最后,使用Add()将生成的ObjectSchemeState添加到缓存中:
                                  • 添加拥有未处理字段的Schema的Key到集合
                                  • 添加所有Schema的KeyObjectSchemeState到映射

                                  未处理字段

                                  注意到前面处理了未处理字段和对应Schema的依赖Schema,在检查开始之前要进行处理:
                                  • 对于每个存在未处理字段的Schema(UnsolvedStates中的键值对),遍历所有UnresolvedKeys:
                                    • 使用GetSchemeIsOptional()尝试获取它的可选项
                                    • 如果可选则加入OptionalKeys集合
                                  由于在ResolveAllUnresolved()调用之前已经完成了所有Schema的遍历,因此可以从缓存MySchemeCache中获取名称为SchemeNameSchema列表(存在同名Schema,但一定在不同脚本文件中):
                                  • segment中解析Schema的名称和定义所在的脚本文件
                                  • 从列表中找到所在脚本文件匹配的Schema
                                  • 尝试获取该Schema的可选性
                                    • 能获取到,直接返回可选性布尔值
                                    • 获取不到,则递归调用获取它的关联Schema(声明时用…Object展开的另一个Schema)的可选性

                                  辅助函数

                                  在源文件中查找符号的导入路径:
                                  解析相对路径为绝对路径:
                                  通过标识符获取Schema定义所在的脚本文件路径:

                                  最终检查

                                  SchemeManager::CheckAllIsValid()中进行最终检查:
                                  对于每个Schema的检查在CheckSchemeOptionalIsValid()中进行:
                                  【TypeScript】浅析TypeScript中的装饰器(旧语法)游戏AI行为决策-目标导向行为规划(GOAP)通用框架
                                  Loading...
                                  Latest posts
                                  工作笔记:一些乱七八糟的踩坑记录
                                  2025-9-2
                                  【TypeScript】浅析TypeScript中的装饰器(旧语法)
                                  2025-8-28
                                  【TypeScript】基于抽象语法树的自定义代码检查实现方案
                                  2025-8-21
                                  自卷笔记:一些乱七八糟的踩坑记录和小知识小技巧
                                  2025-7-23
                                  自制Python任务调度模块-MySchedule
                                  2025-7-23
                                  游戏AI行为决策-行为树(BehaviorTree)框架搭建与应用
                                  2025-7-23