前端大概要的知道 AST

認識 AST

定義: 在計算機科學中,抽象語法樹是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。之所以說語法是 “抽象” 的,是因爲這裏的語法並不會表示出真實語法中出現的每個細節。

從定義中我們只需要知道一件事就行,那就是 AST 是一種樹形結構,並且是某種代碼的一種抽象表示。

在線可視化網站:https://astexplorer.net/ ,利用這個網站我們可以很清晰的看到各種語言的 AST 結構。

estree[1]

estree 就是 es 語法對應的標準 AST,作爲一個前端也比較方便理解。我們以官方文檔爲例

https://github.com/estree/estree/blob/master/es5.md

  1. 下面看一個代碼
console.log('1')

AST 爲

{
  "type""Program",
  "start": 0, // 起始位置
  "end": 16, // 結束位置,字符長度
  "body"[
    {
      "type""ExpressionStatement", // 表達式語句
      "start": 0,
      "end": 16,
      "expression"{
        "type""CallExpression", // 函數方法調用式
        "start": 0,
        "end": 16,
        "callee"{
          "type""MemberExpression", // 成員表達式 console.log
          "start": 0,
          "end": 11,
          "object"{
            "type""Identifier", // 標識符,可以是表達式或者結構模式
            "start": 0,
            "end": 7,
            "name""console"
          },
          "property"{
            "type""Identifier", 
            "start": 8,
            "end": 11,
            "name""log"
          },
          "computed": false, // 成員表達式的計算結果,如果爲 true 則是 console[log]false 則爲 console.log
          "optional"false
        },
        "arguments"[ // 參數
          {
            "type""Literal", // 文字標記,可以是表達式
            "start": 12,
            "end": 15,
            "value""1",
            "raw""'1'"
          }
        ],
        "optional"false
      }
    }
  ],
  "sourceType""module"
}
  1. 看兩個稍微複雜的代碼
const b = { a: 1 };
const { a } = b;
function add(a, b) {
    return a + b;
}

這裏建議讀者自己將上述代碼複製進上面提到的網站中,自行理解 estree 的各種節點類型。當然了,我們也不可能看一篇文章就記住那麼多類型,只要心裏有個大致的概念即可。

認識 acorn[2]

由 JavaScript 編寫的 JavaScript 解析器,類似的解析器還有很多,比如 Esprima[3] 和 Shift[4] ,關於他們的性能,Esprima 的官網給了個測試地址 [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
  }

獲得 ast 之後我們想還原之前的函數怎麼辦,這裏可以使用 astring[6]

import * as astring from 'astring';

const code = astring.generate(ast);

實現普通函數轉換爲箭頭函數

接下來我們就可以利用 AST 來實現一些字符串匹配不太容易實現的操作,比如將普通函數轉化爲箭頭函數。

我們先來看兩個函數的 AST 有什麼區別

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"
}

找到區別之後我們就可以有大致的思路

  1. 找到 FunctionDeclaration

  2. 將其替換爲VariableDeclaration VariableDeclarator 節點

  3. VariableDeclarator 節點的 init 屬性下新建 ArrowFunctionExpression 節點

  4. 並將 FunctionDeclaration 節點的相關屬性替換到 ArrowFunctionExpression 上即可

但是由於 acorn 處理的 ast 只是單純的對象,並不具備類似 dom 節點之類的對節點的操作能力,如果需要操作節點,需要寫很多工具函數, 所以我這裏就簡單寫一下。

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))

結果如下

如果想要代碼更加健壯,可以使用 recast[7],提供了對 ast 的各種操作

// 用螺絲刀解析機器
const ast = recast.parse(code);

// ast可以處理很巨大的代碼文件
// 但我們現在只需要代碼塊的第一個body,即add函數
const add  = ast.program.body[0]

console.log(add)

// 引入變量聲明,變量符號,函數聲明三種“模具”
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對象重新轉回可以閱讀的代碼
const output = recast.print(ast).code;

console.log(output)

這裏只是示例代碼,展示 recast 的一些操作,最好的情況還是能遍歷節點自動替換。

這樣我們就完成了將普通函數轉換成箭頭函數的操作,但 ast 的作用不止於此,作爲一個前端在工作中可能涉及 ast 的地方,就是自定義 eslint 、 stylelint 等插件,下面我們就趁熱打鐵,分別實現。

實現一個 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 代碼轉化爲抽象語法樹 AST。

import * as espree from "espree";

const ast = espree.parse(code);

經常使用的還有 @typescript-eslint/parser[10] , 這裏可以拓展 ts 的 lint;

開發一個 eslint 插件

準備

eslint 官方也有個介紹,如何給 eslint 做貢獻 https://eslint.org/docs/developer-guide/contributing/

  1. 安裝 yeoman 並初始化環境,yeoman 就是一個腳手架,方便創建 eslint 的插件和 rule
 npm install -g yo generator-eslint

創建一個插件文件夾並進入

創建 plugin

yo eslint:plugin

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

創建 rule 規則

yo eslint:rule

這裏的 id 會生成 eslint-plugin-[id] 插件唯一標識符

生成的文件列表爲

然後就可以實現插件了

這時候我們可以回頭看一下剛剛生成的文件

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: { // 這條規則的元數據,
    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 可以通過識別參數從而避免無效的規則配置 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 的插件需要根據它規定的特定規則進行編寫

JSONSchema 定義 https://json-schema.org/understanding-json-schema/

大致有兩種形式,enum 和 object

schema: [
    {
        "enum"["always""never"]
    },
    {
        "type""object",
        "properties"{ // 這裏的意思就是可以有個叫 exceptRange 的參數,值爲布爾類型
            "exceptRange"{
                "type""boolean"
            }
        },
        "additionalProperties"false
    }
]
    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 ,在最上面

    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[]; //  此範圍所有引用的數組
        through: Reference[]; // 由未定義的變量組成的數組
        functionExpressionScope: boolean;
    }

Scope 相關的源碼可以參考 https://github.com/estools/escope,scope 可視化可以看這裏 http://mazurov.github.io/escope-demo/

這裏的 through 就是當前作用域無法解析的變量,比如

function a() {
         function b() {
            let c = d;
    }
}

這裏面明顯是 d 無法解析,那麼

可以看到,在全局作用域的 through 中可以找到這個 d。

自動修復

可以再 report 中調用 fix 相關的函數來進行修復,下面是 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 的寫法,這是一個禁用 == != 並且修復爲=== !==的規則

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}=`);
    }
};

修復的代碼在 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;
        }
    });
}

實現 no-getNodeRef

  1. 實現一個禁用 getNodeRef 的插件

當我們在內部使用的跨端框架中使用下面的配置之後,將不再支持 getNodeRef 屬性,取而代之的是使用 createSelectorQuery

  compilerNGOptions: {
    removeComponentElement: true,
  },

首先我們先看一下 getNodeRef 的用法

class A extends C {
    componmentDidMount() {
        this.getNodeRef('');
    }
}

在上面的可視化網站中可以看到下面的

那麼我們就可以很暴力的寫出 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"
    }],
  }, ],
});

測試的結果

實現 care-about-scroll

並沒有什麼實際的用處,僅僅是因爲我們使用的框架中的 scroll event 有 bug,android 和 IOS 端參數有問題,安卓的 e.detail.scrollHeight 對應 ios 的 e.detail.scrollTop,再次說明,這是個框架的 bug,在這裏使用僅僅爲了演示 eslint 編寫插件的一些能力。

我們的預期目標是在同一個函數中,如果使用了上述一個屬性和沒有使用另一個屬性,則出現提示。

代碼爲

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"
    }],
  }],
});

發佈插件

登錄之後直接發佈即可

npm publish

使用插件

首先按照剛剛發佈的插件

npm i eslint-plugin-cpf -D

在 eslintrc.js 中新增配置

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

效果如下

更改代碼後正常

實現一個 Stylelint 插件

介紹

Stylelint 插件和 eslint 插件的區別主要是

但是整體的思想都是一樣的,css 的節點類型也少很多,可以參考 https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md

實現 cpf-style-plugin/max-depth-2

內部跨端框架 的 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);
  };
});

這裏有區別的是,eslint 都是對標準語法樹進行操作,而這裏的 css 樹,準確來說應該是 less 的 ast 樹,並不會先轉成 css 再進行我們的 lint 操作,因此我們需要考慮 rule 節點可能以 & 開頭,也導致寫法上有一點彆扭。

使用插件

使用的話只需要更改 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,
    }
}

看一下效果

實現一個 React Live Code

你可能會覺得 live code 和 ast 有啥關係,只不過是放入 new Function 即可,但是形如 import export 等功能,利用字符串匹配實現是不太穩定的,我們可以利用 AST 來實現這些方法,這裏爲了簡潔,最後一行表示 export default ,思想是一樣的,利用 AST 查找到我們需要的參數即可。

https://codesandbox.io/s/react-live-editor-3j7t2?file=/src/index.js

其中上半部分爲編輯器,下半部分爲事實的效果,我們的工作是分析最後一行的組件並展示出來。

其中編輯器的部分負責代碼的樣式,使用的是 react-simple-code-editor[11],主要的用法如下

<Editor 
    value={code}
    onValueChange={code ={xxx}}
/>

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

  1. 首先我們需要將 JSX 代碼轉換爲 es5 代碼,這裏用到 @babel/standalone[12],這是一個環境使用的 babel 插件,可以這麼使用
import { transform as babelTransform } from "@babel/standalone";

const tcode = babelTransform(code, { presets: ["es2015""react"] }).code;
  1. 然後我們需要獲取最後一行代碼 <Greet /> 並將其轉化爲,其實也就是找到 React.createElement(Greet) 這個,這裏就可以使用 ast 進行查找。過程略過,我們得到了這個節點 rnode,最後將這個rnode 轉換爲 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 中執行。
const renderFunc = return new Function("React""render""require", code);
  1. 最後執行上述的代碼
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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zd8x5Om2psKfrZLfL9Og-A