<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端高級進階必備的 AST知識和實戰(zhàn)

          共 26493字,需瀏覽 53分鐘

           ·

          2022-04-30 02:00

          術(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

          1. 下面看一個代碼
          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"
          }
          1. 看兩個稍微復(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ū)別之后我們就可以有大致的思路

          1. 找到 FunctionDeclaration
          1. 將其替換為VariableDeclaration VariableDeclarator 節(jié)點
          1. VariableDeclarator 節(jié)點的 init 屬性下新建 ArrowFunctionExpression 節(jié)點
          1. 并將 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/

          1. 安裝 yeoman 并初始化環(huán)境,yeoman 就是一個腳手架,方便創(chuàng)建 eslint 的插件和 rule
          ?npm?install?-g?yo?generator-eslint

          創(chuàng)建一個插件文件夾并進入

          創(chuàng)建 plugin

          yo?eslint:plugin
          image.png

          最重要的是 ID,這樣插件發(fā)布之后,會以 eslint-plugin-[id] 的形式發(fā)布到 npm 上,不可以使用特殊字符。

          創(chuàng)建 rule 規(guī)則

          yo?eslint:rule
          image.png

          這里的 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

          1. 實現(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é)果

          image.png

          實現(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
          image.png

          使用插件

          首先按照剛剛發(fā)布的插件

          npm?i?eslint-plugin-cpf?-D

          在 eslintrc.js 中新增配置

          moudule.exports?=?{
          ????plugins:?['cpfabc'],
          ????rules:?{
          ????????'cpfabc/no-getnoderef':?'error',
          ????????'cpfabc/care-about-scroll':?'error',
          ????}
          }

          效果如下

          image.png
          image.png

          更改代碼后正常

          image.png

          實現(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: @開頭的一些屬性,如 @media
          • Rule: 常用的 css 選擇器
          • Declaration: 鍵值對,如 color: red
          • Comment: 注釋

          實現(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}}
          />

          所以主要的工作在獲取編輯器代碼之后的工作

          1. 首先我們需要將 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;
          1. 然后我們需要獲取最后一行代碼 并將其轉(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));"

          1. 將上述的代碼塞入 new Function 中執(zhí)行。
          const?renderFunc?=?return?new?Function("React",?"render",?"require",?code);
          1. 最后執(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

          參考資料

          [1]

          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

          - END -

          ???

          便內(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

          瀏覽 98
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码专区一区二区三区 | 亚洲日韩高清在线 | 91丨国产丨熟女 熟女 | www.97.色色 | 一级黄色视频免费看 |