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 .

注意: There are two types of passes that plugins can register, ElementPasses ,和 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 文本 elements, we need a reference to the 文本 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());
    }
}
					

注意: 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));
					

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
					

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 文本 does contain the expected greeting, so there's no warning
  • The second 文本 would at runtime have the wrong warning ( "Hello" 而不是 "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 文本 element, we correctly warn about the wrong greeting.
  • As NotText does not derive from 文本 , the analysis will skip it, as the 继承 check will discard it.
  • The custom MyText element inherits from 文本 , 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