使用 TypeScript 编写代码分析器(从头开始)

2025-06-10

使用 TypeScript 编写代码分析器(从头开始)

Exercism是一个在线平台,旨在帮助您通过实践和指导提高编码技能。

Exercism为您提供了涵盖众多语言轨道的数千个练习。一旦您开始学习某个语言轨道,就会看到一组核心练习需要您完成。每个练习都是一个充满趣味的挑战,旨在帮助您更深入地了解该语言的特性。

在撰写本文时,我是JavaScriptTypeScript轨道的维护者,最近我们一直致力于实现部分体验的自动化。

本文将介绍如何使用ESTree兼容工具进行 AST 解析和遍历。本文将重点介绍JavaScriptTypeScript代码中最常见的某些 token 类型。

它教您如何自己探索这些树,并参考代码示例和实际生产实现。

阅读本文时,请思考一下你自己的 JavaScript 和 TypeScript 代码。一旦你理解了浏览器(以及类似的工具eslint)如何解析你的代码,你或许就能更好地理解这门语言是如何定义和构建的。

🚧 所有GitHub链接均为master链接,这意味着从撰写本文到您点击它们,内容可能会发生变化。但是,为了确保代码示例有意义,分析器代码库链接指向的是特定的提交 ( 9ff332b)。这意味着您看到的代码可能与当前使用的代码不同。

目录

这张名为“感受自由”的照片发生在英国邓杰内斯,照片中一辆红色的大众桑巴停在棕色房屋附近。

📝 练习

在本文中,我将为练习编写一个分析器gigasecond,适用于 TypeScript 和 JavaScript 轨道。描述只有两行:

给定一个时刻,确定一千兆秒过去之后的时刻。

一千兆秒等于10^9(1,000,000,000) 秒。

规范数据提示了我需要编写的代码,但幸运的是,练习在JavaScriptTypeScript轨道中都实现了。

JavaScript 实现

JavaScript 实现要求我们编写一个名为的导出,gigasecond该导出返回Date比输入晚一千兆秒的时间Date

  // Test case from the JavaScript track
  test('tells a gigasecond anniversary since midnight', () => {
    const gs = gigasecond(new Date(Date.UTC(2015, 8, 14)));
    const expectedDate = new Date(Date.UTC(2047, 4, 23, 1, 46, 40));
    expect(gs).toEqual(expectedDate);
  });
Enter fullscreen mode Exit fullscreen mode

TypeScript 实现

TypeScript 实现要求我们编写一个默认导出,Gigasecond该导出是一个类,该类具有一个date()函数,该函数返回一个Date比构造函数晚一千兆秒的函数Date

  // Test case from the TypeScript track
  it('tells a gigasecond anniversary since midnight', () => {
    const gs = new Gigasecond(new Date(Date.UTC(2015, 8, 14)))
    const expectedDate = new Date(Date.UTC(2047, 4, 23, 1, 46, 40))
    expect(gs.date()).toEqual(expectedDate)
  })
Enter fullscreen mode Exit fullscreen mode

💯 最佳解决方案

在着手为这两种实现编写分析器之前,我必须先确定最佳解决方案。如果我知道预期的代码结果,我就可以尝试识别它并从那里开始工作。

JavaScript 解决方案

JavaScript 中的实现很简单。它使用Date构造函数Date#getTime和一个常量来生成适当的结果。

const GIGASECOND_IN_MS = (10 ** 9) * 1000

export function gigasecond(date) {
  return new Date(date.getTime() + GIGASECOND_IN_MS)
}
Enter fullscreen mode Exit fullscreen mode

必须注意这里的特殊性:

  • 最佳方法是将GIGASECOND_IN_MS值提取为顶级常量
  • 常数的值 ( (10 ** 9) * 1000) 可以以多种同样有效的形式最优地写出。然而,写出数字本身却被认为是一种错误。以下所有形式都应该被认为是最优的:
    • 10 ** 12
    • 1e9 * 1e3
    • 1e12
    • Math.pow(10, 9) * 1000
    • Math.pow(10, 12)
  • Date#valueOf并非最佳选择。它被标记为“此方法通常由 JavaScript 内部调用而不是在代码中显式调用。”,尽管它在功能上是等效的。
  • 最后,Date.parse(date)它不是一个好的选择,因为它应该只处理字符串。它返回的值与getTime给定日期时相同,是因为该日期被强制转换为字符串,然后进行解析。

TypeScript 解决方案

TypeScript 实现需要一个classas default export,并有一个 method date()。该算法与 JavaScript 解决方案完全相同,但需要类型注释。

const GIGASECOND_IN_MS = (10 ** 9) * 1000

export default class Gigasecond {

  private readonly futureDate: Readonly<Date>

  constructor(date: Readonly<Date>) {
    this.futureDate = Object.freeze(new Date(date.getTime() + GIGASECOND_IN_MS))
  }

  date(): Readonly<Date> {
    return this.futureDate
  }
}
Enter fullscreen mode Exit fullscreen mode

除了前面描述的 JavaScript 变体和规则外,计算可以在constructor(如上所示)函数中进行date。在函数中,计算过程如下:

const GIGASECOND_IN_MS = (10 ** 9) * 1000

export default class Gigasecond {

  constructor(private readonly date: Date) { }

  date(): Date {
    return new Date(date.getTime() + GIGASECOND_IN_MS)
  }
}
Enter fullscreen mode Exit fullscreen mode

👩🏽‍💻分析代码

现在是时候实际编写分析器了。我们将首先关注 JavaScript 实现。由于目前已有 JavaScript 分析器在运行,并且这些分析器是开源的,因此本示例将使用javascript-analyzer存储库中的实用程序和基类。

💬 抽象语法树

JavaScript 分析器将处理代码解决方案的抽象语法树 (AST) 。虽然还有其他方法可以编写分析器,但就本文而言,我们主要讨论 AST 解析。

yarn add @typescript-eslint/parser @typescript-eslint/typescript-estree
Enter fullscreen mode Exit fullscreen mode

TypeScript ESLint 团队构建了一个很棒的解析器,它输出一个ESTree —— 一种指定格式的 token 和输入代码的信息。它可以兼容JavaScriptTypeScript,因此非常适合我们的用例。我更喜欢使用这种类型的树,因为它的规范允许与其他工具互操作。

处理parsereslint 配置,然后调用使用 TypeScript 编译代码并将结果转换为与ESTreetypescript-estree匹配的包。您可以前往AST Explorer亲自尝试,方法是将上面的示例代码粘贴到输入字段并选择。⚠注意:此处的解析器版本通常与最新的解析器不同步。@typescript-eslint/parser

你可能会想:为什么不使用TypeScript 编译器 API 呢?它内置了 TypeScript 语言的 AST 解析功能。它还公开了许多辅助函数(例如isIdentifer)。

原因是输出结果并非ESTree 规范格式,这意味着您将无法使用其他工具来处理它。实际上,TypeScript-ESTree 包实际上在底层使用了编译器然后将其转换为规范,这意味着没有锁定。您不会受到编译器更改的约束。

🏃🏽‍💨运行解析器

现在包已经到位,让我们来解析输入代码。

import { parse, TSESTreeOptions } from "@typescript-eslint/typescript-estree";

const options: TSESTreeOptions = {
  comment: false,
  jsx: false
}

const program = parse(source, options)
// => Program({ body: [...], sourceType: "module", tokens: [...] })
Enter fullscreen mode Exit fullscreen mode

这给了我们和你在AST Explorer中看到的相同的输出,在根目录下有 aProgram和 its body。我们不需要其他字段,但这tokens很有趣。它列出了在构建树时从源头解析的标记。

💎 你可以在parsers/AstParser.ts

🔎 寻找主入口点

我们正在寻找一个名为 的函数gigasecond。我们知道以下内容:

  • 必须编辑export
  • 它的名字必须gigasecond

输入代码声明了一个函数,就像上面的最优解一样,因此树中保存了一个,FunctionDeclaration其中Identifier

export function gigasecond(date) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode
{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "gigasecond"
  },
  generator: false,
  expression: false,
  async: false,
  params: [ ... ],
  // The body is a block { ... }
  body: {
    type: "BlockStatement",
    body: [ ... ]
  }
}
Enter fullscreen mode Exit fullscreen mode

这是它可以搜索的内容。在 AST 中搜索的最常见方法是遍历该树。从某个节点(通常是根/程序)开始,然后访问每个项目。

我们知道我们的解析和兼容EStree,就像可以识别(和转换)代码一样。eslinteslintprettier

# Not a dev dependency!
yarn add eslint
Enter fullscreen mode Exit fullscreen mode
import { TSESTree } from "@typescript-eslint/typescript-estree"
import { traverse } from 'eslint/lib/util/traverser'

traverse(program, {
  enter(node: TSESTree.Node) {
    // ...
  }
})
Enter fullscreen mode Exit fullscreen mode

编写这段代码时,TypeScript 会报错说这个库没有类型,不幸的是,在编写时仍然如此。不过,你可以复制declarations.d.ts我写的这段代码来获得类型补全。

该方法将在程序中的每个节点enter上调用。在代码块内部,“TraverserContext” 暴露了两个方法:enter

  • this.skip():跳过该节点的进一步遍历,这意味着它不会访问当前节点的任何其他键(以及子键);
  • this.break():完全停止遍历。

现在找到入口点就很简单了。

import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/typescript-estree"

let entry: TSESTree.FunctionDeclaration | undefined = undefined

traverse(program, {
  enter(node: TSESTree.Node) {
    switch (node.type) {

      // function name() {}
      case AST_NODE_TYPES.FunctionDeclaration:
        if (node.id && node.id.name === 'gigasecond') {
          entry = node
          this.break()
        }
        break;
    }
  }
})

entry
// => FunctionDeclaration({
//      id: { type: "Identifier", name: "gigasecond" }, ...
//    })
Enter fullscreen mode Exit fullscreen mode

不幸的是,上面的遍历器FunctionDeclaration在等效代码中使用了ArrowFunctionExpression时失败了FunctionExpression。稍后会详细介绍。

💎 你可以在analyzers/utils/extract_main_method.ts

🔎 查找顶级常量

代码找到了两个组件中的第一个。现在还需要找到第二个。它是一个顶级组件const,但名称未知。

const GIGASECOND_IN_MS = (10 ** 9) * 1000
Enter fullscreen mode Exit fullscreen mode
{
  type: "VariableDeclaration",
  declarations: [
    {
      type: "VariableDeclarator",
      id: {
        type: "Identifier",
        name: "GIGASECOND_IN_MS"
      },
      init: {
        type: "BinaryExpression",
        operator: "*",
        // Left is another BinaryExpression with **
        left: { ... },
        // Right is a Literal
        right: { ... }
      }
    }
  ],
  kind: "const"
}
Enter fullscreen mode Exit fullscreen mode

这里没有什么特别有用的。考虑到它需要接受的数据种类繁多,我不能依赖init它的特定类型。它的名称也不固定,因为它没有被export编辑,因此没有经过测试。

然而,这里有几个限制会有所帮助:

  • 它必须是顶级常量
  • 无法命名gigasecond
  • 在最优解中,实际上只有一个不是的顶级常数entry
type FoundConst = { kind: TSESTree.VariableDeclaration['kind'] }
  & TSESTree.VariableDeclarator

let bigNumber: FoundConst | undefined = undefined

traverse(program, {
  enter(node: TSESTree.Node) {
    switch (node.type) {

      // const NAME = ...
      case AST_NODE_TYPES.VariableDeclaration:
        const declaration = node.declarations.find(
          (declaration) => declaration.id && declaration.id.name !== 'gigasecond')
        )

        if (declaration) {
          bigNumber = { kind: node.kind, ...declaration }
          this.break()
        }

        break;

      default:
        // This doesn't declare a variable, so skip the node
        this.skip()
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

稍后,我可以检查bigNumber['kind']并确保它是const,或者附加一条const我喜欢的消息。

💎 你可以在analyzers/utils/find_top_level_constants.ts

算法

现在我找到了entry要点,我可以弄清楚参数的名称是什么了(date)。因为我还知道顶层常量,所以我知道常量的名称是什么GIGASECOND_IN_MS

new Date(...)
Enter fullscreen mode Exit fullscreen mode

这里没什么特别的。它是一个new表达式,以表达式作为第一个参数。

{
  type: "NewExpression",
  callee: {
    type: "Identifier",
    name: "Date"
  },
  arguments: [ ... ]
}
Enter fullscreen mode Exit fullscreen mode
let newDate: TSESTree.NewExpression | undefined = undefined

traverse(program, {
  enter(node: TSESTree.Node) {
    switch (node.type) {

      // new Date(...)
      case AST_NODE_TYPES.NewExpression:
        if (
          node.callee.type === AST_NODE_TYPES.Identifier
          && node.callee.name === 'Date'
        ) {
          newDate = node;
          this.break()
        }
        break;

      default:
        // This doesn't declare a variable, so skip the node
        this.skip()
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

💎 你可以在analyzers/utils/find_new_expression.ts

内部表达式的类型为BinaryExpression。在 EStree 兼容输出中,带有两个部分的 和 运算符(例如+-*)是二元表达式,而带有一个部分的 和 运算符(例如~!)是一元表达式。

date.getTime() + GIGASECOND_IN_MS
Enter fullscreen mode Exit fullscreen mode
{
  type: "BinaryExpression",
  operator: "+",
  left: {
    type: "CallExpression",
    callee: {
      type: "MemberExpression",
      object: {
        type: "Identifier",
        name: "date"
      },
      property: {
        type: "Identifier",
        name: "getTime"
      }
    },
    arguments: []
  },
  right: {
    type: "Identifier",
    name: "GIGASECOND_IN_MS"
  }
}
Enter fullscreen mode Exit fullscreen mode

很多东西我们已经见过了,还有一些新类型。让我们来看看。

对象的属性

当解析器遇到对象属性访问器(object.property)时,它会被解析为MemberExpression。根据写法的不同,属性可能是IdentifierLiteral

date.getTime
// ^       ^
// object  property
// |       |
// |       identifier (name = getTime)
// identifier (name = date)

date['getTime']
// ^       ^
// object  property
// |       |
// |       literal (value = getTime)
// identifier (name = date)
Enter fullscreen mode Exit fullscreen mode

“执行”属性

如果 后面有括号MemberExpression,则整个表达式将被解析为 的子表达式。标识符CallExpression后面的括号也是如此

date.getTime ( )
// ---------| ^ |
// ^        | argument(s) of call expression
// member expression
//              |
// -------------|
// call expression

gigasecond(INPUT)
// ------|   ^   |
// ^     | argument of call expression
// identifier    |
//               |
// --------------|
// call expression
Enter fullscreen mode Exit fullscreen mode

匹配标识符

我需要查找并匹配源代码提供的两个标识符:

  • 千兆秒第一个参数(用于arg.getTime()
  • 顶级常量(用于time + CONSTANT

const argumentName = entry.id.name
// => "gigasecond"
const constantName = bigNumber.id.name
// => "GIGASECOND_IN_MS"

let optimalExpression: boolean = false

// NOTE: passing in the newDate as root, so this is a subtree traversal!
traverse(newDate, {
  enter(node: TSESTree.Node) {
    switch (node.type) {

      // new Date(x.z() + y)
      case AST_NODE_TYPES.BinaryExpression:
        this.break()

        if (node.operator !== '+') {
          optimalExpression = false;
          return;
        }


        // This allows the order to be reversed
        const leftType = node.left.type
        const constSide = leftType === AST_NODE_TYPES.Identifier
          ? node.left
          : node.right
        const expressionSide = leftType === AST_NODE_TYPES.CallExpression
          ? node.left
          : node.right

        if (constSide === expressionSide) {
          // throw new Error("not optimal! this is not x.z() + y")
          optimalExpression = false
          return
        }

        if (constSide.id.name !== constantName) {
          optimalExpression = false
          return
        }

        const { object, property } = expressionSide.callee
        optimalExpression =
          object.type === AST_NODE_TYPES.Identifier
          && object.name === argumentName
          && ((
            property.type === AST_NODE_TYPES.Identifier
            && property.name === 'getTime'
          ) || (
            property.type === AST_NODE_TYPES.Literal
            && property.value === 'getTime'
          ))

      break;
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

💎 您可以在以下位置找到它:

✅ 自动指导

当所有这些部分组合在一起时,它就是千兆秒级的分析仪。还有一些事情需要检查:

  • 是否bigNumber.kind等于"const"?如果不等于,请添加注释
  • 使用其中一个推导式是否值得GIGASECOND_IN_MS?如果没有,请添加注释。
  • 只有一个参数吗gigasecond?确保它不是一个...splat参数,并且没有value = "default"
  • gigasecond实际上是导出的吗?是export内联的吗?如果不是,请添加注释。

由于第一个(kind相等性检查)已经提到,而第二个与调用内部new Date(...)表达式非常相似,因此我省略了如何实现它们。如果您需要一些灵感,可以查看千兆秒分析器源代码。第三个是测试entryfor parameters

至于exports,这些由 处理💎 extract_export,但我会向您展示它的要点。

📦 测试导出

导出JavaScript基本上TypeScript有三种类型。使用核心语言功能(即使用关键字)的导出是最简单的:

export const inline = {}
export default class InlineClass {}
export default defaultSpecifier
export { specifier }
export { specifier as renamed }
Enter fullscreen mode Exit fullscreen mode

它们default export有自己的令牌类型ExportDefaultDeclaration

{
  type: "ExportDefaultDeclaration",
  declaration: {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

没有default修饰符的属于 类型ExportNamedDeclaration

{
  type: "ExportNamedDeclaration",
  declaration: {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

属性declaration是稍微有点棘手的地方。内联export语句,无论是否是默认的,后面都会跟着相同的标记类型,就像它们没有export关键字一样,类似于用括号将表达式括起来的方式CallExpression

内联导出

这意味着第一个例子是VariableDeclaration带有单个的VariableDeclaractoridIdentifier带有的name = "inline"init是带有的ObjectExpression。同样,第二个例子是ClassDeclaration带有的,作为id带有Identifiername = "InlineClass"

说明符导出

第三个具有declaration类型。这Identifier与exportsname = "defaultSpecifier"类似inline

然而,第四个和第五个却没有属性declaration相反,它们有一个specifiers属性,在本例中,只有一个项目:

{
  type: "ExportSpecifier",
  local: {
    type: "Identifier",
    name: "specifier"
  }
  exported: {
    type: "Identifier",
    name: "specifier" // or "renamed"
  }
}
Enter fullscreen mode Exit fullscreen mode

使用local属性来确定导出什么(内部名称是什么)以及exported属性如何导入(导出名称是什么)。

CommonJS 导出

最后,有些导出不使用关键字,而是使用(就我而言) defunct module.exports

module.exports = singleExport
module.exports = { specifier }
module.exports.default = defaultExport
module.exports.renamed = specifier
Enter fullscreen mode Exit fullscreen mode

由于这些不使用关键字,因此它们被解释为ExpressionStatements,因为它们是AssignmentExpressions。以下是重要属性和表示的简要概述表:

表达 类型 支柱 价值
module.exports.renamed = specifier AssignmentExpression
operator "="
Identifier right "specifier"
MemberExpression left 🔽 module.exports.renamed 🔽
module.exports.renamed MemberExpression
Identifier property "renamed"
MemberExpression object 🔽 module.exports 🔽
module.exports MemberExpression
Identifier property "exports"
Identifier object "module"

还有使用 的变体object['accessor'],其中accessor不是 而是IdentifierLiteral但除此之外都是一样的。

🔀 测试变体

如前所述,在 JavaScript 和 TypeScript 中有很多方法可以编写函数。分析器的源代码💎 extract_main_method中有一个实用方法。它可以检测以下变体:

function name() {}
// FunctionDeclaration

const name = () => {}
// ArrowFunctionExpression

const name = function() {}
// FunctionExpression

export default {
  name: () => {}
}
// ExportDefaultDeclaration + ObjectExpression + (Arrow)FunctionExpression
Enter fullscreen mode Exit fullscreen mode

以及 TypeScript 特定的变体(但它们适用于两者)

class Foo {
  name() {}
}
// MethodDefinition + FunctionExpression

class Foo {
  static name = () => {}
  static name = function name() {}
}
// ClassProperty + (Arrow)FunctionExpression
Enter fullscreen mode Exit fullscreen mode

遍历 TypeScript 树

正如你所注意到的,到目前为止,我们所做的只是检查 JavaScript 代码,并展示了它是如何被解析和遍历的。为了使该解决方案能够解析 TypeScript 代码,我们需要对遍历器进行一些修改,并测试一些额外的属性。

🔑 访客钥匙

traverser遍历树时,它会根据一组称为 的键来决定“遍历🚶🏽‍”哪些节点visitor keys。由于TypeScript超集JavaScript,因此它具有相同的键,甚至更多。

import { visitorKeys } from "@typescript-eslint/parser/dist/visitor-keys"

traverse(root, {
  enter(node: Node) {
    // ...
  },

  visitorKeys
})
Enter fullscreen mode Exit fullscreen mode

如果您查看该文件的源代码,您会发现它实际上导入了eslint访问者键(以便访问所有 J​​avaScript 键)并添加了特定的 TypeScript 键。

📖 类型注释

这些很有趣。

class Gigasecond {
  constructor(private readonly date: Date) { }
}
Enter fullscreen mode Exit fullscreen mode
{
  type: "ClassDeclaration",
  id: {
    type: "Identifier",
    name: "Gigasecond"
  },
  // everything inside the class { body }
  body: {
    type: "ClassBody",
    body: [
      {
        // a constructor is a regular method definition...
        type: "MethodDefinition",
        key: {
          type: "Identifier",
          name: "constructor"
        },
        value: {
          type: "FunctionExpression",
          params: [{ /*...*/ }],
          generator: false,
          expression: false,
          async: false,
          body: { /*...*/ }
        }
        computed: false,
        static: false,  // (typescript static keyword)
        // ... but with a special kind
        kind: 'constructor'
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

上述内容与 JavaScript 等效内容相比没有特殊属性,但这是因为源代码中除了 之外没有params类型注释constructor

{
  type: "Identifier",
  name: "date",
  // : ...
  typeAnnotation: {
    type: "TSTypeAnnotation",
    typeAnnotation: {
      // Readonly
      type: "TSTypeReference",
      typeName: {
        type: "Identifier"
        name: "Readonly"
      },
      // <...>
      typeParameters: {
        type: "TSTypeParameterInstantiation",
        // Each type between the < brackets >
        params: [
          {
            type: "TSTypeReference",
            typeName: {
              type: "Identifier",
              name: "Date"
            }
          }
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

一些关键的观察结果:

  • 类型注释有自己的访问者键typeAnnotation
  • 所有 TypeScript 节点都以 开头TS
  • 泛型类型只是TSTypeReference同时具有一个typeName和一个或多个的typeParameters
  • 剥离类型几乎和删除typeAnnotation键一样简单,这几乎与babelpreset-typescript所做的完全一样

类属性

在 TypeScript 中,你可以使用诸如private和 之类的关键字来注释类属性readonly。此外,它们还可以具有类型(Date在本例中为 )。

class Gigasecond {
  private readonly futureDate: Date
}
Enter fullscreen mode Exit fullscreen mode
{
  type: "ClassProperty",
  key: {
    type: "Identifier",
    name: "futureDate"
  },
  computed: false,
  static: false,            // static keyword
  readonly: true,           // readonly keyword
  accessibility: "private", // private, protected, public keywords
  typeAnnotation: {
    type: "TSTypeAnnotation",
    typeAnnotation: {
      type: "TSTypeReference",
      typeName: {
        type: "Identifier",
        name: "Date"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript 关键字private和直接readonly修饰ClassProperty,但类型又在 上typeAnnotation。如果源代码中省略了类型注释(即:隐式any),则typeAnnotationAST 中不存在该键。

↩ 返回类型

我们现在要讨论的最后一种类型是函数return类型。大多数其他类型注解只是此类型以及之前提到的类型的变体。

class Gigasecond {
  date(): Date {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  type: "MethodDefinition",
  key: {
    type: "Identifier",
    name: "date"
  },
  value: {
    type: "FunctionExpression",
    generator: false,
    expression: false,
    async: false,
    body: { /*...*/ },
    params: [],
    returnType: {
      type: "TSTypeAnnotation",
      typeAnnotation: {
        type: "TSTypeReference",
        typeName: {
          type: "Identifier",
          name: "Date"
        }
      }
    }
  },
  computed: false,
  static: false, // static keyword
  kind: "method"
}
Enter fullscreen mode Exit fullscreen mode

你可能已经注意到了,typeAnnotation不在MethodDefinition这是因为类上的方法定义实际上是将函数表达式绑定(): Date { ... }到标识符 上date

在 上,FunctionExpression您可以找到之前未遇到过的类型注释。其结构与returnType相同typeAnnotationClassProperty

结论

将代码解释为抽象语法树并寻找某些属性需要大量的树遍历;因为某些 AST 解析器的输出格式有规范,所以您可以自己编写工具。

gigasecond如果学生提供的是与最优解完全相同的变体,则本文内容(格式不同)将用于自动批准练习。即使学生提供的不是最优解,分析器的结果也有足够的空间提供有意义的评论。

荷兰鹿特丹伊拉斯谟桥的照片,展示了建筑物附近的灰色混凝土桥。

参考

分析器参考

锻炼存储库

套餐

鏂囩珷鏉ユ簮锛�https://dev.to/xpbytes/writing-a-code-analyzer-in-typescript-5ec3
PREV
图像优化、处理和托管
NEXT
逐步解释字符串匹配正则表达式