Skip to content

7.acorn实现插入节点

万万没想到,acorn在新增Block或者是Line注释节点的类型中,表现的如此拉胯。acorn.parse默认只解析javascript语法,默认不解析注释节点,如果需要获取源文件中的注释节点,还需要用回调函数onComment来获取全部的注释节点。

javascript
import acorn from 'acorn'
import walk from 'acorn-walk'
import escodegen from 'escodegen'
import fs from 'fs-extra'

const workflow = async () => {
  const source = await fs.readFile('./demo.js')
  
  const ast = acorn.parse(source, { sourceType: 'module', ecmaVersion: 2021 })
  
  const code = escodegen.generate(ast, { 
    format: {
      semicolons: false,
    },
    comment: true
  })
}

这一段代码,其实什么都没有做,只是把源代码用解析成了ast,然后又用escodegen转换为源代码,最后你会发现,我源代码中的注释呢????

所以,第一步就是要先把源文件中的注释复原,下面的代码就是acornBlock节点的解析数据结构,startend都是我从loc中结构出来的。

javascript
Node: {
    type: 'Block',
    value: '*\n * @NOTE before\n ',
    start: Position { line: 1, column: 0 },
    end: Position { line: 3, column: 3 },
    loc: SourceLocation { start: [Position], end: [Position] }
  },

下面是VariableDeclaration节点的数据结构:

javascript
Node {
  type: 'VariableDeclaration',
  start: 272,
  end: 308,
  loc: SourceLocation {
    start: Position { line: 24, column: 0 },
    end: Position { line: 24, column: 36 }
  },
  declarations: [
    Node {
      type: 'VariableDeclarator',
      start: 276,
      end: 308,
      loc: [SourceLocation],
      id: [Node],
      init: [Node]
    }
  ],
  kind: 'var'
}

我本来是打算用startend做查询的条件,找出comments数组中的出现的位置最近那一个,就必然是节点的注释。但是后来试了一下,发现用loc.start.line行号是最佳的选择。

对于还原注释类型的节点代码处理,有一个小操作。我们用行号去找,需要对比的就是注释节点的结束行号,在正常的VariableDeclaration 的起始行号之前,如果满足需求了,就对解析出来的注释节点comments进行shift操作。

javascript
  walk.simple(ast, {
    VariableDeclaration (node) {
      const { id } = node.declarations[0]
      const commentNode = {
        type: "Block",
        value: ['\n', `* this is comment ${id.name}`, `* ${new Date().toLocaleDateString()} \n`].join('\n'),
        loc: node.loc
      }
      const commentFilters = comments.filter(c => (c.loc.end.line) <= node.loc.start.line)
      let tail = null

      if (commentFilters.length) {
        tail = comments.shift()
      }

      node.leadingComments = [].concat(tail).filter(Boolean)
      node.leadingComments = [].concat(node.leadingComments, commentNode).filter(Boolean)
      node.trailingComments = [].concat(node.trailingComments, commentNode).filter(Boolean)
    }
  })

在上面的代码中,可以用const commentFilters = comments.filter(c => (c.loc.end.line) <= node.loc.start.line) 找出comments中最靠近当前遍历节点的节点集合,一旦集合里面有值了,就说明当前节点是最靠近注释节点的那个节点,就可以使用shift操作取出第一位节点。

为什么可以这么做?

注释节点的生成,其实是按照节点在源代码中出现的位置具体生成的,它本来就是有顺序的一个集合。所以我们找最靠近注释节点的正常节点,完全可以使用这个方法,而不用担心它会出现乱序。

在解决还原源代码中的注释节点之后,我们的需要实现的需求,实质是加新的注释节点。这里与babel一样,可以用leadingCommentstrailingComments来实现对目标节点添加前置注释和后置注释。

如何将AST重新转换为源代码?

在acorn内部没有提供方法,但是它的官方提供了另外一个escodegen库来做这个事情,其实质我猜想应该与babel差不了太多。与babel完全相同的地方大致就是它们最终生成的代码都是带句尾分号的,所以也可以采取同样的操作,用prettier来改变代码风格,满足自己的需求。

源代码地址: https://github.com/stack-wuh/mini-cli/blob/main/core/create-node-babel/acorn.js

参考文档:

  1. escodegen: https://github.com/estools/escodegen/wiki/API
  2. acorn: https://github.com/acornjs/acorn/tree/master/acorn

MIT License.