How to become friends with qmllint
As Qt developers, we produce many lines of QML code every day. Of course, we are all aware of the importance of maintainable, well-organised pieces. We know the best practices, and we try to follow them every time we add new code to the existing base. However, as our project becomes more complex, it can be difficult to keep track of all the best practices and ensure that the code meets the required standards. That’s one of the reasons why you should consider qmllint as a candidate for your new friend.
In this article, I want to show you how I work with qmllint and how you can integrate qmllint checks into your daily tasks.
What is qmllint?
In short, qmllint is a powerful linter tool designed explicitly for QML. Its main purpose is to check the syntactic validity of QML files and to warn about some QML anti-patterns. It is easily configurable so that we can enable checking and printing only the warnings that are interesting to us. I always enable all of them to make sure that the QML code is written without any mistakes.
For example, this tool can warn you about:
- Unqualified access to properties
- Issues related to compiling QML code
- Deprecated code
- Many others…
Using the tool will definitely make it easier to apply the best practices for clean code and to keep the code in the best possible shape.
Where to begin
I have prepared a short code snippet for this article. Of course, the mistakes in the code are made on purpose to demonstrate the usage of this tool.
Here is the start-up code:
import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
readonly property int delegateWidth: 640
readonly property int delegateHeight: 30
ListModel {
id: items
ListElement {
name: "red"
textColor: "red"
}
ListElement {
name: "blue"
textColor: "blue"
}
ListElement {
name: "green"
textColor: "green"
}
}
ListView {
id: listView
anchors.fill: parent
model: items
delegate: Label {
text: model.name
color: model.textColor
width: delegateWidth
height: delegateHeight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
}
}
Now, we can check the code with this tool:
1. Make sure the path to directory where qmllint.exe is located is added to environment variables.
2. Simply run command.
3. qmllint <pathToFile>
4. Check the console output.
As you can see, qmllint gives us some warnings even in this short piece of code. I know you will never make mistakes like this, but believe me, your teammates might, or even you might have a bad day and forget to follow the best practices.
Let’s take a closer look at the output. We have some unqualified access warnings and the information that the model was implicitly injected into the delegate. It not only prints the warnings but also gives us information on how to get rid of them. So let’s follow the suggestions.
5. Fix the printed warnings following the output.
6. import QtQuick
7. import QtQuick.Window
8. import QtQuick.Controls
9.
10. Window {
11. id: root //Added id to the root component
12. width: 640
13. height: 480
14. visible: true
15. title: qsTr("Hello World")
16.
17. readonly property int delegateWidth: 640
18. readonly property int delegateHeight: 30
19.
20. ListModel {
21. id: items
22.
23. ListElement {
24. name: "red"
25. textColor: "red"
26. }
27. ListElement {
28. name: "blue"
29. textColor: "blue"
30. }
31. ListElement {
32. name: "green"
33. textColor: "green"
34. }
35. }
36.
37. ListView {
38. id: listView
39. anchors.fill: parent
40. model: items
41.
42. delegate: Label {
43. //Added required property to delegate
44. required property string name
45. required property string textColor
46.
47. text: name
48. color: textColor
49.
50. //Added id to directly access the properties from root.
51. width: root.delegateWidth
52. height: root.delegateHeight
53.
54. horizontalAlignment: Qt.AlignHCenter
55. verticalAlignment: Qt.AlignVCenter
56. }
57. }
58. }
6. Rerun qmllint.
It found another warning and provided us with information on how it could be fixed. Let’s make the change.
//Added necessary pragma at the top of the file
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello World")
readonly property int delegateWidth: 640
readonly property int delegateHeight: 30
ListModel {
id: items
ListElement {
name: "red"
textColor: "red"
}
ListElement {
name: "blue"
textColor: "blue"
}
ListElement {
name: "green"
textColor: "green"
}
}
ListView {
id: listView
anchors.fill: parent
model: items
delegate: Label {
required property string name
required property string textColor
text: name
color: textColor
width: root.delegateWidth
height: root.delegateHeight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
}
}
The next run of qmllint will not print anything. We fixed the whole file.
Ok, so we have finished our first qmllint check. What now?
It would be great to configure the qmllint settings to be able to check all the files with one command, make qmllint aware of external modules, store the output somewhere, and maybe we can give it more power so that it can resolve our warnings automatically.
Configuration of qmllint
qmllint can be configured separately for each project. This can be useful if you want to disable some of the warnings or provide a path to additional modules.
It is possible to achieve the same result by running the qmllint command with additional options, but it is much better to configure qmllint once in a separate file. It can generate the default configuration file for us.
qmllint –write-defaults
The .qmllint.ini file is created in the directory from which we executed the command. qmllint is aware of these settings if it lint the file located in the same directory as settings or child directories. You can ignore these settings if you wish or override some of the options in the command line.
//ignore settings file
qmllint --ignore-settings
//overwrite CompilerWarnings option
qmllint --compiler warning
Now we can specify which category should be disabled or treated as an info message. In our case, I recommend enabling CompilerWarnings. This will warn you about the parts of the qml code that cannot be compiled by qmlsc.
[Warnings]
ImportFailure=warning
ReadOnlyProperty=warning
BadSignalHandlerParameters=warning
UnusedImports=info
DuplicatedName=warning
PrefixedImportType=warning
AccessSingletonViaObject=warning
Deprecated=warning
ControlsSanity=disable
UnresolvedType=warning
LintPluginWarnings=disable
MultilineStrings=info
RestrictedType=warning
PropertyAliasCycles=warning
VarUsedBeforeDeclaration=warning
AttachedPropertyReuse=disable
RequiredProperty=warning
WithStatement=warning
InheritanceCycle=warning
UnqualifiedAccess=warning
UncreatableType=warning
MissingProperty=warning
InvalidLintDirective=warning
**CompilerWarnings=warning**
UseProperFunction=warning
NonListProperty=warning
IncompatibleType=warning
TopLevelComponent=warning
MissingType=warning
DuplicatePropertyBinding=warning
[General]
DisableDefaultImports=false
AdditionalQmlImportPaths=
OverwriteImportTypes=
DisablePlugins=
ResourcePath=
Ensure awareness of modules
Let’s extend our application a little bit so that it will use some modules. For example, we want to create a custom button that will be used as a delegate in our ListView. The button can also be used in different parts of the application, so it’s fine to put it in a separate module.
File structure:
//Main.qml
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import CustomControls
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello World")
readonly property int delegateWidth: 640
readonly property int delegateHeight: 30
ListModel {
id: items
ListElement {
name: "red"
textColor: "red"
}
ListElement {
name: "blue"
textColor: "blue"
}
ListElement {
name: "green"
textColor: "green"
}
ListElement {
name: "black"
textColor: "black"
}
}
ListView {
id: listView
anchors.fill: parent
model: items
delegate: CustomButton {
required property string name
required property string textColor
text: name
color: textColor
}
}
}
//CustomButton.qml
import QtQuick
import QtQuick.Controls.Fusion as Fusion
Fusion.Button {
id: control
property alias color: content.color
implicitWidth: 640
implicitHeight: 30
contentItem: Fusion.Label {
id: content
text: control.text
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
Let’s lint Main.qml.
Why are there so many warnings? We only made minor changes to the file, such as adding a module and changing the label to CustomButton. The reason is that the tool is not aware of the modules we have created. So, how can we make it aware of the modules?
There are two ways:
1. We can run qmllint with an additional option.
qmllint -I imports Main.qml
2. Add the path to module parent dir in the settings file.
AdditionalQmlImportPaths=imports
Let’s run qmllint again.
The module is now read correctly, and all warnings have disappeared without any changes to the code. There is only one message left saying that the file has unused import. This is left over from the previous version of the code when we used Label. It is easy to forget to remove the imports that are no longer needed. So, every time a new module is created, we need to ensure the path is correctly passed to qmllint settings.
Storing the output in JSON file
In some situations, it is not sufficient to view the output directly in the console. For example, the output may be so long that reading the warnings is no longer clear, or the report needs to be stored somewhere for further analysis. It allows us to easily store the output in JSON format.
qmllint –json report.json Main.qml
The above command creates a report.json file. The results of linting the Main.qml file are stored in this file.
//report.json
{
"files": [
{
"filename": "C:/Users/kaj/Documents/qmllint_test/Main.qml",
"success": true,
"warnings": [
{
"charOffset": 74,
"column": 1,
"id": "unused-imports",
"length": 6,
"line": 5,
"message": "Unused import",
"suggestions": [],
"type": "info"
}
]
}
],
"revision": 3
}
Checking all QML files in the application
Ok, we know how to check the single file. However, for larger projects, it will be much better to be able to check all the files in the project with a single qmllint check. It is possible to pass multiple files as arguments. QMLllint will check them all. However, this is still not the best solution.
qmllint Main.qml imports/CustomControl/CustomButton.qml
I recommend hiding the execution of the command in a script. In my case, I write a simple Python script that checks for all qml files in the directory and subdirectories and runs the qmllint on them. The result is stored in the report.json file.
import os
import subprocess
import sys
command = ["qmllint"]
json_file = "report.json"
directory = os.getcwd()
qml_files = []
command += ["--json", json_file]
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith(".qml"):
path = os.path.join(root, file)
qml_files.append(path)
if not qml_files:
print(f"No .qml files found in {directory}")
sys.exit(1)
command += qml_files
print(f"Linting {len(qml_files)} QML files in {directory}")
with open(json_file, "w") as f:
subprocess.run(command, stdout=f, stderr=subprocess.PIPE)
Self-sufficient qmllint
In a large project, we may have a very long list of warnings in several files. Most of them may be related to “unqualified access”. The fix is trivial, but it will certainly take some time to apply. In this case, we can delegate qmllint to do it for us. Let’s try it on the previous version of the code with Label as delegate for our ListView:
delegate: Label {
required property string name
required property string textColor
text: name
color: textColor
width: delegateWidth
height: delegateHeight
}
qmllint output:
Now we are sure that the warnings are easy to fix, and qmllint knows exactly how to fix them. So, we can allow it to fix it automatically. The only thing we need to do is to run the same command with an additional option “-f”
qmllint -f Main.qml
The fix is applied to Main.qml and the previous version of the file is stored in Main.qml.bak file, that we can backup it in case something goes wrong. Main.qml after fix:
delegate: Label {
required property string name
required property string textColor
text: name
color: textColor
width: root.delegateWidth
height: root.delegateHeight
}
As we can see, it does exactly the same thing that we do, but we can save some time by delegating the work to qmllint.
Summary
qmllint is a very useful tool for Qt/QML developers. If used often and in the right way, it can ensure that your qml files are written without errors.
I hope I have shown you why you should use qmllint in your project and how to use it. Of course, there are many more ways to use it, but the ones I have described in the article, in my opinion, are the most important. The next step for you is to integrate the qmllint checks into your CI pipeline and see how many errors you missed. Remember that qmllint is your friend and will always tell you when you made a mistake.
Our experts will help you build high-quality, feature-rich and user-friendly applications using Qt. Visit our Qt offering page to find out more.
About the author
RECOMMENDED ARTICLES