servo / servo

Servo, the embeddable, independent, memory-safe, modular, parallel web rendering engine
https://servo.org
Mozilla Public License 2.0
27.99k stars 2.99k forks source link

Add a flag to make debugging compiled event handlers easier #15051

Open jdm opened 7 years ago

jdm commented 7 years ago

This is a patch that I wrote which makes the following code easier to debug:

some_element.setAttribute('onclick', '/* some inline JS that can throw an exception */');

Without that patch, the error is reported as occurring on line N of the page that is evaluating the setAttribute call, where N is the line number inside the inline JS that triggers the exception (which is often 1 since there are rarely line breaks). With the patch, the error is reported as occurring on line M+N of the file that contains the setAttribute call, where M is the line number that contains the setAttribute call, which means it's much easier to relate the exception to the actual code that threw it.

However! This is not clearly an improvement in all cases, because it could appear that the setAttribute call is the source of the exception. Additionally, consider the following:

var s = "/* inline JS that throws an exception */";
some_element.setAttribute('onclick', s);

This patch is only a marginal improvement improvement in cases where the inline JS source is not present in the line that is reported as the cause, because it still requires backtracking.

A WPT test of this case reveals that Firefox chooses to report the line starting at 1, while Chrome appears to try to report the line matching the end of the script block that created the uncompiled event handler. I'm inclined to make this behaviour optional in Servo, as I believe it can help track down web compatibility problems.

Thoughts, @Ms2ger?

jdm commented 7 years ago

As an example of how Chrome's behaviour (which this patch implements with more accuracy) can go wrong:

<!doctype html>
<html>
 <head>
  <title>window.onerror - runtime error in compiled event handler</title>
  <script src="/resources/testharness.js"></script>
  <script src="/resources/testharnessreport.js"></script>
 </head>
 <body>
  <div id=log></div>
  <script>
    setup({allow_uncaught_exception:true});
    var t = async_test();
    addEventListener('error', t.step_func_done(function(e) {
      assert_true(e instanceof ErrorEvent);
      assert_equals(e.filename, location.href);
      assert_equals(e.lineno, 4);
    }));
  </script>
  <div id="eventtarget"></div>
  <script>
    t.step(function() {
      var div = document.querySelector('#eventtarget');
      div.setAttribute('onclick', '\n\n\nundefined_variable');
      div.dispatchEvent(new Event('click'));
    });
  </script>
</body>
</html>

In Chrome, this reports an error on line 29 of the HTML file.

Ms2ger commented 7 years ago

Perhaps we should just always use line M? None of the solutions here is particularly great.

jdm commented 7 years ago

Hmm, the spec actually calls out that the script location that sets event handler content attributes should be stored: https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes

jdm commented 4 years ago

A new attempt at a patch:

diff --git a/components/script/dom/eventtarget.rs b/components/script/dom/eventtarget.rs
index b65fcc6baf15..c62376033fc6 100644
--- a/components/script/dom/eventtarget.rs
+++ b/components/script/dom/eventtarget.rs
@@ -42,7 +42,6 @@ use js::rust::wrappers::CompileFunction;
 use js::rust::{CompileOptionsWrapper, RootedObjectVectorWrapper};
 use libc::c_char;
 use servo_atoms::Atom;
-use servo_url::ServoUrl;
 use std::collections::hash_map::Entry::{Occupied, Vacant};
 use std::collections::HashMap;
 use std::default::Default;
@@ -81,7 +80,7 @@ pub enum ListenerPhase {
 #[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)]
 struct InternalRawUncompiledHandler {
     source: DOMString,
-    url: ServoUrl,
+    filename: String,
     line: usize,
 }

@@ -436,7 +435,7 @@ impl EventTarget {
     /// <https://html.spec.whatwg.org/multipage/#event-handler-attributes:event-handler-content-attributes-3>
     pub fn set_event_handler_uncompiled(
         &self,
-        url: ServoUrl,
+        filename: String,
         line: usize,
         ty: &str,
         source: DOMString,
@@ -444,7 +443,7 @@ impl EventTarget {
         let handler = InternalRawUncompiledHandler {
             source: source,
             line: line,
-            url: url,
+            filename,
         };
         self.set_inline_event_listener(
             Atom::from(ty),
@@ -494,7 +493,7 @@ impl EventTarget {

         // Step 3.9

-        let url_serialized = CString::new(handler.url.to_string()).unwrap();
+        let filename = CString::new(handler.filename).unwrap();
         let name = CString::new(&**ty).unwrap();

         // Step 3.9, subsection ParameterList
@@ -516,9 +515,8 @@ impl EventTarget {
         };

         let cx = window.get_cx();
-        let options = unsafe {
-            CompileOptionsWrapper::new(*cx, url_serialized.as_ptr(), handler.line as u32)
-        };
+        let options =
+            unsafe { CompileOptionsWrapper::new(*cx, filename.as_ptr(), handler.line as u32) };

         // Step 3.9, subsection Scope steps 1-6
         let scopechain = RootedObjectVectorWrapper::new(*cx);
diff --git a/components/script/dom/htmlbodyelement.rs b/components/script/dom/htmlbodyelement.rs
index 775af6a686c5..053973804d6d 100644
--- a/components/script/dom/htmlbodyelement.rs
+++ b/components/script/dom/htmlbodyelement.rs
@@ -18,6 +18,7 @@ use cssparser::RGBA;
 use dom_struct::dom_struct;
 use embedder_traits::EmbedderMsg;
 use html5ever::{LocalName, Prefix};
+use js::rust::describe_scripted_caller;
 use servo_url::ServoUrl;
 use style::attr::AttrValue;

@@ -170,6 +171,7 @@ impl VirtualMethods for HTMLBodyElement {
         }
     }

+    #[allow(unsafe_code)]
     fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
         let do_super_mutate = match (attr.local_name(), mutation) {
             (name, AttributeMutation::Set(_)) if name.starts_with("on") => {
@@ -196,10 +198,11 @@ impl VirtualMethods for HTMLBodyElement {
                     &local_name!("onunload") |
                     &local_name!("onerror") => {
                         let evtarget = window.upcast::<EventTarget>(); // forwarded event
-                        let source_line = 1; //TODO(#9604) obtain current JS execution line
+                        let caller = unsafe { describe_scripted_caller(*window.get_cx()) }
+                            .unwrap_or_default();
                         evtarget.set_event_handler_uncompiled(
-                            window.get_url(),
-                            source_line,
+                            caller.filename,
+                            caller.line as usize,
                             &name[2..],
                             DOMString::from((**attr.value()).to_owned()),
                         );
diff --git a/components/script/dom/htmlelement.rs b/components/script/dom/htmlelement.rs
index 4c4d86f16423..462274041376 100644
--- a/components/script/dom/htmlelement.rs
+++ b/components/script/dom/htmlelement.rs
@@ -12,6 +12,7 @@ use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
 use crate::dom::bindings::error::{Error, ErrorResult};
 use crate::dom::bindings::inheritance::Castable;
 use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
+use crate::dom::bindings::reflector::DomObject;
 use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
 use crate::dom::bindings::str::DOMString;
 use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner};
@@ -33,6 +34,7 @@ use crate::dom::text::Text;
 use crate::dom::virtualmethods::VirtualMethods;
 use dom_struct::dom_struct;
 use html5ever::{LocalName, Prefix};
+use js::rust::describe_scripted_caller;
 use script_layout_interface::message::QueryMsg;
 use std::collections::HashSet;
 use std::default::Default;
@@ -841,15 +843,17 @@ impl VirtualMethods for HTMLElement {
         Some(self.upcast::<Element>() as &dyn VirtualMethods)
     }

+    #[allow(unsafe_code)]
     fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
         self.super_type().unwrap().attribute_mutated(attr, mutation);
         match (attr.local_name(), mutation) {
             (name, AttributeMutation::Set(_)) if name.starts_with("on") => {
                 let evtarget = self.upcast::<EventTarget>();
-                let source_line = 1; //TODO(#9604) get current JS execution line
+                let caller = unsafe { describe_scripted_caller(*self.global().get_cx()) }
+                    .unwrap_or_default();
                 evtarget.set_event_handler_uncompiled(
-                    window_from_node(self).get_url(),
-                    source_line,
+                    caller.filename,
+                    caller.line as usize,
                     &name[2..],
                     // FIXME(ajeffrey): Convert directly from AttrValue to DOMString
                     DOMString::from(&**attr.value()),

This caused:

  ▶ Unexpected subtest result in /html/webappapis/scripting/processing-model-2/runtime-error-in-attribute.html:
  │ FAIL [expected PASS] window.onerror - runtime error in attribute
  │   → assert_equals: second arg expected "http://web-platform.test:8000/html/webappapis/scripting/processing-model-2/runtime-error-in-attribute.html" but got ""
  │ 
  │ window.onerror<@http://web-platform.test:8000/html/webappapis/scripting/processing-model-2/runtime-error-in-attribute.html:20:22
  │ Test.prototype.step@http://web-platform.test:8000/resources/testharness.js:1988:25
  │ Test.prototype.step_func/<@http://web-platform.test:8000/resources/testharness.js:2013:35
  │ @http://web-platform.test:8000/html/webappapis/scripting/processing-model-2/runtime-error-in-attribute.html:29:37
  │ Test.prototype.step@http://web-platform.test:8000/resources/testharness.js:1988:25
  └ @http://web-platform.test:8000/html/webappapis/scripting/processing-model-2/runtime-error-in-attribute.html:26:7