diff options
author | Andrei Golubev <andrei.golubev@qt.io> | 2021-02-16 12:52:41 +0100 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2021-12-17 14:39:09 +0000 |
commit | dbc134e207943b23edb90552a89d02b8f43812af (patch) | |
tree | dec2b4319ca6f2b9499d1ca77ee241f35c792866 /tools/qmltc/prototype/codegenerator.cpp | |
parent | 39433310aa3a25e2711ce426d456a8e78e3cd325 (diff) |
Use qmltc compiler prototype as a fallback implementation
Task-number: QTBUG-91927
Task-number: QTBUG-96041
Task-number: QTBUG-84368
Change-Id: I47320b5f3ed8efff6fb234778df5fae5be5b64f2
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
(cherry picked from commit e7ce5abf24f04d1b071343f07ca28b6a5d9ad4b9)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
Diffstat (limited to 'tools/qmltc/prototype/codegenerator.cpp')
-rw-r--r-- | tools/qmltc/prototype/codegenerator.cpp | 1857 |
1 files changed, 1857 insertions, 0 deletions
diff --git a/tools/qmltc/prototype/codegenerator.cpp b/tools/qmltc/prototype/codegenerator.cpp new file mode 100644 index 0000000000..d02c98f04b --- /dev/null +++ b/tools/qmltc/prototype/codegenerator.cpp @@ -0,0 +1,1857 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "prototype/codegenerator.h" +#include "prototype/qml2cppdefaultpasses.h" +#include "prototype/qml2cpppropertyutils.h" +#include "prototype/codegeneratorutil.h" +#include "prototype/codegeneratorwriter.h" + +#include <QtCore/qfileinfo.h> +#include <QtCore/qhash.h> +#include <QtCore/qset.h> +#include <QtCore/qregularexpression.h> + +#include <QtCore/qloggingcategory.h> + +#include <optional> +#include <utility> +#include <numeric> + +static constexpr char newLineLatin1[] = +#ifdef Q_OS_WIN32 + "\r\n"; +#else + "\n"; +#endif + +Q_LOGGING_CATEGORY(lcCodeGenerator, "qml.qmltc.compiler", QtWarningMsg); + +static void writeToFile(const QString &path, const QByteArray &data) +{ + // When not using dependency files, changing a single qml invalidates all + // qml files and would force the recompilation of everything. To avoid that, + // we check if the data is equal to the existing file, if yes, don't touch + // it so the build system will not recompile unnecessary things. + // + // If the build system use dependency file, we should anyway touch the file + // so qmlcompiler is not re-run + QFileInfo fi(path); + if (fi.exists() && fi.size() == data.size()) { + QFile oldFile(path); + oldFile.open(QIODevice::ReadOnly); + if (oldFile.readAll() == data) + return; + } + QFile file(path); + file.open(QIODevice::WriteOnly); + file.write(data); +} + +static QString figureReturnType(const QQmlJSMetaMethod &m) +{ + const bool isVoidMethod = + m.returnTypeName() == u"void" || m.methodType() == QQmlJSMetaMethod::Signal; + Q_ASSERT(isVoidMethod || m.returnType()); + // TODO: should really be m.returnTypeName(). for now, avoid inconsistencies + // due to QML/C++ name collisions and unique C++ name generation + QString type; + if (isVoidMethod) { + type = u"void"_qs; + } else { + type = CodeGeneratorUtility::getInternalNameAwareOfAccessSemantics(m.returnType()); + } + return type; +} + +static QList<QQmlJSAotVariable> +compileMethodParameters(const QStringList &names, + const QList<QSharedPointer<const QQmlJSScope>> &types, + bool allowUnnamed = false) +{ + QList<QQmlJSAotVariable> paramList; + const auto size = names.size(); + paramList.reserve(size); + for (qsizetype i = 0; i < size; ++i) { + Q_ASSERT(types[i]); // assume verified + QString name = names[i]; + Q_ASSERT(allowUnnamed || !name.isEmpty()); // assume verified + if (name.isEmpty() && allowUnnamed) + name = u"unnamed_" + QString::number(i); + paramList.emplaceBack(QQmlJSAotVariable { + CodeGeneratorUtility::getInternalNameAwareOfAccessSemantics(types[i]), names[i], + QString() }); + } + return paramList; +} + +// this version just returns runtimeFunctionIndices[relative]. used in +// property assignments like `property var p: function() {}` +static qsizetype relativeToAbsoluteRuntimeIndex(const QmlIR::Object *irObject, qsizetype relative) +{ + // TODO: for some reason, this function is necessary. do we really need it? + // why does it have to be a special case? + return irObject->runtimeFunctionIndices.at(relative); +} + +// this version expects that \a relative points to a to-be-executed function. +// for this, it needs to detect whether a case like `onSignal: function() {}` is +// present and return nested function index instead of the +// runtimeFunctionIndices[relative] +static qsizetype relativeToAbsoluteRuntimeIndex(const QmlIR::Document *doc, + const QmlIR::Object *irObject, qsizetype relative) +{ + int absoluteIndex = irObject->runtimeFunctionIndices.at(relative); + Q_ASSERT(absoluteIndex >= 0); + Q_ASSERT(doc->javaScriptCompilationUnit.unitData()); + const QV4::CompiledData::Function *f = + doc->javaScriptCompilationUnit.unitData()->functionAt(absoluteIndex); + Q_ASSERT(f); + if (f->nestedFunctionIndex != std::numeric_limits<uint32_t>::max()) + return f->nestedFunctionIndex; + return absoluteIndex; +} + +// finds property for given scope and returns it together with the absolute +// property index in the property array of the corresponding QMetaObject. +static std::pair<QQmlJSMetaProperty, int> getMetaPropertyIndex(const QQmlJSScope::ConstPtr &scope, + const QString &propertyName) +{ + auto owner = QQmlJSScope::ownerOfProperty(scope, propertyName); + Q_ASSERT(owner); + auto p = owner->ownProperty(propertyName); + if (!p.isValid()) + return { p, -1 }; + int index = p.index(); + if (index < 0) // this property doesn't have index - comes from QML + return { p, -1 }; + // owner is not included in absolute index + for (QQmlJSScope::ConstPtr base = owner->baseType(); base; base = base->baseType()) + index += int(base->ownProperties().size()); + return { p, index }; +} + +struct QmlIrBindingCompare +{ +private: + using T = QmlIR::Binding; + using I = typename QmlIR::PoolList<T>::Iterator; + static QHash<uint, qsizetype> orderTable; + +public: + bool operator()(const I &x, const I &y) const + { + return orderTable[x->type] < orderTable[y->type]; + } + bool operator()(const T &x, const T &y) const + { + return orderTable[x.type] < orderTable[y.type]; + } +}; + +QHash<uint, qsizetype> QmlIrBindingCompare::orderTable = { + { QmlIR::Binding::Type_Invalid, 100 }, + // value assignments (and object bindings) are "equal" + { QmlIR::Binding::Type_Boolean, 0 }, + { QmlIR::Binding::Type_Number, 0 }, + { QmlIR::Binding::Type_String, 0 }, + { QmlIR::Binding::Type_Object, 0 }, + // translations are also "equal" between themselves, but come after + // assignments + { QmlIR::Binding::Type_Translation, 1 }, + { QmlIR::Binding::Type_TranslationById, 1 }, + // attached properties and group properties might contain assignments + // inside, so come next + { QmlIR::Binding::Type_AttachedProperty, 2 }, + { QmlIR::Binding::Type_GroupProperty, 2 }, + // JS bindings come last because they can use values from other categories + { QmlIR::Binding::Type_Script, 3 }, + // { QmlIR::Binding::Type_Null, 100 }, // TODO: what is this used for? +}; + +static QList<typename QmlIR::PoolList<QmlIR::Binding>::Iterator> +toOrderedSequence(typename QmlIR::PoolList<QmlIR::Binding>::Iterator first, + typename QmlIR::PoolList<QmlIR::Binding>::Iterator last, qsizetype n) +{ + // bindings actually have to sorted so that e.g. value assignments come + // before script bindings. this is important for the code generator as it + // would just emit instructions one by one, regardless of the ordering + using I = typename QmlIR::PoolList<QmlIR::Binding>::Iterator; + QList<I> sorted; + sorted.reserve(n); + for (auto it = first; it != last; ++it) + sorted << it; + // NB: use stable sort for bindings, because this matters: relative order of + // bindings must be preserved - this might affect UI + std::stable_sort(sorted.begin(), sorted.end(), QmlIrBindingCompare {}); + return sorted; +} + +Q_LOGGING_CATEGORY(lcCodeGen, "qml.compiler.CodeGenerator", QtWarningMsg); + +CodeGenerator::CodeGenerator(const QString &url, QQmlJSLogger *logger, QmlIR::Document *doc, + const Qmltc::TypeResolver *localResolver) + : m_url(url), + m_logger(logger), + m_doc(doc), + m_localTypeResolver(localResolver), + m_qmlSource(doc->code.split(QLatin1String(newLineLatin1))) +{ +} + +void CodeGenerator::constructObjects(QSet<QString> &requiredCppIncludes) +{ + const auto &objects = m_doc->objects; + m_objects.reserve(objects.size()); + + m_typeToObjectIndex.reserve(objects.size()); + + for (qsizetype objectIndex = 0; objectIndex != objects.size(); ++objectIndex) { + QmlIR::Object *irObject = objects[objectIndex]; + if (!irObject) { + recordError(QQmlJS::SourceLocation {}, + u"Internal compiler error: IR object is null"_qs); + return; + } + QQmlJSScope::Ptr object = m_localTypeResolver->scopeForLocation(irObject->location); + if (!object) { + recordError(irObject->location, u"Object of unknown type"_qs); + return; + } + m_typeToObjectIndex.insert(object, objectIndex); + m_objects.emplaceBack(CodeGenObject { irObject, object }); + } + + // objects are constructed, now we can run compiler passes to make sure they + // are in good state + Qml2CppCompilerPassExecutor executor(m_doc, m_localTypeResolver, m_url, m_objects, + m_typeToObjectIndex); + executor.addPass(&verifyTypes); + executor.addPass(&checkForNamingCollisionsWithCpp); + executor.addPass([this](const Qml2CppContext &context, QList<Qml2CppObject> &objects) { + m_typeCounts = makeUniqueCppNames(context, objects); + }); + const auto setupQmlBaseTypes = [&](const Qml2CppContext &context, + QList<Qml2CppObject> &objects) { + m_qmlCompiledBaseTypes = setupQmlCppTypes(context, objects); + }; + executor.addPass(setupQmlBaseTypes); + const auto resolveAliases = [&](const Qml2CppContext &context, QList<Qml2CppObject> &objects) { + m_aliasesToIds = deferredResolveValidateAliases(context, objects); + }; + executor.addPass(resolveAliases); + const auto populateCppIncludes = [&](const Qml2CppContext &context, + QList<Qml2CppObject> &objects) { + requiredCppIncludes = findCppIncludes(context, objects); + }; + executor.addPass(populateCppIncludes); + const auto resolveImplicitComponents = [&](const Qml2CppContext &context, + QList<Qml2CppObject> &objects) { + m_implicitComponentMapping = findAndResolveImplicitComponents(context, objects); + }; + executor.addPass(resolveImplicitComponents); + executor.addPass(&setObjectIds); + // run all passes: + executor.run(m_logger); +} + +void CodeGenerator::generate(const Options &options) +{ + m_options = options; + GeneratedCode code; + const QString rootClassName = QFileInfo(m_url).baseName(); + Q_ASSERT(!rootClassName.isEmpty()); + Q_ASSERT(!options.outputHFile.isEmpty()); + Q_ASSERT(!options.outputCppFile.isEmpty()); + const QString hPath = options.outputHFile; + const QString cppPath = options.outputCppFile; + m_isAnonymous = rootClassName.at(0).isLower(); + const QString url = QFileInfo(m_url).fileName(); + + // generate url method straight away to be able to use it everywhere + compileUrlMethod(); + + QSet<QString> requiredCppIncludes; + constructObjects(requiredCppIncludes); // this populates all the codegen objects + // no point in compiling anything if there are errors + if (m_logger->hasErrors() || m_logger->hasWarnings()) + return; + + const auto isWithinSubComponent = [&](QQmlJSScope::ConstPtr type) { + for (; type; type = type->parentScope()) { + qsizetype index = m_typeToObjectIndex.value(type, -1); + if (index < 0) + return false; + const QmlIR::Object *irObject = m_objects[index].irObject; + if (irObject->flags & QV4::CompiledData::Object::IsComponent) + return true; + } + return false; + }; + + // compile everything + QList<QQmlJSAotObject> compiledObjects; + compiledObjects.reserve(m_objects.size()); + for (const auto &object : m_objects) { + if (object.type->scopeType() != QQmlJSScope::QMLScope) { + qCDebug(lcCodeGenerator) << u"Scope '" + object.type->internalName() + + u"' is not QMLScope, so it is skipped."; + continue; + } + if (isWithinSubComponent(object.type)) { + // e.g. when creating a view delegate + qCDebug(lcCodeGenerator) << u"Scope '" + object.type->internalName() + + u"' is a QQmlComponent sub-component. It won't be compiled to C++."; + continue; + } + compiledObjects.emplaceBack(); // create new object + compileObject(compiledObjects.back(), object); + } + // no point in generating anything if there are errors + if (m_logger->hasErrors() || m_logger->hasWarnings()) + return; + + QQmlJSProgram program { compiledObjects, m_urlMethod, url, hPath, cppPath, + options.outNamespace, requiredCppIncludes }; + + // write everything + GeneratedCodeUtils codeUtils(code); + CodeGeneratorWriter::write(codeUtils, program); + + writeToFile(hPath, code.header.toUtf8()); + writeToFile(cppPath, code.implementation.toUtf8()); +} + +QString buildCallSpecialMethodValue(bool documentRoot, const QString &outerFlagName, + bool overridesInterface) +{ + const QString callInBase = overridesInterface ? u"false"_qs : u"true"_qs; + if (documentRoot) { + return outerFlagName + u" && " + callInBase; + } else { + return callInBase; + } +} + +void CodeGenerator::compileObject(QQmlJSAotObject &compiled, const CodeGenObject &object) +{ + compiled.cppType = object.type->internalName(); + const QString baseClass = object.type->baseType()->internalName(); + + const bool baseTypeIsCompiledQml = m_qmlCompiledBaseTypes.contains(object.type->baseTypeName()); + const qsizetype objectIndex = m_typeToObjectIndex[object.type]; + const bool documentRoot = objectIndex == 0; + const bool hasParserStatusInterface = object.type->hasInterface(u"QQmlParserStatus"_qs); + const bool hasFinalizerHookInterface = object.type->hasInterface(u"QQmlFinalizerHook"_qs); + + compiled.baseClasses = { baseClass }; + + // add ctors code + compiled.baselineCtor.access = QQmlJSMetaMethod::Protected; + compiled.externalCtor.access = QQmlJSMetaMethod::Public; + compiled.init.access = QQmlJSMetaMethod::Protected; + // TODO: all below could actually be hidden? (but need to befriend the + // document root, etc.) + compiled.endInit.access = QQmlJSMetaMethod::Public; + compiled.completeComponent.access = QQmlJSMetaMethod::Public; + compiled.finalizeComponent.access = QQmlJSMetaMethod::Public; + compiled.handleOnCompleted.access = QQmlJSMetaMethod::Public; + + compiled.baselineCtor.name = compiled.cppType; + compiled.externalCtor.name = compiled.cppType; + compiled.init.name = u"QML_init"_qs; + compiled.init.returnType = u"QQmlRefPointer<QQmlContextData>"_qs; + compiled.endInit.name = u"QML_endInit"_qs; + compiled.endInit.returnType = u"void"_qs; + compiled.completeComponent.name = u"QML_completeComponent"_qs; + compiled.completeComponent.returnType = u"void"_qs; + compiled.finalizeComponent.name = u"QML_finalizeComponent"_qs; + compiled.finalizeComponent.returnType = u"void"_qs; + compiled.handleOnCompleted.name = u"QML_handleOnCompleted"_qs; + compiled.handleOnCompleted.returnType = u"void"_qs; + + QQmlJSAotVariable engine(u"QQmlEngine *"_qs, u"engine"_qs, QString()); + QQmlJSAotVariable parent(u"QObject *"_qs, u"parent"_qs, u"nullptr"_qs); + compiled.baselineCtor.parameterList = { parent }; + compiled.externalCtor.parameterList = { engine, parent }; + QQmlJSAotVariable ctxtdata(u"const QQmlRefPointer<QQmlContextData> &"_qs, u"parentContext"_qs, + QString()); + QQmlJSAotVariable finalizeFlag(u"bool"_qs, u"canFinalize"_qs, QString()); + QQmlJSAotVariable callSpecialMethodFlag(u"bool"_qs, u"callSpecialMethodNow"_qs, QString()); + if (documentRoot) { + compiled.init.parameterList = { engine, ctxtdata, finalizeFlag, callSpecialMethodFlag }; + compiled.endInit.parameterList = { engine, finalizeFlag }; + compiled.completeComponent.parameterList = { callSpecialMethodFlag }; + compiled.finalizeComponent.parameterList = { callSpecialMethodFlag }; + } else { + compiled.init.parameterList = { engine, ctxtdata }; + compiled.endInit.parameterList = { engine, CodeGeneratorUtility::compilationUnitVariable }; + } + + if (baseTypeIsCompiledQml) { + // call baseline ctor of the QML-originated base class. it also takes + // care of QObject::setParent() call + compiled.baselineCtor.initializerList = { baseClass + u"(parent)" }; + } else { + // default call to ctor is enough, but QQml_setParent_noEvent() - is + // needed (note, this is a faster version of QObject::setParent()) + compiled.baselineCtor.body << u"QQml_setParent_noEvent(this, " + parent.name + u");"; + } + + compiled.externalCtor.initializerList = { compiled.baselineCtor.name + u"(parent)" }; + if (documentRoot) { + compiled.externalCtor.body << u"// document root:"_qs; + compiled.endInit.body << u"auto " + CodeGeneratorUtility::compilationUnitVariable.name + + u" = " + u"QQmlEnginePrivate::get(engine)->compilationUnitFromUrl(" + + m_urlMethod.name + u"());"; + + // call init method of the document root + compiled.externalCtor.body << compiled.init.name + + u"(engine, QQmlContextData::get(engine->rootContext()), /* finalize */ " + u"true, /* call special method */ true);"; + } else { + compiled.externalCtor.body << u"// not document root:"_qs; + compiled.externalCtor.body + << compiled.init.name + u"(engine, QQmlData::get(parent)->outerContext);"; + } + + compiled.init.body << u"Q_UNUSED(engine);"_qs; + if (documentRoot) { + compiled.init.body << u"Q_UNUSED(" + callSpecialMethodFlag.name + u")"; + compiled.completeComponent.body << u"Q_UNUSED(" + callSpecialMethodFlag.name + u")"; + compiled.finalizeComponent.body << u"Q_UNUSED(" + callSpecialMethodFlag.name + u")"; + } + + // compiled.init.body << u"Q_UNUSED(" + finalizeFlag.name + u");"; + compiled.init.body << u"auto context = parentContext;"_qs; + // TODO: context hierarchy is way-over-the-top complicated already + + // -1. if the parent scope of this type has base type as compiled qml and + // this parent scope is not a root object, we have to go one level up in the + // context. in a nutshell: + // * parentScope->outerContext == parentContext of this type + // * parentScope->outerContext != context of this document + // * parentScope->outerContext is a child of context of this document + // > to ensure correct context, we must use context->parent() instead of + // > parentContext + if (QQmlJSScope::ConstPtr parent = object.type->parentScope(); parent + && m_qmlCompiledBaseTypes.contains(parent->baseTypeName()) + && m_typeToObjectIndex[parent] != 0) { + compiled.init.body << u"// NB: context->parent() is the context of the root "_qs; + compiled.init.body << u"context = context->parent();"_qs; + } + + // 0. call parent's init if necessary + if (baseTypeIsCompiledQml) { + compiled.init.body << u"// 0. call parent's init"_qs; + QString lhs; + if (documentRoot) + lhs = u"context = "_qs; + const QString callParserStatusSpecialMethod = buildCallSpecialMethodValue( + documentRoot, callSpecialMethodFlag.name, hasParserStatusInterface); + compiled.init.body << lhs + baseClass + u"::" + compiled.init.name + + u"(engine, context, /* finalize */ false, /* call special method */ " + + callParserStatusSpecialMethod + u");"; + + compiled.completeComponent.body << u"// call parent's completeComponent"_qs; + compiled.completeComponent.body << baseClass + u"::" + compiled.completeComponent.name + + u"(" + callParserStatusSpecialMethod + u");"; + + const QString callFinalizerHookSpecialMethod = buildCallSpecialMethodValue( + documentRoot, callSpecialMethodFlag.name, hasFinalizerHookInterface); + compiled.finalizeComponent.body << u"// call parent's finalizeComponent"_qs; + compiled.finalizeComponent.body << baseClass + u"::" + compiled.finalizeComponent.name + + u"(" + callFinalizerHookSpecialMethod + u");"; + + compiled.handleOnCompleted.body << u"// call parent's Component.onCompleted handler"_qs; + compiled.handleOnCompleted.body + << baseClass + u"::" + compiled.handleOnCompleted.name + u"();"; + } + // 1. create new context through QQmlCppContextRegistrator + if (documentRoot) { + Q_ASSERT(objectIndex == 0); + compiled.init.body << u"// 1. create context for this type (root)"_qs; + compiled.init.body + << QStringLiteral( + "context = %1->createInternalContext(%1->compilationUnitFromUrl(%2()), " + "context, 0, true);") + .arg(u"QQmlEnginePrivate::get(engine)"_qs, m_urlMethod.name); + } else { + // non-root objects adopt parent context and use that one instead of + // creating own context + compiled.init.body << u"// 1. use current as context of this type (non-root)"_qs; + compiled.init.body << u"// context = context;"_qs; + } + + // TODO: optimize step 2: do we need context = parentContext? simplify + // QQmlCppContextRegistrator::set also + + // 2. + if (baseTypeIsCompiledQml && !documentRoot) { + } else { // !baseTypeIsCompiledQml || documentRoot + // set up current context + compiled.init.body << u"// 2. normal flow, set context for this object"_qs; + const QString enumValue = documentRoot ? u"QQmlContextData::DocumentRoot"_qs + : u"QQmlContextData::OrdinaryObject"_qs; + compiled.init.body << QStringLiteral( + "%1->setInternalContext(this, context, QQmlContextData::%2);") + .arg(u"QQmlEnginePrivate::get(engine)"_qs, enumValue); + if (documentRoot) + compiled.init.body << u"context->setContextObject(this);"_qs; + } + // 3. set id if it's present in the QML document + if (!m_doc->stringAt(object.irObject->idNameIndex).isEmpty()) { + Q_ASSERT(object.irObject->id >= 0); + compiled.init.body << u"// 3. set id since it exists"_qs; + compiled.init.body << u"context->setIdValue(" + QString::number(object.irObject->id) + + u", this);"; + } + + // TODO: we might want to optimize storage space when there are no object + // bindings, but this requires deep checking (e.g. basically go over all + // bindings and all bindings of attached/grouped properties) + compiled.init.body << u"// create objects for object bindings in advance:"_qs; + // TODO: support private and protected variables + compiled.variables.emplaceBack(CodeGeneratorUtility::childrenOffsetVariable); + compiled.init.body << CodeGeneratorUtility::childrenOffsetVariable.name + + u" = QObject::children().size();"; + + // magic step: if the type has a QQmlParserStatus interface, we should call + // it's method here + if (hasParserStatusInterface) { + const QString indent = documentRoot ? u" "_qs : QString(); + + compiled.init.body << u"// this type has QQmlParserStatus interface:"_qs; + compiled.init.body << u"Q_ASSERT(dynamic_cast<QQmlParserStatus *>(this) != nullptr);"_qs; + if (documentRoot) + compiled.init.body << u"if (" + callSpecialMethodFlag.name + u")"; + compiled.init.body << indent + u"this->classBegin();"; + + compiled.completeComponent.lastLines << u"// this type has QQmlParserStatus interface:"_qs; + compiled.completeComponent.lastLines + << u"Q_ASSERT(dynamic_cast<QQmlParserStatus *>(this) != nullptr);"_qs; + if (documentRoot) + compiled.completeComponent.lastLines << u"if (" + callSpecialMethodFlag.name + u")"; + compiled.completeComponent.lastLines << indent + u"this->componentComplete();"; + } + + // magic step: if the type has a QQmlFinalizerHook interface, we should call + // it's method here + if (hasFinalizerHookInterface) { + QString indent; + compiled.finalizeComponent.lastLines << u"// this type has QQmlFinalizerHook interface:"_qs; + compiled.finalizeComponent.lastLines + << u"Q_ASSERT(dynamic_cast<QQmlFinalizerHook *>(this) != nullptr);"_qs; + if (documentRoot) { + compiled.finalizeComponent.lastLines << u"if (" + callSpecialMethodFlag.name + u")"; + indent = u" "_qs; + } + compiled.finalizeComponent.lastLines << indent + u"this->componentFinalized();"_qs; + } + + // NB: step 4 - and everything below that - is done at the very end of QML + // init and so we use lastLines. we need to make sure that objects from + // object bindings are created (and initialized) before we start the + // finalization: we need fully-set context at the beginning of QML finalize. + + // 4. finalize if necessary + if (documentRoot) { + compiled.init.lastLines << u"// 4. document root, call finalize"_qs; + compiled.init.lastLines << u"if (" + finalizeFlag.name + u") {"; + compiled.init.lastLines << u" " + compiled.endInit.name + + u"(engine, /* finalize */ true);"; + compiled.init.lastLines << u"}"_qs; + } + // return + compiled.init.lastLines << u"return context;"_qs; + + // TODO: is property update group needed? + compiled.endInit.body << u"Q_UNUSED(engine);"_qs; + compiled.endInit.body << u"Q_UNUSED(" + CodeGeneratorUtility::compilationUnitVariable.name + + u")"_qs; + // compiled.endInit.body << u"Q_UNUSED(" + finalizeFlag.name + u");"; + if (baseTypeIsCompiledQml) { + compiled.endInit.body << u"{ // call parent's finalize"_qs; + compiled.endInit.body << baseClass + u"::" + compiled.endInit.name + + u"(engine, /* finalize */ false);"; + compiled.endInit.body << u"}"_qs; + } + // TODO: decide whether begin/end property update group is needed + // compiled.endInit.body << u"Qt::beginPropertyUpdateGroup(); // defer binding evaluation"_qs; + + // add basic MOC stuff + compiled.mocCode = { + u"Q_OBJECT"_qs, + (m_isAnonymous ? u"QML_ANONYMOUS"_qs : u"QML_ELEMENT"_qs), + }; + + if (object.type->isSingleton()) { + if (m_isAnonymous) { + recordError(object.type->sourceLocation(), + QStringLiteral(u"This singleton type won't be accessible from the outside. " + "Consider changing the file name so that it starts with a " + "capital letter.")); + return; + } + compiled.mocCode << u"QML_SINGLETON"_qs; + compiled.externalCtor.access = QQmlJSMetaMethod::Private; + } + + // compile enums + const auto enums = object.type->ownEnumerations(); + compiled.enums.reserve(enums.size()); + for (auto it = enums.cbegin(); it != enums.cend(); ++it) + compileEnum(compiled, it.value()); + + // compile properties in order + auto properties = object.type->ownProperties().values(); + compiled.variables.reserve(properties.size()); + std::sort(properties.begin(), properties.end(), + [](const QQmlJSMetaProperty &x, const QQmlJSMetaProperty &y) { + return x.index() < y.index(); + }); + for (const QQmlJSMetaProperty &p : properties) { + if (p.index() == -1) { + recordError(object.type->sourceLocation(), + u"Property '" + p.propertyName() + + u"' has incomplete information (internal error)"); + continue; + } + if (p.isAlias()) { + compileAlias(compiled, p, object.type); + } else { // normal property + compileProperty(compiled, p, object.type); + } + } + + // compile methods + QHash<QString, const QmlIR::Function *> irFunctionsByName; + std::for_each(object.irObject->functionsBegin(), object.irObject->functionsEnd(), + [&](const QmlIR::Function &function) { + irFunctionsByName.insert(m_doc->stringAt(function.nameIndex), + std::addressof(function)); + }); + const auto methods = object.type->ownMethods(); + compiled.functions.reserve(methods.size()); + for (auto it = methods.cbegin(); it != methods.cend(); ++it) { + const QmlIR::Function *irFunction = irFunctionsByName.value(it.key(), nullptr); + compileMethod(compiled, it.value(), irFunction, object); + } + + // NB: just clearing is safe since we do not call this function recursively + m_localChildrenToEndInit.clear(); + m_localChildrenToFinalize.clear(); + + // compile bindings + const auto sortedBindings = + toOrderedSequence(object.irObject->bindingsBegin(), object.irObject->bindingsEnd(), + object.irObject->bindingCount()); + + // for (auto it : sortedBindings) + // compileBinding(compiled, *it, object, u"this"_qs); + + // NB: can't use lower_bound since it only accepts a value, not a unary + // predicate + auto scriptBindingsBegin = + std::find_if(sortedBindings.cbegin(), sortedBindings.cend(), + [](auto it) { return it->type == QmlIR::Binding::Type_Script; }); + auto it = sortedBindings.cbegin(); + for (; it != scriptBindingsBegin; ++it) + compileBinding(compiled, **it, object, { object.type, u"this"_qs, u""_qs, false }); + + // NB: finalize children before creating/setting script bindings for `this` + for (qsizetype i = 0; i < m_localChildrenToEndInit.size(); ++i) { + compiled.endInit.body << m_localChildrenToEndInit.at(i) + u"->" + compiled.endInit.name + + u"(engine, " + CodeGeneratorUtility::compilationUnitVariable.name + u");"; + } + + const auto buildChildAtString = [](const QQmlJSScope::ConstPtr &type, + const QString &i) -> QString { + return u"static_cast<" + type->internalName() + u"* >(QObject::children().at(" + + CodeGeneratorUtility::childrenOffsetVariable.name + u" + " + i + u"))"; + }; + // TODO: there's exceptional redundancy (!) in this code generation part + for (qsizetype i = 0; i < m_localChildrenToFinalize.size(); ++i) { + QString index = QString::number(i); + const QQmlJSScope::ConstPtr &child = m_localChildrenToFinalize.at(i); + const QString childAt = buildChildAtString(child, index); + // NB: children are not document roots, so all special methods are argument-less + compiled.completeComponent.body + << childAt + u"->" + compiled.completeComponent.name + u"();"; + compiled.finalizeComponent.body + << childAt + u"->" + compiled.finalizeComponent.name + u"();"; + compiled.handleOnCompleted.body + << childAt + u"->" + compiled.handleOnCompleted.name + u"();"; + } + + for (; it != sortedBindings.cend(); ++it) + compileBinding(compiled, **it, object, { object.type, u"this"_qs, u""_qs, false }); + + // add finalization steps only to document root + if (documentRoot) { + compiled.endInit.body << u"if (" + finalizeFlag.name + u") {"; + + // at this point, all bindings must've been finished, thus, we need: + // 1. componentComplete() + // 2. finalize callbacks / componentFinalized() + // 3. Component.onCompleted() + + // 1. + compiled.endInit.body << u" this->" + compiled.completeComponent.name + + u"(/* complete component */ true);"; + + // 2 + compiled.endInit.body << u" this->" + compiled.finalizeComponent.name + + u"(/* finalize component */ true);"; + + // 3. + compiled.endInit.body << u" this->" + compiled.handleOnCompleted.name + u"();"; + + compiled.endInit.body << u"}"_qs; + } + + // compiled.endInit.body << u"Qt::endPropertyUpdateGroup();"_qs; +} +void CodeGenerator::compileEnum(QQmlJSAotObject ¤t, const QQmlJSMetaEnum &e) +{ + const auto intValues = e.values(); + QStringList values; + values.reserve(intValues.size()); + std::transform(intValues.cbegin(), intValues.cend(), std::back_inserter(values), + [](int x) { return QString::number(x); }); + + // structure: (C++ type name, enum keys, enum values, MOC line) + current.enums.emplaceBack(e.name(), e.keys(), std::move(values), + u"Q_ENUM(%1)"_qs.arg(e.name())); +} + +#if 1 +void CodeGenerator::compileProperty(QQmlJSAotObject ¤t, const QQmlJSMetaProperty &p, + const QQmlJSScope::ConstPtr &owner) +{ + Q_ASSERT(!p.isAlias()); // will be handled separately + Q_ASSERT(p.type()); + + const QString name = p.propertyName(); + const QString variableName = u"m_" + name; + const QString underlyingType = getUnderlyingType(p); + // only check for isList() here as it needs some special arrangements. + // otherwise, getUnderlyingType() handles the specifics of a type in C++ + if (p.isList()) { + const QString storageName = variableName + u"_storage"; + current.variables.emplaceBack(u"QList<" + p.type()->internalName() + u" *>", storageName, + QString()); + current.baselineCtor.initializerList.emplaceBack(variableName + u"(" + underlyingType + + u"(this, std::addressof(" + storageName + + u")))"); + } + + // along with property, also add relevant moc code, so that we can use the + // property in Qt/QML contexts + QStringList mocPieces; + mocPieces.reserve(10); + mocPieces << underlyingType << name; + + Qml2CppPropertyData compilationData(p); + + // 1. add setter and getter + if (p.isWritable()) { + QQmlJSAotMethod setter {}; + setter.returnType = u"void"_qs; + setter.name = compilationData.write; + // QQmlJSAotVariable + setter.parameterList.emplaceBack( + CodeGeneratorUtility::wrapInConstRefIfNotAPointer(underlyingType), name + u"_", + u""_qs); + setter.body << variableName + u".setValue(" + name + u"_);"; + setter.body << u"emit " + compilationData.notify + u"();"; + current.functions.emplaceBack(setter); + mocPieces << u"WRITE"_qs << setter.name; + } + + QQmlJSAotMethod getter {}; + getter.returnType = underlyingType; + getter.name = compilationData.read; + getter.body << u"return " + variableName + u".value();"; + current.functions.emplaceBack(getter); + mocPieces << u"READ"_qs << getter.name; + + // 2. add bindable + QQmlJSAotMethod bindable {}; + bindable.returnType = u"QBindable<" + underlyingType + u">"; + bindable.name = compilationData.bindable; + bindable.body << u"return QBindable<" + underlyingType + u">(std::addressof(" + variableName + + u"));"; + current.functions.emplaceBack(bindable); + mocPieces << u"BINDABLE"_qs << bindable.name; + + // 3. add/check notify (actually, this is already done inside QmltcVisitor) + + if (owner->isPropertyRequired(name)) + mocPieces << u"REQUIRED"_qs; + + // 4. add moc entry + // e.g. Q_PROPERTY(QString p READ getP WRITE setP BINDABLE bindableP) + current.mocCode << u"Q_PROPERTY(" + mocPieces.join(u" "_qs) + u")"; + + // 5. add extra moc entry if this property is marked default + if (name == owner->defaultPropertyName()) + current.mocCode << u"Q_CLASSINFO(\"DefaultProperty\", \"%1\")"_qs.arg(name); + + // structure: (C++ type name, name, C++ class name, C++ signal name) + current.properties.emplaceBack(underlyingType, variableName, current.cppType, + compilationData.notify); +} +#else +void CodeGenerator::compileProperty(QQmlJSAotObject ¤t, const QQmlJSMetaProperty &p, + const QQmlJSScope::ConstPtr &owner) +{ + if (p.isAlias()) // ignore aliases here, they are processed separately + return; + + Q_ASSERT(p.type()); + + const QString inputName = p.propertyName() + u"_"; + const QString memberName = u"m_" + p.propertyName(); + QString underlyingType = p.type()->internalName(); + // NB: can be a pointer or a list, can't be both (list automatically assumes + // that it holds pointers though). check isList() first, as list<QtObject> + // would be both a list and a pointer (weird). + if (p.isList()) { + current.variables.emplaceBack(u"QList<" + underlyingType + u" *>", memberName + u"_storage", + QString()); + current.baselineCtor.initializerList.emplaceBack( + memberName + u"(QQmlListProperty<" + underlyingType + u">(this, std::addressof(" + + memberName + u"_storage)))"); + underlyingType = u"QQmlListProperty<" + underlyingType + u">"; + } else if (isPointer(p)) { + underlyingType += u'*'; + } + + Qml2CppPropertyData compilationData(p); + // structure: + // C++ type name + // name + // default value + current.variables.emplaceBack(u"QProperty<" + underlyingType + u">", memberName, QString()); + + // with property added, also add relevant moc code, so that we can use the + // property in QML contexts + QStringList mocLines; + mocLines.reserve(10); + mocLines << underlyingType << p.propertyName(); + + // 1. add setter and getter + QQmlJSAotMethod setter {}; + setter.returnType = u"void"_qs; + setter.name = compilationData.write; + // QQmlJSAotVariable + setter.parameterList.emplaceBack( + CodeGeneratorUtility::wrapInConstRefIfNotAPointer(underlyingType), inputName, u""_qs); + setter.body << memberName + u".setValue(" + inputName + u"_);"; + // TODO: Qt.binding() (and old bindings?) requires signal emission + setter.body << u"emit " + compilationData.notify + u"();"; + current.functions.emplaceBack(setter); + mocLines << u"WRITE"_qs << setter.name; + + QQmlJSAotMethod getter {}; + getter.returnType = underlyingType; + getter.name = compilationData.read; + getter.body << u"return " + memberName + u".value();"; + current.functions.emplaceBack(getter); + mocLines << u"READ"_qs << getter.name; + + // 2. add bindable + QQmlJSAotMethod bindable {}; + bindable.returnType = u"QBindable<" + underlyingType + u">"; + bindable.name = compilationData.bindable; + bindable.body << u"return QBindable<" + underlyingType + u">(std::addressof(" + memberName + + u"));"; + current.functions.emplaceBack(bindable); + mocLines << u"BINDABLE"_qs << bindable.name; + + // 3. add notify + QQmlJSAotMethod notify {}; + notify.returnType = u"void"_qs; + notify.name = compilationData.notify; + notify.type = QQmlJSMetaMethod::Signal; + current.functions.emplaceBack(notify); + + // 4. add moc entry + // Q_PROPERTY(QString text READ text WRITE setText BINDABLE bindableText NOTIFY textChanged) + current.mocCode << u"Q_PROPERTY(" + mocLines.join(u" "_qs) + u")"; + + // 5. add extra moc entry if this property is default one + if (p.propertyName() == owner->defaultPropertyName()) { + // Q_CLASSINFO("DefaultProperty", propertyName) + current.mocCode << u"Q_CLASSINFO(\"DefaultProperty\", \"%1\")"_qs.arg(p.propertyName()); + } +} +#endif + +void CodeGenerator::compileAlias(QQmlJSAotObject ¤t, const QQmlJSMetaProperty &alias, + const QQmlJSScope::ConstPtr &owner) +{ + const QString aliasName = alias.propertyName(); + Q_ASSERT(!aliasName.isEmpty()); + + QStringList aliasExprBits = alias.aliasExpression().split(u'.'); + Q_ASSERT(!aliasExprBits.isEmpty()); + QString idString = aliasExprBits.front(); + Q_ASSERT(!idString.isEmpty()); + QQmlJSScope::ConstPtr type = m_localTypeResolver->scopeForId(idString, owner); + Q_ASSERT(type); + const bool aliasToProperty = aliasExprBits.size() > 1; // and not an object + + QString latestAccessor = u"this"_qs; + QString latestAccessorNonPrivate; // TODO: crutch for notify + QStringList prologue; + QStringList writeSpecificEpilogue; + + if (owner != type) { // cannot start at `this`, need to fetch object through context at run time + Q_ASSERT(m_typeToObjectIndex.contains(type) + && m_typeToObjectIndex[type] <= m_objects.size()); + const int id = m_objects[m_typeToObjectIndex[type]].irObject->id; + Q_ASSERT(id >= 0); // since the type is found by id, it must have an id + + // TODO: the context fetching must be done once in the notify code + // (across multiple alias properties). now there's a huge duplication + prologue << u"auto context = QQmlData::get(this)->outerContext;"_qs; + // there's a special case: when `this` type has compiled QML type as a base + // type and it is not a root, it has a non-root first context, so we need to + // step one level up + if (m_qmlCompiledBaseTypes.contains(owner->baseTypeName()) + && m_typeToObjectIndex[owner] != 0) { + Q_ASSERT(!owner->baseTypeName().isEmpty()); + prologue << u"// `this` is special: not a root and its base type is compiled"_qs; + prologue << u"context = context->parent().data();"_qs; + } + // doing the above allows us to lookup id object by integer, which is fast + latestAccessor = u"alias_objectById_" + idString; // unique enough + if (aliasToProperty) { + prologue << u"auto " + latestAccessor + u" = static_cast<" + type->internalName() + + u"*>(context->idValue(" + QString::number(id) + u"));"; + } else { + // kind of a crutch for object aliases: expect that the user knows + // what to do and so accept QObject* + prologue << u"QObject *" + latestAccessor + u" = context->idValue(" + + QString::number(id) + u");"; + } + prologue << u"Q_ASSERT(" + latestAccessor + u");"; + } + + struct AliasData + { + QString underlyingType; + QString readLine; + QString writeLine; + QString bindableLine; + } info; // note, we only really need this crutch to cover the alias to object case + QQmlJSMetaProperty resultingProperty; + + if (aliasToProperty) { + // the following code goes to prologue section. the idea of that is to + // construct the necessary code to fetch the resulting property + for (qsizetype i = 1; i < aliasExprBits.size() - 1; ++i) { + const QString &bit = qAsConst(aliasExprBits)[i]; + Q_ASSERT(!bit.isEmpty()); + Q_ASSERT(type); + QQmlJSMetaProperty p = type->property(bit); + + QString currAccessor = + CodeGeneratorUtility::generate_getPrivateClass(latestAccessor, p); + if (p.type()->accessSemantics() == QQmlJSScope::AccessSemantics::Value) { + // we need to read the property to a local variable and then + // write the updated value once the actual operation is done + QString localVariableName = u"alias_" + bit; // should be fairly unique + prologue << u"auto " + localVariableName + u" = " + currAccessor + u"->" + p.read() + + u"();"; + writeSpecificEpilogue + << currAccessor + u"->" + p.write() + u"(" + localVariableName + u");"; + // NB: since accessor becomes a value type, wrap it into an + // addressof operator so that we could access it as a pointer + currAccessor = CodeGeneratorUtility::generate_addressof(localVariableName); // reset + } else { + currAccessor += u"->" + p.read() + u"()"; // amend + } + + type = p.type(); + latestAccessor = currAccessor; + } + + resultingProperty = type->property(aliasExprBits.back()); + latestAccessorNonPrivate = latestAccessor; + latestAccessor = + CodeGeneratorUtility::generate_getPrivateClass(latestAccessor, resultingProperty); + info.underlyingType = resultingProperty.type()->internalName(); + if (resultingProperty.isList()) { + info.underlyingType = u"QQmlListProperty<" + info.underlyingType + u">"; + } else if (isPointer(resultingProperty)) { + info.underlyingType += u"*"_qs; + } + // reset to generic type when having alias to id: + if (m_aliasesToIds.contains(resultingProperty)) + info.underlyingType = u"QObject*"_qs; + + // READ must always be present + info.readLine = latestAccessor + u"->" + resultingProperty.read() + u"()"; + if (QString setName = resultingProperty.write(); !setName.isEmpty()) + info.writeLine = latestAccessor + u"->" + setName + u"(%1)"; + if (QString bindableName = resultingProperty.bindable(); !bindableName.isEmpty()) + info.bindableLine = latestAccessor + u"->" + bindableName + u"()"; + } else { + info.underlyingType = u"QObject*"_qs; + info.readLine = latestAccessor; + // NB: id-pointing aliases are read-only, also alias to object is not + // bindable since it's not a QProperty itself + } + + QStringList mocLines; + mocLines.reserve(10); + mocLines << info.underlyingType << aliasName; + + Qml2CppPropertyData compilationData(aliasName); + // 1. add setter and getter + if (!info.readLine.isEmpty()) { + QQmlJSAotMethod getter {}; + getter.returnType = info.underlyingType; + getter.name = compilationData.read; + getter.body += prologue; + getter.body << u"return " + info.readLine + u";"; + // getter.body += writeSpecificEpilogue; + current.functions.emplaceBack(getter); + mocLines << u"READ"_qs << getter.name; + } // else always an error? + + if (!info.writeLine.isEmpty()) { + QQmlJSAotMethod setter {}; + setter.returnType = u"void"_qs; + setter.name = compilationData.write; + + QList<QQmlJSMetaMethod> methods = type->methods(resultingProperty.write()); + if (methods.isEmpty()) { + // QQmlJSAotVariable + setter.parameterList.emplaceBack( + CodeGeneratorUtility::wrapInConstRefIfNotAPointer(info.underlyingType), + aliasName + u"_", u""_qs); + } else { + setter.parameterList = compileMethodParameters(methods.at(0).parameterNames(), + methods.at(0).parameterTypes(), true); + } + + setter.body += prologue; + if (aliasToProperty) { + QStringList parameterNames; + parameterNames.reserve(setter.parameterList.size()); + std::transform(setter.parameterList.cbegin(), setter.parameterList.cend(), + std::back_inserter(parameterNames), + [](const QQmlJSAotVariable &x) { return x.name; }); + QString commaSeparatedParameterNames = parameterNames.join(u", "_qs); + setter.body << info.writeLine.arg(commaSeparatedParameterNames) + u";"; + } else { + setter.body << info.writeLine + u";"; + } + setter.body += writeSpecificEpilogue; + current.functions.emplaceBack(setter); + mocLines << u"WRITE"_qs << setter.name; + } + + // 2. add bindable + if (!info.bindableLine.isEmpty()) { + QQmlJSAotMethod bindable {}; + bindable.returnType = u"QBindable<" + info.underlyingType + u">"; + bindable.name = compilationData.bindable; + bindable.body += prologue; + bindable.body << u"return " + info.bindableLine + u";"; + current.functions.emplaceBack(bindable); + mocLines << u"BINDABLE"_qs << bindable.name; + } + // 3. add notify - which is pretty special + if (QString notifyName = resultingProperty.notify(); !notifyName.isEmpty()) { + // notify is very special (even more than reasonable) + current.endInit.body << u"{ // alias notify connection:"_qs; + // TODO: part of the prologue (e.g. QQmlData::get(this)->outerContext) + // must be shared across different notifies (to speed up finalize) + current.endInit.body += prologue; + // TODO: use non-private accessor since signals must exist on the public + // type, not on the private one -- otherwise, they can't be used from + // C++ which is not what's wanted (this is a mess) + current.endInit.body << u"QObject::connect(" + latestAccessorNonPrivate + u", &" + + type->internalName() + u"::" + notifyName + u", this, &" + current.cppType + + u"::" + compilationData.notify + u");"; + current.endInit.body << u"}"_qs; + } + + // 4. add moc entry + // Q_PROPERTY(QString text READ text WRITE setText BINDABLE bindableText NOTIFY textChanged) + current.mocCode << u"Q_PROPERTY(" + mocLines.join(u" "_qs) + u")"; + + // 5. add extra moc entry if this alias is default one + if (aliasName == owner->defaultPropertyName()) { + // Q_CLASSINFO("DefaultProperty", propertyName) + current.mocCode << u"Q_CLASSINFO(\"DefaultProperty\", \"%1\")"_qs.arg(aliasName); + } +} + +void CodeGenerator::compileMethod(QQmlJSAotObject ¤t, const QQmlJSMetaMethod &m, + const QmlIR::Function *f, const CodeGenObject &object) +{ + Q_UNUSED(object); + const QString returnType = figureReturnType(m); + + const auto paramNames = m.parameterNames(); + const auto paramTypes = m.parameterTypes(); + Q_ASSERT(paramNames.size() == paramTypes.size()); + const QList<QQmlJSAotVariable> paramList = compileMethodParameters(paramNames, paramTypes); + + const auto methodType = QQmlJSMetaMethod::Type(m.methodType()); + + QStringList code; + // the function body code only makes sense if the method is not a signal - + // and there is a corresponding QmlIR::Function + if (f) { + Q_ASSERT(methodType != QQmlJSMetaMethod::Signal); + code += CodeGeneratorUtility::generate_callExecuteRuntimeFunction( + m_urlMethod.name + u"()", + relativeToAbsoluteRuntimeIndex(m_doc, object.irObject, f->index), u"this"_qs, + returnType, paramList); + } + + QQmlJSAotMethod compiled {}; + compiled.returnType = returnType; + compiled.name = m.methodName(); + compiled.parameterList = std::move(paramList); + compiled.body = std::move(code); + compiled.type = methodType; + compiled.access = m.access(); + if (methodType != QQmlJSMetaMethod::Signal) + compiled.declPreambles << u"Q_INVOKABLE"_qs; // TODO: do we need this for signals as well? + current.functions.emplaceBack(compiled); +} + +template<typename Iterator> +static QString getPropertyOrAliasNameFromIr(const QmlIR::Document *doc, Iterator first, int pos) +{ + std::advance(first, pos); // assume that first is correct - due to earlier check + return doc->stringAt(first->nameIndex); +} + +void CodeGenerator::compileBinding(QQmlJSAotObject ¤t, const QmlIR::Binding &binding, + const CodeGenObject &object, + const CodeGenerator::AccessorData &accessor) +{ + // TODO: cache property name somehow, so we don't need to look it up again + QString propertyName = m_doc->stringAt(binding.propertyNameIndex); + if (propertyName.isEmpty()) { + // if empty, try default property + for (QQmlJSScope::ConstPtr t = object.type->baseType(); t && propertyName.isEmpty(); + t = t->baseType()) + propertyName = t->defaultPropertyName(); + } + Q_ASSERT(!propertyName.isEmpty()); + QQmlJSMetaProperty p = object.type->property(propertyName); + QQmlJSScope::ConstPtr propertyType = p.type(); + // Q_ASSERT(propertyType); // TODO: doesn't work with signals + + const auto addPropertyLine = [&](const QString &propertyName, const QQmlJSMetaProperty &p, + const QString &value, bool constructQVariant = false) { + // TODO: there mustn't be this special case. instead, alias resolution + // must be done in QQmlJSImportVisitor subclass, that would handle this + // mess (see resolveValidateOrSkipAlias() in qml2cppdefaultpasses.cpp) + if (p.isAlias() && m_qmlCompiledBaseTypes.contains(object.type->baseTypeName())) { + qCDebug(lcCodeGenerator) << u"Property '" + propertyName + u"' is an alias on type '" + + object.type->internalName() + + u"' which is a QML type compiled to C++. The assignment is special " + u"in this case"; + current.endInit.body += CodeGeneratorUtility::generate_assignToSpecialAlias( + object.type, propertyName, p, value, accessor.name, constructQVariant); + } else { + current.endInit.body += CodeGeneratorUtility::generate_assignToProperty( + object.type, propertyName, p, value, accessor.name, constructQVariant); + } + }; + + switch (binding.type) { + case QmlIR::Binding::Type_Boolean: { + addPropertyLine(propertyName, p, binding.value.b ? u"true"_qs : u"false"_qs); + break; + } + case QmlIR::Binding::Type_Number: { + QString value = m_doc->javaScriptCompilationUnit.bindingValueAsString(&binding); + addPropertyLine(propertyName, p, value); + break; + } + case QmlIR::Binding::Type_String: { + const QString str = m_doc->stringAt(binding.stringIndex); + addPropertyLine(propertyName, p, CodeGeneratorUtility::createStringLiteral(str)); + break; + } + case QmlIR::Binding::Type_TranslationById: { // TODO: add test + const QV4::CompiledData::TranslationData &translation = + m_doc->javaScriptCompilationUnit.unitData() + ->translations()[binding.value.translationDataIndex]; + const QString id = m_doc->stringAt(binding.stringIndex); + addPropertyLine(propertyName, p, + u"qsTrId(\"" + id + u"\", " + QString::number(translation.number) + u")"); + break; + } + case QmlIR::Binding::Type_Translation: { // TODO: add test + const QV4::CompiledData::TranslationData &translation = + m_doc->javaScriptCompilationUnit.unitData() + ->translations()[binding.value.translationDataIndex]; + int lastSlash = m_url.lastIndexOf(QChar(u'/')); + const QStringView context = (lastSlash > -1) + ? QStringView { m_url }.mid(lastSlash + 1, m_url.length() - lastSlash - 5) + : QStringView(); + const QString comment = m_doc->stringAt(translation.commentIndex); + const QString text = m_doc->stringAt(translation.stringIndex); + + addPropertyLine(propertyName, p, + u"QCoreApplication::translate(\"" + context + u"\", \"" + text + u"\", \"" + + comment + u"\", " + QString::number(translation.number) + u")"); + break; + } + case QmlIR::Binding::Type_Script: { + QString bindingSymbolName = object.type->internalName() + u'_' + propertyName + u"_binding"; + bindingSymbolName.replace(u'.', u'_'); // can happen with group properties + compileScriptBinding(current, binding, bindingSymbolName, object, propertyName, + propertyType, accessor); + break; + } + case QmlIR::Binding::Type_Object: { + // NB: object is compiled with compileObject, here just need to use it + const CodeGenObject &bindingObject = m_objects.at(binding.value.objectIndex); + + // Note: despite a binding being set for `accessor`, we use "this" as a + // parent of a created object. Both attached and grouped properties are + // parented by "this", so lifetime-wise we should be fine. If not, we're + // in a fairly deep trouble, actually, since it's essential to use + // `this` (at least at present it seems to be so) - e.g. the whole logic + // of separating object binding into object creation and object binding + // setup relies on the fact that we use `this` + const QString qobjectParent = u"this"_qs; + const QString objectAddr = accessor.name; + + if (binding.flags & QmlIR::Binding::IsOnAssignment) { + const QString onAssignmentName = u"onAssign_" + propertyName; + const auto uniqueId = UniqueStringId(current, onAssignmentName); + if (!m_onAssignmentObjectsCreated.contains(uniqueId)) { + m_onAssignmentObjectsCreated.insert(uniqueId); + current.init.body << u"new " + bindingObject.type->internalName() + u"(engine, " + + qobjectParent + u");"; + + // static_cast is fine, because we (must) know the exact type + current.endInit.body << u"auto " + onAssignmentName + u" = static_cast<" + + bindingObject.type->internalName() + + u" *>(QObject::children().at(" + + CodeGeneratorUtility::childrenOffsetVariable.name + u" + " + + QString::number(m_localChildrenToEndInit.size()) + u"));"; + + m_localChildrenToEndInit.append(onAssignmentName); + m_localChildrenToFinalize.append(bindingObject.type); + } + + // NB: we expect one "on" assignment per property, so creating + // QQmlProperty each time should be fine (unlike QQmlListReference) + current.endInit.body << u"{"_qs; + current.endInit.body << u"QQmlProperty qmlprop(" + objectAddr + u", QStringLiteral(\"" + + propertyName + u"\"));"; + current.endInit.body << u"QT_PREPEND_NAMESPACE(QQmlCppOnAssignmentHelper)::set(" + + onAssignmentName + u", qmlprop);"; + current.endInit.body << u"}"_qs; + break; + } + + if (!propertyType) { + recordError(binding.location, + u"Binding on property '" + propertyName + u"' of unknown type"); + return; + } + + const auto setObjectBinding = [&](const QString &value) { + if (p.isList() || (binding.flags & QmlIR::Binding::IsListItem)) { + const QString refName = u"listref_" + propertyName; + const auto uniqueId = UniqueStringId(current, refName); + if (!m_listReferencesCreated.contains(uniqueId)) { + m_listReferencesCreated.insert(uniqueId); + // TODO: figure if Unicode support is needed here + current.endInit.body << u"QQmlListReference " + refName + u"(" + objectAddr + + u", QByteArrayLiteral(\"" + propertyName + u"\"));"; + current.endInit.body << u"Q_ASSERT(" + refName + u".canAppend());"; + } + current.endInit.body << refName + u".append(" + value + u");"; + } else { + addPropertyLine(propertyName, p, value, /* through QVariant = */ true); + } + }; + + if (bindingObject.irObject->flags & QV4::CompiledData::Object::IsComponent) { + // NB: this one is special - and probably becomes a view delegate. + // object binding separation also does not apply here + const QString objectName = makeGensym(u"sc"_qs); + Q_ASSERT(m_typeToObjectIndex.contains(bindingObject.type)); + Q_ASSERT(m_implicitComponentMapping.contains( + int(m_typeToObjectIndex[bindingObject.type]))); + const int index = + m_implicitComponentMapping[int(m_typeToObjectIndex[bindingObject.type])]; + current.endInit.body << u"{"_qs; + current.endInit.body << QStringLiteral( + "auto thisContext = QQmlData::get(%1)->outerContext;") + .arg(qobjectParent); + current.endInit.body << QStringLiteral( + "auto %1 = QQmlObjectCreator::createComponent(engine, " + "QQmlEnginePrivate::get(engine)->" + "compilationUnitFromUrl(%2()), %3, %4, thisContext);") + .arg(objectName, m_urlMethod.name, + QString::number(index), qobjectParent); + current.endInit.body << QStringLiteral("thisContext->installContext(QQmlData::get(%1), " + "QQmlContextData::OrdinaryObject);") + .arg(objectName); + setObjectBinding(objectName); + current.endInit.body << u"}"_qs; + break; + } + + current.init.body << u"new " + bindingObject.type->internalName() + u"(engine, " + + qobjectParent + u");"; + + const QString objectName = makeGensym(u"o"_qs); + + // static_cast is fine, because we (must) know the exact type + current.endInit.body << u"auto " + objectName + u" = static_cast<" + + bindingObject.type->internalName() + u" *>(QObject::children().at(" + + CodeGeneratorUtility::childrenOffsetVariable.name + u" + " + + QString::number(m_localChildrenToEndInit.size()) + u"));"; + setObjectBinding(objectName); + + m_localChildrenToEndInit.append(objectName); + m_localChildrenToFinalize.append(bindingObject.type); + break; + } + case QmlIR::Binding::Type_AttachedProperty: { + Q_ASSERT(accessor.name == u"this"_qs); // TODO: doesn't have to hold, in fact + const auto attachedObject = m_objects.at(binding.value.objectIndex); + const auto &irObject = attachedObject.irObject; + const auto &type = attachedObject.type; + Q_ASSERT(irObject && type); + if (propertyName == u"Component"_qs) { + // TODO: there's a special QQmlComponentAttached, which has to be + // called? c.f. qqmlobjectcreator.cpp's finalize() + const auto compileComponent = [&](const QmlIR::Binding &b) { + Q_ASSERT(b.type == QmlIR::Binding::Type_Script); + compileScriptBindingOfComponent(current, irObject, type, b, + m_doc->stringAt(b.propertyNameIndex)); + }; + std::for_each(irObject->bindingsBegin(), irObject->bindingsEnd(), compileComponent); + } else { + const QString attachingTypeName = propertyName; // acts as an identifier + QString attachedTypeName = type->attachedTypeName(); // TODO: check if == internalName? + if (attachedTypeName.isEmpty()) // TODO: shouldn't happen ideally + attachedTypeName = type->baseTypeName(); + const QString attachedMemberName = u"m_" + attachingTypeName; + const auto uniqueId = UniqueStringId(current, attachedMemberName); + // add attached type as a member variable to allow noop lookup + if (!m_attachedTypesAlreadyRegistered.contains(uniqueId)) { + m_attachedTypesAlreadyRegistered.insert(uniqueId); + current.variables.emplaceBack(attachedTypeName + u" *", attachedMemberName, + u"nullptr"_qs); + // Note: getting attached property is fairly expensive + const QString getAttachedPropertyLine = u"qobject_cast<" + attachedTypeName + + u" *>(qmlAttachedPropertiesObject<" + attachedTypeName + + u">(this, /* create = */ true))"; + current.endInit.body + << attachedMemberName + u" = " + getAttachedPropertyLine + u";"; + } + + // compile bindings of the attached property + auto sortedBindings = toOrderedSequence( + irObject->bindingsBegin(), irObject->bindingsEnd(), irObject->bindingCount()); + for (auto it : qAsConst(sortedBindings)) + compileBinding(current, *it, attachedObject, + { object.type, attachedMemberName, propertyName, false }); + } + break; + } + case QmlIR::Binding::Type_GroupProperty: { + Q_ASSERT(accessor.name == u"this"_qs); // TODO: doesn't have to hold, in fact + if (p.read().isEmpty()) { + recordError(binding.location, + u"READ function of group property '" + propertyName + u"' is unknown"); + return; + } + + const auto groupedObject = m_objects.at(binding.value.objectIndex); + const auto &irObject = groupedObject.irObject; + const auto &type = groupedObject.type; + Q_ASSERT(irObject && type); + + const bool isValueType = + propertyType->accessSemantics() == QQmlJSScope::AccessSemantics::Value; + if (!isValueType + && propertyType->accessSemantics() != QQmlJSScope::AccessSemantics::Reference) { + recordError(binding.location, + u"Group property '" + propertyName + u"' has unsupported access semantics"); + return; + } + + QString groupAccessor = CodeGeneratorUtility::generate_getPrivateClass(accessor.name, p) + + u"->" + p.read() + u"()"; + // NB: used when isValueType == true + QString groupPropertyVarName = accessor.name + u"_group_" + propertyName; + + const auto isGroupAffectingBinding = [](const QmlIR::Binding &b) { + // script bindings do not require value type group property variable + // to actually be present. + return b.type != QmlIR::Binding::Type_Invalid && b.type != QmlIR::Binding::Type_Script; + }; + const bool generateValueTypeCode = isValueType + && std::any_of(irObject->bindingsBegin(), irObject->bindingsEnd(), + isGroupAffectingBinding); + + // value types are special + if (generateValueTypeCode) { + if (p.write().isEmpty()) { // just reject this + recordError(binding.location, + u"Group property '" + propertyName + + u"' is a value type without a setter"); + return; + } + + current.endInit.body << u"auto " + groupPropertyVarName + u" = " + groupAccessor + u";"; + // TODO: addressof operator is a crutch to make the binding logic + // work, which expects that `accessor.name` is a pointer type. + groupAccessor = CodeGeneratorUtility::generate_addressof(groupPropertyVarName); + } + + // compile bindings of the grouped property + auto sortedBindings = toOrderedSequence(irObject->bindingsBegin(), irObject->bindingsEnd(), + irObject->bindingCount()); + const auto compile = [&](typename QmlIR::PoolList<QmlIR::Binding>::Iterator it) { + compileBinding(current, *it, groupedObject, + { object.type, groupAccessor, propertyName, isValueType }); + }; + + // NB: can't use lower_bound since it only accepts a value, not a unary + // predicate + auto scriptBindingsBegin = + std::find_if(sortedBindings.cbegin(), sortedBindings.cend(), + [](auto it) { return it->type == QmlIR::Binding::Type_Script; }); + auto it = sortedBindings.cbegin(); + for (; it != scriptBindingsBegin; ++it) { + Q_ASSERT((*it)->type != QmlIR::Binding::Type_Script); + compile(*it); + } + + // NB: script bindings are special on group properties. if our group is + // a value type, the binding would be installed on the *object* that + // holds the value type and not on the value type itself. this may cause + // subtle side issues (esp. when script binding is actually a simple + // enum value assignment - which is not recognized specially): + // + // auto valueTypeGroupProperty = getCopy(); + // installBinding(valueTypeGroupProperty, "subproperty1"); // changes subproperty1 value + // setCopy(valueTypeGroupProperty); // oops, subproperty1 value changed to old again + if (generateValueTypeCode) { // write the value type back + current.endInit.body << CodeGeneratorUtility::generate_getPrivateClass(accessor.name, p) + + u"->" + p.write() + u"(" + groupPropertyVarName + u");"; + } + + // once the value is written back, process the script bindings + for (; it != sortedBindings.cend(); ++it) { + Q_ASSERT((*it)->type == QmlIR::Binding::Type_Script); + compile(*it); + } + break; + } + case QmlIR::Binding::Type_Null: { + // poor check: null bindings are only supported for var and objects + if (propertyType != m_localTypeResolver->varType() + && propertyType->accessSemantics() != QQmlJSScope::AccessSemantics::Reference) { + // TODO: this should really be done before the compiler here + recordError(binding.location, u"Cannot assign null to incompatible property"_qs); + } else if (propertyType->accessSemantics() == QQmlJSScope::AccessSemantics::Reference) { + addPropertyLine(p.propertyName(), p, u"nullptr"_qs); + } else { + addPropertyLine(p.propertyName(), p, u"QVariant::fromValue(nullptr)"_qs); + } + break; + } + case QmlIR::Binding::Type_Invalid: { + recordError(binding.valueLocation, + u"Binding on property '" + propertyName + u"' is invalid or unsupported"); + break; + } + } +} + +QString CodeGenerator::makeGensym(const QString &base) +{ + QString gensym = base; + gensym.replace(QLatin1String("."), QLatin1String("_")); + while (gensym.startsWith(QLatin1Char('_')) && gensym.size() >= 2 + && (gensym[1].isUpper() || gensym[1] == QLatin1Char('_'))) { + gensym.remove(0, 1); + } + + if (!m_typeCounts.contains(gensym)) { + m_typeCounts.insert(gensym, 1); + } else { + gensym += u"_" + QString::number(m_typeCounts[gensym]++); + } + + return gensym; +} + +// returns compiled script binding for "property changed" handler in a form of object type +static QQmlJSAotObject compileScriptBindingPropertyChangeHandler( + const QmlIR::Document *doc, const QmlIR::Binding &binding, const QmlIR::Object *irObject, + const QQmlJSAotMethod &urlMethod, const QString &functorCppType, + const QString &objectCppType, const QList<QQmlJSAotVariable> &slotParameters) +{ + QQmlJSAotObject bindingFunctor {}; + bindingFunctor.cppType = functorCppType; + bindingFunctor.ignoreInit = true; + + // default member variable and ctor: + const QString pointerToObject = objectCppType + u" *"; + bindingFunctor.variables.emplaceBack( + QQmlJSAotVariable { pointerToObject, u"m_self"_qs, u"nullptr"_qs }); + bindingFunctor.baselineCtor.name = functorCppType; + bindingFunctor.baselineCtor.parameterList.emplaceBack( + QQmlJSAotVariable { pointerToObject, u"self"_qs, QString() }); + bindingFunctor.baselineCtor.initializerList.emplaceBack(u"m_self(self)"_qs); + + // call operator: + QQmlJSAotMethod callOperator {}; + callOperator.returnType = u"void"_qs; + callOperator.name = u"operator()"_qs; + callOperator.parameterList = slotParameters; + callOperator.modifiers << u"const"_qs; + callOperator.body += CodeGeneratorUtility::generate_callExecuteRuntimeFunction( + urlMethod.name + u"()", + relativeToAbsoluteRuntimeIndex(doc, irObject, binding.value.compiledScriptIndex), + u"m_self"_qs, u"void"_qs, slotParameters); + + bindingFunctor.functions.emplaceBack(std::move(callOperator)); + + return bindingFunctor; +} + +void CodeGenerator::compileScriptBinding(QQmlJSAotObject ¤t, const QmlIR::Binding &binding, + const QString &bindingSymbolName, + const CodeGenObject &object, const QString &propertyName, + const QQmlJSScope::ConstPtr &propertyType, + const CodeGenerator::AccessorData &accessor) +{ + auto objectType = object.type; + + // returns signal by name. signal exists if std::optional<> has value + const auto signalByName = [&](const QString &name) -> std::optional<QQmlJSMetaMethod> { + const QList<QQmlJSMetaMethod> signalMethods = objectType->methods(name); + if (signalMethods.isEmpty()) + return {}; + // TODO: no clue how to handle redefinition, so just record an error + if (signalMethods.size() != 1) { + recordError(binding.location, u"Binding on redefined signal '" + name + u"'"); + return {}; + } + QQmlJSMetaMethod s = signalMethods.at(0); + if (s.methodType() != QQmlJSMetaMethod::Signal) + return {}; + return s; + }; + + // TODO: add Invalid case which would reject garbage handlers + enum BindingKind { + JustProperty, // is a binding on property + SignalHandler, // is a slot related to custom signal + PropertyChangeHandler, // is a slot related to property's "changed" signal + }; + + // these only make sense when binding is on signal handler + QList<QQmlJSAotVariable> slotParameters; + QString signalName; + QString signalReturnType; + + const BindingKind kind = [&]() -> BindingKind { + if (!QmlIR::IRBuilder::isSignalPropertyName(propertyName)) + return BindingKind::JustProperty; + + const auto offset = static_cast<uint>(strlen("on")); + signalName = propertyName[offset].toLower() + propertyName.mid(offset + 1); + + std::optional<QQmlJSMetaMethod> possibleSignal = signalByName(signalName); + if (possibleSignal) { // signal with signalName exists + QQmlJSMetaMethod s = possibleSignal.value(); + const auto paramNames = s.parameterNames(); + const auto paramTypes = s.parameterTypes(); + Q_ASSERT(paramNames.size() == paramTypes.size()); + slotParameters = compileMethodParameters(paramNames, paramTypes); + signalReturnType = figureReturnType(s); + return BindingKind::SignalHandler; + } else if (propertyName.endsWith(u"Changed"_qs)) { + return BindingKind::PropertyChangeHandler; + } + return BindingKind::JustProperty; + }(); + + switch (kind) { + case BindingKind::JustProperty: { + if (!propertyType) { + recordError(binding.location, + u"Binding on property '" + propertyName + u"' of unknown type"); + return; + } + + auto [property, absoluteIndex] = getMetaPropertyIndex(object.type, propertyName); + if (absoluteIndex < 0) { + recordError(binding.location, u"Binding on unknown property '" + propertyName + u"'"); + return; + } + + QString bindingTarget = accessor.name; + + int valueTypeIndex = -1; + if (accessor.isValueType) { + Q_ASSERT(accessor.scope != object.type); + bindingTarget = u"this"_qs; // TODO: not necessarily "this"? + auto [groupProperty, groupPropertyIndex] = + getMetaPropertyIndex(accessor.scope, accessor.propertyName); + if (groupPropertyIndex < 0) { + recordError(binding.location, + u"Binding on group property '" + accessor.propertyName + + u"' of unknown type"); + return; + } + valueTypeIndex = absoluteIndex; + absoluteIndex = groupPropertyIndex; // e.g. index of accessor.name + } + + current.endInit.body += CodeGeneratorUtility::generate_createBindingOnProperty( + CodeGeneratorUtility::compilationUnitVariable.name, + u"this"_qs, // NB: always using enclosing object as a scope for the binding + relativeToAbsoluteRuntimeIndex(object.irObject, binding.value.compiledScriptIndex), + bindingTarget, // binding target + absoluteIndex, property, valueTypeIndex, accessor.name); + break; + } + case BindingKind::SignalHandler: { + QString This_signal = u"this"_qs; + QString This_slot = u"this"_qs; + QString objectClassName_signal = objectType->internalName(); + QString objectClassName_slot = objectType->internalName(); + // TODO: ugly crutch to make stuff work + if (accessor.name != u"this"_qs) { // e.g. if attached property + This_signal = accessor.name; + This_slot = u"this"_qs; // still + objectClassName_signal = objectType->baseTypeName(); + objectClassName_slot = current.cppType; // real base class where slot would go + } + Q_ASSERT(!objectClassName_signal.isEmpty()); + Q_ASSERT(!objectClassName_slot.isEmpty()); + const QString slotName = makeGensym(signalName + u"_slot"); + + if (objectType->isSingleton()) { // TODO: support + recordError(binding.location, + u"Binding on singleton type '" + objectClassName_signal + + u"' is not supported"); + return; + } + + // SignalHander specific: + QQmlJSAotMethod slotMethod {}; + slotMethod.returnType = signalReturnType; + slotMethod.name = slotName; + slotMethod.parameterList = slotParameters; + + slotMethod.body += CodeGeneratorUtility::generate_callExecuteRuntimeFunction( + m_urlMethod.name + u"()", + relativeToAbsoluteRuntimeIndex(m_doc, object.irObject, + binding.value.compiledScriptIndex), + u"this"_qs, // Note: because script bindings always use current QML object scope + signalReturnType, slotParameters); + slotMethod.type = QQmlJSMetaMethod::Slot; + + current.functions << std::move(slotMethod); + current.endInit.body << u"QObject::connect(" + This_signal + u", " + + CodeGeneratorUtility::generate_qOverload(slotParameters, + u"&" + objectClassName_signal + + u"::" + signalName) + + u", " + This_slot + u", &" + objectClassName_slot + u"::" + slotName + + u");"; + break; + } + // TODO: fix code generation for property change handlers - it is broken for + // many cases. see QTBUG-91956 + case BindingKind::PropertyChangeHandler: { + const QString objectClassName = objectType->internalName(); + const QString bindingFunctorName = makeGensym(bindingSymbolName + u"Functor"); + + QString actualPropertyName; + QRegularExpression onChangedRegExp(u"on(\\w+)Changed"_qs); + QRegularExpressionMatch match = onChangedRegExp.match(propertyName); + if (match.hasMatch()) { + actualPropertyName = match.captured(1); + actualPropertyName[0] = actualPropertyName.at(0).toLower(); + } else { + recordError(binding.location, u"Missing parameter name in on*Changed handler"_qs); + return; + } + if (!objectType->hasProperty(actualPropertyName)) { + recordError(binding.location, u"No property named " + actualPropertyName); + return; + } + QQmlJSMetaProperty actualProperty = objectType->property(actualPropertyName); + const auto actualPropertyType = actualProperty.type(); + if (!actualPropertyType) { + recordError(binding.location, + u"Binding on property '" + actualPropertyName + u"' of unknown type"); + return; + } + QString typeOfQmlBinding = + u"std::unique_ptr<QPropertyChangeHandler<" + bindingFunctorName + u">>"; + + current.children << compileScriptBindingPropertyChangeHandler( + m_doc, binding, object.irObject, m_urlMethod, bindingFunctorName, objectClassName, + slotParameters); + + QString bindableString = actualProperty.bindable(); + if (bindableString.isEmpty()) { // TODO: always should come from prop.bindalbe() + recordError(binding.location, + u"Property '" + actualPropertyName + + u"' is non-bindable. Change handlers for such properties are not " + u"supported"); + break; + } + // TODO: the finalize.lastLines has to be used here -- somehow if property gets a binding, + // the change handler is not updated?! + + // TODO: this could be dropped if QQmlEngine::setContextForObject() is + // done before currently generated C++ object is constructed + current.endInit.body << bindingSymbolName + u".reset(new QPropertyChangeHandler<" + + bindingFunctorName + u">(" + + CodeGeneratorUtility::generate_getPrivateClass(accessor.name, + actualProperty) + + u"->" + bindableString + u"().onValueChanged(" + bindingFunctorName + u"(" + + accessor.name + u"))));"; + + current.variables.emplaceBack( + QQmlJSAotVariable { typeOfQmlBinding, bindingSymbolName, QString() }); + // current.ctor.initializerList << bindingSymbolName + u"()"; + break; + } + } +} + +// TODO: should use "compileScriptBinding" instead of custom code +void CodeGenerator::compileScriptBindingOfComponent(QQmlJSAotObject ¤t, + const QmlIR::Object *irObject, + const QQmlJSScope::ConstPtr objectType, + const QmlIR::Binding &binding, + const QString &propertyName) +{ + // returns signal by name. signal exists if std::optional<> has value + const auto signalByName = [&](const QString &name) -> std::optional<QQmlJSMetaMethod> { + const QList<QQmlJSMetaMethod> signalMethods = objectType->methods(name); + // TODO: no clue how to handle redefinition, so just record an error + if (signalMethods.size() != 1) { + recordError(binding.location, u"Binding on redefined signal '" + name + u"'"); + return {}; + } + QQmlJSMetaMethod s = signalMethods.at(0); + if (s.methodType() != QQmlJSMetaMethod::Signal) + return {}; + return s; + }; + + QString signalName; + QString signalReturnType; + + Q_ASSERT(QmlIR::IRBuilder::isSignalPropertyName(propertyName)); + const auto offset = static_cast<uint>(strlen("on")); + signalName = propertyName[offset].toLower() + propertyName.mid(offset + 1); + + std::optional<QQmlJSMetaMethod> possibleSignal = signalByName(signalName); + if (!possibleSignal) { + recordError(binding.location, u"Component signal '" + signalName + u"' is not recognized"); + return; + } + Q_ASSERT(possibleSignal); // Component's script binding is always a signal handler + QQmlJSMetaMethod s = possibleSignal.value(); + Q_ASSERT(s.parameterNames().isEmpty()); // Component signals do not have parameters + signalReturnType = figureReturnType(s); + + const QString slotName = makeGensym(signalName + u"_slot"); + + // SignalHander specific: + QQmlJSAotMethod slotMethod {}; + slotMethod.returnType = signalReturnType; + slotMethod.name = slotName; + + // Component is special: + int runtimeIndex = + relativeToAbsoluteRuntimeIndex(m_doc, irObject, binding.value.compiledScriptIndex); + slotMethod.body += CodeGeneratorUtility::generate_callExecuteRuntimeFunction( + m_urlMethod.name + u"()", runtimeIndex, u"this"_qs, signalReturnType); + slotMethod.type = QQmlJSMetaMethod::Slot; + + // TODO: there's actually an attached type, which has completed/destruction + // signals that are typically emitted -- do we care enough about supporting + // that? see QQmlComponentAttached + if (signalName == u"completed"_qs) { + current.handleOnCompleted.body << slotName + u"();"; + } else if (signalName == u"destruction"_qs) { + if (!current.dtor) { + current.dtor = QQmlJSAotSpecialMethod {}; + current.dtor->name = u"~" + current.cppType; + } + current.dtor->firstLines << slotName + u"();"; + } + current.functions << std::move(slotMethod); +} + +void CodeGenerator::compileUrlMethod() +{ + m_urlMethod.returnType = u"const QUrl &"_qs; + m_urlMethod.name = u"url"_qs; + m_urlMethod.body << u"static QUrl docUrl = %1;"_qs.arg( + CodeGeneratorUtility::toResourcePath(m_options.resourcePath)); + m_urlMethod.body << u"return docUrl;"_qs; + m_urlMethod.declPreambles << u"static"_qs; + m_urlMethod.modifiers << u"noexcept"_qs; +} + +void CodeGenerator::recordError(const QQmlJS::SourceLocation &location, const QString &message) +{ + m_logger->logCritical(message, Log_Compiler, location); +} + +void CodeGenerator::recordError(const QV4::CompiledData::Location &location, const QString &message) +{ + recordError(QQmlJS::SourceLocation { 0, 0, location.line, location.column }, message); +} |