The QML Language Server provides the information gathered by the QML Linter through the Language Server Protocol (LSP) to an editor of your liking. LSP is an open, JSON-RPC-based protocol that enables an editor to talk to a language server that supplies language-specific features like code completion, refactoring capabilities, and warning markers. Kindly refer to your editor’s documentation on how to configure it to run the qmlls binary that is distributed alongside Qt Qml.
Configuring the linter and language server
The build system sets up linter targets automatically. In order for the language server to find your imports, however, it also needs to be configured. It’s possible to specify the “-b” (build directory) argument when running qmlls manually but an editor will normally just run a single instance and point it at the file being edited. Using CMake’s configure_file feature we can generate the configuration file and place it in our source directory automatically. Create a qmlls.ini.in as follows:
[General]
buildDir=${QT_QML_OUTPUT_DIRECTORY}
configure_file(qmlls.ini.in ${CMAKE_SOURCE_DIR}/.qmlls.ini)
Qt 6.7 can actually do this for you when the project is configured with -DQT_QML_GENERATE_QMLLS_INI=ON. Note that the resulting configuration file is specific to the particular build and should not be checked into version control. You might want to add it to your .gitignore file.
A project can likewise provide a .qmllint.ini in its source directory. This file configures which checks the linter performs and which of them should be treated as a fatal error. A default file which can then be tweaked is created by running:
$ qmllint --write-defaults
Function type annotations
Consider a stop watch and a simple function to format the elapsed time display:
function formatElapsedTime(time) {
//: Elapsed seconds
return qsTr("%L1 s").arg(time / 1000);
}
function formatElapsedTime(time : int) : string {
//: Elapsed seconds
return qsTr("%L1 s").arg(time / 1000);
}
Despite the type annotations, it’s not possible to overload JavaScript functions: there can only be one “formatElapsedTime” function in our component. Calling the function is also still possible with unexpected types: while a function taking a Rectangle will throw a TypeError when called with a Button (unless Button implements Rectangle, of course), the formatElapsedTime function will happily accept a string for “time” and then try to interpret it as an int.
Additionally, an annotated function interfaces better with C++. Rather than having to wrap its arguments in QVariant, the actual types can be used on the C++ side as well:
QString text;
QMetaObject::invokeMethod(timerPage, "formatElapsedTime", qReturnArg(text), 1200);
qDebug() << text; // "1.2 s"
Simple as that. Also note the new invokeMethod syntax introduced in Qt 6.5, no more Q_ARG.
QML compiler magic
If we now look at the magic TimerPage_qml.cpp generated by qmlcachegen hidden in our build directory, we can suddenly find an actual C++ function at the end of the file:
// formatElapsedTime at line 22, column 5
QString r8_0;
double r6_0 = double((*static_cast<int*>(argumentsPtr[0])));
double r12_0;
double r2_1;
double r11_0;
QString r2_0;
QString r10_0;
r2_0 = QStringLiteral("%L1 s");
r10_0 = std::move(r2_0);
aotContext->captureTranslation();
r2_0 = QCoreApplication::translate(aotContext->translationContext().toUtf8().constData(), std::move(r10_0).toUtf8().constData(), "", -1);
r8_0 = std::move(r2_0);
r12_0 = r6_0;
r2_1 = double(1000);
r2_1 = (r12_0 / r2_1);
r11_0 = r2_1;
r2_0 = std::move(r8_0).arg(r11_0);
return r2_0;
It even transformed the qsTr statement into a proper call to QCoreApplication::translate. How cool is that?!
Of course you cannot be expected to read obscure C++ files that still mostly consist of QML byte code. Nevertheless, it would be useful to know in advance if a QML expression is sub-optimal. Running qmlcachegen in verbose mode provides a lot of diagnostics on why it did or didn’t decide to generate C++ code:
set_target_properties(myhmi PROPERTIES
QT_QMLCACHEGEN_ARGUMENTS "–-verbose"
)
Warning: TimerPage.qml:20:5: Functions without type annotations won't be compiled [compiler]
function formatElapsedTime(time) {
^^^^^^^^^
Unfortunately, it cannot deal with “optional imports” yet that are used by QtQuick.Controls. After all, they dynamically load a style appropriate for the given platform. This also means that it’s not advisable to generally build the project verbose like this since you will just drown in warnings. Instead, you need to import the relevant style yourself, such as QtQuick.Controls.Windows in order to make full use of the QML Compiler. This might be a valid option for an embedded application but not really for a desktop application that tries to actually fit into its environment.
Conclusion
We have now seen QML compiler magic in action, and I hope you enjoyed this peek under the hood of what makes QML work. With QML modules and type annotations we unlock a wealth of useful tools that make our applications faster to develop, more reliable, and easier to debug. We can probably all agree that making use of modern QML tooling will pay off in the long run.