// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include #include #include #include struct PropertyInfo { QString name; bool writable; }; static QV4::ReturnedValue asManaged(const QJSManagedValue &value) { const QJSValue jsVal = value.toJSValue(); const QV4::Managed *managed = QJSValuePrivate::asManagedType(&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 getPropertyInfos(const QJSManagedValue &value) { QV4::Scope scope(value.engine()->handle()); QV4::ScopedObject scoped(scope, asManaged(value)); if (!scoped) return {}; QList infos; QScopedPointer iterator(scoped->ownPropertyKeys(scoped)); QV4::Scoped internalClass(scope, scoped->internalClass()); for (auto key = iterator->next(scoped); key.isValid(); key = iterator->next(scoped)) { if (key.isSymbol()) continue; const auto *entry = internalClass->d()->propertyTable.lookup(key); infos.append({ key.toQString(), !entry || internalClass->d()->propertyData.at(entry->index).isWritable() }); }; return infos; } struct State { QMap constructors; QMap prototypes; QSet primitives; }; static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray *classes, State *seen, const QString &name, QJSManagedValue *constructed); static QString findClassName(const QJSManagedValue &value) { 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"); QV4::Scope scope(value.engine()->handle()); if (QV4::ScopedValue scoped(scope, asManaged(value)); scoped->isManaged()) return scoped->managed()->vtable()->className; Q_UNREACHABLE_RETURN(QString()); } 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); QJsonObject classObject; QV4::Scope scope(value.engine()->handle()); classObject[QStringLiteral("className")] = name; classObject[QStringLiteral("qualifiedClassName")] = name; classObject[QStringLiteral("classInfos")] = QJsonArray({ QJsonObject({ { QStringLiteral("name"), QStringLiteral("QML.Element") }, { QStringLiteral("value"), QStringLiteral("anonymous") } }) }); if (value.isObject() || value.isFunction()) classObject[QStringLiteral("object")] = true; else classObject[QStringLiteral("gadget")] = true; const QJSManagedValue prototype = value.prototype(); 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 (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"), protoName } })}; } QJsonArray properties, methods; auto defineProperty = [&](const QJSManagedValue &prop, const PropertyInfo &info) { QJsonObject propertyObject; propertyObject.insert(QStringLiteral("name"), info.name); // Insert faux member entry if we're allowed to write to this if (info.writable) propertyObject.insert(QStringLiteral("member"), QStringLiteral("fakeMember")); 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 { // 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); } } return propertyObject; }; QList unRetrievedProperties; QJSManagedValue constructed; for (const PropertyInfo &info : getPropertyInfos(value)) { QJSManagedValue prop = checkedProperty(value, info.name); if (prop.engine()->hasError()) { unRetrievedProperties.append(info); prop.engine()->catchError(); continue; } // Method or constructor if (prop.isFunction()) { QV4::Scoped propFunction(scope, asManaged(prop)); QJsonObject methodObject; methodObject.insert(QStringLiteral("access"), QStringLiteral("public")); methodObject.insert(QStringLiteral("name"), info.name); methodObject.insert(QStringLiteral("isJavaScriptFunction"), true); const int formalParams = propFunction->getLength(); 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; if (!ctorName.endsWith(QStringLiteral("Constructor"))) ctorName += QStringLiteral("Constructor"); } methodObject.insert( QStringLiteral("returnType"), buildConstructor(prop, classes, seen, ctorName, &constructed)); } QJsonArray arguments; for (int i = 0; i < formalParams; i++) arguments.append(QJsonObject {}); methodObject.insert(QStringLiteral("arguments"), arguments); methods.append(methodObject); continue; } // ...else it's just a property properties.append(defineProperty(prop, info)); } for (const PropertyInfo &info : unRetrievedProperties) { QJSManagedValue prop = checkedProperty( constructed.isUndefined() ? value : constructed, info.name); if (prop.engine()->hasError()) { qWarning() << "Cannot retrieve property " << info.name << "of" << name << constructed.toString(); qWarning().noquote() << " " << prop.engine()->catchError().toString(); } properties.append(defineProperty(prop, info)); } classObject[QStringLiteral("properties")] = properties; classObject[QStringLiteral("methods")] = methods; classes->append(classObject); return name; } static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray *classes, State *seen, const QString &name, QJSManagedValue *constructed) { 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, 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[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); QCommandLineParser parser; parser.addHelpOption(); parser.setApplicationDescription("Internal development tool."); parser.addPositionalArgument("path", "Output json path.", "path"); parser.process(app); const QStringList args = parser.positionalArguments(); if (auto size = args.size(); size == 0) { qWarning().noquote().nospace() << app.applicationName() << ": Output path missing."; return EXIT_FAILURE; } else if (size >= 2) { qWarning().noquote().nospace() << app.applicationName() << ": Too many output paths given. Only one allowed."; } const QString fileName = args.at(0); QJSEngine engine; engine.installExtensions(QJSEngine::AllExtensions); QJsonArray classesArray; State seen; // object. Do this first to claim the "Object" name for the prototype. buildClass(QJSManagedValue(engine.newObject(), &engine), &classesArray, &seen, QStringLiteral("object")); 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( QJsonArray({ QJsonObject({ {QStringLiteral("classes"), classesArray} }) }) ); QFile file(fileName); if (!file.open(QFile::WriteOnly)) { qWarning() << "Failed to write metatypes json to" << fileName; return 1; } file.write(metatypesJson.toJson()); file.close(); return 0; }