Typescript 類型檢查原理之Override 是如何實現的
本文轉載自微信公眾號「神光的編程秘籍」,作者神說要有光 。轉載本文請聯系神光的編程秘籍公眾號。
前段時間寫過一篇類型檢查的實現原理的文章,實現了簡單的賦值語句和函數調用的類型檢查。實際上類型檢查的情況特別多,一篇文章肯定寫不完,所以我準備用系列文章來講述各種類型檢查的實現原理,幫助大家更好的掌握 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 插件通關秘籍》,其中有詳細的講解。
本文源碼鏈接 https://github.com/QuarkGluonPlasma/babel-plugin-exercize/tree/master/exercize-type-checker/src