aboutsummaryrefslogtreecommitdiffstats
path: root/src/qmltyperegistrar/qqmltyperegistrar.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/qmltyperegistrar/qqmltyperegistrar.cpp')
-rw-r--r--src/qmltyperegistrar/qqmltyperegistrar.cpp542
1 files changed, 542 insertions, 0 deletions
diff --git a/src/qmltyperegistrar/qqmltyperegistrar.cpp b/src/qmltyperegistrar/qqmltyperegistrar.cpp
new file mode 100644
index 0000000000..058a3180b0
--- /dev/null
+++ b/src/qmltyperegistrar/qqmltyperegistrar.cpp
@@ -0,0 +1,542 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QFile>
+#include <QCborArray>
+#include <QCborValue>
+
+#include "qqmltyperegistrar_p.h"
+#include "qqmltypescreator_p.h"
+#include "qanystringviewutils_p.h"
+#include "qqmltyperegistrarconstants_p.h"
+#include "qqmltyperegistrarutils_p.h"
+
+#include <algorithm>
+
+QT_BEGIN_NAMESPACE
+using namespace Qt::Literals;
+using namespace Constants;
+using namespace Constants::MetatypesDotJson;
+using namespace Constants::MetatypesDotJson::Qml;
+using namespace QAnyStringViewUtils;
+
+struct ExclusiveVersionRange
+{
+ QAnyStringView fileName;
+ QString claimerName;
+ QTypeRevision addedIn;
+ QTypeRevision removedIn;
+};
+
+/*!
+ * \brief True if x was removed before y was introduced.
+ * \param o
+ * \return
+ */
+bool operator<(const ExclusiveVersionRange &x, const ExclusiveVersionRange &y)
+{
+ if (x.removedIn.isValid())
+ return y.addedIn.isValid() ? x.removedIn <= y.addedIn : true;
+ else
+ return false;
+}
+
+/*!
+ * \brief True when x and y share a common version. (Warning: not transitive!)
+ * \param o
+ * \return
+ */
+bool operator==(const ExclusiveVersionRange &x, const ExclusiveVersionRange &y)
+{
+ return !(x < y) && !(y < x);
+}
+
+bool QmlTypeRegistrar::argumentsFromCommandLineAndFile(QStringList &allArguments,
+ const QStringList &arguments)
+{
+ allArguments.reserve(arguments.size());
+ for (const QString &argument : arguments) {
+ // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it
+ if (argument.startsWith(QLatin1Char('@'))) {
+ QString optionsFile = argument;
+ optionsFile.remove(0, 1);
+ if (optionsFile.isEmpty()) {
+ warning(optionsFile) << "The @ option requires an input file";
+ return false;
+ }
+ QFile f(optionsFile);
+ if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ warning(optionsFile) << "Cannot open options file specified with @";
+ return false;
+ }
+ while (!f.atEnd()) {
+ QString line = QString::fromLocal8Bit(f.readLine().trimmed());
+ if (!line.isEmpty())
+ allArguments << line;
+ }
+ } else {
+ allArguments << argument;
+ }
+ }
+ return true;
+}
+
+int QmlTypeRegistrar::runExtract(const QString &baseName, const MetaTypesJsonProcessor &processor)
+{
+ if (processor.types().isEmpty()) {
+ error(baseName) << "No types to register found in library";
+ return EXIT_FAILURE;
+ }
+ QFile headerFile(baseName + u".h");
+ bool ok = headerFile.open(QFile::WriteOnly);
+ if (!ok) {
+ error(headerFile.fileName()) << "Cannot open header file for writing";
+ return EXIT_FAILURE;
+ }
+
+ QString includeGuard = baseName;
+ static const QRegularExpression nonAlNum(QLatin1String("[^a-zA-Z0-9_]"));
+ includeGuard.replace(nonAlNum, QLatin1String("_"));
+
+ auto prefix = QString::fromLatin1(
+ "#ifndef %1_H\n"
+ "#define %1_H\n"
+ "#include <QtQml/qqml.h>\n"
+ "#include <QtQml/qqmlmoduleregistration.h>\n").arg(includeGuard);
+ const QList<QString> includes = processor.includes();
+ for (const QString &include: includes)
+ prefix += u"\n#include <%1>"_s.arg(include);
+ headerFile.write((prefix + processor.extractRegisteredTypes()).toUtf8() + "\n#endif\n");
+
+ QFile sourceFile(baseName + u".cpp");
+ ok = sourceFile.open(QFile::WriteOnly);
+ if (!ok) {
+ error(sourceFile.fileName()) << "Cannot open implementation file for writing";
+ return EXIT_FAILURE;
+ }
+ // the string split is necessaury because cmake's automoc scanner would otherwise pick up the include
+ QString code = u"#include \"%1.h\"\n#include "_s.arg(baseName);
+ code += uR"("moc_%1.cpp")"_s.arg(baseName);
+ sourceFile.write(code.toUtf8());
+ sourceFile.write("\n");
+ return EXIT_SUCCESS;
+}
+
+MetaType QmlTypeRegistrar::findType(QAnyStringView name) const
+{
+ for (const MetaType &type : m_types) {
+ if (type.qualifiedClassName() != name)
+ continue;
+ return type;
+ }
+ return MetaType();
+};
+
+MetaType QmlTypeRegistrar::findTypeForeign(QAnyStringView name) const
+{
+ for (const MetaType &type : m_foreignTypes) {
+ if (type.qualifiedClassName() != name)
+ continue;
+ return type;
+ }
+ return MetaType();
+};
+
+QString conflictingVersionToString(const ExclusiveVersionRange &r)
+{
+ using namespace Qt::StringLiterals;
+
+ QString s = r.claimerName;
+ if (r.addedIn.isValid()) {
+ s += u" (added in %1.%2)"_s.arg(r.addedIn.majorVersion()).arg(r.addedIn.minorVersion());
+ }
+ if (r.removedIn.isValid()) {
+ s += u" (removed in %1.%2)"_s.arg(r.removedIn.majorVersion())
+ .arg(r.removedIn.minorVersion());
+ }
+ return s;
+};
+
+// Return a name for the registration variable containing the module to
+// avoid clashes in Unity builds.
+static QString registrationVarName(const QString &module)
+{
+ auto specialCharPred = [](QChar c) { return !c.isLetterOrNumber(); };
+ QString result = module;
+ result[0] = result.at(0).toLower();
+ result.erase(std::remove_if(result.begin(), result.end(), specialCharPred), result.end());
+ return result + "Registration"_L1;
+}
+
+void QmlTypeRegistrar::write(QTextStream &output, QAnyStringView outFileName) const
+{
+ output << uR"(/****************************************************************************
+** Generated QML type registration code
+**
+** WARNING! All changes made in this file will be lost!
+*****************************************************************************/
+
+)"_s;
+
+ output << u"#include <QtQml/qqml.h>\n"_s;
+ output << u"#include <QtQml/qqmlmoduleregistration.h>\n"_s;
+
+ for (const QString &include : m_includes)
+ output << u"\n#include <%1>"_s.arg(include);
+
+ output << u"\n\n"_s;
+
+ // Keep this in sync with _qt_internal_get_escaped_uri in CMake
+ QString moduleAsSymbol = m_module;
+ static const QRegularExpression nonAlnumRegexp(QLatin1String("[^A-Za-z0-9]"));
+ moduleAsSymbol.replace(nonAlnumRegexp, QStringLiteral("_"));
+
+ QString underscoredModuleAsSymbol = m_module;
+ underscoredModuleAsSymbol.replace(QLatin1Char('.'), QLatin1Char('_'));
+
+ if (underscoredModuleAsSymbol != moduleAsSymbol
+ || underscoredModuleAsSymbol.isEmpty()
+ || underscoredModuleAsSymbol.front().isDigit()) {
+ warning(outFileName) << m_module << "is an invalid QML module URI. You cannot import this.";
+ }
+
+ const QString functionName = QStringLiteral("qml_register_types_") + moduleAsSymbol;
+ output << uR"(
+#if !defined(QT_STATIC)
+#define Q_QMLTYPE_EXPORT Q_DECL_EXPORT
+#else
+#define Q_QMLTYPE_EXPORT
+#endif
+)"_s;
+
+ if (!m_targetNamespace.isEmpty())
+ output << u"namespace "_s << m_targetNamespace << u" {\n"_s;
+
+ output << u"Q_QMLTYPE_EXPORT void "_s << functionName << u"()\n{"_s;
+ const quint8 majorVersion = m_moduleVersion.majorVersion();
+ const quint8 minorVersion = m_moduleVersion.minorVersion();
+
+ for (const auto &version : m_pastMajorVersions) {
+ output << uR"(
+ qmlRegisterModule("%1", %2, 0);
+ qmlRegisterModule("%1", %2, 254);)"_s.arg(m_module)
+ .arg(version);
+ }
+
+ if (minorVersion != 0) {
+ output << uR"(
+ qmlRegisterModule("%1", %2, 0);)"_s.arg(m_module)
+ .arg(majorVersion);
+ }
+
+ QVector<QAnyStringView> typesRegisteredAnonymously;
+
+ const auto fillTypesRegisteredAnonymously = [&](const auto &members, QAnyStringView typeName) {
+ bool foundRevisionEntry = false;
+ for (const auto &entry : members) {
+ if (entry.revision.isValid()) {
+ foundRevisionEntry = true;
+ break;
+ }
+ }
+
+ if (!foundRevisionEntry)
+ return false;
+
+ if (typesRegisteredAnonymously.contains(typeName))
+ return true;
+
+ typesRegisteredAnonymously.append(typeName);
+
+ if (m_followForeignVersioning) {
+ output << uR"(
+ qmlRegisterAnonymousTypesAndRevisions<%1>("%2", %3);)"_s.arg(typeName.toString(), m_module)
+ .arg(majorVersion);
+ return true;
+ }
+
+ for (const auto &version
+ : m_pastMajorVersions + decltype(m_pastMajorVersions){ majorVersion }) {
+ output << uR"(
+ qmlRegisterAnonymousType<%1, 254>("%2", %3);)"_s.arg(typeName.toString(), m_module)
+ .arg(version);
+ }
+
+ return true;
+ };
+
+
+ QHash<QString, QList<ExclusiveVersionRange>> qmlElementInfos;
+
+ for (const MetaType &classDef : std::as_const(m_types)) {
+
+ // Do not generate C++ registrations for JavaScript types.
+ if (classDef.inputFile().isEmpty())
+ continue;
+
+ QString className = classDef.qualifiedClassName().toString();
+ QString targetName = className;
+
+ // If either the foreign or the local part is a namespace we need to
+ // generate a namespace registration.
+ bool targetIsNamespace = classDef.kind() == MetaType::Kind::Namespace;
+
+ QAnyStringView extendedName;
+ QList<QString> qmlElementNames;
+ QTypeRevision addedIn;
+ QTypeRevision removedIn;
+
+ for (const ClassInfo &v : classDef.classInfos()) {
+ const QAnyStringView name = v.name;
+ if (name == S_ELEMENT) {
+ qmlElementNames.append(v.value.toString());
+ } else if (name == S_FOREIGN) {
+ targetName = v.value.toString();
+ } else if (name == S_FOREIGN_IS_NAMESPACE) {
+ targetIsNamespace = targetIsNamespace || (v.value == S_TRUE);
+ } else if (name == S_EXTENDED) {
+ extendedName = v.value;
+ } else if (name == S_ADDED_IN_VERSION) {
+ int version = toInt(v.value);
+ addedIn = QTypeRevision::fromEncodedVersion(version);
+ addedIn = handleInMinorVersion(addedIn, majorVersion);
+ } else if (name == S_REMOVED_IN_VERSION) {
+ int version = toInt(v.value);
+ removedIn = QTypeRevision::fromEncodedVersion(version);
+ removedIn = handleInMinorVersion(removedIn, majorVersion);
+ }
+ }
+
+ for (QString qmlElementName : std::as_const(qmlElementNames)) {
+ if (qmlElementName == S_ANONYMOUS)
+ continue;
+ if (qmlElementName == S_AUTO)
+ qmlElementName = className;
+ qmlElementInfos[qmlElementName].append({
+ classDef.inputFile(),
+ className,
+ addedIn,
+ removedIn
+ });
+ }
+
+ // We want all related metatypes to be registered by name, so that we can look them up
+ // without including the C++ headers. That's the reason for the QMetaType(foo).id() calls.
+
+ if (targetIsNamespace) {
+ // We need to figure out if the _target_ is a namespace. If not, it already has a
+ // QMetaType and we don't need to generate one.
+
+ QString targetTypeName = targetName;
+
+ const QList<QAnyStringView> namespaces
+ = MetaTypesJsonProcessor::namespaces(classDef);
+
+ const FoundType target = QmlTypesClassDescription::findType(
+ m_types, m_foreignTypes, targetName, namespaces);
+
+ if (!target.javaScript.isEmpty() && target.native.isEmpty())
+ warning(target.javaScript) << "JavaScript type cannot be used as namespace";
+
+ if (target.native.kind() == MetaType::Kind::Object)
+ targetTypeName += " *"_L1;
+
+ // If there is no foreign type, the local one is a namespace.
+ // Otherwise, only do metaTypeForNamespace if the target _metaobject_ is a namespace.
+ // Not if we merely consider it to be a namespace for QML purposes.
+ if (className == targetName || target.native.kind() == MetaType::Kind::Namespace) {
+ output << uR"(
+ {
+ Q_CONSTINIT static auto metaType = QQmlPrivate::metaTypeForNamespace(
+ [](const QtPrivate::QMetaTypeInterface *) {return &%1::staticMetaObject;},
+ "%2");
+ QMetaType(&metaType).id();
+ })"_s.arg(targetName, targetTypeName);
+ } else {
+ Q_ASSERT(!targetTypeName.isEmpty());
+ output << u"\n QMetaType::fromType<%1>().id();"_s.arg(targetTypeName);
+ }
+
+ auto metaObjectPointer = [](QAnyStringView name) -> QString {
+ QString result;
+ const QLatin1StringView staticMetaObject = "::staticMetaObject"_L1;
+ result.reserve(1 + name.length() + staticMetaObject.length());
+ result.append('&'_L1);
+ name.visit([&](auto view) { result.append(view); });
+ result.append(staticMetaObject);
+ return result;
+ };
+
+ if (!qmlElementNames.isEmpty()) {
+ output << uR"(
+ qmlRegisterNamespaceAndRevisions(%1, "%2", %3, nullptr, %4, %5);)"_s
+ .arg(metaObjectPointer(targetName), m_module)
+ .arg(majorVersion)
+ .arg(metaObjectPointer(className),
+ extendedName.isEmpty() ? QStringLiteral("nullptr")
+ : metaObjectPointer(extendedName));
+ }
+ } else {
+ if (!qmlElementNames.isEmpty()) {
+ auto checkRevisions = [&](const auto &array, QLatin1StringView type) {
+ for (auto it = array.begin(); it != array.end(); ++it) {
+ if (!it->revision.isValid())
+ continue;
+
+ QTypeRevision revision = it->revision;
+ if (m_moduleVersion < revision) {
+ warning(classDef)
+ << className << "is trying to register" << type
+ << it->name
+ << "with future version" << revision
+ << "when module version is only" << m_moduleVersion;
+ }
+ }
+ };
+
+ const Method::Container methods = classDef.methods();
+ const Property::Container properties = classDef.properties();
+
+ if (m_moduleVersion.isValid()) {
+ checkRevisions(properties, S_PROPERTY);
+ checkRevisions(methods, S_METHOD);
+ }
+
+ output << uR"(
+ qmlRegisterTypesAndRevisions<%1>("%2", %3);)"_s.arg(className, m_module).arg(majorVersion);
+
+ const BaseType::Container superClasses = classDef.superClasses();
+
+ for (const BaseType &object : classDef.superClasses()) {
+ if (object.access != Access::Public)
+ continue;
+
+ QAnyStringView superClassName = object.name;
+
+ QVector<QAnyStringView> classesToCheck;
+
+ auto checkForRevisions = [&](QAnyStringView typeName) -> void {
+ auto typeAsMap = findType(typeName);
+
+ if (typeAsMap.isEmpty()) {
+ typeAsMap = findTypeForeign(typeName);
+ if (typeAsMap.isEmpty())
+ return;
+
+ if (!fillTypesRegisteredAnonymously(
+ typeAsMap.properties(), typeName)) {
+ if (!fillTypesRegisteredAnonymously(
+ typeAsMap.sigs(), typeName)) {
+ fillTypesRegisteredAnonymously(
+ typeAsMap.methods(), typeName);
+ }
+ }
+ }
+
+ for (const BaseType &object : typeAsMap.superClasses()) {
+ if (object.access == Access::Public)
+ classesToCheck << object.name;
+ }
+ };
+
+ checkForRevisions(superClassName);
+
+ while (!classesToCheck.isEmpty())
+ checkForRevisions(classesToCheck.takeFirst());
+ }
+ } else {
+ Q_ASSERT(!className.isEmpty());
+ output << uR"(
+ QMetaType::fromType<%1%2>().id();)"_s.arg(
+ className, classDef.kind() == MetaType::Kind::Object ? u" *" : u"");
+ }
+ }
+ }
+
+ for (const auto [qmlName, exportsForSameQmlName] : qmlElementInfos.asKeyValueRange()) {
+ // needs a least two cpp classes exporting the same qml element to potentially have a
+ // conflict
+ if (exportsForSameQmlName.size() < 2)
+ continue;
+
+ // sort exports by versions to find conflicting exports
+ std::sort(exportsForSameQmlName.begin(), exportsForSameQmlName.end());
+ auto conflictingExportStartIt = exportsForSameQmlName.cbegin();
+ while (1) {
+ // conflicting versions evaluate to true under operator==
+ conflictingExportStartIt =
+ std::adjacent_find(conflictingExportStartIt, exportsForSameQmlName.cend());
+ if (conflictingExportStartIt == exportsForSameQmlName.cend())
+ break;
+
+ auto conflictingExportEndIt = std::find_if_not(
+ conflictingExportStartIt, exportsForSameQmlName.cend(),
+ [=](const auto &x) -> bool { return x == *conflictingExportStartIt; });
+ QString registeringCppClasses = conflictingExportStartIt->claimerName;
+ std::for_each(std::next(conflictingExportStartIt), conflictingExportEndIt,
+ [&](const auto &q) {
+ registeringCppClasses += u", %1"_s.arg(conflictingVersionToString(q));
+ });
+ warning(conflictingExportStartIt->fileName)
+ << qmlName << "is registered multiple times by the following C++ classes:"
+ << registeringCppClasses;
+ conflictingExportStartIt = conflictingExportEndIt;
+ }
+ }
+
+ output << uR"(
+ qmlRegisterModule("%1", %2, %3);
+}
+
+static const QQmlModuleRegistration %5("%1", %4);
+)"_s.arg(m_module)
+ .arg(majorVersion)
+ .arg(minorVersion)
+ .arg(functionName, registrationVarName(m_module));
+
+ if (!m_targetNamespace.isEmpty())
+ output << u"} // namespace %1\n"_s.arg(m_targetNamespace);
+}
+
+bool QmlTypeRegistrar::generatePluginTypes(const QString &pluginTypesFile)
+{
+ QmlTypesCreator creator;
+ creator.setOwnTypes(m_types);
+ creator.setForeignTypes(m_foreignTypes);
+ creator.setReferencedTypes(m_referencedTypes);
+ creator.setModule(m_module.toUtf8());
+ creator.setVersion(QTypeRevision::fromVersion(m_moduleVersion.majorVersion(), 0));
+
+ return creator.generate(pluginTypesFile);
+}
+
+void QmlTypeRegistrar::setModuleNameAndNamespace(const QString &module,
+ const QString &targetNamespace)
+{
+ m_module = module;
+ m_targetNamespace = targetNamespace;
+}
+void QmlTypeRegistrar::setModuleVersions(QTypeRevision moduleVersion,
+ const QList<quint8> &pastMajorVersions,
+ bool followForeignVersioning)
+{
+ m_moduleVersion = moduleVersion;
+ m_pastMajorVersions = pastMajorVersions;
+ m_followForeignVersioning = followForeignVersioning;
+}
+void QmlTypeRegistrar::setIncludes(const QList<QString> &includes)
+{
+ m_includes = includes;
+}
+void QmlTypeRegistrar::setTypes(
+ const QVector<MetaType> &types, const QVector<MetaType> &foreignTypes)
+{
+ m_types = types;
+ m_foreignTypes = foreignTypes;
+}
+void QmlTypeRegistrar::setReferencedTypes(const QList<QAnyStringView> &referencedTypes)
+{
+ m_referencedTypes = referencedTypes;
+}
+
+QT_END_NAMESPACE