很多项目历史悠久,其中很多 文件或是 export 出去的变量 已经不再使用,非常影响维护迭代。举个例子来说,后端问你:“某某接口统计一下某接口是否还有使用?”你在项目里一搜,好家伙,还有好几处使用呢,结果那些定义或文件是从未被引入的,这就会误导你们去继续维护这个文件或接口,影响迭代效率。
先从删除废弃的 exports 讲起,后文会讲删除废弃文件。
删除 exports,有几个难点:
先给出整体的思路,公司内的小伙伴推荐了 pzavolinsky/ts-unused-exports[2] 这个开源库,并且已经在项目中稳定使用了一段时间,这个库可以搞定上述第一步的诉求,也就是找出 export 出去,但是其他文件未 import 的变量。但下面两步依然很棘手,先给出我的结论:
对分析出的文件调用 ESLint 的 API,no-unused-vars
这个 ESLint rule 天生就可以分析出文件内部某个变量是否使用,但默认情况下它是不支持对 export 出去的变量进行分析的,因为既然你 export 了这个变量,那其实 ESLint 就认为你这个变量会被外部使用。对于这个限制,其实只需要 fork 下来稍微改写即可。
2 . 如何稳定的删除这些变量?
自己编写 rule fixer
删除掉分析出来的无用变量,之后就是格式化,由于 ESLint 删除代码后格式会乱掉,所以手动调用 prettier API 让代码恢复美观即可。
接下来我会对上述每一步详细讲解。
使用测试下来, pzavolinsky/ts-unused-exports[3] 确实可以靠谱的分析出 未使用的 export 变量 ,但是这种分析 import、export
关系的工具,只是局限于此,不会分析 export
出去的这个变量 在代码内部是否有使用到 。
第二步的问题比较复杂,这里最终选用 ESLint
配合自己 fork 改写 no-unused-vars
这个 rule
,并且自己提供规则对应的修复方案 fixer
来实现。
fix
函数,直到不再有新的可修复错误为止。no-unused-vars
默认是不考虑 export
出去的变量的,而经过我对源码的阅读发现,仅仅 修改少量的代码 就可以打破这个限制,让 export
出去的变量也可以被分析,在模块内部是否使用。export
出去的变量 被其他模块引用 ,但由于在 模块内部未使用 ,也会 被分析为未使用变量 。所以需要给 rule
提供一个 varsPattern
的选项,把分析范围限定在 ts-unused-exports
给出的 导出未使用变量 中,如 varsPattern: '^foo$|^bar$'
。no-unused-vars
只给出提示,没有提供 自动修复 的方案,需要自己编写,下面详细讲解。当我们在 IDE 中编写代码时,有时会发现保存之后一些 ESLint 飘红的部分被自动修复了,但另一部分却没有反应。这其实是 ESLint 的 rule fixer
的作用。参考官方文档的 Apply Fixer[6] 章节,每个 ESLint Rule 的编写者都可以决定自己的这条规则 是否可以自动修复,以及如何修复。修复不是凭空产生的,需要作者自己对相应的 AST 节点做分析、删除等操作,好在 ESLint 提供了一个 fixer
工具包,里面封装了很多好用的节点操作方法,比如 fixer.remove()
, fixer.replaceText()
。官方的 no-unused-vars
由于稳定性等原因未提供代码的自动修复方案,需要自己对这个 rule
写对应的 fixer[7] 。官方给出的解释在 Add fix/suggestions to no-unused-vars
rule · Issue #14585 · eslint/eslint[8] 。
把 ESLint Plugin 单独拆分到一个目录中,结构如下:
packages/eslint-plugin-deadvars
├── ast-utils.js
├── eslint-plugin.js
├── eslint-rule-typescript-unused-vars.js
├── eslint-rule-unused-vars.js
├── eslint-rule.js
└── package.json
eslint-plugin.js
: 插件入口,外部引入后才可以使用 rule
eslint-rule-unused-vars.js
: ESLint 官方的 eslint/no-unused-vars
代码,主要的核心代码都在里面。eslint-rule-typescript-unused-vars
: typescript-eslint/no-unused-vars
内部的代码,继承了 eslint/no-unused-vars
,增加了一些 TypeScript AST 节点的分析。eslint-rule.js
:规则入口,引入了 typescript rule
,并且利用 eslint-rule-composer[9] 给这个规则增加了自动修复的逻辑。我们的分析涉及到删除,所以必须有一个严格的限定范围,就是 exports 出去 且被 ts-unused-exports 认定为 外部未使用 的变量。所以考虑增加一个配置 varsPattern
,把 ts-unused-exports 分析出的未使用变量名传入进去,限定在这个名称范围内。主要改动逻辑是在 collectUnusedVariables
这个函数中,这个函数的作用是 收集作用域中没有使用到的变量 ,这里把 exports 且不符合变量名范围 的全部跳过不处理。
else if (
config.varsIgnorePattern &&
config.varsIgnorePattern.test(def.name.name)
) {
// skip ignored variables
continue;
} else if (
+ isExported(variable) &&
+ config.varsPattern &&
+ !config.varsPattern.test(def.name.name)
) {
+ // 符合 varsPattern
+ continue;
}
这样外部就可以这样使用这样的方式来限定分析范围:
rules: {
'@deadvars/no-unused-vars': [
'error',
{ varsPattern: '^foo$|^bar$' },
]
}
接着删除掉原版中 收集未使用变量时 对 isExported
的判断,把 exports 出去但文件内部未使用 的变量也收集起来。由于上一步已经限定了变量名,所以这里只会收集到 ts-unused-exports 分析出来的变量。
if (
!isUsedVariable(variable) &&
- !isExported(variable) &&
!hasRestSpreadSibling(variable)
) {
unusedVars.push(variable);
}
接下来主要就是增加自动修复,这部分的逻辑在 eslint-rule.js
中,简单来说就是对上一步分析出来的各种未使用变量的 AST 节点进行判断和删除。贴一下简化的函数处理代码:
module.exports = ruleComposer.mapReports(rule, (problem, context) => {
problem.fix = fixer => {
const { node } = problem;
const { parent } = node;
// 函数节点
switch (parent.type) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
// 调用 fixer 进行删除
return fixer.remove(parent);
...
...
default:
return null;
}
};
return problem;
});
目前会对以下几种节点类型进行删除:
后续新增节点的删除逻辑,只需要维护这个文件即可。
之前基于 webpack-deadcode-plugin[10] 做了一版无用代码删除,但是在实际使用的过程中,发现一些问题。
首先是 速度太慢 ,这个插件会基于 webpack 编译的结果来分析哪些文件是无用的,每次使用都需要编译一遍项目。
而且前几天加入了 fork-ts-checker-webpack-plugin 进行类型检查之后, 这个删除方案突然失效了 ,检测出来的只有 .less 类型的无用文件,经过和排查后发现是这个插件的锅,它会把 src 目录下的所有 ts 文件 都加入到 webpack 的依赖中,也就是 compilation.fileDependencies
(可以尝试开启这个插件,在开发环境试着手动改一个完全未导入的 ts 文件,一样会触发重新编译)
而 deadcode-plugin 就是依赖 compilation.fileDependencies
这个变量来判断哪些文件未被使用,所有 ts 文件都在这个变量中的话,扫描出来的无用文件自然就只有其他类型了。
这个行为应该是插件的官方有意而为之,考虑如下情况:
// 直接导入一个 TS 类型
import { IProps } from "./type.ts";
// use IProps
在使用旧版的 fork-ts-checker-webpack-plugin 时,如果此时改动了 IProps 造成了类型错误,是不会触发 webpack 的编译报错的。
经过排查,目前官方的行为好像是把 tsconfig 中的 include
里的所有 ts 文件加入到依赖中,方便改动触发编译,而我们项目中的 include
是 ["src/**/*.ts"]
,所以……
具体讨论可以查看这个 Issue:Files that provide only type dependencies for main entry and unused files are not being checked for[11]
首先尝试在 deadcode 模式中手动删除 fork-ts-checker-webpack-plugin,这样可以扫描出无用依赖,但是上文中那样从文件中只导入类型的情况,还是会被认为是无用的文件而误删。
考虑到现实场景中单独建一个 type.ts 文件书写接口或类型的情况比较多,只好先放弃这个方案。
转而一想, pzavolinsky/ts-unused-exports[12] 这个工具既然都能分析出 所有文件的 导入导出变量的依赖关系 ,那分析出未使用的文件应该也是小意思才对。
经过源码调试,大概梳理出了这个工具的原理:
ts.parseJsonConfigFileContent
API 扫描出项目内完整的 ts 文件路径。 {
"path": "src/component/A",
"fullPath": "/Users/admin/works/test/src/component/A.tsx",
{
"path": "src/component/B",
"fullPath": "/Users/admin/works/test/apps/app/src/component/B.tsx",
}
...
2 . 通过 TypeScript 内置的一些 compile API 分析出文件之间的 exports 和 imports 关系。
{
"path": "src/component/A",
"fullPath": "/Users/admin/works/test/src/component/A.tsx",
"imports": {
"styled-components": ["default"],
"react": ["default"],
"src/components/B": ["TestComponentB"]
},
"exports": ["TestComponentA"]
}
3 . 根据上述信息来分析出每个文件中每个变量的使用次数,筛选出未使用的变量并且输出。
到此思路也就有了,把所有文件中的 imports
信息取一个合集,然后从第一步的文件集合中找出未出现在 imports
里的文件即可。
在第一次检测出无用文件并删除后,很可能会暴露出一些新的无用文件。比如以下这样的例子:
[
{
"path": "a",
"imports": "b"
},
{
"path": "b",
"imports": "c"
},
{
"path": "c"
}
]
文件 a 引入了文件 b,文件 b 引入了文件 c。
第一轮扫描的时候,没有任何文件引入 a,所以会把 a 视作无用文件。
由于 a 引入了 b,所以不会把 b 视作无用的文件,同理 c 也不会视作无用文件。
所以 第一轮删除只会删掉 a 文件 。
只要在每次删除后,把 files 范围缩小,比如第一次删除了 a 以后,files 只留下:
[
{
path: "b",
imports: "c",
},
{
path: "c",
},
];
此时会发现没有文件再引入 b 了,b 也会被加入无用文件的列表,再重复此步骤,即可删除 c 文件。
原项目只考虑到了单个项目和单个 tsconfig 的处理,而如今 monorepo 已经非常流行了,monorepo 中每个项目都有自己的 tsconfig,形成一个自己的 project,而经常有项目 A 里的文件或变量被项目 B 所依赖使用的情况。
而如果单独扫描单个项目内的文件,就会把很多被子项目使用的文件误删掉。
这里的思路也很简单:
--deps
参数,允许传入多个子项目的 tsconfig 路径。imports
部分,找出从别名为 @main
的主项目中引入的依赖(比如 import { Button } from '@main/components'
)imports
合并到主项目的依赖集合中,共同进行接下来的扫描步骤。TypeScript 提供的 API,默认只会扫描 .ts, .tsx
后缀的文件,在开启 allowJS
选项后也会扫描 .js, .jsx
后缀的文件。而项目中很多的 .less, .svg
的文件也都未被使用,但它们都被忽略掉了。
这里我断点跟进 ts.parseJsonConfigFileContent
函数内部,发现有一些比较隐蔽的参数和逻辑,用比较 hack 的方式支持了自定义后缀。
当然,这里还涉及到了一些比较麻烦的改造,比如这个库原本是没有考虑 index.ts, index.less
同时存在这种情况的,通过源码的一些改造最终绕过了这个限制。
目前默认支持了.less, .sass, .scss
这些类型文件的扫描 ,只要你确保该后缀的引入都是通过 import
语法,那么就可以通过增加的 extraFileExtensions
配置来增加自定义后缀。
import * as ts from "typescript";
const result = ts.parseJsonConfigFileContent(
parseJsonResult.config,
ts.sys,
basePath,
undefined,
undefined,
undefined,
extraFileExtensions?.map((extension) => ({
extension,
isMixedContent: false,
// hack ways to scan all files
scriptKind: ts.ScriptKind.Deferred,
}))
);
ts-prune[13] 是完全基于 TypeScript 服务实现的一个 dead exports 检测方案。
TypeScript 服务提供了一个实用的 API:findAllReferences[14] ,我们平时在 VSCode 里右键点击一个变量,选择 “Find All References” 时,就会调用这个底层 API 找出所有的引用。
ts-morph[15] 这个库封装了包括 findAllReferences
在内的一些底层 API,提供更加简洁易用的调用方式。
ts-prune 就是基于 ts-morph 封装而成。
一段最简化的基于 ts-morph 的检测 dead exports 的代码如下:
// this could be improved... (ex. ignore interfaces/type aliases that describe a parameter type in the same file)
import { Project, TypeGuards, Node } from "ts-morph";
const project = new Project({ tsConfigFilePath: "tsconfig.json" });
for (const file of project.getSourceFiles()) {
file.forEachChild((child) => {
if (TypeGuards.isVariableStatement(child)) {
if (isExported(child)) child.getDeclarations().forEach(checkNode);
} else if (isExported(child)) checkNode(child);
});
}
function isExported(node: Node) {
return TypeGuards.isExportableNode(node) && node.isExported();
}
function checkNode(node: Node) {
if (!TypeGuards.isReferenceFindableNode(node)) return;
const file = node.getSourceFile();
if (
node.findReferencesAsNodes().filter((n) => n.getSourceFile() !== file)
.length === 0
)
console.log(
`[${file.getFilePath()}:${node.getStartLineNumber()}: ${
TypeGuards.hasName(node) ? node.getName() : node.getText()
}`
);
}
findAllReferences
的检测范围包括文件内部,开箱即用。findAllReferences
的调用,在大型项目中速度还是有点慢。findAllReferences
并不识别 Dynamic Import 语法,需要额外处理 import()
形式导入的模块。所以综合评估下来,最后还是选择了 ts-unused-exports + ESLint 的方案。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/AbnsbiQj1zT73AsHs6s-vQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。