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 fromText
, the analysis will skip it, as theinherits
check will discard it. - The custom
MyText
element inherits fromText
, 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.