为什么 JS 源映射通常处于令牌粒度?

Why are JS sourcemaps typically at token granularity?

JavaScripts source maps 似乎通常不比 token 粒度更细。 例如,identity-map uses token granularity.

我知道我见过其他例子,但不记得在哪里了。

为什么我们不使用基于 AST 节点的粒度来代替?也就是说,如果我们的源映射具有所有 AST 节点的位置并且只有 AST 节点的开始,那么缺点是什么?

据我了解,源映射用于崩溃堆栈解码和调试:永远不会有错误位置或有用的断点不在某个 AST 节点的开头,对吧?

更新 1

一些进一步的说明:

for (const token of tokens) {
    generator.addMapping({
      source: "source.js",
      original: token.location(),
      generated: generated.get(token).location(),
    });
}

下面是在 AST 节点级别添加位置的示例:

for (const node of nodes) {
    generator.addMapping({
      source: "source.js",
      original: node.location(),
      generated: generated.get(node).location(),
    });
}

更新 2

Q1:为什么期望 AST 节点的启动数少于令牌的启动数?

A1:因为如果 AST 节点的起始数多于令牌的起始数,那么就会有一个 AST 节点从非令牌开始。对于解析器的作者来说,这将是一项了不起的成就!为了具体说明,假设您有以下 JavaScript 语句:

const a = function *() { return a + ++ b }

以下是令牌开头的位置:

const a = function *() { return a + ++ b } /*
^     ^   ^        ^^^ ^ ^      ^ ^ ^  ^ ^
*/

这里是大多数解析器会说 AST 节点开始的大致位置。

const a = function *() { return a + ++ b } /*
^     ^   ^              ^      ^   ^  ^
*/

源地图位置的数量减少了 46%


Q2:为什么期望 AST-Node-granularity source maps 更小?

A2:见上文A1


Q3:您会使用什么格式来引用 AST 节点?

A3:无格式。请参阅上面 更新 1 中的示例代码。我说的是为 AST 节点的开始添加源映射位置。该过程几乎与为令牌开始添加源地图位置的过程完全相同,只是您添加的位置更少。


问题 4:您如何断言处理源映射的所有工具都使用相同的 AST 表示?

A4:假设我们控制整个管道并且在所有地方都使用相同的解析器。


可以使用 AST 粒度,但通常要构建 AST,无论如何都需要先标记代码。出于调试目的,AST 是不必要的步骤,因为语法分析器必须输入标记化数据才能工作。

一个有趣的资源on topic

我建议也探索一下 acornJS sourcecode and take a look how it produces AST

TypeScript 编译器实际上只在 AST 节点边界上发出 sourcemap 位置,除了一些例外,以提高与期望映射到特定位置的某些工具的兼容性,因此基于标记的映射实际上不是很通用。在您给出的示例中,TS 的源地图适用于这样的位置:

const a = function *() { return a + ++ b } /*
^     ^^  ^              ^      ^^  ^  ^^^
*/

通常是每个标识符 AST 节点的开始 结束(否则加上开始)。

映射标识符 AST 节点的开始 结束位置的基本原理非常简单 - 当您重命名标识符时,您希望重命名标识符的选择范围是能够映射回原始标识符,而不必依赖启发式方法。