因为团队内部开启了一个持续的前端代码质量改进计划,其中一个专项就是TS类型覆盖率,期间用到了type-coverage这个仓库,所以借这篇文章分享一下这个工具,并顺便从源码阅读的角度来分析一下该工具的源码,我自己fork了一个仓库,完成了中文版本的ReadMe文件并对核心代码添加了关键注释,需要的同学可以点击传送门。
一、基本介绍
type-coverage是一个用于检查typescript代码的类型覆盖率的CLI工具,TS代码的类型覆盖率能够在某种程度上反映代码的质量水平(因为使用TS最主要的一个原因之一就是它所提供的类型安全保证)。
type-coverage该工具将检查所有标识符的类型,类型覆盖率 = any类型的所有标识符
/ 所有的标识符
,值越高,则越优秀。
typescript-coverage-report是用于生成 TypeScript 覆盖率报告的节点命令行工具,该工具基于type-coverage生成器实现。
工具的主要使用场景
(1) 呈现把项目的JS代码渐进迁移到TS代码的进度。
(2) 呈现把项目中宽松TS代码渐进转变为严格TS代码的进度。
(3) 避免引入意外的
any
(4) 持续改进该指标以提升代码的质量。
工具的安装
安装方式1 yarn global add type-coverage
安装方式2 pnpm i type-coverage -g
二、基本使用
命令行工具
直接运行获取项目的TS类型覆盖率
$ pnpm type-coverage 669 / 689 97.09% type-coverage success.
查看更多的操作指令
$ pnpm type-coverage --help type-coverage [options] [-- file1.ts file2.ts ...] -p, --project string? 告诉 CLI `tsconfig.json` 文件的位置 --detail boolean? 显示详情 --at-least number? 设置闸值,如果覆盖率小于该值,则失败 --debug boolean? 显示调试信息 --strict boolean? 是否开启严格模式 --ignore-catch boolean? 忽略捕获 --cache boolean? 是否使用缓存 --ignore-files string[]? 忽略文件 --ignore-unread boolean? 允许写入Any类型的变量 -h,--help boolean? 显示帮助信息 --is number? 设置闸值,如果覆盖率不等于该值,则失败 --update boolean? 如果 package.json 中存在typeCoverage选项,则根据 "atLeast" or "is" 值更新 --update-if-higher boolean? 如果新的类型覆盖率更高,则将package.json 中的"typeCoverage"更新到当前这个结果 --ignore-nested boolean? 忽略any类型的参数, eg: Promise --ignore-as-assertion boolean? 忽略 as 断言, eg: foo as string --ignore-type-assertion boolean? 忽略类型断言, eg: foo --ignore-non-null-assertion boolean? 忽略非空断言, eg: foo! --ignore-object boolean? Object 类型不视为 any,, eg: foo: Object --ignore-empty-type boolean? 忽略空类型, eg: foo: {} --show-relative-path boolean? 在详细信息中显示相对路径 --history-file string? 保存历史记录的文件名 --no-detail-when-failed boolean? 当CLI失败时不显示详细信息 --report-semantic-error boolean? 报告 typescript 语义错误 -- file1.ts file2.ts ... string[]? 仅检查这些文件, 适用于 lint-staged --cache-directory string? 设置缓存目录
获取覆盖率的详情
$ pnpm type-coverage --detail /wendingding/client/src/http.ts:21:52: headers /wendingding/client/src/http.ts:24:14: headers /wendingding/client/src/http.ts:28:19: headers /wendingding/client/src/http.ts:35:20: data /wendingding/client/src/http.ts:36:25: data /wendingding/client/src/http.ts:38:8: error /wendingding/client/src/http.ts:57:31: error /wendingding/client/src/http.ts:66:44: data /wendingding/client/src/http.ts:67:39: data /wendingding/client/src/http.ts:74:43: data /wendingding/client/src/http.ts:75:38: data /wendingding/client/src/http.ts:78:45: data /wendingding/client/src/http.ts:79:40: data /wendingding/client/src/main.ts:22:37: el /wendingding/client/src/main.ts:23:7: blocks /wendingding/client/src/main.ts:23:16: el /wendingding/client/src/main.ts:23:19: querySelectorAll /wendingding/client/src/main.ts:24:3: blocks /wendingding/client/src/main.ts:24:10: forEach /wendingding/client/src/main.ts:24:19: block 669 / 689 97.09% type-coverage success.
详情中的格式说明 文件路径:类型未覆盖变量所在的代码行:类型未覆盖变量在这一行中的位置(indexOf): 变量名
设置闸值
$ pnpm type-coverage --is 99 669 / 689 97.09% The type coverage rate(97.09%) is not the target(99%).
严格模式
$ pnpm type-coverage --strict 667 / 689 96.80%
使用严格模式来进行计算,通常会降低类型覆盖率。
在严格模式下,具体执行的时候会有几点处理上的区别:
(1) 如果标识符的存在且至少包含一个any
, 比如any[]
, ReadonlyArray
, Promise
, Foo
,那么他将被视为any
。
(2) 类型断言,类似 foo as string
, foo!
, foo
将会被识别为未覆盖,排除foo as const
, foo
, foo as unknown
和由isTypeAssignableTo
支持的其它安全类型断言
(3)Object类型(like foo: Object
) 和 空类型 (like foo: {}
) 将被视为any。
覆盖率报告
推荐使用来生成类型覆盖率的报告
安装包
$ yarn add --dev typescript-coverage-report# OR$ npm install --save-dev typescript-coverage-report# OR $ pnpm i typescript-coverage-report
运行包
$ yarn typescript-coverage-report# OR$ npm run typescript-coverage-report# ORpnpm typescript-coverage-report
其他更多的操作命令
% pnpm typescript-coverage-report --helpUsage: typescript-coverage-report [options]Node command tool to generate TypeScript coverage report.Options: -V, --version 版本号 -o, --outputDir [string] 设置生产报告的位置(输出目录) -t, --threshold [number] 需要的最低覆盖率. (默认值: 80) -s, --strict [boolean] 是否开启严格模式. (默认值: 否) -d, --debug [boolean] 是否显示调试信息. (默认值: 否) -c, --cache [boolean] 保存并复用缓存中的类型检查结果. (默认值: 否) -p, --project [string] tsconfig 文件的文件路径, eg: --project "./app/tsconfig.app.json" (默认值: ".") -i, --ignore-files [string[]] 忽略指定文件, eg: --ignore-files "demo1/*.ts" --ignore-files "demo2/foo.ts" (默认值: 否) --ignore-catch [boolean] ignore type any for (try-)catch clause variable (default: false) -u, --ignore-unread [boolean] 允许写入具有Any类型的变量 (default: 否) -h, --help 显示帮助信息
控制台的输出
上述命令执行后默认会在根目录生成coverage-ts
文件夹,下面给出目录结构
.├── assets│ ├── source-file.css│ └── source-file.js├── files│ └── src├── index.html└── typescript-coverage.json
打开coverage-ts/index.html
文件,能够以网页的方式查看更加详细的报告内容。
通过网页版本的报告能够更直观的帮助我们来进行专项的优化。
VSCode插件
工具库还提供了VSCVod插件版本,在VSCode插件管理器中,搜索然后安装插件后,在代码编写阶段就能够得到类型提示。
可以通过 Preferences
– Settings
– Extensions
– Type Coverage
来对插件进行配置,如果 VSCode 插件的结果与 CLI 的结果不同,则有可能项目根目录的 tsconfig.json
与 CLI tsconfig.json
配置不同或存在冲突。
如果插件无法正常工作,可以参考下issues/86#issuecomment找找看有没有对应的解决办法
Pull PR中使用
当然,我们也可以把该工具集成到
四、内部结构和工作原理
项目的结构
.├── cli│ ├── README.md│ ├── bin│ │ └── type-coverage│ ├── package.json│ └── src│ ├── index.ts│ ├── lib.d.ts│ └── tsconfig.json├── core│ ├── README.md│ ├── package.json│ └── src│ ├── cache.ts│ ├── checker.ts│ ├── core.ts│ ├── dependencies.ts│ ├── ignore.ts│ ├── index.ts│ ├── interfaces.ts│ ├── tsconfig.json│ └── tsconfig.ts├── plugin│ ├── README.md│ ├── package.json│ └── src│ ├── index.ts│ └── tsconfig.json└── vscode ├── README.md ├── package.json └── src ├── index.ts └── tsconfig.json9 directories, 25 files
核心模块的关系
核心方法梳理
命令行工具的调用逻辑
async function executeCommandLine() { /* 接收命令行参数 */ const argv = minimist(process.argv.slice(2), { '--': true }) as unknown as CliArgs const { ... 省略 } = await getTarget(argv); /* 核心主流程 */ const { correctCount, totalCount, anys } = await lint(project, {... 省略配置项}); /* 计算类型覆盖率 */ const percent = Math.floor(10000 * correctCount / totalCount) / 100 const percentString = percent.toFixed(2) console.log(`${correctCount} / ${totalCount} ${percentString}%`)}
主流程(Core)
//core.ts const defaultLintOptions: LintOptions = { debug: false, files: undefined, oldProgram: undefined, strict: false, enableCache: false, ignoreCatch: false, ignoreFiles: undefined, ignoreUnreadAnys: false, fileCounts: false, ignoreNested: false, ignoreAsAssertion: false, ignoreTypeAssertion: false, ignoreNonNullAssertion: false, ignoreObject: false, ignoreEmptyType: false, reportSemanticError: false,} /* 核心函数:主流程 */export async function lint(project: string, options?: Partial) { /* 检测的前置条件(参数) */ const lintOptions = { ...defaultLintOptions, ...options } /* 获取项目的根目录和编译选项 */ const { rootNames, compilerOptions } = await getProjectRootNamesAndCompilerOptions(project) /* 通过ts创建处理程序 */ const program = ts.createProgram(rootNames, compilerOptions, undefined, lintOptions.oldProgram) /* 获取类型检查器 */ const checker = program.getTypeChecker() /* 声明用于保存文件的集合 */ const allFiles = new Set() /* 声明用于保存文件信息的数组 */ const sourceFileInfos: SourceFileInfo[] = [] /* 根据配置参数从缓存中读取类型检查结果(缓存的数据) */ const typeCheckResult = await readCache(lintOptions.enableCache, lintOptions.cacheDirectory) /* 读取配置参数中的忽略文件信息 */ const ignoreFileGlobs = lintOptions.ignoreFiles ? (typeof lintOptions.ignoreFiles === 'string' ? [lintOptions.ignoreFiles] : lintOptions.ignoreFiles) : undefined /* 获取所有的SourceFiles并遍历 */ for (const sourceFile of program.getSourceFiles()) { let file = sourceFile.fileName if (!file.includes('node_modules')) { /* 如果不是绝对路径 */ if (!lintOptions.absolutePath) { /* process.cwd() 是当前Node进程执行时的文件夹地址,也就是工作目录,保证了文件在不同的目录下执行时,路径始终不变 */ /* __dirname 是被执行的js文件的地址,也就是文件所在目录 */ /* 计算得到文件的相对路径 */ file = path.relative(process.cwd(), file) /* 如果路径以..开头则跳过该文件 */ if (file.startsWith('..')) { continue } } /* 如果lintOptions.files中不包含该文件,则跳过 */ if (lintOptions.files && !lintOptions.files.includes(file)) { continue } /* 如果该文件存在于忽略配置项中,则跳过 */ if (ignoreFileGlobs && ignoreFileGlobs.some((f) => minimatch(file, f))) { continue } /* 添加文件到集合 */ allFiles.add(file) /* 计算文件的哈希值 */ const hash = await getFileHash(file, lintOptions.enableCache) /* 检查该文件是否存在缓存数据 */ const cache = typeCheckResult.cache[file] /* 如果存在缓存数据 */ if (cache) { /* 如果配置项定义了ignoreNested 则忽略 嵌套的any */ if (lintOptions.ignoreNested) { cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.containsAny) } /* 如果配置项定义了ignoreAsAssertion 则忽略 不安全的as */ if (lintOptions.ignoreAsAssertion) { cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.unsafeAs) } /* 如果配置项定义了ignoreTypeAssertion 则忽略 不安全的类型断言 */ if (lintOptions.ignoreTypeAssertion) { cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.unsafeTypeAssertion) } /* 如果配置项定义了ignoreNonNullAssertion 则忽略 不安全的非空断言 */ if (lintOptions.ignoreNonNullAssertion) { cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.unsafeNonNull) } } /* 更新sourceFileInfos对象数组 */ sourceFileInfos.push({ file, /* 文件路径 */ sourceFile, hash,/* 哈希值 */ cache: cache && cache.hash === hash ? cache : undefined /* 该文件的缓存信息 */ }) } } /* 如果启用了缓存 */ if (lintOptions.enableCache) { /* 获取依赖集合 */ const dependencies = collectDependencies(sourceFileInfos, allFiles) /* 遍历sourceFileInfos */ for (const sourceFileInfo of sourceFileInfos) { /* 如果没有使用缓存,那就清理依赖 */ if (!sourceFileInfo.cache) { clearCacheOfDependencies(sourceFileInfo, dependencies, sourceFileInfos) } } } let correctCount = 0 let totalCount = 0 /* 声明anys数组 */ const anys: AnyInfo[] = [] /* 声明fileCounts映射 */ const fileCounts = new Map<string, Pick>()/* 遍历sourceFileInfos */ for (const { sourceFile, file, hash, cache } of sourceFileInfos) { /* 如果存在缓存,那么直接根据缓存处理后就跳过 */ if (cache) { /* 累加correctCount和totalCount */ correctCount += cache.correctCount totalCount += cache.totalCount /* 把缓存的anys合并到anys数据中 */ anys.push(...cache.anys.map((a) => ({ file, ...a }))) if (lintOptions.fileCounts) { /* 统计每个文件的数据 */ fileCounts.set(file, { correctCount: cache.correctCount, totalCount: cache.totalCount, }) } continue } /* 获取忽略的集合 */ const ingoreMap = collectIgnoreMap(sourceFile, file) /* 组织上下文对象 */ const context: FileContext = { file, sourceFile, typeCheckResult: { correctCount: 0, totalCount: 0, anys: [] }, ignoreCatch: lintOptions.ignoreCatch, ignoreUnreadAnys: lintOptions.ignoreUnreadAnys, catchVariables: {}, debug: lintOptions.debug, strict: lintOptions.strict, processAny: lintOptions.processAny, checker, ingoreMap, ignoreNested: lintOptions.ignoreNested, ignoreAsAssertion: lintOptions.ignoreAsAssertion, ignoreTypeAssertion: lintOptions.ignoreTypeAssertion, ignoreNonNullAssertion: lintOptions.ignoreNonNullAssertion, ignoreObject: lintOptions.ignoreObject, ignoreEmptyType: lintOptions.ignoreEmptyType, } /* 关键流程:单个文件遍历所有的子节点 */ sourceFile.forEachChild(node => { /* 检测节点,并更新context的值 */ /* ?为什么选择引用传递?? */ checkNode(node, context) }) /* 更新correctCount 把当前文件的数据累加上*/ correctCount += context.typeCheckResult.correctCount /* 更新totalCount 把当前文件的数据累加上*/ totalCount += context.typeCheckResult.totalCount /* 把当前文件的anys数据累加 */ anys.push(...context.typeCheckResult.anys.map((a) => ({ file, ...a }))) if (lintOptions.reportSemanticError) { const diagnostics = program.getSemanticDiagnostics(sourceFile) for (const diagnostic of diagnostics) { if (diagnostic.start !== undefined) { totalCount++ let text: string if (typeof diagnostic.messageText === 'string') { text = diagnostic.messageText } else { text = diagnostic.messageText.messageText } const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, diagnostic.start) anys.push({ line, character, text, kind: FileAnyInfoKind.semanticError, file, }) } } } /* 如果需要统计每个文件的信息 */ if (lintOptions.fileCounts) { /* 更新当前文件的统计结果 */ fileCounts.set(file, { correctCount: context.typeCheckResult.correctCount, totalCount: context.typeCheckResult.totalCount }) } /* 如果启用了缓存 */ if (lintOptions.enableCache) { /* 把本次计算的结果保存一份到缓存对象中 */ const resultCache = typeCheckResult.cache[file] /* 如果该缓存对象已经存在,那么就更新数据,否则那就新建缓存对象 */ if (resultCache) { resultCache.hash = hash resultCache.correctCount = context.typeCheckResult.correctCount resultCache.totalCount = context.typeCheckResult.totalCount resultCache.anys = context.typeCheckResult.anys } else { typeCheckResult.cache[file] = { hash, ...context.typeCheckResult } } } } /* 再操作的最后,检查是否启用了缓存 */ if (lintOptions.enableCache) { /* 如果启用了缓存,那就把缓存数据保存起来 */ await saveCache(typeCheckResult, lintOptions.cacheDirectory) } // 返回计算的结果 return { correctCount, totalCount, anys, program, fileCounts }}
在core.ts
文件中,提供了两个同步和异步两种方式来执行任务,他们分别是lint
和lintSync
,上面的代码给出了核心代码和部分注释,整体来看处理逻辑比较简单,这里给出源码中这些函数间的关系图。
核心逻辑就是根据目录先获取所有的文件,然后再遍历这些文件,接着依次处理每个文件中的标识符,统计correctCount、totalCount和anys。
关键模块(checker)
//checkNode方法 核心检测函数(递归调用)export function checkNode(node: ts.Node | undefined, context: FileContext): void { if (node === undefined) { return } if (context.debug) { const { line, character } = ts.getLineAndCharacterOfPosition(context.sourceFile, node.getStart(context.sourceFile)) console.log(`node: ${context.file}:${line + 1}:${character + 1}: ${node.getText(context.sourceFile)} ${node.kind}(kind)`) } checkNodes(node.decorators, context) checkNodes(node.modifiers, context) if (skippedNodeKinds.has(node.kind)) { return } /* 关键字 */ if (node.kind === ts.SyntaxKind.ThisKeyword) { collectData(node, context) return } /* 标识符 */ if (ts.isIdentifier(node)) { if (context.catchVariables[node.escapedText as string]) { return } collectData(node, context) return } /* 其他处理 */ if (ts.isQualifiedName(node)) { checkNode(node.left, context) checkNode(node.right, context) return }}// Nodes检查function checkNodes(nodes: ts.NodeArray | undefined, context: FileContext): void { if (nodes === undefined) { return } for (const node of nodes) { checkNode(node, context) }}/* 收集器 */function collectData(node: ts.Node, context: FileContext) { const types: ts.Type[] = [] const type = context.checker.getTypeAtLocation(node) if (type) { types.push(type) } const contextualType = context.checker.getContextualType(node as ts.Expression) if (contextualType) { types.push(contextualType) } if (types.length > 0) { context.typeCheckResult.totalCount++ if (types.every((t) => typeIsAnyOrInTypeArguments(t, context.strict && !context.ignoreNested, context))) { const kind = types.every((t) => typeIsAnyOrInTypeArguments(t, false, context)) ? FileAnyInfoKind.any : FileAnyInfoKind.containsAny const success = collectAny(node, context, kind) if (!success) { //收集所有的any collectNotAny(node, context, type) } } else { //收集所有的 notAny collectNotAny(node, context, type) } }}
主流程和checker类型检查模块的函数调用关系
项目源码中核心函数间的调用关系图
按照功能模块来划分的话,主要包含主函数lint、缓存处理(saveCache、readCache等)、和类型检查(checkNode等),其中checkNode中涉及到了很多Node节点的类型,而ts.
相关的方法也都值得关注。
四、拓展内容type-coverage项目依赖的主要模块(包)
模块(包) | 描述 |
---|---|
Definitely Typed | TypeScript 类型定义 |
minimist | 命令行参数解析器 |
fast-glob | 文件系统操作 |
minimatch | 路径匹配库 |
normalize-path | 规范化路径 |
clean-scripts | 元文件脚本清理CLI |
clean-release | 文件操作的CLI |
rimraf | 删除文件(夹) |
tslib | TS运行时库 |
tsutils | Typescript工具函数 |
部门依赖(模块)的补充说明
Definitely Typed这是一个TypeScript 类型定义的仓库,下面这些依赖都根植于这个库
@types/minimatch@types/minimist@types/node@types/normalize-path
minimist是参数解析器核心库。
在项目中的使用场景
//文件路径 packages/cli/src/index.tsimport minimist = require('minimist')//...const argv = minimist(process.argv.slice(2), { '--': true }) as unknown as CliArgsconst showVersion = argv.v || argv.version...
fast-glob遍历文件系统并根据 Unix Bash shell 使用的规则返回匹配指定模式的路径名集合。
项目中的使用场景
//文件路径 /packages/core/src/tsconfig.tsimport fg = require('fast-glob')//...const includeFiles = await fg(rules, { ignore, cwd: dirname, })files.push(...includeFiles)
minimatch是 npm 内部使用的匹配库,它通过将 glob 表达式转换为 JavaScriptRegExp 对象来工作。
项目中的使用场景
//文件路径 /packages/core/src/core.ts#L3import minimatch = require('minimatch')//...if (ignoreFileGlobs && ignoreFileGlobs.some((f) => minimatch(file, f))) { continue}
具体的使用示例
minimatch("bar.foo", "*.foo") // true!minimatch("bar.foo", "*.bar") // false!minimatch("bar.foo", "*.+(bar|foo)", { debug: true }) // true, and noisy!minimatch('/a/b', '/a/*/c/d', { partial: true }) // true, might be /a/b/c/dminimatch('/a/b', '/**/d', { partial: true }) // true, might be /a/b/.../dminimatch('/x/y/z', '/a/**/z', { partial: true }) // false, because x !== a
normalize-path规范化路径的库。
项目中的使用场景
import normalize = require('normalize-path')async function getRootNames(config: JsonConfig, dirname: string) {//...let rules: string[] = []for (const file of include) { const currentPath = path.resolve(dirname, file) const stats = await statAsync(currentPath) if (stats === undefined || stats.isFile()) { rules.push(currentPath) } else if (stats.isDirectory()) { rules.push(`${currentPath.endsWith('/') ? currentPath.substring(0, currentPath.length - 1) : currentPath}/**/*`) }}rules = rules.map((r) => normalize(r))ignore = ignore.map((r) => normalize(r))}
clean-scripts是使 package.json脚本干净的 CLI 工具。
项目中的使用场景
//文件路径 /clean-scripts.config.tsimport { Tasks, readWorkspaceDependencies } from 'clean-scripts'const tsFiles = `"packages/**/src/**/*.ts"`const workspaces = readWorkspaceDependencies()export default { build: [ new Tasks(workspaces.map((d) => ({ name: d.name, script: [ `rimraf ${d.path}/dist/`, `tsc -p ${d.path}/src/`, ], dependencies: d.dependencies }))), ...workspaces.map((d) => `node packages/cli/dist/index.js -p ${d.path}/src --detail --strict --suppressError`) ], lint: { ts: `eslint --ext .js,.ts ${tsFiles}`, export: `no-unused-export ${tsFiles} --need-module tslib --need-module ts-plugin-type-coverage --ignore-module vscode --strict`, markdown: `markdownlint README.md` }, test: [], fix: `eslint --ext .js,.ts ${tsFiles} --fix`}
在上面的build命令中发现了rimraf
,在lint命令中发现了tslib
这个工具包。
tslib是一个用于TypeScript的运行时库,其中包含所有 TypeScript 辅助函数。
rimraf以包的形式包装rm -rf命令,用来删除文件和文件夹的,不管文件夹是否为空,都可删除。
适用场景:项目中build文件的时候每次都会生成一个dist目录,有时需要把dist目录里的所以旧文件全部删掉,就可以使用rimraf命令
安装和使用
$ pnpm install rimraf -g
clean-release是 CLI 工具,用于将要发布的文件复制到tmp clean 目录中,用于 npm 发布、electronjs打包、docker映像创建或部署。
项目中的使用
//文件路径 /clean-release.config.ts#L8import { Configuration } from 'clean-release'const config: Configuration = { include: [ 'packages/*/dist/*', 'packages/*/es/*', 'packages/*/bin/*', 'packages/*/package.json', 'packages/*/README.md', ], exclude: [ ], askVersion: true, changesGitStaged: true, postScript: ({ dir, tag, version, effectedWorkspacePaths = [] }) => [ ...effectedWorkspacePaths.map((w) => w.map((e) => { if (e === 'packages/vscode') { return tag ? undefined : `cd "${dir}/${e}" && yarn install --registry=https://registry.npmjs.org/ && rm -f "${dir}/yarn.lock" && vsce publish ${version}` } return tag ? `npm publish "${dir}/${e}" --access public --tag ${tag}` : `npm publish "${dir}/${e}" --access public` })), `git-commits-to-changelog --release ${version}`, 'git add CHANGELOG.md', `git commit -m "${version}"`, `git tag -a v${version} -m 'v${version}'`, 'git push', `git push origin v${version}` ]}export default config
tsutilsTypescript工具函数。
在type-coverage的项目中,该工具库用来处理忽略,即当注释中存在type-coverage:ignore-line
和type-coverage:ignore-next-line
的时候,这部分代码将不被记入到类型覆盖率的计算中。
下面列出项目中使用该工具的代码部分
//文件路径:/packages/core/src/ignore.ts#L4import * as ts from 'typescript'import * as utils from 'tsutils/util'export function collectIgnoreMap(sourceFile: ts.SourceFile, file: string) { const ingoreMap: { [file: string]: Set } = {} utils.forEachComment(sourceFile, (_, comment) => { const commentText = comment.kind === ts.SyntaxKind.SingleLineCommentTrivia ? sourceFile.text.substring(comment.pos + 2, comment.end).trim() : sourceFile.text.substring(comment.pos + 2, comment.end - 2).trim() if (commentText.includes('type-coverage:ignore-next-line')) { if (!ingoreMap[file]) { ingoreMap[file] = new Set() } const line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line ingoreMap[file]?.add(line + 1) } else if (commentText.includes('type-coverage:ignore-line')) { if (!ingoreMap[file]) { ingoreMap[file] = new Set() } const line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line ingoreMap[file]?.add(line) } }) return ingoreMap}
原创文章,访问个人站点 文顶顶 以获得更好的阅读体验。版权声明:著作权归作者所有,商业转载请联系文顶顶获得授权,非商业转载请注明出处。