Bug 969786: Implement Debugger.Source.prototype.introductionScript. r=sfink
☠☠ backed out by 782acfe6edfd ☠ ☠
authorJim Blandy <jimb@mozilla.com>
Wed, 26 Feb 2014 15:20:00 -0800
changeset 171182 a79a64806e6c3de5a4be9124e77a84ce309ef7e2
parent 171181 198decf16acf8ed23b2825bb8eba26a943fc9168
child 171183 d599392113983b0f47c7ced5290ac6d247e9c755
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewerssfink
bugs969786
milestone30.0a1
Bug 969786: Implement Debugger.Source.prototype.introductionScript. r=sfink
js/src/jit-test/tests/debug/Source-introductionScript-01.js
js/src/jit-test/tests/debug/Source-introductionScript-02.js
js/src/jsscript.cpp
js/src/vm/Debugger.cpp
toolkit/devtools/server/tests/mochitest/chrome.ini
toolkit/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionScript.html
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Source-introductionScript-01.js
@@ -0,0 +1,118 @@
+// Dynamically generated sources should have their introduction script and
+// offset set correctly.
+
+var g = newGlobal();
+var dbg = new Debugger;
+var gDO = dbg.addDebuggee(g);
+var log;
+
+// Direct eval, while the frame is live.
+dbg.onDebuggerStatement = function (frame) {
+  log += 'd';
+  var source = frame.script.source;
+  var introducer = frame.older;
+  assertEq(source.introductionScript, introducer.script);
+  assertEq(source.introductionOffset, introducer.offset);
+};
+log = '';
+g.eval('\n\neval("\\n\\ndebugger;");');
+assertEq(log, 'd');
+
+// Direct eval, after the frame has been popped.
+var introducer, introduced;
+dbg.onDebuggerStatement = function (frame) {
+  log += 'de1';
+  introducer = frame.script;
+  dbg.onDebuggerStatement = function (frame) {
+    log += 'de2';
+    introduced = frame.script.source;
+  };
+};
+log = '';
+g.evaluate('debugger; eval("\\n\\ndebugger;");', { lineNumber: 1812 });
+assertEq(log, 'de1de2');
+assertEq(introduced.introductionScript, introducer);
+assertEq(introducer.getOffsetLine(introduced.introductionOffset), 1812);
+
+// Indirect eval, while the frame is live.
+dbg.onDebuggerStatement = function (frame) {
+  log += 'd';
+  var source = frame.script.source;
+  var introducer = frame.older;
+  assertEq(source.introductionScript, introducer.script);
+  assertEq(source.introductionOffset, introducer.offset);
+};
+log = '';
+g.eval('\n\n(0,eval)("\\n\\ndebugger;");');
+assertEq(log, 'd');
+
+// Indirect eval, after the frame has been popped.
+var introducer, introduced;
+dbg.onDebuggerStatement = function (frame) {
+  log += 'de1';
+  introducer = frame.script;
+  dbg.onDebuggerStatement = function (frame) {
+    log += 'de2';
+    introduced = frame.script.source;
+  };
+};
+log = '';
+g.evaluate('debugger; (0,eval)("\\n\\ndebugger;");', { lineNumber: 1066 });
+assertEq(log, 'de1de2');
+assertEq(introduced.introductionScript, introducer);
+assertEq(introducer.getOffsetLine(introduced.introductionOffset), 1066);
+
+// Function constructor.
+dbg.onDebuggerStatement = function (frame) {
+  log += 'o';
+  var outerScript = frame.script;
+  var outerOffset = frame.offset;
+  dbg.onDebuggerStatement = function (frame) {
+    log += 'i';
+    var source = frame.script.source;
+    assertEq(source.introductionScript, outerScript);
+    assertEq(outerScript.getOffsetLine(source.introductionOffset),
+             outerScript.getOffsetLine(outerOffset));
+  };
+};
+log = '';
+g.eval('\n\n\ndebugger; Function("debugger;")()');
+assertEq(log, 'oi');
+
+// Function constructor, after the the introduction call's frame has been
+// popped.
+var introducer;
+dbg.onDebuggerStatement = function (frame) {
+  log += 'F2';
+  introducer = frame.script;
+};
+log = '';
+var fDO = gDO.evalInGlobal('debugger; Function("origami;")', { lineNumber: 1685 }).return;
+var source = fDO.script.source;
+assertEq(log, 'F2');
+assertEq(source.introductionScript, introducer);
+assertEq(introducer.getOffsetLine(source.introductionOffset), 1685);
+
+// If the introduction script is in a different global from the script it
+// introduced, we don't record it.
+dbg.onDebuggerStatement = function (frame) {
+  log += 'x';
+  var source = frame.script.source;
+  assertEq(source.introductionScript, undefined);
+  assertEq(source.introductionOffset, undefined);
+};
+log = '';
+g.eval('debugger;'); // introduction script is this top-level script
+assertEq(log, 'x');
+
+// If the code is introduced by a function that doesn't provide
+// introduction information, that shouldn't be a problem.
+dbg.onDebuggerStatement = function (frame) {
+  log += 'x';
+  var source = frame.script.source;
+  assertEq(source.introductionScript, undefined);
+  assertEq(source.introductionOffset, undefined);
+};
+log = '';
+g.eval('evaluate("debugger;", { lineNumber: 1729 });');
+assertEq(log, 'x');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Source-introductionScript-02.js
@@ -0,0 +1,31 @@
+// Calls to 'eval', etc. by JS primitives get attributed to the point of
+// the primitive's call.
+
+var g = newGlobal();
+var dbg = new Debugger;
+var gDO = dbg.addDebuggee(g);
+var log = '';
+
+function outerHandler(frame) {
+  log += 'o';
+  var outerScript = frame.script;
+
+  dbg.onDebuggerStatement = function (frame) {
+    log += 'i';
+    var source = frame.script.source;
+    var introScript = source.introductionScript;
+    assertEq(introScript, outerScript);
+    assertEq(introScript.getOffsetLine(source.introductionOffset), 1234);
+  };
+};
+
+log = '';
+dbg.onDebuggerStatement = outerHandler;
+g.evaluate('debugger; ["debugger;"].map(eval)', { lineNumber: 1234 });
+assertEq(log, 'oi');
+
+log = '';
+dbg.onDebuggerStatement = outerHandler;
+g.evaluate('debugger; "debugger;".replace(/.*/, eval);',
+           { lineNumber: 1234 });
+assertEq(log, 'oi');
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -1821,17 +1821,17 @@ ScriptSource::initFromOptions(ExclusiveC
     JS_ASSERT(!filename_);
     JS_ASSERT(!introducerFilename_);
 
     originPrincipals_ = options.originPrincipals();
     if (originPrincipals_)
         JS_HoldPrincipals(originPrincipals_);
 
     introductionType_ = options.introductionType;
-    introductionOffset_ = options.introductionOffset;
+    setIntroductionOffset(options.introductionOffset);
 
     if (options.hasIntroductionInfo) {
         JS_ASSERT(options.introductionType != nullptr);
         const char *filename = options.filename() ? options.filename() : "<unknown>";
         char *formatted = FormatIntroducedFilename(cx, filename, options.introductionLineno,
                                                    options.introductionType);
         if (!formatted)
             return false;
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -3916,32 +3916,52 @@ static bool
 DebuggerSource_getElementProperty(JSContext *cx, unsigned argc, Value *vp)
 {
     THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get elementAttributeName)", args, obj, sourceObject);
     args.rval().set(sourceObject->elementAttributeName());
     return Debugger::fromChildJSObject(obj)->wrapDebuggeeValue(cx, args.rval());
 }
 
 static bool
+DebuggerSource_getIntroductionScript(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionScript)", args, obj, sourceObject);
+
+    RootedScript script(cx, sourceObject->introductionScript());
+    if (script) {
+        RootedObject scriptDO(cx, Debugger::fromChildJSObject(obj)->wrapScript(cx, script));
+        if (!scriptDO)
+            return false;
+        args.rval().setObject(*scriptDO);
+    } else {
+        args.rval().setUndefined();
+    }
+    return true;
+}
+
+static bool
 DebuggerSource_getIntroductionOffset(JSContext *cx, unsigned argc, Value *vp)
 {
     THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionOffset)", args, obj, sourceObject);
 
+    // Regardless of what's recorded in the ScriptSourceObject and
+    // ScriptSource, only hand out the introduction offset if we also have
+    // the script within which it applies.
     ScriptSource *ss = sourceObject->source();
-    if (ss->hasIntroductionOffset())
+    if (ss->hasIntroductionOffset() && sourceObject->introductionScript())
         args.rval().setInt32(ss->introductionOffset());
     else
         args.rval().setUndefined();
     return true;
 }
 
 static bool
 DebuggerSource_getIntroductionType(JSContext *cx, unsigned argc, Value *vp)
 {
-    THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionOffset)", args, obj, sourceObject);
+    THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionType)", args, obj, sourceObject);
 
     ScriptSource *ss = sourceObject->source();
     if (ss->hasIntroductionType()) {
         JSString *str = js_NewStringCopyZ<CanGC>(cx, ss->introductionType());
         if (!str)
             return false;
         args.rval().setString(str);
     } else {
@@ -3950,16 +3970,17 @@ DebuggerSource_getIntroductionType(JSCon
     return true;
 }
 
 static const JSPropertySpec DebuggerSource_properties[] = {
     JS_PSG("text", DebuggerSource_getText, 0),
     JS_PSG("url", DebuggerSource_getUrl, 0),
     JS_PSG("element", DebuggerSource_getElement, 0),
     JS_PSG("displayURL", DebuggerSource_getDisplayURL, 0),
+    JS_PSG("introductionScript", DebuggerSource_getIntroductionScript, 0),
     JS_PSG("introductionOffset", DebuggerSource_getIntroductionOffset, 0),
     JS_PSG("introductionType", DebuggerSource_getIntroductionType, 0),
     JS_PSG("elementAttributeName", DebuggerSource_getElementProperty, 0),
     JS_PS_END
 };
 
 static const JSFunctionSpec DebuggerSource_methods[] = {
     JS_FS_END
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -7,16 +7,17 @@ support-files =
   nonchrome_unsafeDereference.html
   inspector_getImageData.html
   large-image.jpg
   small-image.gif
   Debugger.Source.prototype.element.js
   Debugger.Source.prototype.element-2.js
   Debugger.Source.prototype.element.html
 
+[test_Debugger.Source.prototype.introductionScript.html]
 [test_Debugger.Source.prototype.introductionType.html]
 [test_Debugger.Source.prototype.element.html]
 [test_Debugger.Script.prototype.global.html]
 [test_connection-manager.html]
 [test_device.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-hide.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionScript.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=969786
+
+Debugger.Source.prototype.introductionScript and .introductionOffset should
+behave when 'eval' is called with no scripted frames active at all.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Debugger.Source.prototype.introductionScript with no caller</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+
+Components.utils.import("resource://gre/modules/jsdebugger.jsm");
+addDebuggerToGlobal(this);
+
+window.onload = function () {
+  SimpleTest.waitForExplicitFinish();
+
+  var dbg, iframeDO, doc, script2DO;
+
+  // Create an iframe to debug.
+  var iframe = document.createElement("iframe");
+  iframe.src = "data:text/html,<div>Hi!</div>";
+  iframe.onload = onLoadHandler;
+  document.body.appendChild(iframe);
+
+  function onLoadHandler() {
+    // Now that the iframe's window has been created, we can add
+    // it as a debuggee.
+    dbg = new Debugger;
+    iframeDO = dbg.addDebuggee(iframe.contentWindow);
+
+    doc = iframe.contentWindow.document;
+    var script = doc.createElement('script');
+    script.text = "setTimeout(eval.bind(null, 'debugger;'), 0);";
+    dbg.onDebuggerStatement = timerHandler;
+    doc.body.appendChild(script);
+  }
+
+  function timerHandler(frame) {
+    // The top stack frame's source should have an undefined
+    // introduction script and introduction offset.
+    var source = frame.script.source;
+    ok(source.introductionScript === undefined,
+       "setTimeout eval introductionScript is undefined");
+    ok(source.introductionOffset === undefined,
+       "setTimeout eval introductionOffset is undefined");
+
+    // Check that the above isn't just some quirk of iframes, or the
+    // browser milieu destroying information: an eval script should indeed
+    // have proper introduction information.
+    var script2 = doc.createElement('script');
+    script2.text = "eval('debugger;');";
+    script2DO = iframeDO.makeDebuggeeValue(script2);
+    dbg.onDebuggerStatement = evalHandler;
+    doc.body.appendChild(script2);
+  }
+
+  function evalHandler(frame) {
+    // The top stack frame's source should be introduced by the script that
+    // called eval.
+    var source = frame.script.source;
+    var frame2 = frame.older;
+
+    ok(source.introductionType === 'eval',
+       "top frame's source was introduced by 'eval'");
+    ok(source.introductionScript === frame2.script,
+       "eval frame's introduction script is the older frame's script");
+    ok(source.introductionOffset === frame2.offset,
+       "eval frame's introduction offset is current offset in older frame");
+    ok(source.introductionScript.source.element === script2DO,
+       "eval frame's introducer belongs to script2 element");
+
+    // The frame that called eval, in turn, should have no introduction
+    // information. (In the future, we certainly could point at the call
+    // that inserted the script element into the document; if that happens,
+    // we can update this test.)
+    ok(frame2.script.source.introductionType === undefined,
+       "older frame has no introduction type");
+    ok(frame2.script.source.introductionScript === undefined,
+       "older frame has no introduction script");
+    ok(frame2.script.source.introductionOffset === null,
+       "older frame has no introduction offset");
+
+    SimpleTest.finish();
+  }
+}
+</script>
+</pre>
+</body>
+</html>