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.