【typescript 类型检查原理】override 是如何实现的

我心飞翔 分类:javascript

这是我参与更文挑战的第5天,活动详情查看: 更文挑战。

前段时间写过一篇类型检查的实现原理的文章,实现了简单的赋值语句和函数调用的类型检查。实际上类型检查的情况特别多,一篇文章肯定写不完,所以我准备用系列文章来讲述各种类型检查的实现原理,帮助大家更好的掌握 typescript。

这一篇我们来实现 4.3 新增的 class 的 override 关键字的类型检查。(源码链接在后面)

override 修饰符是干嘛的

首先,我们来看下这个修饰符的作用:被 override 标示的方法必须得在父类中存在,否则会报错。

class Animal {
  getName() { return ''; }
}
class Dog extends Animal {
  override bak() {
    return 'wang';
  }
  override getName() {
    return 'wang';
  }
}
 

上面这段代码会报错:This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'.就是说重写的放在父类不存在,这样能避免父类重构的时候把一些子类需要重写的方法给去掉。

如何实现 override 修饰符的类型检查

其实所有的修饰符,包括 override、public、static 等,在 parse 成 AST 后都是作为一个属性存在的,这个 override 也是,我们通过 astexplorer.net 来查看一下。

可以看到 override 属性为 true。这样我们就可以通过这个属性把该 class 的所有的需要 override 的 ClassMethod 过滤出来。

然后还可以拿到 superClass 的名字,从作用域中找到对应的声明,然后遍历 AST 找到它所声明的所有 ClassMethod。

两者对比一下,所有不在父类中的 ClassMethod 都需要报错。

代码实现

我们基于 babel 来做 parser 和分析,写一个插件来做 override 的类型检查。关于 babel 插件的基础可以看小册《babel 插件通关秘籍》。

开启语法 typescript 插件来解析 ts 语法。

const { transformFromAstSync } = require('@babel/core');
const  parser = require('@babel/parser');

const ast = parser.parse(sourceCode, {
    sourceType: 'unambiguous',
    plugins: ['typescript']
});

const { code } = transformFromAstSync(ast, sourceCode, {
    plugins: [overrideCheckerPlugin]
});
 

插件要处理的是 ClassDeclaration,我们先搭一个基本的结构:

const { declare } = require('@babel/helper-plugin-utils');

const overrideCheckerPlugin = declare((api, options, dirname) => {
    api.assertVersion(7);

    return {
        pre(file) {
            file.set('errors', []);
        },
        visitor: {
            ClassDeclaration(path, state) {
                const semanticErrors = state.file.get('errors');
                //...
                state.file.set('errors', semanticErrors);
            }
        },
        post(file) {
            console.log(file.get('errors'));
        }
    }
});
 

具体的检查逻辑是拿到父类的所有方法名,拿到当前类的所有 override 方法名,然后做下过滤。

我们首先要拿到父类的 ast,通过名字从作用域中查找。

const superClass = path.node.superClass;
if (superClass) {
    const superClassPath = path.scope.getBinding(superClass.name).path;
}
 

然后封装一个方法来拿父类方法名,通过 path.traverse 来遍历 ast,把收集到的方法名存到 state 中。

function getAllClassMethodNames(classDeclarationNodePath) {
    const state = {
        allSuperMethodNames: []
    }
    classDeclarationNodePath.traverse({
        ClassMethod(path) {
            state.allSuperMethodNames.push(path.get('key').toString())
        }
    });
    return state.allSuperMethodNames;
}
 

这样就拿到了所有父类方法名。

之后需要拿到当前类的所有方法名并过滤出 override 为 true 且不在父类中的进行报错。

const superClass = path.node.superClass;
if (superClass) {
    const superClassPath = path.scope.getBinding(superClass.name).path;
    const allMethodNames = getAllClassMethodNames(superClassPath);

    path.traverse({
        ClassMethod(path) {
            if (path.node.override){
                const methodName = path.get('key').toString();
                const superClassName = superClassPath.get('id').toString();
                if (!allMethodNames.includes(methodName)) {
                    // 报错                                    
                }
            }
        }
    });
}
 

报错的部分使用 code frame 来创建友好的代码打印格式,通过 Error.stackTraceLimit 设置为 0 去掉调用栈信息。

const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
let errorMessage = `this member cannot have an 'override' modifier because it is not declared in the base class '${superClassName}'`;
semanticErrors.push(path.get('key').buildCodeFrameError(errorMessage, Error));
Error.stackTraceLimit = tmp;
 

这样,我们就完成了 override 的类型检查,整体代码如下:

const { declare } = require('@babel/helper-plugin-utils');
function getAllClassMethodNames(classDeclarationNodePath) {
const state = {
allSuperMethodNames: []
}
classDeclarationNodePath.traverse({
ClassMethod(path) {
state.allSuperMethodNames.push(path.get('key').toString())
}
});
return state.allSuperMethodNames;
}
const overrideCheckerPlugin = declare((api, options, dirname) => {
api.assertVersion(7);
return {
pre(file) {
file.set('errors', []);
},
visitor: {
ClassDeclaration(path, state) {
const semanticErrors = state.file.get('errors');
const superClass = path.node.superClass;
if (superClass) {
const superClassPath = path.scope.getBinding(superClass.name).path;
const allMethodNames = getAllClassMethodNames(superClassPath);
path.traverse({
ClassMethod(path) {
if (path.node.override){
const methodName = path.get('key').toString();
const superClassName = superClassPath.get('id').toString();
if (!allMethodNames.includes(methodName)) {
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
let errorMessage = `this member cannot have an 'override' modifier because it is not declared in the base class '${superClassName}'`;
semanticErrors.push(path.get('key').buildCodeFrameError(errorMessage, Error));
Error.stackTraceLimit = tmp;                                    
}
}
}
});
}
state.file.set('errors', semanticErrors);
}
},
post(file) {
console.log(file.get('errors'));
}
}
});
module.exports = overrideCheckerPlugin;

github 链接

测试效果

我们用最开始的代码来测试一下:

class Animal {
getName() { return ''; }
}
class Dog extends Animal {
override bak() {
return 'wang';
}
override getName() {
return 'wang';
}
}

打印信息为:

正确的识别出了 bak 在父类不存在的错误。

至此,我们实现了 override 的类型检查!

总结

类型检查情况很多,所以需要一个系列文章去讲,这一篇我们来实现 override 的类型检查。

override 是 ts 4.3 加入的特性,带有 override 修饰符的方法必须在父类中有对应的声明,否则会报错。

我们通过 babel 插件的方式实现了类型检查,思路是从作用域取出父类的声明,然后通过 path.traverse 拿到所有方法名,之后再取当前类的所有方法名,对于没在父类中声明并且带有 override 修饰符的方法进行报错。

本文是 【typescript 类型检查原理】系列文章的第二篇,后续还会有更多 typescript 类型检查的实现原理揭秘的文章。希望能够帮助大家更好的掌握 typescript。

关于 babel 插件的知识,可以看我的 babel 小册《babel 插件通关秘籍》,其中有详细的讲解。

本文源码链接

回复

我来回复
  • 暂无回复内容