-
Notifications
You must be signed in to change notification settings - Fork 2
/
getOriginalFrames.ts
93 lines (78 loc) · 4.1 KB
/
getOriginalFrames.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import { Location, DebuggerScope, SourcemapScope, UnavailableValue, DebuggerValue, DebuggerFrame, ScopeType, OriginalLocation } from "./types";
import { assert, compareLocations, isEnclosing, isInRange } from "./util";
// Compute the original frames and scopes given a location, scope information from the sourcemap
// and the scopes received from the debugger (containing the bindings for the generated variables)
export function getOriginalFrames(
location: Location,
originalLocation: OriginalLocation,
sourcemapScopes: SourcemapScope[],
debuggerScopes: DebuggerScope[]
): DebuggerFrame[] {
const scopes = sourcemapScopes.filter(scope => isInRange(location, scope));
// Sort from outermost to innermost, assuming debuggerScopes is also sorted that way
scopes.sort((s1, s2) => compareLocations(s1.start, s2.start));
// The number of scopes received from the debugger must be the same as the number
// of scopes declared by the sourcemap for the generated source at the given location
// plus one (the global scope, i.e. `window`, which is not declared in the sourcemap)
const generatedScopes = scopes.filter(scope => scope.isInGeneratedSource);
assert(debuggerScopes.length === generatedScopes.length + 1);
const originalFrames: DebuggerFrame[] = [];
// The outermost original scope is identical to the outermost generated scope,
// which is the global scope
let originalScopes: DebuggerScope[] = [debuggerScopes[0]];
let originalName: string | null = null;
for (const scope of scopes) {
if (!scope.isInOriginalSource) {
continue;
}
if (scope.isOutermostInlinedScope) {
assert(scope.callsite);
originalFrames.unshift({ name: originalName, location: scope.callsite, scopes: originalScopes });
originalName = null;
originalScopes = [debuggerScopes[0]];
}
const enclosingGeneratedScopes = sourcemapScopes.filter(
sourcemapScope => sourcemapScope.isInGeneratedSource && isEnclosing(sourcemapScope, scope)
);
const enclosingDebuggerScopes = debuggerScopes.slice(0, enclosingGeneratedScopes.length + 1);
const originalBindings = scope.bindings.map(({ varname, expression }) => {
// We use `lookupScopeValue()`, which only works if `expression` is the name of a
// generated variable, to support arbitrary expressions we'd need to use `evaluateWithScopes()`
const value = expression !== null ? lookupScopeValue(expression, enclosingDebuggerScopes) : { unavailable: true } as UnavailableValue;
return { varname, value };
});
originalScopes.push({ bindings: originalBindings });
if (scope.type === ScopeType.NAMED_FUNCTION) {
originalName = scope.name;
} else if (scope.type === ScopeType.ANONYMOUS_FUNCTION) {
originalName = "<anonymous>";
}
}
originalFrames.unshift({ name: originalName, location: originalLocation, scopes: originalScopes });
return originalFrames;
}
function lookupScopeValue(expression: string, scopes: DebuggerScope[]): DebuggerValue {
if (expression.startsWith('"') && expression.endsWith('"')) {
return { value: expression.slice(1, -1) };
}
for (let i = scopes.length - 1; i >= 0; i--) {
const binding = scopes[i].bindings.find(binding => binding.varname === expression);
if (binding) {
return binding.value;
}
}
return { unavailable: true } as UnavailableValue;
}
// To emulate evaluating arbitrary expressions in a given scope chain we'd need a debugger
// command that evaluates a function expression in the global scope and applies it to
// the given debugger values, e.g. https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-callFunctionOn
function evaluateWithScopes(
expression: string,
scopes: DebuggerScope[],
evaluateWithArguments: (functionDeclaration: string, args: DebuggerValue[]) => DebuggerValue
) {
const nonGlobalScopes = scopes.slice(1);
const varnames = nonGlobalScopes.flatMap(scope => scope.bindings.map(binding => binding.varname));
const values = nonGlobalScopes.flatMap(scope => scope.bindings.map(binding => binding.value));
return evaluateWithArguments(`(${varnames.join(",")}) => (${expression})`, values);
}