前端高級進階必備的 AST知識和實戰(zhàn)
大廠技術(shù)??堅持周更??精選好文
認識 AST
定義: 在計算機科學(xué)中,抽象語法樹是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個節(jié)點都表示源代碼中的一種結(jié)構(gòu)。之所以說語法是“抽象”的,是因為這里的語法并不會表示出真實語法中出現(xiàn)的每個細節(jié)。
從定義中我們只需要知道一件事就行,那就是 AST 是一種樹形結(jié)構(gòu),并且是某種代碼的一種抽象表示。
在線可視化網(wǎng)站:https://astexplorer.net/ ,利用這個網(wǎng)站我們可以很清晰的看到各種語言的 AST 結(jié)構(gòu)。
estree[1]
estree 就是 es 語法對應(yīng)的標準 AST,作為一個前端也比較方便理解。我們以官方文檔為例
https://github.com/estree/estree/blob/master/es5.md
下面看一個代碼
console.log('1')
AST 為
{
??"type":?"Program",
??"start":?0,?//?起始位置
??"end":?16,?//?結(jié)束位置,字符長度
??"body":?[
????{
??????"type":?"ExpressionStatement",?//?表達式語句
??????"start":?0,
??????"end":?16,
??????"expression":?{
????????"type":?"CallExpression",?//?函數(shù)方法調(diào)用式
????????"start":?0,
????????"end":?16,
????????"callee":?{
??????????"type":?"MemberExpression",?//?成員表達式?console.log
??????????"start":?0,
??????????"end":?11,
??????????"object":?{
????????????"type":?"Identifier",?//?標識符,可以是表達式或者結(jié)構(gòu)模式
????????????"start":?0,
????????????"end":?7,
????????????"name":?"console"
??????????},
??????????"property":?{
????????????"type":?"Identifier",?
????????????"start":?8,
????????????"end":?11,
????????????"name":?"log"
??????????},
??????????"computed":?false,?//?成員表達式的計算結(jié)果,如果為?true?則是?console[log],?false?則為?console.log
??????????"optional":?false
????????},
????????"arguments":?[?//?參數(shù)
??????????{
????????????"type":?"Literal",?//?文字標記,可以是表達式
????????????"start":?12,
????????????"end":?15,
????????????"value":?"1",
????????????"raw":?"'1'"
??????????}
????????],
????????"optional":?false
??????}
????}
??],
??"sourceType":?"module"
}
看兩個稍微復(fù)雜的代碼
const?b?=?{?a:?1?};
const?{?a?}?=?b;
function?add(a,?b)?{
????return?a?+?b;
}
這里建議讀者自己將上述代碼復(fù)制進上面提到的網(wǎng)站中,自行理解 estree 的各種節(jié)點類型。當然了,我們也不可能看一篇文章就記住那么多類型,只要心里有個大致的概念即可。
認識 acorn[2]
由 JavaScript 編寫的 JavaScript 解析器,類似的解析器還有很多,比如 Esprima[3] 和 Shift[4] ,關(guān)于他們的性能,Esprima 的官網(wǎng)給了個測試地址[5],但是由于 acron 代碼比較精簡,且 webpack 和 eslint 都依賴 acorn,因此我們這次從 acorn 下手,了解如何使用 AST。

基本操作
acorn 的操作很簡單
import?*?as?acorn?from?'acorn';
const?code?=?'xxx';
const?ast?=?acorn.parse(code,?options)
這樣我們就能拿到代碼的 ast 了,options 的定義如下
??interface?Options?{
????ecmaVersion:?3?|?5?|?6?|?7?|?8?|?9?|?10?|?11?|?12?|?13?|?2015?|?2016?|?2017?|?2018?|?2019?|?2020?|?2021?|?2022?|?'latest'
????sourceType?:?'script'?|?'module'
????onInsertedSemicolon?:?(lastTokEnd:?number,?lastTokEndLoc?:?Position)?=>?void
????onTrailingComma?:?(lastTokEnd:?number,?lastTokEndLoc?:?Position)?=>?void
????allowReserved?:?boolean?|?'never'
????allowReturnOutsideFunction?:?boolean
????allowImportExportEverywhere?:?boolean
????allowAwaitOutsideFunction?:?boolean
????allowSuperOutsideMethod?:?boolean
????allowHashBang?:?boolean
????locations?:?boolean
????onToken?:?((token:?Token)?=>?any)?|?Token[]
????onComment?:?((
??????isBlock:?boolean,?text:?string,?start:?number,?end:?number,?startLoc?:?Position,
??????endLoc?:?Position
????)?=>?void)?|?Comment[]
????ranges?:?boolean
????program?:?Node
????sourceFile?:?string
????directSourceFile?:?string
????preserveParens?:?boolean
??}
ecmaVersion ECMA 版本,默認時 es7 locations 默認為 false,設(shè)置為 true 時節(jié)點會攜帶一個 loc 對象來表示當前開始與結(jié)束的行數(shù)。 onComment 回調(diào)函數(shù),每當代碼執(zhí)行到注釋的時候都會觸發(fā),可以獲取當前的注釋內(nèi)容
獲得 ast 之后我們想還原之前的函數(shù)怎么辦,這里可以使用 astring[6]
import?*?as?astring?from?'astring';
const?code?=?astring.generate(ast);
實現(xiàn)普通函數(shù)轉(zhuǎn)換為箭頭函數(shù)
接下來我們就可以利用 AST 來實現(xiàn)一些字符串匹配不太容易實現(xiàn)的操作,比如將普通函數(shù)轉(zhuǎn)化為箭頭函數(shù)。
我們先來看兩個函數(shù)的AST有什么區(qū)別
function?add(a,?b)?{
????return?a?+?b;
}
const?add?=?(a,?b)?=>?{
????return?a?+?b;
}
{
??"type":?"Program",
??"start":?0,
??"end":?41,
??"body":?[
????{
??????"type":?"FunctionDeclaration",
??????"start":?0,
??????"end":?40,
??????"id":?{
????????"type":?"Identifier",
????????"start":?9,
????????"end":?12,
????????"name":?"add"
??????},
??????"expression":?false,
??????"generator":?false,
??????"async":?false,
??????"params":?[
????????{
??????????"type":?"Identifier",
??????????"start":?13,
??????????"end":?14,
??????????"name":?"a"
????????},
????????{
??????????"type":?"Identifier",
??????????"start":?16,
??????????"end":?17,
??????????"name":?"b"
????????}
??????],
??????"body":?{
????????"type":?"BlockStatement",
????????"start":?19,
????????"end":?40,
????????"body":?[
??????????{
????????????"type":?"ReturnStatement",
????????????"start":?25,
????????????"end":?38,
????????????"argument":?{
??????????????"type":?"BinaryExpression",
??????????????"start":?32,
??????????????"end":?37,
??????????????"left":?{
????????????????"type":?"Identifier",
????????????????"start":?32,
????????????????"end":?33,
????????????????"name":?"a"
??????????????},
??????????????"operator":?"+",
??????????????"right":?{
????????????????"type":?"Identifier",
????????????????"start":?36,
????????????????"end":?37,
????????????????"name":?"b"
??????????????}
????????????}
??????????}
????????]
??????}
????}
??],
??"sourceType":?"module"
}
{
??"type":?"Program",
??"start":?0,
??"end":?43,
??"body":?[
????{
??????"type":?"VariableDeclaration",
??????"start":?0,
??????"end":?43,
??????"declarations":?[
????????{
??????????"type":?"VariableDeclarator",
??????????"start":?6,
??????????"end":?43,
??????????"id":?{
????????????"type":?"Identifier",
????????????"start":?6,
????????????"end":?9,
????????????"name":?"add"
??????????},
??????????"init":?{
????????????"type":?"ArrowFunctionExpression",
????????????"start":?12,
????????????"end":?43,
????????????"id":?null,
????????????"expression":?false,
????????????"generator":?false,
????????????"async":?false,
????????????"params":?[
??????????????{
????????????????"type":?"Identifier",
????????????????"start":?13,
????????????????"end":?14,
????????????????"name":?"a"
??????????????},
??????????????{
????????????????"type":?"Identifier",
????????????????"start":?16,
????????????????"end":?17,
????????????????"name":?"b"
??????????????}
????????????],
????????????"body":?{
??????????????"type":?"BlockStatement",
??????????????"start":?22,
??????????????"end":?43,
??????????????"body":?[
????????????????{
??????????????????"type":?"ReturnStatement",
??????????????????"start":?28,
??????????????????"end":?41,
??????????????????"argument":?{
????????????????????"type":?"BinaryExpression",
????????????????????"start":?35,
????????????????????"end":?40,
????????????????????"left":?{
??????????????????????"type":?"Identifier",
??????????????????????"start":?35,
??????????????????????"end":?36,
??????????????????????"name":?"a"
????????????????????},
????????????????????"operator":?"+",
????????????????????"right":?{
??????????????????????"type":?"Identifier",
??????????????????????"start":?39,
??????????????????????"end":?40,
??????????????????????"name":?"b"
????????????????????}
??????????????????}
????????????????}
??????????????]
????????????}
??????????}
????????}
??????],
??????"kind":?"const"
????}
??],
??"sourceType":?"module"
}
找到區(qū)別之后我們就可以有大致的思路
找到 FunctionDeclaration
將其替換為 VariableDeclarationVariableDeclarator節(jié)點
在 VariableDeclarator節(jié)點的init屬性下新建ArrowFunctionExpression節(jié)點
并將 FunctionDeclaration節(jié)點的相關(guān)屬性替換到ArrowFunctionExpression上即可
但是由于 acorn 處理的 ast 只是單純的對象,并不具備類似 dom 節(jié)點之類的對節(jié)點的操作能力,如果需要操作節(jié)點,需要寫很多工具函數(shù), 所以我這里就簡單寫一下。
import?*?as?acorn?from?"acorn";
import?*?as?astring?from?'astring';
import?{?createNode,?walkNode?}?from?"./utils.js";
const?code?=?'function?add(a,?b)?{?return?a+b;?}?function?dd(a)?{?return?a?+?1?}';
console.log('in:',?code);
const?ast?=?acorn.parse(code);
walkNode(ast,?(node)?=>?{
????if(node.type?===?'FunctionDeclaration')?{
????????node.type?=?'VariableDeclaration';
????????const?variableDeclaratorNode?=?createNode('VariableDeclarator');
????????variableDeclaratorNode.id?=?node.id;
????????delete?node.id;
????????const?arrowFunctionExpressionNode?=?createNode('ArrowFunctionExpression');
????????arrowFunctionExpressionNode.params?=?node.params;
????????delete?node.params;
????????arrowFunctionExpressionNode.body?=?node.body;
????????delete?node.body;
????????variableDeclaratorNode.init?=?arrowFunctionExpressionNode;
????????node.declarations?=?[variableDeclaratorNode];
????????node.kind?=?'const';
????}
})
console.log('out:',?astring.generate(ast))
結(jié)果如下

如果想要代碼更加健壯,可以使用 recast[7],提供了對 ast 的各種操作
//?用螺絲刀解析機器
const?ast?=?recast.parse(code);
//?ast可以處理很巨大的代碼文件
//?但我們現(xiàn)在只需要代碼塊的第一個body,即add函數(shù)
const?add??=?ast.program.body[0]
console.log(add)
//?引入變量聲明,變量符號,函數(shù)聲明三種“模具”
const?{variableDeclaration,?variableDeclarator,?functionExpression}?=?recast.types.builders
//?將準備好的組件置入模具,并組裝回原來的ast對象。
ast.program.body[0]?=?variableDeclaration("const",?[
??variableDeclarator(add.id,?functionExpression(
????null,?//?Anonymize?the?function?expression.
????add.params,
????add.body
??))
]);
//將AST對象重新轉(zhuǎn)回可以閱讀的代碼
const?output?=?recast.print(ast).code;
console.log(output)
這里只是示例代碼,展示 recast 的一些操作,最好的情況還是能遍歷節(jié)點自動替換。
這樣我們就完成了將普通函數(shù)轉(zhuǎn)換成箭頭函數(shù)的操作,但 ast 的作用不止于此,作為一個前端在工作中可能涉及 ast 的地方,就是自定義 eslint 、 stylelint 等插件,下面我們就趁熱打鐵,分別實現(xiàn)。
實現(xiàn)一個 ESlint 插件
介紹
ESlint 使用 Espree (基于 acron) 解析 js 代碼,利用 AST 分析代碼中的模式,且完全插件化。
ESlint 配置
工作中我們最常接觸的就是 eslint 的配置,我們寫的插件也需要從這里配置從而生效
//?.eslintrc.js
moudule.export?=?{
????extends:?['eslint:recommend'],
????parser:?'@typescript-eslint/parser',?//?解析器,
????plugins:?['plugin1'],?//?插件
????rules:?{
????????semi:?['error',?'alwayls'],
????????quotes:?['error',?'double'],
????????'plugin1/rule1':?'error',
????},
????processor:?'',?//?特定文件中使用?eslint?檢測
}
parser,默認使用 espree[8],對 acorn[9] 的一層封裝,將 js 代碼轉(zhuǎn)化為抽象語法樹 AST。
import?*?as?espree?from?"espree";
const?ast?=?espree.parse(code);
經(jīng)常使用的還有 @typescript-eslint/parser[10] ,這里可以拓展 ts 的 lint;
開發(fā)一個 eslint 插件
準備
eslint 官方也有個介紹,如何給 eslint 做貢獻 https://eslint.org/docs/developer-guide/contributing/
安裝 yeoman 并初始化環(huán)境,yeoman 就是一個腳手架,方便創(chuàng)建 eslint 的插件和 rule
?npm?install?-g?yo?generator-eslint
創(chuàng)建一個插件文件夾并進入
創(chuàng)建 plugin
yo?eslint:plugin

最重要的是 ID,這樣插件發(fā)布之后,會以 eslint-plugin-[id] 的形式發(fā)布到 npm 上,不可以使用特殊字符。
創(chuàng)建 rule 規(guī)則
yo?eslint:rule

這里的 id 會生成 eslint-plugin-[id] 插件唯一標識符
生成的文件列表為

然后就可以實現(xiàn)插件了
這時候我們可以回頭看一下剛剛生成的文件
rules/cpf-plugin.js
可以參考
https://cn.eslint.org/docs/developer-guide/working-with-rules
/**
?*?@fileoverview?cpf?better
?*?@author?tsutomu
?*/
"use?strict";
//------------------------------------------------------------------------------
//?Rule?Definition
//------------------------------------------------------------------------------
/**
?*?@type?{import('eslint').Rule.RuleModule}
?*/
module.exports?=?{
??meta:?{?//?這條規(guī)則的元數(shù)據(jù),
????type:?null,?//?類別?`problem`,?`suggestion`,?or?`layout`
????docs:?{?//?文檔
??????description:?"cpf?better",
??????category:?"Fill?me?in",
??????recommended:?false,
??????url:?null,?//?URL?to?the?documentation?page?for?this?rule
????},
????fixable:?null,?//?Or?`code`?or?`whitespace`
????schema:?[],?//?重點,?eslint?可以通過識別參數(shù)從而避免無效的規(guī)則配置?Add?a?schema?if?the?rule?has?options
??},
??create(context)?{
????//?variables?should?be?defined?here
????//----------------------------------------------------------------------
????//?Helpers
????//----------------------------------------------------------------------
????//?any?helper?functions?should?go?here?or?else?delete?this?section
????//----------------------------------------------------------------------
????//?Public
????//----------------------------------------------------------------------
????return?{
??????//?visitor?functions?for?different?types?of?nodes
????};
??},
};
Eslint 的插件需要根據(jù)它規(guī)定的特定規(guī)則進行編寫
Meta 中比較重要的是 schema,主要是設(shè)置入?yún)?,我們來看一?shcema 的規(guī)則 https://eslint.org/docs/developer-guide/working-with-rules#options-schemas
JSONSchema 定義 https://json-schema.org/understanding-json-schema/
大致有兩種形式,enum 和 object
schema:?[
????{
????????"enum":?["always",?"never"]
????},
????{
????????"type":?"object",
????????"properties":?{?//?這里的意思就是可以有個叫?exceptRange?的參數(shù),值為布爾類型
????????????"exceptRange":?{
????????????????"type":?"boolean"
????????????}
????????},
????????"additionalProperties":?false
????}
]
下面看下 create,返回了一個對象,需要在其中編寫遇到對應(yīng)節(jié)點所需要執(zhí)行的方法, context 則提供了一些方便的方法,包括 context.report上報錯誤和context.getSourceCode獲取源代碼。
????create(context:?RuleContext):?RuleListener;
????
????interface?RuleContext?{
????????id:?string;
????????options:?any[];
????????settings:?{?[name:?string]:?any?};
????????parserPath:?string;
????????parserOptions:?Linter.ParserOptions;
????????parserServices:?SourceCode.ParserServices;
????????getAncestors():?ESTree.Node[];
????????getDeclaredVariables(node:?ESTree.Node):?Scope.Variable[];
????????getFilename():?string;
????????getPhysicalFilename():?string;
????????getCwd():?string;
????????getScope():?Scope.Scope;
????????getSourceCode():?SourceCode;
????????markVariableAsUsed(name:?string):?boolean;
????????report(descriptor:?ReportDescriptor):?void;
????}
no-console 插件源碼解析
寫自己的插件之前,不妨看下官方的插件源碼,也更方便理解里面的各種概念。
/**
?*?@fileoverview?Rule?to?flag?use?of?console?object
?*?@author?Nicholas?C.?Zakas
?*/
"use?strict";
//------------------------------------------------------------------------------
//?Requirements
//------------------------------------------------------------------------------
const?astUtils?=?require("./utils/ast-utils");
//------------------------------------------------------------------------------
//?Rule?Definition
//------------------------------------------------------------------------------
/**?@type?{import('../shared/types').Rule}?*/
module.exports?=?{
????meta:?{
????????type:?"suggestion",
????????docs:?{
????????????description:?"disallow?the?use?of?`console`",
????????????recommended:?false,
????????????url:?"https://eslint.org/docs/rules/no-console"
????????},
????????schema:?[
????????????{
????????????????type:?"object",
????????????????properties:?{
????????????????????allow:?{
????????????????????????type:?"array",
????????????????????????items:?{
????????????????????????????type:?"string"
????????????????????????},
????????????????????????minItems:?1,
????????????????????????uniqueItems:?true
????????????????????}
????????????????},
????????????????additionalProperties:?false
????????????}
????????],
????????messages:?{
????????????unexpected:?"Unexpected?console?statement."
????????}
????},
????create(context)?{
????????const?options?=?context.options[0]?||?{};
????????const?allowed?=?options.allow?||?[];
????????/**
?????????*?Checks?whether?the?given?reference?is?'console'?or?not.
?????????*?@param?{eslint-scope.Reference}?reference?The?reference?to?check.
?????????*?@returns?{boolean}?`true`?if?the?reference?is?'console'.
?????????*/
????????function?isConsole(reference)?{
????????????const?id?=?reference.identifier;
????????????return?id?&&?id.name?===?"console";
????????}
????????/**
?????????*?Checks?whether?the?property?name?of?the?given?MemberExpression?node
?????????*?is?allowed?by?options?or?not.
?????????*?@param?{ASTNode}?node?The?MemberExpression?node?to?check.
?????????*?@returns?{boolean}?`true`?if?the?property?name?of?the?node?is?allowed.
?????????*/
????????function?isAllowed(node)?{
????????????const?propertyName?=?astUtils.getStaticPropertyName(node);
????????????return?propertyName?&&?allowed.indexOf(propertyName)?!==?-1;
????????}
????????/**
?????????*?Checks?whether?the?given?reference?is?a?member?access?which?is?not
?????????*?allowed?by?options?or?not.
?????????*?@param?{eslint-scope.Reference}?reference?The?reference?to?check.
?????????*?@returns?{boolean}?`true`?if?the?reference?is?a?member?access?which
?????????*??????is?not?allowed?by?options.
?????????*/
????????function?isMemberAccessExceptAllowed(reference)?{
????????????const?node?=?reference.identifier;
????????????const?parent?=?node.parent;
????????????return?(
????????????????parent.type?===?"MemberExpression"?&&
????????????????parent.object?===?node?&&
????????????????!isAllowed(parent)
????????????);
????????}
????????/**
?????????*?Reports?the?given?reference?as?a?violation.
?????????*?@param?{eslint-scope.Reference}?reference?The?reference?to?report.
?????????*?@returns?{void}
?????????*/
????????function?report(reference)?{
????????????const?node?=?reference.identifier.parent;
????????????context.report({
????????????????node,
????????????????loc:?node.loc,
????????????????messageId:?"unexpected"
????????????});
????????}
????????return?{
????????????"Program:exit"()?{
????????????????const?scope?=?context.getScope();?//?獲取當前作用域,及全局作用域
????????????????const?consoleVar?=?astUtils.getVariableByName(scope,?"console");?//?向上遍歷,查找
????????????????const?shadowed?=?consoleVar?&&?consoleVar.defs.length?>?0;?//?這里是判斷別名
????????????????/*
?????????????????*?'scope.through'?includes?all?references?to?undefined
?????????????????*?variables.?If?the?variable?'console'?is?not?defined,?it?uses
?????????????????*?'scope.through'.
?????????????????*/
????????????????//?如果?console?是未定義的,那么他就在?scope.through?中
????????????????const?references?=?consoleVar
??????????????????????consoleVar.references
????????????????????:?scope.through.filter(isConsole);
????????????????if?(!shadowed)?{
????????????????????references
????????????????????????.filter(isMemberAccessExceptAllowed)
????????????????????????.forEach(report);
????????????????}
????????????}
????????};
????}
};
對照看一下console.log 的ast ,在最上面
Scope 作用域定義
????interface?Scope?{
????????type:
????????????|?"block"
????????????|?"catch"
????????????|?"class"
????????????|?"for"
????????????|?"function"
????????????|?"function-expression-name"
????????????|?"global"?//?及?Program
????????????|?"module"
????????????|?"switch"
????????????|?"with"
????????????|?"TDZ";
????????isStrict:?boolean;
????????upper:?Scope?|?null;?//?父級作用域
????????childScopes:?Scope[];?//?子級作用域
????????variableScope:?Scope;
????????block:?ESTree.Node;
????????variables:?Variable[];?//?變量
????????set:?Map<string,?Variable>;?//?變量?set?便于快速查找
????????references:?Reference[];?//??此范圍所有引用的數(shù)組
????????through:?Reference[];?//?由未定義的變量組成的數(shù)組
????????functionExpressionScope:?boolean;
????}
Scope 相關(guān)的源碼可以參考 https://github.com/estools/escope,scope 可視化可以看這里 http://mazurov.github.io/escope-demo/
這里的 through 就是當前作用域無法解析的變量,比如
function?a()?{
?????????function?b()?{
????????????let?c?=?d;
????}
}
這里面明顯是 d 無法解析,那么

可以看到,在全局作用域的 through 中可以找到這個 d。
自動修復(fù)
可以再 report 中調(diào)用 fix 相關(guān)的函數(shù)來進行修復(fù),下面是 fix 的
interface?RuleFixer?{
????insertTextAfter(nodeOrToken:?ESTree.Node?|?AST.Token,?text:?string):?Fix;
????insertTextAfterRange(range:?AST.Range,?text:?string):?Fix;
????insertTextBefore(nodeOrToken:?ESTree.Node?|?AST.Token,?text:?string):?Fix;
????insertTextBeforeRange(range:?AST.Range,?text:?string):?Fix;
????remove(nodeOrToken:?ESTree.Node?|?AST.Token):?Fix;
????removeRange(range:?AST.Range):?Fix;
????replaceText(nodeOrToken:?ESTree.Node?|?AST.Token,?text:?string):?Fix;
????replaceTextRange(range:?AST.Range,?text:?string):?Fix;
}
interface?Fix?{
????range:?AST.Range;
????text:?string;
}
用法為
report(context,?message,?type,?{
????node,
????loc,
????fix:?(fixer)?{
????????return?fixer.inserTextAfter(token,??string);
????}
})
可以看下 eqeqeq 的寫法,這是一個禁用 == != 并且修復(fù)為=== !==的規(guī)則
return?{
????BinaryExpression(node)?{
????????const?isNull?=?isNullCheck(node);
????????if?(node.operator?!==?"=="?&&?node.operator?!==?"!=")?{
????????????if?(enforceInverseRuleForNull?&&?isNull)?{
????????????????report(node,?node.operator.slice(0,?-1));
????????????}
????????????return;
????????}
????????if?(config?===?"smart"?&&?(isTypeOfBinary(node)?||
????????????????areLiteralsAndSameType(node)?||?isNull))?{
????????????return;
????????}
????????if?(!enforceRuleForNull?&&?isNull)?{
????????????return;
????????}
????????report(node,?`${node.operator}=`);
????}
};
修復(fù)的代碼在 report 中
function?report(node,?expectedOperator)?{
????const?operatorToken?=?sourceCode.getFirstTokenBetween(
????????node.left,
????????node.right,
????????token?=>?token.value?===?node.operator
????);
????context.report({
????????node,
????????loc:?operatorToken.loc,
????????messageId:?"unexpected",
????????data:?{?expectedOperator,?actualOperator:?node.operator?},
????????fix(fixer)?{
????????????//?If?the?comparison?is?a?`typeof`?comparison?or?both?sides?are?literals?with?the?same?type,?then?it's?safe?to?fix.
????????????if?(isTypeOfBinary(node)?||?areLiteralsAndSameType(node))?{
????????????????return?fixer.replaceText(operatorToken,?expectedOperator);
????????????}
????????????return?null;
????????}
????});
}
實現(xiàn) no-getNodeRef
實現(xiàn)一個禁用 getNodeRef 的插件
當我們在內(nèi)部使用的跨端框架中使用下面的配置之后,將不再支持 getNodeRef 屬性,取而代之的是使用 createSelectorQuery
??compilerNGOptions:?{
????removeComponentElement:?true,
??},
首先我們先看一下 getNodeRef 的用法
class?A?extends?C?{
????componmentDidMount()?{
????????this.getNodeRef('');
????}
}
在上面的可視化網(wǎng)站中可以看到下面的

那么我們就可以很暴力的寫出 rule ,如下
return?{
??//?visitor?functions?for?different?types?of?nodes
??CallExpression:?(node)?=>?{
????if(node.callee.property?&&?node.callee.property.name?===?'getNodeRef'?&&?node.callee.object.type?===?'ThisExpression')?{
??????context.report({
????????node,
????????message:?'禁用?getNodeRef'
??????})
????}
??}
};
測試的代碼
const?ruleTester?=?new?RuleTester();
ruleTester.run("no-getnoderef",?rule,?{
??valid:?[
????//?give?me?some?code?that?won't?trigger?a?warning
????{
??????code:?'function?getNodeRef()?{};?getNodeRef();'
????}
??],
??invalid:?[{
????code:?"?this.getNodeRef('');",
????errors:?[{
??????message:?"禁用?getNodeRef",
??????type:?"CallExpression"
????}],
??},?],
});
測試的結(jié)果

實現(xiàn) care-about-scroll
并沒有什么實際的用處,僅僅是因為我們使用的框架中的 scroll event 有 bug,android 和 IOS 端參數(shù)有問題,安卓的 e.detail.scrollHeight 對應(yīng) ios 的 e.detail.scrollTop,再次說明,這是個框架的 bug,在這里使用僅僅為了演示 eslint 編寫插件的一些能力。
我們的預(yù)期目標是在同一個函數(shù)中,如果使用了上述一個屬性和沒有使用另一個屬性,則出現(xiàn)提示。
代碼為
return?{
??//?visitor?functions?for?different?types?of?nodes
??"Identifier":?(node)?=>?{
????if((node.name?===?'scrollHeight'?||?node.name?===?'scrollTop')?&&?node.parent?&&?node.parent.object.property.name?===?'detail')?{
??????const?block?=?findUpperNode(node,?'BlockStatement');
??????if(block)?{
????????let?checked?=?false;
????????walkNode(block,?(_node)?=>?{
??????????if(_node.type?===?'Identifier'?&&?_node.name?===?IDENTIFIERS[node.name])?{
????????????checked?=?true;
????????????return?true;
??????????}
??????????return?false;
????????});
????????if(!checked)?{
??????????context.report({node,?message:?`缺少?${IDENTIFIERS[node.name]}`})
????????}
??????}
????}
??}
};
測試代碼如下
ruleTester.run("care-about-scroll",?rule,?{
??valid:?[
????//?give?me?some?code?that?won't?trigger?a?warning
????{
??????code:?"function?handleScroll(e)?{?var?a?=?e.detail.scrollTop;?var?b?=?e.detail.scrollHeight;?}"
????}
??],
??invalid:?[{
????code:?"function?handleScroll(e)?{?var?a?=?e.detail.scrollTop;?}",
????errors:?[{
??????message:?"缺少?scrollHeight",
??????type:?"Identifier"
????}],
??},?],
});
發(fā)布插件
登錄之后直接發(fā)布即可
npm?publish

使用插件
首先按照剛剛發(fā)布的插件
npm?i?eslint-plugin-cpf?-D
在 eslintrc.js 中新增配置
moudule.exports?=?{
????plugins:?['cpfabc'],
????rules:?{
????????'cpfabc/no-getnoderef':?'error',
????????'cpfabc/care-about-scroll':?'error',
????}
}
效果如下


更改代碼后正常

實現(xiàn)一個 Stylelint 插件
介紹
Stylelint 插件和 eslint 插件的區(qū)別主要是
解釋器,postcss 入口,這里可以使用本地文件開發(fā) Ast,因為 css 本身就有結(jié)構(gòu),這里更像 dom 樹,每個節(jié)點有 type 和 nodes(子節(jié)點),甚至并沒有對 less 之類的代碼進行轉(zhuǎn)換。也因此 stylelint 的插件寫起來更像直接對字符串進行處理,不會體現(xiàn) ast 的作用。
但是整體的思想都是一樣的,css 的節(jié)點類型也少很多,可以參考https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md
Root: 根節(jié)點,指代當前 css 文件AtRule: @開頭的一些屬性,如 @mediaRule: 常用的 css 選擇器Declaration: 鍵值對,如 color: redComment: 注釋
實現(xiàn) cpf-style-plugin/max-depth-2
內(nèi)部跨端框架 的 ttss 最多支持兩層的 css 組合選擇器,即下面是可行的
div?div?{}
而下面是不行的
div?div?div?{}
而對于 less
.a?{
????&-b?{
????????&-c?{
????????}
????}
}
/////
.a-b-c?{}
其實只有一層,所以我們的代碼需要注意這點
首先建立一個文件叫 cpf-style-plugin.js
const?stylelint?=?require('stylelint');
const?{?ruleMessages,?report?}?=?stylelint.utils;
const?ruleName?=?'cpf-style-plugin/max-depth-2';
const?messages?=?ruleMessages(ruleName,?{
??//…?linting?messages?can?be?specified?here
??expected:?'不允許三層',
??test:?(...any)?=>?`${JSON.stringify(any)}xxx`,
});
module.exports.ruleName?=?ruleName;
module.exports.messages?=?messages;
module.exports?=?stylelint.createPlugin(ruleName,?function?ruleFunction()?{
??return?function?lint(postcssRoot,?postcssResult)?{
????function?helperDep(n,?dep)?{
??????if?(n.nodes)?{
????????n.nodes.forEach((newNode)?=>?{
??????????if?(newNode.type?===?'rule')?{
????????????const?selectorNum?=?newNode.selector
??????????????.split('?')
??????????????.reduce((p,?c)?=>?p?+?(/^[a-zA-z.#].*/.test(c)???1?:?0),?0);
????????????if?(dep?+?selectorNum?>?2)?{
??????????????report({
????????????????message:?messages.expected,
????????????????node:?newNode,
????????????????result:?postcssResult,
??????????????});
????????????}
????????????helperDep(newNode,?dep?+?selectorNum);
??????????}
????????});
??????}
????}
????helperDep(postcssRoot,?0);
??};
});
這里有區(qū)別的是,eslint 都是對標準語法樹進行操作,而這里的 css 樹,準確來說應(yīng)該是 less 的 ast 樹,并不會先轉(zhuǎn)成 css 再進行我們的 lint 操作,因此我們需要考慮 rule 節(jié)點可能以 & 開頭,也導(dǎo)致寫法上有一點別扭。
使用插件
使用的話只需要更改 stylelintrc.js 即可
module.exports?=?{
????snippet:?['less'],
????extends:?"stylelint-config-standard",
????plugins:?['./cpf-style-plugin.js'],
????rules:?{
????????'color-function-notation':?'legacy',
????????'cpf-style-plugin/max-depth-2':?true,
????}
}
看一下效果

實現(xiàn)一個 React Live Code
你可能會覺得 live code 和 ast 有啥關(guān)系,只不過是放入 new Function 即可,但是形如 import export 等功能,利用字符串匹配實現(xiàn)是不太穩(wěn)定的,我們可以利用 AST 來實現(xiàn)這些方法,這里為了簡潔,最后一行表示 export default ,思想是一樣的,利用 AST 查找到我們需要的參數(shù)即可。
https://codesandbox.io/s/react-live-editor-3j7t2?file=/src/index.js

其中上半部分為編輯器,下半部分為事實的效果,我們的工作是分析最后一行的組件并展示出來。
其中編輯器的部分負責(zé)代碼的樣式,使用的是 react-simple-code-editor[11],主要的用法如下
????value={code}
????onValueChange={code?=>?{xxx}}
/>
所以主要的工作在獲取編輯器代碼之后的工作
首先我們需要將 JSX 代碼轉(zhuǎn)換為 es5 代碼,這里用到 @babel/standalone[12],這是一個環(huán)境使用的 babel 插件,可以這么使用
import?{?transform?as?babelTransform?}?from?"@babel/standalone";
const?tcode?=?babelTransform(code,?{?presets:?["es2015",?"react"]?}).code;
然后我們需要獲取最后一行代碼 并將其轉(zhuǎn)化為,其實也就是找到React.createElement(Greet)這個,這里就可以使用 ast 進行查找。過程略過,我們得到了這個節(jié)點rnode,最后將這個rnode轉(zhuǎn)換為React.createElement,我們最終得到了這樣的代碼
code?=?"'use?strict';
var?_x?=?_interopRequireDefault(require('x'));
function?_interopRequireDefault(obj)?{
????return?obj?&&?obj.__esModule???obj?:?{?default:?obj?};
}
function?Greet()?{
????return?React.createElement('span',?null,?'Hello?World!');
}
render(React.createElement(Greet,?null));"
將上述的代碼塞入 new Function 中執(zhí)行。
const?renderFunc?=?return?new?Function("React",?"render",?"require",?code);
最后執(zhí)行上述的代碼
import?React?from?"react";
function?render(node)?{
????ReactDOM.render(node,?domElement);
}
function?require(moduleName)?{
????//?自定義
}
renderFunc(React,?render,?require)
參考
https://github.com/jamiebuilds/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js
https://segmentfault.com/a/1190000016231512
https://juejin.cn/post/6844903450287800327
https://medium.com/swlh/writing-your-first-custom-stylelint-rule-a9620bb2fb73
https://juejin.cn/post/7054008042764894222
參考資料
estree: https://github.com/estree/estree
[2]acorn: https://github.com/acornjs/acorn
[3]Esprima: https://github.com/jquery/esprima
[4]Shift: https://github.com/shapesecurity/shift-parser-js
[5]測試地址: https://esprima.org/test/compare.html
[6]astring: https://www.npmjs.com/package/astring
[7]recast: https://www.npmjs.com/package/recast
[8]espree: https://github.com/eslint/espree
[9]acorn: https://github.com/acornjs/acorn
[10]@typescript-eslint/parser: https://typescript-eslint.io/docs/linting/
[11]react-simple-code-editor: https://www.npmjs.com/package/react-simple-code-editor
[12]@babel/standalone: https://babeljs.io/docs/en/babel-standalone
???謝謝支持
以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^
喜歡的話別忘了?分享、點贊、收藏?三連哦~。
歡迎關(guān)注公眾號?趣談前端?收獲大廠一手好文章~
LowCode可視化低代碼社區(qū)介紹
LowCode低代碼社區(qū)(http://lowcode.dooring.cn)是由在一線互聯(lián)網(wǎng)公司深耕技術(shù)多年的技術(shù)專家創(chuàng)辦,意在為企業(yè)技術(shù)人員提供低代碼可視化相關(guān)的技術(shù)交流和分享,并且鼓勵國內(nèi)擁有相關(guān)業(yè)務(wù)的企業(yè)積極推薦自身產(chǎn)品,為國內(nèi)B端技術(shù)領(lǐng)域積累知識資產(chǎn)。同時我們還歡迎開源大牛們分享自己的開源項目和技術(shù)視頻。
如需入駐請加下方小編微信:?lowcode-dooring

