基于AST实现对项目全函数缺少入参TS类型的扫描及覆盖率统计

142 阅读3分钟

背景

一个旧项目想要通过引入 Typescript 来解决类型难题,对于TS,虽然Leader倡导大家积极的写类型,但是实际上很多TS文件中,实际使用类型的地方却不如预期,因为很多同学只是单纯的将文件类型由JS改为TS,加了一些不痛不痒的类型声明,甚至很多地方直接any大法,那么有什么办法可以扫描统计TS文件中真实的TS类型使用程度呢?

思考

TS类型覆盖率其实是一个大话题,想思考如何解决这个问题先,不妨先思考一下如果通过工具来扫描代码文件中所有函数入参的类型覆盖情况,如果可以独立设计完成,那么TS类型覆盖率也可以实现

image.png

分解问题

既然要了解一个TS文件中所有函数的入参类型,那么最直接的办法就是将TS代码文件解析为AST,然后遍历寻找文件中所有的函数AST节点,然后针对这些节点进一步遍历寻找它的参数部分(参数可能有多个,依次都要检测),如果发现相应的参数属性为空,或者为any,则判定该函数未实现类型覆盖

代码实现

import * as ts from 'typescript'; 
import * as fs from 'fs'; 
import * as path from 'path';

// 定义一个接口来存储函数信息 
interface FunctionInfo { 
    name: string; file: 
    string; line: number; 
    hasParameterTypes: boolean; 
    hasReturnType: boolean; 
}

// 创建一个函数来分析单个TS文件并返回函数信息 
function analyzeFile(file: string): FunctionInfo[] { 
    const functions: FunctionInfo[] = []; 
    const sourceCode = fs.readFileSync(file, 'utf8'); 
    const sourceFile = ts.createSourceFile( file, sourceCode, ts.ScriptTarget.Latest, true ); // 遍历 AST 树,查找函数声明 
    const traverse = (node: ts.Node) => {  // 递归函数
        if (ts.isFunctionDeclaration(node)) {   // 判定AST节点是否为函数
            const name = node.name ? node.name.getText() : 'anonymous';  // 匿名函数统一给一个名称anonymous
            const line =sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1; // 获取函数定义的代码行
            const hasParameterTypes = node.parameters.every(p => !!p.type);  // 判断它的参数是不是每个都定义了类型
            
            functions.push({ name, file, line, hasParameterTypes}); 
       } 
       ts.forEachChild(node, traverse); }; 
       traverse(sourceFile); 
       return functions; 
}

通过上述简单的脚本,我们就可以把一个TS代码文件中所有的函数入参情况塞入了一个自定义的对象数组中,那么接下来就很简单了,直接上代码:

// 针对单文件的分析入口函数
function main(file: string) {
    let total = 0; 
    let missingTypes = 0;
    const missResults: FunctionInfo[] = [];
    
    const functions = analyzeFile(file);
    total += functions.length;
    for (const func of functions) { 
        if (!func.hasParameterTypes) { 
            missingTypes++;
            missResults.push(func);
        } 
    }
}

那么 main 方法便返回了某一个TS文件中缺少类型定义的函数以及其相关信息

既然已经实现了某个TS文件的函数入参无类型扫描,那么实现整个项目的函数入参无类型扫描无非就是扫描项目中所有的TS文件,具体实现如下:

// 创建一个函数来扫描指定目录下的所有TS文件 
function scanDirectory(dir: string): string[] { 
    const files: string[] = []; 
    const entries = fs.readdirSync(dir, { withFileTypes: true }); 
    for (const entry of entries) { 
        const fullPath = path.join(dir, entry.name); 
        if (entry.isDirectory()) {  // 判断是否为目录,如果是,则继续递归
            files.push(...scanDirectory(fullPath)); 
        } else if (entry.isFile() && entry.name.endsWith('.ts')) {
            files.push(fullPath);  // 将遍历得到的ts文件信息都推入数组
        } 
    } 
    return files; 
}

上面函数核心就是通过遍历制定目录寻找目录下面所有的ts文件

完成了上述所有步骤,最后我们重写一下扫描工具的入口函数main,让其支持扫描整个代码项目目录:

// 重写后支持全量扫描的 main 函数
function main(dir: string) { 
    const files = scanDirectory(dir); 
    let total = 0; 
    let missingTypes = 0; 
    const results: FunctionInfo[] = [];
    
    for (const file of files) { 
        const functions = analyzeFile(file); 
        total += functions.length; 
        for (const func of functions) { 
            if (!func.hasParameterTypes) { 
                missingTypes++; 
                results.push(func); 
            } 
        } 
    }
    
    console.log(`总共扫描了 ${total} 个函数。`); 
    console.log(`其中 ${missingTypes} 个函数缺少类型定义:`);
    console.table(results);
}

// 运行程序 

main('./src'); // 替换为你要扫描的代码仓库源码目录

有问题可以在评论区留言,帮助大家学习AST是我分享的初衷。