Bug 1000967 - Add source notes for |new| expression and function calls to improve source maps and debugging. r=ejpbruel

This commit is contained in:
Nick Fitzgerald 2014-06-20 13:09:00 -04:00
parent 7e5658fbee
commit 89c96462f2
12 changed files with 325 additions and 146 deletions

View File

@ -41,7 +41,7 @@ function runTests()
method: "display",
code: error1,
result: error1 + openComment +
"Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:1" + closeComment,
"Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
label: "error display output"
},
{
@ -78,7 +78,7 @@ function runTests()
method: "run",
code: error1,
result: error1 + openComment +
"Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:1" + closeComment,
"Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
label: "error run output"
},
{

View File

@ -37,7 +37,7 @@ function runTests()
method: "display",
code: error,
result: error + openComment + "Exception: Ouch!\n@" +
scratchpad.uniqueName + ":1:1" + closeComment,
scratchpad.uniqueName + ":1:7" + closeComment,
label: "error display output",
},
{
@ -57,7 +57,7 @@ function runTests()
method: "run",
code: error,
result: error + openComment + "Exception: Ouch!\n@" +
scratchpad.uniqueName + ":1:1" + closeComment,
scratchpad.uniqueName + ":1:7" + closeComment,
label: "error run output",
},
{

View File

@ -20,7 +20,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=923010
}, { "mandatory": { "BOGUS": 5 } } )
ok(false, "That call to createAnswer should have thrown");
} catch (e) {
is(e.lineNumber, 20, "Exception should have been on line 20");
is(e.lineNumber, 16, "Exception should have been on line 16");
is(e.message,
"createAnswer passed invalid constraints - unknown mandatory constraint: BOGUS",
"Should have the exception we expect");

View File

@ -37,8 +37,8 @@
[ "Callback message", msg, "Error: hello" ],
[ "Event error-object", errorEvent.error, thrown],
[ "Callback error-object", error, thrown ],
[ "Event column", errorEvent.colno, 6 ], // Sadly not correct right now
[ "Callback column", column, 6 ]
[ "Event column", errorEvent.colno, 15 ],
[ "Callback column", column, 15 ]
]);
</script>
<script>

View File

@ -50,6 +50,9 @@ using mozilla::PodCopy;
static bool
SetSrcNoteOffset(ExclusiveContext *cx, BytecodeEmitter *bce, unsigned index, unsigned which, ptrdiff_t offset);
static bool
UpdateSourceCoordNotes(ExclusiveContext *cx, BytecodeEmitter *bce, uint32_t offset);
struct frontend::StmtInfoBCE : public StmtInfoBase
{
StmtInfoBCE *down; /* info for enclosing statement */
@ -273,8 +276,10 @@ EmitJump(ExclusiveContext *cx, BytecodeEmitter *bce, JSOp op, ptrdiff_t off)
}
static ptrdiff_t
EmitCall(ExclusiveContext *cx, BytecodeEmitter *bce, JSOp op, uint16_t argc)
EmitCall(ExclusiveContext *cx, BytecodeEmitter *bce, JSOp op, uint16_t argc, ParseNode *pn=nullptr)
{
if (pn && !UpdateSourceCoordNotes(cx, bce, pn->pn_pos.begin))
return -1;
return Emit3(cx, bce, op, ARGC_HI(argc), ARGC_LO(argc));
}
@ -4505,7 +4510,7 @@ EmitForOf(ExclusiveContext *cx, BytecodeEmitter *bce, ParseNode *pn, ptrdiff_t t
return false;
if (Emit1(cx, bce, JSOP_UNDEFINED) < 0) // ITER NEXT ITER UNDEFINED
return false;
if (EmitCall(cx, bce, JSOP_CALL, 1) < 0) // ITER RESULT
if (EmitCall(cx, bce, JSOP_CALL, 1, forHead) < 0) // ITER RESULT
return false;
CheckTypeSet(cx, bce, JSOP_CALL);
if (Emit1(cx, bce, JSOP_DUP) < 0) // ITER RESULT RESULT
@ -5196,7 +5201,7 @@ EmitYieldStar(ExclusiveContext *cx, BytecodeEmitter *bce, ParseNode *iter)
return false;
if (Emit1(cx, bce, JSOP_SWAP) < 0) // @@ITERATOR ITERABLE
return false;
if (EmitCall(cx, bce, JSOP_CALL, 0) < 0) // ITER
if (EmitCall(cx, bce, JSOP_CALL, 0, iter) < 0) // ITER
return false;
CheckTypeSet(cx, bce, JSOP_CALL);
@ -5268,7 +5273,7 @@ EmitYieldStar(ExclusiveContext *cx, BytecodeEmitter *bce, ParseNode *iter)
return false;
if (Emit2(cx, bce, JSOP_PICK, (jsbytecode)3) < 0) // ITER THROW ITER EXCEPTION
return false;
if (EmitCall(cx, bce, JSOP_CALL, 1) < 0) // ITER RESULT
if (EmitCall(cx, bce, JSOP_CALL, 1, iter) < 0) // ITER RESULT
return false;
CheckTypeSet(cx, bce, JSOP_CALL);
JS_ASSERT(bce->stackDepth == depth + 1);
@ -5305,7 +5310,7 @@ EmitYieldStar(ExclusiveContext *cx, BytecodeEmitter *bce, ParseNode *iter)
return false;
if (Emit2(cx, bce, JSOP_PICK, (jsbytecode)3) < 0) // ITER NEXT ITER RECEIVED
return false;
if (EmitCall(cx, bce, JSOP_CALL, 1) < 0) // ITER RESULT
if (EmitCall(cx, bce, JSOP_CALL, 1, iter) < 0) // ITER RESULT
return false;
CheckTypeSet(cx, bce, JSOP_CALL);
JS_ASSERT(bce->stackDepth == depth + 1);
@ -5626,7 +5631,7 @@ EmitCallOrNew(ExclusiveContext *cx, BytecodeEmitter *bce, ParseNode *pn)
}
if (!spread) {
if (EmitCall(cx, bce, pn->getOp(), argc) < 0)
if (EmitCall(cx, bce, pn->getOp(), argc, pn) < 0)
return false;
} else {
if (Emit1(cx, bce, pn->getOp()) < 0)

View File

@ -8,7 +8,7 @@ dbg.onDebuggerStatement = function (frame) {
assertEq(exc.message, "diaf");
assertEq(exc.fileName, "fail");
assertEq(exc.lineNumber, 4);
assertEq(exc.columnNumber, 20);
assertEq(exc.columnNumber, 24);
return;
}
throw new Error("deleteProperty should throw");

View File

@ -0,0 +1,27 @@
// getColumnOffsets correctly places function calls.
var global = newGlobal();
Debugger(global).onDebuggerStatement = function (frame) {
var script = frame.eval("f").return.script;
script.getAllColumnOffsets().forEach(function (offset) {
script.setBreakpoint(offset.offset, {
hit: function (frame) {
assertEq(offset.lineNumber, 1);
global.log += offset.columnNumber + " ";
}
});
});
};
global.log = "";
// 1 2 3 4
// 0123456789012345678901234567890123456789012345678
global.eval("function f(){ 1 && print(print()) && new Error() } debugger;");
global.f();
// 14 - Enter the function body
// 25 - Inner print()
// 19 - Outer print()
// 37 - new Error()
// 48 - Exit the function body
assertEq(global.log, "14 25 19 37 48 ");

View File

@ -25,7 +25,9 @@ test(foo, 1);
//234567890123456789
test(function(f) { return f.bar; }, 19);
test(function(f) { return f(); }, 19);
// 1 2
//2345678901234567890123456
test(function(f) { return f(); }, 26);
/* Cover negative colspan case using for(;;) loop with error in update part. */
test(function(){
//0 1 2 3 4
@ -41,9 +43,9 @@ test(function() { var tmp = null; tmp.foo; }, 35)
/* Just a generic 'throw'. */
test(function() {
//234567890123
//234567890123456789
foo({}); throw new Error('a');
}, 13);
}, 19);
/* Be sure to report the right statement */
test(function() {

View File

@ -1,5 +1,5 @@
actual = 'No Error';
expected = /column-numbers\.js:4:5/;
expected = /column-numbers\.js:4:11/;
try {
throw new Error("test");
}

View File

@ -10,6 +10,8 @@ const Cr = Components.results;
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { worker } = Cu.import("resource://gre/modules/devtools/worker-loader.js", {})
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
const { promiseInvoke } = devtools.require("devtools/async-utils");
const Services = devtools.require("Services");
// Always log packets when running tests. runxpcshelltests.py will throw
@ -22,12 +24,16 @@ const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
const { DebuggerServer } = devtools.require("devtools/server/main");
const { DebuggerServer: WorkerDebuggerServer } = worker.require("devtools/server/main");
function dumpn(msg) {
dump("DBG-TEST: " + msg + "\n");
}
function tryImport(url) {
try {
Cu.import(url);
} catch (e) {
dump("Error importing " + url + "\n");
dump(DevToolsUtils.safeErrorString(e) + "\n");
dumpn("Error importing " + url);
dumpn(DevToolsUtils.safeErrorString(e));
throw e;
}
}
@ -78,9 +84,9 @@ let listener = {
// If we've been given an nsIScriptError, then we can print out
// something nicely formatted, for tools like Emacs to pick up.
var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
scriptErrorFlagsToKind(aMessage.flags) + ": " +
aMessage.errorMessage + "\n");
dumpn(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
scriptErrorFlagsToKind(aMessage.flags) + ": " +
aMessage.errorMessage);
var string = aMessage.errorMessage;
} catch (x) {
// Be a little paranoid with message, as the whole goal here is to lose
@ -106,7 +112,7 @@ let listener = {
// If we throw an error here because of them our tests start failing.
// So, we'll just dump the message to the logs instead, to make sure the
// information isn't lost.
dump("head_dbg.js observed a console message: " + string + "\n");
dumpn("head_dbg.js observed a console message: " + string);
}
};
@ -121,7 +127,7 @@ function check_except(func)
do_check_true(true);
return;
}
dump("Should have thrown an exception: " + func.toString());
dumpn("Should have thrown an exception: " + func.toString());
do_check_true(false);
}
@ -336,9 +342,9 @@ TracingTransport.prototype = {
dumpLog: function() {
for (let entry of this.packets) {
if (entry.type === "sent") {
dump("trace.expectSend(" + entry.packet + ");\n");
dumpn("trace.expectSend(" + entry.packet + ");");
} else {
dump("trace.expectReceive(" + entry.packet + ");\n");
dumpn("trace.expectReceive(" + entry.packet + ");");
}
}
}
@ -407,3 +413,171 @@ const Test = task => () => {
};
const assert = do_check_true;
/**
* Create a promise that is resolved on the next occurence of the given event.
*
* @param DebuggerClient client
* @param String event
* @returns Promise
*/
function waitForEvent(client, event) {
dumpn("Waiting for event: " + event);
return new Promise((resolve, reject) => {
client.addOneTimeListener(event, (_, packet) => resolve(packet));
});
}
/**
* Create a promise that is resolved on the next pause.
*
* @param DebuggerClient client
* @returns Promise
*/
function waitForPause(client) {
return waitForEvent(client, "paused");
}
/**
* Execute the action on the next tick and return a promise that is resolved on
* the next pause.
*
* When using promises and Task.jsm, we often want to do an action that causes a
* pause and continue the task once the pause has ocurred. Unfortunately, if we
* do the action that causes the pause within the task's current tick we will
* pause before we have a chance to yield the promise that waits for the pause
* and we enter a dead lock. The solution is to create the promise that waits
* for the pause, schedule the action to run on the next tick of the event loop,
* and finally yield the promise.
*
* @param Function action
* @param DebuggerClient client
* @returns Promise
*/
function executeOnNextTickAndWaitForPause(action, client) {
const paused = waitForPause(client);
executeSoon(action);
return paused;
}
/**
* Create a promise that is resolved with the server's response to the client's
* Remote Debugger Protocol request. If a response with the `error` property is
* received, the promise is rejected. Any extra arguments passed in are
* forwarded to the method invocation.
*
* See `setBreakpoint` below, for example usage.
*
* @param DebuggerClient/ThreadClient/SourceClient/etc client
* @param Function method
* @param any args
* @returns Promise
*/
function rdpRequest(client, method, ...args) {
return promiseInvoke(client, method, ...args)
.then(response => {
const { error, message } = response;
if (error) {
throw new Error(error + ": " + message);
}
return response;
});
}
/**
* Set a breakpoint over the Remote Debugging Protocol.
*
* @param ThreadClient threadClient
* @param {url, line[, column[, condition]]} breakpointOptions
* @returns Promise
*/
function setBreakpoint(threadClient, breakpointOptions) {
dumpn("Setting a breakpoint: " + JSON.stringify(breakpointOptions, null, 2));
return rdpRequest(threadClient, threadClient.setBreakpoint, breakpointOptions);
}
/**
* Resume JS execution for the specified thread.
*
* @param ThreadClient threadClient
* @returns Promise
*/
function resume(threadClient) {
dumpn("Resuming.");
return rdpRequest(threadClient, threadClient.resume);
}
/**
* Resume JS execution for the specified thread and then wait for the next pause
* event.
*
* @param DebuggerClient client
* @param ThreadClient threadClient
* @returns Promise
*/
function resumeAndWaitForPause(client, threadClient) {
const paused = waitForPause(client);
return resume(threadClient).then(() => paused);
}
/**
* Get the list of sources for the specified thread.
*
* @param ThreadClient threadClient
* @returns Promise
*/
function getSources(threadClient) {
dumpn("Getting sources.");
return rdpRequest(threadClient, threadClient.getSources);
}
/**
* Resume JS execution for a single step and wait for the pause after the step
* has been taken.
*
* @param DebuggerClient client
* @param ThreadClient threadClient
* @returns Promise
*/
function stepIn(client, threadClient) {
dumpn("Stepping in.");
const paused = waitForPause(client);
return rdpRequest(threadClient, threadClient.stepIn)
.then(() => paused);
}
/**
* Get the list of `count` frames currently on stack, starting at the index
* `first` for the specified thread.
*
* @param ThreadClient threadClient
* @param Number first
* @param Number count
* @returns Promise
*/
function getFrames(threadClient, first, count) {
dumpn("Getting frames.");
return rdpRequest(threadClient, threadClient.getFrames, first, count);
}
/**
* Black box the specified source.
*
* @param SourceClient sourceClient
* @returns Promise
*/
function blackBox(sourceClient) {
dumpn("Black boxing source: " + sourceClient.actor);
return rdpRequest(sourceClient, sourceClient.blackBox);
}
/**
* Stop black boxing the specified source.
*
* @param SourceClient sourceClient
* @returns Promise
*/
function unBlackBox(sourceClient) {
dumpn("Un-black boxing source: " + sourceClient.actor);
return rdpRequest(sourceClient, sourceClient.unblackBox);
}

View File

@ -17,7 +17,7 @@ function run_test()
gClient.connect(function() {
attachTestTabAndResume(gClient, "test-black-box", function(aResponse, aTabClient, aThreadClient) {
gThreadClient = aThreadClient;
test_black_box();
testBlackBox();
});
});
do_test_pending();
@ -26,18 +26,71 @@ function run_test()
const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
const SOURCE_URL = "http://example.com/source.js";
function test_black_box()
{
gClient.addOneTimeListener("paused", function () {
gThreadClient.setBreakpoint({
url: SOURCE_URL,
line: 2
}, function (aResponse) {
do_check_true(!aResponse.error, "Should be able to set breakpoint.");
gThreadClient.resume(test_black_box_default);
});
});
const testBlackBox = Task.async(function* () {
yield executeOnNextTickAndWaitForPause(evalCode, gClient);
yield setBreakpoint(gThreadClient, {
url: SOURCE_URL,
line: 2
});
yield resume(gThreadClient);
const sourcesResponse = yield getSources(gThreadClient);
let sourceClient = gThreadClient.source(
sourcesResponse.sources.filter(s => s.url == BLACK_BOXED_URL)[0]);
do_check_true(!sourceClient.isBlackBoxed,
"By default the source is not black boxed.");
// Test that we can step into `doStuff` when we are not black boxed.
yield runTest(
function onSteppedLocation(aLocation) {
do_check_eq(aLocation.url, BLACK_BOXED_URL);
do_check_eq(aLocation.line, 2);
},
function onDebuggerStatementFrames(aFrames) {
do_check_true(!aFrames.some(f => f.source.isBlackBoxed));
}
);
let blackBoxResponse = yield blackBox(sourceClient);
do_check_true(sourceClient.isBlackBoxed);
// Test that we step through `doStuff` when we are black boxed and its frame
// doesn't show up.
yield runTest(
function onSteppedLocation(aLocation) {
do_check_eq(aLocation.url, SOURCE_URL);
do_check_eq(aLocation.line, 3);
},
function onDebuggerStatementFrames(aFrames) {
for (let f of aFrames) {
if (f.where.url == BLACK_BOXED_URL) {
do_check_true(f.source.isBlackBoxed);
} else {
do_check_true(!f.source.isBlackBoxed)
}
}
}
);
let unBlackBoxResponse = yield unBlackBox(sourceClient);
do_check_true(!sourceClient.isBlackBoxed);
// Test that we can step into `doStuff` again.
yield runTest(
function onSteppedLocation(aLocation) {
do_check_eq(aLocation.url, BLACK_BOXED_URL);
do_check_eq(aLocation.line, 2);
},
function onDebuggerStatementFrames(aFrames) {
do_check_true(!aFrames.some(f => f.source.isBlackBoxed));
}
);
finishClient(gClient);
});
function evalCode() {
Components.utils.evalInSandbox(
"" + function doStuff(k) { // line 1
let arg = 15; // line 2 - Step in here
@ -53,11 +106,11 @@ function test_black_box()
"" + function runTest() { // line 1
doStuff( // line 2 - Break here
function (n) { // line 3 - Step through `doStuff` to here
debugger; // line 5
} // line 6
); // line 7
} // line 8
+ "\n debugger;", // line 9
debugger; // line 4
} // line 5
); // line 6
} + "\n" // line 7
+ "debugger;", // line 8
gDebuggee,
"1.8",
SOURCE_URL,
@ -65,110 +118,28 @@ function test_black_box()
);
}
function test_black_box_default() {
gThreadClient.getSources(function (aResponse) {
do_check_true(!aResponse.error, "Should be able to get sources.");
const runTest = Task.async(function* (onSteppedLocation, onDebuggerStatementFrames) {
let packet = yield executeOnNextTickAndWaitForPause(gDebuggee.runTest,
gClient);
do_check_eq(packet.why.type, "breakpoint");
let sourceClient = gThreadClient.source(
aResponse.sources.filter(s => s.url == BLACK_BOXED_URL)[0]);
do_check_true(!sourceClient.isBlackBoxed,
"By default the source is not black boxed.");
yield stepIn(gClient, gThreadClient);
yield stepIn(gClient, gThreadClient);
yield stepIn(gClient, gThreadClient);
// Test that we can step into `doStuff` when we are not black boxed.
runTest(
function onSteppedLocation(aLocation) {
do_check_eq(aLocation.url, BLACK_BOXED_URL,
"Should step into `doStuff`.");
do_check_eq(aLocation.line, 2,
"Should step into `doStuff`.");
},
function onDebuggerStatementFrames(aFrames) {
do_check_true(!aFrames.some(f => f.source.isBlackBoxed));
},
test_black_boxing.bind(null, sourceClient)
);
});
}
const location = yield getCurrentLocation();
onSteppedLocation(location);
function test_black_boxing(aSourceClient) {
aSourceClient.blackBox(function (aResponse) {
do_check_true(!aResponse.error, "Should not get an error black boxing.");
do_check_true(aSourceClient.isBlackBoxed,
"The source client should report itself as black boxed correctly.");
packet = yield resumeAndWaitForPause(gClient, gThreadClient);
do_check_eq(packet.why.type, "debuggerStatement");
// Test that we step through `doStuff` when we are black boxed and its frame
// doesn't show up.
runTest(
function onSteppedLocation(aLocation) {
do_check_eq(aLocation.url, SOURCE_URL,
"Should step through `doStuff`.");
do_check_eq(aLocation.line, 3,
"Should step through `doStuff`.");
},
function onDebuggerStatementFrames(aFrames) {
for (let f of aFrames) {
if (f.where.url == BLACK_BOXED_URL) {
do_check_true(f.source.isBlackBoxed, "Should be black boxed");
} else {
do_check_true(!f.source.isBlackBoxed, "Should not be black boxed")
}
}
},
test_unblack_boxing.bind(null, aSourceClient)
);
});
}
let { frames } = yield getFrames(gThreadClient, 0, 100);
onDebuggerStatementFrames(frames);
function test_unblack_boxing(aSourceClient) {
aSourceClient.unblackBox(function (aResponse) {
do_check_true(!aResponse.error, "Should not get an error un-black boxing");
do_check_true(!aSourceClient.isBlackBoxed, "The source is not black boxed.");
return resume(gThreadClient);
});
// Test that we can step into `doStuff` again.
runTest(
function onSteppedLocation(aLocation) {
do_check_eq(aLocation.url, BLACK_BOXED_URL,
"Should step into `doStuff`.");
do_check_eq(aLocation.line, 2,
"Should step into `doStuff`.");
},
function onDebuggerStatementFrames(aFrames) {
do_check_true(!aFrames.some(f => f.source.isBlackBoxed));
},
finishClient.bind(null, gClient)
);
});
}
function runTest(aOnSteppedLocation, aOnDebuggerStatementFrames, aFinishedCallback) {
gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
do_check_eq(aPacket.why.type, "breakpoint");
gClient.addOneTimeListener("paused", function () {
gClient.addOneTimeListener("paused", function () {
getCurrentLocation(function (aLocation) {
aOnSteppedLocation(aLocation);
gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
do_check_eq(aPacket.why.type, "debuggerStatement");
gThreadClient.getFrames(0, 100, function ({frames}) {
aOnDebuggerStatementFrames(frames);
gThreadClient.resume(aFinishedCallback);
});
});
gThreadClient.resume();
});
});
gThreadClient.stepIn();
});
gThreadClient.stepIn();
});
gDebuggee.runTest();
}
function getCurrentLocation(aCallback) {
gThreadClient.getFrames(0, 1, function ({frames, error}) {
do_check_true(!error, "Should not get an error: " + error);
let [{where}] = frames;
aCallback(where);
});
}
const getCurrentLocation = Task.async(function* () {
const response = yield getFrames(gThreadClient, 0, 1);
return response.frames[0].where;
});

View File

@ -291,7 +291,7 @@ add_task(function* test_state() {
let state = barrier.state[0];
do_print("State: " + JSON.stringify(barrier.state, null, "\t"));
Assert.equal(state.filename, filename);
Assert.equal(state.lineNumber, lineNumber + 2);
Assert.equal(state.lineNumber, lineNumber + 1);
Assert.equal(state.name, BLOCKER_NAME);
deferred.resolve();