diff options
Diffstat (limited to 'tools')
66 files changed, 1996 insertions, 4444 deletions
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index a99cd33c24..0b89bea46e 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -3,79 +3,70 @@ # Generated from tools.pro. -if(QT_FEATURE_qml_devtools) - add_subdirectory(qmldom) - if(QT_FEATURE_commandlineparser) - add_subdirectory(qmllint) - add_subdirectory(qmltc) - add_subdirectory(qmltyperegistrar) - endif() - add_subdirectory(qmlimportscanner) - add_subdirectory(qmlformat) - if (TARGET Qt::LanguageServerPrivate - AND NOT WASM AND NOT IOS AND NOT ANDROID AND NOT QNX AND NOT INTEGRITY AND NOT WEBOS) +add_subdirectory(qmldom) +if(QT_FEATURE_commandlineparser) + add_subdirectory(qmllint) + add_subdirectory(qmltc) + add_subdirectory(qmltyperegistrar) + add_subdirectory(qmljsrootgen) +endif() +add_subdirectory(qmlimportscanner) +add_subdirectory(qmlformat) +if(TARGET Qt::LanguageServerPrivate AND QT_FEATURE_commandlineparser AND QT_FEATURE_filesystemwatcher) + if (NOT CMAKE_CROSSCOMPILING OR QT_FORCE_BUILD_TOOLS) add_subdirectory(qmlls) endif() endif() -if(QT_FEATURE_qml_devtools AND QT_FEATURE_xmlstreamwriter) +if(QT_FEATURE_xmlstreamwriter) # special case begin # Do not build qmlcachegen here but build it at src/ # time, so that we can use it for our own .qml files in src/imports. # add_subdirectory(qmlcachegen) # special case end endif() -if(QT_FEATURE_thread AND NOT ANDROID AND NOT WASM AND NOT IOS AND NOT rtems) - add_subdirectory(qml) -endif() -if(QT_FEATURE_qml_profiler - AND QT_FEATURE_thread - AND NOT ANDROID - AND NOT IOS - AND NOT WASM - AND NOT rtems) - add_subdirectory(qmlprofiler) -endif() -if(QT_FEATURE_qml_preview AND QT_FEATURE_thread AND NOT ANDROID AND NOT WASM AND NOT IOS AND NOT rtems) - add_subdirectory(qmlpreview) -endif() -if(QT_BUILD_SHARED_LIBS AND QT_FEATURE_thread AND TARGET Qt::Quick AND NOT ANDROID AND NOT WASM AND NOT IOS AND NOT rtems) - add_subdirectory(qmlscene) - add_subdirectory(qmltime) -endif() -if(QT_BUILD_SHARED_LIBS - AND QT_FEATURE_process - AND QT_FEATURE_regularexpression - AND QT_FEATURE_thread - AND TARGET Qt::Quick - AND NOT ANDROID - AND NOT IOS - AND NOT WASM - AND NOT rtems) - add_subdirectory(qmlplugindump) -endif() -if(TARGET Qt::Quick - AND TARGET Qt::Widgets - AND QT_FEATURE_checkbox - AND QT_FEATURE_combobox - AND QT_FEATURE_dialogbuttonbox - AND QT_FEATURE_formlayout - AND QT_FEATURE_groupbox - AND QT_FEATURE_lineedit - AND QT_FEATURE_mainwindow - AND QT_FEATURE_spinbox - AND QT_FEATURE_textedit - AND NOT ANDROID - AND NOT IOS - AND NOT WASM - AND NOT rtems) - add_subdirectory(qmleasing) -endif() -if(QT_FEATURE_thread AND TARGET Qt::QuickTest AND NOT ANDROID AND NOT WASM AND NOT rtems) - add_subdirectory(qmltestrunner) -endif() -if(QT_FEATURE_private_tests AND QT_FEATURE_thread AND NOT ANDROID AND NOT WASM AND NOT IOS AND NOT rtems) - add_subdirectory(qmljs) -endif() -if (QT_FEATURE_private_tests AND NOT CMAKE_CROSSCOMPILING) - add_subdirectory(qmljsrootgen) -endif() + +if(NOT (ANDROID OR WASM OR IOS OR VISIONOS OR rtems)) + if(QT_FEATURE_thread) + add_subdirectory(qml) + if(QT_FEATURE_qml_profiler) + add_subdirectory(qmlprofiler) + endif() + if(QT_FEATURE_qml_preview) + add_subdirectory(qmlpreview) + endif() + if(QT_BUILD_SHARED_LIBS AND TARGET Qt::Quick) + add_subdirectory(qmlscene) + add_subdirectory(qmltime) + endif() + if(QT_BUILD_SHARED_LIBS + AND QT_FEATURE_process + AND QT_FEATURE_regularexpression + AND TARGET Qt::Quick) + add_subdirectory(qmlplugindump) + endif() + if(TARGET Qt::QuickTest) + add_subdirectory(qmltestrunner) + endif() + if(QT_FEATURE_private_tests) + add_subdirectory(qmljs) + endif() + endif() # QT_FEATURE_thread + + if(TARGET Qt::Quick + AND TARGET Qt::Widgets + AND QT_FEATURE_checkbox + AND QT_FEATURE_combobox + AND QT_FEATURE_dialogbuttonbox + AND QT_FEATURE_formlayout + AND QT_FEATURE_groupbox + AND QT_FEATURE_lineedit + AND QT_FEATURE_mainwindow + AND QT_FEATURE_spinbox + AND QT_FEATURE_textedit) + add_subdirectory(qmleasing) + endif() + + if(TARGET Qt::Quick AND TARGET Qt::Svg) + add_subdirectory(svgtoqml) + endif() +endif() # NOT (ANDROID OR WASM OR IOS OR rtems) diff --git a/tools/qml/CMakeLists.txt b/tools/qml/CMakeLists.txt index 0bbde6d288..b209c730d3 100644 --- a/tools/qml/CMakeLists.txt +++ b/tools/qml/CMakeLists.txt @@ -19,35 +19,18 @@ qt_internal_add_app(qml set_target_properties(qml PROPERTIES WIN32_EXECUTABLE FALSE) -set_source_files_properties( - conf/default.qml - conf/resizeToItem.qml - conf/content/resizeItemToWindow.qml - conf/content/resizeWindowToItem.qml - PROPERTIES QT_SKIP_QUICKCOMPILER 1 -) - -# Resources: -set(qml_resource_files - "conf/content/resizeItemToWindow.qml" - "conf/content/resizeWindowToItem.qml" - "conf/default.qml" - "conf/resizeToItem.qml" - "resources/qml-64.png" -) - -qt_internal_add_resource(qml qml - PREFIX - "/qt-project.org/QmlRuntime" - FILES - ${qml_resource_files} -) - # Turn the tool into its own self-contained qml module qt6_add_qml_module(qml - RESOURCE_PREFIX "/qt-project.org" + RESOURCE_PREFIX "/qt-project.org/imports" URI QmlRuntime.Config VERSION 1.0 + QML_FILES + default.qml + resizeToItem.qml + ResizeItemToWindow.qml + ResizeWindowToItem.qml + RESOURCES + resources/qml-64.png ) qt_internal_extend_target(qml CONDITION TARGET Qt::Gui @@ -60,14 +43,6 @@ qt_internal_extend_target(qml CONDITION TARGET Qt::Widgets Qt::Widgets ) -#### Keys ignored in scope 4:.:.:qml.pro:WIN32: -# RC_ICONS = "resources/qml.ico" - -#### Keys ignored in scope 5:.:.:qml.pro:APPLE: -# ICON = "resources/qml.icns" -# OTHER_FILES = "resources/Info.plist" -# QMAKE_INFO_PLIST = "resources/Info.plist" - qt_internal_extend_target(qml CONDITION QT_FEATURE_qml_debug DEFINES QT_QML_DEBUG_NO_WARNING diff --git a/tools/qml/conf/content/resizeItemToWindow.qml b/tools/qml/ResizeItemToWindow.qml index 4a6d06591c..a4d8bfec40 100644 --- a/tools/qml/conf/content/resizeItemToWindow.qml +++ b/tools/qml/ResizeItemToWindow.qml @@ -1,23 +1,20 @@ // Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 import QtQuick.Window 2.0 import QtQuick 2.0 Window { property Item containedObject: null - property bool __resizeGuard: false onContainedObjectChanged: { if (containedObject == undefined || containedObject == null) { visible = false; return; } - __resizeGuard = true width = containedObject.width; height = containedObject.height; containedObject.parent = contentItem; visible = true; - __resizeGuard = false } - onWidthChanged: if (!__resizeGuard && containedObject) containedObject.width = width - onHeightChanged: if (!__resizeGuard && containedObject) containedObject.height = height + onWidthChanged: if (containedObject) containedObject.width = width + onHeightChanged: if (containedObject) containedObject.height = height } diff --git a/tools/qml/conf/content/resizeWindowToItem.qml b/tools/qml/ResizeWindowToItem.qml index 942e739d13..b969971bc2 100644 --- a/tools/qml/conf/content/resizeWindowToItem.qml +++ b/tools/qml/ResizeWindowToItem.qml @@ -1,5 +1,5 @@ // Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 import QtQuick.Window 2.0 import QtQuick 2.0 diff --git a/tools/qml/conf/default.qml b/tools/qml/conf/default.qml deleted file mode 100644 index c9bae6dcd0..0000000000 --- a/tools/qml/conf/default.qml +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -import QmlRuntime.Config 1.0 - -Configuration { - PartialScene { - itemType: "QQuickItem" - container: "qrc:/qt-project.org/QmlRuntime/conf/content/resizeItemToWindow.qml" - } -} diff --git a/tools/qml/conf/resizeToItem.qml b/tools/qml/conf/resizeToItem.qml deleted file mode 100644 index 7c1389a7a7..0000000000 --- a/tools/qml/conf/resizeToItem.qml +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -import QmlRuntime.Config 1.0 - -Configuration { - PartialScene { - itemType: "QQuickItem" - container: "qrc:/qt-project.org/QmlRuntime/conf/content/resizeWindowToItem.qml" - } -} diff --git a/tools/qml/default.qml b/tools/qml/default.qml new file mode 100644 index 0000000000..54a521193c --- /dev/null +++ b/tools/qml/default.qml @@ -0,0 +1,10 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +import QmlRuntime.Config + +Configuration { + PartialScene { + itemType: "QQuickItem" + container: "qrc:/qt-project.org/imports/QmlRuntime/Config/ResizeItemToWindow.qml" + } +} diff --git a/tools/qml/main.cpp b/tools/qml/main.cpp index ae5596e000..da544c5563 100644 --- a/tools/qml/main.cpp +++ b/tools/qml/main.cpp @@ -73,12 +73,18 @@ static QQmlApplicationEngine *qae = nullptr; #if defined(Q_OS_DARWIN) || defined(QT_GUI_LIB) static int exitTimerId = -1; #endif -static const QString iconResourcePath(QStringLiteral(":/qt-project.org/QmlRuntime/resources/qml-64.png")); -static const QString confResourcePath(QStringLiteral(":/qt-project.org/QmlRuntime/conf/")); +static const QString iconResourcePath(QStringLiteral(":/qt-project.org/imports/QmlRuntime/Config/resources/qml-64.png")); +static const QString confResourcePath(QStringLiteral(":/qt-project.org/imports/QmlRuntime/Config/")); static const QString customConfFileName(QStringLiteral("configuration.qml")); static bool verboseMode = false; static bool quietMode = false; static bool glShareContexts = true; +static bool disableShaderCache = true; +#if defined(QT_GUI_LIB) +static bool requestAlphaChannel = false; +static bool requestMSAA = false; +static bool requestCoreProfile = false; +#endif static void loadConf(const QString &override, bool quiet) // Terminates app on failure { @@ -152,8 +158,16 @@ static void listConfFiles() { const QDir confResourceDir(confResourcePath); printf("%s\n", qPrintable(QCoreApplication::translate("main", "Built-in configurations:"))); - for (const QFileInfo &fi : confResourceDir.entryInfoList(QDir::Files)) - printf(" %s\n", qPrintable(fi.baseName())); + for (const QFileInfo &fi : confResourceDir.entryInfoList(QDir::Files)) { + if (fi.completeSuffix() != QLatin1String("qml")) + continue; + + const QString baseName = fi.baseName(); + if (baseName.isEmpty() || baseName[0].isUpper()) + continue; + + printf(" %s\n", qPrintable(baseName)); + } printf("%s\n", qPrintable(QCoreApplication::translate("main", "Other configurations:"))); bool foundOther = false; const QStringList otherLocations = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation); @@ -324,6 +338,14 @@ static void getAppFlags(int argc, char **argv) QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); } else if (!strcmp(argv[i], "-disable-context-sharing") || !strcmp(argv[i], "--disable-context-sharing")) { glShareContexts = false; + } else if (!strcmp(argv[i], "-enable-shader-cache") || !strcmp(argv[i], "--enable-shader-cache")) { + disableShaderCache = false; + } else if (!strcmp(argv[i], "-transparent") || !strcmp(argv[i], "--transparent")) { + requestAlphaChannel = true; + } else if (!strcmp(argv[i], "-multisample") || !strcmp(argv[i], "--multisample")) { + requestMSAA = true; + } else if (!strcmp(argv[i], "-core-profile") || !strcmp(argv[i], "--core-profile")) { + requestCoreProfile = true; } } #else @@ -362,8 +384,31 @@ int main(int argc, char *argv[]) { getAppFlags(argc, argv); + // Must set the default QSurfaceFormat before creating the app object if + // AA_ShareOpenGLContexts is going to be set. +#if defined(QT_GUI_LIB) + QSurfaceFormat surfaceFormat; + surfaceFormat.setDepthBufferSize(24); + surfaceFormat.setStencilBufferSize(8); + if (requestMSAA) + surfaceFormat.setSamples(4); + if (requestAlphaChannel) + surfaceFormat.setAlphaBufferSize(8); + if (qEnvironmentVariableIsSet("QSG_CORE_PROFILE") + || qEnvironmentVariableIsSet("QML_CORE_PROFILE") + || requestCoreProfile) + { + // intentionally requesting 4.1 core to play nice with macOS + surfaceFormat.setVersion(4, 1); + surfaceFormat.setProfile(QSurfaceFormat::CoreProfile); + } + QSurfaceFormat::setDefaultFormat(surfaceFormat); +#endif + if (glShareContexts) QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + if (disableShaderCache) + QCoreApplication::setAttribute(Qt::AA_DisableShaderDiskCache); std::unique_ptr<QCoreApplication> app; switch (applicationType) { @@ -390,7 +435,6 @@ int main(int argc, char *argv[]) app->setOrganizationDomain("qt-project.org"); QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); - QQmlApplicationEngine e; QStringList files; QString confFile; QString translationFile; @@ -399,8 +443,8 @@ int main(int argc, char *argv[]) QCommandLineParser parser; parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); - const QCommandLineOption helpOption = parser.addHelpOption(); - const QCommandLineOption versionOption = parser.addVersionOption(); + parser.addHelpOption(); + parser.addVersionOption(); #ifdef QT_GUI_LIB QCommandLineOption apptypeOption(QStringList() << QStringLiteral("a") << QStringLiteral("apptype"), QCoreApplication::translate("main", "Select which application class to use. Default is gui."), @@ -444,11 +488,22 @@ int main(int argc, char *argv[]) parser.addOption(glSoftwareOption); // Just for the help text... we've already handled this argument above QCommandLineOption glCoreProfile(QStringLiteral("core-profile"), QCoreApplication::translate("main", "Force use of OpenGL Core Profile.")); - parser.addOption(glCoreProfile); + parser.addOption(glCoreProfile); // Just for the help text... we've already handled this argument above QCommandLineOption glContextSharing(QStringLiteral("disable-context-sharing"), QCoreApplication::translate("main", "Disable the use of a shared GL context for QtQuick Windows")); parser.addOption(glContextSharing); // Just for the help text... we've already handled this argument above + // Options relevant for other 3D APIs as well + QCommandLineOption shaderCaching(QStringLiteral("enable-shader-cache"), + QCoreApplication::translate("main", "Enable persistent caching of generated shaders")); + parser.addOption(shaderCaching); // Just for the help text... we've already handled this argument above + QCommandLineOption transparentOption(QStringLiteral("transparent"), + QCoreApplication::translate("main", "Requests an alpha channel in order to enable semi-transparent windows.")); + parser.addOption(transparentOption); // Just for the help text... we've already handled this argument above + QCommandLineOption multisampleOption(QStringLiteral("multisample"), + QCoreApplication::translate("main", "Requests 4x multisample antialiasing.")); + parser.addOption(multisampleOption); // Just for the help text... we've already handled this argument above #endif // QT_GUI_LIB + // Debugging and verbosity options QCommandLineOption quietOption(QStringLiteral("quiet"), QCoreApplication::translate("main", "Suppress all output.")); @@ -464,7 +519,7 @@ int main(int argc, char *argv[]) parser.addOption(fixedAnimationsOption); QCommandLineOption rhiOption(QStringList() << QStringLiteral("r") << QStringLiteral("rhi"), QCoreApplication::translate("main", "Set the backend for the Qt graphics abstraction (RHI). " - "Backend is one of: default, vulkan, metal, d3d11, gl"), + "Backend is one of: default, vulkan, metal, d3d11, d3d12, opengl"), QStringLiteral("backend")); parser.addOption(rhiOption); QCommandLineOption selectorOption(QStringLiteral("S"), QCoreApplication::translate("main", @@ -477,14 +532,7 @@ int main(int argc, char *argv[]) parser.addPositionalArgument("args", QCoreApplication::translate("main", "Arguments after '--' are ignored, but passed through to the application.arguments variable in QML."), "[-- args...]"); - if (!parser.parse(QCoreApplication::arguments())) { - qWarning() << parser.errorText(); - exit(1); - } - if (parser.isSet(versionOption)) - parser.showVersion(); - if (parser.isSet(helpOption)) - parser.showHelp(); + parser.process(*app); if (parser.isSet(verboseOption)) verboseMode = true; if (parser.isSet(quietOption)) { @@ -507,6 +555,9 @@ int main(int argc, char *argv[]) if (parser.isSet(fixedAnimationsOption)) QUnifiedTimer::instance()->setConsistentTiming(true); #endif + + QQmlApplicationEngine e; + for (const QString &importPath : parser.values(importOption)) e.addImportPath(importPath); @@ -517,17 +568,6 @@ int main(int argc, char *argv[]) if (!customSelectors.isEmpty()) e.setExtraFileSelectors(customSelectors); -#if defined(QT_GUI_LIB) - if (qEnvironmentVariableIsSet("QSG_CORE_PROFILE") || qEnvironmentVariableIsSet("QML_CORE_PROFILE") || parser.isSet(glCoreProfile)) { - QSurfaceFormat surfaceFormat; - surfaceFormat.setStencilBufferSize(8); - surfaceFormat.setDepthBufferSize(24); - surfaceFormat.setVersion(4, 1); - surfaceFormat.setProfile(QSurfaceFormat::CoreProfile); - QSurfaceFormat::setDefaultFormat(surfaceFormat); - } -#endif - files << parser.values(qmlFileOption); if (parser.isSet(configOption)) confFile = parser.value(configOption); @@ -550,9 +590,8 @@ int main(int argc, char *argv[]) #if QT_CONFIG(translation) // Need to be installed before QQmlApplicationEngine's automatic translation loading // (qt_ translations are loaded there) + QTranslator translator; if (!translationFile.isEmpty()) { - QTranslator translator; - if (translator.load(translationFile)) { app->installTranslator(&translator); if (verboseMode) diff --git a/tools/qml/resizeToItem.qml b/tools/qml/resizeToItem.qml new file mode 100644 index 0000000000..5bddd8ebaa --- /dev/null +++ b/tools/qml/resizeToItem.qml @@ -0,0 +1,10 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +import QmlRuntime.Config + +Configuration { + PartialScene { + itemType: "QQuickItem" + container: "qrc:/qt-project.org/imports/QmlRuntime/Config/ResizeWindowToItem.qml" + } +} diff --git a/tools/qmlaotstats/CMakeLists.txt b/tools/qmlaotstats/CMakeLists.txt new file mode 100644 index 0000000000..1511f19e4b --- /dev/null +++ b/tools/qmlaotstats/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_get_tool_target_name(target_name qmlaotstats) +qt_internal_add_tool(${target_name} + TARGET_DESCRIPTION "QML ahead-of-time compiler statistics aggregator" + TOOLS_TARGET Qml # special case + INSTALL_DIR "${INSTALL_LIBEXECDIR}" + SOURCES + main.cpp + LIBRARIES + Qt::CorePrivate + Qt::QmlPrivate + Qt::QmlCompilerPrivate + Qt::QmlToolingSettingsPrivate +) +qt_internal_return_unless_building_tools() diff --git a/tools/qmlaotstats/main.cpp b/tools/qmlaotstats/main.cpp new file mode 100644 index 0000000000..24b34efec3 --- /dev/null +++ b/tools/qmlaotstats/main.cpp @@ -0,0 +1,83 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QCommandLineParser> +#include <QCoreApplication> +#include <QDir> +#include <QFile> +#include <QFileInfo> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> + +#include <private/qqmljscompilerstats_p.h> +#include <private/qqmljscompilerstatsreporter_p.h> + +using namespace Qt::Literals::StringLiterals; + +bool saveFormattedStats(const QString &stats, const QString &outputPath) +{ + QString directory = QFileInfo(outputPath).dir().path(); + if (!QDir().mkpath(directory)) { + qDebug() << "Could not ensure the existence of" << directory; + return false; + } + + QFile outputFile(outputPath); + if (!outputFile.open(QIODevice::Text | QIODevice::WriteOnly)) { + qDebug() << "Could not open file" << outputPath; + return false; + } + + if (outputFile.write(stats.toLatin1()) == -1) { + qDebug() << "Could not write formatted AOT stats to" << outputPath; + return false; + } else { + qDebug() << "Formatted AOT stats saved to" << outputPath; + } + + return true; +} + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); + + QCommandLineParser parser; + parser.addHelpOption(); + parser.setApplicationDescription("Internal development tool."); + parser.addPositionalArgument("mode", "Choose whether to aggregate or display aotstats files", + "[aggregate|format]"); + parser.addPositionalArgument("input", "Aggregate mode: the aotstatslist file to aggregate. " + "Format mode: the aotstats file to display."); + parser.addPositionalArgument("output", "Aggregate mode: the path where to store the " + "aggregated aotstats. Format mode: the the path where " + "the formatted output will be saved."); + parser.process(app); + + const auto &positionalArgs = parser.positionalArguments(); + if (positionalArgs.size() != 3) { + qDebug().noquote() << parser.helpText(); + return EXIT_FAILURE; + } + + const auto &mode = positionalArgs.first(); + if (mode == u"aggregate"_s) { + const auto aggregated = QQmlJS::AotStats::aggregateAotstatsList(positionalArgs[1]); + if (!aggregated.has_value()) + return EXIT_FAILURE; + if (!aggregated->saveToDisk(positionalArgs[2])) + return EXIT_FAILURE; + + } else if (mode == u"format"_s) { + const auto aotstats = QQmlJS::AotStats::parseAotstatsFile(positionalArgs[1]); + if (!aotstats.has_value()) + return EXIT_FAILURE; + const QQmlJS::AotStatsReporter reporter(aotstats.value()); + if (!saveFormattedStats(reporter.format(), positionalArgs[2])) + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/tools/qmlcachegen/qmlcachegen.cpp b/tools/qmlcachegen/qmlcachegen.cpp index 93c534d429..37a2298717 100644 --- a/tools/qmlcachegen/qmlcachegen.cpp +++ b/tools/qmlcachegen/qmlcachegen.cpp @@ -15,21 +15,24 @@ #include <QLoggingCategory> #include <private/qqmlirbuilder_p.h> -#include <private/qqmljsparser_p.h> +#include <private/qqmljscompiler_p.h> #include <private/qqmljslexer_p.h> -#include <private/qqmljsresourcefilemapper_p.h> #include <private/qqmljsloadergenerator_p.h> -#include <private/qqmljscompiler_p.h> +#include <private/qqmljsparser_p.h> +#include <private/qqmljsresourcefilemapper_p.h> +#include <private/qqmljsutils_p.h> #include <private/qresourcerelocater_p.h> #include <algorithm> +using namespace Qt::Literals::StringLiterals; + static bool argumentsFromCommandLineAndFile(QStringList& allArguments, const QStringList &arguments) { allArguments.reserve(arguments.size()); for (const QString &argument : arguments) { // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it - if (argument.startsWith(QLatin1Char('@'))) { + if (argument.startsWith(u'@')) { QString optionsFile = argument; optionsFile.remove(0, 1); if (optionsFile.isEmpty()) { @@ -59,51 +62,54 @@ int main(int argc, char **argv) QHashSeed::setDeterministicGlobalSeed(); QCoreApplication app(argc, argv); - QCoreApplication::setApplicationName(QStringLiteral("qmlcachegen")); + QCoreApplication::setApplicationName("qmlcachegen"_L1); QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); - QCommandLineOption bareOption(QStringLiteral("bare"), QCoreApplication::translate("main", "Do not include default import directories. This may be used to run qmlcachegen on a project using a different Qt version.")); + QCommandLineOption bareOption("bare"_L1, QCoreApplication::translate("main", "Do not include default import directories. This may be used to run qmlcachegen on a project using a different Qt version.")); parser.addOption(bareOption); - QCommandLineOption filterResourceFileOption(QStringLiteral("filter-resource-file"), QCoreApplication::translate("main", "Filter out QML/JS files from a resource file that can be cached ahead of time instead")); + QCommandLineOption filterResourceFileOption("filter-resource-file"_L1, QCoreApplication::translate("main", "Filter out QML/JS files from a resource file that can be cached ahead of time instead")); parser.addOption(filterResourceFileOption); - QCommandLineOption resourceFileMappingOption(QStringLiteral("resource-file-mapping"), QCoreApplication::translate("main", "Path from original resource file to new one"), QCoreApplication::translate("main", "old-name=new-name")); + QCommandLineOption resourceFileMappingOption("resource-file-mapping"_L1, QCoreApplication::translate("main", "Path from original resource file to new one"), QCoreApplication::translate("main", "old-name=new-name")); parser.addOption(resourceFileMappingOption); - QCommandLineOption resourceOption(QStringLiteral("resource"), QCoreApplication::translate("main", "Qt resource file that might later contain one of the compiled files"), QCoreApplication::translate("main", "resource-file-name")); + QCommandLineOption resourceOption("resource"_L1, QCoreApplication::translate("main", "Qt resource file that might later contain one of the compiled files"), QCoreApplication::translate("main", "resource-file-name")); parser.addOption(resourceOption); - QCommandLineOption resourcePathOption(QStringLiteral("resource-path"), QCoreApplication::translate("main", "Qt resource file path corresponding to the file being compiled"), QCoreApplication::translate("main", "resource-path")); + QCommandLineOption resourcePathOption("resource-path"_L1, QCoreApplication::translate("main", "Qt resource file path corresponding to the file being compiled"), QCoreApplication::translate("main", "resource-path")); parser.addOption(resourcePathOption); - QCommandLineOption resourceNameOption(QStringLiteral("resource-name"), - QCoreApplication::translate("main", "Required to generate qmlcache_loader without qrc files. This is the name of the Qt resource the input files belong to."), - QCoreApplication::translate("main", "compiled-file-list")); + QCommandLineOption resourceNameOption("resource-name"_L1, QCoreApplication::translate("main", "Required to generate qmlcache_loader without qrc files. This is the name of the Qt resource the input files belong to."), QCoreApplication::translate("main", "compiled-file-list")); parser.addOption(resourceNameOption); - QCommandLineOption directCallsOption(QStringLiteral("direct-calls"), QCoreApplication::translate("main", "This option is ignored.")); + QCommandLineOption directCallsOption("direct-calls"_L1, QCoreApplication::translate("main", "This option is ignored.")); directCallsOption.setFlags(QCommandLineOption::HiddenFromHelp); parser.addOption(directCallsOption); - QCommandLineOption importsOption( - QStringLiteral("i"), - QCoreApplication::translate("main", "Import extra qmldir"), - QCoreApplication::translate("main", "qmldir file")); + QCommandLineOption staticOption("static"_L1, QCoreApplication::translate("main", "This option is ignored.")); + staticOption.setFlags(QCommandLineOption::HiddenFromHelp); + parser.addOption(staticOption); + QCommandLineOption importsOption("i"_L1, QCoreApplication::translate("main", "Import extra qmldir"), QCoreApplication::translate("main", "qmldir file")); parser.addOption(importsOption); - QCommandLineOption importPathOption( - QStringLiteral("I"), - QCoreApplication::translate("main", "Look for QML modules in specified directory"), - QCoreApplication::translate("main", "import directory")); + QCommandLineOption importPathOption("I"_L1, QCoreApplication::translate("main", "Look for QML modules in specified directory"), QCoreApplication::translate("main", "import directory")); parser.addOption(importPathOption); - QCommandLineOption onlyBytecode( - QStringLiteral("only-bytecode"), - QCoreApplication::translate( - "main", "Generate only byte code for bindings and functions, no C++ code")); + QCommandLineOption onlyBytecode("only-bytecode"_L1, QCoreApplication::translate("main", "Generate only byte code for bindings and functions, no C++ code")); parser.addOption(onlyBytecode); + QCommandLineOption verboseOption("verbose"_L1, QCoreApplication::translate("main", "Output compile warnings")); + parser.addOption(verboseOption); + QCommandLineOption warningsAreErrorsOption("warnings-are-errors"_L1, QCoreApplication::translate("main", "Treat warnings as errors")); + parser.addOption(warningsAreErrorsOption); + + QCommandLineOption validateBasicBlocksOption("validate-basic-blocks"_L1, QCoreApplication::translate("main", "Performs checks on the basic blocks of a function compiled ahead of time to validate its structure and coherence")); + parser.addOption(validateBasicBlocksOption); + + QCommandLineOption dumpAotStatsOption("dump-aot-stats"_L1, QCoreApplication::translate("main", "Dumps statistics about ahead-of-time compilation of bindings and functions")); + parser.addOption(dumpAotStatsOption); + QCommandLineOption moduleIdOption("module-id"_L1, QCoreApplication::translate("main", "Identifies the module of the qml file being compiled for aot stats"), QCoreApplication::translate("main", "id")); + parser.addOption(moduleIdOption); - QCommandLineOption outputFileOption(QStringLiteral("o"), QCoreApplication::translate("main", "Output file name"), QCoreApplication::translate("main", "file name")); + QCommandLineOption outputFileOption("o"_L1, QCoreApplication::translate("main", "Output file name"), QCoreApplication::translate("main", "file name")); parser.addOption(outputFileOption); - parser.addPositionalArgument(QStringLiteral("[qml file]"), - QStringLiteral("QML source file to generate cache for.")); + parser.addPositionalArgument("[qml file]"_L1, "QML source file to generate cache for."_L1); parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); @@ -125,26 +131,31 @@ int main(int argc, char **argv) if (parser.isSet(outputFileOption)) outputFileName = parser.value(outputFileOption); - if (outputFileName.endsWith(QLatin1String(".cpp"))) { + if (outputFileName.endsWith(".cpp"_L1)) { target = GenerateCpp; - if (outputFileName.endsWith(QLatin1String("qmlcache_loader.cpp"))) + if (outputFileName.endsWith("qmlcache_loader.cpp"_L1)) target = GenerateLoader; } if (target == GenerateLoader && parser.isSet(resourceNameOption)) target = GenerateLoaderStandAlone; + if (parser.isSet(dumpAotStatsOption) && !parser.isSet(moduleIdOption)) { + fprintf(stderr, "--dump-aot-stats set without setting --module-id"); + return EXIT_FAILURE; + } + const QStringList sources = parser.positionalArguments(); if (sources.isEmpty()){ parser.showHelp(); } else if (sources.size() > 1 && (target != GenerateLoader && target != GenerateLoaderStandAlone)) { - fprintf(stderr, "%s\n", qPrintable(QStringLiteral("Too many input files specified: '") + sources.join(QStringLiteral("' '")) + QLatin1Char('\''))); + fprintf(stderr, "%s\n", qPrintable("Too many input files specified: '"_L1 + sources.join("' '"_L1) + u'\'')); return EXIT_FAILURE; } const QString inputFile = !sources.isEmpty() ? sources.first() : QString(); if (outputFileName.isEmpty()) - outputFileName = inputFile + QLatin1Char('c'); + outputFileName = inputFile + u'c'; if (parser.isSet(filterResourceFileOption)) return qRelocateResourceFile(inputFile, outputFileName); @@ -156,7 +167,7 @@ int main(int argc, char **argv) if (!qQmlJSGenerateLoader( mapper.resourcePaths(QQmlJSResourceFileMapper::allQmlJSFilter()), outputFileName, parser.values(resourceFileMappingOption), &error.message)) { - error.augment(QLatin1String("Error generating loader stub: ")).print(); + error.augment("Error generating loader stub: "_L1).print(); return EXIT_FAILURE; } return EXIT_SUCCESS; @@ -166,7 +177,7 @@ int main(int argc, char **argv) QQmlJSCompileError error; if (!qQmlJSGenerateLoader(sources, outputFileName, parser.values(resourceNameOption), &error.message)) { - error.augment(QLatin1String("Error generating loader stub: ")).print(); + error.augment("Error generating loader stub: "_L1).print(); return EXIT_FAILURE; } return EXIT_SUCCESS; @@ -201,13 +212,12 @@ int main(int argc, char **argv) } if (target == GenerateCpp) { - inputFileUrl = QStringLiteral("qrc://") + inputResourcePath; + inputFileUrl = "qrc://"_L1 + inputResourcePath; saveFunction = [inputResourcePath, outputFileName]( const QV4::CompiledData::SaveableUnitPointer &unit, const QQmlJSAotFunctionMap &aotFunctions, QString *errorString) { - return qSaveQmlJSUnitAsCpp(inputResourcePath, outputFileName, unit, aotFunctions, - errorString); + return qSaveQmlJSUnitAsCpp(inputResourcePath, outputFileName, unit, aotFunctions, errorString); }; } else { @@ -223,20 +233,20 @@ int main(int argc, char **argv) }; } - if (inputFile.endsWith(QLatin1String(".qml"))) { + if (inputFile.endsWith(".qml"_L1)) { QQmlJSCompileError error; if (target != GenerateCpp || inputResourcePath.isEmpty() || parser.isSet(onlyBytecode)) { if (!qCompileQmlFile(inputFile, saveFunction, nullptr, &error, /* storeSourceLocation */ false)) { - error.augment(QStringLiteral("Error compiling qml file: ")).print(); + error.augment("Error compiling qml file: "_L1).print(); return EXIT_FAILURE; } } else { QStringList importPaths; if (parser.isSet(resourceOption)) { - importPaths.append(QLatin1String(":/qt-project.org/imports")); - importPaths.append(QLatin1String(":/qt/qml")); + importPaths.append("qt-project.org/imports"_L1); + importPaths.append("qt/qml"_L1); }; if (parser.isSet(importPathOption)) @@ -254,39 +264,50 @@ int main(int argc, char **argv) logger.setCategoryIgnored(qmlCompiler, false); logger.setCategoryFatal(qmlCompiler, true); - // By default, we're completely silent, - // as the lcAotCompiler category default is QtFatalMsg - const bool loggingEnabled = lcAotCompiler().isDebugEnabled() - || lcAotCompiler().isInfoEnabled() || lcAotCompiler().isWarningEnabled() - || lcAotCompiler().isCriticalEnabled(); - if (!loggingEnabled) + if (!parser.isSet(verboseOption) && !parser.isSet(warningsAreErrorsOption)) logger.setSilent(true); QQmlJSAotCompiler cppCodeGen( - &importer, u':' + inputResourcePath, parser.values(importsOption), &logger); + &importer, u':' + inputResourcePath, + QQmlJSUtils::cleanPaths(parser.values(importsOption)), &logger); + + if (parser.isSet(dumpAotStatsOption)) { + QQmlJS::QQmlJSAotCompilerStats::setRecordAotStats(true); + QQmlJS::QQmlJSAotCompilerStats::setModuleId(parser.value(moduleIdOption)); + } + + if (parser.isSet(validateBasicBlocksOption)) + cppCodeGen.m_flags.setFlag(QQmlJSAotCompiler::ValidateBasicBlocks); if (!qCompileQmlFile(inputFile, saveFunction, &cppCodeGen, &error, /* storeSourceLocation */ true)) { - error.augment(QStringLiteral("Error compiling qml file: ")).print(); + error.augment("Error compiling qml file: "_L1).print(); return EXIT_FAILURE; } QList<QQmlJS::DiagnosticMessage> warnings = importer.takeGlobalWarnings(); if (!warnings.isEmpty()) { - logger.log(QStringLiteral("Type warnings occurred while compiling file:"), + logger.log("Type warnings occurred while compiling file:"_L1, qmlImport, QQmlJS::SourceLocation()); logger.processMessages(warnings, qmlImport); + if (parser.isSet(warningsAreErrorsOption)) + return EXIT_FAILURE; } + + if (parser.isSet(dumpAotStatsOption)) + QQmlJS::QQmlJSAotCompilerStats::instance()->saveToDisk(outputFileName + u".aotstats"_s); } - } else if (inputFile.endsWith(QLatin1String(".js")) || inputFile.endsWith(QLatin1String(".mjs"))) { + } else if (inputFile.endsWith(".js"_L1) || inputFile.endsWith(".mjs"_L1)) { QQmlJSCompileError error; if (!qCompileJSFile(inputFile, inputFileUrl, saveFunction, &error)) { - error.augment(QLatin1String("Error compiling js file: ")).print(); + error.augment("Error compiling js file: "_L1).print(); return EXIT_FAILURE; } } else { fprintf(stderr, "Ignoring %s input file as it is not QML source code - maybe remove from QML_FILES?\n", qPrintable(inputFile)); + if (parser.isSet(warningsAreErrorsOption)) + return EXIT_FAILURE; } return EXIT_SUCCESS; diff --git a/tools/qmldom/qmldomtool.cpp b/tools/qmldom/qmldomtool.cpp index 30eeda3792..99c81e0a95 100644 --- a/tools/qmldom/qmldomtool.cpp +++ b/tools/qmldom/qmldomtool.cpp @@ -208,18 +208,18 @@ int main(int argc, char *argv[]) DomItem env(envPtr); qDebug() << "will load\n"; if (dep != Dependencies::None) - env.loadBuiltins(); + envPtr->loadBuiltins(); QList<DomItem> loadedFiles(positionalArguments.size()); qsizetype iPos = 0; for (const QString &s : std::as_const(positionalArguments)) { - env.loadFile( - s, QString(), + envPtr->loadFile( + FileToLoad::fromFileSystem(envPtr, s), [&loadedFiles, iPos](Path, const DomItem &, const DomItem &newIt) { loadedFiles[iPos] = newIt; }, - LoadOption::DefaultLoad, fileType); + fileType); } - envPtr->loadPendingDependencies(env); + envPtr->loadPendingDependencies(); bool hadFailures = false; const qsizetype largestFileSizeToCheck = 32000; @@ -245,7 +245,7 @@ int main(int argc, char *argv[]) QDir d(rDir); target = d.filePath(f.fileName()); } - MutableDomItem res = qmlFile.writeOut(target, nBackups, lwOptions, &fw, checks); + auto res = qmlFile.writeOut(target, nBackups, lwOptions, &fw, checks); switch (fw.status) { case FileWriter::Status::ShouldWrite: case FileWriter::Status::SkippedDueToFailure: @@ -257,7 +257,7 @@ int main(int argc, char *argv[]) case FileWriter::Status::SkippedEqual: qDebug() << "no change"; } - hadFailures = hadFailures || !bool(res); + hadFailures = hadFailures || !res; } } else if (parser.isSet(dumpAstOption)) { if (pathsToDump.size() > 1) { @@ -285,7 +285,7 @@ int main(int argc, char *argv[]) }; qsizetype iPathToDump = 0; bool globalPaths = false; - for (auto p : pathsToDump) + for (const auto &p : pathsToDump) if (p.headKind() == Path::Kind::Root) globalPaths = true; if (globalPaths) diff --git a/tools/qmlformat/CMakeLists.txt b/tools/qmlformat/CMakeLists.txt index 7675363ce4..908901b9f5 100644 --- a/tools/qmlformat/CMakeLists.txt +++ b/tools/qmlformat/CMakeLists.txt @@ -13,11 +13,10 @@ qt_internal_add_tool(${target_name} TOOLS_TARGET Qml # special case SOURCES qmlformat.cpp - ../shared/qqmltoolingsettings.h - ../shared/qqmltoolingsettings.cpp LIBRARIES Qt::Core Qt::QmlDomPrivate + Qt::QmlToolingSettingsPrivate ) qt_internal_return_unless_building_tools() diff --git a/tools/qmlformat/qmlformat.cpp b/tools/qmlformat/qmlformat.cpp index b1e0bb75de..e26a6412c9 100644 --- a/tools/qmlformat/qmlformat.cpp +++ b/tools/qmlformat/qmlformat.cpp @@ -19,7 +19,8 @@ # include <QCommandLineParser> #endif -#include "../shared/qqmltoolingsettings.h" +#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h> + using namespace QQmlJS::Dom; @@ -45,45 +46,16 @@ struct Options QStringList errors; }; -bool parseFile(const QString &filename, const Options &options) +// TODO refactor +// Move out to the LineWriterOptions class / helper +static LineWriterOptions composeLwOptions(const Options &options, QStringView code) { - DomItem env = - DomEnvironment::create(QStringList(), - QQmlJS::Dom::DomEnvironment::Option::SingleThreaded - | QQmlJS::Dom::DomEnvironment::Option::NoDependencies); - DomItem tFile; // place where to store the loaded file - env.loadFile( - filename, QString(), - [&tFile](Path, const DomItem &, const DomItem &newIt) { - tFile = newIt; // callback called when everything is loaded that receives the loaded - // external file pair (path, oldValue, newValue) - }, - LoadOption::DefaultLoad); - env.loadPendingDependencies(); - DomItem qmlFile = tFile.fileObject(); - std::shared_ptr<QmlFile> qmlFilePtr = qmlFile.ownerAs<QmlFile>(); - if (!qmlFilePtr || !qmlFilePtr->isValid()) { - qmlFile.iterateErrors( - [](DomItem, ErrorMessage msg) { - errorToQDebug(msg); - return true; - }, - true); - qWarning().noquote() << "Failed to parse" << filename; - return false; - } - - // Turn AST back into source code - if (options.verbose) - qWarning().noquote() << "Dumping" << filename; - LineWriterOptions lwOptions; lwOptions.formatOptions.indentSize = options.indentWidth; lwOptions.formatOptions.useTabs = options.tabs; lwOptions.updateOptions = LineWriterOptions::Update::None; if (options.newline == "native") { // find out current line endings... - QStringView code = qmlFilePtr->code(); int newlineIndex = code.indexOf(QChar(u'\n')); int crIndex = code.indexOf(QChar(u'\r')); if (newlineIndex >= 0) { @@ -108,37 +80,111 @@ bool parseFile(const QString &filename, const Options &options) } else if (options.newline == "unix") { lwOptions.lineEndings = LineWriterOptions::LineEndings::Unix; } else { - qWarning().noquote() << "Unknown line ending type" << options.newline; - return false; + qWarning().noquote() << "Unknown line ending type" << options.newline << ", using default"; } if (options.normalize) lwOptions.attributesSequence = LineWriterOptions::AttributesSequence::Normalize; else lwOptions.attributesSequence = LineWriterOptions::AttributesSequence::Preserve; - WriteOutChecks checks = WriteOutCheck::Default; - if (options.force || qmlFilePtr->code().size() > 32000) - checks = WriteOutCheck::None; lwOptions.objectsSpacing = options.objectsSpacing; lwOptions.functionsSpacing = options.functionsSpacing; + return lwOptions; +} - MutableDomItem res; +static void logParsingErrors(const DomItem &fileItem, const QString &filename) +{ + fileItem.iterateErrors( + [](const DomItem &, const ErrorMessage &msg) { + errorToQDebug(msg); + return true; + }, + true); + qWarning().noquote() << "Failed to parse" << filename; +} + +// TODO +// refactor this workaround. ExternalOWningItem is not recognized as an owning type +// in ownerAs. +static std::shared_ptr<ExternalOwningItem> getFileItemOwner(const DomItem &fileItem) +{ + std::shared_ptr<ExternalOwningItem> filePtr = nullptr; + switch (fileItem.internalKind()) { + case DomType::JsFile: + filePtr = fileItem.ownerAs<JsFile>(); + break; + default: + filePtr = fileItem.ownerAs<QmlFile>(); + break; + } + return filePtr; +} + +// TODO refactor +// Introduce better encapsulation and separation of concerns and move to DOM API +// returns a DomItem corresponding to the loaded file and bool indicating the validity of the file +static std::pair<DomItem, bool> parse(const QString &filename) +{ + auto envPtr = + DomEnvironment::create(QStringList(), + QQmlJS::Dom::DomEnvironment::Option::SingleThreaded + | QQmlJS::Dom::DomEnvironment::Option::NoDependencies); + // placeholder for a node + // containing metadata (ExternalItemInfo) about the loaded file + DomItem fMetadataItem; + envPtr->loadFile(FileToLoad::fromFileSystem(envPtr, filename), + // callback called when everything is loaded that receives the + // loaded external file pair (path, oldValue, newValue) + [&fMetadataItem](Path, const DomItem &, const DomItem &extItemInfo) { + fMetadataItem = extItemInfo; + }); + auto fItem = fMetadataItem.fileObject(); + auto filePtr = getFileItemOwner(fItem); + return { fItem, filePtr && filePtr->isValid() }; +} + +static bool parseFile(const QString &filename, const Options &options) +{ + const auto [fileItem, validFile] = parse(filename); + if (!validFile) { + logParsingErrors(fileItem, filename); + return false; + } + + // Turn AST back into source code + if (options.verbose) + qWarning().noquote() << "Dumping" << filename; + + const auto &code = getFileItemOwner(fileItem)->code(); + auto lwOptions = composeLwOptions(options, code); + WriteOutChecks checks = WriteOutCheck::Default; + //Disable writeOutChecks for some usecases + if (options.force || + code.size() > 32000 || + fileItem.internalKind() == DomType::JsFile) { + checks = WriteOutCheck::None; + } + + bool res = false; if (options.inplace) { if (options.verbose) qWarning().noquote() << "Writing to file" << filename; FileWriter fw; const unsigned numberOfBackupFiles = 0; - res = qmlFile.writeOut(filename, numberOfBackupFiles, lwOptions, &fw, checks); + res = fileItem.writeOut(filename, numberOfBackupFiles, lwOptions, &fw, checks); } else { QFile out; - out.open(stdout, QIODevice::WriteOnly); - LineWriter lw([&out](QStringView s) { out.write(s.toUtf8()); }, filename, lwOptions); - OutWriter ow(lw); - res = qmlFile.writeOutForFile(ow, checks); - ow.flush(); + if (out.open(stdout, QIODevice::WriteOnly)) { + LineWriter lw([&out](QStringView s) { out.write(s.toUtf8()); }, filename, lwOptions); + OutWriter ow(lw); + res = fileItem.writeOutForFile(ow, checks); + ow.flush(); + } else { + res = false; + } } - return bool(res); + return res; } Options buildCommandLineOptions(const QCoreApplication &app) @@ -216,8 +262,7 @@ Options buildCommandLineOptions(const QCoreApplication &app) QStringList files; if (!parser.value("files").isEmpty()) { QFile file(parser.value("files")); - file.open(QIODevice::Text | QIODevice::ReadOnly); - if (file.isOpen()) { + if (file.open(QIODevice::Text | QIODevice::ReadOnly)) { QTextStream in(&file); while (!in.atEnd()) { QString file = in.readLine(); @@ -291,6 +336,10 @@ int main(int argc, char *argv[]) return settings.writeDefaults() ? 0 : -1; auto getSettings = [&](const QString &file, Options options) { + // Perform formatting inplace if --files option is set. + if (!options.files.isEmpty()) + options.inplace = true; + if (options.ignoreSettings || !settings.search(file)) return options; diff --git a/tools/qmljs/CMakeLists.txt b/tools/qmljs/CMakeLists.txt index 4a1f8488b2..40faa5d013 100644 --- a/tools/qmljs/CMakeLists.txt +++ b/tools/qmljs/CMakeLists.txt @@ -49,11 +49,6 @@ set_target_properties(qmljs PROPERTIES WIN32_EXECUTABLE FALSE) ## Scopes: ##################################################################### -qt_internal_extend_target(qmljs CONDITION WIN32 - DEFINES - NOMINMAX -) - qt_internal_extend_target(qmljs CONDITION disassembler AND ((TEST_architecture_arch STREQUAL "i386") OR (TEST_architecture_arch STREQUAL "x86_64")) DEFINES WTF_USE_UDIS86=1 diff --git a/tools/qmljs/qmljs.cpp b/tools/qmljs/qmljs.cpp index ae01e7560f..8a1c4d0ded 100644 --- a/tools/qmljs/qmljs.cpp +++ b/tools/qmljs/qmljs.cpp @@ -19,6 +19,7 @@ #include <QtCore/QFile> #include <QtCore/QFileInfo> #include <QtCore/QDateTime> +#include <QtCore/qcommandlineparser.h> #include <private/qqmljsengine_p.h> #include <private/qqmljslexer_p.h> #include <private/qqmljsparser_p.h> @@ -49,43 +50,48 @@ int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); - QStringList args = app.arguments(); - args.removeFirst(); - bool runAsQml = false; - bool runAsModule = false; - bool cache = false; + QCommandLineParser parser; + parser.addHelpOption(); + parser.setApplicationDescription("Utility to execute scripts in QML's V4 engine"); + parser.addVersionOption(); + parser.addPositionalArgument("files", "Files to execute.", "[files...]"); - if (!args.isEmpty()) { - if (args.constFirst() == QLatin1String("--jit")) { - qputenv("QV4_JIT_CALL_THRESHOLD", QByteArray("0")); - args.removeFirst(); - } - if (args.constFirst() == QLatin1String("--interpret")) { - qputenv("QV4_FORCE_INTERPRETER", QByteArray("1")); - args.removeFirst(); - } - if (args.constFirst() == QLatin1String("--qml")) { - runAsQml = true; - args.removeFirst(); - } + QCommandLineOption forceJit("jit", "Force JIT."); + parser.addOption(forceJit); - if (args.constFirst() == QLatin1String("--module")) { - runAsModule = true; - args.removeFirst(); - } + QCommandLineOption forceInterpreter("interpret", "Force interpreter."); + parser.addOption(forceInterpreter); - if (args.constFirst() == QLatin1String("--cache")) { - cache = true; - args.removeFirst(); - } + QCommandLineOption qml("qml", "Run as QML."); + parser.addOption(qml); + + QCommandLineOption module("module", "Run as Module."); + parser.addOption(module); - if (args.constFirst() == QLatin1String("--help")) { - std::cerr << "Usage: qmljs [|--jit|--interpret|--qml] file..." << std::endl; - return EXIT_SUCCESS; + QCommandLineOption cache("cache", "Use cache."); + parser.addOption(cache); + + parser.process(app); + + bool jitEnabled = false; + + if (parser.isSet(forceJit)) { + qputenv("QV4_JIT_CALL_THRESHOLD", QByteArray("0")); + jitEnabled = true; + } + if (parser.isSet(forceInterpreter)) { + qputenv("QV4_FORCE_INTERPRETER", QByteArray("1")); + if (jitEnabled) { + std::cerr << "You cannot use 'Force JIT' and 'Force Interpreter' at the same time."; + return EXIT_FAILURE; } } + const bool runAsQml = parser.isSet(qml); + const bool runAsModule = parser.isSet(module); + const bool useCache = parser.isSet(cache); + const QStringList args = parser.positionalArguments(); QV4::ExecutionEngine vm; @@ -94,12 +100,12 @@ int main(int argc, char *argv[]) QV4::GlobalExtensions::init(vm.globalObject, QJSEngine::ConsoleExtension | QJSEngine::GarbageCollectionExtension); - for (const QString &fn : std::as_const(args)) { + for (const QString &fn : args) { QV4::ScopedValue result(scope); if (runAsModule) { auto module = vm.loadModule(QUrl::fromLocalFile(QFileInfo(fn).absoluteFilePath())); if (module.compiled) { - if (module.compiled->instantiate(&vm)) + if (module.compiled->instantiate()) module.compiled->evaluate(); } else if (module.native) { // Nothing to do. Native modules have no global code. @@ -113,12 +119,12 @@ int main(int argc, char *argv[]) return EXIT_FAILURE; } QScopedPointer<QV4::Script> script; - if (cache && QFile::exists(fn + QLatin1Char('c'))) { - QQmlRefPointer<QV4::ExecutableCompilationUnit> unit - = QV4::ExecutableCompilationUnit::create(); + if (useCache && QFile::exists(fn + QLatin1Char('c'))) { + auto unit = QQml::makeRefPointer<QV4::CompiledData::CompilationUnit>(); QString error; if (unit->loadFromDisk(QUrl::fromLocalFile(fn), QFileInfo(fn).lastModified(), &error)) { - script.reset(new QV4::Script(&vm, nullptr, unit)); + script.reset(new QV4::Script( + &vm, nullptr, vm.insertCompilationUnit(std::move(unit)))); } else { std::cout << "Error loading" << qPrintable(fn) << "from disk cache:" << qPrintable(error) << std::endl; } @@ -134,12 +140,13 @@ int main(int argc, char *argv[]) } if (!scope.hasException()) { const auto unit = script->compilationUnit; - if (cache && unit && !(unit->unitData()->flags & QV4::CompiledData::Unit::StaticData)) { + if (useCache && unit && !(unit->unitData()->flags & QV4::CompiledData::Unit::StaticData)) { if (unit->unitData()->sourceTimeStamp == 0) { const_cast<QV4::CompiledData::Unit*>(unit->unitData())->sourceTimeStamp = QFileInfo(fn).lastModified().toMSecsSinceEpoch(); } QString saveError; - if (!unit->saveToDisk(QUrl::fromLocalFile(fn), &saveError)) { + if (!unit->baseCompilationUnit()->saveToDisk( + QUrl::fromLocalFile(fn), &saveError)) { std::cout << "Error saving JS cache file: " << qPrintable(saveError) << std::endl; } } diff --git a/tools/qmljsrootgen/main.cpp b/tools/qmljsrootgen/main.cpp index 2bd54692d1..acd375144e 100644 --- a/tools/qmljsrootgen/main.cpp +++ b/tools/qmljsrootgen/main.cpp @@ -10,6 +10,7 @@ #include <QtCore/qcoreapplication.h> #include <QtCore/qfile.h> +#include <QtCore/qcommandlineparser.h> #include <QtCore/qjsondocument.h> #include <QtCore/qjsonarray.h> @@ -141,8 +142,8 @@ static QString buildClass(const QJSManagedValue &value, QJsonArray *classes, protoName = name.at(0).toUpper() + name.mid(1) + QStringLiteral("Prototype"); } - auto it = seen->prototypes.find(protoName); - if (it == seen->prototypes.end()) { + auto it = seen->prototypes.constFind(protoName); + if (it == seen->prototypes.cend()) { seen->prototypes.insert(protoName, prototype.toJSValue()); buildClass(prototype, classes, seen, protoName); } else if (!it->strictlyEquals(prototype.toJSValue())) { @@ -311,8 +312,8 @@ static QString buildConstructor(const QJSManagedValue &constructor, QJsonArray * Q_UNREACHABLE(); } - auto it = seen->constructors.find(name); - if (it == seen->constructors.end()) { + auto it = seen->constructors.constFind(name); + if (it == seen->constructors.cend()) { seen->constructors.insert(name, constructor.toJSValue()); return buildClass(*constructed, classes, seen, name); } else if (!constructor.strictlyEquals(QJSManagedValue(*it, constructor.engine()))) { @@ -326,14 +327,22 @@ int main(int argc, char *argv[]) QCoreApplication app(argc, argv); QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); - QStringList args = app.arguments(); + QCommandLineParser parser; + parser.addHelpOption(); + parser.setApplicationDescription("Internal development tool."); + parser.addPositionalArgument("path", "Output json path.", "path"); - if (args.size() != 2) { - qWarning().noquote() << app.applicationName() << "[output json path]"; - return 1; + parser.process(app); + + const QStringList args = parser.positionalArguments(); + if (auto size = args.size(); size == 0) { + qWarning().noquote().nospace() << app.applicationName() << ": Output path missing."; + return EXIT_FAILURE; + } else if (size >= 2) { + qWarning().noquote().nospace() << app.applicationName() << ": Too many output paths given. Only one allowed."; } - QString fileName = args.at(1); + const QString fileName = args.at(0); QJSEngine engine; engine.installExtensions(QJSEngine::AllExtensions); diff --git a/tools/qmllint/CMakeLists.txt b/tools/qmllint/CMakeLists.txt index 2802d6d3af..ae3de5901d 100644 --- a/tools/qmllint/CMakeLists.txt +++ b/tools/qmllint/CMakeLists.txt @@ -13,11 +13,10 @@ qt_internal_add_tool(${target_name} TOOLS_TARGET Qml # special case SOURCES main.cpp - ../shared/qqmltoolingsettings.h - ../shared/qqmltoolingsettings.cpp LIBRARIES Qt::CorePrivate Qt::QmlCompilerPrivate + Qt::QmlToolingSettingsPrivate ) qt_internal_return_unless_building_tools() diff --git a/tools/qmllint/main.cpp b/tools/qmllint/main.cpp index 5c35c19928..da0f21acb5 100644 --- a/tools/qmllint/main.cpp +++ b/tools/qmllint/main.cpp @@ -1,11 +1,14 @@ // Copyright (C) 2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Sergio Martins <sergio.martins@kdab.com> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "../shared/qqmltoolingsettings.h" +#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h> +#include <QtQmlToolingSettings/private/qqmltoolingutils_p.h> -#include <QtQmlCompiler/private/qqmljsresourcefilemapper_p.h> #include <QtQmlCompiler/private/qqmljscompiler_p.h> #include <QtQmlCompiler/private/qqmljslinter_p.h> +#include <QtQmlCompiler/private/qqmljsloggingutils_p.h> +#include <QtQmlCompiler/private/qqmljsresourcefilemapper_p.h> +#include <QtQmlCompiler/private/qqmljsutils_p.h> #include <QtCore/qdebug.h> #include <QtCore/qfile.h> @@ -29,10 +32,39 @@ using namespace Qt::StringLiterals; constexpr int JSON_LOGGING_FORMAT_REVISION = 3; +bool argumentsFromCommandLineAndFile(QStringList& allArguments, const QStringList &arguments) +{ + allArguments.reserve(arguments.size()); + for (const QString &argument : arguments) { + // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it + if (argument.startsWith(u'@')) { + QString optionsFile = argument; + optionsFile.remove(0, 1); + if (optionsFile.isEmpty()) { + qWarning().nospace() << "The @ option requires an input file"; + return false; + } + QFile f(optionsFile); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning().nospace() << "Cannot open options file specified with @"; + return false; + } + while (!f.atEnd()) { + QString line = QString::fromLocal8Bit(f.readLine().trimmed()); + if (!line.isEmpty()) + allArguments << line; + } + } else { + allArguments << argument; + } + } + return true; +} + int main(int argv, char *argc[]) { QHashSeed::setDeterministicGlobalSeed(); - QList<QQmlJSLogger::Category> categories; + QList<QQmlJS::LoggerCategory> categories; QCoreApplication app(argv, argc); QCoreApplication::setApplicationName("qmllint"); @@ -92,6 +124,11 @@ All warnings can be set to three levels: const QString qmlImportPathsSetting = QLatin1String("AdditionalQmlImportPaths"); settings.addOption(qmlImportPathsSetting); + QCommandLineOption environmentOption( + QStringList() << "E", + QLatin1String("Use the QML_IMPORT_PATH environment variable to look for QML Modules")); + parser.addOption(environmentOption); + QCommandLineOption qmlImportNoDefault( QStringList() << "bare", QLatin1String("Do not include default import directories or the current directory. " @@ -150,20 +187,48 @@ All warnings can be set to three levels: QLatin1String("directory")); parser.addOption(pluginPathsOption); - auto addCategory = [&](const QQmlJSLogger::Category &category) { + QCommandLineOption maxWarnings( + QStringList() << "W" + << "max-warnings", + QLatin1String("Exit with an error code if more than \"count\" many" + "warnings are found by qmllint. By default or if \"count\" " + "is -1, warnings do not cause qmllint " + "to return with an error exit code."), + "count" + ); + parser.addOption(maxWarnings); + settings.addOption("MaxWarnings", -1); + + auto levelToString = [](const QQmlJS::LoggerCategory &category) -> QString { + Q_ASSERT(category.isIgnored() || category.level() != QtCriticalMsg); + if (category.isIgnored()) + return QStringLiteral("disable"); + + switch (category.level()) { + case QtInfoMsg: + return QStringLiteral("info"); + case QtWarningMsg: + return QStringLiteral("warning"); + default: + Q_UNREACHABLE(); + break; + } + }; + + auto addCategory = [&](const QQmlJS::LoggerCategory &category) { categories.push_back(category); - if (category.isDefault) + if (category.isDefault()) return; QCommandLineOption option( category.id().name().toString(), - category.description - + QStringLiteral(" (default: %1)").arg(category.levelToString()), - QStringLiteral("level"), category.levelToString()); - if (category.ignored) + category.description() + + QStringLiteral(" (default: %1)").arg(levelToString(category)), + QStringLiteral("level"), levelToString(category)); + if (category.isIgnored()) option.setFlags(QCommandLineOption::HiddenFromHelp); parser.addOption(option); - settings.addOption(QStringLiteral("Warnings/") + category.settingsName, - category.levelToString()); + settings.addOption(QStringLiteral("Warnings/") + category.settingsName(), + levelToString(category)); }; for (const auto &category : QQmlJSLogger::defaultCategories()) { @@ -172,11 +237,16 @@ All warnings can be set to three levels: parser.addPositionalArgument(QLatin1String("files"), QLatin1String("list of qml or js files to verify")); - if (!parser.parse(app.arguments())) { - if (parser.unknownOptionNames().isEmpty()) { - qWarning().noquote() << parser.errorText(); - return 1; - } + + QStringList arguments; + if (!argumentsFromCommandLineAndFile(arguments, app.arguments())) { + // argumentsFromCommandLine already printed any necessary warnings. + return 1; + } + + if (!parser.parse(arguments)) { + qWarning().noquote() << parser.errorText(); + return 1; } // Since we can't use QCommandLineParser::process(), we need to handle version and help manually @@ -192,21 +262,30 @@ All warnings can be set to three levels: auto updateLogLevels = [&]() { for (auto &category : categories) { - if (category.isDefault) + if (category.isDefault()) continue; const QString &key = category.id().name().toString(); - const QString &settingsName = QStringLiteral("Warnings/") + category.settingsName; + const QString &settingsName = QStringLiteral("Warnings/") + category.settingsName(); if (parser.isSet(key) || settings.isSet(settingsName)) { const QString value = parser.isSet(key) ? parser.value(key) : settings.value(settingsName).toString(); // Do not try to set the levels if it's due to a default config option. // This way we can tell which options have actually been overwritten by the user. - if (category.levelToString() == value && !parser.isSet(key)) + if (levelToString(category) == value && !parser.isSet(key)) continue; - if (!category.setLevel(value)) { + if (value == "disable"_L1) { + category.setLevel(QtCriticalMsg); + category.setIgnored(true); + } else if (value == "info"_L1) { + category.setLevel(QtInfoMsg); + category.setIgnored(false); + } else if (value == "warning"_L1) { + category.setLevel(QtWarningMsg); + category.setIgnored(false); + } else { qWarning() << "Invalid logging level" << value << "provided for" << category.id().name().toString() << "(allowed are: disable, info, warning)"; @@ -235,8 +314,8 @@ All warnings can be set to three levels: QStringList defaultQmldirFiles; if (parser.isSet(qmldirFilesOption)) { - defaultQmldirFiles = parser.values(qmldirFilesOption); - } else { + defaultQmldirFiles = QQmlJSUtils::cleanPaths(parser.values(qmldirFilesOption)); + } else if (!parser.isSet(qmlImportNoDefault)){ // If nothing given explicitly, use the qmldir file from the current directory. QFileInfo qmldirFile(QStringLiteral("qmldir")); if (qmldirFile.isFile()) { @@ -267,7 +346,7 @@ All warnings can be set to three levels: QQmlJSLinter linter(qmlImportPaths, pluginPaths, useAbsolutePath); for (const QQmlJSLinter::Plugin &plugin : linter.plugins()) { - for (const QQmlJSLogger::Category &category : plugin.categories()) + for (const QQmlJS::LoggerCategory &category : plugin.categories()) addCategory(category); } @@ -300,76 +379,109 @@ All warnings can be set to three levels: QJsonArray jsonFiles; for (const QString &filename : positionalArguments) { - if (!parser.isSet(ignoreSettings)) { + if (!parser.isSet(ignoreSettings)) settings.search(filename); - updateLogLevels(); + updateLogLevels(); - const QDir fileDir = QFileInfo(filename).absoluteDir(); - auto addAbsolutePaths = [&](QStringList &list, const QStringList &entries) { - for (const QString &file : entries) - list << (QFileInfo(file).isAbsolute() ? file : fileDir.filePath(file)); - }; + const QDir fileDir = QFileInfo(filename).absoluteDir(); + auto addAbsolutePaths = [&](QStringList &list, const QStringList &entries) { + for (const QString &file : entries) + list << (QFileInfo(file).isAbsolute() ? file : fileDir.filePath(file)); + }; - resourceFiles = defaultResourceFiles; + resourceFiles = defaultResourceFiles; - addAbsolutePaths(resourceFiles, settings.value(resourceSetting).toStringList()); + addAbsolutePaths(resourceFiles, settings.value(resourceSetting).toStringList()); - qmldirFiles = defaultQmldirFiles; - if (settings.isSet(qmldirFilesSetting) - && !settings.value(qmldirFilesSetting).toStringList().isEmpty()) { - qmldirFiles = {}; - addAbsolutePaths(qmldirFiles, - settings.value(qmldirFilesSetting).toStringList()); - } + qmldirFiles = defaultQmldirFiles; + if (settings.isSet(qmldirFilesSetting) + && !settings.value(qmldirFilesSetting).toStringList().isEmpty()) { + qmldirFiles = {}; + addAbsolutePaths(qmldirFiles, settings.value(qmldirFilesSetting).toStringList()); + } + + if (parser.isSet(qmlImportNoDefault) + || (settings.isSet(qmlImportNoDefaultSetting) + && settings.value(qmlImportNoDefaultSetting).toBool())) { + qmlImportPaths = {}; + } else { + qmlImportPaths = defaultImportPaths; + } - if (parser.isSet(qmlImportNoDefault) - || (settings.isSet(qmlImportNoDefaultSetting) - && settings.value(qmlImportNoDefaultSetting).toBool())) { - qmlImportPaths = {}; + if (parser.isSet(qmlImportPathsOption)) + qmlImportPaths << parser.values(qmlImportPathsOption); + if (parser.isSet(environmentOption)) { + if (silent) { + qmlImportPaths << qEnvironmentVariable("QML_IMPORT_PATH") + .split(QDir::separator(), Qt::SkipEmptyParts) + << qEnvironmentVariable("QML2_IMPORT_PATH") + .split(QDir::separator(), Qt::SkipEmptyParts); } else { - qmlImportPaths = defaultImportPaths; + if (const QStringList dirsFromEnv = + QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML_IMPORT_PATH"_s); + !dirsFromEnv.isEmpty()) { + qInfo().nospace().noquote() + << "Using import directories passed from environment variable " + "\"QML_IMPORT_PATH\": \"" + << dirsFromEnv.join(u"\", \""_s) << "\"."; + qmlImportPaths << dirsFromEnv; + } + if (const QStringList dirsFromEnv = + QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv( + u"QML2_IMPORT_PATH"_s); + !dirsFromEnv.isEmpty()) { + qInfo().nospace().noquote() << "Using import directories passed from the " + "deprecated environment variable " + "\"QML2_IMPORT_PATH\": \"" + << dirsFromEnv.join(u"\", \""_s) << "\"."; + qmlImportPaths << dirsFromEnv; + } } + } - if (parser.isSet(qmlImportPathsOption)) - qmlImportPaths << parser.values(qmlImportPathsOption); - - addAbsolutePaths(qmlImportPaths, settings.value(qmlImportPathsSetting).toStringList()); + addAbsolutePaths(qmlImportPaths, settings.value(qmlImportPathsSetting).toStringList()); - QSet<QString> disabledPlugins; + QSet<QString> disabledPlugins; - if (parser.isSet(pluginsDisable)) { - for (const QString &plugin : parser.values(pluginsDisable)) - disabledPlugins << plugin.toLower(); - } + if (parser.isSet(pluginsDisable)) { + for (const QString &plugin : parser.values(pluginsDisable)) + disabledPlugins << plugin.toLower(); + } - if (settings.isSet(pluginsDisableSetting)) { - for (const QString &plugin : settings.value(pluginsDisableSetting).toStringList()) - disabledPlugins << plugin.toLower(); - } + if (settings.isSet(pluginsDisableSetting)) { + for (const QString &plugin : settings.value(pluginsDisableSetting).toStringList()) + disabledPlugins << plugin.toLower(); + } - linter.setPluginsEnabled(!disabledPlugins.contains("all")); + linter.setPluginsEnabled(!disabledPlugins.contains("all")); - if (!linter.pluginsEnabled()) - continue; + if (!linter.pluginsEnabled()) + continue; - auto &plugins = linter.plugins(); + auto &plugins = linter.plugins(); - for (auto &plugin : plugins) - plugin.setEnabled(!disabledPlugins.contains(plugin.name().toLower())); - } + for (auto &plugin : plugins) + plugin.setEnabled(!disabledPlugins.contains(plugin.name().toLower())); const bool isFixing = parser.isSet(fixFile); QQmlJSLinter::LintResult lintResult; if (parser.isSet(moduleOption)) { - lintResult = linter.lintModule(filename, silent, useJson ? &jsonFiles : nullptr); + lintResult = linter.lintModule(filename, silent, useJson ? &jsonFiles : nullptr, + qmlImportPaths, resourceFiles); } else { lintResult = linter.lintFile(filename, nullptr, silent || isFixing, useJson ? &jsonFiles : nullptr, qmlImportPaths, qmldirFiles, resourceFiles, categories); } - success &= (lintResult == QQmlJSLinter::LintSuccess); + success &= (lintResult == QQmlJSLinter::LintSuccess || lintResult == QQmlJSLinter::HasWarnings); + if (success && parser.isSet(maxWarnings)) + { + int value = parser.value(maxWarnings).toInt(); + if (value != -1 && value < linter.logger()->warnings().size()) + success = false; + } if (isFixing) { if (lintResult != QQmlJSLinter::LintSuccess && lintResult != QQmlJSLinter::HasWarnings) @@ -451,8 +563,10 @@ All warnings can be set to three levels: QTextStream(stdout) << QString::fromUtf8(json); } else { QFile file(fileName); - file.open(QFile::WriteOnly); - file.write(json); + if (file.open(QFile::WriteOnly)) + file.write(json); + else + success = false; } } diff --git a/tools/qmlls/CMakeLists.txt b/tools/qmlls/CMakeLists.txt index dcc703141e..d61a18582e 100644 --- a/tools/qmlls/CMakeLists.txt +++ b/tools/qmlls/CMakeLists.txt @@ -8,28 +8,16 @@ qt_internal_add_app(qmlls TARGET_DESCRIPTION "QML Language Server" SOURCES - lspcustomtypes.h - qlanguageserver.h qlanguageserver_p.h qlanguageserver.cpp - qqmllanguageserver.h qqmllanguageserver.cpp qmllanguageservertool.cpp - workspace.cpp workspace.h - textblock.h textblock.cpp - textcursor.h textcursor.cpp - textcursor.cpp textcursor.h - textdocument.cpp textdocument.h - qmllintsuggestions.h qmllintsuggestions.cpp - textsynchronization.cpp textsynchronization.h - qmlcompletionsupport.h qmlcompletionsupport.cpp - qqmlcodemodel.h qqmlcodemodel.cpp - ../shared/qqmltoolingsettings.h - ../shared/qqmltoolingsettings.cpp DEFINES QT_USE_QSTRINGBUILDER LIBRARIES - Qt::QmlPrivate - Qt::CorePrivate - Qt::QmlDomPrivate - Qt::LanguageServerPrivate - Qt::QmlCompilerPrivate + Qt::QmlLSPrivate + Qt::QmlToolingSettingsPrivate ) set_target_properties(qmlls PROPERTIES WIN32_EXECUTABLE FALSE) + +if(NOT QT6_IS_SHARED_LIBS_BUILD) + qt_import_plugins(qmlls INCLUDE Qt::QmlLSQuickPlugin) + target_link_libraries(qmlls PRIVATE Qt::QmlLSQuickPlugin) +endif() diff --git a/tools/qmlls/lspcustomtypes.h b/tools/qmlls/lspcustomtypes.h deleted file mode 100644 index f46b36e578..0000000000 --- a/tools/qmlls/lspcustomtypes.h +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef LSPCUSTOMTYPES_H -#define LSPCUSTOMTYPES_H -#include <QtLanguageServer/private/qlanguageserverspec_p.h> - -QT_BEGIN_NAMESPACE - -namespace QLspSpecification { - -class UriToBuildDirs -{ -public: - QByteArray baseUri = {}; - QList<QByteArray> buildDirs = {}; - - template<typename W> - void walk(W &w) - { - field(w, "baseUri", baseUri); - field(w, "buildDirs", buildDirs); - } -}; - -namespace Notifications { -constexpr auto AddBuildDirsMethod = "$/addBuildDirs"; - -class AddBuildDirsParams -{ -public: - QList<UriToBuildDirs> buildDirsToSet = {}; - - template<typename W> - void walk(W &w) - { - field(w, "buildDirsToSet", buildDirsToSet); - } -}; -} // namespace Notifications -} // namespace QLspSpecification - -QT_END_NAMESPACE - -#endif // LSPCUSTOMTYPES_H diff --git a/tools/qmlls/qlanguageserver.cpp b/tools/qmlls/qlanguageserver.cpp deleted file mode 100644 index 4f9c9fe77d..0000000000 --- a/tools/qmlls/qlanguageserver.cpp +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "qlanguageserver_p.h" -#include <QtLanguageServer/private/qlspnotifysignals_p.h> -#include <QtJsonRpc/private/qjsonrpcprotocol_p_p.h> - -QT_BEGIN_NAMESPACE - -using namespace QLspSpecification; -using namespace Qt::StringLiterals; - -Q_LOGGING_CATEGORY(lspServerLog, "qt.languageserver.server") - -QLanguageServerPrivate::QLanguageServerPrivate(const QJsonRpcTransport::DataHandler &h) - : protocol(h) -{ -} - -/*! -\internal -\class QLanguageServer -\brief Implements a server for the language server protocol - -QLanguageServer is a class that uses the QLanguageServerProtocol to -provide a server implementation. -It handles the lifecycle management, and can be extended via -QLanguageServerModule subclasses. - -The language server keeps a strictly monotonically increasing runState that can be queried -from any thread (and is thus mutex gated), the normal run state is DidInitialize. - -The language server also keeps track of the task canceled by the client (or implicitly when -shutting down, and isRequestCanceled can be called from any thread. -*/ - -QLanguageServer::QLanguageServer(const QJsonRpcTransport::DataHandler &h, QObject *parent) - : QObject(*new QLanguageServerPrivate(h), parent) -{ - Q_D(QLanguageServer); - registerMethods(*d->protocol.typedRpc()); - d->notifySignals.registerHandlers(&d->protocol); -} - -QLanguageServerProtocol *QLanguageServer::protocol() -{ - Q_D(QLanguageServer); - return &d->protocol; -} - -QLanguageServer::RunStatus QLanguageServer::runStatus() const -{ - const Q_D(QLanguageServer); - QMutexLocker l(&d->mutex); - return d->runStatus; -} - -void QLanguageServer::finishSetup() -{ - Q_D(QLanguageServer); - RunStatus rStatus; - { - QMutexLocker l(&d->mutex); - rStatus = d->runStatus; - if (rStatus == RunStatus::NotSetup) - d->runStatus = RunStatus::SettingUp; - } - if (rStatus != RunStatus::NotSetup) { - emit lifecycleError(); - return; - } - emit runStatusChanged(RunStatus::SettingUp); - - registerHandlers(&d->protocol); - for (auto module : d->modules) - module->registerHandlers(this, &d->protocol); - - { - QMutexLocker l(&d->mutex); - rStatus = d->runStatus; - if (rStatus == RunStatus::SettingUp) - d->runStatus = RunStatus::DidSetup; - } - if (rStatus != RunStatus::SettingUp) { - emit lifecycleError(); - return; - } - emit runStatusChanged(RunStatus::DidSetup); -} - -void QLanguageServer::addServerModule(QLanguageServerModule *serverModule) -{ - Q_D(QLanguageServer); - Q_ASSERT(serverModule); - RunStatus rStatus; - { - QMutexLocker l(&d->mutex); - rStatus = d->runStatus; - if (rStatus == RunStatus::NotSetup) { - if (d->modules.contains(serverModule->name())) { - d->modules.insert(serverModule->name(), serverModule); - qCWarning(lspServerLog) << "Duplicate add of QLanguageServerModule named" - << serverModule->name() << ", overwriting."; - } else { - d->modules.insert(serverModule->name(), serverModule); - } - } - } - if (rStatus != RunStatus::NotSetup) { - qCWarning(lspServerLog) << "Called QLanguageServer::addServerModule after setup"; - emit lifecycleError(); - return; - } -} - -QLanguageServerModule *QLanguageServer::moduleByName(const QString &n) const -{ - const Q_D(QLanguageServer); - QMutexLocker l(&d->mutex); - return d->modules.value(n); -} - -QLspNotifySignals *QLanguageServer::notifySignals() -{ - Q_D(QLanguageServer); - return &d->notifySignals; -} - -void QLanguageServer::registerMethods(QJsonRpc::TypedRpc &typedRpc) -{ - typedRpc.installMessagePreprocessor( - [this](const QJsonDocument &doc, const QJsonParseError &err, - const QJsonRpcProtocol::Handler<QJsonRpcProtocol::Response> &responder) { - Q_D(QLanguageServer); - if (!doc.isObject()) { - qCWarning(lspServerLog) - << "non object jsonrpc message" << doc << err.errorString(); - return QJsonRpcProtocol::Processing::Stop; - } - bool sendErrorResponse = false; - RunStatus rState; - QJsonValue id = doc.object()[u"id"]; - { - QMutexLocker l(&d->mutex); - // the normal case is d->runStatus == RunStatus::DidInitialize - if (d->runStatus != RunStatus::DidInitialize) { - if (d->runStatus == RunStatus::DidSetup && !doc.isNull() - && doc.object()[u"method"].toString() - == QString::fromUtf8( - QLspSpecification::Requests::InitializeMethod)) { - return QJsonRpcProtocol::Processing::Continue; - } else if (!doc.isNull() - && doc.object()[u"method"].toString() - == QString::fromUtf8( - QLspSpecification::Notifications::ExitMethod)) { - return QJsonRpcProtocol::Processing::Continue; - } - if (id.isString() || id.isDouble()) { - sendErrorResponse = true; - rState = d->runStatus; - } else { - return QJsonRpcProtocol::Processing::Stop; - } - } - } - if (!sendErrorResponse) { - if (id.isString() || id.isDouble()) { - QMutexLocker l(&d->mutex); - d->requestsInProgress.insert(id, QRequestInProgress {}); - } - return QJsonRpcProtocol::Processing::Continue; - } - if (rState == RunStatus::NotSetup || rState == RunStatus::DidSetup) - responder(QJsonRpcProtocol::MessageHandler::error( - int(QLspSpecification::ErrorCodes::ServerNotInitialized), - u"Request on non initialized Language Server (runStatus %1): %2"_s - .arg(int(rState)) - .arg(QString::fromUtf8(doc.toJson())))); - else - responder(QJsonRpcProtocol::MessageHandler::error( - int(QLspSpecification::ErrorCodes::InvalidRequest), - u"Method called on stopping Language Server (runStatus %1)"_s.arg( - int(rState)))); - return QJsonRpcProtocol::Processing::Stop; - }); - typedRpc.installOnCloseAction([this](QJsonRpc::TypedResponse::Status, - const QJsonRpc::IdType &id, QJsonRpc::TypedRpc &) { - Q_D(QLanguageServer); - QJsonValue idValue = QTypedJson::toJsonValue(id); - bool lastReq; - { - QMutexLocker l(&d->mutex); - d->requestsInProgress.remove(idValue); - lastReq = d->runStatus == RunStatus::WaitPending && d->requestsInProgress.size() <= 1; - if (lastReq) - d->runStatus = RunStatus::Stopping; - } - if (lastReq) - executeShutdown(); - }); -} - -void QLanguageServer::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &serverInfo) -{ - Q_D(QLanguageServer); - for (auto module : std::as_const(d->modules)) - module->setupCapabilities(clientInfo, serverInfo); -} - -const QLspSpecification::InitializeParams &QLanguageServer::clientInfo() const -{ - const Q_D(QLanguageServer); - - if (int(runStatus()) < int(RunStatus::DidInitialize)) - qCWarning(lspServerLog) << "asked for Language Server clientInfo before initialization"; - return d->clientInfo; -} - -const QLspSpecification::InitializeResult &QLanguageServer::serverInfo() const -{ - const Q_D(QLanguageServer); - if (int(runStatus()) < int(RunStatus::DidInitialize)) - qCWarning(lspServerLog) << "asked for Language Server serverInfo before initialization"; - return d->serverInfo; -} - -void QLanguageServer::receiveData(const QByteArray &d) -{ - protocol()->receiveData(d); -} - -void QLanguageServer::registerHandlers(QLanguageServerProtocol *protocol) -{ - QObject::connect(notifySignals(), &QLspNotifySignals::receivedCancelNotification, this, - [this](const QLspSpecification::Notifications::CancelParamsType ¶ms) { - Q_D(QLanguageServer); - QJsonValue id = QTypedJson::toJsonValue(params.id); - QMutexLocker l(&d->mutex); - if (d->requestsInProgress.contains(id)) - d->requestsInProgress[id].canceled = true; - else - qCWarning(lspServerLog) - << "Ignoring cancellation of non in progress request" << id; - }); - - protocol->registerInitializeRequestHandler( - [this](const QByteArray &, - const QLspSpecification::Requests::InitializeParamsType ¶ms, - QLspSpecification::Responses::InitializeResponseType &&response) { - qCDebug(lspServerLog) << "init"; - Q_D(QLanguageServer); - RunStatus rStatus; - { - QMutexLocker l(&d->mutex); - rStatus = d->runStatus; - if (rStatus == RunStatus::DidSetup) - d->runStatus = RunStatus::Initializing; - } - if (rStatus != RunStatus::DidSetup) { - if (rStatus == RunStatus::NotSetup || rStatus == RunStatus::SettingUp) - response.sendErrorResponse( - int(QLspSpecification::ErrorCodes::InvalidRequest), - u"Initialization request received on non setup language server"_s - .toUtf8()); - else - response.sendErrorResponse( - int(QLspSpecification::ErrorCodes::InvalidRequest), - u"Received multiple initialization requests"_s.toUtf8()); - emit lifecycleError(); - return; - } - emit runStatusChanged(RunStatus::Initializing); - d->clientInfo = params; - setupCapabilities(d->clientInfo, d->serverInfo); - { - QMutexLocker l(&d->mutex); - d->runStatus = RunStatus::DidInitialize; - } - emit runStatusChanged(RunStatus::DidInitialize); - response.sendResponse(d->serverInfo); - }); - - QObject::connect(notifySignals(), &QLspNotifySignals::receivedInitializedNotification, this, - [this](const QLspSpecification::Notifications::InitializedParamsType &) { - Q_D(QLanguageServer); - { - QMutexLocker l(&d->mutex); - d->clientInitialized = true; - } - emit clientInitialized(this); - }); - - protocol->registerShutdownRequestHandler( - [this](const QByteArray &, const QLspSpecification::Requests::ShutdownParamsType &, - QLspSpecification::Responses::ShutdownResponseType &&response) { - Q_D(QLanguageServer); - RunStatus rStatus; - bool shouldExecuteShutdown = false; - { - QMutexLocker l(&d->mutex); - rStatus = d->runStatus; - if (rStatus == RunStatus::DidInitialize) { - d->shutdownResponse = std::move(response); - if (d->requestsInProgress.size() <= 1) { - d->runStatus = RunStatus::Stopping; - shouldExecuteShutdown = true; - } else { - d->runStatus = RunStatus::WaitPending; - } - } - } - if (rStatus != RunStatus::DidInitialize) - emit lifecycleError(); - else if (shouldExecuteShutdown) - executeShutdown(); - }); - - QObject::connect(notifySignals(), &QLspNotifySignals::receivedExitNotification, this, - [this](const QLspSpecification::Notifications::ExitParamsType &) { - if (runStatus() != RunStatus::Stopped) - emit lifecycleError(); - else - emit exit(); - }); -} - -void QLanguageServer::executeShutdown() -{ - RunStatus rStatus = runStatus(); - if (rStatus != RunStatus::Stopping) { - emit lifecycleError(); - return; - } - emit shutdown(); - QLspSpecification::Responses::ShutdownResponseType shutdownResponse; - { - Q_D(QLanguageServer); - QMutexLocker l(&d->mutex); - rStatus = d->runStatus; - if (rStatus == RunStatus::Stopping) { - shutdownResponse = std::move(d->shutdownResponse); - d->runStatus = RunStatus::Stopped; - } - } - if (rStatus != RunStatus::Stopping) - emit lifecycleError(); - else - shutdownResponse.sendResponse(nullptr); -} - -bool QLanguageServer::isRequestCanceled(const QJsonRpc::IdType &id) const -{ - const Q_D(QLanguageServer); - QJsonValue idVal = QTypedJson::toJsonValue(id); - QMutexLocker l(&d->mutex); - return d->requestsInProgress.value(idVal).canceled || d->runStatus != RunStatus::DidInitialize; -} - -bool QLanguageServer::isInitialized() const -{ - switch (runStatus()) { - case RunStatus::NotSetup: - case RunStatus::SettingUp: - case RunStatus::DidSetup: - case RunStatus::Initializing: - return false; - case RunStatus::DidInitialize: - case RunStatus::WaitPending: - case RunStatus::Stopping: - case RunStatus::Stopped: - break; - } - return true; -} - -QT_END_NAMESPACE diff --git a/tools/qmlls/qlanguageserver.h b/tools/qmlls/qlanguageserver.h deleted file mode 100644 index d4172b192c..0000000000 --- a/tools/qmlls/qlanguageserver.h +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef QLANGUAGESERVER_P_H -#define QLANGUAGESERVER_P_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 <QtLanguageServer/private/qlanguageserverspec_p.h> -#include <QtLanguageServer/private/qlanguageserverprotocol_p.h> -#include <QtLanguageServer/private/qlspnotifysignals_p.h> -#include <QtCore/qloggingcategory.h> - -QT_BEGIN_NAMESPACE - -class QLanguageServer; -class QLanguageServerPrivate; -Q_DECLARE_LOGGING_CATEGORY(lspServerLog) - -class QLanguageServerModule : public QObject -{ - Q_OBJECT -public: - QLanguageServerModule(QObject *parent = nullptr) : QObject(parent) { } - virtual QString name() const = 0; - virtual void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) = 0; - virtual void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &) = 0; -}; - -class QLanguageServer : public QObject -{ - Q_OBJECT - Q_PROPERTY(RunStatus runStatus READ runStatus NOTIFY runStatusChanged) - Q_PROPERTY(bool isInitialized READ isInitialized) -public: - QLanguageServer(const QJsonRpcTransport::DataHandler &h, QObject *parent = nullptr); - enum class RunStatus { - NotSetup, - SettingUp, - DidSetup, - Initializing, - DidInitialize, // normal state of execution - WaitPending, - Stopping, - Stopped - }; - Q_ENUM(RunStatus) - - QLanguageServerProtocol *protocol(); - void finishSetup(); - void registerHandlers(QLanguageServerProtocol *protocol); - void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &serverInfo); - void addServerModule(QLanguageServerModule *serverModule); - QLanguageServerModule *moduleByName(const QString &n) const; - QLspNotifySignals *notifySignals(); - - // API - RunStatus runStatus() const; - bool isInitialized() const; - bool isRequestCanceled(const QJsonRpc::IdType &id) const; - const QLspSpecification::InitializeParams &clientInfo() const; - const QLspSpecification::InitializeResult &serverInfo() const; - -public Q_SLOTS: - void receiveData(const QByteArray &d); -Q_SIGNALS: - void runStatusChanged(RunStatus); - void clientInitialized(QLanguageServer *server); - void shutdown(); - void exit(); - void lifecycleError(); - -private: - void registerMethods(QJsonRpc::TypedRpc &typedRpc); - void executeShutdown(); - Q_DECLARE_PRIVATE(QLanguageServer) -}; - -QT_END_NAMESPACE - -#endif // QLANGUAGESERVER_P_H diff --git a/tools/qmlls/qlanguageserver_p.h b/tools/qmlls/qlanguageserver_p.h deleted file mode 100644 index f051309f90..0000000000 --- a/tools/qmlls/qlanguageserver_p.h +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef QLANGUAGESERVER_P_P_H -#define QLANGUAGESERVER_P_P_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 "qlanguageserver.h" -#include <QtLanguageServer/private/qlanguageserverprotocol_p.h> -#include <QtCore/QMutex> -#include <QtCore/QHash> -#include <QtCore/private/qobject_p.h> -#include <QtLanguageServer/private/qlspnotifysignals_p.h> - -#include <memory> - -QT_BEGIN_NAMESPACE - -class QRequestInProgress -{ -public: - bool canceled = false; -}; - -class QLanguageServerPrivate : public QObjectPrivate -{ -public: - QLanguageServerPrivate(const QJsonRpcTransport::DataHandler &h); - mutable QMutex mutex; - // mutex gated, monotonically increasing - QLanguageServer::RunStatus runStatus = QLanguageServer::RunStatus::NotSetup; - QHash<QJsonValue, QRequestInProgress> requestsInProgress; // mutex gated - bool clientInitialized = false; // mutex gated - QLspSpecification::InitializeParams clientInfo; // immutable after runStatus > DidInitialize - QLspSpecification::InitializeResult serverInfo; // immutable after runStatus > DidInitialize - QLspSpecification::Responses::ShutdownResponseType shutdownResponse; - QHash<QString, QLanguageServerModule *> modules; - QLanguageServerProtocol protocol; - QLspNotifySignals notifySignals; -}; - -QT_END_NAMESPACE -#endif // QLANGUAGESERVER_P_P_H diff --git a/tools/qmlls/qmlcompletionsupport.cpp b/tools/qmlls/qmlcompletionsupport.cpp deleted file mode 100644 index a148bc3935..0000000000 --- a/tools/qmlls/qmlcompletionsupport.cpp +++ /dev/null @@ -1,665 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "qmlcompletionsupport.h" -#include "qqmllanguageserver.h" -#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> -#include <QtCore/qthreadpool.h> -#include <QtCore/private/qduplicatetracker_p.h> -#include <QtCore/QRegularExpression> -#include <QtQmlDom/private/qqmldomexternalitems_p.h> -#include <QtQmlDom/private/qqmldomtop_p.h> - -QT_USE_NAMESPACE -using namespace QLspSpecification; -using namespace QQmlJS::Dom; -using namespace Qt::StringLiterals; - -Q_LOGGING_CATEGORY(complLog, "qt.languageserver.completions") - -void QmlCompletionSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) -{ - protocol->registerCompletionRequestHandler( - [this](const QByteArray &, const CompletionParams &cParams, - LSPPartialResponse< - std::variant<QList<CompletionItem>, CompletionList, std::nullptr_t>, - std::variant<CompletionList, QList<CompletionItem>>> &&response) { - QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( - QmlLsp::lspUriToQmlUrl(cParams.textDocument.uri)); - if (!doc.textDocument) { - response.sendResponse(QList<CompletionItem>()); - return; - } - CompletionRequest *req = new CompletionRequest; - std::optional<int> targetVersion; - { - QMutexLocker l(doc.textDocument->mutex()); - targetVersion = doc.textDocument->version(); - if (!targetVersion) { - qCWarning(complLog) << "no target version for completions in " - << QString::fromUtf8(cParams.textDocument.uri); - } - req->code = doc.textDocument->toPlainText(); - } - req->minVersion = (targetVersion ? *targetVersion : 0); - req->response = std::move(response); - req->completionParams = cParams; - { - QMutexLocker l(&m_mutex); - m_completions.insert(req->completionParams.textDocument.uri, req); - } - if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= req->minVersion) - updatedSnapshot(QmlLsp::lspUriToQmlUrl(req->completionParams.textDocument.uri)); - }); - protocol->registerCompletionItemResolveRequestHandler( - [](const QByteArray &, const CompletionItem &cParams, - LSPResponse<CompletionItem> &&response) { response.sendResponse(cParams); }); -} - -QmlCompletionSupport::QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel) - : m_codeModel(codeModel) -{ - QObject::connect(m_codeModel, &QmlLsp::QQmlCodeModel::updatedSnapshot, this, - &QmlCompletionSupport::updatedSnapshot); -} - -QmlCompletionSupport::~QmlCompletionSupport() -{ - QMutexLocker l(&m_mutex); - qDeleteAll(m_completions); - m_completions.clear(); -} - -QString QmlCompletionSupport::name() const -{ - return u"QmlCompletionSupport"_s; -} - -void QmlCompletionSupport::setupCapabilities( - const QLspSpecification::InitializeParams &, - QLspSpecification::InitializeResult &serverCapabilities) -{ - QLspSpecification::CompletionOptions cOptions; - if (serverCapabilities.capabilities.completionProvider) - cOptions = *serverCapabilities.capabilities.completionProvider; - cOptions.resolveProvider = false; - cOptions.triggerCharacters = QList<QByteArray>({ QByteArray(".") }); - serverCapabilities.capabilities.completionProvider = cOptions; -} - -void QmlCompletionSupport::updatedSnapshot(const QByteArray &url) -{ - QmlLsp::OpenDocumentSnapshot doc = m_codeModel->snapshotByUrl(url); - QList<CompletionRequest *> toCompl; - { - QMutexLocker l(&m_mutex); - for (auto [it, end] = m_completions.equal_range(url); it != end; ++it) { - if (doc.docVersion && it.value()->minVersion <= *doc.docVersion) - toCompl.append(it.value()); - } - if (!m_completions.isEmpty()) - qCDebug(complLog) << "updated " << QString::fromUtf8(url) << " v" - << (doc.docVersion ? (*doc.docVersion) : -1) << ", completing" - << m_completions.size() << "/" << m_completions.size(); - for (auto req : toCompl) - m_completions.remove(url, req); - } - for (auto it = toCompl.rbegin(), end = toCompl.rend(); it != end; ++it) { - CompletionRequest *req = *it; - QThreadPool::globalInstance()->start([req, doc]() mutable { - req->sendCompletions(doc); - delete req; - }); - } -} - -struct ItemLocation -{ - int depth = -1; - DomItem domItem; - FileLocations::Tree fileLocation; -}; - -QString CompletionRequest::urlAndPos() const -{ - return QString::fromUtf8(completionParams.textDocument.uri) + u":" - + QString::number(completionParams.position.line) + u":" - + QString::number(completionParams.position.character); -} - -// return the position of 0 based line and char offsets, never goes to the "next" line, but might -// return the position of the \n or \r that starts the next line (never the one that starts line) -static qsizetype posAfterLineChar(QString code, int line, int character) -{ - int targetLine = line; - qsizetype i = 0; - while (i != code.size() && targetLine != 0) { - QChar c = code.at(i++); - if (c == u'\n') { - --targetLine; - } - if (c == u'\r') { - if (i != code.size() && code.at(i) == u'\n') - ++i; - --targetLine; - } - } - qsizetype leftChars = character; - while (i != code.size() && leftChars) { - QChar c = code.at(i); - if (c == u'\n' || c == u'\r') - break; - ++i; - if (!c.isLowSurrogate()) - --leftChars; - } - return i; -} - -static QList<ItemLocation> findLastItemsContaining(DomItem file, int line, int character) -{ - QList<ItemLocation> itemsFound; - std::shared_ptr<QmlFile> filePtr = file.ownerAs<QmlFile>(); - if (!filePtr) - return itemsFound; - FileLocations::Tree t = filePtr->fileLocationsTree(); - Q_ASSERT(t); - QString code = filePtr->code(); // do something more advanced wrt to changes wrt to this->code? - if (code.isEmpty()) - qCWarning(complLog) << "no code"; - QList<ItemLocation> toDo; - qsizetype targetPos = posAfterLineChar(code, line, character); - Q_ASSERT(targetPos >= 0); - auto containsTarget = [targetPos](QQmlJS::SourceLocation l) { - if constexpr (sizeof(qsizetype) <= sizeof(quint32)) { - return l.begin() <= quint32(targetPos) && quint32(targetPos) < l.end(); - } else { - return l.begin() <= targetPos && targetPos < l.end(); - } - }; - if (containsTarget(t->info().fullRegion)) { - ItemLocation loc; - loc.depth = 0; - loc.domItem = file; - loc.fileLocation = t; - toDo.append(loc); - } - while (!toDo.isEmpty()) { - ItemLocation iLoc = toDo.last(); - toDo.removeLast(); - if (itemsFound.isEmpty() || itemsFound.constFirst().depth <= iLoc.depth) { - if (!itemsFound.isEmpty() && itemsFound.constFirst().depth < iLoc.depth) - itemsFound.clear(); - itemsFound.append(iLoc); - } - auto subEls = iLoc.fileLocation->subItems(); - for (auto it = subEls.begin(); it != subEls.end(); ++it) { - auto subLoc = std::static_pointer_cast<AttachedInfoT<FileLocations>>(it.value()); - Q_ASSERT(subLoc); - if (containsTarget(subLoc->info().fullRegion)) { - ItemLocation subItem; - subItem.depth = iLoc.depth + 1; - subItem.domItem = iLoc.domItem.path(it.key()); - subItem.fileLocation = subLoc; - toDo.append(subItem); - } - } - } - return itemsFound; -} - -// finds the filter string, the base (for fully qualified accesses) and the whole string -// just before pos in code -struct CompletionContextStrings -{ - CompletionContextStrings(QString code, qsizetype pos); - -public: - // line up until pos - QStringView preLine() const - { - return QStringView(m_code).mid(m_lineStart, m_pos - m_lineStart); - } - // the part used to filter the completion (normally actual filtering is left to the client) - QStringView filterChars() const - { - return QStringView(m_code).mid(m_filterStart, m_pos - m_filterStart); - } - // the base part (qualified access) - QStringView base() const - { - return QStringView(m_code).mid(m_baseStart, m_filterStart - m_baseStart); - } - // if we are at line start - bool atLineStart() const { return m_atLineStart; } - -private: - QString m_code; // the current code - qsizetype m_pos = {}; // current position of the cursor - qsizetype m_filterStart = {}; // start of the characters that are used to filter the suggestions - qsizetype m_lineStart = {}; // start of the current line - qsizetype m_baseStart = {}; // start of the dotted expression that ends at the cursor position - bool m_atLineStart = {}; // if there are only spaces before base -}; - -CompletionContextStrings::CompletionContextStrings(QString code, qsizetype pos) - : m_code(code), m_pos(pos) -{ - // computes the context just before pos in code. - // After this code all the values of all the attributes should be correct (see above) - // handle also letter or numbers represented a surrogate pairs? - m_filterStart = m_pos; - while (m_filterStart != 0) { - QChar c = code.at(m_filterStart - 1); - if (!c.isLetterOrNumber() && c != u'_') - break; - else - --m_filterStart; - } - // handle spaces? - m_baseStart = m_filterStart; - while (m_baseStart != 0) { - QChar c = code.at(m_baseStart - 1); - if (c != u'.' || m_baseStart == 1) - break; - c = code.at(m_baseStart - 2); - if (!c.isLetterOrNumber() && c != u'_') - break; - qsizetype baseEnd = --m_baseStart; - while (m_baseStart != 0) { - QChar c = code.at(m_baseStart - 1); - if (!c.isLetterOrNumber() && c != u'_') - break; - else - --m_baseStart; - } - if (m_baseStart == baseEnd) - break; - } - m_atLineStart = true; - m_lineStart = m_baseStart; - while (m_lineStart != 0) { - QChar c = code.at(m_lineStart - 1); - if (c == u'\n' || c == '\r') - break; - if (!c.isSpace()) - m_atLineStart = false; - --m_lineStart; - } -} - -enum class TypeCompletionsType { None, Types, TypesAndAttributes }; - -enum class FunctionCompletion { None, Declaration }; - -enum class ImportCompletionType { None, Module, Version }; - -void CompletionRequest::sendCompletions(QmlLsp::OpenDocumentSnapshot &doc) -{ - QList<CompletionItem> res = completions(doc); - response.sendResponse(res); -} - -static QList<CompletionItem> importCompletions(DomItem &file, const CompletionContextStrings &ctx) -{ - // returns completions for import statements, ctx is supposed to be in an import statement - QList<CompletionItem> res; - ImportCompletionType importCompletionType = ImportCompletionType::None; - QRegularExpression spaceRe(uR"(\W+)"_s); - QList<QStringView> linePieces = ctx.preLine().split(spaceRe, Qt::SkipEmptyParts); - qsizetype effectiveLength = linePieces.size() - + ((!ctx.preLine().isEmpty() && ctx.preLine().last().isSpace()) ? 1 : 0); - if (effectiveLength < 2) { - CompletionItem comp; - comp.label = "import"; - comp.kind = int(CompletionItemKind::Keyword); - res.append(comp); - } - if (linePieces.isEmpty() || linePieces.first() != u"import") - return res; - if (effectiveLength == 2) { - // the cursor is after the import, possibly in a partial module name - importCompletionType = ImportCompletionType::Module; - } else if (effectiveLength == 3) { - if (linePieces.last() != u"as") { - // the cursor is after the module, possibly in a partial version token (or partial as) - CompletionItem comp; - comp.label = "as"; - comp.kind = int(CompletionItemKind::Keyword); - res.append(comp); - importCompletionType = ImportCompletionType::Version; - } - } - DomItem env = file.environment(); - if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) { - switch (importCompletionType) { - case ImportCompletionType::None: - break; - case ImportCompletionType::Module: { - QDuplicateTracker<QString> modulesSeen; - for (const QString &uri : envPtr->moduleIndexUris(env)) { - QStringView base = ctx.base(); // if we allow spaces we should get rid of them - if (uri.startsWith(base)) { - QStringList rest = uri.mid(base.size()).split(u'.'); - if (rest.isEmpty()) - continue; - - const QString label = rest.first(); - if (!modulesSeen.hasSeen(label)) { - CompletionItem comp; - comp.label = label.toUtf8(); - comp.kind = int(CompletionItemKind::Module); - res.append(comp); - } - } - } - break; - } - case ImportCompletionType::Version: - if (ctx.base().isEmpty()) { - for (int majorV : - envPtr->moduleIndexMajorVersions(env, linePieces.at(1).toString())) { - CompletionItem comp; - comp.label = QString::number(majorV).toUtf8(); - comp.kind = int(CompletionItemKind::Constant); - res.append(comp); - } - } else { - bool hasMajorVersion = ctx.base().endsWith(u'.'); - int majorV = -1; - if (hasMajorVersion) - majorV = ctx.base().mid(0, ctx.base().size() - 1).toInt(&hasMajorVersion); - if (!hasMajorVersion) - break; - if (std::shared_ptr<ModuleIndex> mIndex = - envPtr->moduleIndexWithUri(env, linePieces.at(1).toString(), majorV)) { - for (int minorV : mIndex->minorVersions()) { - CompletionItem comp; - comp.label = QString::number(minorV).toUtf8(); - comp.kind = int(CompletionItemKind::Constant); - res.append(comp); - } - } - } - break; - } - } - return res; -} - -static QList<CompletionItem> idsCompletions(DomItem component) -{ - qCDebug(complLog) << "adding ids completions"; - QList<CompletionItem> res; - for (const QString &k : component.field(Fields::ids).keys()) { - CompletionItem comp; - comp.label = k.toUtf8(); - comp.kind = int(CompletionItemKind::Value); - res.append(comp); - } - return res; -} - -static QList<CompletionItem> bindingsCompletions(DomItem &containingObject) -{ - // returns valid bindings completions (i.e. reachable properties and signal handlers) - QList<CompletionItem> res; - qCDebug(complLog) << "binding completions"; - containingObject.visitPrototypeChain( - [&res](DomItem &it) { - qCDebug(complLog) << "prototypeChain" << it.internalKindStr() << it.canonicalPath(); - if (const QmlObject *itPtr = it.as<QmlObject>()) { - // signal handlers - auto methods = itPtr->methods(); - auto it = methods.cbegin(); - while (it != methods.cend()) { - if (it.value().methodType == MethodInfo::MethodType::Signal) { - CompletionItem comp; - QString signal = it.key(); - comp.label = - (u"on"_s + signal.at(0).toUpper() + signal.mid(1)).toUtf8(); - res.append(comp); - } - ++it; - } - // properties that can be bound - auto pDefs = itPtr->propertyDefs(); - for (auto it2 = pDefs.keyBegin(); it2 != pDefs.keyEnd(); ++it2) { - qCDebug(complLog) << "adding property" << *it2; - CompletionItem comp; - comp.label = it2->toUtf8(); - comp.insertText = (*it2 + u": "_s).toUtf8(); - comp.kind = int(CompletionItemKind::Property); - res.append(comp); - } - } - return true; - }, - VisitPrototypesOption::Normal); - return res; -} - -static QList<CompletionItem> reachableSymbols(DomItem &context, const CompletionContextStrings &ctx, - TypeCompletionsType typeCompletionType, - FunctionCompletion completeMethodCalls) -{ - // returns completions for the reachable types or attributes from context - QList<CompletionItem> res; - QMap<CompletionItemKind, QSet<QString>> symbols; - QSet<quintptr> visited; - QList<Path> visitedRefs; - auto addLocalSymbols = [&res, typeCompletionType, completeMethodCalls, &symbols](DomItem &el) { - switch (typeCompletionType) { - case TypeCompletionsType::None: - return false; - case TypeCompletionsType::Types: - switch (el.internalKind()) { - case DomType::ImportScope: { - const QSet<QString> localSymbols = el.localSymbolNames( - LocalSymbolsType::QmlTypes | LocalSymbolsType::Namespaces); - qCDebug(complLog) << "adding local symbols of:" << el.internalKindStr() - << el.canonicalPath() << localSymbols; - symbols[CompletionItemKind::Class] += localSymbols; - break; - } - default: { - qCDebug(complLog) << "skipping local symbols for non type" << el.internalKindStr() - << el.canonicalPath(); - break; - } - } - break; - case TypeCompletionsType::TypesAndAttributes: - auto localSymbols = el.localSymbolNames(LocalSymbolsType::All); - if (const QmlObject *elPtr = el.as<QmlObject>()) { - auto methods = elPtr->methods(); - auto it = methods.cbegin(); - while (it != methods.cend()) { - localSymbols.remove(it.key()); - if (completeMethodCalls == FunctionCompletion::Declaration) { - QStringList parameters; - for (const MethodParameter &pInfo : std::as_const(it->parameters)) { - QStringList param; - if (!pInfo.typeName.isEmpty()) - param << pInfo.typeName; - if (!pInfo.name.isEmpty()) - param << pInfo.name; - if (pInfo.defaultValue) { - param << u"= " + pInfo.defaultValue->code(); - } - parameters.append(param.join(' ')); - } - - QString commentsStr; - - if (!it->comments.regionComments.isEmpty()) { - for (const Comment &c : it->comments.regionComments[0].preComments) { - commentsStr += c.rawComment().toString().trimmed() + '\n'; - } - } - - CompletionItem comp; - comp.documentation = - u"%1%2(%3)"_s.arg(commentsStr, it.key(), parameters.join(u", ")) - .toUtf8(); - comp.label = (it.key() + u"()").toUtf8(); - comp.kind = int(CompletionItemKind::Function); - - if (it->typeName.isEmpty()) - comp.detail = "returns void"; - else - comp.detail = (u"returns "_s + it->typeName).toUtf8(); - - // Only append full bracket if there are no parameters - if (it->parameters.isEmpty()) - comp.insertText = comp.label; - else - // add snippet support? - comp.insertText = (it.key() + u"(").toUtf8(); - - res.append(comp); - } - ++it; - } - } - qCDebug(complLog) << "adding local symbols of:" << el.internalKindStr() - << el.canonicalPath() << localSymbols; - symbols[CompletionItemKind::Field] += localSymbols; - break; - } - return true; - }; - if (ctx.base().isEmpty()) { - if (typeCompletionType != TypeCompletionsType::None) { - qCDebug(complLog) << "adding symbols reachable from:" << context.internalKindStr() - << context.canonicalPath(); - DomItem it = context.proceedToScope(); - it.visitScopeChain(addLocalSymbols, LookupOption::Normal, &defaultErrorHandler, - &visited, &visitedRefs); - } - } else { - QList<QStringView> baseItems = ctx.base().split(u'.', Qt::SkipEmptyParts); - Q_ASSERT(!baseItems.isEmpty()); - auto addReachableSymbols = [&visited, &visitedRefs, &addLocalSymbols](Path, - DomItem &it) -> bool { - qCDebug(complLog) << "adding directly accessible symbols of" << it.internalKindStr() - << it.canonicalPath(); - it.visitDirectAccessibleScopes(addLocalSymbols, VisitPrototypesOption::Normal, - &defaultErrorHandler, &visited, &visitedRefs); - return true; - }; - Path toSearch = Paths::lookupSymbolPath(ctx.base().toString().chopped(1)); - context.resolve(toSearch, addReachableSymbols, &defaultErrorHandler); - // add attached types? technically we should... - } - for (auto symbolKinds = symbols.constBegin(); symbolKinds != symbols.constEnd(); - ++symbolKinds) { - for (auto symbol = symbolKinds.value().constBegin(); - symbol != symbolKinds.value().constEnd(); ++symbol) { - CompletionItem comp; - comp.label = symbol->toUtf8(); - comp.kind = int(symbolKinds.key()); - res.append(comp); - } - } - return res; -} - -QList<CompletionItem> CompletionRequest::completions(QmlLsp::OpenDocumentSnapshot &doc) const -{ - QList<CompletionItem> res; - if (!doc.validDoc) { - qCWarning(complLog) << "No valid document for completions for " - << QString::fromUtf8(completionParams.textDocument.uri); - // try to add some import and global completions? - return res; - } - if (!doc.docVersion || *doc.docVersion < minVersion) { - qCWarning(complLog) << "sendCompletions on older doc version"; - } else if (!doc.validDocVersion || *doc.validDocVersion < minVersion) { - qCWarning(complLog) << "using outdated valid doc, position might be incorrect"; - } - DomItem file = doc.validDoc.fileObject(QQmlJS::Dom::GoTo::MostLikely); - // clear reference cache to resolve latest versions (use a local env instead?) - if (std::shared_ptr<DomEnvironment> envPtr = file.environment().ownerAs<DomEnvironment>()) - envPtr->clearReferenceCache(); - qsizetype pos = posAfterLineChar(code, completionParams.position.line, - completionParams.position.character); - CompletionContextStrings ctx(code, pos); - QList<ItemLocation> itemsFound = - findLastItemsContaining(file, completionParams.position.line, - completionParams.position.character - ctx.filterChars().size()); - if (itemsFound.size() > 1) { - QStringList paths; - for (auto &it : itemsFound) - paths.append(it.domItem.canonicalPath().toString()); - qCWarning(complLog) << "Multiple elements of " << urlAndPos() - << " at the same depth:" << paths << "(using first)"; - } - DomItem currentItem; - if (!itemsFound.isEmpty()) - currentItem = itemsFound.first().domItem; - qCDebug(complLog) << "Completion at " << urlAndPos() << " " << completionParams.position.line - << ":" << completionParams.position.character << "offset:" << pos - << "base:" << ctx.base() << "filter:" << ctx.filterChars() - << "lastVersion:" << (doc.docVersion ? (*doc.docVersion) : -1) - << "validVersion:" << (doc.validDocVersion ? (*doc.validDocVersion) : -1) - << "in" << currentItem.internalKindStr() << currentItem.canonicalPath(); - DomItem containingObject = currentItem.qmlObject(); - TypeCompletionsType typeCompletionType = TypeCompletionsType::None; - FunctionCompletion methodCompletion = FunctionCompletion::Declaration; - - if (!containingObject) { - methodCompletion = FunctionCompletion::None; - // global completions - if (ctx.atLineStart()) { - if (ctx.base().isEmpty()) { - { - CompletionItem comp; - comp.label = "pragma"; - comp.kind = int(CompletionItemKind::Keyword); - res.append(comp); - } - } - typeCompletionType = TypeCompletionsType::Types; - } - // Import completion - res += importCompletions(file, ctx); - } else { - methodCompletion = FunctionCompletion::Declaration; - bool addIds = false; - - if (ctx.atLineStart() && currentItem.internalKind() != DomType::ScriptExpression - && currentItem.internalKind() != DomType::List) { - // add bindings - methodCompletion = FunctionCompletion::None; - if (ctx.base().isEmpty()) { - for (const QStringView &s : std::array<QStringView, 5>( - { u"property", u"readonly", u"default", u"signal", u"function" })) { - CompletionItem comp; - comp.label = s.toUtf8(); - comp.kind = int(CompletionItemKind::Keyword); - res.append(comp); - } - res += bindingsCompletions(containingObject); - typeCompletionType = TypeCompletionsType::Types; - } else { - // handle value types later with type expansion - typeCompletionType = TypeCompletionsType::TypesAndAttributes; - } - } else { - addIds = true; - typeCompletionType = TypeCompletionsType::TypesAndAttributes; - } - if (addIds) { - res += idsCompletions(containingObject.component()); - } - } - - DomItem context = containingObject; - if (!context) - context = file; - // adds types and attributes - res += reachableSymbols(context, ctx, typeCompletionType, methodCompletion); - return res; -} diff --git a/tools/qmlls/qmlcompletionsupport.h b/tools/qmlls/qmlcompletionsupport.h deleted file mode 100644 index ce149667bd..0000000000 --- a/tools/qmlls/qmlcompletionsupport.h +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#ifndef QMLCOMPLETIONSUPPORT_H -#define QMLCOMPLETIONSUPPORT_H - -#include "qlanguageserver.h" -#include "qqmlcodemodel.h" -#include <QtCore/qmutex.h> -#include <QtCore/qhash.h> - -struct CompletionRequest -{ - int minVersion; - QString code; - QLspSpecification::CompletionParams completionParams; - QLspSpecification::LSPPartialResponse< - std::variant<QList<QLspSpecification::CompletionItem>, - QLspSpecification::CompletionList, std::nullptr_t>, - std::variant<QLspSpecification::CompletionList, - QList<QLspSpecification::CompletionItem>>> - response; - void sendCompletions(QmlLsp::OpenDocumentSnapshot &); - QString urlAndPos() const; - QList<QLspSpecification::CompletionItem> completions(QmlLsp::OpenDocumentSnapshot &doc) const; -}; - -class QmlCompletionSupport : public QLanguageServerModule -{ - Q_OBJECT -public: - QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel); - ~QmlCompletionSupport(); - QString name() const override; - void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; - void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &) override; -public Q_SLOTS: - void updatedSnapshot(const QByteArray &uri); - -private: - QmlLsp::QQmlCodeModel *m_codeModel; - QMutex m_mutex; - QMultiHash<QString, CompletionRequest *> m_completions; -}; - -#endif // QMLCOMPLETIONSUPPORT_H diff --git a/tools/qmlls/qmllanguageservertool.cpp b/tools/qmlls/qmllanguageservertool.cpp index 896e0c049c..aa69b058bc 100644 --- a/tools/qmlls/qmllanguageservertool.cpp +++ b/tools/qmlls/qmllanguageservertool.cpp @@ -1,12 +1,14 @@ // Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "qqmllanguageserver.h" + +#include <QtQmlLS/private/qqmllanguageserver_p.h> #include <QtCore/qdebug.h> #include <QtCore/qfile.h> #include <QtCore/qdir.h> #include <QtCore/qfileinfo.h> #include <QtCore/qcoreapplication.h> -#include "../shared/qqmltoolingsettings.h" +#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h> +#include <QtQmlToolingSettings/private/qqmltoolingutils_p.h> #include <QtCore/qdiriterator.h> #include <QtCore/qjsonobject.h> #include <QtCore/qjsonarray.h> @@ -48,47 +50,83 @@ class StdinReader : public QObject { Q_OBJECT public: - void run() - { - auto guard = qScopeGuard([this]() { emit eof(); }); - const constexpr qsizetype bufSize = 1024; - qsizetype bytesInBuf = 0; - char bufferData[2 * bufSize]; - char *buffer = static_cast<char *>(bufferData); - - auto trySend = [this, &bytesInBuf, buffer]() { - if (bytesInBuf == 0) - return; - qsizetype toSend = bytesInBuf; - bytesInBuf = 0; - QByteArray dataToSend(buffer, toSend); - emit receivedData(dataToSend); - }; - QHttpMessageStreamParser streamParser( + StdinReader() + : m_streamReader( [](const QByteArray &, const QByteArray &) { /* just a header, do nothing */ }, - [&trySend](const QByteArray &) { + [this](const QByteArray &) { + // stop reading until we are sure that the server is not shutting down + m_isReading = false; + // message body - trySend(); + m_shouldSendData = true; }, - [&trySend](QtMsgType, QString) { + [this](QtMsgType, QString) { // there was an error - trySend(); + m_shouldSendData = true; }, - QHttpMessageStreamParser::UNBUFFERED); - - while (std::cin.get(buffer[bytesInBuf])) { // should poll/select and process events - qsizetype readNow = std::cin.readsome(buffer + bytesInBuf + 1, bufSize) + 1; - QByteArray toAdd(buffer + bytesInBuf, readNow); - bytesInBuf += readNow; - if (bytesInBuf >= bufSize) - trySend(); - streamParser.receiveData(toAdd); - } - trySend(); + QHttpMessageStreamParser::UNBUFFERED) + { } + + void sendData() + { + const bool isEndOfMessage = !m_isReading && !m_hasEof; + const qsizetype toSend = m_bytesInBuf; + m_bytesInBuf = 0; + const QByteArray dataToSend(m_buffer, toSend); + emit receivedData(dataToSend, isEndOfMessage); + } + +private: + const static constexpr qsizetype s_bufSize = 1024; + qsizetype m_bytesInBuf = 0; + char m_buffer[2 * s_bufSize] = {}; + QHttpMessageStreamParser m_streamReader; + /*! + \internal + Indicates if the current message is not read out entirely. + */ + bool m_isReading = true; + /*! + \internal + Indicates if an EOF was encountered. No more data can be read after an EOF. + */ + bool m_hasEof = false; + /*! + \internal + Indicates whether sendData() should be called or not. + */ + bool m_shouldSendData = false; signals: - void receivedData(const QByteArray &data); + void receivedData(const QByteArray &data, bool canRequestMoreData); void eof(); +public slots: + void readNextMessage() + { + if (m_hasEof) + return; + m_isReading = true; + // Try to fill up the buffer as much as possible before calling the queued signal: + // each loop iteration might read only one character from std::in in the worstcase, this + // happens for example on macos. + while (m_isReading) { + // block while waiting for some data + if (!std::cin.get(m_buffer[m_bytesInBuf])) { + m_hasEof = true; + emit eof(); + return; + } + // see if more data is available and fill the buffer with it + qsizetype readNow = std::cin.readsome(m_buffer + m_bytesInBuf + 1, s_bufSize) + 1; + QByteArray toAdd(m_buffer + m_bytesInBuf, readNow); + m_bytesInBuf += readNow; + m_streamReader.receiveData(std::move(toAdd)); + + m_shouldSendData |= m_bytesInBuf >= s_bufSize; + if (std::exchange(m_shouldSendData, false)) + sendData(); + } + } }; // To debug: @@ -131,7 +169,7 @@ int main(int argv, char *argc[]) QCoreApplication app(argv, argc); QCoreApplication::setApplicationName("qmlls"); QCoreApplication::setApplicationVersion(QT_VERSION_STR); -#if QT_CONFIG(commandlineparser) + QCommandLineParser parser; QQmlToolingSettings settings(QLatin1String("qmlls")); parser.setApplicationDescription(QLatin1String(R"(QML languageserver)")); @@ -163,6 +201,17 @@ int main(int argv, char *argc[]) parser.addOption(buildDirOption); settings.addOption(buildDir); + QString qmlImportPath = QStringLiteral(u"qml-import-path"); + QCommandLineOption qmlImportPathOption( + QStringList() << "I", QLatin1String("Look for QML modules in the specified directory"), + qmlImportPath); + parser.addOption(qmlImportPathOption); + + QCommandLineOption environmentOption( + QStringList() << "E", + QLatin1String("Use the QML_IMPORT_PATH environment variable to look for QML Modules")); + parser.addOption(environmentOption); + QCommandLineOption writeDefaultsOption( QStringList() << "write-defaults", QLatin1String("Writes defaults settings to .qmlls.ini and exits (Warning: This " @@ -174,6 +223,17 @@ int main(int argv, char *argc[]) "command line options into consideration")); parser.addOption(ignoreSettings); + QCommandLineOption noCMakeCallsOption( + QStringList() << "no-cmake-calls", + QLatin1String("Disables automatic CMake rebuilds and C++ file watching.")); + parser.addOption(noCMakeCallsOption); + settings.addOption("no-cmake-calls", "false"); + + QCommandLineOption docDir(QStringList() << "p", + QLatin1String("Documentation path to use for the documentation hints feature")); + parser.addOption(docDir); + settings.addOption("docDir"); + parser.process(app); if (parser.isSet(writeDefaultsOption)) { @@ -203,7 +263,6 @@ int main(int argv, char *argc[]) QThread::sleep(waitSeconds); qDebug() << "starting"; } -#endif QMutex writeMutex; QQmlLanguageServer qmlServer( [&writeMutex](const QByteArray &data) { @@ -212,19 +271,100 @@ int main(int argv, char *argc[]) std::cout.flush(); }, (parser.isSet(ignoreSettings) ? nullptr : &settings)); - if (parser.isSet(buildDirOption)) - qmlServer.codeModel()->setBuildPathsForRootUrl(QByteArray(), parser.values(buildDirOption)); + + if (parser.isSet(docDir)) + qmlServer.codeModel()->setDocumentationRootPath(parser.value(docDir).toUtf8()); + + const bool disableCMakeCallsViaEnvironment = + qmlGetConfigOption<bool, qmlConvertBoolConfigOption>("QMLLS_NO_CMAKE_CALLS"); + + if (disableCMakeCallsViaEnvironment || parser.isSet(noCMakeCallsOption)) { + if (disableCMakeCallsViaEnvironment) { + qWarning() << "Disabling CMake calls via QMLLS_NO_CMAKE_CALLS environment variable."; + } else { + qWarning() << "Disabling CMake calls via command line switch."; + } + + qmlServer.codeModel()->disableCMakeCalls(); + } + + if (parser.isSet(buildDirOption)) { + const QStringList dirs = + QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, buildDirOption); + + qInfo().nospace().noquote() + << "Using build directories passed by -b: \"" << dirs.join(u"\", \""_s) << "\"."; + + qmlServer.codeModel()->setBuildPathsForRootUrl(QByteArray(), dirs); + } else if (QStringList dirsFromEnv = + QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv("QMLLS_BUILD_DIRS"); + !dirsFromEnv.isEmpty()) { + + // warn now at qmlls startup that those directories will be used later in qqmlcodemodel when + // searching for build folders. + qInfo().nospace().noquote() << "Using build directories passed from environment variable " + "\"QMLLS_BUILD_DIRS\": \"" + << dirsFromEnv.join(u"\", \""_s) << "\"."; + + } else { + qInfo() << "Using the build directories found in the .qmlls.ini file. Your build folder " + "might not be found if no .qmlls.ini files are present in the root source " + "folder."; + } + QStringList importPaths{ QLibraryInfo::path(QLibraryInfo::QmlImportsPath) }; + if (parser.isSet(qmlImportPathOption)) { + const QStringList pathsFromOption = + QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, qmlImportPathOption); + qInfo().nospace().noquote() << "Using import directories passed by -I: \"" + << pathsFromOption.join(u"\", \""_s) << "\"."; + importPaths << pathsFromOption; + } + if (parser.isSet(environmentOption)) { + if (const QStringList dirsFromEnv = + QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML_IMPORT_PATH"_s); + !dirsFromEnv.isEmpty()) { + qInfo().nospace().noquote() + << "Using import directories passed from environment variable " + "\"QML_IMPORT_PATH\": \"" + << dirsFromEnv.join(u"\", \""_s) << "\"."; + importPaths << dirsFromEnv; + } + + if (const QStringList dirsFromEnv2 = + QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML2_IMPORT_PATH"_s); + !dirsFromEnv2.isEmpty()) { + qInfo().nospace().noquote() + << "Using import directories passed from the deprecated environment variable " + "\"QML2_IMPORT_PATH\": \"" + << dirsFromEnv2.join(u"\", \""_s) << "\"."; + importPaths << dirsFromEnv2; + } + } + qmlServer.codeModel()->setImportPaths(importPaths); + StdinReader r; + QThread workerThread; + r.moveToThread(&workerThread); QObject::connect(&r, &StdinReader::receivedData, qmlServer.server(), &QLanguageServer::receiveData); - QObject::connect(&r, &StdinReader::eof, &app, [&app]() { + QObject::connect(qmlServer.server(), &QLanguageServer::readNextMessage, &r, + &StdinReader::readNextMessage); + auto exit = [&app, &workerThread]() { + workerThread.quit(); + workerThread.wait(); QTimer::singleShot(100, &app, []() { QCoreApplication::processEvents(); QCoreApplication::exit(); }); - }); - QThreadPool::globalInstance()->start([&r]() { r.run(); }); + }; + QObject::connect(&r, &StdinReader::eof, &app, exit); + QObject::connect(qmlServer.server(), &QLanguageServer::shutdown, exit); + + emit r.readNextMessage(); + workerThread.start(); app.exec(); + workerThread.quit(); + workerThread.wait(); return qmlServer.returnValue(); } diff --git a/tools/qmlls/qmllintsuggestions.cpp b/tools/qmlls/qmllintsuggestions.cpp deleted file mode 100644 index d08209a054..0000000000 --- a/tools/qmlls/qmllintsuggestions.cpp +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "qmllintsuggestions.h" -#include <QtLanguageServer/private/qlanguageserverspec_p.h> -#include <QtQmlCompiler/private/qqmljslinter_p.h> -#include <QtQmlCompiler/private/qqmljslogger_p.h> -#include <QtCore/qlibraryinfo.h> -#include <QtCore/qtimer.h> -#include <QtCore/qdebug.h> - -using namespace QLspSpecification; -using namespace QQmlJS::Dom; -using namespace Qt::StringLiterals; - -Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint") - -QT_BEGIN_NAMESPACE -namespace QmlLsp { - -static DiagnosticSeverity severityFromMsgType(QtMsgType t) -{ - switch (t) { - case QtDebugMsg: - return DiagnosticSeverity::Hint; - case QtInfoMsg: - return DiagnosticSeverity::Information; - case QtWarningMsg: - return DiagnosticSeverity::Warning; - case QtCriticalMsg: - case QtFatalMsg: - break; - } - return DiagnosticSeverity::Error; -} - -static void codeActionHandler( - const QByteArray &, const CodeActionParams ¶ms, - LSPPartialResponse<std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>, - QList<std::variant<Command, CodeAction>>> &&response) -{ - QList<std::variant<Command, CodeAction>> responseData; - - for (const Diagnostic &diagnostic : params.context.diagnostics) { - if (!diagnostic.data.has_value()) - continue; - - const auto &data = diagnostic.data.value(); - - int version = data[u"version"].toInt(); - QJsonArray suggestions = data[u"suggestions"].toArray(); - - QList<TextDocumentEdit> edits; - QString message; - for (const QJsonValue &suggestion : suggestions) { - QString replacement = suggestion[u"replacement"].toString(); - message += suggestion[u"message"].toString() + u"\n"; - - TextEdit textEdit; - textEdit.range = { Position { suggestion[u"lspBeginLine"].toInt(), - suggestion[u"lspBeginCharacter"].toInt() }, - Position { suggestion[u"lspEndLine"].toInt(), - suggestion[u"lspEndCharacter"].toInt() } }; - textEdit.newText = replacement.toUtf8(); - - TextDocumentEdit textDocEdit; - textDocEdit.textDocument = { params.textDocument, version }; - textDocEdit.edits.append(textEdit); - - edits.append(textDocEdit); - } - message.chop(1); - WorkspaceEdit edit; - edit.documentChanges = edits; - - CodeAction action; - action.kind = u"refactor.rewrite"_s.toUtf8(); - action.edit = edit; - action.title = message.toUtf8(); - - responseData.append(action); - } - - response.sendResponse(responseData); -} - -void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) -{ - protocol->registerCodeActionRequestHandler(&codeActionHandler); -} - -void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &, - QLspSpecification::InitializeResult &serverInfo) -{ - serverInfo.capabilities.codeActionProvider = true; -} - -QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel) - : m_server(server), m_codeModel(codeModel) -{ - QObject::connect(m_codeModel, &QmlLsp::QQmlCodeModel::updatedSnapshot, this, - &QmlLintSuggestions::diagnose, Qt::DirectConnection); -} - -void QmlLintSuggestions::diagnose(const QByteArray &url) -{ - const int maxInvalidMsec = 4000; - qCDebug(lintLog) << "diagnose start"; - QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUrl(url); - QList<Diagnostic> diagnostics; - std::optional<int> version; - DomItem doc; - { - QMutexLocker l(&m_mutex); - LastLintUpdate &lastUpdate = m_lastUpdate[url]; - if (lastUpdate.version && *lastUpdate.version == version) { - qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc"; - return; - } - if (snapshot.validDocVersion - && (!lastUpdate.version || *snapshot.validDocVersion > *lastUpdate.version)) { - doc = snapshot.validDoc; - version = snapshot.validDocVersion; - } else if (snapshot.docVersion - && (!lastUpdate.version || *snapshot.docVersion > *lastUpdate.version)) { - if (!lastUpdate.version || !snapshot.validDocVersion - || (lastUpdate.invalidUpdatesSince - && lastUpdate.invalidUpdatesSince->msecsTo(QDateTime::currentDateTime()) - > maxInvalidMsec)) { - doc = snapshot.doc; - version = snapshot.docVersion; - } else if (!lastUpdate.invalidUpdatesSince) { - lastUpdate.invalidUpdatesSince = QDateTime::currentDateTime(); - QTimer::singleShot(maxInvalidMsec, Qt::VeryCoarseTimer, this, - [this, url]() { diagnose(url); }); - } - } - if (doc) { - // update immediately, and do not keep track of sent version, thus in extreme cases sent - // updates could be out of sync - lastUpdate.version = version; - lastUpdate.invalidUpdatesSince.reset(); - } - } - QString fileContents; - if (doc) { - qCDebug(lintLog) << "has doc, do real lint"; - QStringList imports = m_codeModel->buildPathsForFileUrl(url); - imports.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)); - // add m_server->clientInfo().rootUri & co? - bool silent = true; - QString filename = doc.canonicalFilePath(); - fileContents = doc.field(Fields::code).value().toString(); - QStringList qmltypesFiles; - QStringList resourceFiles; - QList<QQmlJSLogger::Category> categories; - - QQmlJSLinter linter(imports); - - linter.lintFile(filename, &fileContents, silent, nullptr, imports, qmltypesFiles, - resourceFiles, categories); - auto addLength = [&fileContents](Position &position, int startOffset, int length) { - int i = startOffset; - int iEnd = i + length; - if (iEnd > int(fileContents.size())) - iEnd = fileContents.size(); - while (i < iEnd) { - if (fileContents.at(i) == u'\n') { - ++position.line; - position.character = 0; - if (i + 1 < iEnd && fileContents.at(i) == u'\r') - ++i; - } else { - ++position.character; - } - ++i; - } - }; - - auto messageToDiagnostic = [&addLength, &version](const Message &message) { - Diagnostic diagnostic; - diagnostic.severity = severityFromMsgType(message.type); - Range &range = diagnostic.range; - Position &position = range.start; - - QQmlJS::SourceLocation srcLoc = message.loc; - - position.line = srcLoc.isValid() ? srcLoc.startLine - 1 : 0; - position.character = srcLoc.isValid() ? srcLoc.startColumn - 1 : 0; - range.end = position; - addLength(range.end, srcLoc.isValid() ? message.loc.offset : 0, srcLoc.isValid() ? message.loc.length : 0); - diagnostic.message = message.message.toUtf8(); - diagnostic.source = QByteArray("qmllint"); - - auto suggestion = message.fixSuggestion; - if (suggestion.has_value()) { - // We need to interject the information about where the fix suggestions end - // here since we don't have access to the textDocument to calculate it later. - QJsonArray fixedSuggestions; - for (const FixSuggestion::Fix &fix : suggestion->fixes) { - QQmlJS::SourceLocation cut = fix.cutLocation; - - int line = cut.isValid() ? cut.startLine - 1 : 0; - int column = cut.isValid() ? cut.startColumn - 1 : 0; - - QJsonObject object; - object[u"lspBeginLine"] = line; - object[u"lspBeginCharacter"] = column; - - Position end = { line, column }; - - addLength(end, srcLoc.isValid() ? cut.offset : 0, - srcLoc.isValid() ? cut.length : 0); - object[u"lspEndLine"] = end.line; - object[u"lspEndCharacter"] = end.character; - - object[u"message"] = fix.message; - object[u"replacement"] = fix.replacementString; - - fixedSuggestions << object; - } - QJsonObject data; - data[u"suggestions"] = fixedSuggestions; - - Q_ASSERT(version.has_value()); - data[u"version"] = version.value(); - - diagnostic.data = data; - } - return diagnostic; - }; - doc.iterateErrors( - [&diagnostics, &addLength](DomItem, ErrorMessage msg) { - Diagnostic diagnostic; - diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level))); - // do something with msg.errorGroups ? - auto &location = msg.location; - Range &range = diagnostic.range; - range.start.line = location.startLine - 1; - range.start.character = location.startColumn - 1; - range.end = range.start; - addLength(range.end, location.offset, location.length); - diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size()); - diagnostic.source = "domParsing"; - diagnostic.message = msg.message.toUtf8(); - diagnostics.append(diagnostic); - return true; - }, - true); - - if (const QQmlJSLogger *logger = linter.logger()) { - qsizetype nDiagnostics = diagnostics.size(); - for (const auto &messages : { logger->infos(), logger->warnings(), logger->errors() }) { - for (const Message &message : messages) { - diagnostics.append(messageToDiagnostic(message)); - } - } - if (diagnostics.size() != nDiagnostics && imports.size() == 1) { - Diagnostic diagnostic; - diagnostic.severity = DiagnosticSeverity::Warning; - Range &range = diagnostic.range; - Position &position = range.start; - position.line = 0; - position.character = 0; - Position &positionEnd = range.end; - positionEnd.line = 1; - diagnostic.message = - "qmlls could not find a build directory, without a build directory " - "containing a current build there could be spurious warnings, you might " - "want to pass the --build-dir <buildDir> option to qmlls, or set the " - "environment variable QMLLS_BUILD_DIRS."; - diagnostic.source = QByteArray("qmllint"); - diagnostics.append(diagnostic); - } - } - } - PublishDiagnosticsParams diagnosticParams; - diagnosticParams.uri = url; - diagnosticParams.diagnostics = diagnostics; - diagnosticParams.version = version; - - m_server->protocol()->notifyPublishDiagnostics(diagnosticParams); - qCDebug(lintLog) << "lint" << QString::fromUtf8(url) << "found" - << diagnosticParams.diagnostics.size() << "issues" - << QTypedJson::toJsonValue(diagnosticParams); -} - -} // namespace QmlLsp -QT_END_NAMESPACE diff --git a/tools/qmlls/qmllintsuggestions.h b/tools/qmlls/qmllintsuggestions.h deleted file mode 100644 index 9d738a269f..0000000000 --- a/tools/qmlls/qmllintsuggestions.h +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef QMLLINTSUGGESTIONS_H -#define QMLLINTSUGGESTIONS_H - -#include "qlanguageserver.h" -#include "qqmlcodemodel.h" - -#include <optional> - -QT_BEGIN_NAMESPACE -namespace QmlLsp { -struct LastLintUpdate -{ - std::optional<int> version; - std::optional<QDateTime> invalidUpdatesSince; -}; - -class QmlLintSuggestions : public QLanguageServerModule -{ - Q_OBJECT -public: - QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel); - - QString name() const override { return QLatin1StringView("QmlLint Suggestions"); } -public Q_SLOTS: - void diagnose(const QByteArray &uri); - void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; - void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &) override; - -private: - QMutex m_mutex; - QHash<QByteArray, LastLintUpdate> m_lastUpdate; - QLanguageServer *m_server; - QmlLsp::QQmlCodeModel *m_codeModel; -}; -} // namespace QmlLsp -QT_END_NAMESPACE -#endif // QMLLINTSUGGESTIONS_H diff --git a/tools/qmlls/qqmlcodemodel.cpp b/tools/qmlls/qqmlcodemodel.cpp deleted file mode 100644 index 9c3521aa57..0000000000 --- a/tools/qmlls/qqmlcodemodel.cpp +++ /dev/null @@ -1,700 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "qqmllanguageserver.h" -#include "qqmlcodemodel.h" -#include <QtCore/qfileinfo.h> -#include <QtCore/qdir.h> -#include <QtCore/qthreadpool.h> -#include <QtCore/qlibraryinfo.h> -#include <QtQmlDom/private/qqmldomtop_p.h> -#include "textdocument.h" - -#include <memory> -#include <algorithm> - -QT_BEGIN_NAMESPACE - -namespace QmlLsp { - -Q_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel") - -using namespace QQmlJS::Dom; -using namespace Qt::StringLiterals; - -/*! -\internal -\class QQmlCodeModel - -The code model offers a view of the current state of the current files, and traks open files. -All methods are threadsafe, and generally return immutable or threadsafe objects that can be -worked on from any thread (unless otherwise noted). -The idea is the let all other operations be as lock free as possible, concentrating all tricky -synchronization here. - -\section2 Global views -\list -\li currentEnv() offers a view that contains the latest version of all the loaded files -\li validEnv() is just like current env but stores only the valid (meaning correctly parsed, - not necessarily without errors) version of a file, it is normally a better choice to load the - dependencies/symbol information from -\endlist - -\section2 OpenFiles -\list -\li snapshotByUrl() returns an OpenDocumentSnapshot of an open document. From it you can get the - document, its latest valid version, scope, all connected to a specific version of the document - and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for - every partial change: document change, validDocument change, scope change). -\li openDocumentByUrl() is a lower level and more intrusive access to OpenDocument objects. These - contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the - current version of the document, and has line by line support. - Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex() - before *any* read or write/modification to it. - It has a version nuber which is supposed to always change and increase. - It is mainly used for highlighting/indenting, and is immediately updated when the user edits a - document. Its use should be avoided if possible, preferring the snapshots. -\endlist - -\section2 Parallelism/Theading -Most operations are not parallel and usually take place in the main thread (but are still thread -safe). -There are two main task that are executed in parallel: Indexing, and OpenDocumentUpdate. -Indexing is meant to keep the global view up to date. -OpenDocumentUpdate keeps the snapshots of the open documents up to date. - -There is always a tension between being responsive, using all threads available, and avoid to hog -too many resources. One can choose different parallelization strategies, we went with a flexiable -approach. -We have (private) functions that execute part of the work: indexSome() and openUpdateSome(). These -do all locking needed, get some work, do it without locks, and at the end update the state of the -code model. If there is more work, then they return true. Thus while (xxxSome()); works until there -is no work left. - -addDirectoriesToIndex(), the internal addDirectory() and addOpenToUpdate() add more work to do. - -indexNeedsUpdate() and openNeedUpdate(), check if there is work to do, and if yes ensure that a -worker thread (or more) that work on it exist. -*/ - -QQmlCodeModel::QQmlCodeModel(QObject *parent, QQmlToolingSettings *settings) - : QObject { parent }, - m_currentEnv(std::make_shared<DomEnvironment>( - QStringList(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)), - DomEnvironment::Option::SingleThreaded)), - m_validEnv(std::make_shared<DomEnvironment>( - QStringList(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)), - DomEnvironment::Option::SingleThreaded)), - m_settings(settings) -{ -} - -QQmlCodeModel::~QQmlCodeModel() -{ - while (true) { - bool shouldWait; - { - QMutexLocker l(&m_mutex); - m_state = State::Stopping; - m_openDocumentsToUpdate.clear(); - shouldWait = m_nIndexInProgress != 0 || m_nUpdateInProgress != 0; - } - if (!shouldWait) - break; - QThread::yieldCurrentThread(); - } -} - -OpenDocumentSnapshot QQmlCodeModel::snapshotByUrl(const QByteArray &url) -{ - return openDocumentByUrl(url).snapshot; -} - -int QQmlCodeModel::indexEvalProgress() const -{ - Q_ASSERT(!m_mutex.tryLock()); // should be called while locked - const int dirCost = 10; - int costToDo = 1; - for (const ToIndex &el : std::as_const(m_toIndex)) - costToDo += dirCost * el.leftDepth; - costToDo += m_indexInProgressCost; - return m_indexDoneCost * 100 / (costToDo + m_indexDoneCost); -} - -void QQmlCodeModel::indexStart() -{ - Q_ASSERT(!m_mutex.tryLock()); // should be called while locked - qCDebug(codeModelLog) << "indexStart"; -} - -void QQmlCodeModel::indexEnd() -{ - Q_ASSERT(!m_mutex.tryLock()); // should be called while locked - qCDebug(codeModelLog) << "indexEnd"; - m_lastIndexProgress = 0; - m_nIndexInProgress = 0; - m_toIndex.clear(); - m_indexInProgressCost = 0; - m_indexDoneCost = 0; -} - -void QQmlCodeModel::indexSendProgress(int progress) -{ - if (progress <= m_lastIndexProgress) - return; - m_lastIndexProgress = progress; - // ### actually send progress -} - -bool QQmlCodeModel::indexCancelled() -{ - QMutexLocker l(&m_mutex); - if (m_state == State::Stopping) - return true; - return false; -} - -void QQmlCodeModel::indexDirectory(const QString &path, int depthLeft) -{ - if (indexCancelled()) - return; - QDir dir(path); - if (depthLeft > 1) { - const QStringList dirs = - dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); - for (const QString &child : dirs) - addDirectory(dir.filePath(child), --depthLeft); - } - const QStringList qmljs = dir.entryList(QStringList({ "*.qml", "*.js", "*.mjs" }), QDir::Files); - int progress = 0; - { - QMutexLocker l(&m_mutex); - m_indexInProgressCost += qmljs.size(); - progress = indexEvalProgress(); - } - indexSendProgress(progress); - if (qmljs.isEmpty()) - return; - DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item(); - for (const QString &file : qmljs) { - if (indexCancelled()) - return; - QString fPath = dir.filePath(file); - QFileInfo fInfo(fPath); - QString cPath = fInfo.canonicalFilePath(); - if (!cPath.isEmpty()) { - newCurrent.loadBuiltins(); - newCurrent.loadFile(cPath, fPath, [](Path, DomItem &, DomItem &) {}, {}); - newCurrent.loadPendingDependencies(); - newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>()); - } - { - QMutexLocker l(&m_mutex); - ++m_indexDoneCost; - --m_indexInProgressCost; - progress = indexEvalProgress(); - } - indexSendProgress(progress); - } -} - -void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server) -{ - Q_UNUSED(server); - // ### create progress, &scan in a separate instance - const int maxDepth = 5; - for (const auto &path : paths) - addDirectory(path, maxDepth); - indexNeedsUpdate(); -} - -void QQmlCodeModel::addDirectory(const QString &path, int depthLeft) -{ - if (depthLeft < 1) - return; - { - QMutexLocker l(&m_mutex); - for (auto it = m_toIndex.begin(); it != m_toIndex.end();) { - if (it->path.startsWith(path)) { - if (it->path.size() == path.size()) - return; - if (it->path.at(path.size()) == u'/') { - it = m_toIndex.erase(it); - continue; - } - } else if (path.startsWith(it->path) && path.at(it->path.size()) == u'/') - return; - ++it; - } - m_toIndex.append({ path, depthLeft }); - } -} - -void QQmlCodeModel::removeDirectory(const QString &path) -{ - { - QMutexLocker l(&m_mutex); - auto toRemove = [path](const QString &p) { - return p.startsWith(path) && (p.size() == path.size() || p.at(path.size()) == u'/'); - }; - auto it = m_toIndex.begin(); - auto end = m_toIndex.end(); - while (it != end) { - if (toRemove(it->path)) - it = m_toIndex.erase(it); - else - ++it; - } - } - if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>()) - validEnvPtr->removePath(path); - if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>()) - currentEnvPtr->removePath(path); -} - -QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options) -{ - QString res; - { - QMutexLocker l(&m_mutex); - res = m_url2path.value(url); - } - if (!res.isEmpty() && options == UrlLookup::Caching) - return res; - QUrl qurl(QString::fromUtf8(url)); - QFileInfo f(qurl.toLocalFile()); - QString cPath = f.canonicalFilePath(); - if (cPath.isEmpty()) - cPath = f.filePath(); - { - QMutexLocker l(&m_mutex); - if (!res.isEmpty() && res != cPath) - m_path2url.remove(res); - m_url2path.insert(url, cPath); - m_path2url.insert(cPath, url); - } - return cPath; -} - -void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText) -{ - { - QMutexLocker l(&m_mutex); - auto &openDoc = m_openDocuments[url]; - if (!openDoc.textDocument) - openDoc.textDocument = std::make_shared<Utils::TextDocument>(); - QMutexLocker l2(openDoc.textDocument->mutex()); - openDoc.textDocument->setVersion(version); - openDoc.textDocument->setPlainText(docText); - } - addOpenToUpdate(url); - openNeedUpdate(); -} - -OpenDocument QQmlCodeModel::openDocumentByUrl(const QByteArray &url) -{ - QMutexLocker l(&m_mutex); - return m_openDocuments.value(url); -} - -void QQmlCodeModel::indexNeedsUpdate() -{ - const int maxIndexThreads = 1; - { - QMutexLocker l(&m_mutex); - if (m_toIndex.isEmpty() || m_nIndexInProgress >= maxIndexThreads) - return; - if (++m_nIndexInProgress == 1) - indexStart(); - } - QThreadPool::globalInstance()->start([this]() { - while (indexSome()) { } - }); -} - -bool QQmlCodeModel::indexSome() -{ - qCDebug(codeModelLog) << "indexSome"; - ToIndex toIndex; - { - QMutexLocker l(&m_mutex); - if (m_toIndex.isEmpty()) { - if (--m_nIndexInProgress == 0) - indexEnd(); - return false; - } - toIndex = m_toIndex.last(); - m_toIndex.removeLast(); - } - bool hasMore = false; - { - auto guard = qScopeGuard([this, &hasMore]() { - QMutexLocker l(&m_mutex); - if (m_toIndex.isEmpty()) { - if (--m_nIndexInProgress == 0) - indexEnd(); - hasMore = false; - } else { - hasMore = true; - } - }); - indexDirectory(toIndex.path, toIndex.leftDepth); - } - return hasMore; -} - -void QQmlCodeModel::openNeedUpdate() -{ - qCDebug(codeModelLog) << "openNeedUpdate"; - const int maxIndexThreads = 1; - { - QMutexLocker l(&m_mutex); - if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxIndexThreads) - return; - if (++m_nUpdateInProgress == 1) - openUpdateStart(); - } - QThreadPool::globalInstance()->start([this]() { - while (openUpdateSome()) { } - }); -} - -bool QQmlCodeModel::openUpdateSome() -{ - qCDebug(codeModelLog) << "openUpdateSome start"; - QByteArray toUpdate; - { - QMutexLocker l(&m_mutex); - if (m_openDocumentsToUpdate.isEmpty()) { - if (--m_nUpdateInProgress == 0) - openUpdateEnd(); - return false; - } - auto it = m_openDocumentsToUpdate.find(m_lastOpenDocumentUpdated); - auto end = m_openDocumentsToUpdate.end(); - if (it == end) - it = m_openDocumentsToUpdate.begin(); - else if (++it == end) - it = m_openDocumentsToUpdate.begin(); - toUpdate = *it; - m_openDocumentsToUpdate.erase(it); - } - bool hasMore = false; - { - auto guard = qScopeGuard([this, &hasMore]() { - QMutexLocker l(&m_mutex); - if (m_openDocumentsToUpdate.isEmpty()) { - if (--m_nUpdateInProgress == 0) - openUpdateEnd(); - hasMore = false; - } else { - hasMore = true; - } - }); - openUpdate(toUpdate); - } - return hasMore; -} - -void QQmlCodeModel::openUpdateStart() -{ - qCDebug(codeModelLog) << "openUpdateStart"; -} - -void QQmlCodeModel::openUpdateEnd() -{ - qCDebug(codeModelLog) << "openUpdateEnd"; -} - -void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText) -{ - qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "(" - << docText.size() << "chars)"; - DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item(); - QStringList loadPaths = buildPathsForFileUrl(url); - loadPaths.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)); - if (std::shared_ptr<DomEnvironment> newCurrentPtr = newCurrent.ownerAs<DomEnvironment>()) { - newCurrentPtr->setLoadPaths(loadPaths); - } - QString fPath = url2Path(url, UrlLookup::ForceLookup); - Path p; - newCurrent.loadFile( - fPath, fPath, docText, QDateTime::currentDateTimeUtc(), - [&p](Path, DomItem &, DomItem &newValue) { p = newValue.fileObject().canonicalPath(); }, - {}); - newCurrent.loadPendingDependencies(); - if (p) { - newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>()); - DomItem item = m_currentEnv.path(p); - { - QMutexLocker l(&m_mutex); - OpenDocument &doc = m_openDocuments[url]; - if (!doc.textDocument) { - qCWarning(lspServerLog) - << "ignoring update to closed document" << QString::fromUtf8(url); - return; - } else { - QMutexLocker l(doc.textDocument->mutex()); - if (doc.textDocument->version() && *doc.textDocument->version() > version) { - qCWarning(lspServerLog) - << "docUpdate: version" << version << "of document" - << QString::fromUtf8(url) << "is not the latest anymore"; - return; - } - } - if (!doc.snapshot.docVersion || *doc.snapshot.docVersion < version) { - doc.snapshot.docVersion = version; - doc.snapshot.doc = item; - } else { - qCWarning(lspServerLog) << "skipping update of current doc to obsolete version" - << version << "of document" << QString::fromUtf8(url); - } - if (item.field(Fields::isValid).value().toBool(false)) { - if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) { - DomItem vDoc = m_validEnv.path(p); - doc.snapshot.validDocVersion = version; - doc.snapshot.validDoc = vDoc; - } else { - qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version" - << version << "of document" << QString::fromUtf8(url); - } - } else { - qCWarning(lspServerLog) - << "avoid update of validDoc to " << version << "of document" - << QString::fromUtf8(url) << "as it is invalid"; - } - } - } - if (codeModelLog().isDebugEnabled()) { - qCDebug(codeModelLog) << "finished update doc of " << url << "to version" << version; - snapshotByUrl(url).dump(qDebug() << "postSnapshot", - OpenDocumentSnapshot::DumpOption::AllCode); - } - // we should update the scope in the future thus call addOpen(url) - emit updatedSnapshot(url); -} - -void QQmlCodeModel::closeOpenFile(const QByteArray &url) -{ - QMutexLocker l(&m_mutex); - m_openDocuments.remove(url); -} - -void QQmlCodeModel::setRootUrls(const QList<QByteArray> &urls) -{ - QMutexLocker l(&m_mutex); - m_rootUrls = urls; -} - -void QQmlCodeModel::addRootUrls(const QList<QByteArray> &urls) -{ - QMutexLocker l(&m_mutex); - for (const QByteArray &url : urls) { - if (!m_rootUrls.contains(url)) - m_rootUrls.append(url); - } -} - -void QQmlCodeModel::removeRootUrls(const QList<QByteArray> &urls) -{ - QMutexLocker l(&m_mutex); - for (const QByteArray &url : urls) - m_rootUrls.removeOne(url); -} - -QList<QByteArray> QQmlCodeModel::rootUrls() const -{ - QMutexLocker l(&m_mutex); - return m_rootUrls; -} - -QStringList QQmlCodeModel::buildPathsForRootUrl(const QByteArray &url) -{ - QMutexLocker l(&m_mutex); - return m_buildPathsForRootUrl.value(url); -} - -static bool isNotSeparator(char c) -{ - return c != '/'; -} - -QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url) -{ - QList<QByteArray> roots; - { - QMutexLocker l(&m_mutex); - roots = m_buildPathsForRootUrl.keys(); - } - // we want to longest match to be first, as it should override shorter matches - std::sort(roots.begin(), roots.end(), [](const QByteArray &el1, const QByteArray &el2) { - if (el1.size() > el2.size()) - return true; - if (el1.size() < el2.size()) - return false; - return el1 < el2; - }); - QStringList buildPaths; - QStringList defaultValues; - if (!roots.isEmpty() && roots.last().isEmpty()) - roots.removeLast(); - QByteArray urlSlash(url); - if (!urlSlash.isEmpty() && isNotSeparator(urlSlash.at(urlSlash.size() - 1))) - urlSlash.append('/'); - // look if the file has a know prefix path - for (const QByteArray &root : roots) { - if (urlSlash.startsWith(root)) { - buildPaths += buildPathsForRootUrl(root); - break; - } - } - QString path = url2Path(url); - if (buildPaths.isEmpty() && m_settings) { - // look in the settings - m_settings->search(path); - QString buildDir = QStringLiteral(u"buildDir"); - if (m_settings->isSet(buildDir)) - buildPaths += m_settings->value(buildDir).toString().split(',', Qt::SkipEmptyParts); - } - if (buildPaths.isEmpty()) { - // default values - buildPaths += buildPathsForRootUrl(QByteArray()); - } - // env variable - QStringList envPaths = qEnvironmentVariable("QMLLS_BUILD_DIRS").split(',', Qt::SkipEmptyParts); - buildPaths += envPaths; - if (buildPaths.isEmpty()) { - // heuristic to find build dir - QDir d(path); - d.setNameFilters(QStringList({ u"build*"_s })); - const int maxDirDepth = 8; - int iDir = maxDirDepth; - QString dirName = d.dirName(); - QDateTime lastModified; - while (d.cdUp() && --iDir > 0) { - for (const QFileInfo &fInfo : d.entryInfoList(QDir::Dirs)) { - if (fInfo.completeBaseName() == u"build" - || fInfo.completeBaseName().startsWith(u"build-%1"_s.arg(dirName))) { - if (iDir > 1) - iDir = 1; - if (!lastModified.isValid() || lastModified < fInfo.lastModified()) { - buildPaths.clear(); - buildPaths.append(fInfo.absoluteFilePath()); - } - } - } - } - } - // add dependent build directories - QStringList res; - std::reverse(buildPaths.begin(), buildPaths.end()); - const int maxDeps = 4; - while (!buildPaths.isEmpty()) { - QString bPath = buildPaths.last(); - buildPaths.removeLast(); - res += bPath; - if (QFile::exists(bPath + u"/_deps") && bPath.split(u"/_deps/"_s).size() < maxDeps) { - QDir d(bPath + u"/_deps"); - for (const QFileInfo &fInfo : d.entryInfoList(QDir::Dirs)) - buildPaths.append(fInfo.absoluteFilePath()); - } - } - return res; -} - -void QQmlCodeModel::setBuildPathsForRootUrl(QByteArray url, const QStringList &paths) -{ - QMutexLocker l(&m_mutex); - if (!url.isEmpty() && isNotSeparator(url.at(url.size() - 1))) - url.append('/'); - if (paths.isEmpty()) - m_buildPathsForRootUrl.remove(url); - else - m_buildPathsForRootUrl.insert(url, paths); -} - -void QQmlCodeModel::openUpdate(const QByteArray &url) -{ - bool updateDoc = false; - bool updateScope = false; - std::optional<int> rNow = 0; - QString docText; - DomItem validDoc; - std::shared_ptr<Utils::TextDocument> document; - { - QMutexLocker l(&m_mutex); - OpenDocument &doc = m_openDocuments[url]; - document = doc.textDocument; - if (!document) - return; - { - QMutexLocker l2(document->mutex()); - rNow = document->version(); - } - if (rNow && (!doc.snapshot.docVersion || *doc.snapshot.docVersion != *rNow)) - updateDoc = true; - else if (doc.snapshot.validDocVersion - && (!doc.snapshot.scopeVersion - || *doc.snapshot.scopeVersion != *doc.snapshot.validDocVersion)) - updateScope = true; - else - return; - if (updateDoc) { - QMutexLocker l2(doc.textDocument->mutex()); - rNow = doc.textDocument->version(); - docText = doc.textDocument->toPlainText(); - } else { - validDoc = doc.snapshot.validDoc; - rNow = doc.snapshot.validDocVersion; - } - } - if (updateDoc) { - newDocForOpenFile(url, *rNow, docText); - } - if (updateScope) { - // to do - } -} - -void QQmlCodeModel::addOpenToUpdate(const QByteArray &url) -{ - QMutexLocker l(&m_mutex); - m_openDocumentsToUpdate.insert(url); -} - -QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options) -{ - dbg.noquote().nospace() << "{"; - dbg << " url:" << QString::fromUtf8(url) << "\n"; - dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n"; - if (options & DumpOption::LatestCode) { - dbg << " doc: ------------\n" - << doc.field(Fields::code).value().toString() << "\n==========\n"; - } else { - dbg << u" doc:" - << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size()) - : u"*none*"_s) - << "\n"; - } - dbg << " validDocVersion:" - << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n"; - if (options & DumpOption::ValidCode) { - dbg << " validDoc: ------------\n" - << validDoc.field(Fields::code).value().toString() << "\n==========\n"; - } else { - dbg << u" validDoc:" - << (validDoc ? u"%1chars"_s.arg( - validDoc.field(Fields::code).value().toString().size()) - : u"*none*"_s) - << "\n"; - } - dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_s) - << "\n"; - dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n"; - dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n"; - dbg << "}"; - return dbg; -} - -} // namespace QmlLsp - -QT_END_NAMESPACE diff --git a/tools/qmlls/qqmlcodemodel.h b/tools/qmlls/qqmlcodemodel.h deleted file mode 100644 index 549f7dd1a1..0000000000 --- a/tools/qmlls/qqmlcodemodel.h +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef QQMLCODEMODEL_H -#define QQMLCODEMODEL_H - -#include <QObject> -#include <QHash> -#include <QtQmlDom/private/qqmldomitem_p.h> -#include <QtQmlCompiler/private/qqmljsscope_p.h> -#include "qlanguageserver_p.h" -#include "textdocument.h" -#include "../shared/qqmltoolingsettings.h" - -#include <functional> -#include <memory> - -QT_BEGIN_NAMESPACE -class TextSynchronization; -namespace QmlLsp { - -class OpenDocumentSnapshot -{ -public: - enum class DumpOption { - NoCode = 0, - LatestCode = 0x1, - ValidCode = 0x2, - AllCode = LatestCode | ValidCode - }; - Q_DECLARE_FLAGS(DumpOptions, DumpOption) - QStringList searchPath; - QByteArray url; - std::optional<int> docVersion; - QQmlJS::Dom::DomItem doc; - std::optional<int> validDocVersion; - QQmlJS::Dom::DomItem validDoc; - std::optional<int> scopeVersion; - QDateTime scopeDependenciesLoadTime; - bool scopeDependenciesChanged = false; - QQmlJSScope::Ptr scope = {}; - QDebug dump(QDebug dbg, DumpOptions dump = DumpOption::NoCode); -}; - -Q_DECLARE_OPERATORS_FOR_FLAGS(OpenDocumentSnapshot::DumpOptions) - -class OpenDocument -{ -public: - OpenDocumentSnapshot snapshot; - std::shared_ptr<Utils::TextDocument> textDocument; -}; - -struct ToIndex -{ - QString path; - int leftDepth; -}; - -class QQmlCodeModel : public QObject -{ - Q_OBJECT -public: - enum class UrlLookup { Caching, ForceLookup }; - enum class State { Running, Stopping }; - - explicit QQmlCodeModel(QObject *parent = nullptr, QQmlToolingSettings *settings = nullptr); - ~QQmlCodeModel(); - QQmlJS::Dom::DomItem currentEnv(); - QQmlJS::Dom::DomItem validEnv(); - OpenDocumentSnapshot snapshotByUrl(const QByteArray &url); - OpenDocument openDocumentByUrl(const QByteArray &url); - - void openNeedUpdate(); - void indexNeedsUpdate(); - void addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server); - void addOpenToUpdate(const QByteArray &); - void removeDirectory(const QString &path); - // void updateDocument(const OpenDocument &doc); - QString url2Path(const QByteArray &url, UrlLookup options = UrlLookup::Caching); - void newOpenFile(const QByteArray &url, int version, const QString &docText); - void newDocForOpenFile(const QByteArray &url, int version, const QString &docText); - void closeOpenFile(const QByteArray &url); - void setRootUrls(const QList<QByteArray> &urls); - QList<QByteArray> rootUrls() const; - void addRootUrls(const QList<QByteArray> &urls); - QStringList buildPathsForRootUrl(const QByteArray &url); - QStringList buildPathsForFileUrl(const QByteArray &url); - void setBuildPathsForRootUrl(QByteArray url, const QStringList &paths); - void removeRootUrls(const QList<QByteArray> &urls); - QQmlToolingSettings *settings(); -Q_SIGNALS: - void updatedSnapshot(const QByteArray &url); -private: - void indexDirectory(const QString &path, int depthLeft); - int indexEvalProgress() const; // to be called in the mutex - void indexStart(); // to be called in the mutex - void indexEnd(); // to be called in the mutex - void indexSendProgress(int progress); - bool indexCancelled(); - bool indexSome(); - void addDirectory(const QString &path, int leftDepth); - bool openUpdateSome(); - void openUpdateStart(); - void openUpdateEnd(); - void openUpdate(const QByteArray &); - mutable QMutex m_mutex; - State m_state = State::Running; - int m_lastIndexProgress = 0; - int m_nIndexInProgress = 0; - QList<ToIndex> m_toIndex; - int m_indexInProgressCost = 0; - int m_indexDoneCost = 0; - int m_nUpdateInProgress = 0; - QQmlJS::Dom::DomItem m_currentEnv; - QQmlJS::Dom::DomItem m_validEnv; - QByteArray m_lastOpenDocumentUpdated; - QSet<QByteArray> m_openDocumentsToUpdate; - QHash<QByteArray, QStringList> m_buildPathsForRootUrl; - QList<QByteArray> m_rootUrls; - QHash<QByteArray, QString> m_url2path; - QHash<QString, QByteArray> m_path2url; - QHash<QByteArray, OpenDocument> m_openDocuments; - QQmlToolingSettings *m_settings; -}; - -} // namespace QmlLsp -QT_END_NAMESPACE -#endif // QQMLCODEMODEL_H diff --git a/tools/qmlls/qqmllanguageserver.cpp b/tools/qmlls/qqmllanguageserver.cpp deleted file mode 100644 index 48639f37d2..0000000000 --- a/tools/qmlls/qqmllanguageserver.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "qqmllanguageserver.h" - -#include "textsynchronization.h" - -#include "qlanguageserver.h" -#include "lspcustomtypes.h" -#include <QtCore/qdir.h> - -#include <iostream> -#include <algorithm> - -QT_BEGIN_NAMESPACE - -namespace QmlLsp { - -using namespace QLspSpecification; -using namespace Qt::StringLiterals; -/*! -\internal -\class QmlLsp::QQmlLanguageServer -\brief Class that sets up a QmlLanguageServer - -This class sets up a QML language server. -It needs a function -\code -std::function<void(const QByteArray &)> sendData -\endcode -to send out its replies, and one should feed the data it receives to the server()->receive() method. -It is expected to call this method only from a single thread, and not to block, the simplest way to -achieve this is to avoid direct calls, and connect it as slot, while reading from another thread. - -The Server is build with separate QLanguageServerModule that implement a given functionality, and -all of them are constructed and registered with the QLanguageServer in the constructor o this class. - -Generally all operations are expected to be done in the object thread, and handlers are always -called from it, but they are free to delegate the response to another thread, the response handler -is thread safe. All the methods of the server() obect are also threadsafe. - -The code model starts other threads to update its state, see its documentation for more information. -*/ -QQmlLanguageServer::QQmlLanguageServer(std::function<void(const QByteArray &)> sendData, - QQmlToolingSettings *settings) - : m_codeModel(nullptr, settings), - m_server(sendData), - m_textSynchronization(&m_codeModel), - m_lint(&m_server, &m_codeModel), - m_workspace(&m_codeModel), - m_completionSupport(&m_codeModel) -{ - m_server.addServerModule(this); - m_server.addServerModule(&m_textSynchronization); - m_server.addServerModule(&m_lint); - m_server.addServerModule(&m_workspace); - m_server.addServerModule(&m_completionSupport); - m_server.finishSetup(); - qCWarning(lspServerLog) << "Did Setup"; -} - -void QQmlLanguageServer::registerHandlers(QLanguageServer *server, - QLanguageServerProtocol *protocol) -{ - Q_UNUSED(protocol); - QObject::connect(server, &QLanguageServer::lifecycleError, this, - &QQmlLanguageServer::errorExit); - QObject::connect(server, &QLanguageServer::exit, this, &QQmlLanguageServer::exit); - QObject::connect(server, &QLanguageServer::runStatusChanged, [](QLanguageServer::RunStatus r) { - qCDebug(lspServerLog) << "runStatus" << int(r); - }); - protocol->typedRpc()->registerNotificationHandler<Notifications::AddBuildDirsParams>( - QByteArray(Notifications::AddBuildDirsMethod), - [this](const QByteArray &, const Notifications::AddBuildDirsParams ¶ms) { - for (const auto &buildDirs : params.buildDirsToSet) { - QStringList dirPaths; - dirPaths.resize(buildDirs.buildDirs.size()); - std::transform(buildDirs.buildDirs.begin(), buildDirs.buildDirs.end(), - dirPaths.begin(), [](const QByteArray &utf8Str) { - return QString::fromUtf8(utf8Str); - }); - m_codeModel.setBuildPathsForRootUrl(buildDirs.baseUri, dirPaths); - } - }); -} - -void QQmlLanguageServer::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &serverInfo) -{ - Q_UNUSED(clientInfo); - QJsonObject expCap; - if (serverInfo.capabilities.experimental.has_value() && serverInfo.capabilities.experimental->isObject()) - expCap = serverInfo.capabilities.experimental->toObject(); - expCap.insert(u"addBuildDirs"_s, QJsonObject({ { u"supported"_s, true } })); - serverInfo.capabilities.experimental = expCap; -} - -QString QQmlLanguageServer::name() const -{ - return u"QQmlLanguageServer"_s; -} - -void QQmlLanguageServer::errorExit() -{ - qCWarning(lspServerLog) << "Error exit"; - fclose(stdin); -} - -void QQmlLanguageServer::exit() -{ - m_returnValue = 0; - fclose(stdin); -} - -int QQmlLanguageServer::returnValue() const -{ - return m_returnValue; -} - -QQmlCodeModel *QQmlLanguageServer::codeModel() -{ - return &m_codeModel; -} - -QLanguageServer *QQmlLanguageServer::server() -{ - return &m_server; -} - -TextSynchronization *QQmlLanguageServer::textSynchronization() -{ - return &m_textSynchronization; -} - -QmlLintSuggestions *QQmlLanguageServer::lint() -{ - return &m_lint; -} - -WorkspaceHandlers *QQmlLanguageServer::worspace() -{ - return &m_workspace; -} - -} // namespace QmlLsp - -QT_END_NAMESPACE diff --git a/tools/qmlls/qqmllanguageserver.h b/tools/qmlls/qqmllanguageserver.h deleted file mode 100644 index 7efe93990a..0000000000 --- a/tools/qmlls/qqmllanguageserver.h +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef QQMLLANGUAGESERVER_H -#define QQMLLANGUAGESERVER_H - -#include "qlanguageserver_p.h" -#include "qqmlcodemodel.h" -#include "textsynchronization.h" -#include "qmllintsuggestions.h" -#include "workspace.h" -#include "qmlcompletionsupport.h" -#include "../shared/qqmltoolingsettings.h" - -QT_BEGIN_NAMESPACE -namespace QmlLsp { - -/* - * The language server protocol calls "URI" what QML calls "URL". - * According to RFC 3986, a URL is a special case of URI that not only - * identifies a resource but also shows how to access it. - * In QML, however, URIs are distinct from URLs. URIs are the - * identifiers of modules, for example "QtQuick.Controls". - * In order to not confuse the terms we interpret language server URIs - * as URLs in the QML code model. - * This method marks a point of translation between the terms, but does - * not have to change the actual URI/URL. - */ -inline QByteArray lspUriToQmlUrl(const QByteArray &uri) { return uri; } - -class QQmlLanguageServer : public QLanguageServerModule -{ - Q_OBJECT -public: - QQmlLanguageServer(std::function<void(const QByteArray &)> sendData, - QQmlToolingSettings *settings = nullptr); - - QString name() const final; - void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) final; - void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &serverInfo) final; - - int returnValue() const; - - QQmlCodeModel *codeModel(); - QLanguageServer *server(); - TextSynchronization *textSynchronization(); - QmlLintSuggestions *lint(); - WorkspaceHandlers *worspace(); - -public Q_SLOTS: - void exit(); - void errorExit(); - -private: - QQmlCodeModel m_codeModel; - QLanguageServer m_server; - TextSynchronization m_textSynchronization; - QmlLintSuggestions m_lint; - WorkspaceHandlers m_workspace; - QmlCompletionSupport m_completionSupport; - int m_returnValue = 1; -}; - -} // namespace QmlLsp -QT_END_NAMESPACE -#endif // QQMLLANGUAGESERVER_H diff --git a/tools/qmlls/textblock.cpp b/tools/qmlls/textblock.cpp deleted file mode 100644 index 04a228922e..0000000000 --- a/tools/qmlls/textblock.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "textblock.h" -#include "textdocument.h" - -#include <QtCore/qstring.h> - -namespace Utils { - -bool TextBlock::isValid() const -{ - return m_document; -} - -void TextBlock::setBlockNumber(int blockNumber) -{ - m_blockNumber = blockNumber; -} - -int TextBlock::blockNumber() const -{ - return m_blockNumber; -} - -void TextBlock::setPosition(int position) -{ - m_position = position; -} - -int TextBlock::position() const -{ - return m_position; -} - -void TextBlock::setLength(int length) -{ - m_length = length; -} - -int TextBlock::length() const -{ - return m_length; -} - -TextBlock TextBlock::next() const -{ - return m_document->findBlockByNumber(m_blockNumber + 1); -} - -TextBlock TextBlock::previous() const -{ - return m_document->findBlockByNumber(m_blockNumber - 1); -} - -int TextBlock::userState() const -{ - return m_document->userState(m_blockNumber); -} - -void TextBlock::setUserState(int state) -{ - m_document->setUserState(m_blockNumber, state); -} - -void TextBlock::setDocument(TextDocument *document) -{ - m_document = document; -} - -TextDocument *TextBlock::document() const -{ - return m_document; -} - -QString TextBlock::text() const -{ - return document()->toPlainText().mid(position(), length()); -} - -int TextBlock::revision() const -{ - return m_revision; -} - -void TextBlock::setRevision(int rev) -{ - m_revision = rev; -} - -bool operator==(const TextBlock &t1, const TextBlock &t2) -{ - return t1.document() == t2.document() && t1.blockNumber() == t2.blockNumber(); -} - -bool operator!=(const TextBlock &t1, const TextBlock &t2) -{ - return !(t1 == t2); -} - -} // namespace Utils diff --git a/tools/qmlls/textblock.h b/tools/qmlls/textblock.h deleted file mode 100644 index 593aa88daf..0000000000 --- a/tools/qmlls/textblock.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef TEXTBLOCK_H -#define TEXTBLOCK_H - -#include <QtCore/qstring.h> - -namespace Utils { - -class TextDocument; -class TextBlockUserData; - -class TextBlock -{ -public: - bool isValid() const; - - void setBlockNumber(int blockNumber); - int blockNumber() const; - - void setPosition(int position); - int position() const; - - void setLength(int length); - int length() const; - - TextBlock next() const; - TextBlock previous() const; - - int userState() const; - void setUserState(int state); - - bool isVisible() const; - void setVisible(bool visible); - - void setLineCount(int count); - int lineCount() const; - - void setDocument(TextDocument *document); - TextDocument *document() const; - - QString text() const; - - int revision() const; - void setRevision(int rev); - - friend bool operator==(const TextBlock &t1, const TextBlock &t2); - friend bool operator!=(const TextBlock &t1, const TextBlock &t2); - -private: - TextDocument *m_document = nullptr; - int m_revision = 0; - - int m_position = 0; - int m_length = 0; - int m_blockNumber = -1; -}; - -} // namespace Utils - -#endif // TEXTBLOCK_H diff --git a/tools/qmlls/textcursor.cpp b/tools/qmlls/textcursor.cpp deleted file mode 100644 index 3a6e7ca080..0000000000 --- a/tools/qmlls/textcursor.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "textcursor.h" -#include "textdocument.h" -#include "textblock.h" - -namespace Utils { - -class TextFrame; -class TextTable; -class TextTableCell; - -TextCursor::TextCursor(TextDocument *document) : m_document(document) { } - -bool TextCursor::movePosition(TextCursor::MoveOperation op, TextCursor::MoveMode mode, int n) -{ - Q_UNUSED(n); - switch (op) { - case NoMove: - return true; - case Start: - m_position = 0; - break; - case PreviousCharacter: - while (--n >= 0) { - if (m_position == 0) - return false; - --m_position; - } - break; - case End: - m_position = m_document->characterCount(); - break; - case NextCharacter: - while (--n >= 0) { - if (m_position == m_document->characterCount()) - return false; - ++m_position; - } - break; - } - - if (mode == MoveAnchor) - m_anchor = m_position; - - return false; -} - -int TextCursor::position() const -{ - return m_position; -} - -void TextCursor::setPosition(int pos, Utils::TextCursor::MoveMode mode) -{ - m_position = pos; - if (mode == MoveAnchor) - m_anchor = pos; -} - -QString TextCursor::selectedText() const -{ - return m_document->toPlainText().mid(qMin(m_position, m_anchor), qAbs(m_position - m_anchor)); -} - -void TextCursor::clearSelection() -{ - m_anchor = m_position; -} - -TextDocument *TextCursor::document() const -{ - return m_document; -} - -void TextCursor::insertText(const QString &text) -{ - const QString orig = m_document->toPlainText(); - const QString left = orig.left(qMin(m_position, m_anchor)); - const QString right = orig.mid(qMax(m_position, m_anchor)); - m_document->setPlainText(left + text + right); -} - -TextBlock TextCursor::block() const -{ - TextBlock current = m_document->firstBlock(); - while (current.isValid()) { - if (current.position() <= position() - && current.position() + current.length() > current.position()) - break; - current = current.next(); - } - return current; -} - -int TextCursor::positionInBlock() const -{ - return m_position - block().position(); -} - -int TextCursor::blockNumber() const -{ - return block().blockNumber(); -} - -void TextCursor::removeSelectedText() -{ - insertText(QString()); -} - -int TextCursor::selectionEnd() const -{ - return qMax(m_position, m_anchor); -} - -bool TextCursor::isNull() const -{ - return m_document == nullptr; -} - -} diff --git a/tools/qmlls/textcursor.h b/tools/qmlls/textcursor.h deleted file mode 100644 index 618cd739c3..0000000000 --- a/tools/qmlls/textcursor.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef TEXTCURSOR_H -#define TEXTCURSOR_H - -#include <QtCore/qstring.h> - -namespace Utils { - -class TextDocument; -class TextBlock; - -class TextCursor -{ -public: - enum MoveOperation { - NoMove, - Start, - PreviousCharacter, - End, - NextCharacter, - }; - - enum MoveMode { MoveAnchor, KeepAnchor }; - - enum SelectionType { Document }; - - TextCursor(); - TextCursor(const TextBlock &block); - TextCursor(TextDocument *document); - - bool movePosition(MoveOperation op, MoveMode = MoveAnchor, int n = 1); - int position() const; - void setPosition(int pos, MoveMode mode = MoveAnchor); - QString selectedText() const; - void clearSelection(); - int anchor() const; - TextDocument *document() const; - void insertText(const QString &text); - TextBlock block() const; - int positionInBlock() const; - int blockNumber() const; - - void select(SelectionType selection); - - bool hasSelection() const; - - void removeSelectedText(); - int selectionEnd() const; - - bool isNull() const; - -private: - TextDocument *m_document = nullptr; - int m_position = 0; - int m_anchor = 0; -}; -} // namespace Utils - -#endif // TEXTCURSOR_H diff --git a/tools/qmlls/textdocument.cpp b/tools/qmlls/textdocument.cpp deleted file mode 100644 index b5d4cc6f68..0000000000 --- a/tools/qmlls/textdocument.cpp +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "textdocument.h" -#include "textblock.h" - -namespace Utils { - -TextDocument::TextDocument(const QString &text) -{ - setPlainText(text); -} - -TextBlock TextDocument::findBlockByNumber(int blockNumber) const -{ - return (blockNumber >= 0 && blockNumber < m_blocks.size()) - ? m_blocks.at(blockNumber).textBlock - : TextBlock(); -} - -TextBlock TextDocument::findBlockByLineNumber(int lineNumber) const -{ - return findBlockByNumber(lineNumber); -} - -QChar TextDocument::characterAt(int pos) const -{ - return m_content.at(pos); -} - -int TextDocument::characterCount() const -{ - return m_content.size(); -} - -TextBlock TextDocument::begin() const -{ - return m_blocks.isEmpty() ? TextBlock() : m_blocks.at(0).textBlock; -} - -TextBlock TextDocument::firstBlock() const -{ - return begin(); -} - -TextBlock TextDocument::lastBlock() const -{ - return m_blocks.isEmpty() ? TextBlock() : m_blocks.last().textBlock; -} - -std::optional<int> TextDocument::version() const -{ - return m_version; -} - -void TextDocument::setVersion(std::optional<int> v) -{ - m_version = v; -} - -QString TextDocument::toPlainText() const -{ - return m_content; -} - -void TextDocument::setPlainText(const QString &text) -{ - m_content = text; - m_blocks.clear(); - - int blockStart = 0; - int blockNumber = 0; - while (blockStart < text.size()) { - Block block; - block.textBlock.setBlockNumber(blockNumber++); - block.textBlock.setPosition(blockStart); - block.textBlock.setDocument(this); - - int blockEnd = text.indexOf('\n', blockStart) + 1; - if (blockEnd == 0) - blockEnd = text.size(); - - block.textBlock.setLength(blockEnd - blockStart); - m_blocks.append(block); - blockStart = blockEnd; - } -} - -bool TextDocument::isModified() const -{ - return m_modified; -} - -void TextDocument::setModified(bool modified) -{ - m_modified = modified; -} - -void TextDocument::setUserState(int blockNumber, int state) -{ - if (blockNumber >= 0 && blockNumber < m_blocks.size()) - m_blocks[blockNumber].userState = state; -} - -int TextDocument::userState(int blockNumber) const -{ - return (blockNumber >= 0 && blockNumber < m_blocks.size()) ? m_blocks[blockNumber].userState - : -1; -} - -QMutex *TextDocument::mutex() const -{ - return &m_mutex; -} - -} // namespace Utils diff --git a/tools/qmlls/textdocument.h b/tools/qmlls/textdocument.h deleted file mode 100644 index e0db66bfb3..0000000000 --- a/tools/qmlls/textdocument.h +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef TEXTDOCUMENT_H -#define TEXTDOCUMENT_H - -#include "textblock.h" - -#include <QtCore/qchar.h> -#include <QtCore/qvector.h> -#include <QtCore/qscopedpointer.h> -#include <QtCore/qmutex.h> - -#include <optional> - -namespace Utils { - -class TextBlockUserData; - -class TextDocument -{ -public: - TextDocument() = default; - explicit TextDocument(const QString &text); - - TextBlock findBlockByNumber(int blockNumber) const; - TextBlock findBlockByLineNumber(int lineNumber) const; - QChar characterAt(int pos) const; - int characterCount() const; - TextBlock begin() const; - TextBlock firstBlock() const; - TextBlock lastBlock() const; - - std::optional<int> version() const; - void setVersion(std::optional<int>); - - QString toPlainText() const; - void setPlainText(const QString &text); - - bool isModified() const; - void setModified(bool modified); - - void setUndoRedoEnabled(bool enable); - - void clear(); - - void setUserState(int blockNumber, int state); - int userState(int blockNumber) const; - QMutex *mutex() const; - -private: - struct Block - { - TextBlock textBlock; - int userState = -1; - }; - - QVector<Block> m_blocks; - - QString m_content; - bool m_modified = false; - std::optional<int> m_version; - mutable QMutex m_mutex; -}; -} // namespace Utils - -#endif // TEXTDOCUMENT_H diff --git a/tools/qmlls/textsynchronization.cpp b/tools/qmlls/textsynchronization.cpp deleted file mode 100644 index dcef4606f7..0000000000 --- a/tools/qmlls/textsynchronization.cpp +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#include "textsynchronization.h" -#include "qqmllanguageserver.h" - -#include "textdocument.h" - -using namespace QLspSpecification; -using namespace Qt::StringLiterals; - -QT_BEGIN_NAMESPACE - -TextSynchronization::TextSynchronization(QmlLsp::QQmlCodeModel *codeModel, QObject *parent) - : QLanguageServerModule(parent), m_codeModel(codeModel) -{ -} - -void TextSynchronization::didCloseTextDocument(const DidCloseTextDocumentParams ¶ms) -{ - m_codeModel->closeOpenFile(QmlLsp::lspUriToQmlUrl(params.textDocument.uri)); -} - -void TextSynchronization::didOpenTextDocument(const DidOpenTextDocumentParams ¶ms) -{ - const TextDocumentItem &item = params.textDocument; - const QString fileName = m_codeModel->url2Path(QmlLsp::lspUriToQmlUrl(item.uri)); - m_codeModel->newOpenFile(QmlLsp::lspUriToQmlUrl(item.uri), item.version, - QString::fromUtf8(item.text)); -} - -void TextSynchronization::didDidChangeTextDocument(const DidChangeTextDocumentParams ¶ms) -{ - QByteArray url = QmlLsp::lspUriToQmlUrl(params.textDocument.uri); - const QString fileName = m_codeModel->url2Path(url); - auto openDoc = m_codeModel->openDocumentByUrl(url); - std::shared_ptr<Utils::TextDocument> document = openDoc.textDocument; - if (!document) { - qCWarning(lspServerLog) << "Ingnoring changes to non open or closed document" - << QString::fromUtf8(url); - return; - } - const auto &changes = params.contentChanges; - { - QMutexLocker l(document->mutex()); - for (const auto &change : changes) { - if (!change.range) { - document->setPlainText(QString::fromUtf8(change.text)); - continue; - } - - const auto &range = *change.range; - const auto &rangeStart = range.start; - const int start = - document->findBlockByNumber(rangeStart.line).position() + rangeStart.character; - const auto &rangeEnd = range.end; - const int end = - document->findBlockByNumber(rangeEnd.line).position() + rangeEnd.character; - - document->setPlainText(document->toPlainText().replace(start, end - start, - QString::fromUtf8(change.text))); - } - document->setVersion(params.textDocument.version); - qCDebug(lspServerLog).noquote() - << "text is\n:----------" << document->toPlainText() << "\n_________"; - } - m_codeModel->addOpenToUpdate(url); - m_codeModel->openNeedUpdate(); -} - -void TextSynchronization::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *) -{ - QObject::connect(server->notifySignals(), - &QLspNotifySignals::receivedDidOpenTextDocumentNotification, this, - &TextSynchronization::didOpenTextDocument); - - QObject::connect(server->notifySignals(), - &QLspNotifySignals::receivedDidChangeTextDocumentNotification, this, - &TextSynchronization::didDidChangeTextDocument); - - QObject::connect(server->notifySignals(), - &QLspNotifySignals::receivedDidCloseTextDocumentNotification, this, - &TextSynchronization::didCloseTextDocument); -} - -QString TextSynchronization::name() const -{ - return u"TextSynchonization"_s; -} - -void TextSynchronization::setupCapabilities(const QLspSpecification::InitializeParams &, - QLspSpecification::InitializeResult &serverInfo) -{ - TextDocumentSyncOptions syncOptions; - syncOptions.openClose = true; - syncOptions.change = TextDocumentSyncKind::Incremental; - serverInfo.capabilities.textDocumentSync = syncOptions; -} - -QT_END_NAMESPACE diff --git a/tools/qmlls/textsynchronization.h b/tools/qmlls/textsynchronization.h deleted file mode 100644 index 69076aa81b..0000000000 --- a/tools/qmlls/textsynchronization.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -#ifndef TEXTSYNCH_H -#define TEXTSYNCH_H - -#include "qqmlcodemodel.h" - -#include "qlanguageserver_p.h" - -QT_BEGIN_NAMESPACE - -class TextSynchronization : public QLanguageServerModule -{ - Q_OBJECT -public: - TextSynchronization(QmlLsp::QQmlCodeModel *codeModel, QObject *parent = nullptr); - QString name() const override; - void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; - void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &) override; - -public Q_SLOTS: - void didOpenTextDocument(const QLspSpecification::DidOpenTextDocumentParams ¶ms); - void didDidChangeTextDocument(const QLspSpecification::DidChangeTextDocumentParams ¶ms); - void didCloseTextDocument(const QLspSpecification::DidCloseTextDocumentParams ¶ms); - -private: - QmlLsp::QQmlCodeModel *m_codeModel; -}; - -QT_END_NAMESPACE -#endif // TEXTSYNCH_H diff --git a/tools/qmlls/workspace.cpp b/tools/qmlls/workspace.cpp deleted file mode 100644 index a18d5f6f8d..0000000000 --- a/tools/qmlls/workspace.cpp +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "workspace.h" -#include "qqmllanguageserver.h" -#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> -#include <QtLanguageServer/private/qlspnotifysignals_p.h> - -#include <QtCore/qfile.h> -#include <variant> - -QT_BEGIN_NAMESPACE -using namespace Qt::StringLiterals; -using namespace QLspSpecification; - -void WorkspaceHandlers::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *) -{ - QObject::connect(server->notifySignals(), - &QLspNotifySignals::receivedDidChangeWorkspaceFoldersNotification, this, - [server, this](const DidChangeWorkspaceFoldersParams ¶ms) { - const WorkspaceFoldersChangeEvent &event = params.event; - - const QList<WorkspaceFolder> &removed = event.removed; - QList<QByteArray> toRemove; - for (const WorkspaceFolder &folder : removed) { - toRemove.append(QmlLsp::lspUriToQmlUrl(folder.uri)); - m_codeModel->removeDirectory( - m_codeModel->url2Path(QmlLsp::lspUriToQmlUrl(folder.uri))); - } - m_codeModel->removeRootUrls(toRemove); - const QList<WorkspaceFolder> &added = event.added; - QList<QByteArray> toAdd; - QStringList pathsToAdd; - for (const WorkspaceFolder &folder : added) { - toAdd.append(QmlLsp::lspUriToQmlUrl(folder.uri)); - pathsToAdd.append( - m_codeModel->url2Path(QmlLsp::lspUriToQmlUrl(folder.uri))); - } - m_codeModel->addRootUrls(toAdd); - m_codeModel->addDirectoriesToIndex(pathsToAdd, server); - }); - - QObject::connect(server->notifySignals(), - &QLspNotifySignals::receivedDidChangeWatchedFilesNotification, this, - [this](const DidChangeWatchedFilesParams ¶ms) { - const QList<FileEvent> &changes = params.changes; - for (const FileEvent &change : changes) { - const QString filename = - m_codeModel->url2Path(QmlLsp::lspUriToQmlUrl(change.uri)); - switch (FileChangeType(change.type)) { - case FileChangeType::Created: - // m_codeModel->addFile(filename); - break; - case FileChangeType::Changed: { - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) - // m_modelManager->setFileContents(filename, file.readAll()); - break; - } - case FileChangeType::Deleted: - // m_modelManager->removeFile(filename); - break; - } - } - // update due to dep changes... - }); - - QObject::connect(server, &QLanguageServer::clientInitialized, this, - &WorkspaceHandlers::clientInitialized); -} - -QString WorkspaceHandlers::name() const -{ - return u"Workspace"_s; -} - -void WorkspaceHandlers::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &serverInfo) -{ - if (!clientInfo.capabilities.workspace - || !clientInfo.capabilities.workspace->value("workspaceFolders").toBool(false)) - return; - WorkspaceFoldersServerCapabilities folders; - folders.supported = true; - folders.changeNotifications = true; - if (!serverInfo.capabilities.workspace) - serverInfo.capabilities.workspace = QJsonObject(); - serverInfo.capabilities.workspace->insert("workspaceFolders", QTypedJson::toJsonValue(folders)); -} - -void WorkspaceHandlers::clientInitialized(QLanguageServer *server) -{ - QLanguageServerProtocol *protocol = server->protocol(); - const auto clientInfo = server->clientInfo(); - QList<Registration> registrations; - if (clientInfo.capabilities.workspace - && clientInfo.capabilities.workspace->value("didChangeWatchedFiles")["dynamicRegistration"] - .toBool(false)) { - const int watchAll = - int(WatchKind::Create) | int(WatchKind::Change) | int(WatchKind::Delete); - DidChangeWatchedFilesRegistrationOptions watchedFilesParams; - FileSystemWatcher qmlWatcher; - qmlWatcher.globPattern = QByteArray("*.{qml,js,mjs}"); - qmlWatcher.kind = watchAll; - FileSystemWatcher qmldirWatcher; - qmldirWatcher.globPattern = "qmldir"; - qmldirWatcher.kind = watchAll; - FileSystemWatcher qmltypesWatcher; - qmltypesWatcher.globPattern = QByteArray("*.qmltypes"); - qmltypesWatcher.kind = watchAll; - watchedFilesParams.watchers = - QList<FileSystemWatcher>({ qmlWatcher, qmldirWatcher, qmltypesWatcher }); - registrations.append(Registration { - // use ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles as id too - ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles, - ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles, - QTypedJson::toJsonValue(watchedFilesParams) }); - } - - if (!registrations.isEmpty()) { - RegistrationParams params; - params.registrations = registrations; - protocol->requestRegistration( - params, - []() { - // successful registration - }, - [protocol](const ResponseError &err) { - LogMessageParams msg; - msg.message = QByteArray("registration of file udates failed, will miss file " - "changes done outside the editor due to error "); - msg.message.append(QString::number(err.code).toUtf8()); - if (!err.message.isEmpty()) - msg.message.append(" "); - msg.message.append(err.message); - msg.type = MessageType::Warning; - qCWarning(lspServerLog) << QString::fromUtf8(msg.message); - protocol->notifyLogMessage(msg); - }); - } - - QSet<QString> rootPaths; - if (std::holds_alternative<QByteArray>(clientInfo.rootUri)) { - QString path = m_codeModel->url2Path( - QmlLsp::lspUriToQmlUrl(std::get<QByteArray>(clientInfo.rootUri))); - rootPaths.insert(path); - } else if (clientInfo.rootPath && std::holds_alternative<QByteArray>(*clientInfo.rootPath)) { - QString path = QString::fromUtf8(std::get<QByteArray>(*clientInfo.rootPath)); - rootPaths.insert(path); - } - - if (clientInfo.workspaceFolders - && std::holds_alternative<QList<WorkspaceFolder>>(*clientInfo.workspaceFolders)) { - for (const WorkspaceFolder &workspace : - std::as_const(std::get<QList<WorkspaceFolder>>(*clientInfo.workspaceFolders))) { - const QUrl workspaceUrl(QString::fromUtf8(QmlLsp::lspUriToQmlUrl(workspace.uri))); - rootPaths.insert(workspaceUrl.toLocalFile()); - } - } - if (m_status == Status::Indexing) - m_codeModel->addDirectoriesToIndex(QStringList(rootPaths.begin(), rootPaths.end()), server); -} - -QT_END_NAMESPACE diff --git a/tools/qmlls/workspace.h b/tools/qmlls/workspace.h deleted file mode 100644 index 03dcf291d1..0000000000 --- a/tools/qmlls/workspace.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#ifndef WORKSPACE_H -#define WORKSPACE_H - -#include "qqmlcodemodel.h" -#include "qlanguageserver.h" - -QT_BEGIN_NAMESPACE - -class WorkspaceHandlers : public QLanguageServerModule -{ - Q_OBJECT -public: - enum class Status { NoIndex, Indexing }; - WorkspaceHandlers(QmlLsp::QQmlCodeModel *codeModel) : m_codeModel(codeModel) { } - QString name() const override; - void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; - void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, - QLspSpecification::InitializeResult &) override; -public Q_SLOTS: - void clientInitialized(QLanguageServer *); - -private: - QmlLsp::QQmlCodeModel *m_codeModel = nullptr; - Status m_status = Status::NoIndex; -}; - -QT_END_NAMESPACE - -#endif // WORKSPACE_H diff --git a/tools/qmlplugindump/main.cpp b/tools/qmlplugindump/main.cpp index eeb8c6cde3..ead1e729b9 100644 --- a/tools/qmlplugindump/main.cpp +++ b/tools/qmlplugindump/main.cpp @@ -30,6 +30,7 @@ #include <QtCore/private/qobject_p.h> #include <QtCore/private/qmetaobject_p.h> #include <QtQmlTypeRegistrar/private/qqmljsstreamwriter_p.h> +#include <QtQml/private/qqmlsignalnames_p.h> #include <QRegularExpression> #include <iostream> @@ -68,13 +69,6 @@ QString inObjectInstantiation; } -static QString enquote(const QString &string) -{ - QString s = string; - return QString("\"%1\"").arg(s.replace(QLatin1Char('\\'), QLatin1String("\\\\")) - .replace(QLatin1Char('"'),QLatin1String("\\\""))); -} - struct QmlVersionInfo { QString pluginImportUri; @@ -268,7 +262,7 @@ QSet<const QMetaObject *> collectReachableMetaObjects(QQmlEngine *engine, QObject *object = nullptr; if (ty.isSingleton()) { - QQmlType::SingletonInstanceInfo *siinfo = ty.singletonInstanceInfo(); + QQmlType::SingletonInstanceInfo::ConstPtr siinfo = ty.singletonInstanceInfo(); if (!siinfo) { std::cerr << "Internal error, " << qPrintable(tyName) << "(" << qPrintable( QString::fromUtf8(ty.typeName()) ) << ")" @@ -278,7 +272,8 @@ QSet<const QMetaObject *> collectReachableMetaObjects(QQmlEngine *engine, if (ty.isQObjectSingleton()) { if (verbose) std::cerr << "Trying to get singleton for " << qPrintable(tyName) - << " (" << qPrintable( siinfo->typeName ) << ")" << std::endl; + << " (" << qPrintable( QString::fromUtf8(siinfo->typeName) ) + << ")" << std::endl; collectReachableMetaObjects(object, &metas, info); object = QQmlEnginePrivate::get(engine)->singletonInstance<QObject*>(ty); } else { @@ -348,7 +343,7 @@ public: relocatableModuleUri = uri; } - QString getExportString(const QQmlType &type, const QmlVersionInfo &versionInfo) + QByteArray getExportString(const QQmlType &type, const QmlVersionInfo &versionInfo) { const QString module = type.module().isEmpty() ? versionInfo.pluginImportUri : type.module(); @@ -358,12 +353,14 @@ public: type.version().hasMinorVersion() ? type.version().minorVersion() : versionInfo.version.minorVersion()); - const QString versionedElement = type.elementName() - + QString::fromLatin1(" %1.%2").arg(version.majorVersion()).arg(version.minorVersion()); + const QByteArray versionedElement + = (type.elementName() + QString::fromLatin1(" %1.%2") + .arg(version.majorVersion()) + .arg(version.minorVersion())).toUtf8(); - return enquote((module == relocatableModuleUri) + return (module == relocatableModuleUri) ? versionedElement - : module + QLatin1Char('/') + versionedElement); + : module.toUtf8() + '/' + versionedElement; } void writeMetaContent(const QMetaObject *meta, KnownAttributes *knownAttributes = nullptr) @@ -375,30 +372,31 @@ public: for (int index = meta->methodOffset(); index < meta->methodCount(); ++index) { QMetaMethod method = meta->method(index); QByteArray signature = method.methodSignature(); - if (signature == QByteArrayLiteral("destroyed(QObject*)") - || signature == QByteArrayLiteral("destroyed()") - || signature == QByteArrayLiteral("deleteLater()")) + if (signature == "destroyed(QObject*)" + || signature == "destroyed()" + || signature == "deleteLater()") { continue; + } dump(method, implicitSignals, knownAttributes); } // and add toString(), destroy() and destroy(int) - if (!knownAttributes || !knownAttributes->knownMethod(QByteArray("toString"), 0, QTypeRevision::zero())) { - qml->writeStartObject(QLatin1String("Method")); - qml->writeScriptBinding(QLatin1String("name"), enquote(QLatin1String("toString"))); + if (!knownAttributes || !knownAttributes->knownMethod("toString", 0, QTypeRevision::zero())) { + qml->writeStartObject("Method"); + qml->writeStringBinding("name", QLatin1String("toString")); qml->writeEndObject(); } - if (!knownAttributes || !knownAttributes->knownMethod(QByteArray("destroy"), 0, QTypeRevision::zero())) { - qml->writeStartObject(QLatin1String("Method")); - qml->writeScriptBinding(QLatin1String("name"), enquote(QLatin1String("destroy"))); + if (!knownAttributes || !knownAttributes->knownMethod("destroy", 0, QTypeRevision::zero())) { + qml->writeStartObject("Method"); + qml->writeStringBinding("name", QLatin1String("destroy")); qml->writeEndObject(); } - if (!knownAttributes || !knownAttributes->knownMethod(QByteArray("destroy"), 1, QTypeRevision::zero())) { - qml->writeStartObject(QLatin1String("Method")); - qml->writeScriptBinding(QLatin1String("name"), enquote(QLatin1String("destroy"))); - qml->writeStartObject(QLatin1String("Parameter")); - qml->writeScriptBinding(QLatin1String("name"), enquote(QLatin1String("delay"))); - qml->writeScriptBinding(QLatin1String("type"), enquote(QLatin1String("int"))); + if (!knownAttributes || !knownAttributes->knownMethod("destroy", 1, QTypeRevision::zero())) { + qml->writeStartObject("Method"); + qml->writeStringBinding("name", QLatin1String("destroy")); + qml->writeStartObject("Parameter"); + qml->writeStringBinding("name", QLatin1String("delay")); + qml->writeStringBinding("type", QLatin1String("int")); qml->writeEndObject(); qml->writeEndObject(); } @@ -408,12 +406,12 @@ public: } } - QString getPrototypeNameForCompositeType( + QByteArray getPrototypeNameForCompositeType( const QMetaObject *metaObject, QList<const QMetaObject *> *objectsToMerge, const QmlVersionInfo &versionInfo) { auto ty = QQmlMetaType::qmlType(metaObject); - QString prototypeName; + QByteArray prototypeName; if (matchingImportUri(ty, versionInfo)) { // dynamic meta objects can break things badly // but extended types are usually fine @@ -464,35 +462,36 @@ public: QList<const QMetaObject *> objectsToMerge; KnownAttributes knownAttributes; // Get C++ base class name for the composite type - QString prototypeName = getPrototypeNameForCompositeType(mainMeta, &objectsToMerge, - versionInfo); - qml->writeScriptBinding(QLatin1String("prototype"), enquote(prototypeName)); + QByteArray prototypeName = getPrototypeNameForCompositeType( + mainMeta, &objectsToMerge, versionInfo); + qml->writeStringBinding("prototype", QUtf8StringView(prototypeName)); - QString qmlTyName = compositeType.qmlTypeName(); - const QString exportString = getExportString(compositeType, versionInfo); + const QByteArray exportString = getExportString(compositeType, versionInfo); // TODO: why don't we simply output the compositeType.elementName() here? // That would make more sense, but it would change the format quite a bit. - qml->writeScriptBinding(QLatin1String("name"), exportString); + qml->writeStringBinding("name", QUtf8StringView(exportString)); - qml->writeArrayBinding(QLatin1String("exports"), QStringList() << exportString); + qml->writeStringListBinding( + "exports", QList<QAnyStringView> { QUtf8StringView(exportString) }); // TODO: shouldn't this be metaObjectRevision().value<quint16>() // rather than version().minorVersion() - qml->writeArrayBinding(QLatin1String("exportMetaObjectRevisions"), QStringList() - << QString::number(compositeType.version().minorVersion())); + qml->writeArrayBinding( + "exportMetaObjectRevisions", + QByteArrayList() << QByteArray::number(compositeType.version().minorVersion())); - qml->writeBooleanBinding(QLatin1String("isComposite"), true); + qml->writeBooleanBinding("isComposite", true); if (compositeType.isSingleton()) { - qml->writeBooleanBinding(QLatin1String("isCreatable"), false); - qml->writeBooleanBinding(QLatin1String("isSingleton"), true); + qml->writeBooleanBinding("isCreatable", false); + qml->writeBooleanBinding("isSingleton", true); } for (int index = mainMeta->classInfoCount() - 1 ; index >= 0 ; --index) { QMetaClassInfo classInfo = mainMeta->classInfo(index); - if (QLatin1String(classInfo.name()) == QLatin1String("DefaultProperty")) { - qml->writeScriptBinding(QLatin1String("defaultProperty"), enquote(QLatin1String(classInfo.value()))); + if (QUtf8StringView(classInfo.name()) == QUtf8StringView("DefaultProperty")) { + qml->writeStringBinding("defaultProperty", QUtf8StringView(classInfo.value())); break; } } @@ -507,22 +506,29 @@ public: qml->writeEndObject(); } - QString getDefaultProperty(const QMetaObject *meta) + QByteArray getDefaultProperty(const QMetaObject *meta) { for (int index = meta->classInfoCount() - 1; index >= 0; --index) { QMetaClassInfo classInfo = meta->classInfo(index); if (QLatin1String(classInfo.name()) == QLatin1String("DefaultProperty")) { - return QLatin1String(classInfo.value()); + return QByteArray(classInfo.value()); } } - return QString(); + return QByteArray(); } struct QmlTypeInfo { QmlTypeInfo() {} - QmlTypeInfo(const QString &exportString, QTypeRevision revision, const QMetaObject *extendedObject, QByteArray attachedTypeId) - : exportString(exportString), revision(revision), extendedObject(extendedObject), attachedTypeId(attachedTypeId) {} - QString exportString; + QmlTypeInfo( + const QByteArray &exportString, QTypeRevision revision, + const QMetaObject *extendedObject, QByteArray attachedTypeId) + : exportString(exportString) + , revision(revision) + , extendedObject(extendedObject) + , attachedTypeId(attachedTypeId) + {} + + QByteArray exportString; QTypeRevision revision = QTypeRevision::zero(); const QMetaObject *extendedObject = nullptr; QByteArray attachedTypeId; @@ -533,11 +539,12 @@ public: qml->writeStartObject("Component"); QByteArray id = convertToId(meta); - qml->writeScriptBinding(QLatin1String("name"), enquote(id)); + qml->writeStringBinding("name", QUtf8StringView(id)); // collect type information QVector<QmlTypeInfo> typeInfo; - for (QQmlType type : qmlTypesByCppName.value(meta->className())) { + const auto types = qmlTypesByCppName.value(meta->className()); + for (const QQmlType &type : types) { const QMetaObject *extendedObject = type.extensionFunction() ? type.metaObject() : nullptr; QByteArray attachedTypeId; if (const QMetaObject *attachedType = type.attachedPropertiesType(engine)) { @@ -546,7 +553,8 @@ public: if (attachedType != meta) attachedTypeId = convertToId(attachedType); } - const QString exportString = getExportString(type, { QString(), QTypeRevision(), false }); + const QByteArray exportString = getExportString( + type, { QString(), QTypeRevision(), false }); QTypeRevision metaObjectRevision = type.metaObjectRevision(); if (extendedObject) { // emulate custom metaobjectrevision out of import @@ -564,7 +572,7 @@ public: // determine default property // TODO: support revisioning of default property - QString defaultProperty = getDefaultProperty(meta); + QByteArray defaultProperty = getDefaultProperty(meta); if (defaultProperty.isEmpty()) { for (const QmlTypeInfo &iter : typeInfo) { if (iter.extendedObject) { @@ -575,31 +583,33 @@ public: } } if (!defaultProperty.isEmpty()) - qml->writeScriptBinding(QLatin1String("defaultProperty"), enquote(defaultProperty)); + qml->writeStringBinding("defaultProperty", defaultProperty); if (meta->superClass()) - qml->writeScriptBinding(QLatin1String("prototype"), enquote(convertToId(meta->superClass()))); + qml->writeStringBinding("prototype", convertToId(meta->superClass())); if (!typeInfo.isEmpty()) { - QMap<QString, QString> exports; // sort exports - for (const QmlTypeInfo &iter : typeInfo) - exports.insert(iter.exportString, QString::number(iter.revision.toEncodedVersion<quint16>())); + QMap<QAnyStringView, QByteArray> exports; // sort exports + for (const QmlTypeInfo &iter : typeInfo) { + exports.insert( + QUtf8StringView(iter.exportString), + QByteArray::number(iter.revision.toEncodedVersion<quint16>())); + } - QStringList exportStrings = exports.keys(); - QStringList metaObjectRevisions = exports.values(); - qml->writeArrayBinding(QLatin1String("exports"), exportStrings); + QByteArrayList metaObjectRevisions = exports.values(); + qml->writeStringListBinding("exports", exports.keys()); if (isUncreatable) - qml->writeBooleanBinding(QLatin1String("isCreatable"), false); + qml->writeBooleanBinding("isCreatable", false); if (isSingleton) - qml->writeBooleanBinding(QLatin1String("isSingleton"), true); + qml->writeBooleanBinding("isSingleton", true); - qml->writeArrayBinding(QLatin1String("exportMetaObjectRevisions"), metaObjectRevisions); + qml->writeArrayBinding("exportMetaObjectRevisions", metaObjectRevisions); for (const QmlTypeInfo &iter : typeInfo) { if (!iter.attachedTypeId.isEmpty()) { - qml->writeScriptBinding(QLatin1String("attachedType"), enquote(iter.attachedTypeId)); + qml->writeStringBinding("attachedType", iter.attachedTypeId); break; } } @@ -611,7 +621,7 @@ public: writeMetaContent(meta); // dump properties from extended metaobjects last - for (auto iter : typeInfo) { + for (const auto &iter : typeInfo) { if (iter.extendedObject) dumpMetaProperties(iter.extendedObject, iter.revision); } @@ -647,13 +657,13 @@ private: bool isList = false, isPointer = false; removePointerAndList(&typeName, &isList, &isPointer); - qml->writeScriptBinding(QLatin1String("type"), enquote(typeName)); + qml->writeStringBinding("type", QUtf8StringView(typeName)); if (isList) - qml->writeScriptBinding(QLatin1String("isList"), QLatin1String("true")); + qml->writeBooleanBinding("isList", true); if (!isWritable) - qml->writeScriptBinding(QLatin1String("isReadonly"), QLatin1String("true")); + qml->writeBooleanBinding("isReadonly", true); if (isPointer) - qml->writeScriptBinding(QLatin1String("isPointer"), QLatin1String("true")); + qml->writeBooleanBinding("isPointer", true); } void dump(const QMetaProperty &prop, QTypeRevision metaRevision = QTypeRevision(), @@ -667,9 +677,9 @@ private: if (knownAttributes && knownAttributes->knownProperty(propName, revision)) return; qml->writeStartObject("Property"); - qml->writeScriptBinding(QLatin1String("name"), enquote(QString::fromUtf8(prop.name()))); + qml->writeStringBinding("name", QUtf8StringView(prop.name())); if (revision != QTypeRevision::zero()) - qml->writeScriptBinding(QLatin1String("revision"), QString::number(revision.toEncodedVersion<quint16>())); + qml->writeNumberBinding("revision", revision.toEncodedVersion<quint16>()); writeTypeProperties(prop.typeName(), prop.isWritable()); qml->writeEndObject(); @@ -682,10 +692,13 @@ private: for (int index = meta->propertyOffset(); index < meta->propertyCount(); ++index) { const QMetaProperty &property = meta->property(index); dump(property, metaRevision, knownAttributes); + const QByteArray changedSignalName = + QQmlSignalNames::propertyNameToChangedSignalName(property.name()); if (knownAttributes) - knownAttributes->knownMethod(QByteArray(property.name()).append("Changed"), - 0, QTypeRevision::fromEncodedVersion(property.revision())); - implicitSignals.insert(QString("%1Changed").arg(QString::fromUtf8(property.name()))); + knownAttributes->knownMethod( + changedSignalName, 0, + QTypeRevision::fromEncodedVersion(property.revision())); + implicitSignals.insert(changedSignalName); } return implicitSignals; } @@ -701,13 +714,13 @@ private: } QByteArray name = meth.name(); - const QString typeName = convertToId(meth.typeName()); + const QByteArray typeName = convertToId(meth.typeName()); if (implicitSignals.contains(name) && !meth.revision() && meth.methodType() == QMetaMethod::Signal && meth.parameterNames().isEmpty() - && typeName == QLatin1String("void")) { + && typeName == "void") { // don't mention implicit signals return; } @@ -716,24 +729,24 @@ private: if (knownAttributes && knownAttributes->knownMethod(name, meth.parameterNames().size(), revision)) return; if (meth.methodType() == QMetaMethod::Signal) - qml->writeStartObject(QLatin1String("Signal")); + qml->writeStartObject("Signal"); else - qml->writeStartObject(QLatin1String("Method")); + qml->writeStartObject("Method"); - qml->writeScriptBinding(QLatin1String("name"), enquote(name)); + qml->writeStringBinding("name", QUtf8StringView(name)); if (revision != QTypeRevision::zero()) - qml->writeScriptBinding(QLatin1String("revision"), QString::number(revision.toEncodedVersion<quint16>())); + qml->writeNumberBinding("revision", revision.toEncodedVersion<quint16>()); - if (typeName != QLatin1String("void")) - qml->writeScriptBinding(QLatin1String("type"), enquote(typeName)); + if (typeName != "void") + qml->writeStringBinding("type", QUtf8StringView(typeName)); for (int i = 0; i < meth.parameterTypes().size(); ++i) { QByteArray argName = meth.parameterNames().at(i); - qml->writeStartObject(QLatin1String("Parameter")); - if (! argName.isEmpty()) - qml->writeScriptBinding(QLatin1String("name"), enquote(argName)); + qml->writeStartObject("Parameter"); + if (!argName.isEmpty()) + qml->writeStringBinding("name", QUtf8StringView(argName)); writeTypeProperties(meth.parameterTypes().at(i), true); qml->writeEndObject(); } @@ -743,17 +756,16 @@ private: void dump(const QMetaEnum &e) { - qml->writeStartObject(QLatin1String("Enum")); - qml->writeScriptBinding(QLatin1String("name"), enquote(QString::fromUtf8(e.name()))); + qml->writeStartObject("Enum"); + qml->writeStringBinding("name", QUtf8StringView(e.name())); - QList<QPair<QString, QString> > namesValues; + QList<QPair<QAnyStringView, int>> namesValues; const int keyCount = e.keyCount(); namesValues.reserve(keyCount); - for (int index = 0; index < keyCount; ++index) { - namesValues.append(qMakePair(enquote(QString::fromUtf8(e.key(index))), QString::number(e.value(index)))); - } + for (int index = 0; index < keyCount; ++index) + namesValues.append(qMakePair(QUtf8StringView(e.key(index)), e.value(index))); - qml->writeScriptObjectLiteralBinding(QLatin1String("values"), namesValues); + qml->writeEnumObjectLiteralBinding("values", namesValues); qml->writeEndObject(); } }; @@ -1167,17 +1179,17 @@ int main(int argc, char *argv[]) // Merge file. QStringList mergeDependencies; - QString mergeComponents; + QByteArray mergeComponents; if (!mergeFile.isEmpty()) { const QStringList merge = readQmlTypes(mergeFile); if (!merge.isEmpty()) { - QRegularExpression re("(\\w+\\.*\\w*\\s*\\d+\\.\\d+)"); + static const QRegularExpression re("(\\w+\\.*\\w*\\s*\\d+\\.\\d+)"); QRegularExpressionMatchIterator i = re.globalMatch(merge[1]); while (i.hasNext()) { QRegularExpressionMatch m = i.next(); mergeDependencies << m.captured(1); } - mergeComponents = merge [2]; + mergeComponents = merge[2].toUtf8(); } } @@ -1317,16 +1329,20 @@ int main(int argc, char *argv[]) QQmlJSStreamWriter qml(&bytes); qml.writeStartDocument(); - qml.writeLibraryImport(QLatin1String("QtQuick.tooling"), 1, 2); - qml.write(QString("\n" + qml.writeLibraryImport("QtQuick.tooling", 1, 2); + qml.write("\n" "// This file describes the plugin-supplied types contained in the library.\n" "// It is used for QML tooling purposes only.\n" "//\n" "// This file was auto-generated by:\n" - "// '%1 %2'\n" + "// '"); + qml.write(QFileInfo(args.at(0)).baseName().toUtf8()); + qml.write(" "); + qml.write(args.mid(1).join(QLatin1Char(' ')).toUtf8()); + qml.write("'\n" "//\n" "// qmlplugindump is deprecated! You should use qmltyperegistrar instead.\n" - "\n").arg(QFileInfo(args.at(0)).baseName(), args.mid(1).join(QLatin1Char(' ')))); + "\n"); qml.writeStartObject("Module"); // put the metaobjects into a map so they are always dumped in the same order diff --git a/tools/qmlprofiler/qmlprofilerdata.cpp b/tools/qmlprofiler/qmlprofilerdata.cpp index 8f75226fd8..c9e917cc00 100644 --- a/tools/qmlprofiler/qmlprofilerdata.cpp +++ b/tools/qmlprofiler/qmlprofilerdata.cpp @@ -3,14 +3,12 @@ #include "qmlprofilerdata.h" -#include <QStringList> -#include <QUrl> -#include <QHash> -#include <QFile> -#include <QXmlStreamReader> -#include <QRegularExpression> -#include <QQueue> -#include <QStack> +#include <QtCore/qfile.h> +#include <QtCore/qqueue.h> +#include <QtCore/qregularexpression.h> +#include <QtCore/qurl.h> +#include <QtCore/qxmlstream.h> +#include <QtCore/qxpfunctional.h> #include <limits> @@ -375,6 +373,132 @@ private: QXmlStreamWriter stream; }; +struct DataIterator +{ + DataIterator( + const QmlProfilerDataPrivate *d, + qxp::function_ref<void(const QQmlProfilerEvent &, qint64)> &&sendEvent) + : d(d) + , sendEvent(std::move(sendEvent)) + {} + + void run(); + +private: + void handleRangeEvent(const QQmlProfilerEvent &event, const QQmlProfilerEventType &type); + void sendPending(); + void endLevel0(); + + const QmlProfilerDataPrivate *d = nullptr; + const qxp::function_ref<void(const QQmlProfilerEvent &, qint64)> sendEvent; + + QQueue<QQmlProfilerEvent> pointEvents; + QList<QQmlProfilerEvent> rangeStarts[MaximumRangeType]; + QList<qint64> rangeEnds[MaximumRangeType]; + + int level = 0; +}; + +void DataIterator::handleRangeEvent( + const QQmlProfilerEvent &event, const QQmlProfilerEventType &type) +{ + QList<QQmlProfilerEvent> &starts = rangeStarts[type.rangeType()]; + switch (event.rangeStage()) { + case RangeStart: { + ++level; + starts.append(event); + break; + } + case RangeEnd: { + const qint64 invalidTimestamp = -1; + QList<qint64> &ends = rangeEnds[type.rangeType()]; + + // -1 because all valid timestamps are >= 0. + ends.resize(starts.size(), invalidTimestamp); + + qsizetype i = starts.size(); + while (ends[--i] != invalidTimestamp) {} + + Q_ASSERT(i >= 0); + Q_ASSERT(starts[i].timestamp() <= event.timestamp()); + + ends[i] = event.timestamp(); + if (--level == 0) + endLevel0(); + break; + } + default: + break; + } +} + +void DataIterator::sendPending() +{ + // Send all pending events in the order of their start times. + + qsizetype index[MaximumRangeType] = { 0, 0, 0, 0, 0, 0 }; + while (true) { + + // Find the range type with the minimum start time. + qsizetype minimum = MaximumRangeType; + qint64 minimumTime = std::numeric_limits<qint64>::max(); + for (qsizetype i = 0; i < MaximumRangeType; ++i) { + const QList<QQmlProfilerEvent> &starts = rangeStarts[i]; + if (starts.size() == index[i]) + continue; + const qint64 timestamp = starts[index[i]].timestamp(); + if (timestamp < minimumTime) { + minimumTime = timestamp; + minimum = i; + } + } + if (minimum == MaximumRangeType) + break; + + // Send all point events that happened before the range we've found. + while (!pointEvents.isEmpty() && pointEvents.front().timestamp() < minimumTime) + sendEvent(pointEvents.dequeue(), 0); + + // Send the range itself + sendEvent(rangeStarts[minimum][index[minimum]], + rangeEnds[minimum][index[minimum]] - minimumTime); + + // Bump the index so that we don't send the same range again + ++index[minimum]; + } +} + +void DataIterator::endLevel0() +{ + sendPending(); + for (qsizetype i = 0; i < MaximumRangeType; ++i) { + rangeStarts[i].clear(); + rangeEnds[i].clear(); + } +} + +void DataIterator::run() +{ + for (const QQmlProfilerEvent &event : std::as_const(d->events)) { + const QQmlProfilerEventType &type = d->eventTypes.at(event.typeIndex()); + if (type.rangeType() != MaximumRangeType) + handleRangeEvent(event, type); + else if (level == 0) + sendEvent(event, 0); + else + pointEvents.enqueue(event); + } + + for (qsizetype i = 0; i < MaximumRangeType; ++i) { + while (rangeEnds[i].size() < rangeStarts[i].size()) { + rangeEnds[i].append(d->traceEndTime); + --level; + } + } + + sendPending(); +} + bool QmlProfilerData::save(const QString &filename) { if (isEmpty()) { @@ -442,6 +566,7 @@ bool QmlProfilerData::save(const QString &filename) stream.writeStartElement("profilerDataModel"); auto sendEvent = [&](const QQmlProfilerEvent &event, qint64 duration = 0) { + Q_ASSERT(duration >= 0); const QQmlProfilerEventType &type = d->eventTypes.at(event.typeIndex()); stream.writeStartElement("range"); stream.writeAttribute("startTime", event.timestamp()); @@ -481,74 +606,7 @@ bool QmlProfilerData::save(const QString &filename) stream.writeEndElement(); }; - QQueue<QQmlProfilerEvent> pointEvents; - QQueue<QQmlProfilerEvent> rangeStarts[MaximumRangeType]; - QStack<qint64> rangeEnds[MaximumRangeType]; - int level = 0; - - auto sendPending = [&]() { - forever { - int minimum = MaximumRangeType; - qint64 minimumTime = std::numeric_limits<qint64>::max(); - for (int i = 0; i < MaximumRangeType; ++i) { - const QQueue<QQmlProfilerEvent> &starts = rangeStarts[i]; - if (starts.isEmpty()) - continue; - if (starts.head().timestamp() < minimumTime) { - minimumTime = starts.head().timestamp(); - minimum = i; - } - } - if (minimum == MaximumRangeType) - break; - - while (!pointEvents.isEmpty() && pointEvents.front().timestamp() < minimumTime) - sendEvent(pointEvents.dequeue()); - - sendEvent(rangeStarts[minimum].dequeue(), - rangeEnds[minimum].pop() - minimumTime); - } - }; - - for (const QQmlProfilerEvent &event : std::as_const(d->events)) { - const QQmlProfilerEventType &type = d->eventTypes.at(event.typeIndex()); - - if (type.rangeType() != MaximumRangeType) { - QQueue<QQmlProfilerEvent> &starts = rangeStarts[type.rangeType()]; - switch (event.rangeStage()) { - case RangeStart: { - ++level; - starts.enqueue(event); - break; - } - case RangeEnd: { - QStack<qint64> &ends = rangeEnds[type.rangeType()]; - if (starts.size() > ends.size()) { - ends.push(event.timestamp()); - if (--level == 0) - sendPending(); - } - break; - } - default: - break; - } - } else { - if (level == 0) - sendEvent(event); - else - pointEvents.enqueue(event); - } - } - - for (int i = 0; i < MaximumRangeType; ++i) { - while (rangeEnds[i].size() < rangeStarts[i].size()) { - rangeEnds[i].push(d->traceEndTime); - --level; - } - } - - sendPending(); + DataIterator(d, std::move(sendEvent)).run(); stream.writeEndElement(); // profilerDataModel diff --git a/tools/qmltc/CMakeLists.txt b/tools/qmltc/CMakeLists.txt index 8e7ce0a586..42e47e24da 100644 --- a/tools/qmltc/CMakeLists.txt +++ b/tools/qmltc/CMakeLists.txt @@ -3,7 +3,7 @@ qt_get_tool_target_name(target_name qmltc) qt_internal_add_tool(${target_name} - TARGET_DESCRIPTION "QML Type Compiler" + TARGET_DESCRIPTION "QML type compiler" TOOLS_TARGET Qml SOURCES main.cpp diff --git a/tools/qmltc/main.cpp b/tools/qmltc/main.cpp index 6ef0afe105..1899f4087e 100644 --- a/tools/qmltc/main.cpp +++ b/tools/qmltc/main.cpp @@ -9,6 +9,7 @@ #include <private/qqmljscompiler_p.h> #include <private/qqmljsresourcefilemapper_p.h> +#include <private/qqmljsutils_p.h> #include <QtCore/qcoreapplication.h> #include <QtCore/qurl.h> @@ -24,6 +25,8 @@ #include <QtQml/private/qqmljsastvisitor_p.h> #include <QtQml/private/qqmljsast_p.h> #include <QtQml/private/qqmljsdiagnosticmessage_p.h> +#include <QtQmlCompiler/qqmlsa.h> +#include <QtQmlCompiler/private/qqmljsliteralbindingcheck_p.h> #include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE @@ -31,8 +34,8 @@ using namespace Qt::StringLiterals; void setupLogger(QQmlJSLogger &logger) // prepare logger to work with compiler { - for (const QQmlJSLogger::Category &category : logger.categories()) { - if (category == qmlUnusedImports) + for (const QQmlJS::LoggerCategory &category : logger.categories()) { + if (category.id() == qmlUnusedImports) continue; logger.setCategoryLevel(category.id(), QtCriticalMsg); logger.setCategoryIgnored(category.id(), false); @@ -99,6 +102,25 @@ int main(int argc, char **argv) QCoreApplication::translate("main", "namespace") }; parser.addOption(namespaceOption); + QCommandLineOption moduleOption{ + u"module"_s, + QCoreApplication::translate("main", + "Name of the QML module that this QML code belongs to."), + QCoreApplication::translate("main", "module") + }; + parser.addOption(moduleOption); + QCommandLineOption exportOption{ u"export"_s, + QCoreApplication::translate( + "main", "Export macro used in the generated C++ code"), + QCoreApplication::translate("main", "export") }; + parser.addOption(exportOption); + QCommandLineOption exportIncludeOption{ + u"exportInclude"_s, + QCoreApplication::translate( + "main", "Header defining the export macro to be used in the generated C++ code"), + QCoreApplication::translate("main", "exportInclude") + }; + parser.addOption(exportIncludeOption); parser.process(app); @@ -153,7 +175,7 @@ int main(int argc, char **argv) if (!parser.isSet(bareOption)) importPaths.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)); - QStringList qmldirFiles = parser.values(qmldirOption); + QStringList qmldirFiles = QQmlJSUtils::cleanPaths(parser.values(qmldirOption)); QString outputCppFile; if (!parser.isSet(outputCppOption)) { @@ -224,6 +246,8 @@ int main(int argc, char **argv) info.outputHFile = parser.value(outputHOption); info.resourcePath = firstQml(paths); info.outputNamespace = parser.value(namespaceOption); + info.exportMacro = parser.value(exportOption); + info.exportInclude = parser.value(exportIncludeOption); if (info.outputCppFile.isEmpty()) { fprintf(stderr, "An output C++ file is required. Pass one using --impl"); @@ -236,24 +260,37 @@ int main(int argc, char **argv) QQmlJSImporter importer { importPaths, &mapper }; importer.setMetaDataMapper(&metaDataMapper); - auto createQmltcVisitor = [](const QQmlJSScope::Ptr &root, QQmlJSImporter *importer, - QQmlJSLogger *logger, const QString &implicitImportDirectory, - const QStringList &qmldirFiles) -> QQmlJSImportVisitor * { - return new QmltcVisitor(root, importer, logger, implicitImportDirectory, qmldirFiles); + auto qmltcVisitor = [](QQmlJS::AST::Node *rootNode, QQmlJSImporter *self, + const QQmlJSImporter::ImportVisitorPrerequisites &p) { + QmltcVisitor v(p.m_target, self, p.m_logger, p.m_implicitImportDirectory, p.m_qmldirFiles); + QQmlJS::AST::Node::accept(rootNode, &v); }; - importer.setImportVisitorCreator(createQmltcVisitor); + importer.setImportVisitor(qmltcVisitor); QQmlJSLogger logger; logger.setFileName(url); logger.setCode(sourceCode); setupLogger(logger); - QmltcVisitor visitor(QQmlJSScope::create(), &importer, &logger, + auto currentScope = QQmlJSScope::create(); + if (parser.isSet(moduleOption)) + currentScope->setOwnModuleName(parser.value(moduleOption)); + + QmltcVisitor visitor(currentScope, &importer, &logger, QQmlJSImportVisitor::implicitImportDirectory(url, &mapper), qmldirFiles); visitor.setMode(QmltcVisitor::Compile); QmltcTypeResolver typeResolver { &importer }; typeResolver.init(&visitor, qmlParser.rootNode()); + using PassManagerPtr = + std::unique_ptr<QQmlSA::PassManager, + decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>; + PassManagerPtr passMan(QQmlSA::PassManagerPrivate::createPassManager(&visitor, &typeResolver), + &QQmlSA::PassManagerPrivate::deletePassManager); + passMan->registerPropertyPass(std::make_unique<QQmlJSLiteralBindingCheck>(passMan.get()), + QString(), QString(), QString()); + passMan->analyze(QQmlJSScope::createQQmlSAElement(visitor.result())); + if (logger.hasErrors()) return EXIT_FAILURE; diff --git a/tools/qmltc/qmltccodewriter.cpp b/tools/qmltc/qmltccodewriter.cpp index 8010f1066c..134de0f98e 100644 --- a/tools/qmltc/qmltccodewriter.cpp +++ b/tools/qmltc/qmltccodewriter.cpp @@ -42,14 +42,14 @@ static QString getFunctionCategory(const QmltcMethod &method) { QString category = getFunctionCategory(static_cast<const QmltcMethodBase &>(method)); switch (method.type) { - case QQmlJSMetaMethod::Signal: + case QQmlJSMetaMethodType::Signal: category = u"Q_SIGNALS"_s; break; - case QQmlJSMetaMethod::Slot: + case QQmlJSMetaMethodType::Slot: category += u" Q_SLOTS"_s; break; - case QQmlJSMetaMethod::Method: - case QQmlJSMetaMethod::StaticMethod: + case QQmlJSMetaMethodType::Method: + case QQmlJSMetaMethodType::StaticMethod: break; } return category; @@ -118,6 +118,7 @@ void QmltcCodeWriter::writeGlobalHeader(QmltcOutputWrapper &code, const QString code.rawAppendToHeader(u"#include <QtCore/qproperty.h>"); code.rawAppendToHeader(u"#include <QtCore/qobject.h>"); code.rawAppendToHeader(u"#include <QtCore/qcoreapplication.h>"); + code.rawAppendToHeader(u"#include <QtCore/qxpfunctional.h>"); code.rawAppendToHeader(u"#include <QtQml/qqmlengine.h>"); code.rawAppendToHeader(u"#include <QtCore/qurl.h>"); // used in engine execution code.rawAppendToHeader(u"#include <QtQml/qqml.h>"); // used for attached properties @@ -160,6 +161,64 @@ void QmltcCodeWriter::writeGlobalHeader(QmltcOutputWrapper &code, const QString } } +void QmltcCodeWriter::write(QmltcOutputWrapper &code, + const QmltcPropertyInitializer &propertyInitializer, + const QmltcType &wrappedType) +{ + code.rawAppendToHeader(u"class " + propertyInitializer.name + u" {"); + + { + { + [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code); + + code.rawAppendToHeader(u"friend class " + wrappedType.cppType + u";"); + } + + code.rawAppendToHeader(u"public:"_s); + + [[maybe_unused]] QmltcOutputWrapper::MemberNameScope typeScope(&code, propertyInitializer.name); + { + [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code); + + write(code, propertyInitializer.constructor); + code.rawAppendToHeader(u""); // blank line + + for (const auto &propertySetter : propertyInitializer.propertySetters) { + write(code, propertySetter); + } + } + + code.rawAppendToHeader(u""); // blank line + code.rawAppendToHeader(u"private:"_s); + + { + [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code); + + write(code, propertyInitializer.component); + write(code, propertyInitializer.initializedCache); + } + } + + code.rawAppendToHeader(u"};"_s); + code.rawAppendToHeader(u""); // blank line +} + +void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcRequiredPropertiesBundle &requiredPropertiesBundle) +{ + code.rawAppendToHeader(u"struct " + requiredPropertiesBundle.name + u" {"); + + { + [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code); + + for (const auto &member : requiredPropertiesBundle.members) { + write(code, member); + } + } + + code.rawAppendToHeader(u"};"_s); + code.rawAppendToHeader(u""); // blank line +} + void QmltcCodeWriter::writeGlobalFooter(QmltcOutputWrapper &code, const QString &sourcePath, const QString &outNamespace) { @@ -187,12 +246,14 @@ static void writeToFile(const QString &path, const QByteArray &data) QFileInfo fi(path); if (fi.exists() && fi.size() == data.size()) { QFile oldFile(path); - oldFile.open(QIODevice::ReadOnly); - if (oldFile.readAll() == data) - return; + if (oldFile.open(QIODevice::ReadOnly)) { + if (oldFile.readAll() == data) + return; + } } QFile file(path); - file.open(QIODevice::WriteOnly); + if (!file.open(QIODevice::WriteOnly)) + qFatal("Could not open file %s", qPrintable(path)); file.write(data); } @@ -209,7 +270,7 @@ void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcProgram &progra code.rawAppendToHeader(u"class " + type.cppType + u";"); // write all the types and their content for (const QmltcType &type : std::as_const(program.compiledTypes)) - write(code, type); + write(code, type, program.exportMacro); // add typeCount definitions. after all types have been written down (so // they are now complete types as per C++). practically, this only concerns @@ -251,10 +312,14 @@ static void dumpFunctions(QmltcOutputWrapper &code, const QList<QmltcMethod> &fu } } -void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcType &type) +void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcType &type, + const QString &exportMacro) { const auto constructClassString = [&]() { - QString str = u"class " + type.cppType; + QString str = u"class "_s; + if (!exportMacro.isEmpty()) + str.append(exportMacro).append(u" "_s); + str.append(type.cppType); QStringList nonEmptyBaseClasses; nonEmptyBaseClasses.reserve(type.baseClasses.size()); std::copy_if(type.baseClasses.cbegin(), type.baseClasses.cend(), @@ -287,6 +352,12 @@ void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcType &type) code.rawAppendToHeader(u"/* External C++ API */"); code.rawAppendToHeader(u"public:", -1); + if (!type.propertyInitializer.name.isEmpty()) + write(code, type.propertyInitializer, type); + + if (type.requiredPropertiesBundle) + write(code, *type.requiredPropertiesBundle); + // NB: when non-document root, the externalCtor won't be public - but we // really don't care about the output format of such types if (!type.ignoreInit && type.externalCtor.access == QQmlJSMetaMethod::Public) { @@ -340,7 +411,7 @@ void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcType &type) // children for (const auto &child : std::as_const(type.children)) - QmltcCodeWriter::write(code, child); + QmltcCodeWriter::write(code, child, exportMacro); // (non-visible) functions dumpFunctions(code, type.functions, std::not_fn(isUserVisibleFunction)); @@ -392,14 +463,14 @@ void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcMethod &method) { const auto [hSignature, cppSignature] = functionSignatures(method); // Note: augment return type with preambles in declaration - code.rawAppendToHeader((method.type == QQmlJSMetaMethod::StaticMethod + code.rawAppendToHeader((method.type == QQmlJSMetaMethodType::StaticMethod ? u"static " + functionReturnType(method) : functionReturnType(method)) + u" " + hSignature + u";"); // do not generate method implementation if it is a signal const auto methodType = method.type; - if (methodType != QQmlJSMetaMethod::Signal) { + if (methodType != QQmlJSMetaMethodType::Signal) { code.rawAppendToCpp(u""_s); // blank line if (method.comments.size() > 0) { code.rawAppendToCpp(u"/*! \\internal"_s); diff --git a/tools/qmltc/qmltccodewriter.h b/tools/qmltc/qmltccodewriter.h index e95777998e..20b0262737 100644 --- a/tools/qmltc/qmltccodewriter.h +++ b/tools/qmltc/qmltccodewriter.h @@ -20,13 +20,15 @@ struct QmltcCodeWriter static void writeGlobalFooter(QmltcOutputWrapper &code, const QString &sourcePath, const QString &outNamespace); static void write(QmltcOutputWrapper &code, const QmltcProgram &program); - static void write(QmltcOutputWrapper &code, const QmltcType &type); + static void write(QmltcOutputWrapper &code, const QmltcType &type, const QString &exportMacro); static void write(QmltcOutputWrapper &code, const QmltcEnum &enumeration); static void write(QmltcOutputWrapper &code, const QmltcMethod &method); static void write(QmltcOutputWrapper &code, const QmltcCtor &ctor); static void write(QmltcOutputWrapper &code, const QmltcDtor &dtor); static void write(QmltcOutputWrapper &code, const QmltcVariable &var); static void write(QmltcOutputWrapper &code, const QmltcProperty &prop); + static void write(QmltcOutputWrapper &code, const QmltcPropertyInitializer &propertyInitializer, const QmltcType& wrappedType); + static void write(QmltcOutputWrapper &code, const QmltcRequiredPropertiesBundle &requiredPropertiesBundle); private: static void writeUrl(QmltcOutputWrapper &code, const QmltcMethod &urlMethod); // special diff --git a/tools/qmltc/qmltccompiler.cpp b/tools/qmltc/qmltccompiler.cpp index 4050136ef2..75bd580e07 100644 --- a/tools/qmltc/qmltccompiler.cpp +++ b/tools/qmltc/qmltccompiler.cpp @@ -8,6 +8,7 @@ #include "qmltccompilerpieces.h" #include <QtCore/qloggingcategory.h> +#include <QtQml/private/qqmlsignalnames_p.h> #include <private/qqmljsutils_p.h> #include <algorithm> @@ -22,6 +23,137 @@ bool qIsReferenceTypeList(const QQmlJSMetaProperty &p) return false; } +static QList<QQmlJSMetaProperty> unboundRequiredProperties( + const QQmlJSScope::ConstPtr &type, + QmltcTypeResolver *resolver +) { + QList<QQmlJSMetaProperty> requiredProperties{}; + + auto isPropertyRequired = [&type, &resolver](const auto &property) { + if (!type->isPropertyRequired(property.propertyName())) + return false; + + if (type->hasPropertyBindings(property.propertyName())) + return false; + + if (property.isAlias()) { + QQmlJSUtils::AliasResolutionVisitor aliasVisitor; + + QQmlJSUtils::ResolvedAlias result = + QQmlJSUtils::resolveAlias(resolver, property, type, aliasVisitor); + + if (result.kind != QQmlJSUtils::AliasTarget_Property) + return false; + + // If the top level alias targets a property that is in + // the top level scope and that property is required, then + // we will already pick up the property during one of the + // iterations. + // Setting the property or the alias is the same so we + // discard one of the two, as otherwise we would require + // the user to pass two values for the same property ,in + // this case the alias. + // + // For example in: + // + // ``` + // Item { + // id: self + // required property int foo + // property alias bar: self.foo + // } + // ``` + // + // Both foo and bar are required but setting one or the + // other is the same operation so that we should choose + // only one. + if (result.owner == type && + type->isPropertyRequired(result.property.propertyName())) + return false; + + if (result.owner->hasPropertyBindings(result.property.propertyName())) + return false; + } + + return true; + }; + + const auto properties = type->properties(); + std::copy_if(properties.cbegin(), properties.cend(), + std::back_inserter(requiredProperties), isPropertyRequired); + std::sort(requiredProperties.begin(), requiredProperties.end(), + [](const auto &left, const auto &right) { + return left.propertyName() < right.propertyName(); + }); + + return requiredProperties; +} + + +// Populates the internal representation for a +// RequiredPropertiesBundle, a class that acts as a bundle of initial +// values that should be set for the required properties of a type. +static void compileRequiredPropertiesBundle( + QmltcType ¤t, + const QQmlJSScope::ConstPtr &type, + QmltcTypeResolver *resolver +) { + + QList<QQmlJSMetaProperty> requiredProperties = unboundRequiredProperties(type, resolver); + + if (requiredProperties.isEmpty()) + return; + + current.requiredPropertiesBundle.emplace(); + current.requiredPropertiesBundle->name = u"RequiredPropertiesBundle"_s; + + current.requiredPropertiesBundle->members.reserve(requiredProperties.size()); + std::transform(requiredProperties.cbegin(), requiredProperties.cend(), + std::back_inserter(current.requiredPropertiesBundle->members), + [](const QQmlJSMetaProperty &property) { + QString type = qIsReferenceTypeList(property) + ? u"const QList<%1*>&"_s.arg( + property.type()->valueType()->internalName()) + : u"passByConstRefOrValue<%1>"_s.arg( + property.type()->augmentedInternalName()); + return QmltcVariable{ type, property.propertyName() }; + }); +} + +static void compileRootExternalConstructorBody( + QmltcType& current, + const QQmlJSScope::ConstPtr &type +) { + current.externalCtor.body << u"// document root:"_s; + // if it's document root, we want to create our QQmltcObjectCreationBase + // that would store all the created objects + current.externalCtor.body << u"QQmltcObjectCreationBase<%1> objectHolder;"_s.arg( + type->internalName()); + current.externalCtor.body + << u"QQmltcObjectCreationHelper creator = objectHolder.view();"_s; + current.externalCtor.body << u"creator.set(0, this);"_s; // special case + + QString initializerName = u"initializer"_s; + if (current.requiredPropertiesBundle) { + // Compose new initializer based on the initial values for required properties. + current.externalCtor.body << u"auto newInitializer = [&](auto& propertyInitializer) {"_s; + for (const auto& member : current.requiredPropertiesBundle->members) { + current.externalCtor.body << u" propertyInitializer.%1(requiredPropertiesBundle.%2);"_s.arg( + QmltcPropertyData(member.name).write, member.name + ); + } + current.externalCtor.body << u" initializer(propertyInitializer);"_s; + current.externalCtor.body << u"};"_s; + + initializerName = u"newInitializer"_s; + } + + // now call init + current.externalCtor.body << current.init.name + + u"(&creator, engine, QQmlContextData::get(engine->rootContext()), /* " + u"endInit */ true, %1);"_s.arg(initializerName); +}; + Q_LOGGING_CATEGORY(lcQmltcCompiler, "qml.qmltc.compiler", QtWarningMsg); const QString QmltcCodeGenerator::privateEngineName = u"ePriv"_s; @@ -87,6 +219,9 @@ void QmltcCompiler::compile(const QmltcCompilerInfo &info) const auto *inlineComponentAName = std::get_if<InlineComponentNameType>(&a); const auto *inlineComponentBName = std::get_if<InlineComponentNameType>(&b); + if (inlineComponentAName == inlineComponentBName) + return false; + // the root comes at last, so (a < b) == true when b is the root and a is not if (inlineComponentAName && !inlineComponentBName) return true; @@ -127,7 +262,7 @@ void QmltcCompiler::compile(const QmltcCompilerInfo &info) }; for (const auto &type : pureTypes) { - Q_ASSERT(type->scopeType() == QQmlJSScope::QMLScope); + Q_ASSERT(type->scopeType() == QQmlSA::ScopeType::QMLScope); compiledTypes.emplaceBack(); // create empty type compileType(compiledTypes.back(), type, compile); } @@ -142,8 +277,11 @@ void QmltcCompiler::compile(const QmltcCompilerInfo &info) program.cppPath = m_info.outputCppFile; program.hPath = m_info.outputHFile; program.outNamespace = m_info.outputNamespace; + program.exportMacro = m_info.exportMacro; program.compiledTypes = compiledTypes; program.includes = m_visitor->cppIncludeFiles(); + if (!m_info.exportMacro.isEmpty() && !m_info.exportInclude.isEmpty()) + program.includes += (m_info.exportInclude); program.urlMethod = urlMethod; QmltcOutput out; @@ -186,7 +324,8 @@ void QmltcCompiler::compileType( // make document root a friend to allow it to access init and endInit const QString rootInternalName = m_visitor->inlineComponent(type->enclosingInlineComponentName())->internalName(); - current.otherCode << u"friend class %1;"_s.arg(rootInternalName); + if (rootInternalName != current.cppType) // avoid GCC13 warning on self-befriending + current.otherCode << "friend class %1;"_L1.arg(rootInternalName); } if (documentRoot || inlineComponent) { auto name = type->inlineComponentName() @@ -210,8 +349,11 @@ void QmltcCompiler::compileType( return scope->parentScope(); return scope; }; - current.otherCode << u"friend class %1;"_s.arg( - realQmlScope(type->parentScope())->internalName()); + + const auto& realScope = realQmlScope(type->parentScope()); + if (realScope != rootType) { + current.otherCode << u"friend class %1;"_s.arg(realScope->internalName()); + } } // make QQmltcObjectCreationHelper a friend of every type since it provides @@ -240,6 +382,17 @@ void QmltcCompiler::compileType( current.finalizeComponent.access = QQmlJSMetaMethod::Protected; current.handleOnCompleted.access = QQmlJSMetaMethod::Protected; + current.propertyInitializer.name = u"PropertyInitializer"_s; + current.propertyInitializer.constructor.access = QQmlJSMetaMethod::Public; + current.propertyInitializer.constructor.name = current.propertyInitializer.name; + current.propertyInitializer.constructor.parameterList = { + QmltcVariable(u"%1&"_s.arg(current.cppType), u"component"_s) + }; + current.propertyInitializer.component.cppType = current.cppType + u"&"; + current.propertyInitializer.component.name = u"component"_s; + current.propertyInitializer.initializedCache.cppType = u"QSet<QString>"_s; + current.propertyInitializer.initializedCache.name = u"initializedCache"_s; + current.baselineCtor.name = current.cppType; current.externalCtor.name = current.cppType; current.init.name = u"QML_init"_s; @@ -259,19 +412,40 @@ void QmltcCompiler::compileType( QmltcVariable creator(u"QQmltcObjectCreationHelper*"_s, u"creator"_s); QmltcVariable engine(u"QQmlEngine*"_s, u"engine"_s); QmltcVariable parent(u"QObject*"_s, u"parent"_s, u"nullptr"_s); + QmltcVariable initializedCache( + u"[[maybe_unused]] const QSet<QString>&"_s, + u"initializedCache"_s, + u"{}"_s + ); QmltcVariable ctxtdata(u"const QQmlRefPointer<QQmlContextData>&"_s, u"parentContext"_s); QmltcVariable finalizeFlag(u"bool"_s, u"canFinalize"_s); current.baselineCtor.parameterList = { parent }; current.endInit.parameterList = { creator, engine }; - current.setComplexBindings.parameterList = { creator, engine }; + current.setComplexBindings.parameterList = { creator, engine, initializedCache }; current.handleOnCompleted.parameterList = { creator }; if (documentRoot || inlineComponent) { - current.externalCtor.parameterList = { engine, parent }; - current.init.parameterList = { creator, engine, ctxtdata, finalizeFlag }; + const QmltcVariable initializer( + u"[[maybe_unused]] qxp::function_ref<void(%1&)>"_s.arg(current.propertyInitializer.name), + u"initializer"_s, + u"[](%1&){}"_s.arg(current.propertyInitializer.name)); + + current.init.parameterList = { creator, engine, ctxtdata, finalizeFlag, initializer }; current.beginClass.parameterList = { creator, finalizeFlag }; current.completeComponent.parameterList = { creator, finalizeFlag }; current.finalizeComponent.parameterList = { creator, finalizeFlag }; + + compileRequiredPropertiesBundle(current, type, m_typeResolver); + + if (current.requiredPropertiesBundle) { + QmltcVariable bundle{ + u"const %1&"_s.arg(current.requiredPropertiesBundle->name), + u"requiredPropertiesBundle"_s, + }; + current.externalCtor.parameterList = { engine, bundle, parent, initializer }; + } else { + current.externalCtor.parameterList = { engine, parent, initializer }; + } } else { current.externalCtor.parameterList = { creator, engine, parent }; current.init.parameterList = { creator, engine, ctxtdata }; @@ -295,18 +469,7 @@ void QmltcCompiler::compileType( // compilation stub: current.externalCtor.body << u"Q_UNUSED(engine)"_s; if (documentRoot || inlineComponent) { - current.externalCtor.body << u"// document root:"_s; - // if it's document root, we want to create our QQmltcObjectCreationBase - // that would store all the created objects - current.externalCtor.body << u"QQmltcObjectCreationBase<%1> objectHolder;"_s.arg( - type->internalName()); - current.externalCtor.body - << u"QQmltcObjectCreationHelper creator = objectHolder.view();"_s; - current.externalCtor.body << u"creator.set(0, this);"_s; // special case - // now call init - current.externalCtor.body << current.init.name - + u"(&creator, engine, QQmlContextData::get(engine->rootContext()), /* " - u"endInit */ true);"; + compileRootExternalConstructorBody(current, type); } else { current.externalCtor.body << u"// not document root:"_s; // just call init, we don't do any setup here otherwise @@ -321,7 +484,7 @@ void QmltcCompiler::compileType( staticCreate.comments << u"Used by the engine for singleton creation."_s << u"See also \\l {https://doc.qt.io/qt-6/qqmlengine.html#QML_SINGLETON}."_s; - staticCreate.type = QQmlJSMetaMethod::StaticMethod; + staticCreate.type = QQmlJSMetaMethodType::StaticMethod; staticCreate.access = QQmlJSMetaMethod::Public; staticCreate.name = u"create"_s; staticCreate.returnType = u"%1 *"_s.arg(current.cppType); @@ -354,6 +517,102 @@ static Iterator partitionBindings(Iterator first, Iterator last) }); } +// Populates the propertyInitializer of the current type based on the +// available properties. +// +// A propertyInitializer is a generated class that provides a +// restricted interface that only allows setting property values and +// internally keep tracks of which properties where actually set, +// intended to be used to allow the user to set up the initial values +// when creating an instance of a component. +// +// For each property of the current type that is known, is not private +// and is writable, a setter method is generated. +// Each setter method knows how to set a specific property, so as to +// provide a strongly typed interface to property setting, as if the +// relevant C++ type was used directly. +// +// Each setter uses the write method for the proprerty when available +// and otherwise falls back to a the more generic +// `QObject::setProperty` for properties where a WRITE method is not +// available or in scope. +static void compilePropertyInitializer(QmltcType ¤t, const QQmlJSScope::ConstPtr &type) { + static auto isFromExtension = [](const QQmlJSMetaProperty &property, const QQmlJSScope::ConstPtr &scope) { + return scope->ownerOfProperty(scope, property.propertyName()).extensionSpecifier != QQmlJSScope::NotExtension; + }; + + current.propertyInitializer.constructor.initializerList << u"component{component}"_s; + + auto properties = type->properties().values(); + for (auto& property: properties) { + if (property.index() == -1) continue; + if (property.isPrivate()) continue; + if (!property.isWritable() && !qIsReferenceTypeList(property)) continue; + + const QString name = property.propertyName(); + + current.propertyInitializer.propertySetters.emplace_back(); + auto& compiledSetter = current.propertyInitializer.propertySetters.back(); + + compiledSetter.userVisible = true; + compiledSetter.returnType = u"void"_s; + compiledSetter.name = QmltcPropertyData(property).write; + + if (qIsReferenceTypeList(property)) { + compiledSetter.parameterList.emplaceBack( + QQmlJSUtils::constRefify(u"QList<%1*>"_s.arg(property.type()->valueType()->internalName())), + name + u"_", QString() + ); + } else { + compiledSetter.parameterList.emplaceBack( + QQmlJSUtils::constRefify(getUnderlyingType(property)), name + u"_", QString() + ); + } + + if (qIsReferenceTypeList(property)) { + compiledSetter.body << u"QQmlListReference list_ref_(&%1, \"%2\");"_s.arg( + current.propertyInitializer.component.name, name + ); + compiledSetter.body << u"list_ref_.clear();"_s; + compiledSetter.body << u"for (const auto& list_item_ : %1_)"_s.arg(name); + compiledSetter.body << u" list_ref_.append(list_item_);"_s; + } else if ( + QQmlJSUtils::bindablePropertyHasDefaultAccessor(property, QQmlJSUtils::PropertyAccessor_Write) + ) { + compiledSetter.body << u"%1.%2().setValue(%3_);"_s.arg( + current.propertyInitializer.component.name, property.bindable(), name); + } else if (type->hasOwnProperty(name)) { + compiledSetter.body << u"%1.%2(%3_);"_s.arg( + current.propertyInitializer.component.name, QmltcPropertyData(property).write, name); + } else if (property.write().isEmpty() || isFromExtension(property, type)) { + // We can end here if a WRITE method is not available or + // if the method is available but not in this scope, so + // that we fallback to the string-based setters.. + // + // For example, types that makes use of QML_EXTENDED + // types, will have the extension types properties + // available and with a WRITE method, but the WRITE method + // will not be available to the extended type, from C++, + // as the type does not directly inherit from the + // extension type. + // + // We specifically scope `setProperty` to `QObject` as + // certain types might have shadowed the method. + // For example, in QtQuick, some types have a property + // called `property` with a `setProperty` WRITE method + // that will produce the shadowing. + compiledSetter.body << u"%1.QObject::setProperty(\"%2\", QVariant::fromValue(%2_));"_s.arg( + current.propertyInitializer.component.name, name); + } else { + compiledSetter.body << u"%1.%2(%3_);"_s.arg( + current.propertyInitializer.component.name, property.write(), name); + } + + compiledSetter.body << u"%1.insert(\"%2\");"_s.arg( + current.propertyInitializer.initializedCache.name, name); + } +} + void QmltcCompiler::compileTypeElements(QmltcType ¤t, const QQmlJSScope::ConstPtr &type) { // compile components of a type: @@ -396,6 +655,7 @@ void QmltcCompiler::compileTypeElements(QmltcType ¤t, const QQmlJSScope::C auto bindings = type->ownPropertyBindingsInQmlIROrder(); partitionBindings(bindings.begin(), bindings.end()); + compilePropertyInitializer(current, type); compileBinding(current, bindings.begin(), bindings.end(), type, { type }); } @@ -450,7 +710,7 @@ compileMethodParameters(const QList<QQmlJSMetaParameter> ¶meterInfos, bool a static QString figureReturnType(const QQmlJSMetaMethod &m) { const bool isVoidMethod = - m.returnTypeName() == u"void" || m.methodType() == QQmlJSMetaMethod::Signal; + m.returnTypeName() == u"void" || m.methodType() == QQmlJSMetaMethodType::Signal; Q_ASSERT(isVoidMethod || m.returnType()); QString type; if (isVoidMethod) { @@ -467,10 +727,10 @@ void QmltcCompiler::compileMethod(QmltcType ¤t, const QQmlJSMetaMethod &m, const auto returnType = figureReturnType(m); const QList<QmltcVariable> compiledParams = compileMethodParameters(m.parameters()); - const auto methodType = QQmlJSMetaMethod::Type(m.methodType()); + const auto methodType = m.methodType(); QStringList code; - if (methodType != QQmlJSMetaMethod::Signal) { + if (methodType != QQmlJSMetaMethodType::Signal) { QmltcCodeGenerator urlGenerator { m_url, m_visitor }; QmltcCodeGenerator::generate_callExecuteRuntimeFunction( &code, urlGenerator.urlMethodName() + u"()", @@ -485,7 +745,7 @@ void QmltcCompiler::compileMethod(QmltcType ¤t, const QQmlJSMetaMethod &m, compiled.body = std::move(code); compiled.type = methodType; compiled.access = m.access(); - if (methodType != QQmlJSMetaMethod::Signal) { + if (methodType != QQmlJSMetaMethodType::Signal) { compiled.declarationPrefixes << u"Q_INVOKABLE"_s; compiled.userVisible = m.access() == QQmlJSMetaMethod::Public; } else { @@ -1057,7 +1317,7 @@ void QmltcCompiler::compileObjectBinding(QmltcType ¤t, const QQmlJSScope::ConstPtr &type, const BindingAccessorData &accessor) { - Q_ASSERT(binding.bindingType() == QQmlJSMetaPropertyBinding::Object); + Q_ASSERT(binding.bindingType() == QQmlSA::BindingType::Object); const QString &propertyName = binding.propertyName(); const QQmlJSMetaProperty property = type->property(propertyName); @@ -1115,7 +1375,7 @@ void QmltcCompiler::compileObjectBinding(QmltcType ¤t, Q_ASSERT(object->baseType()->internalName() == u"QQmlComponent"_s); if (int id = m_visitor->runtimeId(object); id >= 0) { - QString idString = m_visitor->addressableScopes().id(object); + QString idString = m_visitor->addressableScopes().id(object, object); if (idString.isEmpty()) idString = u"<unknown>"_s; QmltcCodeGenerator::generate_setIdValue(block, u"thisContext"_s, id, objectName, @@ -1154,8 +1414,8 @@ void QmltcCompiler::compileValueSourceOrInterceptorBinding(QmltcType ¤t, const QQmlJSScope::ConstPtr &type, const BindingAccessorData &accessor) { - Q_ASSERT(binding.bindingType() == QQmlJSMetaPropertyBinding::ValueSource - || binding.bindingType() == QQmlJSMetaPropertyBinding::Interceptor); + Q_ASSERT(binding.bindingType() == QQmlSA::BindingType::ValueSource + || binding.bindingType() == QQmlSA::BindingType::Interceptor); const QString &propertyName = binding.propertyName(); const QQmlJSMetaProperty property = type->property(propertyName); @@ -1163,7 +1423,7 @@ void QmltcCompiler::compileValueSourceOrInterceptorBinding(QmltcType ¤t, // NB: object is compiled with compileType(), here just need to use it QSharedPointer<const QQmlJSScope> object; - if (binding.bindingType() == QQmlJSMetaPropertyBinding::Interceptor) + if (binding.bindingType() == QQmlSA::BindingType::Interceptor) object = binding.interceptorType(); else object = binding.valueSourceType(); @@ -1212,7 +1472,7 @@ void QmltcCompiler::compileAttachedPropertyBinding(QmltcType ¤t, const QQmlJSScope::ConstPtr &type, const BindingAccessorData &accessor) { - Q_ASSERT(binding.bindingType() == QQmlJSMetaPropertyBinding::AttachedProperty); + Q_ASSERT(binding.bindingType() == QQmlSA::BindingType::AttachedProperty); const QString &propertyName = binding.propertyName(); const QQmlJSMetaProperty property = type->property(propertyName); @@ -1276,7 +1536,7 @@ void QmltcCompiler::compileGroupPropertyBinding(QmltcType ¤t, const QQmlJSScope::ConstPtr &type, const BindingAccessorData &accessor) { - Q_ASSERT(binding.bindingType() == QQmlJSMetaPropertyBinding::GroupProperty); + Q_ASSERT(binding.bindingType() == QQmlSA::BindingType::GroupProperty); const QString &propertyName = binding.propertyName(); const QQmlJSMetaProperty property = type->property(propertyName); @@ -1333,7 +1593,7 @@ void QmltcCompiler::compileGroupPropertyBinding(QmltcType ¤t, auto it = subbindings.begin(); Q_ASSERT(std::all_of(it, firstScript, [](const auto &x) { - return x.bindingType() != QQmlJSMetaPropertyBinding::Script; + return x.bindingType() != QQmlSA::BindingType::Script; })); compile(it, firstScript); it = firstScript; @@ -1354,7 +1614,7 @@ void QmltcCompiler::compileGroupPropertyBinding(QmltcType ¤t, // once the value is written back, process the script bindings Q_ASSERT(std::all_of(it, subbindings.end(), [](const auto &x) { - return x.bindingType() == QQmlJSMetaPropertyBinding::Script; + return x.bindingType() == QQmlSA::BindingType::Script; })); compile(it, subbindings.end()); } @@ -1368,8 +1628,8 @@ void QmltcCompiler::compileTranslationBinding(QmltcType ¤t, const QQmlJSScope::ConstPtr &type, const BindingAccessorData &accessor) { - Q_ASSERT(binding.bindingType() == QQmlJSMetaPropertyBinding::Translation - || binding.bindingType() == QQmlJSMetaPropertyBinding::TranslationById); + Q_ASSERT(binding.bindingType() == QQmlSA::BindingType::Translation + || binding.bindingType() == QQmlSA::BindingType::TranslationById); const QString &propertyName = binding.propertyName(); @@ -1446,7 +1706,7 @@ void QmltcCompiler::compileBinding(QmltcType ¤t, const auto location = binding.sourceLocation(); // make sure group property is not generalized by checking if type really has a property // called propertyName. If not, it is probably an id. - if (binding.bindingType() == QQmlJSMetaPropertyBinding::GroupProperty + if (binding.bindingType() == QQmlSA::BindingType::GroupProperty && type->hasProperty(propertyName)) { qCWarning(lcQmltcCompiler) << QStringLiteral("Binding at line %1 column %2 is not deferred as it is a " @@ -1493,26 +1753,32 @@ void QmltcCompiler::compileBindingByType(QmltcType ¤t, accessor.name, constructFromQObject); }; switch (binding.bindingType()) { - case QQmlJSMetaPropertyBinding::BoolLiteral: { + case QQmlSA::BindingType::BoolLiteral: { const bool value = binding.boolValue(); assignToProperty(metaProperty, value ? u"true"_s : u"false"_s); break; } - case QQmlJSMetaPropertyBinding::NumberLiteral: { + case QQmlSA::BindingType::NumberLiteral: { assignToProperty(metaProperty, QString::number(binding.numberValue())); break; } - case QQmlJSMetaPropertyBinding::StringLiteral: { - assignToProperty(metaProperty, QQmlJSUtils::toLiteral(binding.stringValue())); + case QQmlSA::BindingType::StringLiteral: { + QString value = QQmlJSUtils::toLiteral(binding.stringValue()); + if (auto type = metaProperty.type()) { + if (type->internalName() == u"QUrl"_s) { + value = u"QUrl(%1)"_s.arg(value); + } + } + assignToProperty(metaProperty, value); break; } - case QQmlJSMetaPropertyBinding::RegExpLiteral: { + case QQmlSA::BindingType::RegExpLiteral: { const QString value = u"QRegularExpression(%1)"_s.arg(QQmlJSUtils::toLiteral(binding.regExpValue())); assignToProperty(metaProperty, value); break; } - case QQmlJSMetaPropertyBinding::Null: { + case QQmlSA::BindingType::Null: { // poor check: null bindings are only supported for var and objects Q_ASSERT(propertyType->isSameType(m_typeResolver->varType()) || propertyType->accessSemantics() == QQmlJSScope::AccessSemantics::Reference); @@ -1522,38 +1788,38 @@ void QmltcCompiler::compileBindingByType(QmltcType ¤t, assignToProperty(metaProperty, u"QVariant::fromValue(nullptr)"_s); break; } - case QQmlJSMetaPropertyBinding::Script: { + case QQmlSA::BindingType::Script: { QString bindingSymbolName = type->internalName() + u'_' + propertyName + u"_binding"; bindingSymbolName.replace(u'.', u'_'); // can happen with group properties compileScriptBinding(current, binding, bindingSymbolName, type, propertyName, propertyType, accessor); break; } - case QQmlJSMetaPropertyBinding::Object: { + case QQmlSA::BindingType::Object: { compileObjectBinding(current, binding, type, accessor); break; } - case QQmlJSMetaPropertyBinding::Interceptor: + case QQmlSA::BindingType::Interceptor: Q_FALLTHROUGH(); - case QQmlJSMetaPropertyBinding::ValueSource: { + case QQmlSA::BindingType::ValueSource: { compileValueSourceOrInterceptorBinding(current, binding, type, accessor); break; } - case QQmlJSMetaPropertyBinding::AttachedProperty: { + case QQmlSA::BindingType::AttachedProperty: { compileAttachedPropertyBinding(current, binding, type, accessor); break; } - case QQmlJSMetaPropertyBinding::GroupProperty: { + case QQmlSA::BindingType::GroupProperty: { compileGroupPropertyBinding(current, binding, type, accessor); break; } - case QQmlJSMetaPropertyBinding::TranslationById: - case QQmlJSMetaPropertyBinding::Translation: { + case QQmlSA::BindingType::TranslationById: + case QQmlSA::BindingType::Translation: { compileTranslationBinding(current, binding, type, accessor); break; } - case QQmlJSMetaPropertyBinding::Invalid: { + case QQmlSA::BindingType::Invalid: { recordError(binding.sourceLocation(), u"This binding is invalid"_s); break; } @@ -1617,8 +1883,11 @@ static std::pair<QQmlJSMetaProperty, int> getMetaPropertyIndex(const QQmlJSScope // index is already added as p.index()) if (type->isSameType(owner)) return; - if (m == QQmlJSScope::ExtensionNamespace) // extension namespace properties are ignored + + // extension namespace and JavaScript properties are ignored + if (m == QQmlJSScope::ExtensionNamespace || m == QQmlJSScope::ExtensionJavaScript) return; + index += int(type->ownProperties().size()); }; QQmlJSUtils::traverseFollowingMetaObjectHierarchy(scope, owner, increment); @@ -1649,10 +1918,10 @@ void QmltcCompiler::compileScriptBinding(QmltcType ¤t, Q_ASSERT(!objectClassName_signal.isEmpty()); Q_ASSERT(!objectClassName_slot.isEmpty()); - const auto signalMethods = objectType->methods(name, QQmlJSMetaMethod::Signal); + const auto signalMethods = objectType->methods(name, QQmlJSMetaMethodType::Signal); Q_ASSERT(!signalMethods.isEmpty()); // an error somewhere else QQmlJSMetaMethod signal = signalMethods.at(0); - Q_ASSERT(signal.methodType() == QQmlJSMetaMethod::Signal); + Q_ASSERT(signal.methodType() == QQmlJSMetaMethodType::Signal); const QString signalName = signal.methodName(); const QString slotName = newSymbol(signalName + u"_slot"); @@ -1672,7 +1941,7 @@ void QmltcCompiler::compileScriptBinding(QmltcType ¤t, objectType->ownRuntimeFunctionIndex(binding.scriptIndex()), u"this"_s, // Note: because script bindings always use current QML object scope signalReturnType, slotParameters); - slotMethod.type = QQmlJSMetaMethod::Slot; + slotMethod.type = QQmlJSMetaMethodType::Slot; current.functions << std::move(slotMethod); current.setComplexBindings.body << u"QObject::connect(" + This_signal + u", " + u"&" @@ -1681,7 +1950,7 @@ void QmltcCompiler::compileScriptBinding(QmltcType ¤t, }; switch (binding.scriptKind()) { - case QQmlJSMetaPropertyBinding::Script_PropertyBinding: { + case QQmlSA::ScriptBindingKind::PropertyBinding: { if (!propertyType) { recordError(binding.sourceLocation(), u"Binding on property '" + propertyName + u"' of unknown type"); @@ -1723,19 +1992,20 @@ void QmltcCompiler::compileScriptBinding(QmltcType ¤t, property, valueTypeIndex, accessor.name); break; } - case QQmlJSMetaPropertyBinding::Script_SignalHandler: { - const auto name = QQmlJSUtils::signalName(propertyName); + case QQmlSA::ScriptBindingKind::SignalHandler: { + const auto name = QQmlSignalNames::handlerNameToSignalName(propertyName); Q_ASSERT(name.has_value()); compileScriptSignal(*name); break; } - case QQmlJSMetaPropertyBinding::Script_ChangeHandler: { + case QQmlSA ::ScriptBindingKind::ChangeHandler: { const QString objectClassName = objectType->internalName(); const QString bindingFunctorName = newSymbol(bindingSymbolName + u"Functor"); - const auto signalName = QQmlJSUtils::signalName(propertyName); + const auto signalName = QQmlSignalNames::handlerNameToSignalName(propertyName); Q_ASSERT(signalName.has_value()); // an error somewhere else - const auto actualProperty = QQmlJSUtils::changeHandlerProperty(objectType, *signalName); + const auto actualProperty = + QQmlJSUtils::propertyFromChangedHandler(objectType, propertyName); Q_ASSERT(actualProperty.has_value()); // an error somewhere else const auto actualPropertyType = actualProperty->type(); if (!actualPropertyType) { @@ -1759,9 +2029,12 @@ void QmltcCompiler::compileScriptBinding(QmltcType ¤t, current.children << compileScriptBindingPropertyChangeHandler( binding, objectType, m_urlMethodName, bindingFunctorName, objectClassName); + current.setComplexBindings.body << u"if (!%1.contains(\"%2\"))"_s.arg( + current.propertyInitializer.initializedCache.name, propertyName); + // TODO: this could be dropped if QQmlEngine::setContextForObject() is // done before currently generated C++ object is constructed - current.setComplexBindings.body << bindingSymbolName + u".reset(new QPropertyChangeHandler<" + current.setComplexBindings.body << u" "_s + bindingSymbolName + u".reset(new QPropertyChangeHandler<" + bindingFunctorName + u">(" + QmltcCodeGenerator::wrap_privateClass(accessor.name, *actualProperty) + u"->" + bindableString + u"().onValueChanged(" + bindingFunctorName + u"(" diff --git a/tools/qmltc/qmltccompiler.h b/tools/qmltc/qmltccompiler.h index d5f07aa3b0..3deab6d44e 100644 --- a/tools/qmltc/qmltccompiler.h +++ b/tools/qmltc/qmltccompiler.h @@ -25,6 +25,8 @@ struct QmltcCompilerInfo QString outputHFile; QString outputNamespace; QString resourcePath; + QString exportMacro; + QString exportInclude; }; class QmltcCompiler @@ -48,7 +50,7 @@ public: static bool isComplexBinding(const QQmlJSMetaPropertyBinding &binding) { // TODO: translation bindings (once supported) are also complex? - return binding.bindingType() == QQmlJSMetaPropertyBinding::Script; + return binding.bindingType() == QQmlSA::BindingType::Script; } private: @@ -185,14 +187,14 @@ private: bool hasErrors() const { return m_logger->hasErrors(); } void recordError(const QQmlJS::SourceLocation &location, const QString &message, - LoggerWarningId id = qmlCompiler) + QQmlJS::LoggerWarningId id = qmlCompiler) { // pretty much any compiler error is a critical error (we cannot // generate code - compilation fails) m_logger->log(message, id, location); } void recordError(const QV4::CompiledData::Location &location, const QString &message, - LoggerWarningId id = qmlCompiler) + QQmlJS::LoggerWarningId id = qmlCompiler) { recordError(QQmlJS::SourceLocation { 0, 0, location.line(), location.column() }, message, id); diff --git a/tools/qmltc/qmltccompilerpieces.cpp b/tools/qmltc/qmltccompilerpieces.cpp index 601cf1bbed..cd1735bc07 100644 --- a/tools/qmltc/qmltccompilerpieces.cpp +++ b/tools/qmltc/qmltccompilerpieces.cpp @@ -15,8 +15,8 @@ static QString scopeName(const QQmlJSScope::ConstPtr &scope) { Q_ASSERT(scope->isFullyResolved()); const auto scopeType = scope->scopeType(); - if (scopeType == QQmlJSScope::GroupedPropertyScope - || scopeType == QQmlJSScope::AttachedPropertyScope) { + if (scopeType == QQmlSA::ScopeType::GroupedPropertyScope + || scopeType == QQmlSA::ScopeType::AttachedPropertyScope) { return scope->baseType()->internalName(); } return scope->internalName(); @@ -237,15 +237,18 @@ void QmltcCodeGenerator::generate_createBindingOnProperty( } *block += prologue; - *block << value + u"->" + bindable + u"().setBinding(" + createBindingForBindable + u");"; + *block << u"if (!initializedCache.contains(\"%1\"))"_s.arg(p.propertyName()); + *block << u" "_s + value + u"->" + bindable + u"().setBinding(" + createBindingForBindable + u");"; *block += epilogue; } else { QString createBindingForNonBindable = - u"QT_PREPEND_NAMESPACE(QQmlCppBinding)::createBindingForNonBindable(" + unitVarName + u" "_s + + 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 + *block << u"if (!initializedCache.contains(\"%1\"))"_s.arg(p.propertyName()); *block << createBindingForNonBindable + u";"; } } @@ -293,8 +296,8 @@ void QmltcCodeGenerator::generate_createTranslationBindingOnProperty( QmltcCodeGenerator::PreparedValue QmltcCodeGenerator::wrap_mismatchingTypeConversion(const QQmlJSMetaProperty &p, QString value) { - auto isDerivedFromBuiltin = [](QQmlJSScope::ConstPtr t, const QString &builtin) { - for (; t; t = t->baseType()) { + auto isDerivedFromBuiltin = [](const QQmlJSScope::ConstPtr &derived, const QString &builtin) { + for (QQmlJSScope::ConstPtr t = derived; t; t = t->baseType()) { if (t->internalName() == builtin) return true; } @@ -302,7 +305,7 @@ QmltcCodeGenerator::wrap_mismatchingTypeConversion(const QQmlJSMetaProperty &p, }; QStringList prologue; QStringList epilogue; - auto propType = p.type(); + const QQmlJSScope::ConstPtr propType = p.type(); if (isDerivedFromBuiltin(propType, u"QVariant"_s)) { const QString variantName = u"var_" + p.propertyName(); prologue << u"{ // accepts QVariant"_s; diff --git a/tools/qmltc/qmltccompilerpieces.h b/tools/qmltc/qmltccompilerpieces.h index bfd22e1b79..3252f19e86 100644 --- a/tools/qmltc/qmltccompilerpieces.h +++ b/tools/qmltc/qmltccompilerpieces.h @@ -152,14 +152,17 @@ struct QmltcCodeGenerator /*! \internal - Generates \a{current.init}'s code. The init method sets up a QQmlContext for - the object and (in case \a type is a document root) calls other object - creation methods in a well-defined order: + Generates \a{current.init}'s code. The init method sets up a + QQmlContext for the object and (in case \a type is a document + root) calls other object creation methods, and a user-provided + initialization callback, in a well-defined order: 1. current.beginClass 2. current.endInit - 3. current.completeComponent - 4. current.finalizeComponent - 5. current.handleOnCompleted + 3. user-provided initialization function + 4. current.setComplexBindings + 5. current.completeComponent + 6. current.finalizeComponent + 7. current.handleOnCompleted This function returns a QScopeGuard with the final instructions that have to be generated at a later point, once everything else is compiled. @@ -260,7 +263,7 @@ inline decltype(auto) QmltcCodeGenerator::generate_initCode(QmltcType ¤t, if (int id = visitor->runtimeId(type); id >= 0) { current.init.body << u"// 3. set id since it is provided"_s; - QString idString = visitor->addressableScopes().id(type); + QString idString = visitor->addressableScopes().id(type, type); if (idString.isEmpty()) idString = u"<unknown>"_s; QmltcCodeGenerator::generate_setIdValue(¤t.init.body, u"context"_s, id, u"this"_s, @@ -294,8 +297,14 @@ inline decltype(auto) QmltcCodeGenerator::generate_initCode(QmltcType ¤t, .arg(current.beginClass.name); current.init.body << QStringLiteral(" %1(creator, engine);") .arg(current.endInit.name); - current.init.body << QStringLiteral(" %1(creator, engine);") - .arg(current.setComplexBindings.name); + + current.init.body << QStringLiteral(" {"); + current.init.body << QStringLiteral(" PropertyInitializer propertyInitializer(*this);"); + current.init.body << QStringLiteral(" initializer(propertyInitializer);"); + current.init.body << QStringLiteral(" %1(creator, engine, propertyInitializer.initializedCache);").arg(current.setComplexBindings.name); + current.init.body << QStringLiteral(" }"); + + current.init.body << QStringLiteral(" %1(creator, /* finalize */ true);") .arg(current.completeComponent.name); current.init.body << QStringLiteral(" %1(creator, /* finalize */ true);") diff --git a/tools/qmltc/qmltcoutputir.h b/tools/qmltc/qmltcoutputir.h index 8884ddc3a3..de531f718d 100644 --- a/tools/qmltc/qmltcoutputir.h +++ b/tools/qmltc/qmltcoutputir.h @@ -75,7 +75,7 @@ struct QmltcMethodBase struct QmltcMethod : QmltcMethodBase { QString returnType; // C++ return type - QQmlJSMetaMethod::Type type = QQmlJSMetaMethod::Method; // Qt function type + QQmlJSMetaMethodType type = QQmlJSMetaMethodType::Method; // Qt function type // TODO: should be a better way to handle this bool userVisible = false; // tells if a function is prioritized during the output generation @@ -92,6 +92,42 @@ struct QmltcDtor : QmltcMethodBase { }; +// Represents a generated class that knows how to set the public, +// writable properties of a compiled QML -> C++ type. +// This is generally intended to be available for the root of the +// document to allow the user to set the initial values for +// properties, when creating a component, with support for strong +// typing. +struct QmltcPropertyInitializer { + QString name; + + QmltcCtor constructor; + + // A member containing a reference to the object for which the + // properties should be set. + QmltcVariable component; + + // A member containing a cache of properties that were actually + // set that can be referenced later.. + QmltcVariable initializedCache; + + // Setter methods for each property. + QList<QmltcMethod> propertySetters; +}; + +// Represents a generated class that contains a bundle of values to +// initialize the required properties of a type. +// +// This is generally intended to be available for the root component +// of the document, where it will be used as a constructor argument to +// force the user to provide initial values for the required +// properties of the constructed type. +struct QmltcRequiredPropertiesBundle { + QString name; + + QList<QmltcVariable> members; +}; + // Represents QML -> C++ compiled type struct QmltcType { @@ -131,6 +167,12 @@ struct QmltcType // needed for singletons std::optional<QmltcMethod> staticCreate{}; + + // A proxy class that provides a restricted interface that only + // allows setting the properties of the type. + QmltcPropertyInitializer propertyInitializer{}; + + std::optional<QmltcRequiredPropertiesBundle> requiredPropertiesBundle{}; }; // Represents whole QML program, compiled to C++ @@ -140,6 +182,8 @@ struct QmltcProgram QString cppPath; // C++ output .cpp path QString hPath; // C++ output .h path QString outNamespace; + QString exportMacro; // if not empty, the macro that should be used to export the generated + // classes QSet<QString> includes; // non-default C++ include files QmltcMethod urlMethod; // returns QUrl of the QML document diff --git a/tools/qmltc/qmltcpropertyutils.h b/tools/qmltc/qmltcpropertyutils.h index db26c498dd..8a69e5ef09 100644 --- a/tools/qmltc/qmltcpropertyutils.h +++ b/tools/qmltc/qmltcpropertyutils.h @@ -6,6 +6,7 @@ #include <private/qqmljsmetatypes_p.h> #include <private/qqmljsscope_p.h> +#include <QtQml/private/qqmlsignalnames_p.h> QT_BEGIN_NAMESPACE @@ -35,13 +36,11 @@ struct QmltcPropertyData QmltcPropertyData(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"; - reset = u"reset" + nameWithUppercase; + write = QQmlSignalNames::addPrefixToPropertyName(u"set", propertyName); + bindable = QQmlSignalNames::addPrefixToPropertyName(u"bindable", propertyName); + notify = QQmlSignalNames::propertyNameToChangedSignalName(propertyName); + reset = QQmlSignalNames::addPrefixToPropertyName(u"reset", propertyName); } QString read; diff --git a/tools/qmltc/qmltctyperesolver.cpp b/tools/qmltc/qmltctyperesolver.cpp index 045f3af088..9c53686736 100644 --- a/tools/qmltc/qmltctyperesolver.cpp +++ b/tools/qmltc/qmltctyperesolver.cpp @@ -12,15 +12,12 @@ #include <QtCore/qfileinfo.h> #include <QtCore/qdiriterator.h> -Q_LOGGING_CATEGORY(lcTypeResolver2, "qml.qmltc.typeresolver", QtInfoMsg); +Q_STATIC_LOGGING_CATEGORY(lcTypeResolver2, "qml.qmltc.typeresolver", QtInfoMsg); void QmltcTypeResolver::init(QmltcVisitor *visitor, QQmlJS::AST::Node *program) { QQmlJSTypeResolver::init(visitor, program); - QQmlJSLiteralBindingCheck literalCheck; - literalCheck.run(visitor, this); - m_root = visitor->result(); QQueue<QQmlJSScope::Ptr> objects; diff --git a/tools/qmltc/qmltcvisitor.cpp b/tools/qmltc/qmltcvisitor.cpp index 236ad76467..a6ec1f8661 100644 --- a/tools/qmltc/qmltcvisitor.cpp +++ b/tools/qmltc/qmltcvisitor.cpp @@ -8,6 +8,7 @@ #include <QtCore/qstack.h> #include <QtCore/qdir.h> #include <QtCore/qloggingcategory.h> +#include <QtQml/private/qqmlsignalnames_p.h> #include <private/qqmljsutils_p.h> @@ -101,6 +102,9 @@ void QmltcVisitor::findCppIncludes() // look in type addCppInclude(type); + if (type->isListProperty()) + addCppInclude(type->valueType()); + // look in type's base type auto base = type->baseType(); if (!base && type->isComposite()) @@ -133,8 +137,9 @@ void QmltcVisitor::findCppIncludes() Q_ASSERT(type); const auto scopeType = type->scopeType(); - if (scopeType != QQmlJSScope::QMLScope && scopeType != QQmlJSScope::GroupedPropertyScope - && scopeType != QQmlJSScope::AttachedPropertyScope) { + if (scopeType != QQmlSA::ScopeType::QMLScope + && scopeType != QQmlSA::ScopeType::GroupedPropertyScope + && scopeType != QQmlSA::ScopeType::AttachedPropertyScope) { continue; } @@ -174,7 +179,7 @@ void QmltcVisitor::findCppIncludes() static void addCleanQmlTypeName(QStringList *names, const QQmlJSScope::ConstPtr &scope) { - Q_ASSERT(scope->scopeType() == QQmlJSScope::QMLScope); + Q_ASSERT(scope->scopeType() == QQmlSA::ScopeType::QMLScope); Q_ASSERT(!scope->isArrayScope()); Q_ASSERT(!scope->baseTypeName().isEmpty()); // the scope is guaranteed to be a new QML type, so any prefixes (separated @@ -197,7 +202,7 @@ bool QmltcVisitor::visit(QQmlJS::AST::UiObjectDefinition *object) } // we're not interested in non-QML scopes - if (m_currentScope->scopeType() != QQmlJSScope::QMLScope) + if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope) return true; if (m_currentScope->isInlineComponent()) { @@ -216,7 +221,7 @@ bool QmltcVisitor::visit(QQmlJS::AST::UiObjectDefinition *object) void QmltcVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *object) { - if (m_currentScope->scopeType() == QQmlJSScope::QMLScope) + if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) m_qmlTypeNames.removeLast(); QQmlJSImportVisitor::endVisit(object); } @@ -268,7 +273,7 @@ bool QmltcVisitor::visit(QQmlJS::AST::UiPublicMember *publicMember) owner->addOwnProperty(property); } - const QString notifyName = name + u"Changed"_s; + const QString notifyName = QQmlSignalNames::propertyNameToChangedSignalName(name); // also check that notify is already a method of the scope { auto owningScope = m_savedBindingOuterScope ? m_savedBindingOuterScope : m_currentScope; @@ -280,7 +285,7 @@ bool QmltcVisitor::visit(QQmlJS::AST::UiPublicMember *publicMember) u"internal error: %1 found for property '%2'"_s.arg(errorString, name), qmlCompiler, publicMember->identifierToken); return false; - } else if (methods[0].methodType() != QQmlJSMetaMethod::Signal) { + } else if (methods[0].methodType() != QQmlJSMetaMethodType::Signal) { m_logger->log(u"internal error: method %1 of property %2 must be a signal"_s.arg( notifyName, name), qmlCompiler, publicMember->identifierToken); @@ -336,17 +341,17 @@ void QmltcVisitor::endVisit(QQmlJS::AST::UiProgram *program) for (const QList<QQmlJSScope::ConstPtr> &qmlTypes : m_pureQmlTypes) for (const QQmlJSScope::ConstPtr &type : qmlTypes) - checkForNamingCollisionsWithCpp(type); + checkNamesAndTypes(type); } QQmlJSScope::ConstPtr fetchType(const QQmlJSMetaPropertyBinding &binding) { switch (binding.bindingType()) { - case QQmlJSMetaPropertyBinding::Object: + case QQmlSA::BindingType::Object: return binding.objectType(); - case QQmlJSMetaPropertyBinding::Interceptor: + case QQmlSA::BindingType::Interceptor: return binding.interceptorType(); - case QQmlJSMetaPropertyBinding::ValueSource: + case QQmlSA::BindingType::ValueSource: return binding.valueSourceType(); // TODO: AttachedProperty and GroupProperty are not supported yet, // but have to also be acknowledged here @@ -435,7 +440,7 @@ void QmltcVisitor::postVisitResolve( // match scopes to indices of QmlIR::Object from QmlIR::Document qsizetype count = 0; const auto setIndex = [&](const QQmlJSScope::Ptr ¤t) { - if (current->scopeType() != QQmlJSScope::QMLScope || current->isArrayScope()) + if (current->scopeType() != QQmlSA::ScopeType::QMLScope || current->isArrayScope()) return; Q_ASSERT(!m_qmlIrObjectIndices.contains(current)); m_qmlIrObjectIndices[current] = count; @@ -547,9 +552,9 @@ void QmltcVisitor::postVisitResolve( if (scope->isArrayScope()) // special kind of QQmlJSScope::QMLScope return; switch (scope->scopeType()) { - case QQmlJSScope::QMLScope: - case QQmlJSScope::GroupedPropertyScope: - case QQmlJSScope::AttachedPropertyScope: { + case QQmlSA::ScopeType::QMLScope: + case QQmlSA::ScopeType::GroupedPropertyScope: + case QQmlSA::ScopeType::AttachedPropertyScope: { ++qmlScopeCount[scope->enclosingInlineComponentName()]; break; } @@ -653,7 +658,7 @@ void QmltcVisitor::setupAliases() } } -void QmltcVisitor::checkForNamingCollisionsWithCpp(const QQmlJSScope::ConstPtr &type) +void QmltcVisitor::checkNamesAndTypes(const QQmlJSScope::ConstPtr &type) { static const QString cppKeywords[] = { u"alignas"_s, @@ -672,20 +677,21 @@ void QmltcVisitor::checkForNamingCollisionsWithCpp(const QQmlJSScope::ConstPtr & u"case"_s, u"catch"_s, u"char"_s, - u"char8_t"_s, u"char16_t"_s, u"char32_t"_s, + u"char8_t"_s, u"class"_s, + u"co_await"_s, + u"co_return"_s, + u"co_yield"_s, u"compl"_s, u"concept"_s, u"const"_s, + u"const_cast"_s, u"consteval"_s, u"constexpr"_s, - u"const_cast"_s, + u"constinit"_s, u"continue"_s, - u"co_await"_s, - u"co_return"_s, - u"co_yield"_s, u"decltype"_s, u"default"_s, u"delete"_s, @@ -753,6 +759,7 @@ void QmltcVisitor::checkForNamingCollisionsWithCpp(const QQmlJSScope::ConstPtr & u"xor"_s, u"xor_eq"_s, }; + Q_ASSERT(std::is_sorted(std::begin(cppKeywords), std::end(cppKeywords))); const auto isReserved = [&](QStringView word) { if (word.startsWith(QChar(u'_')) && word.size() >= 2 @@ -769,6 +776,23 @@ void QmltcVisitor::checkForNamingCollisionsWithCpp(const QQmlJSScope::ConstPtr & qmlCompiler, type->sourceLocation()); }; + const auto validateType = [&type, this](const QQmlJSScope::ConstPtr &typeToCheck, + QStringView name, QStringView errorPrefix) { + if (type->moduleName().isEmpty() || typeToCheck.isNull()) + return; + + if (typeToCheck->isComposite() && typeToCheck->moduleName() != type->moduleName()) { + m_logger->log( + QStringLiteral( + "Can't compile the %1 type \"%2\" to C++ because it " + "lives in \"%3\" instead of the current file's \"%4\" QML module.") + .arg(errorPrefix, name, typeToCheck->moduleName(), type->moduleName()), + qmlCompiler, type->sourceLocation()); + } + }; + + validateType(type->baseType(), type->baseTypeName(), u"QML base"); + const auto enums = type->ownEnumerations(); for (auto it = enums.cbegin(); it != enums.cend(); ++it) { const QQmlJSMetaEnum e = it.value(); @@ -783,16 +807,23 @@ void QmltcVisitor::checkForNamingCollisionsWithCpp(const QQmlJSScope::ConstPtr & for (auto it = properties.cbegin(); it != properties.cend(); ++it) { const QQmlJSMetaProperty &p = it.value(); validate(p.propertyName(), u"Property"); + + if (!p.isAlias() && !p.typeName().isEmpty()) + validateType(p.type(), p.typeName(), u"QML property"); } const auto methods = type->ownMethods(); for (auto it = methods.cbegin(); it != methods.cend(); ++it) { const QQmlJSMetaMethod &m = it.value(); validate(m.methodName(), u"Method"); + if (!m.returnTypeName().isEmpty()) + validateType(m.returnType(), m.returnTypeName(), u"QML method return"); - const auto parameterNames = m.parameterNames(); - for (const auto &name : parameterNames) - validate(name, u"Method '%1' parameter"_s.arg(m.methodName())); + for (const auto ¶meter : m.parameters()) { + validate(parameter.name(), u"Method '%1' parameter"_s.arg(m.methodName())); + if (!parameter.typeName().isEmpty()) + validateType(parameter.type(), parameter.typeName(), u"QML parameter"); + } } // TODO: one could also test signal handlers' parameters but we do not store diff --git a/tools/qmltc/qmltcvisitor.h b/tools/qmltc/qmltcvisitor.h index 0ec9349527..111df0e885 100644 --- a/tools/qmltc/qmltcvisitor.h +++ b/tools/qmltc/qmltcvisitor.h @@ -22,7 +22,7 @@ class QmltcVisitor : public QQmlJSImportVisitor void postVisitResolve(const QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> &qmlIrOrderedBindings); void setupAliases(); - void checkForNamingCollisionsWithCpp(const QQmlJSScope::ConstPtr &type); + void checkNamesAndTypes(const QQmlJSScope::ConstPtr &type); void setRootFilePath(); QString sourceDirectoryPath(const QString &path); @@ -57,7 +57,7 @@ public: qsizetype creationIndex(const QQmlJSScope::ConstPtr &type) const { - Q_ASSERT(type->scopeType() == QQmlJSScope::QMLScope); + Q_ASSERT(type->scopeType() == QQmlSA::ScopeType::QMLScope); return m_creationIndices.value(type, -1); } @@ -68,13 +68,13 @@ public: qsizetype qmlComponentIndex(const QQmlJSScope::ConstPtr &type) const { - Q_ASSERT(type->scopeType() == QQmlJSScope::QMLScope); + Q_ASSERT(type->scopeType() == QQmlSA::ScopeType::QMLScope); return m_syntheticTypeIndices.value(type, -1); } qsizetype qmlIrObjectIndex(const QQmlJSScope::ConstPtr &type) const { - Q_ASSERT(type->scopeType() == QQmlJSScope::QMLScope); + Q_ASSERT(type->scopeType() == QQmlSA::ScopeType::QMLScope); Q_ASSERT(m_qmlIrObjectIndices.contains(type)); return m_qmlIrObjectIndices.value(type, -1); } diff --git a/tools/qmltime/qmltime.h b/tools/qmltime/qmltime.h index 7812600456..fa0f44a5e4 100644 --- a/tools/qmltime/qmltime.h +++ b/tools/qmltime/qmltime.h @@ -38,6 +38,5 @@ private: QQuickView m_view; QQuickItem *m_item; }; -QML_DECLARE_TYPE(Timer); #endif // QMLTIME_H diff --git a/tools/qmltyperegistrar/qmltyperegistrar.cpp b/tools/qmltyperegistrar/qmltyperegistrar.cpp index 5d895bbed7..937c8a356f 100644 --- a/tools/qmltyperegistrar/qmltyperegistrar.cpp +++ b/tools/qmltyperegistrar/qmltyperegistrar.cpp @@ -3,12 +3,14 @@ #include <QCoreApplication> #include <QCommandLineParser> +#include <QDir> #include <QFile> #include <QScopedPointer> #include <cstdlib> #include <QtQmlTypeRegistrar/private/qqmltyperegistrar_p.h> +#include <QtQmlTypeRegistrar/private/qqmltyperegistrarutils_p.h> using namespace Qt::Literals; @@ -17,6 +19,11 @@ int main(int argc, char **argv) // Produce reliably the same output for the same input by disabling QHash's random seeding. QHashSeed::setDeterministicGlobalSeed(); + // No, you are not supposed to mess with the message pattern. + // Qt Creator wants to read those messages as-is and we want the convenience + // of QDebug to print them. + qputenv("QT_MESSAGE_PATTERN", "%{if-category}%{category}: %{endif}%{message}"); + QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("qmltyperegistrar")); QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); @@ -88,6 +95,13 @@ int main(int argc, char **argv) "want to follow Qt's versioning scheme.")); parser.addOption(followForeignVersioningOption); + QCommandLineOption jsroot(QStringLiteral("jsroot")); + jsroot.setDescription( + QStringLiteral("Use the JavaScript root object's meta types as sole input and do not " + "generate any C++ output. Only useful in combination with " + "--generate-qmltypes")); + parser.addOption(jsroot); + QCommandLineOption extract(u"extract"_s); extract.setDescription( u"Extract QML types from a module and use QML_FOREIGN to register them"_s); @@ -104,20 +118,43 @@ int main(int argc, char **argv) const QString module = parser.value(importNameOption); + const QLatin1String jsrootMetaTypes + = QLatin1String(":/qt-project.org/meta_types/jsroot_metatypes.json"); + QStringList files = parser.positionalArguments(); + if (parser.isSet(jsroot)) { + if (parser.isSet(extract)) { + error(module) << "If --jsroot is passed, no type registrations can be extracted."; + return EXIT_FAILURE; + } + if (parser.isSet(outputOption)) { + error(module) << "If --jsroot is passed, no C++ output can be generated."; + return EXIT_FAILURE; + } + if (!files.isEmpty() || parser.isSet(foreignTypesOption)) { + error(module) << "If --jsroot is passed, no further metatypes can be processed."; + return EXIT_FAILURE; + } + + files.append(jsrootMetaTypes); + } + MetaTypesJsonProcessor processor(parser.isSet(privateIncludesOption)); - if (!processor.processTypes(parser.positionalArguments())) + if (!processor.processTypes(files)) return EXIT_FAILURE; processor.postProcessTypes(); - if (parser.isSet(foreignTypesOption)) - processor.processForeignTypes(parser.value(foreignTypesOption).split(QLatin1Char(','))); + if (!parser.isSet(jsroot)) { + processor.processForeignTypes(jsrootMetaTypes); + if (parser.isSet(foreignTypesOption)) + processor.processForeignTypes(parser.value(foreignTypesOption).split(QLatin1Char(','))); + } processor.postProcessForeignTypes(); if (parser.isSet(extract)) { if (!parser.isSet(outputOption)) { - fprintf(stderr, "Error: The output file name must be provided\n"); + error(module) << "The output file name must be provided"; return EXIT_FAILURE; } QString baseName = parser.value(outputOption); @@ -137,25 +174,35 @@ int main(int argc, char **argv) parser.isSet(followForeignVersioningOption)); typeRegistrar.setTypes(processor.types(), processor.foreignTypes()); - if (parser.isSet(outputOption)) { - // extract does its own file handling - QString outputName = parser.value(outputOption); - QFile file(outputName); - if (!file.open(QIODeviceBase::WriteOnly)) { - fprintf(stderr, "Error: Cannot open %s for writing\n", qPrintable(outputName)); - return EXIT_FAILURE; + if (!parser.isSet(jsroot)) { + if (module.isEmpty()) { + warning(module) << "The module name is empty. Cannot generate C++ code"; + } else if (parser.isSet(outputOption)) { + // extract does its own file handling + QString outputName = parser.value(outputOption); + QFile file(outputName); + if (!file.open(QIODeviceBase::WriteOnly)) { + error(QDir::toNativeSeparators(outputName)) + << "Cannot open file for writing:" << file.errorString(); + return EXIT_FAILURE; + } + QTextStream output(&file); + typeRegistrar.write(output, outputName); + } else { + QTextStream output(stdout); + typeRegistrar.write(output, "stdout"); } - QTextStream output(&file); - typeRegistrar.write(output); - } else { - QTextStream output(stdout); - typeRegistrar.write(output); } if (!parser.isSet(pluginTypesOption)) return EXIT_SUCCESS; typeRegistrar.setReferencedTypes(processor.referencedTypes()); - typeRegistrar.generatePluginTypes(parser.value(pluginTypesOption)); + typeRegistrar.setUsingDeclarations(processor.usingDeclarations()); + const QString qmltypes = parser.value(pluginTypesOption); + if (!typeRegistrar.generatePluginTypes(qmltypes)) { + error(qmltypes) << "Cannot generate qmltypes file"; + return EXIT_FAILURE; + } return EXIT_SUCCESS; } diff --git a/tools/shared/qqmltoolingsettings.cpp b/tools/shared/qqmltoolingsettings.cpp deleted file mode 100644 index dffd59fbc7..0000000000 --- a/tools/shared/qqmltoolingsettings.cpp +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "qqmltoolingsettings.h" - -#include <algorithm> - -#include <QtCore/qdebug.h> -#include <QtCore/qdir.h> -#include <QtCore/qfileinfo.h> -#include <QtCore/qset.h> -#include <QtCore/qsettings.h> -#include <QtCore/qstandardpaths.h> - -using namespace Qt::StringLiterals; - -void QQmlToolingSettings::addOption(const QString &name, QVariant defaultValue) -{ - m_values[name] = defaultValue; -} - -bool QQmlToolingSettings::read(const QString &settingsFilePath) -{ - if (!QFileInfo::exists(settingsFilePath)) - return false; - - if (m_currentSettingsPath == settingsFilePath) - return true; - - QSettings settings(settingsFilePath, QSettings::IniFormat); - - for (const QString &key : settings.allKeys()) - m_values[key] = settings.value(key).toString(); - - m_currentSettingsPath = settingsFilePath; - - return true; -} - -bool QQmlToolingSettings::writeDefaults() const -{ - const QString path = QFileInfo(u".%1.ini"_s.arg(m_toolName)).absoluteFilePath(); - - QSettings settings(path, QSettings::IniFormat); - for (auto it = m_values.constBegin(); it != m_values.constEnd(); ++it) { - settings.setValue(it.key(), it.value().isNull() ? QString() : it.value()); - } - - settings.sync(); - - if (settings.status() != QSettings::NoError) { - qWarning() << "Failed to write default settings to" << path - << "Error:" << settings.status(); - return false; - } - - qInfo() << "Wrote default settings to" << path; - return true; -} - -bool QQmlToolingSettings::search(const QString &path) -{ - QFileInfo fileInfo(path); - QDir dir(fileInfo.isDir() ? path : fileInfo.dir()); - - QSet<QString> dirs; - - const QString settingsFileName = u".%1.ini"_s.arg(m_toolName); - - while (dir.exists() && dir.isReadable()) { - const QString dirPath = dir.absolutePath(); - - if (m_seenDirectories.contains(dirPath)) { - const QString cachedIniPath = m_seenDirectories[dirPath]; - if (cachedIniPath.isEmpty()) - return false; - - return read(cachedIniPath); - } - - dirs << dirPath; - - const QString iniFile = dir.absoluteFilePath(settingsFileName); - - if (read(iniFile)) { - for (const QString &dir : std::as_const(dirs)) - m_seenDirectories[dir] = iniFile; - return true; - } - - if (!dir.cdUp()) - break; - } - - if (const QString iniFile = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, u"%1.ini"_s.arg(m_toolName)); !iniFile.isEmpty()) { - if (read(iniFile)) { - for (const QString &dir : std::as_const(dirs)) - m_seenDirectories[dir] = iniFile; - return true; - } - } - - // No INI file found anywhere, record the failure so we won't have to traverse the entire - // filesystem again - for (const QString &dir : std::as_const(dirs)) - m_seenDirectories[dir] = QString(); - - return false; -} - -QVariant QQmlToolingSettings::value(QString name) const -{ - return m_values.value(name); -} - -bool QQmlToolingSettings::isSet(QString name) const -{ - if (!m_values.contains(name)) - return false; - - QVariant variant = m_values[name]; - - // Unset is encoded as an empty string - return !(variant.canConvert(QMetaType(QMetaType::QString)) && variant.toString().isEmpty()); -} diff --git a/tools/shared/qqmltoolingsettings.h b/tools/shared/qqmltoolingsettings.h deleted file mode 100644 index 483147b3e9..0000000000 --- a/tools/shared/qqmltoolingsettings.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#ifndef QQMLTOOLINGSETTINGS_H -#define QQMLTOOLINGSETTINGS_H - -#include <QtCore/qstring.h> -#include <QtCore/qhash.h> -#include <QtCore/qvariant.h> - -class QQmlToolingSettings -{ -public: - QQmlToolingSettings(const QString &toolName) : m_toolName(toolName) { } - - void addOption(const QString &name, const QVariant defaultValue = QVariant()); - - bool writeDefaults() const; - bool search(const QString &path); - - QVariant value(QString name) const; - bool isSet(QString name) const; - -private: - QString m_toolName; - QString m_currentSettingsPath; - QHash<QString, QString> m_seenDirectories; - QVariantHash m_values; - - bool read(const QString &settingsFilePath); -}; - -#endif diff --git a/tools/svgtoqml/CMakeLists.txt b/tools/svgtoqml/CMakeLists.txt new file mode 100644 index 0000000000..fd6c91e7ea --- /dev/null +++ b/tools/svgtoqml/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## svgtoqml Tool: +##################################################################### + +qt_get_tool_target_name(target_name svgtoqml) +qt_internal_add_tool(${target_name} + TARGET_DESCRIPTION "SVG to QML Converter" + TOOLS_TARGET Quick + SOURCES + main.cpp + LIBRARIES + Qt::Core + Qt::Gui + Qt::Qml + Qt::Quick + Qt::QuickVectorImageGeneratorPrivate +) +qt_internal_return_unless_building_tools() + +set(resource_files + "main.qml" +) + +qt_internal_add_resource(${target_name} "qml" + PREFIX + "/" + FILES + ${resource_files} +) diff --git a/tools/svgtoqml/main.cpp b/tools/svgtoqml/main.cpp new file mode 100644 index 0000000000..51c2f741ca --- /dev/null +++ b/tools/svgtoqml/main.cpp @@ -0,0 +1,141 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QGuiApplication> +#include <QQmlApplicationEngine> +#include <QCommandLineParser> +#include <QFile> +#include <QQuickWindow> +#include <QQuickItem> +#include <QtQuickVectorImageGenerator/private/qquickitemgenerator_p.h> +#include <QtQuickVectorImageGenerator/private/qquickqmlgenerator_p.h> +#include <QtQuickVectorImageGenerator/private/qquickvectorimageglobal_p.h> + +#define ENABLE_GUI + +int main(int argc, char *argv[]) +{ +#ifdef ENABLE_GUI + QGuiApplication app(argc, argv); +#else + QCoreApplication app(argc, argv); +#endif + + QCommandLineParser parser; + parser.setApplicationDescription("SVG to QML converter [tech preview]"); + parser.addHelpOption(); + parser.addPositionalArgument("input", QCoreApplication::translate("main", "SVG file to read.")); + parser.addPositionalArgument("output", QCoreApplication::translate("main", "QML file to write."), "[output]"); + + QCommandLineOption optimizeOption("optimize-paths", + QCoreApplication::translate("main", "Optimize paths for the curve renderer.")); + parser.addOption(optimizeOption); + + QCommandLineOption curveRendererOption("curve-renderer", + QCoreApplication::translate("main", "Use the curve renderer in generated QML.")); + parser.addOption(curveRendererOption); + + QCommandLineOption typeNameOption(QStringList() << "t" << "type-name", + QCoreApplication::translate("main", "Use <typename> for Shape."), + QCoreApplication::translate("main", "typename")); + parser.addOption(typeNameOption); + + QCommandLineOption copyrightOption("copyright-statement", + QCoreApplication::translate("main", "Add <string> as a comment at the start of the generated file."), + QCoreApplication::translate("main", "string")); + parser.addOption(copyrightOption); + + QCommandLineOption outlineModeOption("outline-stroke-mode", + QCoreApplication::translate("main", "Stroke the outline (contour) of the filled shape instead of " + "the original path. Also sets optimize-paths.")); + parser.addOption(outlineModeOption); + + QCommandLineOption keepPathsOption("keep-external-paths", + QCoreApplication::translate("main", "Any paths to external files will be retained in the QML output. " + "The paths will be reformatted as relative to the output file. If " + "this is not enabled, copies of the file will be saved to the asset output " + "directory. Embedded data will still be saved to files, even if " + "this option is set.")); + parser.addOption(keepPathsOption); + + QCommandLineOption assetOutputDirectoryOption("asset-output-directory", + QCoreApplication::translate("main", "If the SVG refers to external or embedded files, such as images, these " + "will be copied into the same directory as the output QML file by default. " + "Set the asset output directory to override the location."), + QCoreApplication::translate("main", "directory")); + parser.addOption(assetOutputDirectoryOption); + + QCommandLineOption assetOutputPrefixOption("asset-output-prefix", + QCoreApplication::translate("main", "If the SVG refers to external or embedded files, such as images, these " + "will be copied to files with unique identifiers. By default, the files will be prefixed " + "with \"svg_asset_\". Set the asset output prefix to override the prefix."), + QCoreApplication::translate("main", "prefix")); + parser.addOption(assetOutputPrefixOption); + +#ifdef ENABLE_GUI + QCommandLineOption guiOption(QStringList() << "v" << "view", + QCoreApplication::translate("main", "Display the generated QML in a window. This is the default behavior if no " + "output file is specified.")); + parser.addOption(guiOption); +#endif + parser.process(app); + const QStringList args = parser.positionalArguments(); + if (args.size() < 1) { + parser.showHelp(1); + } + + const QString inFileName = args.at(0); + + QString commentString = QLatin1String("Generated from SVG file %1").arg(inFileName); + + const auto outFileName = args.size() > 1 ? args.at(1) : QString{}; + const auto typeName = parser.value(typeNameOption); + const auto assetOutputDirectory = parser.value(assetOutputDirectoryOption); + const auto assetOutputPrefix = parser.value(assetOutputPrefixOption); + const bool keepPaths = parser.isSet(keepPathsOption); + auto copyrightString = parser.value(copyrightOption); + + if (!copyrightString.isEmpty()) { + copyrightString = copyrightString.replace("\\n", "\n"); + commentString = copyrightString + u"\n" + commentString; + } + + QQuickVectorImageGenerator::GeneratorFlags flags; + if (parser.isSet(curveRendererOption)) + flags |= QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer; + if (parser.isSet(optimizeOption)) + flags |= QQuickVectorImageGenerator::GeneratorFlag::OptimizePaths; + if (parser.isSet(outlineModeOption)) + flags |= (QQuickVectorImageGenerator::GeneratorFlag::OutlineStrokeMode + | QQuickVectorImageGenerator::GeneratorFlag::OptimizePaths); + + QQuickQmlGenerator generator(inFileName, flags, outFileName); + generator.setShapeTypeName(typeName); + generator.setCommentString(commentString); + generator.setAssetFileDirectory(assetOutputDirectory); + generator.setAssetFilePrefix(assetOutputPrefix); + generator.setRetainFilePaths(keepPaths); + bool ok = generator.generate(); + +#ifdef ENABLE_GUI + if (ok && (parser.isSet(guiOption) || outFileName.isEmpty())) { + app.setOrganizationName("QtProject"); + const QUrl url(QStringLiteral("qrc:/main.qml")); + QQmlApplicationEngine engine; + QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, + &app, [&](QObject *obj, const QUrl &objUrl){ + if (!obj && url == objUrl) + QCoreApplication::exit(-1); + if (obj) { + auto *containerItem = obj->findChild<QQuickItem*>(QStringLiteral("svg_item")); + QQuickItemGenerator generator(inFileName, flags, containerItem); + generator.generate(); + } + }); + engine.load(url); + return app.exec(); + } +#endif + + return ok ? 0 : 1; +} diff --git a/tools/svgtoqml/main.qml b/tools/svgtoqml/main.qml new file mode 100644 index 0000000000..9a8ceefe5d --- /dev/null +++ b/tools/svgtoqml/main.qml @@ -0,0 +1,21 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Shapes +import QtQuick.Controls +import QtQuick.Layouts + +Window { + id: mainWindow + width: 1280 + height: 960 + visible: true + color: "white" + + Item { + id: svg + objectName: "svg_item" + anchors.centerIn: parent + } +} |