Revision control

++ // Copyright (C) 2019 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++ // Qt-Security score:significant
++
++ #include "qqmltyperegistrarutils_p.h"
++ #include "qqmltypesclassdescription_p.h"
++
++ #include "qanystringviewutils_p.h"
++ #include "qmetatypesjsonprocessor_p.h"
++ #include "qqmltyperegistrarconstants_p.h"
++
++ #include <QtCore/qcborarray.h>
++ #include <QtCore/qcbormap.h>
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++ using namespace Constants;
++ using namespace Constants::MetatypesDotJson;
++ using namespace Constants::MetatypesDotJson::Qml;
++ using namespace QAnyStringViewUtils;
++
++ template<typename Container>
++ static void collectExtraVersions(const Container &items, QList<QTypeRevision> &extraVersions)
++ {
++ for (const auto &obj : items) {
++ if (obj.revision.isValid() && !extraVersions.contains(obj.revision))
++ extraVersions.append(obj.revision);
++ }
++ }
++
++ struct Compare {
++ bool operator()(const QAnyStringView &typeName, const MetaType &type) const
++ {
++ return typeName < type.qualifiedClassName();
++ }
++
++ bool operator()(const MetaType &type, const QAnyStringView &typeName) const
++ {
++ return type.qualifiedClassName() < typeName;
++ }
++ };
++
++ FoundType::FoundType(const MetaType &single, FoundType::Origin origin)
++ {
++ if (single.inputFile().isEmpty()) {
++ javaScript = single;
++ javaScriptOrigin = origin;
++ } else {
++ native = single;
++ nativeOrigin = origin;
++ }
++ }
++
++ MetaType FoundType::select(const MetaType &category, QAnyStringView relation) const
++ {
++ if (category.inputFile().isEmpty()) {
++ if (javaScript.isEmpty()) {
++ warning(category)
++ << relation << "type of" << category.qualifiedClassName()
++ << "is not a JavaScript type";
++ }
++ return javaScript;
++ }
++
++ if (native.isEmpty()) {
++ warning(category)
++ << relation << "of" << category.qualifiedClassName()
++ << "is not a native type";
++ }
++ return native;
++ }
++
++ FoundType QmlTypesClassDescription::findType(
++ const QList<MetaType> &types, const QList<MetaType> &foreign,
++ const QAnyStringView &name, const QList<QAnyStringView> &namespaces)
++ {
++ const auto tryFindType = [&](QAnyStringView qualifiedName) -> FoundType {
++ FoundType result;
++ for (const QList<MetaType> &t : {types, foreign}) {
++ const auto [first, last] = std::equal_range(
++ t.begin(), t.end(), qualifiedName, Compare());
++ for (auto it = first; it != last; ++it) {
++ Q_ASSERT(it->qualifiedClassName() == qualifiedName);
++
++ if (it->inputFile().isEmpty()) {
++ if (result.javaScript.isEmpty()) {
++ result.javaScript = *it;
++ result.javaScriptOrigin = (&t == &types)
++ ? FoundType::OwnTypes
++ : FoundType::ForeignTypes;
++ } else {
++ warning(result.javaScript)
++ << "Multiple JavaScript types called" << qualifiedName << "found!";
++ }
++ } else if (result.native.isEmpty()) {
++ result.native = *it;
++ result.nativeOrigin = (&t == &types)
++ ? FoundType::OwnTypes
++ : FoundType::ForeignTypes;
++ } else {
++ warning(result.native)
++ << "Multiple C++ types called" << qualifiedName << "found!\n"
++ << "(other occurrence in :"
++ << it->inputFile() << ":" << it->lineNumber() << ")\n"
++ << "This violates the One Definition Rule!";
++ }
++ }
++ }
++
++ return result;
++ };
++
++ if (startsWith(name, QLatin1String("::")))
++ return tryFindType(name.mid(2));
++
++ QString qualified;
++ for (int i = 0, end = namespaces.length(); i != end; ++i) {
++ for (int j = 0; j < end - i; ++j) {
++ namespaces[j].visit([&](auto data) { qualified.append(data); });
++ qualified.append(QLatin1String("::"));
++ }
++ name.visit([&](auto data) { qualified.append(data); });
++ if (const FoundType found = tryFindType(qualified))
++ return found;
++
++ qualified.truncate(0);
++ }
++
++ return tryFindType(name);
++ }
++
++ void QmlTypesClassDescription::collectSuperClasses(
++ const MetaType &classDef, const QList<MetaType> &types,
++ const QList<MetaType> &foreign, CollectMode mode, QTypeRevision defaultRevision)
++ {
++ const QList<QAnyStringView> namespaces = MetaTypesJsonProcessor::namespaces(classDef);
++ QAnyStringView superClassCandidate;
++ for (const BaseType &superObject : std::as_const(classDef.superClasses())) {
++ if (superObject.access == Access::Public) {
++ const QAnyStringView superName = superObject.name;
++
++ const CollectMode superMode = (mode == TopLevel) ? SuperClass : RelatedType;
++ if (const FoundType found = findType(types, foreign, superName, namespaces)) {
++ const MetaType other = found.select(classDef, "Base");
++ collect(other, types, foreign, superMode, defaultRevision);
++ if (mode == TopLevel && superClass.isEmpty())
++ superClass = other.qualifiedClassName();
++ } else {
++ // If we cannot resolve anything but find a correctly formed superObject,
++ // we can at least populate the name. Further tooling might locate the type
++ // in a different module.
++ superClassCandidate = superName;
++ }
++ }
++ }
++
++ if (mode == TopLevel && superClass.isEmpty())
++ superClass = superClassCandidate;
++ }
++
++ void QmlTypesClassDescription::collectInterfaces(const MetaType &classDef)
++ {
++ for (const Interface &iface : classDef.ifaces())
++ implementsInterfaces << interfaceName(iface);
++ }
++
++ void QmlTypesClassDescription::handleRegisterEnumClassesUnscoped(
++ const MetaType &classDef, QAnyStringView value)
++ {
++ if (value == S_FALSE)
++ enforcesScopedEnums = true;
++ else if (value == S_TRUE)
++ warning(classDef) << "Setting RegisterEnumClassesUnscoped to true has no effect.";
++ else
++ warning(classDef) << "Unrecognized value for RegisterEnumClassesUnscoped:" << value;
++ }
++
++ void QmlTypesClassDescription::collectLocalAnonymous(
++ const MetaType &classDef, const QList<MetaType> &types,
++ const QList<MetaType> &foreign, QTypeRevision defaultRevision)
++ {
++ file = classDef.inputFile();
++ lineNumber = classDef.lineNumber();
++
++ resolvedClass = classDef;
++ className = classDef.qualifiedClassName();
++
++ switch (classDef.kind()) {
++ case MetaType::Kind::Object:
++ accessSemantics = DotQmltypes::S_REFERENCE;
++ break;
++ case MetaType::Kind::Gadget:
++ accessSemantics = DotQmltypes::S_VALUE;
++ break;
++ case MetaType::Kind::Namespace:
++ case MetaType::Kind::Unknown:
++ accessSemantics = DotQmltypes::S_NONE;
++ break;
++ }
++
++ for (const ClassInfo &obj : classDef.classInfos()) {
++ if (obj.name == S_DEFAULT_PROPERTY)
++ defaultProp = obj.value;
++ else if (obj.name == S_PARENT_PROPERTY)
++ parentProp = obj.value;
++ else if (obj.name == S_REGISTER_ENUM_CLASSES_UNSCOPED)
++ handleRegisterEnumClassesUnscoped(classDef, obj.value);
++ }
++
++ collectInterfaces(classDef);
++ collectSuperClasses(classDef, types, foreign, TopLevel, defaultRevision);
++ }
++
++ void QmlTypesClassDescription::collect(
++ const MetaType &classDef, const QList<MetaType> &types,
++ const QList<MetaType> &foreign, CollectMode mode, QTypeRevision defaultRevision)
++ {
++ if (file.isEmpty()) {
++ file = classDef.inputFile();
++ lineNumber = classDef.lineNumber();
++ }
++
++ const QAnyStringView classDefName = classDef.className();
++ const QList<QAnyStringView> namespaces = MetaTypesJsonProcessor::namespaces(classDef);
++
++ QAnyStringView foreignTypeName;
++ bool foreignIsNamespace = false;
++ bool isConstructible = false;
++ for (const ClassInfo &obj : classDef.classInfos()) {
++ const QAnyStringView name = obj.name;
++ const QAnyStringView value = obj.value;
++
++ if (name == S_DEFAULT_PROPERTY) {
++ if (mode != RelatedType && defaultProp.isEmpty())
++ defaultProp = value;
++ continue;
++ }
++
++ if (name == S_PARENT_PROPERTY) {
++ if (mode != RelatedType && parentProp.isEmpty())
++ parentProp = value;
++ continue;
++ }
++
++ if (name == S_REGISTER_ENUM_CLASSES_UNSCOPED) {
++ if (mode != RelatedType)
++ handleRegisterEnumClassesUnscoped(classDef, value);
++ continue;
++ }
++
++ if (name == S_ADDED_IN_VERSION) {
++ const QTypeRevision revision = handleInMinorVersion(
++ QTypeRevision::fromEncodedVersion(toInt(value)),
++ defaultRevision.majorVersion());
++ revisions.append(revision);
++ if (mode == TopLevel)
++ addedInRevision = revision;
++ continue;
++ }
++
++ if (name == S_EXTRA_VERSION) {
++ const QTypeRevision revision = handleInMinorVersion(
++ QTypeRevision::fromEncodedVersion(toInt(value)),
++ defaultRevision.majorVersion());
++ revisions.append(revision);
++ continue;
++ }
++
++ if (mode != TopLevel)
++ continue;
++
++ if (name == S_REMOVED_IN_VERSION) {
++ removedInRevision = handleInMinorVersion(
++ QTypeRevision::fromEncodedVersion(toInt(value)),
++ defaultRevision.majorVersion());
++ continue;
++ }
++
++ // These only apply to the original class
++ if (name == S_ELEMENT) {
++ if (value == S_AUTO)
++ elementNames.append(classDefName);
++ else if (value != S_ANONYMOUS)
++ elementNames.append(value);
++ } else if (name == S_CREATABLE) {
++ isCreatable = (value != S_FALSE);
++ } else if (name == S_CREATION_METHOD) {
++ isStructured = (value == S_STRUCTURED);
++ isConstructible = isStructured || (value == S_CONSTRUCT);
++ } else if (name == S_ATTACHED) {
++ if (const FoundType attached = collectRelated(
++ value, types, foreign, defaultRevision, namespaces)) {
++ attachedType = attached.select(classDef, "Attached").qualifiedClassName();
++ }
++ } else if (name == S_EXTENDED) {
++ if (const FoundType extension = collectRelated(
++ value, types, foreign, defaultRevision, namespaces)) {
++ javaScriptExtensionType = extension.javaScript.qualifiedClassName();
++ nativeExtensionType = extension.native.qualifiedClassName();
++ }
++ if (value == classDefName)
++ isSelfExtension = true;
++ } else if (name == S_EXTENSION_IS_JAVA_SCRIPT) {
++ if (value == S_TRUE)
++ extensionIsJavaScript = true;
++ } else if (name == S_EXTENSION_IS_NAMESPACE) {
++ if (value == S_TRUE)
++ extensionIsNamespace = true;
++ } else if (name == S_SEQUENCE) {
++ if (const FoundType element = collectRelated(
++ value, types, foreign, defaultRevision, namespaces)) {
++ sequenceValueType = element.select(classDef, "Sequence value").qualifiedClassName();
++ } else {
++ // TODO: get rid of this once we have JSON data for the builtins.
++ sequenceValueType = value;
++ }
++ } else if (name == S_SINGLETON) {
++ if (value == S_TRUE)
++ isSingleton = true;
++ } else if (name == S_FOREIGN) {
++ foreignTypeName = value;
++ } else if (name == S_FOREIGN_IS_NAMESPACE) {
++ foreignIsNamespace = (value == S_TRUE);
++ } else if (name == S_PRIMITIVE_ALIAS) {
++ primitiveAliases.append(value);
++ } else if (name == S_ROOT) {
++ isRootClass = (value == S_TRUE);
++ } else if (name == S_HAS_CUSTOM_PARSER) {
++ if (value == S_TRUE)
++ hasCustomParser = true;
++ } else if (name == S_DEFERRED_PROPERTY_NAMES) {
++ deferredNames = split(value, QLatin1StringView(","));
++ } else if (name == S_IMMEDIATE_PROPERTY_NAMES) {
++ immediateNames = split(value, QLatin1StringView(","));
++ }
++ }
++
++ if (addedInRevision.isValid() && !elementNames.isEmpty())
++ revisions.append(addedInRevision);
++
++ // If the local type is a namespace the result can only be a namespace,
++ // no matter what the foreign type is.
++ const bool isNamespace = foreignIsNamespace || classDef.kind() == MetaType::Kind::Namespace;
++
++ MetaType resolved = classDef;
++ if (!foreignTypeName.isEmpty()) {
++ // We can re-use a type with own QML.* macros as target of QML.Foreign
++ if (const FoundType found = findType(foreign, types, foreignTypeName, namespaces)) {
++ resolved = found.select(classDef, "Foreign");
++
++ // Default properties and enum classes are always local.
++ defaultProp = {};
++ enforcesScopedEnums = false;
++
++ // Foreign type can have a default property or an attached type,
++ // or RegisterEnumClassesUnscoped classinfo.
++ for (const ClassInfo &obj : resolved.classInfos()) {
++ const QAnyStringView foreignName = obj.name;
++ const QAnyStringView foreignValue = obj.value;
++ if (defaultProp.isEmpty() && foreignName == S_DEFAULT_PROPERTY) {
++ defaultProp = foreignValue;
++ } else if (parentProp.isEmpty() && foreignName == S_PARENT_PROPERTY) {
++ parentProp = foreignValue;
++ } else if (foreignName == S_REGISTER_ENUM_CLASSES_UNSCOPED) {
++ handleRegisterEnumClassesUnscoped(resolved, foreignValue);
++ } else if (foreignName == S_ATTACHED) {
++ if (const FoundType attached = collectRelated(
++ foreignValue, types, foreign, defaultRevision, namespaces)) {
++ attachedType = attached.select(resolved, "Attached").qualifiedClassName();
++ }
++ } else if (foreignName == S_EXTENDED) {
++ if (const FoundType extension = collectRelated(
++ foreignValue, types, foreign, defaultRevision, namespaces)) {
++ nativeExtensionType = extension.native.qualifiedClassName();
++ javaScriptExtensionType = extension.javaScript.qualifiedClassName();
++ }
++ } else if (foreignName == S_EXTENSION_IS_JAVA_SCRIPT) {
++ if (foreignValue == S_TRUE)
++ extensionIsJavaScript = true;
++ } else if (foreignName == S_EXTENSION_IS_NAMESPACE) {
++ if (foreignValue == S_TRUE)
++ extensionIsNamespace = true;
++ } else if (foreignName == S_SEQUENCE) {
++ if (const FoundType element = collectRelated(
++ foreignValue, types, foreign, defaultRevision, namespaces)) {
++ sequenceValueType
++ = element.select(resolved, "Sequence value").qualifiedClassName();
++ }
++ }
++ }
++ if (mode == TopLevel) {
++ resolvedFile = resolved.inputFile();
++ lineNumberInResolvedFile = resolved.lineNumber();
++ }
++ } else {
++ className = foreignTypeName;
++ resolved = MetaType();
++ }
++ }
++
++ if (!resolved.isEmpty()) {
++ if (mode == RelatedType || !elementNames.isEmpty()) {
++ collectExtraVersions(resolved.properties(), revisions);
++ collectExtraVersions(resolved.methods(), revisions);
++ collectExtraVersions(resolved.sigs(), revisions);
++ }
++
++ collectSuperClasses(resolved, types, foreign, mode, defaultRevision);
++ }
++
++ if (mode != TopLevel)
++ return;
++
++ if (!resolved.isEmpty())
++ collectInterfaces(resolved);
++
++ if (!addedInRevision.isValid()) {
++ addedInRevision = defaultRevision;
++ }
++ if (addedInRevision <= defaultRevision
++ && (!removedInRevision.isValid() || defaultRevision < removedInRevision)) {
++ revisions.append(defaultRevision);
++ }
++
++ std::sort(revisions.begin(), revisions.end());
++ const auto end = std::unique(revisions.begin(), revisions.end());
++ revisions.erase(QList<QTypeRevision>::const_iterator(end), revisions.constEnd());
++
++ resolvedClass = resolved;
++ if (className.isEmpty() && !resolved.isEmpty())
++ className = resolved.qualifiedClassName();
++
++ if (!sequenceValueType.isEmpty()) {
++ isCreatable = false;
++ accessSemantics = DotQmltypes::S_SEQUENCE;
++ } else if (isNamespace) {
++ isCreatable = false;
++ accessSemantics = DotQmltypes::S_NONE;
++ } else if (resolved.kind() == MetaType::Kind::Object) {
++ accessSemantics = DotQmltypes::S_REFERENCE;
++ } else {
++ isCreatable = isConstructible;
++
++ if (resolved.isEmpty()) {
++ if (elementNames.isEmpty()) {
++ // If no resolved, we generally assume it's a value type defined by the
++ // foreign/extended trick.
++ accessSemantics = DotQmltypes::S_VALUE;
++ }
++
++ for (auto elementName = elementNames.begin(); elementName != elementNames.end();) {
++ if (elementName->isEmpty() || elementName->front().isLower()) {
++ // If no resolved, we generally assume it's a value type defined by the
++ // foreign/extended trick.
++ accessSemantics = DotQmltypes::S_VALUE;
++ ++elementName;
++ } else {
++ // Objects and namespaces always have metaobjects and therefore classDefs.
++ // However, we may not be able to resolve the metaobject at compile time. See
++ // the "Invisible" test case. In that case, we must not assume anything about
++ // access semantics.
++
++ warning(classDef)
++ << "Refusing to generate non-lowercase name"
++ << *elementName << "for unknown foreign type";
++ elementName = elementNames.erase(elementName);
++
++ if (elementNames.isEmpty()) {
++ // Make it completely inaccessible.
++ // We cannot get enums from anonymous types after all.
++ accessSemantics = DotQmltypes::S_NONE;
++ }
++ }
++ }
++ } else if (resolved.kind() == MetaType::Kind::Gadget) {
++ accessSemantics = DotQmltypes::S_VALUE;
++ } else {
++ accessSemantics = DotQmltypes::S_NONE;
++ }
++ }
++ }
++
++ FoundType QmlTypesClassDescription::collectRelated(
++ QAnyStringView related, const QList<MetaType> &types, const QList<MetaType> &foreign,
++ QTypeRevision defaultRevision, const QList<QAnyStringView> &namespaces)
++ {
++ if (FoundType other = findType(types, foreign, related, namespaces)) {
++ if (!other.native.isEmpty())
++ collect(other.native, types, foreign, RelatedType, defaultRevision);
++ if (!other.javaScript.isEmpty())
++ collect(other.javaScript, types, foreign, RelatedType, defaultRevision);
++ return other;
++ }
++ return FoundType();
++ }
++
++ struct UsingCompare {
++ bool operator()(const UsingDeclaration &a, QAnyStringView b) const
++ {
++ return a.alias < b;
++ }
++
++ bool operator()(QAnyStringView a, const UsingDeclaration &b) const
++ {
++ return a < b.alias;
++ }
++ };
++
++ ResolvedTypeAlias::ResolvedTypeAlias(
++ QAnyStringView alias, const QList<UsingDeclaration> &usingDeclarations)
++ : type(alias)
++ {
++ handleVoid();
++ if (type.isEmpty())
++ return;
++
++ handleList();
++
++ if (!isList) {
++ handlePointer();
++ handleConst();
++ }
++
++ while (true) {
++ const auto usingDeclaration = std::equal_range(
++ usingDeclarations.begin(), usingDeclarations.end(), type, UsingCompare());
++ if (usingDeclaration.first == usingDeclaration.second)
++ break;
++
++ type = usingDeclaration.first->original;
++ handleVoid();
++ if (type.isEmpty())
++ return;
++
++ if (isPointer) {
++ handleConst();
++ continue;
++ }
++
++ if (!isList) {
++ handleList();
++ if (!isList) {
++ handlePointer();
++ handleConst();
++ }
++ }
++ }
++ }
++
++ void ResolvedTypeAlias::handleVoid()
++ {
++ if (!isPointer && type == "void")
++ type = "";
++ }
++
++ void ResolvedTypeAlias::handleList()
++ {
++ for (QLatin1StringView list : {"QQmlListProperty<"_L1, "QList<"_L1}) {
++ if (!startsWith(type, list) || type.back() != '>'_L1)
++ continue;
++
++ const int listSize = list.size();
++ const QAnyStringView elementType = trimmed(type.mid(listSize, type.size() - listSize - 1));
++
++ // QQmlListProperty internally constructs the pointer. Passing an explicit '*' will
++ // produce double pointers. QList is only for value types. We can't handle QLists
++ // of pointers (unless specially registered, but then they're not isList).
++ if (elementType.back() == '*'_L1)
++ continue;
++
++ isList = true;
++ type = elementType;
++ return;
++ }
++ }
++
++ void ResolvedTypeAlias::handlePointer()
++ {
++ if (type.back() == '*'_L1) {
++ isPointer = true;
++ type = type.chopped(1);
++ while (type.back().isSpace())
++ type.chop(1);
++ }
++ }
++
++ void ResolvedTypeAlias::handleConst()
++ {
++ if (startsWith(type, "const "_L1)) {
++ isConstant = true;
++ type = type.sliced(strlen("const "));
++ }
++ }
++
++ QT_END_NAMESPACE