Bug 904144 - Parse "//# sourceURL=..." directives and expose them on Debugger.Source; r=jimb
authorNick Fitzgerald <fitzgen@gmail.com>
Fri, 20 Sep 2013 14:57:09 -0700
changeset 148250 1f1d6e481cec71eaa61ab305735e39a89b8f455b
parent 148249 389e16afcbde9c79da69878b139381da69217ea2
child 148251 b2e48d9dd4732813283825728d13e4e4f292d3bc
push id25332
push usercbook@mozilla.com
push dateMon, 23 Sep 2013 11:07:56 +0000
treeherdermozilla-central@00bf153a66e4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs904144
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 904144 - Parse "//# sourceURL=..." directives and expose them on Debugger.Source; r=jimb
js/src/frontend/BytecodeCompiler.cpp
js/src/frontend/TokenStream.cpp
js/src/frontend/TokenStream.h
js/src/jit-test/tests/debug/Source-displayURL-deprecated.js
js/src/jit-test/tests/debug/Source-displayURL.js
js/src/js.msg
js/src/jsscript.cpp
js/src/jsscript.h
js/src/shell/js.cpp
js/src/vm/Debugger.cpp
--- a/js/src/frontend/BytecodeCompiler.cpp
+++ b/js/src/frontend/BytecodeCompiler.cpp
@@ -35,16 +35,26 @@ CheckLength(ExclusiveContext *cx, size_t
         if (cx->isJSContext())
             JS_ReportErrorNumber(cx->asJSContext(), js_GetErrorMessage, NULL, JSMSG_SOURCE_TOO_LONG);
         return false;
     }
     return true;
 }
 
 static bool
+SetSourceURL(ExclusiveContext *cx, TokenStream &tokenStream, ScriptSource *ss)
+{
+    if (tokenStream.hasSourceURL()) {
+        if (!ss->setSourceURL(cx, tokenStream.sourceURL()))
+            return false;
+    }
+    return true;
+}
+
+static bool
 SetSourceMap(ExclusiveContext *cx, TokenStream &tokenStream, ScriptSource *ss)
 {
     if (tokenStream.hasSourceMapURL()) {
         if (!ss->setSourceMapURL(cx, tokenStream.sourceMapURL()))
             return false;
     }
     return true;
 }
@@ -352,16 +362,19 @@ frontend::CompileScript(ExclusiveContext
             return NULL;
 
         parser.handler.freeTree(pn);
     }
 
     if (!MaybeCheckEvalFreeVariables(cx, evalCaller, scopeChain, parser, pc.ref()))
         return NULL;
 
+    if (!SetSourceURL(cx, parser.tokenStream, ss))
+        return NULL;
+
     if (!SetSourceMap(cx, parser.tokenStream, ss))
         return NULL;
 
     /*
      * Source map URLs passed as a compile option (usually via a HTTP source map
      * header) override any source map urls passed as comment pragmas.
      */
     if (options.sourceMapURL) {
@@ -573,16 +586,19 @@ CompileFunctionBody(JSContext *cx, Mutab
 
         if (!EmitFunctionScript(cx, &funbce, fn->pn_body))
             return false;
     } else {
         fun.set(fn->pn_funbox->function());
         JS_ASSERT(IsAsmJSModuleNative(fun->native()));
     }
 
+    if (!SetSourceURL(cx, parser.tokenStream, ss))
+        return false;
+
     if (!SetSourceMap(cx, parser.tokenStream, ss))
         return false;
 
     if (!sct.complete())
         return false;
 
     return true;
 }
--- a/js/src/frontend/TokenStream.cpp
+++ b/js/src/frontend/TokenStream.cpp
@@ -269,16 +269,17 @@ TokenStream::TokenStream(ExclusiveContex
     cursor(),
     lookahead(),
     lineno(options.lineno),
     flags(),
     linebase(base - options.column),
     prevLinebase(NULL),
     userbuf(cx, base - options.column, length + options.column), // See comment below
     filename(options.filename),
+    sourceURL_(NULL),
     sourceMapURL_(NULL),
     tokenbuf(cx),
     cx(cx),
     originPrincipals(options.originPrincipals()),
     strictModeGetter(smg),
     tokenSkip(cx, &tokens),
     linebaseSkip(cx, &linebase),
     prevLinebaseSkip(cx, &prevLinebase)
@@ -328,16 +329,17 @@ TokenStream::TokenStream(ExclusiveContex
 }
 
 #ifdef _MSC_VER
 #pragma warning(pop)
 #endif
 
 TokenStream::~TokenStream()
 {
+    js_free(sourceURL_);
     js_free(sourceMapURL_);
 
     JS_ASSERT_IF(originPrincipals, originPrincipals->refcount);
 }
 
 // Use the fastest available getc.
 #if defined(HAVE_GETC_UNLOCKED)
 # define fast_getc getc_unlocked
@@ -802,66 +804,101 @@ CharsMatch(const jschar *p, const char *
     while (*q) {
         if (*p++ != *q++)
             return false;
     }
     return true;
 }
 
 bool
-TokenStream::getSourceMappingURL(bool isMultiline, bool shouldWarnDeprecated)
+TokenStream::getDirectives(bool isMultiline, bool shouldWarnDeprecated)
 {
-    // Match comments of the form "//# sourceMappingURL=<url>" or
-    // "/\* //# sourceMappingURL=<url> *\/"
+    // Match directive comments used in debugging, such as "//# sourceURL" and
+    // "//# sourceMappingURL". Use of "//@" instead of "//#" is deprecated.
     //
     // To avoid a crashing bug in IE, several JavaScript transpilers wrap single
     // line comments containing a source mapping URL inside a multiline
     // comment. To avoid potentially expensive lookahead and backtracking, we
     // only check for this case if we encounter a '#' character.
+
+    if (!getSourceURL(isMultiline, shouldWarnDeprecated))
+        return false;
+    if (!getSourceMappingURL(isMultiline, shouldWarnDeprecated))
+        return false;
+
+    return true;
+}
+
+bool
+TokenStream::getDirective(bool isMultiline, bool shouldWarnDeprecated,
+                          const char *directive, int directiveLength,
+                          const char *errorMsgPragma, jschar **destination) {
+    JS_ASSERT(directiveLength <= 18);
     jschar peeked[18];
     int32_t c;
 
-    if (peekChars(18, peeked) && CharsMatch(peeked, " sourceMappingURL=")) {
-        if (shouldWarnDeprecated && !reportWarning(JSMSG_DEPRECATED_SOURCE_MAP)) {
+    if (peekChars(directiveLength, peeked) && CharsMatch(peeked, directive)) {
+        if (shouldWarnDeprecated &&
+            !reportWarning(JSMSG_DEPRECATED_PRAGMA, errorMsgPragma))
             return false;
-        }
 
-        skipChars(18);
+        skipChars(directiveLength);
         tokenbuf.clear();
 
         while ((c = peekChar()) && c != EOF && !IsSpaceOrBOM2(c)) {
             getChar();
-            // Source mapping URLs can occur in both single- and multiline
-            // comments. If we're currently inside a multiline comment, we also
-            // need to recognize multiline comment terminators.
+            // Debugging directives can occur in both single- and multi-line
+            // comments. If we're currently inside a multi-line comment, we also
+            // need to recognize multi-line comment terminators.
             if (isMultiline && c == '*' && peekChar() == '/') {
                 ungetChar('*');
                 break;
             }
             tokenbuf.append(c);
         }
 
         if (tokenbuf.empty())
-            // The source map's URL was missing, but not quite an exception that
-            // we should stop and drop everything for, though.
+            // The directive's URL was missing, but this is not quite an
+            // exception that we should stop and drop everything for.
             return true;
 
-        size_t sourceMapURLLength = tokenbuf.length();
+        size_t length = tokenbuf.length();
 
-        js_free(sourceMapURL_);
-        sourceMapURL_ = cx->pod_malloc<jschar>(sourceMapURLLength + 1);
-        if (!sourceMapURL_)
+        js_free(*destination);
+        *destination = cx->pod_malloc<jschar>(length + 1);
+        if (!*destination)
             return false;
 
-        PodCopy(sourceMapURL_, tokenbuf.begin(), sourceMapURLLength);
-        sourceMapURL_[sourceMapURLLength] = '\0';
+        PodCopy(*destination, tokenbuf.begin(), length);
+        (*destination)[length] = '\0';
     }
+
     return true;
 }
 
+bool
+TokenStream::getSourceURL(bool isMultiline, bool shouldWarnDeprecated)
+{
+    // Match comments of the form "//# sourceURL=<url>" or
+    // "/\* //# sourceURL=<url> *\/"
+
+    return getDirective(isMultiline, shouldWarnDeprecated, " sourceURL=", 11,
+                        "sourceURL", &sourceURL_);
+}
+
+bool
+TokenStream::getSourceMappingURL(bool isMultiline, bool shouldWarnDeprecated)
+{
+    // Match comments of the form "//# sourceMappingURL=<url>" or
+    // "/\* //# sourceMappingURL=<url> *\/"
+
+    return getDirective(isMultiline, shouldWarnDeprecated, " sourceMappingURL=", 18,
+                        "sourceMappingURL", &sourceMapURL_);
+}
+
 JS_ALWAYS_INLINE Token *
 TokenStream::newToken(ptrdiff_t adjust)
 {
     cursor = (cursor + 1) & ntokensMask;
     Token *tp = &tokens[cursor];
     tp->pos.begin = userbuf.addressOfNextRawChar() + adjust - userbuf.base();
 
     // NOTE: tp->pos.end is not set until the very end of getTokenInternal().
@@ -1501,17 +1538,18 @@ TokenStream::getTokenInternal(Modifier m
         tp->type = matchChar('=') ? TOK_MULASSIGN : TOK_MUL;
         goto out;
 
       case '/':
         // Look for a single-line comment.
         if (matchChar('/')) {
             c = peekChar();
             if (c == '@' || c == '#') {
-                if (!getSourceMappingURL(false, getChar() == '@'))
+                bool shouldWarn = getChar() == '@';
+                if (!getDirectives(false, shouldWarn))
                     goto error;
             }
 
         skipline:
             while ((c = getChar()) != EOF && c != '\n')
                 continue;
             ungetChar(c);
             cursor = (cursor - 1) & ntokensMask;
@@ -1519,17 +1557,18 @@ TokenStream::getTokenInternal(Modifier m
         }
 
         // Look for a multi-line comment.
         if (matchChar('*')) {
             unsigned linenoBefore = lineno;
             while ((c = getChar()) != EOF &&
                    !(c == '*' && matchChar('/'))) {
                 if (c == '@' || c == '#') {
-                    if (!getSourceMappingURL(true, c == '@'))
+                    bool shouldWarn = c == '@';
+                    if (!getDirectives(true, shouldWarn))
                         goto error;
                 }
             }
             if (c == EOF) {
                 reportError(JSMSG_UNTERMINATED_COMMENT);
                 goto error;
             }
             if (linenoBefore != lineno)
--- a/js/src/frontend/TokenStream.h
+++ b/js/src/frontend/TokenStream.h
@@ -591,16 +591,24 @@ class MOZ_STACK_CLASS TokenStream
     void tell(Position *);
     void seek(const Position &pos);
     void seek(const Position &pos, const TokenStream &other);
 
     size_t positionToOffset(const Position &pos) const {
         return pos.buf - userbuf.base();
     }
 
+    bool hasSourceURL() const {
+        return sourceURL_ != NULL;
+    }
+
+    jschar *sourceURL() {
+        return sourceURL_;
+    }
+
     bool hasSourceMapURL() const {
         return sourceMapURL_ != NULL;
     }
 
     jschar *sourceMapURL() {
         return sourceMapURL_;
     }
 
@@ -798,16 +806,22 @@ class MOZ_STACK_CLASS TokenStream
     int32_t getCharIgnoreEOL();
     void ungetChar(int32_t c);
     void ungetCharIgnoreEOL(int32_t c);
     Token *newToken(ptrdiff_t adjust);
     bool peekUnicodeEscape(int32_t *c);
     bool matchUnicodeEscapeIdStart(int32_t *c);
     bool matchUnicodeEscapeIdent(int32_t *c);
     bool peekChars(int n, jschar *cp);
+
+    bool getDirectives(bool isMultiline, bool shouldWarnDeprecated);
+    bool getDirective(bool isMultiline, bool shouldWarnDeprecated,
+                      const char *directive, int directiveLength,
+                      const char *errorMsgPragma, jschar **destination);
+    bool getSourceURL(bool isMultiline, bool shouldWarnDeprecated);
     bool getSourceMappingURL(bool isMultiline, bool shouldWarnDeprecated);
 
     // |expect| cannot be an EOL char.
     bool matchChar(int32_t expect) {
         MOZ_ASSERT(!TokenBuf::isRawEOLChar(expect));
         return JS_LIKELY(userbuf.hasRawChars()) &&
                userbuf.matchRawChar(expect);
     }
@@ -838,16 +852,17 @@ class MOZ_STACK_CLASS TokenStream
     unsigned            cursor;             // index of last parsed token
     unsigned            lookahead;          // count of lookahead tokens
     unsigned            lineno;             // current line number
     Flags               flags;              // flags -- see above
     const jschar        *linebase;          // start of current line;  points into userbuf
     const jschar        *prevLinebase;      // start of previous line;  NULL if on the first line
     TokenBuf            userbuf;            // user input buffer
     const char          *filename;          // input filename or null
+    jschar              *sourceURL_;        // the user's requested source URL or null
     jschar              *sourceMapURL_;     // source map's filename or null
     CharBuffer          tokenbuf;           // current token string buffer
     bool                maybeEOL[256];      // probabilistic EOL lookup table
     bool                maybeStrSpecial[256];   // speeds up string scanning
     uint8_t             isExprEnding[TOK_LIMIT];// which tokens definitely terminate exprs?
     ExclusiveContext    *const cx;
     JSPrincipals        *const originPrincipals;
     StrictModeGetter    *strictModeGetter;  // used to test for strict mode
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Source-displayURL-deprecated.js
@@ -0,0 +1,26 @@
+/* -*- Mode: javascript; js-indent-level: 4; -*- */
+// Source.prototype.sourceURL can be a string or null.
+
+let g = newGlobal('new-compartment');
+let dbg = new Debugger;
+let gw = dbg.addDebuggee(g);
+
+function getDisplayURL() {
+    let fw = gw.makeDebuggeeValue(g.f);
+    return fw.script.source.displayURL;
+}
+
+// Comment pragmas
+g.evaluate('function f() {}\n' +
+           '//@ sourceURL=file:///var/quux.js');
+assertEq(getDisplayURL(), 'file:///var/quux.js');
+
+g.evaluate('function f() {}\n' +
+           '/*//@ sourceURL=file:///var/quux.js*/');
+assertEq(getDisplayURL(), 'file:///var/quux.js');
+
+g.evaluate('function f() {}\n' +
+           '/*\n' +
+           '//@ sourceURL=file:///var/quux.js\n' +
+           '*/');
+assertEq(getDisplayURL(), 'file:///var/quux.js');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Source-displayURL.js
@@ -0,0 +1,71 @@
+/* -*- Mode: javascript; js-indent-level: 4; -*- */
+// Source.prototype.sourceURL can be a string or null.
+
+let g = newGlobal('new-compartment');
+let dbg = new Debugger;
+let gw = dbg.addDebuggee(g);
+
+function getDisplayURL() {
+    let fw = gw.makeDebuggeeValue(g.f);
+    return fw.script.source.displayURL;
+}
+
+// Without a source url
+g.evaluate("function f(x) { return 2*x; }");
+assertEq(getDisplayURL(), null);
+
+// With a source url
+g.evaluate("function f(x) { return 2*x; }", {sourceURL: 'file:///var/foo.js'});
+assertEq(getDisplayURL(), 'file:///var/foo.js');
+
+// Nested functions
+let fired = false;
+dbg.onDebuggerStatement = function (frame) {
+    fired = true;
+    assertEq(frame.script.source.displayURL, 'file:///var/bar.js');
+};
+g.evaluate('(function () { (function () { debugger; })(); })();',
+           {sourceURL: 'file:///var/bar.js'});
+assertEq(fired, true);
+
+// Comment pragmas
+g.evaluate('function f() {}\n' +
+           '//# sourceURL=file:///var/quux.js');
+assertEq(getDisplayURL(), 'file:///var/quux.js');
+
+g.evaluate('function f() {}\n' +
+           '/*//# sourceURL=file:///var/quux.js*/');
+assertEq(getDisplayURL(), 'file:///var/quux.js');
+
+g.evaluate('function f() {}\n' +
+           '/*\n' +
+           '//# sourceURL=file:///var/quux.js\n' +
+           '*/');
+assertEq(getDisplayURL(), 'file:///var/quux.js');
+
+// Spaces are disallowed by the URL spec (they should have been
+// percent-encoded).
+g.evaluate('function f() {}\n' +
+           '//# sourceURL=http://example.com/has illegal spaces');
+assertEq(getDisplayURL(), 'http://example.com/has');
+
+// When the URL is missing, we don't set the sourceMapURL and we don't skip the
+// next line of input.
+g.evaluate('function f() {}\n' +
+           '//# sourceURL=\n' +
+           'function z() {}');
+assertEq(getDisplayURL(), null);
+assertEq('z' in g, true);
+
+// The last comment pragma we see should be the one which sets the displayURL.
+g.evaluate('function f() {}\n' +
+           '//# sourceURL=http://example.com/foo.js\n' +
+           '//# sourceURL=http://example.com/bar.js');
+assertEq(getDisplayURL(), 'http://example.com/bar.js');
+
+// With both a comment and the evaluate option.
+g.evaluate('function f() {}\n' +
+           '//# sourceURL=http://example.com/foo.js',
+           {sourceMapURL: 'http://example.com/bar.js'});
+assertEq(getDisplayURL(), 'http://example.com/foo.js');
+
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -348,17 +348,17 @@ MSG_DEF(JSMSG_DEBUG_VARIABLE_NOT_FOUND, 
 MSG_DEF(JSMSG_PARAMETER_AFTER_REST,   295, 0, JSEXN_SYNTAXERR, "parameter after rest parameter")
 MSG_DEF(JSMSG_NO_REST_NAME,           296, 0, JSEXN_SYNTAXERR, "no parameter name after ...")
 MSG_DEF(JSMSG_ARGUMENTS_AND_REST,     297, 0, JSEXN_SYNTAXERR, "'arguments' object may not be used in conjunction with a rest parameter")
 MSG_DEF(JSMSG_FUNCTION_ARGUMENTS_AND_REST, 298, 0, JSEXN_ERR, "the 'arguments' property of a function with a rest parameter may not be used")
 MSG_DEF(JSMSG_REST_WITH_DEFAULT,      299, 0, JSEXN_SYNTAXERR, "rest parameter may not have a default")
 MSG_DEF(JSMSG_NONDEFAULT_FORMAL_AFTER_DEFAULT, 300, 0, JSEXN_SYNTAXERR, "parameter(s) with default followed by parameter without default")
 MSG_DEF(JSMSG_YIELD_IN_DEFAULT,       301, 0, JSEXN_SYNTAXERR, "yield in default expression")
 MSG_DEF(JSMSG_INTRINSIC_NOT_DEFINED,  302, 1, JSEXN_REFERENCEERR, "no intrinsic function {0}")
-MSG_DEF(JSMSG_ALREADY_HAS_SOURCE_MAP_URL, 303, 1, JSEXN_ERR,      "{0} is being assigned a source map URL, but already has one")
+MSG_DEF(JSMSG_ALREADY_HAS_PRAGMA, 303, 2, JSEXN_ERR,      "{0} is being assigned a {1}, but already has one")
 MSG_DEF(JSMSG_PAR_ARRAY_BAD_ARG,      304, 1, JSEXN_RANGEERR, "invalid ParallelArray{0} argument")
 MSG_DEF(JSMSG_PAR_ARRAY_BAD_PARTITION, 305, 0, JSEXN_ERR, "argument must be divisible by outermost dimension")
 MSG_DEF(JSMSG_PAR_ARRAY_REDUCE_EMPTY, 306, 0, JSEXN_ERR, "cannot reduce ParallelArray object whose outermost dimension is empty")
 MSG_DEF(JSMSG_PAR_ARRAY_ALREADY_FLAT, 307, 0, JSEXN_ERR, "cannot flatten 1-dimensional ParallelArray object")
 MSG_DEF(JSMSG_PAR_ARRAY_SCATTER_CONFLICT, 308, 0, JSEXN_ERR, "no conflict resolution function provided")
 MSG_DEF(JSMSG_PAR_ARRAY_SCATTER_BOUNDS, 309, 0, JSEXN_ERR, "index in scatter vector out of bounds")
 MSG_DEF(JSMSG_CANT_REPORT_NC_AS_NE,   310, 0, JSEXN_TYPEERR, "proxy can't report a non-configurable own property as non-existent")
 MSG_DEF(JSMSG_CANT_REPORT_E_AS_NE,    311, 0, JSEXN_TYPEERR, "proxy can't report an existing own property as non-existent on a non-extensible object")
@@ -395,17 +395,17 @@ MSG_DEF(JSMSG_USE_ASM_DIRECTIVE_FAIL, 34
 MSG_DEF(JSMSG_USE_ASM_TYPE_FAIL,      342, 1, JSEXN_TYPEERR, "asm.js type error: {0}")
 MSG_DEF(JSMSG_USE_ASM_LINK_FAIL,      343, 1, JSEXN_TYPEERR, "asm.js link error: {0}")
 MSG_DEF(JSMSG_USE_ASM_TYPE_OK,        344, 1, JSEXN_ERR,     "successfully compiled asm.js code ({0})")
 MSG_DEF(JSMSG_BAD_ARROW_ARGS,         345, 0, JSEXN_SYNTAXERR, "invalid arrow-function arguments (parentheses around the arrow-function may help)")
 MSG_DEF(JSMSG_YIELD_IN_ARROW,         346, 0, JSEXN_SYNTAXERR, "arrow function may not contain yield")
 MSG_DEF(JSMSG_WRONG_VALUE,            347, 2, JSEXN_ERR, "expected {0} but found {1}")
 MSG_DEF(JSMSG_PAR_ARRAY_SCATTER_BAD_TARGET, 348, 1, JSEXN_ERR, "target for index {0} is not an integer")
 MSG_DEF(JSMSG_SELFHOSTED_UNBOUND_NAME,349, 0, JSEXN_TYPEERR, "self-hosted code may not contain unbound name lookups")
-MSG_DEF(JSMSG_DEPRECATED_SOURCE_MAP,  350, 0, JSEXN_SYNTAXERR, "Using //@ to indicate source map URL pragmas is deprecated. Use //# instead")
+MSG_DEF(JSMSG_DEPRECATED_PRAGMA,  350, 1, JSEXN_SYNTAXERR, "Using //@ to indicate {0} pragmas is deprecated. Use //# instead")
 MSG_DEF(JSMSG_BAD_DESTRUCT_ASSIGN,    351, 1, JSEXN_SYNTAXERR, "can't assign to {0} using destructuring assignment")
 MSG_DEF(JSMSG_TYPEDOBJECT_ARRAYTYPE_BAD_ARGS, 352, 0, JSEXN_ERR, "Invalid arguments")
 MSG_DEF(JSMSG_TYPEDOBJECT_BINARYARRAY_BAD_INDEX, 353, 0, JSEXN_RANGEERR, "invalid or out-of-range index")
 MSG_DEF(JSMSG_TYPEDOBJECT_STRUCTTYPE_BAD_ARGS, 354, 0, JSEXN_RANGEERR, "invalid field descriptor")
 MSG_DEF(JSMSG_TYPEDOBJECT_NOT_BINARYSTRUCT,   355, 1, JSEXN_TYPEERR, "{0} is not a BinaryStruct")
 MSG_DEF(JSMSG_TYPEDOBJECT_SUBARRAY_INTEGER_ARG, 356, 1, JSEXN_ERR, "argument {0} must be an integer")
 MSG_DEF(JSMSG_TYPEDOBJECT_STRUCTTYPE_EMPTY_DESCRIPTOR, 357, 0, JSEXN_ERR, "field descriptor cannot be empty")
 MSG_DEF(JSMSG_TYPEDOBJECT_STRUCTTYPE_BAD_FIELD, 358, 1, JSEXN_ERR, "field {0} is not a valid BinaryData Type descriptor")
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -1203,16 +1203,17 @@ SourceCompressionTask::compress()
 }
 
 void
 ScriptSource::destroy()
 {
     JS_ASSERT(ready());
     adjustDataSize(0);
     js_free(filename_);
+    js_free(sourceURL_);
     js_free(sourceMapURL_);
     if (originPrincipals_)
         JS_DropPrincipals(TlsPerThreadData.get()->runtimeFromMainThread(), originPrincipals_);
     ready_ = false;
     js_free(this);
 }
 
 size_t
@@ -1293,16 +1294,41 @@ ScriptSource::performXDR(XDRState<mode> 
                 js_free(sourceMapURL_);
                 sourceMapURL_ = NULL;
             }
             return false;
         }
         sourceMapURL_[sourceMapURLLen] = '\0';
     }
 
+    uint8_t haveSourceURL = hasSourceURL();
+    if (!xdr->codeUint8(&haveSourceURL))
+        return false;
+
+    if (haveSourceURL) {
+        uint32_t sourceURLLen = (mode == XDR_DECODE) ? 0 : js_strlen(sourceURL_);
+        if (!xdr->codeUint32(&sourceURLLen))
+            return false;
+
+        if (mode == XDR_DECODE) {
+            size_t byteLen = (sourceURLLen + 1) * sizeof(jschar);
+            sourceURL_ = static_cast<jschar *>(xdr->cx()->malloc_(byteLen));
+            if (!sourceURL_)
+                return false;
+        }
+        if (!xdr->codeChars(sourceURL_, sourceURLLen)) {
+            if (mode == XDR_DECODE) {
+                js_free(sourceURL_);
+                sourceURL_ = NULL;
+            }
+            return false;
+        }
+        sourceURL_[sourceURLLen] = '\0';
+    }
+
     uint8_t haveFilename = !!filename_;
     if (!xdr->codeUint8(&haveFilename))
         return false;
 
     if (haveFilename) {
         const char *fn = filename();
         if (!xdr->codeCString(&fn))
             return false;
@@ -1326,30 +1352,62 @@ ScriptSource::setFilename(ExclusiveConte
     filename_ = cx->pod_malloc<char>(len);
     if (!filename_)
         return false;
     js_memcpy(filename_, filename, len);
     return true;
 }
 
 bool
+ScriptSource::setSourceURL(ExclusiveContext *cx, const jschar *sourceURL)
+{
+    JS_ASSERT(sourceURL);
+    if (hasSourceURL()) {
+        if (cx->isJSContext() &&
+            !JS_ReportErrorFlagsAndNumber(cx->asJSContext(), JSREPORT_WARNING,
+                                          js_GetErrorMessage, NULL,
+                                          JSMSG_ALREADY_HAS_PRAGMA, filename_,
+                                          "//# sourceURL"))
+        {
+            return false;
+        }
+    }
+    size_t len = js_strlen(sourceURL) + 1;
+    if (len == 1)
+        return true;
+    sourceURL_ = js_strdup(cx, sourceURL);
+    if (!sourceURL_)
+        return false;
+    return true;
+}
+
+const jschar *
+ScriptSource::sourceURL()
+{
+    JS_ASSERT(hasSourceURL());
+    return sourceURL_;
+}
+
+bool
 ScriptSource::setSourceMapURL(ExclusiveContext *cx, const jschar *sourceMapURL)
 {
     JS_ASSERT(sourceMapURL);
     if (hasSourceMapURL()) {
         if (cx->isJSContext() &&
-            !JS_ReportErrorFlagsAndNumber(cx->asJSContext(), JSREPORT_WARNING, js_GetErrorMessage,
-                                          NULL, JSMSG_ALREADY_HAS_SOURCE_MAP_URL, filename_))
+            !JS_ReportErrorFlagsAndNumber(cx->asJSContext(), JSREPORT_WARNING,
+                                          js_GetErrorMessage, NULL,
+                                          JSMSG_ALREADY_HAS_PRAGMA, filename_,
+                                          "//# sourceMappingURL"))
         {
             return false;
         }
     }
 
     size_t len = js_strlen(sourceMapURL) + 1;
-    if (len == 1) 
+    if (len == 1)
         return true;
     sourceMapURL_ = js_strdup(cx, sourceMapURL);
     if (!sourceMapURL_)
         return false;
     return true;
 }
 
 const jschar *
--- a/js/src/jsscript.h
+++ b/js/src/jsscript.h
@@ -299,32 +299,34 @@ class ScriptSource
         // this union is adjustDataSize(). Don't do it elsewhere.
         jschar *source;
         unsigned char *compressed;
     } data;
     uint32_t refs;
     uint32_t length_;
     uint32_t compressedLength_;
     char *filename_;
+    jschar *sourceURL_;
     jschar *sourceMapURL_;
     JSPrincipals *originPrincipals_;
 
     // True if we can call JSRuntime::sourceHook to load the source on
     // demand. If sourceRetrievable_ and hasSourceData() are false, it is not
     // possible to get source at all.
     bool sourceRetrievable_:1;
     bool argumentsNotIncluded_:1;
     bool ready_:1;
 
   public:
     ScriptSource(JSPrincipals *originPrincipals)
       : refs(0),
         length_(0),
         compressedLength_(0),
         filename_(NULL),
+        sourceURL_(NULL),
         sourceMapURL_(NULL),
         originPrincipals_(originPrincipals),
         sourceRetrievable_(false),
         argumentsNotIncluded_(false),
         ready_(true)
     {
         data.source = NULL;
         if (originPrincipals_)
@@ -362,16 +364,21 @@ class ScriptSource
     template <XDRMode mode>
     bool performXDR(XDRState<mode> *xdr);
 
     bool setFilename(ExclusiveContext *cx, const char *filename);
     const char *filename() const {
         return filename_;
     }
 
+    // Source URLs
+    bool setSourceURL(ExclusiveContext *cx, const jschar *sourceURL);
+    const jschar *sourceURL();
+    bool hasSourceURL() const { return sourceURL_ != NULL; }
+
     // Source maps
     bool setSourceMapURL(ExclusiveContext *cx, const jschar *sourceMapURL);
     const jschar *sourceMapURL();
     bool hasSourceMapURL() const { return sourceMapURL_ != NULL; }
 
     JSPrincipals *originPrincipals() const { return originPrincipals_; }
 
   private:
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -901,16 +901,17 @@ Evaluate(JSContext *cx, unsigned argc, j
     }
 
     bool newContext = false;
     bool compileAndGo = true;
     bool noScriptRval = false;
     const char *fileName = "@evaluate";
     RootedObject element(cx);
     JSAutoByteString fileNameBytes;
+    RootedString sourceURL(cx);
     RootedString sourceMapURL(cx);
     unsigned lineNumber = 1;
     RootedObject global(cx, NULL);
     bool catchTermination = false;
     bool saveFrameChain = false;
     RootedObject callerGlobal(cx, cx->global());
 
     global = JS_GetGlobalForObject(cx, &args.callee());
@@ -961,16 +962,24 @@ Evaluate(JSContext *cx, unsigned argc, j
                 return false;
         }
 
         if (!JS_GetProperty(cx, opts, "element", &v))
             return false;
         if (!JSVAL_IS_PRIMITIVE(v))
             element = JSVAL_TO_OBJECT(v);
 
+        if (!JS_GetProperty(cx, opts, "sourceURL", &v))
+            return false;
+        if (!JSVAL_IS_VOID(v)) {
+            sourceURL = JS_ValueToString(cx, v);
+            if (!sourceURL)
+                return false;
+        }
+
         if (!JS_GetProperty(cx, opts, "sourceMapURL", &v))
             return false;
         if (!JSVAL_IS_VOID(v)) {
             sourceMapURL = JS_ValueToString(cx, v);
             if (!sourceMapURL)
                 return false;
         }
 
@@ -1049,16 +1058,23 @@ Evaluate(JSContext *cx, unsigned argc, j
         CompileOptions options(cx);
         options.setFileAndLine(fileName, lineNumber);
         options.setElement(element);
         RootedScript script(cx, JS::Compile(cx, global, options, codeChars, codeLength));
         JS_SetOptions(cx, oldopts);
         if (!script)
             return false;
 
+        if (sourceURL && !script->scriptSource()->hasSourceURL()) {
+            const jschar *surl = JS_GetStringCharsZ(cx, sourceURL);
+            if (!surl)
+                return false;
+            if (!script->scriptSource()->setSourceURL(cx, surl))
+                return false;
+        }
         if (sourceMapURL && !script->scriptSource()->hasSourceMapURL()) {
             const jschar *smurl = JS_GetStringCharsZ(cx, sourceMapURL);
             if (!smurl)
                 return false;
             if (!script->scriptSource()->setSourceMapURL(cx, smurl))
                 return false;
         }
         if (!JS_ExecuteScript(cx, global, script, vp)) {
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -3748,19 +3748,40 @@ DebuggerSource_getUrl(JSContext *cx, uns
             return false;
         args.rval().setString(str);
     } else {
         args.rval().setNull();
     }
     return true;
 }
 
+static bool
+DebuggerSource_getDisplayURL(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get url)", args, obj, sourceObject);
+
+    ScriptSource *ss = sourceObject->source();
+    JS_ASSERT(ss);
+
+    if (ss->hasSourceURL()) {
+        JSString *str = JS_NewUCStringCopyZ(cx, ss->sourceURL());
+        if (!str)
+            return false;
+        args.rval().setString(str);
+    } else {
+        args.rval().setNull();
+    }
+
+    return true;
+}
+
 static const JSPropertySpec DebuggerSource_properties[] = {
     JS_PSG("text", DebuggerSource_getText, 0),
     JS_PSG("url", DebuggerSource_getUrl, 0),
+    JS_PSG("displayURL", DebuggerSource_getDisplayURL, 0),
     JS_PS_END
 };
 
 static const JSFunctionSpec DebuggerSource_methods[] = {
     JS_FS_END
 };