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 | |
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')
-rw-r--r-- | tools/qmltc/CMakeLists.txt | 10 | ||||
-rw-r--r-- | tools/qmltc/main.cpp | 32 | ||||
-rw-r--r-- | tools/qmltc/prototype/codegenerator.cpp | 1857 | ||||
-rw-r--r-- | tools/qmltc/prototype/codegenerator.h | 180 | ||||
-rw-r--r-- | tools/qmltc/prototype/codegeneratorutil.cpp | 238 | ||||
-rw-r--r-- | tools/qmltc/prototype/codegeneratorutil.h | 126 | ||||
-rw-r--r-- | tools/qmltc/prototype/codegeneratorwriter.cpp | 438 | ||||
-rw-r--r-- | tools/qmltc/prototype/codegeneratorwriter.h | 57 | ||||
-rw-r--r-- | tools/qmltc/prototype/generatedcodeprimitives.h | 126 | ||||
-rw-r--r-- | tools/qmltc/prototype/qml2cppcontext.h | 108 | ||||
-rw-r--r-- | tools/qmltc/prototype/qml2cppdefaultpasses.cpp | 941 | ||||
-rw-r--r-- | tools/qmltc/prototype/qml2cppdefaultpasses.h | 75 | ||||
-rw-r--r-- | tools/qmltc/prototype/qml2cpppropertyutils.h | 73 | ||||
-rw-r--r-- | tools/qmltc/prototype/qmlcompiler.h | 177 | ||||
-rw-r--r-- | tools/qmltc/prototype/typeresolver.cpp | 148 | ||||
-rw-r--r-- | tools/qmltc/prototype/typeresolver.h | 87 | ||||
-rw-r--r-- | tools/qmltc/prototype/visitor.cpp | 62 | ||||
-rw-r--r-- | tools/qmltc/prototype/visitor.h | 62 |
18 files changed, 4780 insertions, 17 deletions
diff --git a/tools/qmltc/CMakeLists.txt b/tools/qmltc/CMakeLists.txt index cda6eef7e8..b5ed382e3e 100644 --- a/tools/qmltc/CMakeLists.txt +++ b/tools/qmltc/CMakeLists.txt @@ -14,6 +14,16 @@ qt_internal_add_tool(${target_name} qmltccompilerpieces.h qmltccompilerutils.h qmltcpropertyutils.h + + prototype/generatedcodeprimitives.h + prototype/qml2cppcontext.h + prototype/visitor.cpp prototype/visitor.h + prototype/qml2cppdefaultpasses.cpp prototype/qml2cppdefaultpasses.h + prototype/codegenerator.cpp prototype/codegenerator.h + prototype/codegeneratorutil.cpp prototype/codegeneratorutil.h + prototype/codegeneratorwriter.cpp prototype/codegeneratorwriter.h + prototype/qmlcompiler.h + prototype/typeresolver.cpp prototype/typeresolver.h DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII diff --git a/tools/qmltc/main.cpp b/tools/qmltc/main.cpp index dd2dfb97ba..8ae320c79e 100644 --- a/tools/qmltc/main.cpp +++ b/tools/qmltc/main.cpp @@ -27,8 +27,9 @@ ****************************************************************************/ #include "qmltccommandlineutils.h" -#include "qmltccompiler.h" -#include "qmltcvisitor.h" +#include "prototype/codegenerator.h" +#include "prototype/visitor.h" +#include "prototype/typeresolver.h" #include <QtQml/private/qqmlirbuilder_p.h> #include <private/qqmljscompiler_p.h> @@ -42,7 +43,7 @@ # include <QtCore/qcommandlineparser.h> #endif -#include <cstdio> +#include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE void setupLogger(QQmlJSLogger &logger) // prepare logger to work with compiler { @@ -163,31 +164,28 @@ int main(int argc, char **argv) return EXIT_FAILURE; } - QmltcCompilerInfo info; - info.outputCppFile = outputCppFile; - info.outputHFile = outputHFile; - info.outputNamespace = parser.value(namespaceOption); - info.resourcePath = parser.value(resourcePathOption); + Options options; + options.outputCppFile = parser.value(outputCppOption); + options.outputHFile = parser.value(outputHOption); + options.resourcePath = parser.value(resourcePathOption); + options.outNamespace = parser.value(namespaceOption); QQmlJSImporter importer { importPaths, /* resource file mapper */ nullptr }; QQmlJSLogger logger; logger.setFileName(url); logger.setCode(sourceCode); setupLogger(logger); - QmltcVisitor visitor(&importer, &logger, implicitImportDirectory, qmldirFiles); - QmltcTypeResolver typeResolver { &importer }; - typeResolver.init(&visitor, document.program); + + Qmltc::Visitor visitor(&importer, &logger, implicitImportDirectory, qmldirFiles); + Qmltc::TypeResolver typeResolver { &importer }; + typeResolver.init(visitor, document.program); if (logger.hasWarnings() || logger.hasErrors()) return EXIT_FAILURE; - if (logger.hasWarnings() || logger.hasErrors()) { - // TODO: how do we print errors/warnings/etc.? - return EXIT_FAILURE; - } + CodeGenerator generator(url, &logger, &document, &typeResolver); + generator.generate(options); - QmltcCompiler compiler(url, &typeResolver, &visitor, &logger); - compiler.compile(info); if (logger.hasWarnings() || logger.hasErrors()) return EXIT_FAILURE; 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); +} diff --git a/tools/qmltc/prototype/codegenerator.h b/tools/qmltc/prototype/codegenerator.h new file mode 100644 index 0000000000..e4a1bb4705 --- /dev/null +++ b/tools/qmltc/prototype/codegenerator.h @@ -0,0 +1,180 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef CODEGENERATOR_H +#define CODEGENERATOR_H + +#include "prototype/typeresolver.h" +#include "prototype/qmlcompiler.h" +#include "prototype/generatedcodeprimitives.h" +#include "prototype/qml2cppcontext.h" + +#include <QtCore/qlist.h> +#include <QtCore/qqueue.h> +#include <QtCore/qhash.h> + +#include <QtQml/private/qqmlirbuilder_p.h> +#include <private/qqmljscompiler_p.h> +#include <private/qqmljstyperesolver_p.h> + +#include <variant> +#include <utility> + +class CodeGenerator +{ +public: + CodeGenerator(const QString &url, QQmlJSLogger *logger, QmlIR::Document *doc, + const Qmltc::TypeResolver *localResolver); + + // main function: given compilation options, generates C++ code (implicitly) + void generate(const Options &options); + + // TODO: this should really be just QQmlJSScope::ConstPtr (and maybe C++ + // class name), but bindings are currently not represented in QQmlJSScope, + // so have to keep track of QmlIR::Object and consequently some extra stuff + using CodeGenObject = Qml2CppObject; + +private: + QString m_url; // document url + QQmlJSLogger *m_logger = nullptr; + QmlIR::Document *m_doc = nullptr; + const Qmltc::TypeResolver *m_localTypeResolver = nullptr; + QStringList m_qmlSource; // QML source code split to lines + + Options m_options = {}; // compilation options + + // convenient object abstraction, laid out as QmlIR::Document.objects + QList<CodeGenObject> m_objects; + // mapping from type to m_objects index + QHash<QQmlJSScope::ConstPtr, qsizetype> m_typeToObjectIndex; // TODO: remove this + // mapping from to-be-wrapped object to the wrapper's object pseudo-index + QHash<int, int> m_implicitComponentMapping; + + QQmlJSAotMethod m_urlMethod; + + // helper struct used for unique string generation + struct UniqueStringId + { + QString combined; + UniqueStringId(const QQmlJSAotObject &compiled, const QString &value) + : combined(compiled.cppType + u"_" + value) + { + Q_ASSERT(!compiled.cppType.isEmpty()); + } + operator QString() { return combined; } + + friend bool operator==(const UniqueStringId &x, const UniqueStringId &y) + { + return x.combined == y.combined; + } + friend bool operator!=(const UniqueStringId &x, const UniqueStringId &y) + { + return !(x == y); + } + friend bool operator<(const UniqueStringId &x, const UniqueStringId &y) + { + return x.combined < y.combined; + } + friend size_t qHash(const UniqueStringId &x) { return qHash(x.combined); } + }; + + // QML attached types that are already added as member variables (faster lookup) + QSet<UniqueStringId> m_attachedTypesAlreadyRegistered; + + // machinery for unique names generation + QHash<QString, qsizetype> m_typeCounts; + QString makeGensym(const QString &base); + + // crutch to remember QQmlListReference names, the unique naming convention + // is used for these so there's never a conflict + QSet<UniqueStringId> m_listReferencesCreated; + + // native QML base type names of the to-be-compiled objects which happen to + // also be generated (but separately) + QSet<QString> m_qmlCompiledBaseTypes; + + // a set of objects that participate in "on" assignments + QSet<UniqueStringId> m_onAssignmentObjectsCreated; + + // a vector of names of children that need to be end-initialized within type + QList<QString> m_localChildrenToEndInit; + // a vector of children that need to be finalized within type + QList<QQmlJSScope::ConstPtr> m_localChildrenToFinalize; + + // a crutch (?) to enforce special generated code for aliases to ids, for + // example: `property alias p: root` + QSet<QQmlJSMetaProperty> m_aliasesToIds; + + // init function that constructs m_objects + void constructObjects(QSet<QString> &requiredCppIncludes); + + bool m_isAnonymous = false; // crutch to distinguish QML_ELEMENT from QML_ANONYMOUS + + // code compilation functions that produce "compiled" entities + void compileObject(QQmlJSAotObject ¤t, const CodeGenObject &object); + void compileEnum(QQmlJSAotObject ¤t, const QQmlJSMetaEnum &e); + void compileProperty(QQmlJSAotObject ¤t, const QQmlJSMetaProperty &p, + const QQmlJSScope::ConstPtr &owner); + void compileAlias(QQmlJSAotObject ¤t, const QQmlJSMetaProperty &alias, + const QQmlJSScope::ConstPtr &owner); + void compileMethod(QQmlJSAotObject ¤t, const QQmlJSMetaMethod &m, + const QmlIR::Function *f, const CodeGenObject &object); + void compileUrlMethod(); // special case + + // helper structure that holds the information necessary for most bindings, + // such as accessor name, which is used to reference the properties like: + // (accessor.name)->(propertyName): this->myProperty. it is also used in + // more advanced scenarios by attached and grouped properties + struct AccessorData + { + QQmlJSScope::ConstPtr scope; // usually object.type + QString name; // usually "this" + QString propertyName; // usually empty + bool isValueType = false; // usually false + }; + void compileBinding(QQmlJSAotObject ¤t, const QmlIR::Binding &binding, + const CodeGenObject &object, const AccessorData &accessor); + // special case (for simplicity) + void compileScriptBinding(QQmlJSAotObject ¤t, const QmlIR::Binding &binding, + const QString &bindingSymbolName, const CodeGenObject &object, + const QString &propertyName, + const QQmlJSScope::ConstPtr &propertyType, + const AccessorData &accessor); + + // TODO: remove this special case + void compileScriptBindingOfComponent(QQmlJSAotObject ¤t, const QmlIR::Object *object, + const QQmlJSScope::ConstPtr objectType, + const QmlIR::Binding &binding, + const QString &propertyName); + + // helper methods: + void recordError(const QQmlJS::SourceLocation &location, const QString &message); + void recordError(const QV4::CompiledData::Location &location, const QString &message); +}; + +#endif // CODEGENERATOR_H diff --git a/tools/qmltc/prototype/codegeneratorutil.cpp b/tools/qmltc/prototype/codegeneratorutil.cpp new file mode 100644 index 0000000000..248a40eebf --- /dev/null +++ b/tools/qmltc/prototype/codegeneratorutil.cpp @@ -0,0 +1,238 @@ +/**************************************************************************** +** +** 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/codegeneratorutil.h" +#include "prototype/qml2cpppropertyutils.h" + +#include <QtCore/qstringbuilder.h> + +#include <tuple> + +// NB: this variable would behave correctly as long as QML init and QML finalize +// are non-virtual functions +const QQmlJSAotVariable CodeGeneratorUtility::childrenOffsetVariable = { u"qsizetype"_qs, + u"QML_choffset"_qs, + QString() }; + +const QQmlJSAotVariable CodeGeneratorUtility::compilationUnitVariable = { + u"QV4::ExecutableCompilationUnit *"_qs, u"QML_cu"_qs, QString() +}; + +static std::tuple<QStringList, QString, QStringList> +wrapIfMismatchingType(const QQmlJSMetaProperty &p, QString value) +{ + auto isDerivedFromBuiltin = [](QQmlJSScope::ConstPtr t, const QString &builtin) { + for (; t; t = t->baseType()) { + if (t->internalName() == builtin) + return true; + } + return false; + }; + QStringList prologue; + QStringList epilogue; + auto propType = p.type(); + if (isDerivedFromBuiltin(propType, u"QVariant"_qs)) { + const QString variantName = u"var_" + p.propertyName(); + prologue << u"{ // accepts QVariant"_qs; + prologue << u"QVariant " + variantName + u";"; + prologue << variantName + u".setValue(" + value + u");"; + epilogue << u"}"_qs; + value = u"std::move(" + variantName + u")"; + } + /*else if (isDerivedFromBuiltin(propType, u"QQmlComponent"_qs)) { + // assume value can always be converted to "QObject *" + prologue << u"// accepts QQmlComponent"_qs; + value = u"new QQmlComponent(" + value + u")"; + }*/ + return { prologue, value, epilogue }; +} + +// TODO: when assigning to a property, if property.type() != value.type(), have +// to wrap it (actually, wrap unconditionally if property.type() is QVariant or +// QQmlComponent -- other cases out of scope for now) +QStringList CodeGeneratorUtility::generate_assignToProperty( + const QQmlJSScope::ConstPtr &type, const QString &propertyName, const QQmlJSMetaProperty &p, + const QString &value, const QString &accessor, bool constructQVariant) +{ + Q_ASSERT(p.isValid()); + Q_ASSERT(!p.isList()); // NB: this code does not handle list properties + + QStringList code; + code.reserve(6); // should always be enough + + if (type->hasOwnProperty(propertyName)) { + Q_ASSERT(!p.isPrivate()); + // this object is compiled, so just assignment should work fine + code << accessor + u"->m_" + propertyName + u" = " + value + u";"; + } else if (QString propertySetter = p.write(); !propertySetter.isEmpty()) { + // there's a WRITE function + auto [prologue, wrappedValue, epilogue] = wrapIfMismatchingType(p, value); + code += prologue; + code << CodeGeneratorUtility::generate_getPrivateClass(accessor, p) + u"->" + propertySetter + + u"(" + wrappedValue + u");"; + code += epilogue; + } else { + // this property is weird, fallback to `setProperty` + code << u"{ // couldn't find property setter, so using QObject::setProperty()"_qs; + QString val = value; + if (constructQVariant) { + const QString variantName = u"var_" + propertyName; + code << u"QVariant " + variantName + u";"; + code << variantName + u".setValue(" + val + u");"; + val = u"std::move(" + variantName + u")"; + } + // NB: setProperty() would handle private properties + code << accessor + u"->setProperty(\"" + propertyName + u"\", " + val + u");"; + code << u"}"_qs; + } + + return code; +} + +QStringList CodeGeneratorUtility::generate_assignToSpecialAlias( + const QQmlJSScope::ConstPtr &type, const QString &propertyName, const QQmlJSMetaProperty &p, + const QString &value, const QString &accessor, bool constructQVariant) +{ + Q_UNUSED(type); + Q_UNUSED(propertyName); + Q_UNUSED(constructQVariant); + + Q_ASSERT(p.isValid()); + Q_ASSERT(!p.isList()); // NB: this code does not handle list properties + Q_ASSERT(p.isAlias()); + Q_ASSERT(p.type()); + + QStringList code; + code.reserve(6); // should always be enough + // pretend there's a WRITE function + Qml2CppPropertyData data(p); + auto [prologue, wrappedValue, epilogue] = wrapIfMismatchingType(p, value); + code += prologue; + code << CodeGeneratorUtility::generate_getPrivateClass(accessor, p) + u"->" + data.write + u"(" + + wrappedValue + u");"; + code += epilogue; + return code; +} + +QStringList CodeGeneratorUtility::generate_callExecuteRuntimeFunction( + const QString &url, qsizetype index, const QString &accessor, const QString &returnType, + const QList<QQmlJSAotVariable> ¶meters) +{ + QStringList code; + code.reserve(12); // should always be enough + + code << u"QQmlEnginePrivate *e = QQmlEnginePrivate::get(qmlEngine(" + accessor + u"));"; + + const QString returnValueName = u"_ret"_qs; + QStringList args; + args.reserve(parameters.size() + 1); + QStringList types; + types.reserve(parameters.size() + 1); + if (returnType == u"void"_qs) { + args << u"nullptr"_qs; + types << u"QMetaType::fromType<void>()"_qs; + } else { + code << returnType + u" " + returnValueName + u"{};"; // TYPE _ret{}; + args << u"const_cast<void *>(reinterpret_cast<const void *>(std::addressof(" + + returnValueName + u")))"; + types << u"QMetaType::fromType<std::decay_t<" + returnType + u">>()"; + } + + for (const QQmlJSAotVariable &p : parameters) { + args << u"const_cast<void *>(reinterpret_cast<const void *>(std::addressof(" + p.name + + u")))"; + types << u"QMetaType::fromType<std::decay_t<" + p.cppType + u">>()"; + } + + code << u"void *_a[] = { " + args.join(u", "_qs) + u" };"; + code << u"QMetaType _t[] = { " + types.join(u", "_qs) + u" };"; + code << u"e->executeRuntimeFunction(" + url + u", " + QString::number(index) + u", " + accessor + + u", " + QString::number(parameters.size()) + u", _a, _t);"; + if (returnType != u"void"_qs) + code << u"return " + returnValueName + u";"; + + return code; +} + +QStringList CodeGeneratorUtility::generate_createBindingOnProperty( + const QString &unitVarName, const QString &scope, qsizetype functionIndex, + const QString &target, int propertyIndex, const QQmlJSMetaProperty &p, int valueTypeIndex, + const QString &subTarget) +{ + QStringList code; + code.reserve(1); + + const QString propName = u"QStringLiteral(\"" + p.propertyName() + u"\")"; + if (QString bindable = p.bindable(); !bindable.isEmpty()) { + // TODO: test that private properties are bindable + QString createBindingForBindable = u"QT_PREPEND_NAMESPACE(QQmlCppBinding)::" + u"createBindingForBindable(" + + unitVarName + u", " + scope + u", " + QString::number(functionIndex) + u", " + + target + u", " + QString::number(propertyIndex) + u", " + + QString::number(valueTypeIndex) + u", " + propName + u")"; + const QString accessor = (valueTypeIndex == -1) ? target : subTarget; + code << CodeGeneratorUtility::generate_getPrivateClass(accessor, p) + u"->" + bindable + + u"().setBinding(" + createBindingForBindable + u");"; + } else { + // TODO: test that bindings on private properties also work + QString createBindingForNonBindable = + u"QT_PREPEND_NAMESPACE(QQmlCppBinding)::createBindingForNonBindable(" + unitVarName + + u", " + scope + u", " + QString::number(functionIndex) + u", " + target + u", " + + QString::number(propertyIndex) + u", " + QString::number(valueTypeIndex) + u", " + + propName + u")"; + // Note: in this version, the binding is set implicitly + code << createBindingForNonBindable + u";"; + } + + return code; +} + +QString CodeGeneratorUtility::generate_qOverload(const QList<QQmlJSAotVariable> ¶ms, + const QString &overloaded) +{ + QStringList types; + types.reserve(params.size()); + for (const QQmlJSAotVariable &p : params) + types.emplaceBack(p.cppType); + return u"qOverload<" + types.join(u", "_qs) + u">(" + overloaded + u")"; +} + +QString CodeGeneratorUtility::generate_addressof(const QString &addressed) +{ + return u"(&" + addressed + u")"; +} + +QString CodeGeneratorUtility::generate_getPrivateClass(const QString &accessor, + const QQmlJSMetaProperty &p) +{ + if (!p.isPrivate()) + return accessor; + + const QString privateType = p.privateClass(); + return u"static_cast<" + privateType + u" *>(QObjectPrivate::get(" + accessor + u"))"; +} diff --git a/tools/qmltc/prototype/codegeneratorutil.h b/tools/qmltc/prototype/codegeneratorutil.h new file mode 100644 index 0000000000..74529f6853 --- /dev/null +++ b/tools/qmltc/prototype/codegeneratorutil.h @@ -0,0 +1,126 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef CODEGENERATORUTIL_H +#define CODEGENERATORUTIL_H + +#include "prototype/qmlcompiler.h" + +#include <private/qqmljsscope_p.h> +#include <private/qqmljsmetatypes_p.h> + +#include <QtCore/qlist.h> +#include <QtCore/qstring.h> + +#include <utility> + +struct CodeGeneratorUtility +{ + // magic variable, necessary for correct handling of object bindings: since + // object creation and binding setup are separated across functions, we + // fetch a subobject by: QObject::children().at(offset + localIndex) + // ^ + // childrenOffsetVariable + // this variable would often be equal to 0, but there's no guarantee. and it + // is required due to non-trivial aliases dependencies: aliases can + // reference any object in the document by id, which automatically means + // that all ids have to be set up before we get to finalization (and the + // only place for it is init) + static const QQmlJSAotVariable childrenOffsetVariable; + + // represents QV4::ExecutableCompilationUnit + static const QQmlJSAotVariable compilationUnitVariable; + + // helper functions: + static QString toResourcePath(const QString &s) + { + return u"QUrl(QStringLiteral(\"qrc:" + s + u"\"))"; + } + + static QString escapeString(QString s) + { + return s.replace(u"\\"_qs, u"\\\\"_qs) + .replace(u"\""_qs, u"\\\""_qs) + .replace(u"\n"_qs, u"\\n"_qs); + } + + static QString createStringLiteral(const QString &s) + { + return u"QStringLiteral(\"" + escapeString(s) + u"\")"; + } + + static QString getInternalNameAwareOfAccessSemantics(const QQmlJSScope::ConstPtr &s) + { + // TODO: sequence is not supported yet + Q_ASSERT(s->accessSemantics() != QQmlJSScope::AccessSemantics::Sequence); + const QString suffix = (s->accessSemantics() == QQmlJSScope::AccessSemantics::Reference) + ? u" *"_qs + : u""_qs; + return s->internalName() + suffix; + } + + static QString wrapInConstRefIfNotAPointer(QString s) + { + if (!s.endsWith(u'*')) + s = u"const " + s + u"&"; + return s; + } + + static QString metaPropertyName(const QString &propertyVariableName) + { + return propertyVariableName + u"_meta"; + } + + // generate functions: + static QStringList generate_assignToProperty(const QQmlJSScope::ConstPtr &type, + const QString &propertyName, + const QQmlJSMetaProperty &p, const QString &value, + const QString &accessor, + bool constructQVariant = false); + static QStringList generate_assignToSpecialAlias(const QQmlJSScope::ConstPtr &type, + const QString &propertyName, + const QQmlJSMetaProperty &p, + const QString &value, const QString &accessor, + bool constructQVariant = false); + // TODO: 3 separate versions: bindable QML, bindable C++, non-bindable C++ + static QStringList + generate_callExecuteRuntimeFunction(const QString &url, qsizetype index, + const QString &accessor, const QString &returnType, + const QList<QQmlJSAotVariable> ¶meters = {}); + static QStringList + generate_createBindingOnProperty(const QString &unitVarName, const QString &scope, + qsizetype functionIndex, const QString &target, + int propertyIndex, const QQmlJSMetaProperty &p, + int valueTypeIndex, const QString &subTarget); + static QString generate_qOverload(const QList<QQmlJSAotVariable> ¶meters, + const QString &overloaded); + static QString generate_addressof(const QString &addressed); + static QString generate_getPrivateClass(const QString &accessor, const QQmlJSMetaProperty &p); +}; + +#endif // CODEGENERATORUTIL_H diff --git a/tools/qmltc/prototype/codegeneratorwriter.cpp b/tools/qmltc/prototype/codegeneratorwriter.cpp new file mode 100644 index 0000000000..5ccd698c63 --- /dev/null +++ b/tools/qmltc/prototype/codegeneratorwriter.cpp @@ -0,0 +1,438 @@ +/**************************************************************************** +** +** 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 "codegeneratorwriter.h" + +#include <private/qqmljsmetatypes_p.h> + +#include <QtCore/qfileinfo.h> + +#include <utility> + +static constexpr char16_t newLine[] = +#ifdef Q_OS_WIN32 + u"\r\n"; +#else + u"\n"; +#endif +static constexpr char newLineLatin1[] = +#ifdef Q_OS_WIN32 + "\r\n"; +#else + "\n"; +#endif + +static QString urlToMacro(const QString &url) +{ + QFileInfo fi(url); + return u"Q_QMLTC_" + fi.baseName().toUpper(); +} + +static QString getFunctionCategory(const QQmlJSAotMethodBase &compiled) +{ + QString category; + switch (compiled.access) { + case QQmlJSMetaMethod::Private: + category = u"private"_qs; + break; + case QQmlJSMetaMethod::Protected: + category = u"protected"_qs; + break; + case QQmlJSMetaMethod::Public: + category = u"public"_qs; + break; + } + return category; +} + +static QString getFunctionCategory(const QQmlJSAotMethod &compiled) +{ + QString category = getFunctionCategory(static_cast<const QQmlJSAotMethodBase &>(compiled)); + switch (compiled.type) { + case QQmlJSMetaMethod::Signal: + category = u"signals"_qs; + break; + case QQmlJSMetaMethod::Slot: + category += u" slots"_qs; + break; + case QQmlJSMetaMethod::Method: + break; + } + return category; +} + +void CodeGeneratorWriter::writeGlobalHeader(GeneratedCodeUtils &code, const QString &sourceName, + const QString &hPath, const QString &cppPath, + const QString &outNamespace, + const QSet<QString> &requiredCppIncludes) +{ + Q_UNUSED(newLineLatin1); + + Q_UNUSED(cppPath); + const QString preamble = + u"// This code is auto-generated by the qmlcompiler tool from the file '" + sourceName + + u"'" + newLine + u"// WARNING! All changes made in this file will be lost!" + newLine; + code.appendToHeader(preamble); + code.appendToImpl(preamble); + code.appendToHeader(u"// NOTE: This generated API is to be considered implementation detail."); + code.appendToHeader( + u"// It may change from version to version and should not be relied upon."); + + const QString headerMacro = urlToMacro(sourceName); + code.appendToHeader(u"#ifndef %1_H"_qs.arg(headerMacro)); + code.appendToHeader(u"#define %1_H"_qs.arg(headerMacro)); + + code.appendToHeader(u"#include <QtCore/qproperty.h>"); + code.appendToHeader(u"#include <QtCore/qobject.h>"); + code.appendToHeader(u"#include <QtCore/qcoreapplication.h>"); + code.appendToHeader(u"#include <QtQml/qqmlengine.h>"); + code.appendToHeader(u"#include <QtCore/qurl.h>"); // used in engine execution + code.appendToHeader(u"#include <QtQml/qqml.h>"); // used for attached properties + + code.appendToHeader(u"#include <private/qqmlengine_p.h>"); // NB: private header + + code.appendToHeader(u"#include <QQmlListProperty>"); // required by list properties + + // include custom C++ includes required by used types + code.appendToHeader(u"// BEGIN(custom_cpp_includes)"); + for (const auto &requiredInclude : requiredCppIncludes) { + code.appendToHeader(u"#include \"" + requiredInclude + u"\""); + } + code.appendToHeader(u"// END(custom_cpp_includes)"); + + code.appendToImpl(u"#include \"" + hPath + u"\""); // include own .h file + code.appendToImpl(u"#include <private/qqmlcppbinding_p.h>"); // QmltcSupportLib + code.appendToImpl(u"#include <private/qqmlcpponassignment_p.h>"); // QmltcSupportLib + + code.appendToImpl(u"#include <private/qqmlobjectcreator_p.h>"); // createComponent() + + code.appendToImpl(u""); + code.appendToImpl(u"#include <private/qobject_p.h>"); // NB: for private properties + code.appendToImpl(u"#include <private/qqmlobjectcreator_p.h>"); // for finalize callbacks + + code.appendToImpl(u""); // blank line + if (!outNamespace.isEmpty()) { + code.appendToHeader(u""); // blank line + code.appendToHeader(u"namespace %1 {"_qs.arg(outNamespace)); + code.appendToImpl(u""); // blank line + code.appendToImpl(u"namespace %1 {"_qs.arg(outNamespace)); + } +} + +void CodeGeneratorWriter::writeGlobalFooter(GeneratedCodeUtils &code, const QString &sourceName, + const QString &hPath, const QString &cppPath, + const QString &outNamespace) +{ + Q_UNUSED(code); + Q_UNUSED(hPath); + Q_UNUSED(cppPath); + + if (!outNamespace.isEmpty()) { + code.appendToImpl(u"} // namespace %1"_qs.arg(outNamespace)); + code.appendToImpl(u""); // blank line + code.appendToHeader(u"} // namespace %1"_qs.arg(outNamespace)); + code.appendToHeader(u""); // blank line + } + + code.appendToHeader(u"#endif // %1_H"_qs.arg(urlToMacro(sourceName))); + code.appendToHeader(u""); // blank line +} + +static QString classString(const QQmlJSAotObject &compiled) +{ + QString str = u"class " + compiled.cppType; + QStringList nonEmptyBaseClasses; + nonEmptyBaseClasses.reserve(compiled.baseClasses.size()); + std::copy_if(compiled.baseClasses.cbegin(), compiled.baseClasses.cend(), + std::back_inserter(nonEmptyBaseClasses), + [](const QString &entry) { return !entry.isEmpty(); }); + if (!nonEmptyBaseClasses.isEmpty()) + str += u" : public " + nonEmptyBaseClasses.join(u", public "_qs); + return str; +} + +void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotObject &compiled) +{ + code.appendToHeader(u""); // just new line + code.appendToImpl(u""); // just new line + + // generate class preamble + code.appendToHeader(classString(compiled)); + code.appendToHeader(u"{"); + for (const QString &mocLine : qAsConst(compiled.mocCode)) { + code.appendToHeader(mocLine, 1); + } + + GeneratedCodeUtils::MemberNamespaceScope thisObjectScope(code, compiled.cppType); + Q_UNUSED(thisObjectScope); + { + GeneratedCodeUtils::HeaderIndentationScope headerIndentScope(code); + Q_UNUSED(headerIndentScope); + + // generate ctor + if (compiled.ignoreInit) { // TODO: this branch should be eliminated + // NB: here the ctor should be public + code.appendToHeader(getFunctionCategory(compiled.baselineCtor) + u":", -1); + CodeGeneratorWriter::write(code, compiled.baselineCtor); + } else { + Q_ASSERT(compiled.baselineCtor.access == compiled.init.access); + code.appendToHeader(getFunctionCategory(compiled.init) + u":", -1); + CodeGeneratorWriter::write(code, compiled.baselineCtor); + CodeGeneratorWriter::write(code, compiled.init); + + // NB: when singleton, this ctor won't be public + code.appendToHeader(getFunctionCategory(compiled.externalCtor) + u":", -1); + CodeGeneratorWriter::write(code, compiled.externalCtor); + // TODO: actually should figure how to make this one protected + code.appendToHeader(u"public:", -1); + CodeGeneratorWriter::write(code, compiled.endInit); + CodeGeneratorWriter::write(code, compiled.completeComponent); + CodeGeneratorWriter::write(code, compiled.finalizeComponent); + CodeGeneratorWriter::write(code, compiled.handleOnCompleted); + } + + // generate dtor + if (compiled.dtor) + CodeGeneratorWriter::write(code, *compiled.dtor); + + // generate enums + code.appendToHeader(u"// BEGIN(enumerations)"); + for (const auto &enumeration : qAsConst(compiled.enums)) + CodeGeneratorWriter::write(code, enumeration); + code.appendToHeader(u"// END(enumerations)"); + + // generate child types + code.appendToHeader(u"// BEGIN(children)"); + for (const auto &child : qAsConst(compiled.children)) + CodeGeneratorWriter::write(code, child); + code.appendToHeader(u"// END(children)"); + + // generate functions + code.appendToHeader(u"// BEGIN(functions)"); + // functions are special as they are grouped by access and kind + QHash<QString, QList<const QQmlJSAotMethod *>> functionsByCategory; + for (const auto &function : qAsConst(compiled.functions)) + functionsByCategory[getFunctionCategory(function)].append(std::addressof(function)); + + for (auto it = functionsByCategory.cbegin(); it != functionsByCategory.cend(); ++it) { + code.appendToHeader(it.key() + u":", -1); + for (const QQmlJSAotMethod *function : qAsConst(it.value())) + CodeGeneratorWriter::write(code, *function); + } + code.appendToHeader(u"// END(functions)"); + + if (!compiled.variables.isEmpty() || !compiled.properties.isEmpty()) { + code.appendToHeader(u""); // blank line + code.appendToHeader(u"protected:", -1); + } + // generate variables + if (!compiled.variables.isEmpty()) { + code.appendToHeader(u"// BEGIN(variables)"); + for (const auto &variable : qAsConst(compiled.variables)) + CodeGeneratorWriter::write(code, variable); + code.appendToHeader(u"// END(variables)"); + } + + // generate properties + if (!compiled.properties.isEmpty()) { + code.appendToHeader(u"// BEGIN(properties)"); + for (const auto &property : qAsConst(compiled.properties)) + CodeGeneratorWriter::write(code, property); + code.appendToHeader(u"// END(properties)"); + } + } + + code.appendToHeader(u"};"); +} + +void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotEnum &compiled) +{ + code.appendToHeader(u"enum " + compiled.cppType + u" {"); + for (qsizetype i = 0; i < compiled.keys.size(); ++i) { + QString str; + if (compiled.values.isEmpty()) { + str += compiled.keys.at(i) + u","; + } else { + str += compiled.keys.at(i) + u" = " + compiled.values.at(i) + u","; + } + code.appendToHeader(str, 1); + } + code.appendToHeader(u"};"); + code.appendToHeader(compiled.ownMocLine); +} + +// NB: property generation is only concerned with property declaration in the header +void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotVariable &compiled) +{ + if (compiled.defaultValue.isEmpty()) { + code.appendToHeader(compiled.cppType + u" " + compiled.name + u";"); + } else { + code.appendToHeader(compiled.cppType + u" " + compiled.name + u" = " + compiled.defaultValue + + u";"); + } +} + +void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotProperty &prop) +{ + Q_ASSERT(prop.defaultValue.isEmpty()); // we don't support it yet + code.appendToHeader(u"Q_OBJECT_BINDABLE_PROPERTY(%1, %2, %3, &%1::%4)"_qs.arg( + prop.containingClass, prop.cppType, prop.name, prop.signalName)); +} + +static QString appendSpace(const QString &s) +{ + if (s.isEmpty()) + return s; + return s + u" "; +} + +static QString prependSpace(const QString &s) +{ + if (s.isEmpty()) + return s; + return u" " + s; +} + +static std::pair<QString, QString> functionSignatures(const QQmlJSAotMethodBase &m) +{ + const QString name = m.name; + const QList<QQmlJSAotVariable> ¶meterList = m.parameterList; + QStringList headerParamList; + QStringList implParamList; + for (const QQmlJSAotVariable &variable : parameterList) { + const QString commonPart = variable.cppType + u" " + variable.name; + implParamList << commonPart; + headerParamList << commonPart; + if (!variable.defaultValue.isEmpty()) + headerParamList.back() += u" = " + variable.defaultValue; + } + const QString headerSignature = name + u"(" + headerParamList.join(u", "_qs) + u")" + + prependSpace(m.modifiers.join(u" ")); + const QString implSignature = name + u"(" + implParamList.join(u", "_qs) + u")" + + prependSpace(m.modifiers.join(u" ")); + return { headerSignature, implSignature }; +} + +static QString functionReturnType(const QQmlJSAotMethodBase &m) +{ + return appendSpace(m.declPreambles.join(u" "_qs)) + m.returnType; +} + +void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotMethod &compiled) +{ + const auto [hSignature, cppSignature] = functionSignatures(compiled); + // Note: augment return type with preambles in declaration + code.appendToHeader(functionReturnType(compiled) + u" " + hSignature + u";"); + + // do not generate method implementation if it is a signal + const auto methodType = compiled.type; + if (methodType != QQmlJSMetaMethod::Signal) { + code.appendToImpl(u""); // just new line + code.appendToImpl(compiled.returnType); + code.appendSignatureToImpl(cppSignature); + code.appendToImpl(u"{"); + { + GeneratedCodeUtils::ImplIndentationScope indentScope(code); + Q_UNUSED(indentScope); + for (const QString &line : qAsConst(compiled.firstLines)) + code.appendToImpl(line); + for (const QString &line : qAsConst(compiled.body)) + code.appendToImpl(line); + for (const QString &line : qAsConst(compiled.lastLines)) + code.appendToImpl(line); + } + code.appendToImpl(u"}"); + } +} + +void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSAotSpecialMethod &compiled) +{ + const auto [hSignature, cppSignature] = functionSignatures(compiled); + const QString returnTypeWithSpace = + compiled.returnType.isEmpty() ? u""_qs : compiled.returnType + u" "; + + code.appendToHeader(returnTypeWithSpace + hSignature + u";"); + + code.appendToImpl(u""); // just new line + if (!returnTypeWithSpace.isEmpty()) + code.appendToImpl(returnTypeWithSpace); + code.appendSignatureToImpl(cppSignature); + if (!compiled.initializerList.isEmpty()) { + code.appendToImpl(u":", 1); + code.appendToImpl(compiled.initializerList.join(u","_qs + newLine + newLine + + u" "_qs.repeated(code.implIndent + 1)), + 1); + } + code.appendToImpl(u"{"); + { + GeneratedCodeUtils::ImplIndentationScope indentScope(code); + Q_UNUSED(indentScope); + for (const QString &line : qAsConst(compiled.firstLines)) + code.appendToImpl(line); + for (const QString &line : qAsConst(compiled.body)) + code.appendToImpl(line); + for (const QString &line : qAsConst(compiled.lastLines)) + code.appendToImpl(line); + } + code.appendToImpl(u"}"); +} + +void CodeGeneratorWriter::writeUrl(GeneratedCodeUtils &code, const QQmlJSAotMethod &urlMethod) +{ + const auto [hSignature, _] = functionSignatures(urlMethod); + Q_UNUSED(_); + Q_ASSERT(!urlMethod.returnType.isEmpty()); + code.appendToImpl(functionReturnType(urlMethod) + hSignature); + code.appendToImpl(u"{"); + { + GeneratedCodeUtils::ImplIndentationScope indentScope(code); + Q_UNUSED(indentScope); + Q_ASSERT(urlMethod.firstLines.isEmpty() && urlMethod.lastLines.isEmpty()); + for (const QString &line : qAsConst(urlMethod.body)) + code.appendToImpl(line); + } + code.appendToImpl(u"}"); +} + +void CodeGeneratorWriter::write(GeneratedCodeUtils &code, const QQmlJSProgram &compiled) +{ + writeGlobalHeader(code, compiled.url, compiled.hPath, compiled.cppPath, compiled.outNamespace, + compiled.includes); + + code.appendToImpl(u""); // just new line + writeUrl(code, compiled.urlMethod); + + // forward declare objects before writing them + for (const QQmlJSAotObject &compiled : qAsConst(compiled.compiledObjects)) + code.appendToHeader(u"class " + compiled.cppType + u";"); + + // write all the objects + for (const QQmlJSAotObject &compiled : qAsConst(compiled.compiledObjects)) + write(code, compiled); + + writeGlobalFooter(code, compiled.url, compiled.hPath, compiled.cppPath, compiled.outNamespace); +} diff --git a/tools/qmltc/prototype/codegeneratorwriter.h b/tools/qmltc/prototype/codegeneratorwriter.h new file mode 100644 index 0000000000..e03a2123ac --- /dev/null +++ b/tools/qmltc/prototype/codegeneratorwriter.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef CODEGENERATORWRITER_H +#define CODEGENERATORWRITER_H + +#include "generatedcodeprimitives.h" +#include "qmlcompiler.h" + +// writes compiled code into the GeneratedCode structure +struct CodeGeneratorWriter +{ + static void writeGlobalHeader(GeneratedCodeUtils &code, const QString &sourceName, + const QString &hPath, const QString &cppPath, + const QString &outNamespace, + const QSet<QString> &requiredCppIncludes); + static void writeGlobalFooter(GeneratedCodeUtils &code, const QString &sourceName, + const QString &hPath, const QString &cppPath, + const QString &outNamespace); + static void write(GeneratedCodeUtils &code, const QQmlJSAotObject &compiled); + static void write(GeneratedCodeUtils &code, const QQmlJSAotEnum &compiled); + static void write(GeneratedCodeUtils &code, const QQmlJSAotVariable &compiled); + static void write(GeneratedCodeUtils &code, const QQmlJSAotProperty &compiled); + static void write(GeneratedCodeUtils &code, const QQmlJSAotMethod &compiled); + static void write(GeneratedCodeUtils &code, const QQmlJSAotSpecialMethod &compiled); + static void write(GeneratedCodeUtils &code, const QQmlJSProgram &compiled); + +private: + static void writeUrl(GeneratedCodeUtils &code, const QQmlJSAotMethod &urlMethod); +}; + +#endif // CODEGENERATORWRITER_H diff --git a/tools/qmltc/prototype/generatedcodeprimitives.h b/tools/qmltc/prototype/generatedcodeprimitives.h new file mode 100644 index 0000000000..46d7b1461f --- /dev/null +++ b/tools/qmltc/prototype/generatedcodeprimitives.h @@ -0,0 +1,126 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef GENERATEDCODEPRIMITIVES_H +#define GENERATEDCODEPRIMITIVES_H + +#include <QtCore/qstring.h> +#include <QtCore/qstack.h> + +// holds generated code for header and implementation files +struct GeneratedCode +{ + QString header; + QString implementation; +}; + +// utility class that provides pretty-printing of the generated code into the +// GeneratedCode buffer +struct GeneratedCodeUtils +{ + GeneratedCode &m_code; // buffer + + QStack<QString> memberNamespaceStack; // member names scopes e.g. MyClass::MySubclass:: + int headerIndent = 0; // header indentation level + int implIndent = 0; // implementation indentation level + + GeneratedCodeUtils(GeneratedCode &code) : m_code(code) { } + + // manages current scope of the generated code, which is necessary for + // implementation file generation. Example: + // class MyClass { MyClass(); }; - in header + // MyClass::MyClass() {} - in implementation file + // MemberNamespaceScope exists to be able to record and use "MyClass::" + struct MemberNamespaceScope + { + GeneratedCodeUtils &m_code; + MemberNamespaceScope(GeneratedCodeUtils &code, const QString &str) : m_code(code) + { + m_code.memberNamespaceStack.push(str); + } + ~MemberNamespaceScope() { m_code.memberNamespaceStack.pop(); } + }; + + // manages current indentation scope: upon creation, increases current + // scope, which is decreased back upon deletion. this is used by append* + // functions that work with GeneratedCode::header to correctly indent the + // input + struct HeaderIndentationScope + { + GeneratedCodeUtils &m_code; + HeaderIndentationScope(GeneratedCodeUtils &code) : m_code(code) { ++m_code.headerIndent; } + ~HeaderIndentationScope() { --m_code.headerIndent; } + }; + + // manages current indentation scope: upon creation, increases current + // scope, which is decreased back upon deletion. this is used by append* + // functions that work with GeneratedCode::implementation to correctly + // indent the input + struct ImplIndentationScope + { + GeneratedCodeUtils &m_code; + ImplIndentationScope(GeneratedCodeUtils &code) : m_code(code) { ++m_code.implIndent; } + ~ImplIndentationScope() { --m_code.implIndent; } + }; + + // appends string \a what with extra indentation \a extraIndent to current + // GeneratedCode::header string + template<typename String> + void appendToHeader(const String &what, int extraIndent = 0) + { + constexpr char16_t newLine[] = u"\n"; + m_code.header += QString((headerIndent + extraIndent) * 4, u' ') + what + newLine; + } + + // appends string \a what with extra indentation \a extraIndent to current + // GeneratedCode::implementation string + template<typename String> + void appendToImpl(const String &what, int extraIndent = 0) + { + constexpr char16_t newLine[] = u"\n"; + m_code.implementation += QString((implIndent + extraIndent) * 4, u' ') + what + newLine; + } + + // appends string \a what with extra indentation \a extraIndent to current + // GeneratedCode::implementation string. this is a special case function + // that expects \a what to be a function signature as \a what is prepended + // with member scope related text. for example, string "foo()" is converted + // to string "MyClass::foo()" before append + template<typename String> + void appendSignatureToImpl(const String &what, int extraIndent = 0) + { + constexpr char16_t newLine[] = u"\n"; + QString signatureScope; + for (const auto &subScope : memberNamespaceStack) + signatureScope += subScope + u"::"; + m_code.implementation += + signatureScope + QString((implIndent + extraIndent) * 4, u' ') + what + newLine; + } +}; + +#endif // GENERATEDCODEPRIMITIVES_H diff --git a/tools/qmltc/prototype/qml2cppcontext.h b/tools/qmltc/prototype/qml2cppcontext.h new file mode 100644 index 0000000000..56bb5a12db --- /dev/null +++ b/tools/qmltc/prototype/qml2cppcontext.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QML2CPPCONTEXT_H +#define QML2CPPCONTEXT_H + +#include "prototype/qmlcompiler.h" +#include "prototype/typeresolver.h" + +#include <private/qqmljsdiagnosticmessage_p.h> +#include <QtQml/private/qqmlirbuilder_p.h> +#include <private/qqmljstyperesolver_p.h> + +#include <QtCore/QList> + +#include <variant> +#include <functional> + +struct Qml2CppContext +{ + const QmlIR::Document *document = nullptr; + const Qmltc::TypeResolver *typeResolver = nullptr; + QString documentUrl; + QQmlJSLogger *logger = nullptr; + const QHash<QQmlJSScope::ConstPtr, qsizetype> *typeIndices = nullptr; // TODO: remove this? + + void recordError(const QQmlJS::SourceLocation &location, const QString &message) const + { + Q_ASSERT(logger); + logger->logCritical(message, Log_Compiler, location); + } + + void recordError(const QV4::CompiledData::Location &location, const QString &message) const + { + recordError(QQmlJS::SourceLocation { 0, 0, location.line, location.column }, message); + } +}; + +struct Qml2CppObject +{ + QmlIR::Object *irObject = nullptr; // raw IR object representation + QQmlJSScope::Ptr type; // Qml/JS type representation for IR object +}; + +// the corner stone of the compiler: compiler pass function prototype. the main +// idea is that, given "context" as input and object hierarchy as input/output, +// you run an arbitrary function (e.g. to verify that QQmlJSScope::ConstPtr +// types are valid). +using Qml2CppCompilerPass = std::function<void(/* IN */ const Qml2CppContext &, + /* INOUT */ QList<Qml2CppObject> &)>; + +class Qml2CppCompilerPassExecutor +{ + Qml2CppContext m_context; + QList<Qml2CppObject> &m_objects; + QList<Qml2CppCompilerPass> m_passes; + +public: + Qml2CppCompilerPassExecutor(const QmlIR::Document *doc, const Qmltc::TypeResolver *resolver, + const QString &url, QList<Qml2CppObject> &objects, + const QHash<QQmlJSScope::ConstPtr, qsizetype> &typeIndices) + : m_context { doc, resolver, url, nullptr, &typeIndices }, m_objects { objects } + { + } + + // add new pass to the executor. first pass is typically the one that + // populates the Qml2CppObject list + void addPass(Qml2CppCompilerPass pass) { m_passes.append(pass); } + + // runs passes in the order of their insertion, populating \a logger on + // errors. right now always stops if new errors are found + void run(QQmlJSLogger *logger) + { + m_context.logger = logger; + for (const auto &pass : m_passes) { + pass(m_context, m_objects); + if (m_context.logger->hasErrors()) + return; + } + } +}; + +#endif // QML2CPPCONTEXT_H diff --git a/tools/qmltc/prototype/qml2cppdefaultpasses.cpp b/tools/qmltc/prototype/qml2cppdefaultpasses.cpp new file mode 100644 index 0000000000..b8560a39af --- /dev/null +++ b/tools/qmltc/prototype/qml2cppdefaultpasses.cpp @@ -0,0 +1,941 @@ +/**************************************************************************** +** +** 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 "qml2cppdefaultpasses.h" +#include "qml2cpppropertyutils.h" + +#include <QtCore/qfileinfo.h> +#include <QtCore/qqueue.h> +#include <QtCore/qloggingcategory.h> + +static QString const cppKeywords[] = { + u"alignas"_qs, + u"alignof"_qs, + u"and"_qs, + u"and_eq"_qs, + u"asm"_qs, + u"atomic_cancel"_qs, + u"atomic_commit"_qs, + u"atomic_noexcept"_qs, + u"auto"_qs, + u"bitand"_qs, + u"bitor"_qs, + u"bool"_qs, + u"break"_qs, + u"case"_qs, + u"catch"_qs, + u"char"_qs, + u"char8_t"_qs, + u"char16_t"_qs, + u"char32_t"_qs, + u"class"_qs, + u"compl"_qs, + u"concept"_qs, + u"const"_qs, + u"consteval"_qs, + u"constexpr"_qs, + u"const_cast"_qs, + u"continue"_qs, + u"co_await"_qs, + u"co_return"_qs, + u"co_yield"_qs, + u"decltype"_qs, + u"default"_qs, + u"delete"_qs, + u"do"_qs, + u"double"_qs, + u"dynamic_cast"_qs, + u"else"_qs, + u"enum"_qs, + u"explicit"_qs, + u"export"_qs, + u"extern"_qs, + u"false"_qs, + u"float"_qs, + u"for"_qs, + u"friend"_qs, + u"goto"_qs, + u"if"_qs, + u"inline"_qs, + u"int"_qs, + u"long"_qs, + u"mutable"_qs, + u"namespace"_qs, + u"new"_qs, + u"noexcept"_qs, + u"not"_qs, + u"not_eq"_qs, + u"nullptr"_qs, + u"operator"_qs, + u"or"_qs, + u"or_eq"_qs, + u"private"_qs, + u"protected"_qs, + u"public"_qs, + u"reflexpr"_qs, + u"register"_qs, + u"reinterpret_cast"_qs, + u"requires"_qs, + u"return"_qs, + u"short"_qs, + u"signed"_qs, + u"sizeof"_qs, + u"static"_qs, + u"static_assert"_qs, + u"static_cast"_qs, + u"struct"_qs, + u"switch"_qs, + u"synchronized"_qs, + u"template"_qs, + u"this"_qs, + u"thread_local"_qs, + u"throw"_qs, + u"true"_qs, + u"try"_qs, + u"typedef"_qs, + u"typeid"_qs, + u"typename"_qs, + u"union"_qs, + u"unsigned"_qs, + u"using"_qs, + u"virtual"_qs, + u"void"_qs, + u"volatile"_qs, + u"wchar_t"_qs, + u"while"_qs, + u"xor"_qs, + u"xor_eq"_qs, +}; + +static bool isReservedWord(QStringView word) +{ + if (word.startsWith(QChar(u'_')) && word.size() >= 2 + && (word[1].isUpper() || word[1] == QChar(u'_'))) { + return true; // Indentifiers starting with underscore and uppercase are reserved in C++ + } + return std::binary_search(std::begin(cppKeywords), std::end(cppKeywords), word); +} + +template<typename InputIterator> +static decltype(auto) findIrElement(const QmlIR::Document *doc, InputIterator first, + InputIterator last, QStringView name) +{ + auto it = std::find_if(first, last, [&](const auto &candidate) { + return name == doc->stringAt(candidate.nameIndex); + }); + Q_ASSERT(it != last); // must be satisfied by the caller + return *it; +} + +// convenience wrapper +template<typename InputIterator> +static decltype(auto) findIrLocation(const QmlIR::Document *doc, InputIterator first, + InputIterator last, QStringView name) +{ + return findIrElement(doc, first, last, name).location; +} + +Q_LOGGING_CATEGORY(lcDefaultPasses, "qml.qmltc.compilerpasses", QtWarningMsg); + +void verifyTypes(const Qml2CppContext &context, QList<Qml2CppObject> &objects) +{ + const auto verifyProperty = [&](const QQmlJSMetaProperty &property, + const QmlIR::Object *irObject) { + // TODO: this whole verify function is a mess + Q_ASSERT(!property.isAlias()); + if (property.propertyName().isEmpty()) + context.recordError(irObject->location, u"Property with unknown name found"_qs); + + const auto type = property.type(); + if (type) + return; + + // search irObject's properties and try to find matching one + const QStringView name = property.propertyName(); + auto loc = findIrLocation(context.document, irObject->propertiesBegin(), + irObject->propertiesEnd(), name); + context.recordError(loc, u"Property '" + name + u"' of unknown type"); + if (property.isList() && !isPointer(property)) { + context.recordError(loc, + u"Property '" + name + + u"' is a list type that contains non-pointer types"); + } + }; + + // Note: this is an intentionally incomplete check + const auto verifyAlias = [&](const QQmlJSMetaProperty &alias, const QmlIR::Object *irObject, + const QQmlJSScope::ConstPtr &objectType) { + Q_ASSERT(alias.isAlias()); + if (alias.propertyName().isEmpty()) { + context.recordError(irObject->location, u"Property alias with unknown name found"_qs); + return; + } + + auto loc = findIrLocation(context.document, irObject->aliasesBegin(), + irObject->aliasesEnd(), alias.propertyName()); + QStringList aliasExprBits = alias.aliasExpression().split(u'.'); + if (aliasExprBits.isEmpty()) { + context.recordError(loc, u"Alias expression is invalid"_qs); + return; + } + + QQmlJSScope::ConstPtr type = context.typeResolver->scopeForId(aliasExprBits[0], objectType); + if (!type) { + context.recordError(loc, u"Alias references an invalid id '" + aliasExprBits[0] + u"'"); + return; + } + aliasExprBits.removeFirst(); + for (const QString &bit : qAsConst(aliasExprBits)) { + if (bit.isEmpty()) { + context.recordError(loc, u"Alias expression contains empty piece"_qs); + break; + } + // TODO: we might need some better location, but this is not + // absolutely essential + QQmlJSMetaProperty p = type->property(bit); + if (!p.isValid()) { + Q_ASSERT(!type->hasProperty(bit)); // we're in deep trouble otherwise + context.recordError(loc, + u"Property '" + bit + u"' in '" + alias.aliasExpression() + + u"' does not exist"); + break; + } + if (!p.type()) { + context.recordError(loc, + u"Property '" + bit + u"' in '" + alias.aliasExpression() + + u"' of unknown type"); + break; + } + type = p.type(); + // NB: the rest is checked at a later point when we set up + // to-be-compiled types properly + } + }; + + const auto verifyBinding = [&](const QmlIR::Binding &binding, + const QQmlJSScope::ConstPtr &type) { + QString propName = context.document->stringAt(binding.propertyNameIndex); + if (propName.isEmpty()) { + Q_ASSERT(type); + if (propName.isEmpty()) { + // if empty, try default property + for (QQmlJSScope::ConstPtr t = type->baseType(); t && propName.isEmpty(); + t = t->baseType()) { + propName = t->defaultPropertyName(); + } + } + // otherwise, it's an error + if (propName.isEmpty()) { + context.recordError(binding.location, + u"Cannot assign to non-existent default property"_qs); + return; + } + } + + // ignore signal properties + if (QmlIR::IRBuilder::isSignalPropertyName(propName)) + return; + + // attached property is special + if (binding.type == QmlIR::Binding::Type_AttachedProperty) { + const auto [attachedObject, attachedType] = objects.at(binding.value.objectIndex); + if (!attachedObject || !attachedType) { + context.recordError(binding.location, + u"Binding on attached object '" + propName + + u"' of unknown type"); + } + // Note: since attached object is part of objects, it will be + // verified at a later point anyway, so nothing has to be done here + return; + } + + QQmlJSMetaProperty p = type->property(propName); + if (!p.isValid()) { + context.recordError(binding.location, + u"Binding on unknown property '" + propName + u"'"); + return; // nothing to do here anyway + } + if (!p.type()) { // Note: aliases also have valid type + context.recordError(binding.location, + u"Binding on property '" + propName + u"' of unknown type"); + } + + // TODO: why isList() needed here? + if (!p.isWritable() && !p.isList() + && !(binding.flags & QmlIR::Binding::InitializerForReadOnlyDeclaration) + && binding.type != QmlIR::Binding::Type_GroupProperty) { + context.recordError(binding.location, + u"Binding on read-only property '" + propName + u"'"); + } + }; + + for (const auto &object : objects) { + const auto [irObject, type] = object; + Q_ASSERT(irObject && type); // assume verified + if (!type->baseType()) { // base class exists + context.recordError(type->sourceLocation(), u"QML type has undefined base type"_qs); + // not critical for type verification, so do not break the iteration + } + + if (type->isInCustomParserParent()) { // has QML_CUSTOMPARSER + // TODO: we might end up supporting it later, but this needs certain + // modifications to QML_CUSTOMPARSER macro at the very least to + // support parser flags - see e.g. + // QQmlCustomParser::AcceptsAttachedProperties + +#if 0 + context.recordError(type->sourceLocation(), + u"QML type of this kind is not supported (QML_CUSTOMPARSER)"_qs); + continue; +#else + // do silent error only because QQmlListModel is affected as well + qCDebug(lcDefaultPasses) << "QML type" << type->internalName() + << "of this kind is not supported (QML_CUSTOMPARSER)"; +#endif + } + + // verify own properties + const auto properties = object.type->ownProperties(); + for (auto it = properties.cbegin(); it != properties.cend(); ++it) { + const auto &prop = *it; + if (prop.isAlias()) + verifyAlias(prop, irObject, object.type); + else + verifyProperty(prop, irObject); + } + + // verify bindings + for (auto it = irObject->bindingsBegin(); it != irObject->bindingsEnd(); ++it) + verifyBinding(*it, type); + } +} + +// verify that object's strings are not reserved C++ words: +// * enumeration name (because enum name stays "as is") and keys +// * own property names +// * own method names and method parameter names +// * TODO: bindings are ignored +// +// additionally, verify that no redefinition happens (e.g. no two properties +// named the same way, etc.) +static void checkObjectStringsForCollisions(const Qml2CppContext &context, + const Qml2CppObject &object) +{ + const auto isValidName = [&](QStringView name, auto getLocation, const QString &errorPrefix, + QSet<QStringView> &seenSymbols) { + // check name (e.g. property name) for correctness + const bool isReserved = isReservedWord(name); + const bool isSeenBefore = seenSymbols.contains(name); + // getLocation() might be slow, so only use it once if there is an error + decltype(getLocation()) location {}; // stub value + if (isReserved || isSeenBefore) + location = getLocation(); + + if (isReserved) { + context.recordError(location, + errorPrefix + u" '" + name + + u"' is a reserved C++ word, consider renaming"); + } + if (isSeenBefore) { + context.recordError(location, errorPrefix + u" with this name already exists"); + } else { + seenSymbols.insert(name); + } + }; + + const auto irObject = object.irObject; + const auto &type = object.type; + + QSet<QStringView> uniqueSymbols; + + const auto enums = type->ownEnumerations(); + for (auto it = enums.cbegin(); it != enums.cend(); ++it) { + const QQmlJSMetaEnum e = it.value(); + QStringView name = e.name(); + QmlIR::Enum irEnum {}; // reuse for enumeration keys + const auto getEnumLocation = [&]() { + irEnum = findIrElement(context.document, irObject->enumsBegin(), irObject->enumsEnd(), + name); + return irEnum.location; + }; + isValidName(name, getEnumLocation, u"Enumeration"_qs, uniqueSymbols); + + const auto enumKeys = e.keys(); + for (const auto &key : enumKeys) { + const auto getEnumKeyLocation = [&]() { + return findIrLocation(context.document, irEnum.enumValuesBegin(), + irEnum.enumValuesEnd(), key); + }; + // no support for enum classes: each key is visible outside of enum + isValidName(key, getEnumKeyLocation, u"Enumeration key"_qs, uniqueSymbols); + } + } + + const auto properties = type->ownProperties(); + for (auto it = properties.cbegin(); it != properties.cend(); ++it) { + const QQmlJSMetaProperty &p = it.value(); + QStringView name = p.propertyName(); + const auto getPropertyLocation = [&]() { return p.type()->sourceLocation(); }; + isValidName(name, getPropertyLocation, u"Property"_qs, uniqueSymbols); + } + + const auto methods = type->ownMethods(); + for (auto it = methods.cbegin(); it != methods.cend(); ++it) { + const QQmlJSMetaMethod &m = it.value(); + QStringView name = m.methodName(); + const auto getMethodLocation = [&]() { return m.returnType()->sourceLocation(); }; + isValidName(name, getMethodLocation, u"Method"_qs, uniqueSymbols); + + const auto parameterNames = m.parameterNames(); + QSet<QStringView> uniqueParameters; // parameters can shadow + for (qsizetype i = 0; i < parameterNames.size(); ++i) { + QStringView paramName = parameterNames.at(i); + const auto getParamLocation = [&]() { + static auto paramTypes = m.parameterTypes(); + return paramTypes.at(i)->sourceLocation(); + }; + isValidName(paramName, getParamLocation, u"Parameter"_qs, uniqueParameters); + } + } +} + +void checkForNamingCollisionsWithCpp(const Qml2CppContext &context, QList<Qml2CppObject> &objects) +{ + for (const auto &object : objects) + checkObjectStringsForCollisions(context, object); +} + +static void updateInternalName(QQmlJSScope::Ptr root, QString prefix, + QHash<QString, qsizetype> &typeCounts) +{ + if (!root) + return; + + // gives unique C++ class name for a basic name based on type occurrence, + // additionally updating the type occurrence + const auto uniqueCppClassName = [&](QString basicName) -> QString { + if (!typeCounts.contains(basicName)) { + typeCounts.insert(basicName, 1); + return basicName; + } + return basicName + u"_" + QString::number(typeCounts[basicName]++); + }; + + switch (root->scopeType()) { + case QQmlJSScope::AttachedPropertyScope: { + // special case + Q_ASSERT(!root->baseTypeName().isEmpty()); + root->setInternalName(root->baseTypeName()); + break; + } + // case QQmlJSScope::JSFunctionScope: + // case QQmlJSScope::JSLexicalScope: + // case QQmlJSScope::GroupedPropertyScope: + // case QQmlJSScope::QMLScope: + // case QQmlJSScope::EnumScope: + default: { + root->setInternalName(prefix); + break; + } + } + + const QList<QQmlJSScope::Ptr> children = root->childScopes(); + for (QQmlJSScope::Ptr child : children) { + updateInternalName(child, + uniqueCppClassName(root->internalName() + u"_" + child->baseTypeName()), + typeCounts); + } +} + +QHash<QString, qsizetype> makeUniqueCppNames(const Qml2CppContext &context, + QList<Qml2CppObject> &objects) +{ + // TODO: fix return type names of the methods as well + Q_UNUSED(objects); + + QHash<QString, qsizetype> typeCounts; + for (const QString &str : cppKeywords) + typeCounts.insert(str, 1); + const auto knownCppNames = context.typeResolver->gatherKnownCppClassNames(); + for (const QString &str : knownCppNames) + typeCounts.insert(str, 1); + + // root is special: + QQmlJSScope::Ptr root = context.typeResolver->root(); + QFileInfo fi(context.documentUrl); + auto cppName = fi.baseName(); + // TODO: root is special and with implicit import directory cppName might be + // in typeCounts. +#if 0 + if (typeCounts.contains(cppName)) { + context.recordError(root->sourceLocation(), + u"Root object name '" + cppName + u"' is reserved"); + return typeCounts; + } +#endif + if (cppName.isEmpty()) { + context.recordError(root->sourceLocation(), u"Root object's name is empty"_qs); + return typeCounts; + } + typeCounts.insert(cppName, 1); + + updateInternalName(root, cppName, typeCounts); + return typeCounts; +} + +static void setupQmlCppType(const Qml2CppContext &context, const QQmlJSScope::Ptr &type, + QString filePath) +{ + Q_ASSERT(type); + if (filePath.isEmpty()) { + context.recordError(type->sourceLocation(), u"QML type has unknown file path"_qs); + return; + } + if (!type->fileName().isEmpty()) // consider this one to be already set up + return; + if (!filePath.endsWith(u".qml"_qs)) { + context.recordError(type->sourceLocation(), + u"QML type has non-QML origin (internal error)"_qs); + return; + } + + // TODO: in reality, the file path could be something else instead of + // "<qml_file_name>_qmltc.h", but this is not supported right now + filePath = QFileInfo(filePath).baseName().toLower() + u".h"_qs; + type->setFileName(filePath); // this file name will be discovered during findCppIncludes + + const auto properties = type->ownProperties(); + for (auto it = properties.cbegin(); it != properties.cend(); ++it) { + QQmlJSMetaProperty p = it.value(); + Q_ASSERT(it.key() == p.propertyName()); + + if (p.isAlias()) // we'll process aliases separately + continue; + + Qml2CppPropertyData compiledData(p); + if (p.read().isEmpty()) + p.setRead(compiledData.read); + if (p.write().isEmpty() && p.isWritable()) + p.setWrite(compiledData.write); + if (p.bindable().isEmpty()) + p.setBindable(compiledData.bindable); + // TODO: p.setNotify(compiledData.notify); - ? + type->addOwnProperty(p); + } +} + +QSet<QString> setupQmlCppTypes(const Qml2CppContext &context, QList<Qml2CppObject> &objects) +{ + QSet<QString> qmlBaseTypes; + QHash<QString, std::pair<QString, QQmlJSScope::Ptr>> qmlTypes = + context.typeResolver->gatherCompiledQmlTypes(); + for (const auto &object : objects) { + // 1. set up object itself + setupQmlCppType(context, object.type, context.documentUrl); + + // 2. set up the base type if it is also QML originated + auto [url, potentialQmlType] = qmlTypes.value(object.type->baseTypeName()); + if (potentialQmlType) { // it is an actual QML type + setupQmlCppType(context, potentialQmlType, url); + qmlBaseTypes.insert(object.type->baseTypeName()); + } + } + return qmlBaseTypes; +} + +#if 0 +// TODO: somewhat of a necessary crutch (or can we just not ignore it - and move +// to qmllint) +template<typename It, typename UnaryPredicate> +static bool traverseAlias(It first, It last, QQmlJSMetaProperty p, UnaryPredicate pred) +{ + QQmlJSScope::ConstPtr type = p.type(); + for (; first != last; ++first) { + p = type->property(*first); + if (pred(p)) + return true; + type = p.type(); + } + return false; +} +#endif + +// TODO: this should really become a part of the visitor. otherwise, the +// to-be-compiled types from different documents do not get resolved aliases +// a.k.a. we have to use QObject::setProperty() instead. +static void resolveValidateOrSkipAlias(const Qml2CppContext &context, + const Qml2CppObject &aliasOwner, QQmlJSMetaProperty alias, + QSet<QQmlJSMetaProperty> &unresolved) +{ + Q_ASSERT(alias.isAlias()); + Q_ASSERT(!alias.propertyName().isEmpty()); + // TODO: we might need some better location, but this is not + // absolutely essential + auto loc = findIrLocation(context.document, aliasOwner.irObject->aliasesBegin(), + aliasOwner.irObject->aliasesEnd(), alias.propertyName()); + QStringList aliasExprBits = alias.aliasExpression().split(u'.'); + Q_ASSERT(aliasExprBits.size() > 1); + + bool canHaveWrite = true; + + QQmlJSMetaProperty p; + QQmlJSScope::ConstPtr type = + context.typeResolver->scopeForId(aliasExprBits[0], aliasOwner.type); + Q_ASSERT(type); + aliasExprBits.removeFirst(); + for (qsizetype i = 0; i < aliasExprBits.size(); ++i) { + const QString &bit = qAsConst(aliasExprBits)[i]; + p = type->property(bit); + Q_ASSERT(p.isValid() && p.type()); + + const bool isYetToBeResolvedAlias = unresolved.contains(p); + if (isYetToBeResolvedAlias) { + // we can do nothing with `alias` at the moment, so just skip it for + // now. the caller must then re-call this function, after `p` is + // processed. we assume no cycles exist at this stage, so the + // caller's code must terminate at some point. + return; + } + + struct Remover + { + QSet<QQmlJSMetaProperty> &unresolved; + const QQmlJSMetaProperty &alias; + bool needUpdate = true; + ~Remover() + { + if (needUpdate) + unresolved.remove(alias); + } + } scopedRemover { unresolved, alias }; + + // validate and resolve the alias: + if (p.read().isEmpty()) { // we won't be able to read this property -- always an error + context.recordError(loc, + u"Property '" + bit + u"' of '" + type->internalName() + + u"' does not have READ method"); + return; + } + // NB: last iteration is special, don't check for WRITE as it might + // not exist even for value property (in this case alias would only + // have a READ method, which is fine) + if (i == aliasExprBits.size() - 1) + continue; // NB: this will trigger scopedRemover, which is good + + if (!p.isWritable()) { + canHaveWrite = false; + // check whether value type property has no WRITE method while it's + // subproperties have WRITE - if so, this is likely at least a + // warning/info + +#if 0 // TODO: something here is broken in release build + // NB: expensive in general case, pretty cheap otherwise, but still + // only run this in debug + const auto hasWrite = [](const QQmlJSMetaProperty &p) { return p.isWritable(); }; + if (p.type()->accessSemantics() == QQmlJSScope::AccessSemantics::Value + && traverseAlias(aliasExprBits.cbegin() + i, aliasExprBits.cend(), p, hasWrite)) { + qCDebug(lcDefaultPasses).noquote().nospace() + << context.documentUrl << ":" << loc.line << ":" << loc.column << ":" + << "Value type property '" << bit << u"' of '" << type->internalName() + << u"' does not have WRITE method while it probably should"; + } +#endif + } + type = p.type(); + scopedRemover.needUpdate = false; + } + + Q_ASSERT(!unresolved.contains(alias)); + + // here, `p` is the actually aliased property, `type` is the owner of this + // property + + Qml2CppPropertyData compiledData(alias); + if (alias.read().isEmpty()) + alias.setRead(compiledData.read); + // TODO: how to handle redefinition/overload? + if (QString setName = p.write(); !setName.isEmpty()) { + QList<QQmlJSMetaMethod> methods = type->methods(setName); + if (methods.size() > 1) { + context.recordError(loc, + u"WRITE method of aliased property '" + p.propertyName() + u"' of '" + + type->internalName() + u"' is ambiguous"); + } + if (canHaveWrite && alias.write().isEmpty()) + alias.setWrite(compiledData.write); + } + if (!p.bindable().isEmpty() && alias.bindable().isEmpty()) + alias.setBindable(compiledData.bindable); + if (QString notifyName = p.notify(); !notifyName.isEmpty()) { + QList<QQmlJSMetaMethod> methods = type->methods(notifyName); + if (methods.size() > 1) { + context.recordError(loc, + u"NOTIFY method of aliased property '" + p.propertyName() + + u"' of '" + type->internalName() + u"' is ambiguous"); + } + if (alias.notify().isEmpty()) + alias.setNotify(compiledData.notify); + } + aliasOwner.type->addOwnProperty(alias); +} + +QSet<QQmlJSMetaProperty> deferredResolveValidateAliases(const Qml2CppContext &context, + QList<Qml2CppObject> &objects) +{ + QSet<QQmlJSMetaProperty> aliasesToId; + + QSet<QQmlJSMetaProperty> unresolved; + for (const auto &object : objects) { + const auto [irObject, type] = object; + Q_ASSERT(irObject && type); // assume verified + + const auto properties = object.type->ownProperties(); + for (QQmlJSMetaProperty p : properties) { + if (!p.isAlias()) + continue; + QStringList aliasExprBits = p.aliasExpression().split(u'.'); + Q_ASSERT(!aliasExprBits.isEmpty()); + if (aliasExprBits.size() == 1) { // special case + Q_ASSERT(context.typeResolver->scopeForId(aliasExprBits.at(0), object.type)); + // since it points to an object, set only the essential stuff + // and continue - it is already resolved after this + Qml2CppPropertyData compiledData(p); + if (p.read().isEmpty()) + p.setRead(compiledData.read); + // NB: id-pointing aliases are read-only + type->addOwnProperty(p); + aliasesToId.insert(p); + continue; + } + unresolved.insert(p); + } + } + + // NB: assume no cycles at this stage - see + // QQmlJSImportVisitor::resolveAliases() + while (!unresolved.isEmpty()) { + for (const auto &object : objects) { + const auto properties = object.type->ownProperties(); + for (QQmlJSMetaProperty p : properties) { + if (!unresolved.contains(p)) // only contains aliases, so p.isAlias() is redundant + continue; + resolveValidateOrSkipAlias(context, object, p, unresolved); + } + } + } + + return aliasesToId; +} + +static void addFirstCppIncludeFromType(QSet<QString> &cppIncludes, + const QQmlJSScope::ConstPtr &type) +{ + auto t = QQmlJSScope::nonCompositeBaseType(type); + if (!t) + return; + if (QString includeFile = t->fileName(); !includeFile.isEmpty()) + cppIncludes.insert(includeFile); +} + +static void populateCppIncludes(QSet<QString> &cppIncludes, const QQmlJSScope::ConstPtr &type) +{ + const auto constructPrivateInclude = [](QStringView publicInclude) -> QString { + if (publicInclude.isEmpty()) + return QString(); + Q_ASSERT(publicInclude.endsWith(u".h"_qs) || publicInclude.endsWith(u".hpp"_qs)); + const qsizetype dotLocation = publicInclude.endsWith(u".h"_qs) ? publicInclude.size() - 2 + : publicInclude.size() - 4; + QStringView extension = publicInclude.sliced(dotLocation); + QStringView includeWithoutExtension = publicInclude.first(dotLocation); + // check if the "public" include is actually private + if (includeWithoutExtension.startsWith(u"private")) + return includeWithoutExtension.toString() + u"_p" + extension.toString(); + return u"private/" + includeWithoutExtension.toString() + u"_p" + extension.toString(); + }; + + // TODO: this pass is VERY slow - we have to do exhaustive search, however, + // because some classes could do forward declarations + + // look in type itself + // addFirstCppIncludeFromType(cppIncludes, type); + + // look in type hierarchy + for (auto t = type; t; t = t->baseType()) { + // NB: Composite types might have include files - this is custom qmltc + // logic for local imports + if (QString includeFile = t->fileName(); !includeFile.isEmpty()) + cppIncludes.insert(includeFile); + + // look in property types + const auto properties = t->ownProperties(); + for (const QQmlJSMetaProperty &p : properties) { + addFirstCppIncludeFromType(cppIncludes, p.type()); + + if (p.isPrivate()) { + const QString ownersInclude = QQmlJSScope::nonCompositeBaseType(t)->fileName(); + QString privateInclude = constructPrivateInclude(ownersInclude); + if (!privateInclude.isEmpty()) + cppIncludes.insert(std::move(privateInclude)); + } + } + + // look in method types + const auto methods = t->ownMethods(); + for (const QQmlJSMetaMethod &m : methods) { + addFirstCppIncludeFromType(cppIncludes, m.returnType()); + + const auto parameters = m.parameterTypes(); + for (const auto &p : parameters) + addFirstCppIncludeFromType(cppIncludes, p); + } + } +} + +QSet<QString> findCppIncludes(const Qml2CppContext &context, QList<Qml2CppObject> &objects) +{ + Q_UNUSED(objects); + QSet<QString> cppIncludes; + + QQueue<QQmlJSScope::ConstPtr> objectsQueue; + objectsQueue.enqueue(context.typeResolver->root()); + + while (!objectsQueue.isEmpty()) { + QQmlJSScope::ConstPtr current = objectsQueue.dequeue(); + Q_ASSERT(current); // assume verified + + populateCppIncludes(cppIncludes, current); + + const auto children = current->childScopes(); + for (auto child : children) + objectsQueue.enqueue(child); + } + + return cppIncludes; +} + +template<typename Update> +static void updateImplicitComponents(const Qml2CppContext &context, Qml2CppObject &object, + QList<Qml2CppObject> &objects, Update update) +{ + const auto checkAndUpdate = [&](const QmlIR::Binding &binding) { + if (binding.type != QmlIR::Binding::Type_Object) + return; + if (object.irObject->flags & QV4::CompiledData::Object::IsComponent) // already set + return; + + QString propName = context.document->stringAt(binding.propertyNameIndex); + if (propName.isEmpty()) { + Q_ASSERT(object.type); + // if empty, try default property + for (QQmlJSScope::ConstPtr t = object.type->baseType(); t && propName.isEmpty(); + t = t->baseType()) + propName = t->defaultPropertyName(); + } + Q_ASSERT(!propName.isEmpty()); // assume verified + QQmlJSMetaProperty p = object.type->property(propName); + Q_ASSERT(p.isValid()); // assume verified + Q_ASSERT(p.type()); // assume verified + + // NB: in case property is a QQmlComponent, we need to handle it + // specially + if (p.type()->internalName() == u"QQmlComponent"_qs) { + // if it's an implicit component, call update function + Q_ASSERT(binding.value.objectIndex < objects.size()); + update(objects[binding.value.objectIndex], binding.value.objectIndex); + } + }; + + std::for_each(object.irObject->bindingsBegin(), object.irObject->bindingsEnd(), checkAndUpdate); +} + +QHash<int, int> findAndResolveImplicitComponents(const Qml2CppContext &context, + QList<Qml2CppObject> &objects) +{ + // TODO: this pass is incomplete. Other cases include component definitions: + // `Component { Item {} }` and maybe something else + int syntheticComponentCount = 0; + QHash<int, int> indexMapping; + const auto setQQmlComponentFlag = [&](Qml2CppObject &object, int objectIndex) { + object.irObject->flags |= QV4::CompiledData::Object::IsComponent; + Q_ASSERT(!indexMapping.contains(objectIndex)); + // TODO: the mapping construction is very ad-hoc, it could be that the + // logic is more complicated. This is to compensate the lack of + // QQmlComponentAndAliasResolver::findAndRegisterImplicitComponents() + // that the QQmlTypeCompiler does + indexMapping[objectIndex] = int(objects.size()) + syntheticComponentCount++; + }; + + for (Qml2CppObject &o : objects) + updateImplicitComponents(context, o, objects, setQQmlComponentFlag); + + return indexMapping; +} + +static void setObjectId(const Qml2CppContext &context, int objectIndex, + QHash<int, int> &idToObjectIndex) +{ + // TODO: this method is basically a copy-paste of + // QQmlComponentAndAliasResolver::collectIdsAndAliases() + + QmlIR::Object *irObject = context.document->objectAt(objectIndex); + Q_ASSERT(irObject); // assume verified + + if (irObject->idNameIndex != 0) { + if (idToObjectIndex.contains(irObject->idNameIndex)) { + context.recordError(irObject->location, u"Object id is not unique"_qs); + return; + } + irObject->id = int(idToObjectIndex.size()); + idToObjectIndex.insert(irObject->idNameIndex, objectIndex); + } + + // NB: rejecting IsComponent only *after* the id is potentially set. this is + // aligned with QQmlTypeCompiler logic + if (irObject->flags & QV4::CompiledData::Object::IsComponent && objectIndex != 0) + return; + + std::for_each(irObject->bindingsBegin(), irObject->bindingsEnd(), + [&](const QmlIR::Binding &binding) { + if (binding.type != QV4::CompiledData::Binding::Type_Object + && binding.type != QV4::CompiledData::Binding::Type_AttachedProperty + && binding.type != QV4::CompiledData::Binding::Type_GroupProperty) { + return; + } + setObjectId(context, binding.value.objectIndex, idToObjectIndex); + }); +} + +void setObjectIds(const Qml2CppContext &context, QList<Qml2CppObject> &objects) +{ + Q_UNUSED(objects); + QHash<int, int> idToObjectIndex; + Q_ASSERT(objects.at(0).irObject == context.document->objectAt(0)); + // NB: unlike QQmlTypeCompiler, only set id for the root, completely + // ignoring the Components + setObjectId(context, 0, idToObjectIndex); +} diff --git a/tools/qmltc/prototype/qml2cppdefaultpasses.h b/tools/qmltc/prototype/qml2cppdefaultpasses.h new file mode 100644 index 0000000000..f2635a4dbd --- /dev/null +++ b/tools/qmltc/prototype/qml2cppdefaultpasses.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QML2CPPPASSES_H +#define QML2CPPPASSES_H + +#include "prototype/qml2cppcontext.h" + +// verifies that each object, property (and etc.) has valid +// QQmlJSScope::ConstPtr associated with it +void verifyTypes(const Qml2CppContext &context, QList<Qml2CppObject> &objects); + +// checks whether some names in QQmlJSScope types: +// - are known C++ vocabulary items +// - are defined several times (e.g. property or enum with the same name appears +// twice) +void checkForNamingCollisionsWithCpp(const Qml2CppContext &context, QList<Qml2CppObject> &objects); + +// TODO: the below passes are not "default" (cannot be directly added) + +// ensures that all QQmlJSScope objects have unique internalName() and checks +// whether some name is C++ reserved keyword +QHash<QString, qsizetype> makeUniqueCppNames(const Qml2CppContext &context, + QList<Qml2CppObject> &objects); + +// sets up QML-originated base types of \a objects and \a objects themselves. +// processed types are expected to be generated to C++. returns a set of QML +// originated base types for all \a objects +QSet<QString> setupQmlCppTypes(const Qml2CppContext &context, QList<Qml2CppObject> &objects); + +// resolves and finishes the verification of property aliases (checks that a +// READ method is present and a WRITE method is present as well if the type is a +// value type, etc.). returns aliases which point to ids. must be done after +// setupQmlCppTypes() since some (own) aliased properties are not fully set up +// untile then and thus do not have the necessary information +QSet<QQmlJSMetaProperty> deferredResolveValidateAliases(const Qml2CppContext &context, + QList<Qml2CppObject> &objects); + +// finds all required C++ include files that are needed for the generated C++ +QSet<QString> findCppIncludes(const Qml2CppContext &context, QList<Qml2CppObject> &objects); + +// finds and resolves implicit QQmlComponent types. returns a mapping from +// QmlIR::Object that is being wrapped into a QQmlComponent to an index of that +// implicit wrapper, which is a synthetic QmlIR::Object +QHash<int, int> findAndResolveImplicitComponents(const Qml2CppContext &context, + QList<Qml2CppObject> &objects); + +void setObjectIds(const Qml2CppContext &context, QList<Qml2CppObject> &objects); + +#endif // QML2CPPPASSES_H diff --git a/tools/qmltc/prototype/qml2cpppropertyutils.h b/tools/qmltc/prototype/qml2cpppropertyutils.h new file mode 100644 index 0000000000..f69e39dd83 --- /dev/null +++ b/tools/qmltc/prototype/qml2cpppropertyutils.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QML2CPPPROPERTYUTILS_H +#define QML2CPPPROPERTYUTILS_H + +#include <private/qqmljsmetatypes_p.h> + +inline bool isPointer(const QQmlJSMetaProperty &p) +{ + Q_ASSERT(p.type()); + return p.type()->accessSemantics() == QQmlJSScope::AccessSemantics::Reference; +} + +inline QString getUnderlyingType(const QQmlJSMetaProperty &p) +{ + QString underlyingType = p.type()->internalName(); + if (p.isList()) { + underlyingType = u"QQmlListProperty<" + underlyingType + u">"; + } else if (isPointer(p)) { + underlyingType += u"*"_qs; + } + return underlyingType; +} + +// dead simple class that, for a given property, creates information for +// the Q_PROPERTY macro (READ/WRITE function names, etc.) +struct Qml2CppPropertyData +{ + Qml2CppPropertyData(const QQmlJSMetaProperty &p) : Qml2CppPropertyData(p.propertyName()) { } + + Qml2CppPropertyData(const QString &propertyName) + { + const QString nameWithUppercase = propertyName[0].toUpper() + propertyName.sliced(1); + + read = propertyName; + write = u"set" + nameWithUppercase; + bindable = u"bindable" + nameWithUppercase; + notify = propertyName + u"Changed"; + } + + QString read; + QString write; + QString bindable; + QString notify; +}; + +#endif // QML2CPPPROPERTYUTILS_H diff --git a/tools/qmltc/prototype/qmlcompiler.h b/tools/qmltc/prototype/qmlcompiler.h new file mode 100644 index 0000000000..c21ccf5f0c --- /dev/null +++ b/tools/qmltc/prototype/qmlcompiler.h @@ -0,0 +1,177 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QMLCOMPILER_H +#define QMLCOMPILER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> + +#include <private/qqmljscompiler_p.h> +#include <private/qqmljsmetatypes_p.h> + +struct Options +{ + QString outputCppFile; + QString outputHFile; + QString moduleUri; + QString resourcePath; + QString outNamespace; + bool debugGenerateLineDirective = false; +}; + +// TODO: rename the classes into Qmltc* pattern + +// Below are the classes that represent a compiled QML types in a string data +// form. These classes should be used to generate C++ code. + +// Represents QML->C++ compiled enumeration type +struct QQmlJSAotEnum +{ + QString cppType; // C++ type of enum + QStringList keys; // enumerator + QStringList values; // enumerator value + QString ownMocLine; // special MOC line that follows enum declaration + + QQmlJSAotEnum() = default; + QQmlJSAotEnum(const QString &t, const QStringList &ks, const QStringList &vs, const QString &l) + : cppType(t), keys(ks), values(vs), ownMocLine(l) + { + } +}; + +// Represents C++ member variable +struct QQmlJSAotVariable +{ + QString cppType; // C++ type of a variable + QString name; // variable name + QString defaultValue; // optional default value + + QQmlJSAotVariable() = default; + QQmlJSAotVariable(const QString &t, const QString &n, const QString &v) + : cppType(t), name(n), defaultValue(v) + { + } +}; + +struct QQmlJSAotProperty : QQmlJSAotVariable +{ + QString containingClass; + QString signalName; + + QQmlJSAotProperty() = default; + QQmlJSAotProperty(const QString t, const QString &n, const QString &c, const QString &s) + : QQmlJSAotVariable(t, n, QString()), containingClass(c), signalName(s) + { + } +}; + +struct QQmlJSAotMethodBase +{ + QString returnType; // C++ return type + QString name; // C++ function name + QList<QQmlJSAotVariable> parameterList; // C++ function parameter list + QStringList body; // C++ code of function body by line + QStringList declPreambles; // e.g. "static" keyword + QStringList modifiers; // e.g. cv-qualifiers, ref-qualifier, noexcept, attributes + + // TODO: these are only needed for Component.onCompleted -- any better way? + QStringList firstLines; // C++ to run at the very beginning of a function + QStringList lastLines; // C++ to run at the very end of a function + + QQmlJSMetaMethod::Access access = QQmlJSMetaMethod::Public; // access specifier +}; + +// Represents QML->C++ compiled member function +struct QQmlJSAotMethod : QQmlJSAotMethodBase +{ + QQmlJSMetaMethod::Type type = QQmlJSMetaMethod::Method; // Qt function type +}; + +// Represents C++ special member function +struct QQmlJSAotSpecialMethod : QQmlJSAotMethodBase +{ + QStringList initializerList; // C++ ctor initializer list +}; + +// Represents QML->C++ compiled class type that is used for C++ code generation +struct QQmlJSAotObject +{ + QString cppType; // C++ class name of the QML object + QStringList baseClasses; // C++ class names of base classes + // TODO: also add "creation string"? + QStringList mocCode; + + // TODO: does it really need to be QHash and not QList? + + // member types: enumerations and child types + QList<QQmlJSAotEnum> enums; + QList<QQmlJSAotObject> children; // these are pretty much always empty + // special member functions + QQmlJSAotSpecialMethod baselineCtor = {}; // does primary initialization + QQmlJSAotMethod init = {}; // begins secondary initialization + QQmlJSAotMethod endInit = {}; // ends initialization (with binding setup) + QQmlJSAotMethod completeComponent = {}; // calls componentComplete() + QQmlJSAotMethod finalizeComponent = {}; // invokes finalizer callbacks + QQmlJSAotMethod handleOnCompleted = {}; // calls Component.onCompleted + QQmlJSAotSpecialMethod externalCtor = {}; // calls baselineCtor, calls init + std::optional<QQmlJSAotSpecialMethod> dtor = {}; + // member functions: methods, signals and slots + QList<QQmlJSAotMethod> functions; + // member variables + QList<QQmlJSAotVariable> variables; + // member properties + QList<QQmlJSAotProperty> properties; + + // TODO: only needed for binding callables - should be revisited + bool ignoreInit = false; // specifies whether init and externalCtor should be ignored +}; + +struct QQmlJSProgram +{ + QList<QQmlJSAotObject> compiledObjects; + QQmlJSAotMethod urlMethod; + QString url; + QString hPath; + QString cppPath; + QString outNamespace; + QSet<QString> includes; +}; + +#endif // QMLCOMPILER_H diff --git a/tools/qmltc/prototype/typeresolver.cpp b/tools/qmltc/prototype/typeresolver.cpp new file mode 100644 index 0000000000..5bfff44928 --- /dev/null +++ b/tools/qmltc/prototype/typeresolver.cpp @@ -0,0 +1,148 @@ +/**************************************************************************** +** +** 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/typeresolver.h" +#include "prototype/visitor.h" + +#include <private/qqmljsimporter_p.h> +#include <private/qv4value_p.h> + +#include <QtCore/qqueue.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qfileinfo.h> +#include <QtCore/qdiriterator.h> + +Q_LOGGING_CATEGORY(lcTypeResolver2, "qml.compiler.typeresolver", QtInfoMsg); + +static QString prefixedName(const QString &prefix, const QString &name) +{ + Q_ASSERT(!prefix.endsWith(u'.')); + return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name); +} + +// TODO: this method is a (almost identical) copy-paste of QQmlJSImporter::importDirectory(). +static void customImportDirectory(QHash<QString, std::pair<QString, QQmlJSScope::Ptr>> &qmlTypes, + QQmlJSImporter *importer, const QString &directory, + const QString &prefix = QString()) +{ + if (directory.startsWith(u':')) { + if (QQmlJSResourceFileMapper *mapper = importer->resourceFileMapper()) { + const auto resources = mapper->filter( + QQmlJSResourceFileMapper::resourceQmlDirectoryFilter(directory.mid(1))); + for (const auto &entry : resources) { + const QString name = QFileInfo(entry.resourcePath).baseName(); + if (name.front().isUpper()) { + qmlTypes.insert( + prefixedName(prefix, name), + std::make_pair(entry.filePath, importer->importFile(entry.filePath))); + } + } + } + return; + } + + QDirIterator it { directory, QStringList() << QLatin1String("*.qml"), QDir::NoFilter }; + while (it.hasNext()) { + it.next(); + if (!it.fileName().front().isUpper()) + continue; // Non-uppercase names cannot be imported anyway. + + qmlTypes.insert(prefixedName(prefix, QFileInfo(it.filePath()).baseName()), + std::make_pair(it.filePath(), importer->importFile(it.filePath()))); + } +} + +namespace Qmltc { + +TypeResolver::TypeResolver(QQmlJSImporter *importer) + : QQmlJSTypeResolver(importer), m_importer(importer) +{ + Q_ASSERT(m_importer); +} + +void TypeResolver::init(Visitor &visitor, QQmlJS::AST::Node *program) +{ + QQmlJSTypeResolver::init(&visitor, program); + m_root = visitor.result(); + + QQueue<QQmlJSScope::Ptr> objects; + objects.enqueue(m_root); + while (!objects.isEmpty()) { + const QQmlJSScope::Ptr object = objects.dequeue(); + const QQmlJS::SourceLocation location = object->sourceLocation(); + qCDebug(lcTypeResolver2()).nospace() << "inserting " << object.data() << " at " + << location.startLine << ':' << location.startColumn; + m_objectsByLocationNonConst.insert({ location.startLine, location.startColumn }, object); + + const auto childScopes = object->childScopes(); + for (const auto &childScope : childScopes) + objects.enqueue(childScope); + } + + m_implicitImportDir = visitor.getImplicitImportDirectory(); + m_importedDirs = visitor.getImportedDirectories(); + m_importedFiles = visitor.getImportedQmlFiles(); +} + +QQmlJSScope::Ptr TypeResolver::scopeForLocation(const QV4::CompiledData::Location &location) const +{ + qCDebug(lcTypeResolver2()).nospace() + << "looking for object at " << location.line << ':' << location.column; + return m_objectsByLocationNonConst.value(location); +} + +QHash<QString, std::pair<QString, QQmlJSScope::Ptr>> TypeResolver::gatherCompiledQmlTypes() const +{ + QHash<QString, std::pair<QString, QQmlJSScope::Ptr>> qmlTypes; + // implicit directory first + customImportDirectory(qmlTypes, m_importer, m_implicitImportDir); + // followed by subdirectories and absolute directories + for (const QString &dir : m_importedDirs) { + QString dirPath = dir; + const QFileInfo dirInfo(dir); + if (dirInfo.isRelative()) { + dirPath = QDir(m_implicitImportDir).filePath(dirPath); + } + customImportDirectory(qmlTypes, m_importer, dirPath); + } + // followed by individual files + for (const QString &file : m_importedFiles) { + QString filePath = file; + const QFileInfo fileInfo(file); + if (fileInfo.isRelative()) { + filePath = QDir(m_implicitImportDir).filePath(filePath); + } + // TODO: file importing is untested + const QString baseName = QFileInfo(filePath).baseName(); + if (baseName.front().isUpper()) + qmlTypes.insert(baseName, std::make_pair(filePath, m_importer->importFile(filePath))); + } + return qmlTypes; +} + +} diff --git a/tools/qmltc/prototype/typeresolver.h b/tools/qmltc/prototype/typeresolver.h new file mode 100644 index 0000000000..6dd98bb470 --- /dev/null +++ b/tools/qmltc/prototype/typeresolver.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef TYPERESOLVER_H +#define TYPERESOLVER_H + +#include "prototype/visitor.h" + +#include <private/qqmljsscope_p.h> +#include <private/qqmljsast_p.h> +#include <private/qqmlirbuilder_p.h> +#include <private/qqmljstyperesolver_p.h> + +namespace Qmltc { +class TypeResolver : public QQmlJSTypeResolver +{ +public: + TypeResolver(QQmlJSImporter *importer); + + // helper function for code generator + QStringList gatherKnownCppClassNames() const + { + QStringList cppNames; + QHash<QString, QQmlJSScope::ConstPtr> builtins = m_importer->builtinInternalNames(); + cppNames.reserve(builtins.size() + m_imports.size()); + const auto getInternalName = [](const QQmlJSScope::ConstPtr &t) { + return t->internalName(); + }; + std::transform(builtins.cbegin(), builtins.cend(), std::back_inserter(cppNames), + getInternalName); + std::transform(m_imports.cbegin(), m_imports.cend(), std::back_inserter(cppNames), + getInternalName); + return cppNames; + } + + void init(Visitor &visitor, QQmlJS::AST::Node *program); + + // TODO: this shouldn't be exposed. instead, all the custom passes on + // QQmlJSScope types must happen inside Visitor + QQmlJSScope::Ptr root() const { return m_root; } + + QQmlJSScope::Ptr scopeForLocation(const QV4::CompiledData::Location &location) const; + + // returns a mapping from "QML name" (typically QML file name without + // extension) to {file path, QML scope/type pointer} pair. the mapping + // contains only native, non-C++ originated, QML types that would be + // compiled into C++. + QHash<QString, std::pair<QString, QQmlJSScope::Ptr>> gatherCompiledQmlTypes() const; + +private: + QQmlJSImporter *m_importer = nullptr; + + QHash<QV4::CompiledData::Location, QQmlJSScope::Ptr> m_objectsByLocationNonConst; + QString m_implicitImportDir; // implicit QML import directory + QQmlJSScope::Ptr m_root; + + QList<QString> m_importedDirs; + QList<QString> m_importedFiles; +}; +} + +#endif // TYPERESOLVER_H diff --git a/tools/qmltc/prototype/visitor.cpp b/tools/qmltc/prototype/visitor.cpp new file mode 100644 index 0000000000..47d16cf505 --- /dev/null +++ b/tools/qmltc/prototype/visitor.cpp @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** 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/visitor.h" + +#include <QtCore/qdir.h> +#include <QtCore/qfileinfo.h> + +namespace Qmltc { +Visitor::Visitor(QQmlJSImporter *importer, QQmlJSLogger *logger, + const QString &implicitImportDirectory, const QStringList &qmltypesFiles) + : QQmlJSImportVisitor(importer, logger, implicitImportDirectory, qmltypesFiles) +{ +} + +bool Visitor::visit(QQmlJS::AST::UiImport *import) +{ + if (!QQmlJSImportVisitor::visit(import)) + return false; + + auto filename = import->fileName.toString(); + if (filename.isEmpty()) + return true; + + const QFileInfo file(filename); + const QString absolute = + file.isRelative() ? QDir(m_implicitImportDirectory).filePath(filename) : filename; + + QFileInfo path(absolute); + if (path.isDir()) { + m_importedDirectories.append(filename); + } else if (path.isFile() && absolute.endsWith(u".qml"_qs)) { + m_importedFiles.append(filename); + } + return true; +} +} diff --git a/tools/qmltc/prototype/visitor.h b/tools/qmltc/prototype/visitor.h new file mode 100644 index 0000000000..4fc925d465 --- /dev/null +++ b/tools/qmltc/prototype/visitor.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef VISITOR_H +#define VISITOR_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#include <private/qqmljsimportvisitor_p.h> + +namespace Qmltc { +class Visitor : public QQmlJSImportVisitor +{ + QStringList m_importedDirectories; + QStringList m_importedFiles; + +public: + Visitor(QQmlJSImporter *importer, QQmlJSLogger *logger, const QString &implicitImportDirectory, + const QStringList &qmltypesFiles = QStringList()); + + bool visit(QQmlJS::AST::UiImport *import) override; + + QString getImplicitImportDirectory() const { return m_implicitImportDirectory; } + QStringList getImportedDirectories() const { return m_importedDirectories; } + QStringList getImportedQmlFiles() const { return m_importedFiles; } +}; +} + +#endif // VISITOR_H |