Rework qmljsrootgen

This way it actually generates interesting data about the JavaScript
types, for example the functions of the String prototype. Add a helper
method to create a symbol to QJSEngine. This should be generally useful.

Change-Id: I6c7b253b9d6cdb61602ceeae0955aed8d942c139
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Ulf Hermann 2021-04-10 08:56:08 +02:00
parent 9cd1e7902c
commit 16b30313ad
6 changed files with 1764 additions and 563 deletions

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,7 @@
#include <private/qv4qobjectwrapper_p.h>
#include <private/qv4stackframe_p.h>
#include <private/qv4module_p.h>
#include <private/qv4symbol_p.h>
#include <QtCore/qdatetime.h>
#include <QtCore/qmetaobject.h>
@ -590,6 +591,22 @@ QJSValue QJSEngine::newObject()
return QJSValuePrivate::fromReturnedValue(v->asReturnedValue());
}
/*!
\since 6.2
Creates a JavaScript object of class Symbol, with value \a name.
The prototype of the created object will be the Symbol prototype object.
\sa newString()
*/
QJSValue QJSEngine::newSymbol(const QString &name)
{
QV4::Scope scope(m_v4Engine);
QV4::ScopedValue v(scope, QV4::Symbol::create(m_v4Engine, u'@' + name));
return QJSValuePrivate::fromReturnedValue(v->asReturnedValue());
}
/*!
\since 5.12

View File

@ -73,6 +73,7 @@ public:
QJSValue importModule(const QString &fileName);
QJSValue newObject();
QJSValue newSymbol(const QString &name);
QJSValue newArray(uint length = 0);
QJSValue newQObject(QObject *object);

View File

@ -124,11 +124,11 @@ public:
return m();
}
Q_QML_EXPORT bool isString() const;
bool isSymbol() const;
Q_QML_PRIVATE_EXPORT bool isString() const;
Q_QML_PRIVATE_EXPORT bool isSymbol() const;
bool isCanonicalNumericIndexString() const;
Q_QML_EXPORT QString toQString() const;
Q_QML_PRIVATE_EXPORT QString toQString() const;
Heap::StringOrSymbol *toStringOrSymbol(ExecutionEngine *e);
quint64 id() const { return val; }
static PropertyKey fromId(quint64 id) {

View File

@ -227,7 +227,7 @@ void TestQmllint::verifyJsRoot()
QProcess typeregistrarProcess;
typeregistrarProcess.setWorkingDirectory(dir.path());
typeregistrarProcess.start(m_qmltyperegistrarPath, {"jsroot.json", "--generate-qmltypes", "jsroot.qmltypes", "--import-name", "QJSEngine", "--major-version", "1", "--minor-version", "0"});
typeregistrarProcess.start(m_qmltyperegistrarPath, {"jsroot.json", "--generate-qmltypes", "jsroot.qmltypes"});
typeregistrarProcess.waitForFinished();
@ -249,7 +249,7 @@ void TestQmllint::verifyJsRoot()
// If any of the following asserts fail you need to update jsroot.qmltypes using the following commands:
//
// qmljsrootgen jsroot.json
// qmltyperegistrar jsroot.json --generate-qmltypes src/imports/builtins/jsroot.qmltypes --import-name QJSEngine --major-version 1 --minor-version 0
// qmltyperegistrar jsroot.json --generate-qmltypes src/imports/builtins/jsroot.qmltypes
QStringList currentLines = currentJsRootContent.split(QLatin1Char('\n'));
QStringList generatedLines = generatedJsRootContent.split(QLatin1Char('\n'));

View File

@ -31,6 +31,7 @@
#include <QtQml/private/qv4global_p.h>
#include <QtQml/private/qv4functionobject_p.h>
#include <QtQml/qjsengine.h>
#include <QtQml/qjsmanagedvalue.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qfile.h>
@ -42,149 +43,159 @@
struct PropertyInfo
{
QString name;
QV4::PropertyAttributes attr;
bool fromConstructor;
bool writable;
};
QList<PropertyInfo> getPropertyInfos(QJSValue *value, bool extractConstructor = false) {
if (!value->isObject())
static QV4::ReturnedValue asManaged(const QJSManagedValue &value)
{
const QJSValue jsVal = value.toJSValue();
const QV4::Managed *managed = QJSValuePrivate::asManagedType<QV4::Managed>(&jsVal);
return managed ? managed->asReturnedValue() : QV4::Encode::undefined();
}
static QJSManagedValue checkedProperty(const QJSManagedValue &value, const QString &name)
{
return value.hasProperty(name) ? QJSManagedValue(value.property(name), value.engine())
: QJSManagedValue(QJSPrimitiveUndefined(), value.engine());
}
QList<PropertyInfo> getPropertyInfos(const QJSManagedValue &value)
{
QV4::Scope scope(value.engine()->handle());
QV4::ScopedObject scoped(scope, asManaged(value));
if (!scoped)
return {};
auto *object = QJSValuePrivate::asManagedType<QV4::Object>(value);
auto *propertyTable = &object->internalClass()->propertyTable;
QList<PropertyInfo> infos;
for (int i = 0; i < propertyTable->d->alloc; i++) {
auto &propKey = propertyTable->d->entries[i].identifier;
if (!propKey.isValid())
QScopedPointer<QV4::OwnPropertyKeyIterator> iterator(scoped->ownPropertyKeys(scoped));
QV4::Scoped<QV4::InternalClass> internalClass(scope, scoped->internalClass());
for (auto key = iterator->next(scoped); key.isValid(); key = iterator->next(scoped)) {
if (key.isSymbol())
continue;
PropertyInfo propInfo {propKey.toQString(), object->internalClass()->propertyData.at(propertyTable->d->entries[i].index), false};
infos << propInfo;
}
if (!extractConstructor || !value->hasProperty(QStringLiteral("constructor"))) {
std::sort(infos.begin(), infos.end(), [](PropertyInfo& a, PropertyInfo& b) { return a.name < b.name; });
return infos;
}
QJSValue constructor = value->property("constructor");
auto *objectCtor = QJSValuePrivate::asManagedType<QV4::Object>(&constructor);
auto *propertyTableCtor = &objectCtor->internalClass()->propertyTable;
for (int i = 0; i < propertyTableCtor->d->alloc; i++) {
auto &propKey = propertyTableCtor->d->entries[i].identifier;
if (!propKey.isValid())
continue;
PropertyInfo propInfo {propKey.toQString(), objectCtor->internalClass()->propertyData.at(propertyTableCtor->d->entries[i].index), true};
infos << propInfo;
}
std::sort(infos.begin(), infos.end(), [](PropertyInfo& a, PropertyInfo& b) { return a.name < b.name; });
const auto *entry = internalClass->d()->propertyTable.lookup(key);
infos.append({
key.toQString(),
!entry || internalClass->d()->propertyData.at(entry->index).isWritable()
});
};
return infos;
}
void buildBaseType(QString className, QJsonArray *classes)
struct State {
QMap<QString, QJSValue> constructors;
QMap<QString, QJSValue> prototypes;
QSet<QString> primitives;
};
static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray *classes,
State *seen, const QString &name);
static QString findClassName(const QJSManagedValue &value)
{
QJsonObject classObject {
{QStringLiteral("className"), className},
{QStringLiteral("qualifiedClassName"), className},
{QStringLiteral("object"), true},
{QStringLiteral("classInfos"),
QJsonArray({
QJsonObject({
{ QStringLiteral("name"), QStringLiteral("QML.Element") },
{ QStringLiteral("value"), QStringLiteral("anonymous") }
})
})
}
};
if (value.isUndefined())
return QStringLiteral("undefined");
if (value.isBoolean())
return QStringLiteral("boolean");
if (value.isNumber())
return QStringLiteral("number");
if (value.isString())
return QStringLiteral("string");
if (value.isSymbol())
return QStringLiteral("symbol");
classes->append(classObject);
QV4::Scope scope(value.engine()->handle());
if (QV4::ScopedValue scoped(scope, asManaged(value)); scoped->isManaged())
return scoped->managed()->vtable()->className;
Q_UNREACHABLE();
return QString();
}
enum class SeenType {
Normal,
Constructed
};
static QString buildClass(const QJSManagedValue &value, QJsonArray *classes,
State *seen, const QString &name)
{
if (value.isNull())
return QString();
if (seen->primitives.contains(name))
return name;
else if (name.at(0).isLower())
seen->primitives.insert(name);
void buildClass(QJSValue value, QJsonArray *classes, bool globalObject, QMap<QString, SeenType> &seen, SeenType seenType = SeenType::Normal) {
QJsonObject classObject;
QV4::Scope scope(value.engine()->handle());
auto *object = QJSValuePrivate::asManagedType<QV4::Object>(&value);
QString className = globalObject ? QStringLiteral("GlobalObject") : QString::fromUtf8(object->vtable()->className);
// Make sure we're not building the same class twice if it has been fully seen
if (seen.contains(className) && (seen[className] != SeenType::Constructed || seenType == SeenType::Constructed))
return;
seen.insert(className, seenType);
// See if there is a lesser duplicate that needs to be removed.
for (auto it = classes->begin(); it != classes->end(); ++it) {
if (it->toObject()[QStringLiteral("className")] == className) {
it = classes->erase(it);
break;
}
}
classObject[QStringLiteral("className")] = className;
classObject[QStringLiteral("qualifiedClassName")] = className;
classObject[QStringLiteral("className")] = name;
classObject[QStringLiteral("qualifiedClassName")] = name;
classObject[QStringLiteral("classInfos")] = QJsonArray({
QJsonObject({
{ QStringLiteral("name"), QStringLiteral("QML.Element") },
{ QStringLiteral("value"), globalObject ? QStringLiteral("auto") : QStringLiteral("anonymous") }
{ QStringLiteral("value"), QStringLiteral("anonymous") }
})
});
if (value.isObject() || value.isFunction())
classObject[QStringLiteral("object")] = true;
else
classObject[QStringLiteral("gadget")] = true;
QV4::Scope scope(object);
const QJSManagedValue prototype = value.prototype();
QJSValue prototype = value.prototype();
// Try to see whether calling the prototype constructor or the prototype's prototype constructor uncovers any types.
// (It usually doesn't)
if (prototype.property("constructor").isCallable()) {
buildClass(prototype.property("constructor").callAsConstructor(), classes, false, seen, SeenType::Constructed);
if (!prototype.isNull()) {
QString protoName;
for (auto it = seen->prototypes.begin(), end = seen->prototypes.end(); it != end; ++it) {
if (prototype.strictlyEquals(QJSManagedValue(*it, value.engine()))) {
protoName = it.key();
break;
}
}
if (prototype.prototype().property("constructor").isCallable()) {
buildClass(prototype.prototype().property("constructor").callAsConstructor(), classes, false, seen, SeenType::Constructed);
if (protoName.isEmpty()) {
if (name.endsWith(QStringLiteral("ErrorPrototype"))
&& name != QStringLiteral("ErrorPrototype")) {
protoName = QStringLiteral("ErrorPrototype");
} else if (name.endsWith(QStringLiteral("Prototype"))) {
protoName = findClassName(prototype);
if (!protoName.endsWith(QStringLiteral("Prototype")))
protoName += QStringLiteral("Prototype");
} else {
protoName = name.at(0).toUpper() + name.mid(1) + QStringLiteral("Prototype");
}
auto it = seen->prototypes.find(protoName);
if (it == seen->prototypes.end()) {
seen->prototypes.insert(protoName, prototype.toJSValue());
buildClass(prototype, classes, seen, protoName);
} else if (!it->strictlyEquals(prototype.toJSValue())) {
qWarning() << "Cannot find a distinct name for the prototype of" << name;
qWarning() << protoName << "is already in use.";
}
}
classObject[QStringLiteral("superClasses")] = QJsonArray {
QJsonObject ({
{ QStringLiteral("access"), QStringLiteral("public") },
{ QStringLiteral("name"), className == QStringLiteral("Object") ? QStringLiteral("QJSValue") : QStringLiteral("Object") }
{ QStringLiteral("name"), protoName }
})};
}
QJsonArray properties, methods;
for (const PropertyInfo &info : getPropertyInfos(&value, className == QStringLiteral("Object"))) {
QJSValue prop;
if (info.fromConstructor)
prop = value.property("constructor").property(info.name);
else
prop = value.property(info.name);
// This appears in many property tables despite not being real properties
if (info.name.startsWith(QStringLiteral("@Symbol.")))
continue;
for (const PropertyInfo &info : getPropertyInfos(value)) {
QJSManagedValue prop = checkedProperty(value, info.name);
if (prop.engine()->hasError()) {
qWarning() << "Cannot retrieve property " << info.name << "of" << name;
qWarning().noquote() << " " << prop.engine()->catchError().toString();
}
// Method or constructor
if (prop.isCallable()) {
const QV4::FunctionObject *propFunction = QJSValuePrivate::asManagedType<QV4::FunctionObject>(&prop);
if (prop.isFunction()) {
QV4::Scoped<QV4::FunctionObject> propFunction(scope, asManaged(prop));
QJsonObject methodObject;
@ -194,26 +205,31 @@ void buildClass(QJSValue value, QJsonArray *classes, bool globalObject, QMap<QSt
if (propFunction->isConstructor()) {
methodObject.insert(QStringLiteral("isConstructor"), true);
QString ctorName;
if (info.name.at(0).isUpper()) {
ctorName = info.name;
} else if (info.name == QStringLiteral("constructor")) {
if (name.endsWith(QStringLiteral("Prototype")))
ctorName = name.chopped(strlen("Prototype"));
else if (name.endsWith(QStringLiteral("PrototypeMember")))
ctorName = name.chopped(strlen("PrototypeMember"));
else
ctorName = name;
QJSValue constructed = prop.callAsConstructor();
if (constructed.isObject()) {
auto *constructedObject = QJSValuePrivate::asManagedType<QV4::Object>(&constructed);
QString classObjectType = constructedObject->vtable()->className;
buildClass(constructed, classes, false, seen, SeenType::Constructed);
methodObject.insert(QStringLiteral("returnType"), classObjectType);
} else {
qWarning() << "Warning: Calling constructor" << info.name << "failed";
if (!ctorName.endsWith(QStringLiteral("Constructor")))
ctorName += QStringLiteral("Constructor");
}
methodObject.insert(
QStringLiteral("returnType"),
buildConstructor(prop, classes, seen, ctorName));
}
const int formalParams = propFunction->getLength();
QJsonArray arguments;
for (int i = 0; i < formalParams; i++) {
for (int i = 0; i < formalParams; i++)
arguments.append(QJsonObject {});
}
methodObject.insert(QStringLiteral("arguments"), arguments);
@ -228,52 +244,91 @@ void buildClass(QJSValue value, QJsonArray *classes, bool globalObject, QMap<QSt
propertyObject.insert(QStringLiteral("name"), info.name);
// Insert faux member entry if we're allowed to write to this
if (info.attr.isWritable())
if (info.writable)
propertyObject.insert(QStringLiteral("member"), QStringLiteral("fakeMember"));
// Writing out the types is kind of hard in this case because we often have no corresponding QObject type
if (prop.isQObject()) {
propertyObject.insert(QStringLiteral("type"), prop.toQObject()->metaObject()->className());
if (!prop.isUndefined() && !prop.isNull()) {
QString propClassName = findClassName(prop);
if (!propClassName.at(0).isLower() && info.name != QStringLiteral("prototype")) {
propClassName = (name == QStringLiteral("GlobalObject"))
? QString()
: name.at(0).toUpper() + name.mid(1);
propClassName += info.name.at(0).toUpper() + info.name.mid(1);
propertyObject.insert(QStringLiteral("type"),
buildClass(prop, classes, seen, propClassName));
} else {
QString type;
if (prop.isString()) {
type = QStringLiteral("string");
} else if (prop.isBool()){
type = QStringLiteral("bool");
} else if (prop.isNumber()) {
type = QStringLiteral("number");
} else if (prop.isUndefined()) {
type = QStringLiteral("undefined");
} else if (prop.isArray() || prop.isNull() || prop.isObject()) {
type = QStringLiteral("object");
} else {
qWarning() << "Warning: Failed to resolve type of property" << info.name;
type = QStringLiteral("undefined");
// If it's the "prototype" property we just refer to generic "Object",
// and if it's a value type, we handle it separately.
propertyObject.insert(QStringLiteral("type"), propClassName);
}
if (seenType != SeenType::Constructed || !prop.isUndefined())
propertyObject.insert(QStringLiteral("type"), type);
}
if (prop.isObject() && !prop.isQObject()) {
buildClass(prop, classes, false, seen);
auto *object = QJSValuePrivate::asManagedType<QV4::Object>(&prop);
propertyObject.insert(QStringLiteral("type"), object->vtable()->className);
}
properties.append(propertyObject);
}
classObject[QStringLiteral("properties")] = properties;
classObject[QStringLiteral("methods")] = methods;
// Make sure that in the unlikely accident that some subclass has already provided a normal replacement for this constructed type
// there are no duplicate entries.
if (seenType != SeenType::Constructed || seen[className] != SeenType::Normal)
classes->append(classObject);
return name;
}
static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray *classes,
State *seen, const QString &name)
{
QJSEngine *engine = constructor.engine();
// If the constructor appears in the global object, use the name from there.
const QJSManagedValue globalObject(engine->globalObject(), engine);
const auto infos = getPropertyInfos(globalObject);
for (const auto &info : infos) {
const QJSManagedValue member(globalObject.property(info.name), engine);
if (member.strictlyEquals(constructor) && info.name != name)
return buildConstructor(constructor, classes, seen, info.name);
}
QJSManagedValue constructed;
if (name == QStringLiteral("Symbol"))
return QStringLiteral("undefined"); // Cannot construct symbols with "new";
if (name == QStringLiteral("URL")) {
constructed = QJSManagedValue(
constructor.callAsConstructor({ QJSValue(QStringLiteral("http://a.bc")) }),
engine);
} else if (name == QStringLiteral("Promise")) {
constructed = QJSManagedValue(
constructor.callAsConstructor(
{ engine->evaluate(QStringLiteral("(function() {})")) }),
engine);
} else if (name == QStringLiteral("DataView")) {
constructed = QJSManagedValue(
constructor.callAsConstructor(
{ engine->evaluate(QStringLiteral("new ArrayBuffer()")) }),
engine);
} else if (name == QStringLiteral("Proxy")) {
constructed = QJSManagedValue(constructor.callAsConstructor(
{ engine->newObject(), engine->newObject() }), engine);
} else {
constructed = QJSManagedValue(constructor.callAsConstructor(), engine);
}
if (engine->hasError()) {
qWarning() << "Calling constructor" << name << "failed";
qWarning().noquote() << " " << engine->catchError().toString();
return QString();
} else if (name.isEmpty()) {
Q_UNREACHABLE();
}
auto it = seen->constructors.find(name);
if (it == seen->constructors.end()) {
seen->constructors.insert(name, constructor.toJSValue());
return buildClass(constructed, classes, seen, name);
} else if (!constructor.strictlyEquals(QJSManagedValue(*it, constructor.engine()))) {
qWarning() << "Two constructors of the same name seen:" << name;
}
return name;
}
int main(int argc, char *argv[])
@ -294,16 +349,42 @@ int main(int argc, char *argv[])
engine.installExtensions(QJSEngine::AllExtensions);
QJsonArray classesArray;
State seen;
// Add JS types so they can be referenced by other classes
for (const QString &name : { QStringLiteral("string"), QStringLiteral("undefined"), QStringLiteral("number"),
QStringLiteral("object"), QStringLiteral("bool"), QStringLiteral("symbol"), QStringLiteral("function")}) {
buildBaseType(name, &classesArray);
}
// object. Do this first to claim the "Object" name for the prototype.
buildClass(QJSManagedValue(engine.newObject(), &engine), &classesArray, &seen,
QStringLiteral("object"));
QMap<QString, SeenType> seen {};
buildClass(engine.globalObject(), &classesArray, true, seen);
buildClass(QJSManagedValue(engine.globalObject(), &engine), &classesArray, &seen,
QStringLiteral("GlobalObject"));
// Add JS types, in case they aren't used anywhere.
// function
buildClass(QJSManagedValue(engine.evaluate(QStringLiteral("(function() {})")), &engine),
&classesArray, &seen, QStringLiteral("function"));
// string
buildClass(QJSManagedValue(QStringLiteral("s"), &engine), &classesArray, &seen,
QStringLiteral("string"));
// undefined
buildClass(QJSManagedValue(QJSPrimitiveUndefined(), &engine), &classesArray, &seen,
QStringLiteral("undefined"));
// number
buildClass(QJSManagedValue(QJSPrimitiveValue(1.1), &engine), &classesArray, &seen,
QStringLiteral("number"));
// boolean
buildClass(QJSManagedValue(QJSPrimitiveValue(true), &engine), &classesArray, &seen,
QStringLiteral("boolean"));
// symbol
buildClass(QJSManagedValue(engine.newSymbol(QStringLiteral("s")), &engine),
&classesArray, &seen, QStringLiteral("symbol"));
// Generate the fake metatypes json structure
QJsonDocument metatypesJson = QJsonDocument(