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

Max*_*ber 22 javascript source-maps gulp-sourcemaps

JavaScript 的源映射似乎通常不比令牌粒度更细。例如,身份映射使用令牌粒度

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

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

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

更新 1

一些进一步的澄清:

  • 该问题与 AST 已知的情况有关。因此,“生成 AST 比生成令牌数组更昂贵”无法回答这个问题。

  • 这个问题的实际影响是,如果我们可以在保留调试器和崩溃堆栈解码器行为的同时降低源映射的粒度,那么源映射可能会小得多。主要优点是调试器的性能:开发工具可能需要很长时间来处理大型源文件,使调试变得痛苦。

  • 以下是使用source-map库在令牌级别添加源地图位置的示例:

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

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

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

更新 2

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

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

const a = function *() { return a + ++ b }
Run Code Online (Sandbox Code Playgroud)

以下是令牌开头的位置:

const a = function *() { return a + ++ b } /*
^     ^   ^        ^^^ ^ ^      ^ ^ ^  ^ ^
*/
Run Code Online (Sandbox Code Playgroud)

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

const a = function *() { return a + ++ b } /*
^     ^   ^              ^      ^   ^  ^
*/
Run Code Online (Sandbox Code Playgroud)

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


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

A2:见上面的A1


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

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


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

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


Wes*_*ham 4

编译TypeScript器实际上只在 AST 节点边界上发出源映射位置,但有一些例外是为了提高与某些需要映射某些位置的工具的兼容性,因此基于标记的映射实际上并不十分通用。在您给出的示例中,TS 的源映射适用于如下位置:

const a = function *() { return a + ++ b } /*
^     ^^  ^              ^      ^^  ^  ^^^
*/
Run Code Online (Sandbox Code Playgroud)

通常是每个 Identifier AST 节点的开始结束(否则加开始)。

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