Stage 1-3: Generate better source in Object.prototype.toSource.

Tag #960.
This commit is contained in:
wolfbeast 2019-02-02 14:29:54 +01:00 committed by Roy Tam
commit f2622bd19f

View file

@ -9,11 +9,13 @@
#include "mozilla/ArrayUtils.h"
#include "jscntxt.h"
#include "jsstr.h"
#include "builtin/Eval.h"
#include "frontend/BytecodeCompiler.h"
#include "jit/InlinableNatives.h"
#include "js/UniquePtr.h"
#include "vm/AsyncFunction.h"
#include "vm/StringBuffer.h"
#include "jsobjinlines.h"
@ -124,6 +126,27 @@ obj_toSource(JSContext* cx, unsigned argc, Value* vp)
return true;
}
template <typename CharT>
static bool
Consume(const CharT*& s, const CharT* e, const char *chars)
{
size_t len = strlen(chars);
if (s + len >= e)
return false;
if (!EqualChars(s, chars, len))
return false;
s += len;
return true;
}
template <typename CharT>
static void
ConsumeSpaces(const CharT*& s, const CharT* e)
{
while (*s == ' ' && s < e)
s++;
}
/*
* Given a function source string, return the offset and length of the part
* between '(function $name' and ')'.
@ -133,37 +156,53 @@ static bool
ArgsAndBodySubstring(mozilla::Range<const CharT> chars, size_t* outOffset, size_t* outLen)
{
const CharT* const start = chars.begin().get();
const CharT* const end = chars.end().get();
const CharT* s = start;
const CharT* e = chars.end().get();
uint8_t parenChomp = 0;
if (s[0] == '(') {
if (s == e)
return false;
// Remove enclosing parentheses.
if (*s == '(' && *(e - 1) == ')') {
s++;
parenChomp = 1;
e--;
}
/* Try to jump "function" keyword. */
s = js_strchr_limit(s, ' ', end);
if (!s)
return false;
(void) Consume(s, e, "async");
ConsumeSpaces(s, e);
(void) (Consume(s, e, "function") || Consume(s, e, "get") || Consume(s, e, "set"));
ConsumeSpaces(s, e);
(void) Consume(s, e, "*");
ConsumeSpaces(s, e);
/*
* Jump over the function's name: it can't be encoded as part
* of an ECMA getter or setter.
*/
s = js_strchr_limit(s, '(', end);
if (!s)
return false;
if (*s == ' ')
// Jump over the function's name.
if (Consume(s, e, "[")) {
s = js_strchr_limit(s, ']', e);
if (!s)
return false;
s++;
ConsumeSpaces(s, e);
if (*s != '(')
return false;
} else {
s = js_strchr_limit(s, '(', e);
if (!s)
return false;
}
*outOffset = s - start;
*outLen = end - s - parenChomp;
*outLen = e - s;
MOZ_ASSERT(*outOffset + *outLen <= chars.length());
return true;
}
enum class PropertyKind {
Getter,
Setter,
Method,
Normal
};
JSString*
js::ObjectToSource(JSContext* cx, HandleObject obj)
{
@ -182,59 +221,28 @@ js::ObjectToSource(JSContext* cx, HandleObject obj)
if (!buf.append('{'))
return nullptr;
RootedValue v0(cx), v1(cx);
MutableHandleValue val[2] = {&v0, &v1};
RootedString str0(cx), str1(cx);
MutableHandleString gsop[2] = {&str0, &str1};
AutoIdVector idv(cx);
if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY | JSITER_SYMBOLS, &idv))
return nullptr;
bool comma = false;
for (size_t i = 0; i < idv.length(); ++i) {
RootedId id(cx, idv[i]);
Rooted<PropertyDescriptor> desc(cx);
if (!GetOwnPropertyDescriptor(cx, obj, id, &desc))
return nullptr;
int valcnt = 0;
if (desc.object()) {
if (desc.isAccessorDescriptor()) {
if (desc.hasGetterObject() && desc.getterObject()) {
val[valcnt].setObject(*desc.getterObject());
gsop[valcnt].set(cx->names().get);
valcnt++;
}
if (desc.hasSetterObject() && desc.setterObject()) {
val[valcnt].setObject(*desc.setterObject());
gsop[valcnt].set(cx->names().set);
valcnt++;
}
} else {
valcnt = 1;
val[0].set(desc.value());
gsop[0].set(nullptr);
}
}
auto AddProperty = [cx, &comma, &buf](HandleId id, HandleValue val, PropertyKind kind) -> bool {
/* Convert id to a string. */
RootedString idstr(cx);
if (JSID_IS_SYMBOL(id)) {
RootedValue v(cx, SymbolValue(JSID_TO_SYMBOL(id)));
idstr = ValueToSource(cx, v);
if (!idstr)
return nullptr;
return false;
} else {
RootedValue idv(cx, IdToValue(id));
idstr = ToString<CanGC>(cx, idv);
if (!idstr)
return nullptr;
return false;
/*
* If id is a string that's not an identifier, or if it's a negative
* integer, then it must be quoted.
* If id is a string that's not an identifier, or if it's a
* negative integer, then it must be quoted.
*/
if (JSID_IS_ATOM(id)
? !IsIdentifier(JSID_TO_ATOM(id))
@ -242,28 +250,65 @@ js::ObjectToSource(JSContext* cx, HandleObject obj)
{
idstr = QuoteString(cx, idstr, char16_t('\''));
if (!idstr)
return nullptr;
return false;
}
}
for (int j = 0; j < valcnt; j++) {
/* Convert val[j] to its canonical source form. */
JSString* valsource = ValueToSource(cx, val[j]);
if (!valsource)
return nullptr;
RootedString valsource(cx, ValueToSource(cx, val));
if (!valsource)
return false;
RootedLinearString valstr(cx, valsource->ensureLinear(cx));
if (!valstr)
return nullptr;
RootedLinearString valstr(cx, valsource->ensureLinear(cx));
if (!valstr)
return false;
size_t voffset = 0;
size_t vlength = valstr->length();
if (comma && !buf.append(", "))
return false;
comma = true;
/*
* Remove '(function ' from the beginning of valstr and ')' from the
* end so that we can put "get" in front of the function definition.
*/
if (gsop[j] && IsFunctionObject(val[j])) {
size_t voffset, vlength;
// Methods and accessors can return exact syntax of source, that fits
// into property without adding property name or "get"/"set" prefix.
// Use the exact syntax when the following conditions are met:
//
// * It's a function object
// (exclude proxies)
// * Function's kind and property's kind are same
// (this can be false for dynamically defined properties)
// * Function has explicit name
// (this can be false for computed property and dynamically defined
// properties)
// * Function's name and property's name are same
// (this can be false for dynamically defined properties)
if (kind == PropertyKind::Getter || kind == PropertyKind::Setter ||
kind == PropertyKind::Method)
{
RootedFunction fun(cx);
if (val.toObject().is<JSFunction>()) {
fun = &val.toObject().as<JSFunction>();
// Method's case should be checked on caller.
if (((fun->isGetter() && kind == PropertyKind::Getter) ||
(fun->isSetter() && kind == PropertyKind::Setter) ||
kind == PropertyKind::Method) &&
fun->explicitName())
{
bool result;
if (!EqualStrings(cx, fun->explicitName(), idstr, &result))
return false;
if (result) {
if (!buf.append(valstr))
return false;
return true;
}
}
}
{
// When falling back try to generate a better string
// representation by skipping the prelude, and also removing
// the enclosing parentheses.
bool success;
JS::AutoCheckCannotGC nogc;
if (valstr->hasLatin1Chars())
@ -271,29 +316,90 @@ js::ObjectToSource(JSContext* cx, HandleObject obj)
else
success = ArgsAndBodySubstring(valstr->twoByteRange(nogc), &voffset, &vlength);
if (!success)
gsop[j].set(nullptr);
kind = PropertyKind::Normal;
}
if (comma && !buf.append(", "))
return nullptr;
comma = true;
if (kind == PropertyKind::Getter) {
if (!buf.append("get "))
return false;
} else if (kind == PropertyKind::Setter) {
if (!buf.append("set "))
return false;
} else if (kind == PropertyKind::Method && fun) {
if (IsWrappedAsyncFunction(fun)) {
if (!buf.append("async "))
return false;
}
if (gsop[j]) {
if (!buf.append(gsop[j]) || !buf.append(' '))
if (fun->isStarGenerator()) {
if (!buf.append('*'))
return false;
}
}
}
bool needsBracket = JSID_IS_SYMBOL(id);
if (needsBracket && !buf.append('['))
return false;
if (!buf.append(idstr))
return false;
if (needsBracket && !buf.append(']'))
return false;
if (kind == PropertyKind::Getter || kind == PropertyKind::Setter ||
kind == PropertyKind::Method)
{
if (!buf.appendSubstring(valstr, voffset, vlength))
return false;
} else {
if (!buf.append(':'))
return false;
if (!buf.append(valstr))
return false;
}
return true;
};
RootedId id(cx);
Rooted<PropertyDescriptor> desc(cx);
RootedValue val(cx);
RootedFunction fun(cx);
for (size_t i = 0; i < idv.length(); ++i) {
id = idv[i];
if (!GetOwnPropertyDescriptor(cx, obj, id, &desc))
return nullptr;
if (!desc.object())
continue;
if (desc.isAccessorDescriptor()) {
if (desc.hasGetterObject() && desc.getterObject()) {
val.setObject(*desc.getterObject());
if (!AddProperty(id, val, PropertyKind::Getter))
return nullptr;
}
if (JSID_IS_SYMBOL(id) && !buf.append('['))
return nullptr;
if (!buf.append(idstr))
return nullptr;
if (JSID_IS_SYMBOL(id) && !buf.append(']'))
return nullptr;
if (!buf.append(gsop[j] ? ' ' : ':'))
return nullptr;
if (!buf.appendSubstring(valstr, voffset, vlength))
return nullptr;
if (desc.hasSetterObject() && desc.setterObject()) {
val.setObject(*desc.setterObject());
if (!AddProperty(id, val, PropertyKind::Setter))
return nullptr;
}
continue;
}
val.set(desc.value());
if (IsFunctionObject(val, fun.address())) {
if (IsWrappedAsyncFunction(fun))
fun = GetUnwrappedAsyncFunction(fun);
if (fun->isMethod()) {
if (!AddProperty(id, val, PropertyKind::Method))
return nullptr;
continue;
}
}
if (!AddProperty(id, val, PropertyKind::Normal))
return nullptr;
}
if (!buf.append('}'))