Skip to content
This repository was archived by the owner on Apr 3, 2024. It is now read-only.

Commit 37d7745

Browse files
authored
include variables from outer scopes (#271)
We were previously omitting variables from closure scopes. This can be problematic specifically in transpiled async functions as most actual variables end up in a closure.
1 parent adcf3ae commit 37d7745

File tree

5 files changed

+170
-16
lines changed

5 files changed

+170
-16
lines changed

src/agent/state.js

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ StateResolver.prototype.capture_ = function() {
153153
});
154154
}
155155

156-
// The frames are resolved after the evaluated expressions so that
157-
// evaluated expressions can be evaluated as much as possible within
156+
// The frames are resolved after the evaluated expressions so that
157+
// evaluated expressions can be evaluated as much as possible within
158158
// the max data size limits
159159
var frames = that.resolveFrames_();
160160

@@ -220,7 +220,7 @@ StateResolver.prototype.resolveFrames_ = function() {
220220
var frames = [];
221221
var frameCount = Math.min(this.state_.frameCount(),
222222
this.config_.capture.maxFrames);
223-
223+
224224
for (var i = 0; i < frameCount; i++) {
225225
var frame = this.state_.frame(i);
226226
if (this.shouldFrameBeResolved_(frame)) {
@@ -343,7 +343,7 @@ StateResolver.prototype.extractArgumentsList_ = function (frame) {
343343

344344
/**
345345
* Iterates and returns variable information for all scopes (excluding global)
346-
* in a given frame. FrameMirrors should return their scope object list with
346+
* in a given frame. FrameMirrors should return their scope object list with
347347
* most deeply nested scope first so variables initially encountered will take
348348
* precedence over subsequent instance with the same name - this is tracked in
349349
* the usedNames map. The argument list given to this function may be
@@ -360,17 +360,29 @@ StateResolver.prototype.resolveLocalsList_ = function (frame, args) {
360360
var self = this;
361361
var usedNames = {};
362362
var makeMirror = this.ctx_.MakeMirror;
363-
return flatten(frame.allScopes().map(
363+
const allScopes = frame.allScopes();
364+
const count = allScopes.length;
365+
366+
// There will always be at least 3 scopes.
367+
// For top-level breakpoints: [local, script, global]
368+
// Other: [..., closure (module IIFE), script, global]
369+
assert(count >= 3);
370+
assert.strictEqual(allScopes[count - 1].scopeType(), ScopeType.Global);
371+
assert.strictEqual(allScopes[count - 2].scopeType(), ScopeType.Script);
372+
373+
// We find the top-level (module global) variable pollute the local variables
374+
// we omit them by default, unless the breakpoint itself is top-level. The
375+
// last two scopes are always omitted.
376+
let scopes;
377+
if (allScopes[count - 3].scopeType() === ScopeType.Closure) {
378+
scopes = allScopes.slice(0, -3);
379+
} else {
380+
assert(allScopes[count - 3].scopeType() === ScopeType.Local);
381+
scopes = allScopes.slice(0, -2);
382+
}
383+
384+
return flatten(scopes.map(
364385
function (scope) {
365-
switch (scope.scopeType()) {
366-
case ScopeType.Global:
367-
// We do not capture globals in the debug client.
368-
case ScopeType.Closure:
369-
// The closure scope is contaminated by Node.JS's require IIFE pattern
370-
// and, if traversed, will cause local variable pools to include what
371-
// are considered node 'globals'. Therefore, avoid traversal.
372-
return [];
373-
}
374386
return transform(
375387
scope.details().object(),
376388
function (locals, value, name) {
@@ -387,7 +399,7 @@ StateResolver.prototype.resolveLocalsList_ = function (frame, args) {
387399
);
388400
}
389401
)).concat((function () {
390-
// The frame receiver is the 'this' context that is present during
402+
// The frame receiver is the 'this' context that is present during
391403
// invocation. Check to see whether a receiver context is substantive,
392404
// (invocations may be bound to null) if so: store in the locals list
393405
// under the name 'context' which is used by the Chrome DevTools.

test/fixtures/ts/async.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// compile with
2+
// $ tsc --lib es6 --target es5 async.ts
3+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4+
return new (P || (P = Promise))(function (resolve, reject) {
5+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7+
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
8+
step((generator = generator.apply(thisArg, _arguments || [])).next());
9+
});
10+
};
11+
var __generator = (this && this.__generator) || function (thisArg, body) {
12+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14+
function verb(n) { return function (v) { return step([n, v]); }; }
15+
function step(op) {
16+
if (f) throw new TypeError("Generator is already executing.");
17+
while (_) try {
18+
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
19+
if (y = 0, t) op = [0, t.value];
20+
switch (op[0]) {
21+
case 0: case 1: t = op; break;
22+
case 4: _.label++; return { value: op[1], done: false };
23+
case 5: _.label++; y = op[1]; op = [0]; continue;
24+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
25+
default:
26+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30+
if (t[2]) _.ops.pop();
31+
_.trys.pop(); continue;
32+
}
33+
op = body.call(thisArg, _);
34+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36+
}
37+
};
38+
function delay(t) {
39+
return new Promise(function (resolve, reject) {
40+
setTimeout(resolve, t);
41+
});
42+
}
43+
function get(path, handler) {
44+
var _this = this;
45+
delay(10).then(function () {
46+
var req = {
47+
name: 'fake request',
48+
path: path
49+
};
50+
var res = {
51+
name: 'fake response',
52+
status: function (statusCode) {
53+
_this.statusCode = statusCode;
54+
return _this;
55+
},
56+
send: function (msg) {
57+
console.log(msg);
58+
}
59+
};
60+
handler(req, res);
61+
});
62+
}
63+
function run() {
64+
var _this = this;
65+
get('/foo', function (req, res) { return __awaiter(_this, void 0, void 0, function () {
66+
return __generator(this, function (_a) {
67+
switch (_a.label) {
68+
case 0: return [4 /*yield*/, delay(10)];
69+
case 1:
70+
_a.sent();
71+
res.status(200);
72+
return [2 /*return*/];
73+
}
74+
});
75+
}); });
76+
}
77+
module.exports = run;

test/fixtures/ts/async.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// compile with
2+
// $ tsc --lib es6 --target es5 async.ts
3+
4+
function delay(t) {
5+
return new Promise((resolve, reject) => {
6+
setTimeout(resolve, t);
7+
});
8+
}
9+
10+
function get(path, handler) {
11+
delay(10).then(() => {
12+
const req = {
13+
name: 'fake request',
14+
path: path
15+
};
16+
const res = {
17+
name: 'fake response',
18+
status: (statusCode) => {
19+
this.statusCode = statusCode;
20+
return this;
21+
},
22+
send: (msg) => {
23+
console.log(msg);
24+
}
25+
}
26+
handler(req, res);
27+
});
28+
}
29+
30+
function run() {
31+
get('/foo', async (req, res) => {
32+
await delay(10);
33+
res.status(200);
34+
});
35+
}
36+
37+
module.exports = run;

test/test-max-data-size.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ describe('maxDataSize', function() {
8888
api.wait(bp, function(err) {
8989
assert.ifError(err);
9090
assert(bp.variableTable.reduce(function(acc, elem) {
91-
return acc && elem.status.description.format !== 'Max data size reached';
91+
return acc &&
92+
(!elem.status ||
93+
elem.status.description.format !== 'Max data size reached');
9294
}), true);
9395
api.clear(bp);
9496
done();

test/test-v8debugapi.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,32 @@ describe('v8debugapi', function() {
12571257
process.nextTick(function() {foo(1);});
12581258
});
12591259
});
1260+
1261+
it('should capture state in transpiled TS async functions', (done) => {
1262+
const bp = {
1263+
id: 'async-id-1',
1264+
location: {
1265+
path: path.join('.', 'test', 'fixtures', 'ts', 'async.js'),
1266+
line: 71
1267+
}
1268+
};
1269+
1270+
const run = require('./fixtures/ts/async.js');
1271+
api.set(bp, (err) => {
1272+
assert.ifError(err);
1273+
api.wait(bp, (err) => {
1274+
assert.ifError(err);
1275+
assert.ok(bp.stackFrames);
1276+
1277+
const topFrame = bp.stackFrames[0];
1278+
assert.ok(topFrame.locals.some((local) => (local.name === '_a')));
1279+
assert.ok(topFrame.locals.some((local) => (local.name === 'res')));
1280+
api.clear(bp);
1281+
done();
1282+
});
1283+
});
1284+
process.nextTick(run);
1285+
});
12601286
});
12611287

12621288
it('should be possible to set deferred breakpoints');

0 commit comments

Comments
 (0)