AST实践之遍历

969 阅读2分钟

AST

AST(Abstract Syntax Tree)是根据一套规范对源码进行结构描述,如操作Js源码的ESTree标准。

例如 const name = "lyla";const描述为

interface VariableDeclaration {
    kind: "const";
}

Acorn

Acorn 一个JS parser,将Js源码解析为Ast。

  1. 基本使用

    const ast = acorn.parse(code, {ecmaVersion: 2020});
    
  2. 通过plugin的方式解析jsx语法acorn-jsx、以及其他插件如acorn-stage3acorn-bigint等。

    const extendAcorn = acorn.Parser.extend(
       require('acorn-stage3'),
       require('acorn-jsx')(),
    )
    const ast = extendAcorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module' });
    

    问题:经测试未支持装饰器Decorators Unexpected character '@'

  3. 遍历 AST nodes

  • acorn-walk
    import * as acornWalk from 'acorn-walk';
    
    acornWalk.full(ast, visit)
    
    function visit(node) {
        switch(node.type) {
            case 'JSXText':
            case 'Literal': {
                const { start, end, value } = node;
                // ...
                break;
            }
            case 'TemplateLiteral': {
                const { start, end } = node;
                const templateContent = code.slice(start, end);
                // ...
                break;
            }
            
        }
    }
    

问题:当使用acorn-jsx插件时,这里会遇到 baseVisitor[type] is not a function,不支持JSXElement等type,可以自定义扩展类型做第三个参数解决: acornWalk.full(ast, visit, { ...acornWalk.base, JSXElement: () => {} })

  • acorn-types

    提供了针对AST类型系统的各类方法,其中包含AST节点遍历机制

    import * as astTypes from 'ast-types';
    
    astTypes.visit(ast, {
        visitJSXText(path) {
            const { node: { start, end, value } } = path;
            // ...
            this.traverse(path);  // 继续向下遍历子节点
        },
        visitTemplateLiteral(path) {
            const { node: { start, end } } = path;
            const templateContent = code.slice(start, end);
            // ...
            this.traverse(path);  // 继续向下遍历子节点
        }
    })
    

总结:Acorn对于解析js有完整的方案,只是对一些ES语法还不支持(仅支持TC39 stage4提案,其他可通过plugin的方式支持)。

Babel

Babel是一个JS Compiler,将ES6+的语法转换为向后兼容的版本,支持JSX语法的转换。

例如:input ?? "Hello world" 会转换为 input != null ? input : "Hello world"

如何做到的?其实是通过操作AST实现: input string -> @babel/parser parser -> AST -> transformer[s] -> AST -> @babel/generator -> output string

  1. 解析为AST

    可以使用@babel/core的transformSync方法,也可以直接使用@babel/parser

  • @babel/core
    import * as babel from '@babel/core';
    
    const { ast } = babel.transformSync(code, {
        ast: true,
        presets: ['@babel/preset-react'],
        plugins: [['@babel/plugin-proposal-decorators', { version: '2021-12' }]],
    })
    
  • @babel/parse
    import * as babelParser from '@babel/parser';
    
    const ast = babelParser.parse(code, { sourceType: "module", plugins: ['jsx'] })
    
  1. 遍历 AST nodes

    使用@babel/traverse遍历AST nodes,结合@babel/types检查AST nodes类型。

    import * as babelTraverse from '@babel/traverse';
    import * as babelTypes from '@babel/types';
    
    babelTraverse.default(ast, {
     StringLiteral({ node }) {
       const { start, end, value } = node as babelTypes.StringLiteral;
       // ...
     },
     TemplateLiteral({ node }) {
       const { start, end } = node as babelTypes.TemplateLiteral;
       const templateContent = code.slice(start, end);
       // ...
     },
     JSXElement({ node }) {
       const { children } = node as babelTypes.JSXElement;
       children.forEach(child => {
         if (babelTypes.isJSXText(child)) {
           const { value, start, end } = child;
           // ...
         }
       })
     }
    })
    
参考
  1. github.com/estree/estr…
  2. github.com/acornjs/aco…
  3. github.com/benjamn/ast…
  4. github.com/babel/babel…
  5. astexplorer.net/
  6. github.com/tc39/propos…