diff options
12 files changed, 454 insertions, 0 deletions
diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter1/CMakeLists.txt b/examples/qmlcompiler/tutorials/helloworld/chapter1/CMakeLists.txt new file mode 100644 index 0000000000..b9466b61b3 --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter1/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.21) + +project(hello_world_plugin VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS QmlCompiler) + +qt_standard_project_setup( + REQUIRES 6.6 +) + +qt_add_plugin(HelloWorldPlugin) + +target_sources(HelloWorldPlugin + PRIVATE + helloplugin.h + helloplugin.cpp +) + +target_link_libraries(HelloWorldPlugin PRIVATE Qt::QmlCompiler) diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter1/helloplugin.cpp b/examples/qmlcompiler/tutorials/helloworld/chapter1/helloplugin.cpp new file mode 100644 index 0000000000..ad36e64fae --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter1/helloplugin.cpp @@ -0,0 +1,19 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +#include "helloplugin.h" +#include <QDebug> + +using namespace Qt::StringLiterals; + +static constexpr QQmlSA::LoggerWarningId helloWorld { "Plugin.HelloWorld.hello-world" }; + +void HelloWorldPlugin::registerPasses(QQmlSA::PassManager *manager, const QQmlSA::Element &rootElement) +{ + const bool pluginIsEnabled = manager->isCategoryEnabled(helloWorld); + qDebug() << "Hello World plugin is" << (pluginIsEnabled ? "enabled" : "disabled"); + if (!pluginIsEnabled) + return; // skip registration if the plugin is disabled anyway + // here we will later register our passes +} + +#include "moc_helloplugin.cpp" diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter1/helloplugin.h b/examples/qmlcompiler/tutorials/helloworld/chapter1/helloplugin.h new file mode 100644 index 0000000000..6666c78be0 --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter1/helloplugin.h @@ -0,0 +1,21 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef HELLO_PLUGIN_H +#define HELLO_PLUGIN_H + +#include <QtCore/qobject.h> +#include <QtQmlCompiler/qqmlsa.h> + +class HelloWorldPlugin : public QObject, public QQmlSA::LintPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QmlLintPluginInterface_iid FILE "plugin.json") + Q_INTERFACES(QQmlSA::LintPlugin) + +public: + void registerPasses(QQmlSA::PassManager *manager, const QQmlSA::Element &rootElement) override; +}; + +#endif + diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter1/plugin.json b/examples/qmlcompiler/tutorials/helloworld/chapter1/plugin.json new file mode 100644 index 0000000000..3d8c1ff206 --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter1/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "HelloWorld", + "author": "Qt Example", + "description": "Demonstrates how to write a qmllint plugin", + "version": "1.0", + "loggingCategories": [ + { + "name": "hello-world", + "settingsName": "HelloWorld", + "description": "Used to create test messages" + } + ] +} diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter1/test.qml b/examples/qmlcompiler/tutorials/helloworld/chapter1/test.qml new file mode 100644 index 0000000000..99af71ad7d --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter1/test.qml @@ -0,0 +1,24 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Item { + id: root + + property string greeting: "Hello" + + component MyText : Text {} + + component NotText : Item { + property string text + } + + Text { text: "Hello world!" } + Text { text: root.greeting } + Text { text: "Goodbye world!" } + NotText { + text: "Does not trigger" + MyText { text: "Goodbye world!" } + } +} diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter2/CMakeLists.txt b/examples/qmlcompiler/tutorials/helloworld/chapter2/CMakeLists.txt new file mode 100644 index 0000000000..b9466b61b3 --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter2/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.21) + +project(hello_world_plugin VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS QmlCompiler) + +qt_standard_project_setup( + REQUIRES 6.6 +) + +qt_add_plugin(HelloWorldPlugin) + +target_sources(HelloWorldPlugin + PRIVATE + helloplugin.h + helloplugin.cpp +) + +target_link_libraries(HelloWorldPlugin PRIVATE Qt::QmlCompiler) diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter2/helloplugin.cpp b/examples/qmlcompiler/tutorials/helloworld/chapter2/helloplugin.cpp new file mode 100644 index 0000000000..431ad97f16 --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter2/helloplugin.cpp @@ -0,0 +1,56 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "helloplugin.h" +#include <QDebug> + +using namespace Qt::StringLiterals; + +static constexpr QQmlSA::LoggerWarningId helloWorld { "Plugin.HelloWorld.hello-world" }; + +class HelloWorldElementPass : public QQmlSA::ElementPass +{ +public: + HelloWorldElementPass(QQmlSA::PassManager *manager); + bool shouldRun(const QQmlSA::Element &element) override; + void run(const QQmlSA::Element &element) override; +private: + QQmlSA::Element m_textType; +}; + +HelloWorldElementPass::HelloWorldElementPass(QQmlSA::PassManager *manager) + : QQmlSA::ElementPass(manager) +{ + m_textType = resolveType("QtQuick", "Text"); +} + +bool HelloWorldElementPass::shouldRun(const QQmlSA::Element &element) +{ + if (!element.inherits(m_textType)) + return false; + if (!element.hasOwnPropertyBindings(u"text"_s)) + return false; + return true; +} + +void HelloWorldElementPass::run(const QQmlSA::Element &element) +{ + auto textBindings = element.ownPropertyBindings(u"text"_s); + for (const auto &textBinding: textBindings) { + if (textBinding.bindingType() != QQmlSA::BindingType::StringLiteral) + continue; + if (textBinding.stringValue() != u"Hello world!"_s) + emitWarning("Incorrect greeting", helloWorld, textBinding.sourceLocation()); + } +} + +void HelloWorldPlugin::registerPasses(QQmlSA::PassManager *manager, const QQmlSA::Element &rootElement) +{ + const bool pluginIsEnabled = manager->isCategoryEnabled(helloWorld); + qDebug() << "Hello World plugin is" << (pluginIsEnabled ? "enabled" : "disabled"); + if (!pluginIsEnabled) + return; // skip registration if the plugin is disabled anyway + manager->registerElementPass(std::make_unique<HelloWorldElementPass>(manager)); +} + +#include "moc_helloplugin.cpp" diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter2/helloplugin.h b/examples/qmlcompiler/tutorials/helloworld/chapter2/helloplugin.h new file mode 100644 index 0000000000..6666c78be0 --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter2/helloplugin.h @@ -0,0 +1,21 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef HELLO_PLUGIN_H +#define HELLO_PLUGIN_H + +#include <QtCore/qobject.h> +#include <QtQmlCompiler/qqmlsa.h> + +class HelloWorldPlugin : public QObject, public QQmlSA::LintPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QmlLintPluginInterface_iid FILE "plugin.json") + Q_INTERFACES(QQmlSA::LintPlugin) + +public: + void registerPasses(QQmlSA::PassManager *manager, const QQmlSA::Element &rootElement) override; +}; + +#endif + diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter2/plugin.json b/examples/qmlcompiler/tutorials/helloworld/chapter2/plugin.json new file mode 100644 index 0000000000..3d8c1ff206 --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter2/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "HelloWorld", + "author": "Qt Example", + "description": "Demonstrates how to write a qmllint plugin", + "version": "1.0", + "loggingCategories": [ + { + "name": "hello-world", + "settingsName": "HelloWorld", + "description": "Used to create test messages" + } + ] +} diff --git a/examples/qmlcompiler/tutorials/helloworld/chapter2/test.qml b/examples/qmlcompiler/tutorials/helloworld/chapter2/test.qml new file mode 100644 index 0000000000..99af71ad7d --- /dev/null +++ b/examples/qmlcompiler/tutorials/helloworld/chapter2/test.qml @@ -0,0 +1,24 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Item { + id: root + + property string greeting: "Hello" + + component MyText : Text {} + + component NotText : Item { + property string text + } + + Text { text: "Hello world!" } + Text { text: root.greeting } + Text { text: "Goodbye world!" } + NotText { + text: "Does not trigger" + MyText { text: "Goodbye world!" } + } +} diff --git a/src/qmlcompiler/doc/qtqmlcompiler.qdocconf b/src/qmlcompiler/doc/qtqmlcompiler.qdocconf index 5ac1c7ef45..d5b45c2731 100644 --- a/src/qmlcompiler/doc/qtqmlcompiler.qdocconf +++ b/src/qmlcompiler/doc/qtqmlcompiler.qdocconf @@ -45,3 +45,5 @@ navigation.cppclassespage = "Qt QML Compiler C++ Classes" # Enforce zero documentation warnings warninglimit = 0 +exampledirs += \ + ../../../examples/qmlcompiler diff --git a/src/qmlcompiler/doc/src/tutorial.qdoc b/src/qmlcompiler/doc/src/tutorial.qdoc new file mode 100644 index 0000000000..8376deb633 --- /dev/null +++ b/src/qmlcompiler/doc/src/tutorial.qdoc @@ -0,0 +1,217 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! +\page qqmlsa-tutorial.html +\title QML Static Analysis Tutorial +\brief An introduction on writing your own qmllint checks +\nextpage QML Static Analysis 1 - Basic Setup + +This tutorial gives an introduction to QQmlSA, the API to statically analyze QML code. + +Through the different steps of this tutorial we will learn about how to set up a basic qmllint +plugin, we will create our own custom analysis pass, and we will add support for fixit-hints. + +Chapter one starts with a minimal plugin and the following chapters introduce new concepts. + +The tutorial's source code is located in the \c{examples/qmlcompiler/tutorials/helloworld} directory. + +Tutorial chapters: + +\list 1 +\li \l {QML Static Analysis 1 - Basic Setup}{Basic Setup} +\li \l {QML Static Analysis 2 - Custom Pass}{Custom Pass} +\li \l {QML Static Analysis 3 - Fixit Hints}{Fixit Hints} +\endlist + +*/ + +/*! +\page qqmlsa-tutorial1.html +\title QML Static Analysis 1 - Basic Setup +\previouspage QML Static Analysis Tutorial +\nextpage QML Static Analysis 2 - Custom Pass + +This chapter introduces the basic structure of a qmllint extension plugin, +and how it can be used with qmllint. + +To create our plugin, we first need to make the QmlCompiler module available: +\quotefromfile tutorials/helloworld/chapter1/CMakeLists.txt +\skipto find_package +\printline find_package + +We then create a plugin, and link it against the QmlCompiler module. + +\skipto qt_add_plugin(HelloWorldPlugin) +\printuntil + +The implementation follows the pattern for \l{The High-Level API: Writing Qt Extensions}{extending +Qt with a plugin}: We subclass the \l{QQmlSA::LintPlugin}, +\quotefromfile tutorials/helloworld/chapter1/helloplugin.h +\skipto class HelloWorldPlugin +\printuntil }; + +The plugin references a \c{plugin.json} file, which contains some important information: +\quotefile tutorials/helloworld/chapter1/plugin.json +\c{name}, \c{author}, \c{version} and \c{description} are meta-data to describe the plugin. + +Each plugin can have one or more logging categories, which are used to thematically group +warnings. +\c{loggingCategories} takes an array of categories, each consisting of +\list + \li \c{name}, which is used to identify the category, and as the flag name in qmllint, + \li \c{settingsName}, which is used to configure the category in qmllint.ini + \li \c{description}, which should describe what kind of warning messages are tagged with the category +\endlist + +In our example, we have only one logging category, \c{hello-world}. For each category, we have to +create a \l{QQmlJS::LoggerWarningId}{LoggerWarningId} object in our plugin. + +\quotefromfile tutorials/helloworld/chapter1/helloplugin.cpp +\skipto static constexpr QQmlSA::LoggerWarningId +\printline static constexpr QQmlSA::LoggerWarningId + +We construct it with a string literal which should have the format +\c{Plugin.<plugin name>.<category name>}. + +Lastly, for our plugin to actually do anything useful, we have to implement the +\c{registerPasses} function. + +\skipto void HelloWorldPlugin::registerPasses( +\printuntil } + +We check whether our category is enabled, and print a diagnostic indicating its status +via \l{qDebug}. +Note that the plugin currently does not do anything useful, as we do not register any +passes. This will done in the next part of this tutorial. + +We can however already verify that our plugin is correctly detected. We use the following command to +check it: + +\badcode +qmllint -P /path/to/the/directory/containing/the/plugin --Plugin.HelloWorld.hello-world info test.qml +\endcode + +The \c{-P} options tells qmllint to look for plugins in the specified folder. To avoid conflicts +with Qt internal categories, plugin categories are always prefixed with "Plugin", then followed by +a dot, the plugin name, another dot and finally the category. +Running the command should print \c{Hello World Plugin is enabled}. + +*/ + + +/*! +\page qqmlsa-tutorial2.html +\title QML Static Analysis 2 - Custom Pass +\previouspage QML Static Analysis 1 - Basic Setup +\nextpage QML Static Analysis 3 - Fixit Hints + +This chapter shows how custom analysis passes can be added to \l{qmllint}, +by extending the plugin we've created in the last chapter. +For demonstration purposes, we will create a plugin which checks whether +\l{Text} elements have "Hello world!" assigned to their text property. + +To do this, we create a new class derived from +\l{QQmlSA::ElementPass}{ElementPass}. + +\note There are two types of passes that +plugins can register, \l{QQmlSA::ElementPass}{ElementPasses}, and \l{QQmlSA::PropertyPass}{PropertyPasses}. +In this tutorial, we will only consider the simpler \c{ElementPass}. + +\quotefromfile tutorials/helloworld/chapter2/helloplugin.cpp +\skipto class HelloWorldElementPass : public QQmlSA::ElementPass +\printuntil }; + +As our \c{HelloWorldElementPass} should analyze \c{Text} elements, +we need a reference to the \c{Text} type. We can use the +\l{QQmlSA::GenericPass::resolveType}{resolveType} function to obtain it. +As we don't want to constantly re-resolve the type, we do this +once in the constructor, and store the type in a member variable. + +\skipto HelloWorldElementPass::HelloWorldElementPass( +\printuntil } + +The actual logic of our pass happens in two functions: +\l{QQmlSA::ElementPass::shouldRun}{shouldRun} and +\l{QQmlSA::ElementPass::run}{run}. They will run on +all Elements in the file that gets analyzed by qmllint. + +In our \c{shouldRun} method, we check whether the current +Element is derived from Text, and check whether it has +a binding on the text property. +\skipto bool HelloWorldElementPass::shouldRun +\printuntil } + +Only elements passing the checks there will then be analyzed by our +pass via its \c{run} method. It would be possible to do all checking +inside of \c{run} itself, but it is generally preferable to have +a separation of concerns – both for performance and to enhance +code readability. + +In our \c{run} function, we retrieve the bindings to the text +property. If the bound value is a string literal, we check +if it's the greeting we expect. + +\skipto void HelloWorldElementPass::run +\printuntil /^}/ + +\note Most of the time, a property will only have one +binding assigned to it. However, there might be for +instance a literal binding and a \l{Behavior} assigned +to the same property. + +Lastly, we need to create an instance of our pass, and +register it with the \l{QQmlSA::PassManager}{PassManager}. This is done by +adding +\skipto manager->registerElementPass +\printline manager->registerElementPass +to the \c{registerPasses} functions of our plugin. + +We can test our plugin by invoking \l{qmllint} on an example +file via +\badcode +qmllint -P /path/to/the/directory/containing/the/plugin --Plugin.HelloWorld.hello-world info test.qml +\endcode + +If \c{test.qml} looks like +\quotefromfile{tutorials/helloworld/chapter2/test.qml} +\skipto import +\printuntil + +we will get +\badcode +Info: test.qml:22:26: Incorrect greeting [Plugin.HelloWorld.hello-world] + MyText { text: "Goodbye world!" } + ^^^^^^^^^^^^^^^^ +Info: test.qml:19:19: Incorrect greeting [Plugin.HelloWorld.hello-world] + Text { text: "Goodbye world!" } +\endcode + +as the output. We can make a few observations here: +\list +\li The first \c{Text} does contain the expected greeting, so there's no warning +\li The second \c{Text} would at runtime have the wrong warning (\c{"Hello"} + instead of \c{"Hello world"}. However, this cannot be detected by qmllint + (in general), as there's no literal binding, but a binding to another property. + As we only check literal bindings, we simply skip over this binding. +\li For the literal binding in the third \c{Text} element, we correctly warn about + the wrong greeting. +\li As \c{NotText} does not derive from \c{Text}, the analysis will skip it, as + the \c{inherits} check will discard it. +\li The custom \c{MyText} element inherits from \c{Text}, and consequently we + see the expected warning. +\endlist + +In summary, we've seen the steps necessary to extend \c{qmllint} with custom passes, +and have also become aware of the limitations of static checks. + +*/ + +/*! +\page qqmlsa-tutorial3.html +\title QML Static Analysis 3 - Fixit Hints +\previouspage QML Static Analysis 2 - Custom Pass + +In this chapter we learn how to improve our custom warnings by amending them +with fixit hints. +*/ |
