Finding Errors in Dynamically Created Javascript Source.

John J. Barton
IBM Almaden Research Center.

Here I will describe a way to use the current jsdIDebuggerService API in Mozilla to debug dynamically evaluated Javascript. The Mozilla Javascript engine (SpiderMonkey) and its debugger API were not design to support eval() debugging. Dynamically evaluated code appears in the debugger as mysterious or non-existent lines in files containing the eval() call. Single stepping and breakpoints are largely not usable. This means debuggers fail for dynamic code. Since Web 2.0 relies on client-side analysis of server-provided data (eg JSON and AJAX) and, increasingly, on client-side specification and download of server-provided code (eg Dojo), eval() is increasingly important. The problem is documented in Bugzilla as Bug #307984. We need a fix.

Incorrect Error Messages from Firefox

Consider the following Javascript code to load the file payload.js:



If the file payload.js is as follows:



then an error will be reported:
Error: b is not defined
However the error will not be reported at line 4 of payload.js, but rather at line 8 of dynloader.js. Changing lines in dynloader.js around line 8 won't change the error message or its reported location, though adding lines above line 5 will.

While this seems at first to be a Javascript implementation error, an alternative implementation is not at all obvious. The error in this case does not occur at a "file" and "line"; it occurs at a line in a buffer created dynamically by the programmer's code. A more accurate error message would give the line as 5 but report the file as "unknown". This would not be more helpful to programmers. The current solution reports the error one line before the line in the file containing the "eval()" call (line 4 in dynloader.js) plus the line number of the error in the eval buffer (line 4 in payload.js) giving line 8.

A run-time debugger can be more helpful by reporting the buffer of source code containing the error and the line of the error relative to that buffer. In principle this is the job of the Javascript engine, but changing that code and distributing it is hard. Here I will show how the Javascript debugger interface, jsdIDebuggerService can be used to support eval() debugging. (I'll call this interface jsd for short).

Buffer-relative Errors in Firefox (SpiderMonkey with jsd debugger interface)

To give a developer better information on the source of errors in eval() buffers we need to

  1. Detect when eval() is called
  2. Save the buffer so we can report errors relative to it
  3. Compute the buffer-relative line number when we get the error
The first step is required to avoid interfering with normal operations. The second one is needed because we don't have any source code files for eval() buffers.

The third is required because the SpiderMonkey engine records line numbers relative to the file of the source that calls eval() rather than relative to the source of the error.

Source, scripts, functions: new terminology

The SpiderMonkey engine and jsd work in terms of scripts. These objects represent either blocks of source like that enclosed in <script> tags or Javascript functions. The jsd documentation mentions "top-level" scripts, which are blocks of source, so I will use the term "leveled scripts" to refer to script objects representing source. The other kind of leveled script is an "eval-level" script, representing the source text evaluated dynamically by eval(). Leveled scripts define objects in the scope of their caller: top-level scripts define global objects like window properties while eval-level scripts define objects in the function that calls the eval(). All other scripts are "unleveled", meaning they are Javascript functions that create a new scope. Unleveled scripts always come from leveled scripts: during the compilation of leveled scripts, unleveled scripts are created. Any given script can be one of four types:
  1. top-level
  2. unleveled from top-level
  3. eval-level
  4. unleveled from eval-level
Current Mozilla debuggers don't support eval-level debugging, including unleveled scripts from eval-level. At the heart of the issue is the mapping of unleveled scripts to leveled scripts. The current jsd maps all scripts to the url that contains the top-level script (e.g. an html or js file). The eval-level scripts and unleveled scripts from eval-level are mapped to the url of top-level script or unleveled script from top-level that called the eval(). The line numbers simply start at the point of the eval() call. When debuggers use this information, users see the debugger step through lines of source in the top-level file that do not make sense.

Debugging eval() scripts

All debugging operations using jsd refer to the jsdIScript interface of scripts; programmers think in terms of source. Therefore to debug eval() code we need to know the eval-level scripts and their source.

Detecting eval() with Double Hooks

To begin our fix-up we need to get control when an eval() is called. There is no specific call back into the debugger API for eval(). Empirically, a sign for eval() is a stack frame with 1) an empty string as its function name and 2) a calling-frame above it on the stack. We'll call these frames "eval-level". (An empty string as a function name without a calling-frame is a "top-level" frame). The jsd scriptHook allows us to see each function after it is compiled so we can test each one to see if it has an empty string function name; scripts that fail to compile never trigger the scriptHook.

Therefore we need to work on two paths:
  1. Compile Eval-level
  2. Run Eval-level
Compilation errors in the eval() body trigger jsd.errorHook; if compile succeeds jsd.scriptHook.onScriptCreated is called. That gives us control to test the function name for blank names. However, neither jsd.errorHook nor jsd.scriptHook.onScriptCreated are passed the call stack, so we cannot analyze for the caling-frame. Therefore along both paths we have to pass control to a second debugger hook, one that is passed the stack.

Compile Eval-level

Along the compile-error path we return false from jsd.errorHook. This tells the Javascript engine to call the jsd.debugHook of type jsdIExecutionHook.

We do the rest of the work in jsd.debugHook. Here we examine the stack (the stack appears as if the jsd.errorHook function was called at the point of the error). The stack is passed to our errorHook as a Javascript object of type jsdIStackFrame; this is the top frame of the stack (up is newer frames). Each frame has a script property of type jsdIScript representing the active function in that frame. The functionName property of the script object identifies the function. This character string is commonly "anonymous" for today's Javascript styles, but the value for eval based frames seems to be a string with no characters "" (empty-string). According to the documentation, top-level functions also have empty names (meaning zero length but not null strings). Top-level functions contain Javascript inside script tags but outside any Javascript functions. These top-level un-named functions are easy to distinguish from un-name eval-level functions by simply seeing if the frame containing the empty-string-named function is top-most or not. (We don't know if other functions may also give empty-string for functionName, but it seems unlikely).

Run Eval-level

For run-time correction we need to trap the eval() before it runs and mark all of the functions that it defines as coming from the its expression body. We can trap the eval() by looking for scripts (i.e. functions) with empty-string names in a jsd.scriptHook.onScriptCreated. We need to distinguish eval-level from top-level functions, but onScriptCreated has no stack to check. Therefore when we see an empty-string-named function, we set a breakpoint on it at PC (program counter) zero triggering a call to jsd.breakpointHook. (Such breakpoints cannot interfere with the operation of the conventional debugger since the conventional debugger cannot see the eval code anyway.) As soon as we return from onScriptCreated, the function runs and breaks immediately into our jsd.breakpointHook; we can undo our breakpoint. We now have the stack frame of the just-running empty-string function. If it has a caller, it must be an eval-level function.

We come back to the problem of marking all of eval-defined functions when we look at saving the source of the eval body.

Summary of eval() detection

Just to summarize, we used two hook functions along each of two paths (hence "double hook"). The first hook in each pair is triggered for special circumstances: errorHook for errors and onScriptCreated for script creation. We use the first hook to call the second hook. In each case the second hook is an jsdIExecutionHook that gets passed a stack. We can look through this stack for a frame with a script named with the empty-string and test if this frame is top or not. In the latter case we have an eval-level frame.

Save the eval buffer

Ultimately the source we want is the string passed as expr in eval(expr). Fortunately jsdIStackFrame provides us with a key tool: it supports a special eval() call. We can use it to get the source code by passing "var src = new String("+evalExpr+")" and storing the result src as our source. This is all that is needed for the compile path.

Binding unleveled functions created by eval() to eval-level source buffers

For the run-time path we need to know when functions were created by our eval(). Empirically all unleveled scripts are emitted from the javascript engine as their leveled script is compiled. Also empirically, a leveled script is compiled completely before any other action by the engine. So from the debugger's point of view, unleveled scripts start appearing in the onScriptCreated callback. Then, when the leveled script compilation is complete, it appears in the onScriptCreated callback. We can queue all the scripts until the leveled script appears. If this leveled script is top-level, we discard the queue. If the leveled script is eval-level, then we create a map assigning each queued unleveled script to the corresponding eval-level script. By consulting this table instead of the built-in mapping of scripts to (erroneous) points in the top-level file, the debugger can correctly represent the state of the machine to programmers.

Assign names for eval() source

Top-level scripts can be named by the file (url) containing their source. Unleveled scripts can be named by the functions they define. (Anonymous Javascript functions can be named by heuristics like the variable name foo in var foo = function() {...};). What name shall we assign to eval-level scripts? In Bug #307984 the proposal was to name eval code via a data: URI that encoded the source code and call site in the URI. This has the advantage that possession of the URI ensures the possession of the source and the name is as unique as the source. It has two disadvantages, the programmer gets no clues from the name as to the origin of the source and for systems like dojo the URI can be quite large and numerous.

Single Stepping and Setting Breakpoints in eval() Source

The double-hook technique allows us to report compile and run-time errors to programmers directly against the lines of code. This is a significant improvement over the current state. Ideally we would also like to apply the complete debugger functionality to the eval code, including single stepping and setting breakpoints. Breakpoints in jsd are set on scripts, but the eval-level and unleveled scripts from eval-level are deleted when they go out of scope. So the debugger has to reset breakpoints in the new script objects created for eval-level source each time we go through a debug-cycle.

Backtracing errors in XMLHttpRequest

At this point we debug eval code based on dynamic buffers. However developers work with code in files. Can we get closer?

In general, analysis of eval bodies requires dynamic (run-time) dependency analysis to allow backtracking to source. This is much more work, but we might be able to make progress with some heursitics for important special cases. For special eval-ed code that is dynamically loaded by XMLHttpRequest we would like to identify the the URL that was loaded to get the eval body, relate the URL to a file, and flag the source line. Further investigations will be needed to see what is possible along these lines.

Acknowledgements

Julian Cerruti introduced me to this problem; Adam Peller, James Ross, and timeless provided key bits of information. Robert Ginda's Venkman and Joe Hewitt's Firebug made this exploration possible.

Notes

Q: Why use debugHook(frame) rather than simply Components.stack?
A: The nsIStackFrame returned by Components.stack is a weaker cousin of the jsdIStackFrame. The latter gives access to the jsdIScript running in the frame, the this object, and the scope object of the frame.

© IBM Corp. 2007