QML Static Analysis 2 - Custom Pass | Qt QML Compiler 6.8.3

- Qt 6.8
- Qt QML Compiler
- QML Static Analysis 2 - Custom Pass
- Qt 6.8.3 Reference Documentation

QML Static Analysis 1 - Basic Setup ◦ QML Static Analysis 3 - Fixit Hints

# QML Static Analysis 2 - Custom Pass

This chapter shows how custom analysis passes can be added to qmllint, by extending the plugin we've created in the last chapter. For demonstration purposes, we will create a plugin which checks whether Text elements have "Hello world!" assigned to their text property.

To do this, we create a new class derived from ElementPass.

Note: There are two types of passes that plugins can register, ElementPasses, and PropertyPasses. In this tutorial, we will only consider the simpler `ElementPass`.

     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;
     };

As our `HelloWorldElementPass` should analyze `Text` elements, we need a reference to the `Text` type. We can use the 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.

     HelloWorldElementPass::HelloWorldElementPass(QQmlSA::PassManager *manager)
         : QQmlSA::ElementPass(manager)
     {
         m_textType = resolveType("QtQuick", "Text");
     }

The actual logic of our pass happens in two functions: shouldRun and run. They will run on all Elements in the file that gets analyzed by qmllint.

In our `shouldRun` method, we check whether the current Element is derived from Text, and check whether it has a binding on the text property.

     bool HelloWorldElementPass::shouldRun(const QQmlSA::Element &element)
     {
         if (!element.inherits(m_textType))
             return false;
         if (!element.hasOwnPropertyBindings(u"text"_s))
             return false;
         return true;
     }

Only elements passing the checks there will then be analyzed by our pass via its `run` method. It would be possible to do all checking inside of `run` itself, but it is generally preferable to have a separation of concerns – both for performance and to enhance code readability.

In our `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.

     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());
         }
     }

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 Behavior assigned to the same property.

Lastly, we need to create an instance of our pass, and register it with the PassManager. This is done by adding

         manager->registerElementPass(std::make_unique<HelloWorldElementPass>(manager));

to the `registerPasses` functions of our plugin.

We can test our plugin by invoking qmllint on an example file via

     qmllint -P /path/to/the/directory/containing/the/plugin --Plugin.HelloWorld.hello-world info test.qml

If `test.qml` looks like

     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!" }
          }
     }

we will get

     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!" }

as the output. We can make a few observations here:

- The first `Text` does contain the expected greeting, so there's no warning
- The second `Text` would at runtime have the wrong warning (`"Hello"` instead of `"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.
- For the literal binding in the third `Text` element, we correctly warn about the wrong greeting.
- As `NotText` does not derive from `Text`, the analysis will skip it, as the `inherits` check will discard it.
- The custom `MyText` element inherits from `Text`, and consequently we see the expected warning.

In summary, we've seen the steps necessary to extend `qmllint` with custom passes, and have also become aware of the limitations of static checks.

QML Static Analysis 1 - Basic Setup ◦ QML Static Analysis 3 - Fixit Hints