TypeScript AST (抽象语法树) 结合 Angular Schematics 的应用

汤汤Tang.png

AST 简单来说就是将 typescript 语法解析为一个一个的节点,在节点信息中包括了该节点的 position, text, kind 等信息,更详细的介绍大家可以查看 AST,在这里也给大家推荐一个查看解析后的 AST 结构的网站,直接将 ts 代码粘贴到其中观察其结构能够更好的帮助你的理解。

ast.png

如果是第一次点进来的同学也可以先看看这篇文章了解一下什么是 Angular Schematics.

为了让大家有更好的理解,这篇文章将会结合其在 Schematics 中插入代码的方式来进行介绍。

1 读取 ts 文件

在使用 AST 来分析 ts 文件之前,我们需要先读取 ts 文件将其转为一个 ts.Node 类型的数组

// 在这里我们借助 Schematics 中 tree 来读取
import { Tree } from '@angular-devkit/schematics';
import * as ts from 'typescript';

// 在这里实现两个函数,将文件读取为 ts.SourceFile
function readIntoSourceFile(tree: Tree, filePath: string): ts.SourceFile {
  // 读取文件的方法使用 `fs` 也可,由于该文章主要是与 Angular Schematics 相结合,所以使用的 tree 读取
  const text = tree.read(filePath);
  if (text === null) {
    console.log('File does not exist.');
    return;
  }

  const sourceText = text.toString('utf-8');
  return ts.createSourceFile(
    filePath,
    sourceText,
    ts.ScriptTarget.Latest,
    true
  );
}

function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
  const nodes: ts.Node[] = [sourceFile];
  const result = [];

  while (nodes.length > 0) {
    const node = nodes.shift();

    if (node) {
      result.push(node);
      if (node.getChildCount(sourceFile) >= 0) {
        nodes.unshift(...node.getChildren());
      }
    }
  }

  return result;
}
复制代码

接下来我们打印一下 readIntoSourceFilegetSourceNodes 得到的结果看一下。

ts-nodes.png

在打印的结果中可以看到我们之前提到的相关属性:

  • position: posend
  • kind: kind

打印的结果并不是非常的直观,还是推荐大家将自己的代码拷贝到前面提到过的 Typescript AST Viewer 中查看。

2 通过 AST 在特定位置插入代码

一样的我们还是先看代码,假设我们要在如下的 ts 文件中插入代码在 people 数组中。

const people = ['XiaoHua', 'XiaoLi', 'LiHua'];
复制代码

首先我们先实现下面的方法来进行对数组的插入

// 由于这里主要结合了 Angular Schematics,所以会有一些特定的用法,但是核心代码与框架无关
/**
 * path: ts 文件路径
 * insertText: 插入的内容,对应我们的例子即为一个数组的元素如 'ZhangSan'
 * target: 插入的目标,对应我们的即为数组的名字 people
 * insertPosition: 插入的位置,对应我们的例子即为在数组的开头或者结尾插入
 */
function insertArray(
  tree: Tree,
  path: string,
  insertText: string,
  target: string,
  insertPosition: 'start' | 'end'
): Tree {
  // 读取成 ts 源文件的方法使用 `fs` 也可
  const sourceFile = readIntoSourceFile(tree, path);
  const nodes = getSourceNodes(sourceFile);

  let arrayNodeSiblings = null;
  let expressionNode = null;

  // 根据传入的数组名字 `people` 来定位到位置
  const arrayNode = nodes.find(
    (n) =>
      (n.kind === ts.SyntaxKind.Identifier ||
        n.kind === ts.SyntaxKind.PropertyAccessExpression) &&
      n.getText() === targetText
  );

  if (!arrayNode || !arrayNode.parent) {
    console.log('Get Component Node Filed.');
    return;
  }

  arrayNodeSiblings = arrayNode.parent.getChildren();

  // 找到我们需要插入的数组元素的位置
  let arrayNodeIndex = arrayNodeSiblings.indexOf(arrayNode);

  arrayNodeSiblings = arrayNodeSiblings.slice(arrayNodeIndex);

  expressionNode = arrayNodeSiblings.find(
    (n) => n.kind === ts.SyntaxKind.ArrayLiteralExpression
  );

  if (!expressionNode) {
    console.log('The target node is not defined');
    return;
  }

  // 根据 SyntaxKind 为 SyntaxList 的节点定位到 people 数组后 [] 的位置
  const listNode = expressionNode
    .getChildren()
    .find((n) => n.kind === ts.SyntaxKind.SyntaxList);

  if (!listNode) {
    console.log(`${targetText} The target node is not defined`);
    return;
  }
  /* 到此我们寻找指定数组的位置的过程已经结束了,目前我们需要插入代码的位置信息已经得到了,接下来我们将新的元素插入到数组中 */

  // 在这里我们可以指定在数组的开始或者结尾插入代码
  const changePosition =
    insertPosition === 'start' ? listNode.getStart() : listNode.getEnd();

  const change = new InsertChange(path, changePosition, insertText);

  const declarationRecorder = tree.beginUpdate(path);
  if (change instanceof InsertChange) {
    declarationRecorder.insertRight(change.pos, change.toAdd);
  }
  tree.commitUpdate(declarationRecorder);

  return tree;
}
复制代码

以上已经完成了核心的方法,我们再总结一下,首先将 ts 文件读取为 ts 源文件形式,之后再获取到代码的所有节点,然后根据节点(node)的类型(SyntaxKind)定位到我们需要插入的任何元素,不仅限于数组,也包括 object, constructor, Function 等等都是能够找到具体位置的。

在上述方法中大家也注意到了 SyntaxKind,大家可以在 node_modulestypescript 里查看到更多详细信息。

3 更多的用法

这里我们以一段简单的 Angular 组件代码为例做简单的介绍,除了插入代码外,我们还能够通过 AST 来获取文件中我们想要获取的内容,大家可以自行尝试,会有更深的理解。

import { Component, Input } from '@angular/core';

@Component({
  selector: 'selector',
  template: `<div></div>`
})
export class AppComponent {
  @Input() input1: string;
  @Input() input2: number;
  @Input() input3: boolean = true;

  constructor() {}
}
复制代码

在通过将改文件解析为 AST 之后,我们可以读取到每一个 Input 的名字,类型,默认值来生成我们的 api 文档。

function getComponentAttribute(tree: Tree, path: string) {
  const sourceFile = readIntoSourceFile(tree, filePath);
  const nodes: ts.Node[] = getSourceNodes(sourceFile);
  const propertyNodes: ts.Node[] = [];
  nodes.forEach((n) => {
    if (n.kind === ts.SyntaxKind.Decorator && n.getText() === '@Input()') {
      propertyNodes.push(n.parent);
    }
  });
  // 之后就可以使用 propertyNodes,再根据 SyntaxKind 来提取你想要的内容了
  ......
}
复制代码

4 总结

本文给大家提供了一种通过 AST 来解析 ts 文件的方法,也给大家提供了一种修改 ts 文件的方法,使用 AST 能够准确的定位到位置。

利用 AST 几乎可以帮我们做一切的操作对一个 ts 文件。这里也推荐大家可以了解一下 Angular Schematics (原理图),其也是借用了 AST 来进行很多文件的操作。了解之后可以定制更加契合你的 cli。

更多的你甚至可以实现在 vscode 中打开一段 ts 代码一样,推断出一个变量的类型。总而言之,除了本文中提到的 插入代码信息提取,还有很多是可以通过分析 AST 来实现的。

Supongo que te gusta

Origin juejin.im/post/7062532492535791624
Recomendado
Clasificación