又有半年没写文章了,趁着最近不那么忙先来水一篇。恰巧最近给typescript-eslint实现的支持decorator metadata的PR合掉了,那就拿这个写写吧。标题因为不知道怎么取,就拿最终实现的目标写上去了。
背景
先说说做这件事的背景吧。我们公司一小伙苦于项目里的循环依赖,写了个脚本来检测,但是检测归检测并没有给出好的解决方案。我看了下如果要从根本上解决(比如抽离公共依赖之类的方法)解决那肯定少不了一番重构,可是老代码谁也不敢轻举妄动啊。
刚好上半年把整个组的typescript升级到3.8以上了,我就想3.8新增的Type-Only Imports或许能解决一部分问题。纵观循环依赖的代码,大概有一半以上确实只依赖了类型,比如下面这种很常见的情况
// Container.ts
import Element from './Element';
export default class Container {
elements: Element[];
}
// Element.ts
import Container from './Container';
export default class Element {
context: Container;
}
虽然有从代码上优化的解决方法(比如抽离接口等方法,本文就不说明了),最简单的方法就是改成import type
,另外明显后者是可以用工具自动完成的。
consistent-type-imports规则
因为这个优化明显可以通过单文件的代码分析来完成,现成的工具里第一个想到做这个的就是eslint了,于是找了找发现果然有现成的规则[consistent-type-imports]。这个规则可以通过配置将文件里只有类型引用的import变量改为import type。
但是试了下这个规则后发现有个问题,会导致项目里的decorator拿不到metadata,一些依赖类型信息实现的功能就会失效。看了下原因是因为当tsconfig里emitDecoratorMetadata === true
时,ts会将带有decorator的类里的成员变量类型、方法参数和返回类型输出到js里。比如下面将成员变量类型输出的情况,会在js里引用该类型,而当前规则无法判断这个情况,将原代码改为import type
,这时实际输出的js里就拿不到这个Type
了。
// input ts with emitDecoratorMetadata = true
import Type from 'type';
class A {
@deco
a?: Type;
}
// output js ignore some helper function
import Type from 'type';
class A {
}
__decorate([
deco,
__metadata("design:type", typeof (_a = typeof Type !== "undefined" && Type) === "function" ? _a : Object)
], A.prototype, "a", void 0);
什么时候会输出decorator metadata
为了修复这个问题首先要看看ts在什么时候会输出decorator metadata。
首先能添加到metadata的类型一定只能是个存在对应类的直接类型,任何加工过的类型和type alias都会给你个Object,下面列了一些不能输出或者输出异常的情况。
type Type = T1;
class A {
// 下面decorator全都省略...
a: T1 | T2; // 直接输出Object
b: Pick<T1, 'key'>; // 同上
c: Type; // 会将 Type 输出,但是type alias会被抹去,所以这里并不能引用到T1
d: React['Component']; // 直接输出Object,使用 React.Component 可以正常输出
e: (a: string) => string; // 直接输出Object
}
decorator通常会出现在4个地方:1. 类的定义上;2. 类的成员变量上;3. 类的方法上;4. 类的方法的参数上;这里以这种方式分开讨论。
类的定义上
当类的定义上存在decorator时,构造函数的所有参数类型都会输出metadata。
@deco
class A {
// Type会被输出到metadata
constructor(t: Type) {}
}
类的成员变量上
当类的成员变量上存在decorator时,该变量的类型会输出metadata
class A {
@deco
field: Type; // Type会被输出到metadata
}
类的方法上
当类的方法上存在decorator时,该方法显式声明的参数类型和返回类型会输出到metadata。另外要注意的是这里指的方法不包括赋值给成员变量的函数。
class A {
@deco
foo(input: Type1): Type2 {} // Type1, Type2会被输出到metadata
@deco
foo = (input: Type1): Type2 => {}; // 这样子只会被当作普通的成员变量处理
}
这里有一种特例就是 getter 和 setter,对于相同名称的accessor,只要给其中一个添加decorator就会输出相关类型的metadata,这个类型以setter参数类型优先。比如下面虽然只给getter加了decorator,却只使用了setter的参数类型Type2作为metadata。
// source
class A {
@deco
get foo(): Type1 {}
set foo(v: Type2) {}
}
// output metadata
__decorate([
deco,
__metadata("design:type", typeof (_b = typeof Type2 !== "undefined" && Type2) === "function" ? _b : Object),
__metadata("design:paramtypes", [typeof (_c = typeof Type2 !== "undefined" && Type2) === "function" ? _c : Object])
], A.prototype, "foo", null);
这里 getter 和 setter 的metadata会按照一定规则合并,一般标识符类型和非复杂的计算值都是支持的。
class A {
// 相同的普通key、字符串、数字字面量和变量可以合并
get a() {}
set ['a'](v: Type) {}
get [1]() {}
set [1](v: Type) {}
// const key = 'k';
get [key]() {}
set [key](v: Type) {}
// 其他类型和表达式不可以合并
get [true]() {}
set [true](v: Type) {}
get ['a' + 'b']() {}
set ['a' + 'b'](v: Type) {}
}
类的方法的参数上
当类的方法的参数上存在decorator时结果和类方法上有decorator差不多,参数类型和返回类型都会添加到metadata。比如下面两种情况结果是一样的
class A {
@deco
foo(input: Type1): Type2 {}
foo(@deco input: Type1): Type2 {}
}
同样 setter 上的参数decorator也是个特例,结果是啥也不会输出。这时不仅metadata不会输出,连decorator的功能也不会生效。这大概是个TS的BUG。所以下面这种情况需要特殊判断排除掉
class A {
set foo(@deco input: Type1): Type2 {}
}
如何给typescript-eslint修复
整理完规则后就是看看如何修复了,具体的代码比较多就不详细说明了,就讲讲涉及的一些模块吧。这里可以直接从consistent-type-imports这个规则的代码入手。
在代码里可以看到创建规则是通过给createRule
传入几个用来描述规则的参数和承载主要逻辑的 create
函数来实现,create
函数返回的是ast visitor,在这里可以实现当前规则所需要的visitor,最后在Program:exit
通过context.report
报告错误信息和修复函数。
一开始我的想法就是直接在规则代码里通过AST分析判断类型是否会被decorator metadata引用到,拿到最终变量的引用是否全是类型和当前的import类型来判断错误以及修复。后来经过维护者提醒需要将部分判断逻辑实现在scope-manager里,这样对所有规则都是有用的,我也觉得很有道理。
scope manager是eslint就有的一个概念,它会根据AST生成作用域以及内部的所有变量和引用,eslint-typescript则是重写以支持了TS的语法特性,比如可以通过变量上的references
拿到所有的引用然后通过Reference
上的isValueReference
和isTypeReference
判断引用类型,在[consistent-type-import]里就可以通过这两点在访问ImportDeclaration
时就能判断是否是正确的import类型。原先的BUG也主要就是scope manager在生成引用类型时没有考虑decorator metadata的影响,那么如何修复就很明显了,在scope manager里生成变量引用时将decorator metadata考虑进来。
scope manager这块也是写各种ast visitor,每次进入新的scope时要 scopeManager.nestForScope(node)
一下,定义了变量时 currentScope.defineIdentifier(...)
,引用了变量时 currentScope.referenceValue
或 currentScope.referenceType
。因为这个需求只和类有关,同时为了方便隔离嵌套类的情况,就独立了一个ClassVisitor
,专门处理class scope的生成,不过好像因为独立的这个visitor没有漏了一些ast node的处理后面还导致了一些bug。。。