aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSami Shalayel <sami.shalayel@qt.io>2024-04-10 14:30:32 +0200
committerSami Shalayel <sami.shalayel@qt.io>2024-04-18 11:39:16 +0200
commit1bef3c4d0db67bd9dcfc7289b6322649af980c22 (patch)
tree6a1231078a137020cabf0add5e1767fe0451ee23
parent547de2d4e5d5619e5d37b91ad534f9260492e339 (diff)
qmlls: add -I and -E option
Add a -I option that specifies additional import paths used to find QML modules. Add a -E option that searches QML modules in the import paths specified by the environment variables QML_IMPORT_PATH. Add an import path field to QQmlCodeModel with setters and getters that may now consist of more than just `QLibraryInfo::path(QLibraryInfo::QmlImportsPath)`. Extract some common code used to read, check and inform about directories used from environment variables or command line options into static methods. This would allow other tooling, like qmllint, to print the same warnings when using environment variables, for example. Add a new test tst_qmlls_cli that allows to test qmlls behavior when fed with different combinations of -I, -E and -b (for build directories) options. The test also enables checking that warnings are correctly emitted and that import paths are correctly used during linting via qmlls. Note that the warnings emitted by qmlls when using -b were enhanced, partially due to the refactoring of the static helpers to read directory names from environment and command line options. Also note that qmlls was informing twice about possibly using .qmlls.ini files before this commit, so also remove the redundant warning. Task-number: QTBUG-114969 Change-Id: I102ca8b50299f277746c5bba6832551a76898fc0 Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
-rw-r--r--src/qmlls/qqmlcodemodel.cpp9
-rw-r--r--src/qmlls/qqmlcodemodel_p.h3
-rw-r--r--src/qmlls/qqmllintsuggestions.cpp2
-rw-r--r--src/qmltoolingsettings/CMakeLists.txt5
-rw-r--r--src/qmltoolingsettings/qqmltoolingsettings.cpp2
-rw-r--r--src/qmltoolingsettings/qqmltoolingutils.cpp57
-rw-r--r--src/qmltoolingsettings/qqmltoolingutils_p.h37
-rw-r--r--tests/auto/qmlls/CMakeLists.txt1
-rw-r--r--tests/auto/qmlls/cli/CMakeLists.txt34
-rw-r--r--tests/auto/qmlls/cli/data/ImportPath1/SomeModule/A.qml5
-rw-r--r--tests/auto/qmlls/cli/data/ImportPath1/SomeModule/qmldir2
-rw-r--r--tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/B.qml5
-rw-r--r--tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/qmldir2
-rw-r--r--tests/auto/qmlls/cli/data/sourceFolder/ImportFromBothPaths.qml10
-rw-r--r--tests/auto/qmlls/cli/data/sourceFolder/ImportFromImportPath1.qml5
-rw-r--r--tests/auto/qmlls/cli/tst_qmlls_cli.cpp309
-rw-r--r--tests/auto/qmlls/cli/tst_qmlls_cli.h38
-rw-r--r--tools/qmlls/qmllanguageservertool.cpp91
18 files changed, 574 insertions, 43 deletions
diff --git a/src/qmlls/qqmlcodemodel.cpp b/src/qmlls/qqmlcodemodel.cpp
index 6c9018baae..726fdc1cfb 100644
--- a/src/qmlls/qqmlcodemodel.cpp
+++ b/src/qmlls/qqmlcodemodel.cpp
@@ -83,15 +83,14 @@ worker thread (or more) that work on it exist.
QQmlCodeModel::QQmlCodeModel(QObject *parent, QQmlToolingSettings *settings)
: QObject { parent },
+ m_importPaths(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)),
m_currentEnv(std::make_shared<DomEnvironment>(
- QStringList(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)),
- DomEnvironment::Option::SingleThreaded,
+ m_importPaths, DomEnvironment::Option::SingleThreaded,
DomCreationOptions{} | DomCreationOption::WithRecovery
| DomCreationOption::WithScriptExpressions
| DomCreationOption::WithSemanticAnalysis)),
m_validEnv(std::make_shared<DomEnvironment>(
- QStringList(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)),
- DomEnvironment::Option::SingleThreaded,
+ m_importPaths, DomEnvironment::Option::SingleThreaded,
DomCreationOptions{} | DomCreationOption::WithRecovery
| DomCreationOption::WithScriptExpressions
| DomCreationOption::WithSemanticAnalysis)),
@@ -594,7 +593,7 @@ void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const
m_rebuildRequired = false;
}
- loadPaths.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath));
+ loadPaths.append(m_importPaths);
if (std::shared_ptr<DomEnvironment> newCurrentPtr = newCurrent.ownerAs<DomEnvironment>()) {
newCurrentPtr->setLoadPaths(loadPaths);
}
diff --git a/src/qmlls/qqmlcodemodel_p.h b/src/qmlls/qqmlcodemodel_p.h
index 1be4ca13d2..8e2be96ef6 100644
--- a/src/qmlls/qqmlcodemodel_p.h
+++ b/src/qmlls/qqmlcodemodel_p.h
@@ -101,6 +101,8 @@ public:
QStringList buildPathsForRootUrl(const QByteArray &url);
QStringList buildPathsForFileUrl(const QByteArray &url);
void setBuildPathsForRootUrl(QByteArray url, const QStringList &paths);
+ QStringList importPaths() const { return m_importPaths; };
+ void setImportPaths(const QStringList &paths) { m_importPaths = paths; };
void removeRootUrls(const QList<QByteArray> &urls);
QQmlToolingSettings *settings();
QStringList findFilePathsFromFileNames(const QStringList &fileNames) const;
@@ -136,6 +138,7 @@ private:
int m_indexInProgressCost = 0;
int m_indexDoneCost = 0;
int m_nUpdateInProgress = 0;
+ QStringList m_importPaths;
QQmlJS::Dom::DomItem m_currentEnv;
QQmlJS::Dom::DomItem m_validEnv;
QByteArray m_lastOpenDocumentUpdated;
diff --git a/src/qmlls/qqmllintsuggestions.cpp b/src/qmlls/qqmllintsuggestions.cpp
index 8d2c937640..abd66ec7d3 100644
--- a/src/qmlls/qqmllintsuggestions.cpp
+++ b/src/qmlls/qqmllintsuggestions.cpp
@@ -308,7 +308,7 @@ void QmlLintSuggestions::diagnoseHelper(const QByteArray &url,
qCDebug(lintLog) << "has doc, do real lint";
QStringList imports = m_codeModel->buildPathsForFileUrl(url);
- imports.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath));
+ imports.append(m_codeModel->importPaths());
const QString filename = doc.canonicalFilePath();
// add source directory as last import as fallback in case there is no qmldir in the build
// folder this mimics qmllint behaviors
diff --git a/src/qmltoolingsettings/CMakeLists.txt b/src/qmltoolingsettings/CMakeLists.txt
index ebc6d8a107..8776a6dd98 100644
--- a/src/qmltoolingsettings/CMakeLists.txt
+++ b/src/qmltoolingsettings/CMakeLists.txt
@@ -7,7 +7,10 @@ qt_internal_add_module(QmlToolingSettingsPrivate
SOURCES
qqmltoolingsettings_p.h
qqmltoolingsettings.cpp
+
+ qqmltoolingutils_p.h
+ qqmltoolingutils.cpp
PUBLIC_LIBRARIES
Qt::Core
GENERATE_CPP_EXPORTS
- )
+)
diff --git a/src/qmltoolingsettings/qqmltoolingsettings.cpp b/src/qmltoolingsettings/qqmltoolingsettings.cpp
index 967318f18f..3f5aecccfb 100644
--- a/src/qmltoolingsettings/qqmltoolingsettings.cpp
+++ b/src/qmltoolingsettings/qqmltoolingsettings.cpp
@@ -3,8 +3,6 @@
#include "qqmltoolingsettings_p.h"
-#include <algorithm>
-
#include <QtCore/qdebug.h>
#include <QtCore/qdir.h>
#include <QtCore/qfileinfo.h>
diff --git a/src/qmltoolingsettings/qqmltoolingutils.cpp b/src/qmltoolingsettings/qqmltoolingutils.cpp
new file mode 100644
index 0000000000..1cf1602b4b
--- /dev/null
+++ b/src/qmltoolingsettings/qqmltoolingutils.cpp
@@ -0,0 +1,57 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "qqmltoolingutils_p.h"
+
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qdir.h>
+
+using namespace Qt::StringLiterals;
+
+/*!
+\internal
+
+Helper utils to help QQmlTooling retrieve certain values from the environment or command line
+options.
+It helps to keep the warning messages consistent between tools like qmlls and qmllint when
+they use environment variables, for examples.
+*/
+
+void QQmlToolingUtils::warnForInvalidDirs(const QStringList &dirs, const QString &origin)
+{
+ for (const QString &path : dirs) {
+ QFileInfo info(path);
+ if (!info.exists()) {
+ qWarning().noquote().nospace()
+ << u"Argument \"%1\" %2 does not exist."_s.arg(path, origin);
+ continue;
+ }
+ if (!info.isDir()) {
+ qWarning().noquote().nospace()
+ << "Argument \"" << path << "\" " << origin << " is not a directory.";
+ continue;
+ }
+ }
+}
+
+QStringList
+QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(const QString &environmentVariableName)
+{
+ const QStringList envPaths = qEnvironmentVariable(environmentVariableName.toUtf8())
+ .split(QDir::listSeparator(), Qt::SkipEmptyParts);
+ warnForInvalidDirs(envPaths,
+ u"from environment variable \"%1\""_s.arg(environmentVariableName));
+ return envPaths;
+}
+
+QStringList QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(const QCommandLineParser &parser,
+ const QCommandLineOption &option)
+{
+ if (!parser.isSet(option))
+ return {};
+
+ const QStringList dirs = parser.values(option);
+ const QString optionName = option.names().constFirst();
+ warnForInvalidDirs(dirs, u"passed to -%1"_s.arg(optionName));
+ return dirs;
+}
diff --git a/src/qmltoolingsettings/qqmltoolingutils_p.h b/src/qmltoolingsettings/qqmltoolingutils_p.h
new file mode 100644
index 0000000000..877d490355
--- /dev/null
+++ b/src/qmltoolingsettings/qqmltoolingutils_p.h
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#ifndef QQMLTOOLINGUTILS_P_H
+#define QQMLTOOLINGUTILS_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/qstring.h>
+#include <QtCore/qstringlist.h>
+#include <QtCore/qcommandlineparser.h>
+#include <QtCore/qcommandlineoption.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlToolingUtils
+{
+private:
+ static void warnForInvalidDirs(const QStringList &dirs, const QString &origin);
+public:
+ static QStringList getAndWarnForInvalidDirsFromEnv(const QString &environmentVariableName);
+ static QStringList getAndWarnForInvalidDirsFromOption(const QCommandLineParser &parser,
+ const QCommandLineOption &option);
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLTOOLINGUTILS_P_H
diff --git a/tests/auto/qmlls/CMakeLists.txt b/tests/auto/qmlls/CMakeLists.txt
index 59ab45a8bf..daf7d64cd4 100644
--- a/tests/auto/qmlls/CMakeLists.txt
+++ b/tests/auto/qmlls/CMakeLists.txt
@@ -7,5 +7,6 @@ if (TARGET Qt::QmlLSPrivate)
add_subdirectory(qqmlcodemodel)
add_subdirectory(qmlls)
add_subdirectory(modules)
+ add_subdirectory(cli)
endif()
diff --git a/tests/auto/qmlls/cli/CMakeLists.txt b/tests/auto/qmlls/cli/CMakeLists.txt
new file mode 100644
index 0000000000..1ac8016f66
--- /dev/null
+++ b/tests/auto/qmlls/cli/CMakeLists.txt
@@ -0,0 +1,34 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qmlls_modules LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data)
+
+qt_internal_add_test(tst_qmlls_cli
+ SOURCES
+ tst_qmlls_cli.cpp
+ tst_qmlls_cli.h
+ DEFINES
+ QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
+ LIBRARIES
+ Qt::Core
+ Qt::QmlDomPrivate
+ Qt::LanguageServerPrivate
+ Qt::Test
+ Qt::QuickTestUtilsPrivate
+ Qt::QmlLSPrivate
+ TESTDATA ${test_data}
+)
+
+if (TARGET qmlls)
+ # standalone test builds do not know the qmlls target
+ # but if TARGET qmlls is known it should be built before this test
+ add_dependencies(tst_qmlls_cli qmlls)
+endif()
diff --git a/tests/auto/qmlls/cli/data/ImportPath1/SomeModule/A.qml b/tests/auto/qmlls/cli/data/ImportPath1/SomeModule/A.qml
new file mode 100644
index 0000000000..5468cae5e1
--- /dev/null
+++ b/tests/auto/qmlls/cli/data/ImportPath1/SomeModule/A.qml
@@ -0,0 +1,5 @@
+import QtQuick
+
+Item {
+ property string helloSomeModule
+}
diff --git a/tests/auto/qmlls/cli/data/ImportPath1/SomeModule/qmldir b/tests/auto/qmlls/cli/data/ImportPath1/SomeModule/qmldir
new file mode 100644
index 0000000000..6e8de8bc53
--- /dev/null
+++ b/tests/auto/qmlls/cli/data/ImportPath1/SomeModule/qmldir
@@ -0,0 +1,2 @@
+module SomeModule
+A 254.0 A.qml
diff --git a/tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/B.qml b/tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/B.qml
new file mode 100644
index 0000000000..33af59274a
--- /dev/null
+++ b/tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/B.qml
@@ -0,0 +1,5 @@
+import QtQuick
+
+Item {
+ property string helloAnotherModule
+}
diff --git a/tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/qmldir b/tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/qmldir
new file mode 100644
index 0000000000..aa4ef803b1
--- /dev/null
+++ b/tests/auto/qmlls/cli/data/ImportPath2/AnotherModule/qmldir
@@ -0,0 +1,2 @@
+module AnotherModule
+B 254.0 B.qml
diff --git a/tests/auto/qmlls/cli/data/sourceFolder/ImportFromBothPaths.qml b/tests/auto/qmlls/cli/data/sourceFolder/ImportFromBothPaths.qml
new file mode 100644
index 0000000000..4a2775ce29
--- /dev/null
+++ b/tests/auto/qmlls/cli/data/sourceFolder/ImportFromBothPaths.qml
@@ -0,0 +1,10 @@
+import SomeModule
+import AnotherModule
+
+A {
+ helloSomeModule: "hello!"
+
+ B {
+ helloAnotherModule: "World!"
+ }
+} \ No newline at end of file
diff --git a/tests/auto/qmlls/cli/data/sourceFolder/ImportFromImportPath1.qml b/tests/auto/qmlls/cli/data/sourceFolder/ImportFromImportPath1.qml
new file mode 100644
index 0000000000..3417701352
--- /dev/null
+++ b/tests/auto/qmlls/cli/data/sourceFolder/ImportFromImportPath1.qml
@@ -0,0 +1,5 @@
+import SomeModule
+
+A {
+ helloSomeModule: "hello!"
+}
diff --git a/tests/auto/qmlls/cli/tst_qmlls_cli.cpp b/tests/auto/qmlls/cli/tst_qmlls_cli.cpp
new file mode 100644
index 0000000000..f871decec1
--- /dev/null
+++ b/tests/auto/qmlls/cli/tst_qmlls_cli.cpp
@@ -0,0 +1,309 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "tst_qmlls_cli.h"
+
+using namespace Qt::StringLiterals;
+
+void tst_qmlls_cli::initTestCase()
+{
+ QQmlDataTest::initTestCase();
+
+ m_qmllsPath = QLibraryInfo::path(QLibraryInfo::BinariesPath) + QLatin1String("/qmlls");
+#ifdef Q_OS_WIN
+ m_qmllsPath += QLatin1String(".exe");
+#endif
+ // allow overriding of the executable, to be able to use a qmlEcho script (as described in
+ // qmllanguageservertool.cpp)
+ m_qmllsPath = qEnvironmentVariable("QMLLS", m_qmllsPath);
+ m_server.setProgram(m_qmllsPath);
+}
+
+void tst_qmlls_cli::cleanup()
+{
+ m_server.closeWriteChannel();
+ m_server.waitForFinished();
+ QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
+ QCOMPARE(m_server.exitStatus(), QProcess::NormalExit);
+}
+
+// Helper structs to avoid confusions between expected and unexpected messages and between expected
+// and unexpected diagnostics.
+struct ExpectedMessages : public QStringList
+{
+ using QStringList::QStringList;
+};
+struct UnexpectedMessages : public QStringList
+{
+ using QStringList::QStringList;
+};
+struct ExpectedDiagnostics : public QStringList
+{
+ using QStringList::QStringList;
+};
+struct UnexpectedDiagnostics : public QStringList
+{
+ using QStringList::QStringList;
+};
+
+// Extra environment variables to be added to qmlls's environment.
+struct Environment : public QList<QPair<QString, QString>>
+{
+ using QList<QPair<QString, QString>>::QList;
+};
+
+void tst_qmlls_cli::warnings_data()
+{
+ QTest::addColumn<QStringList>("args");
+ QTest::addColumn<Environment>("environment");
+ QTest::addColumn<QString>("filePath");
+ // messages are printed to stderr and not shown in editor:
+ QTest::addColumn<ExpectedMessages>("expectedMessages");
+ QTest::addColumn<UnexpectedMessages>("unexpectedMessages");
+ // diagnostics are passed via LSP to be shown in editor:
+ QTest::addColumn<ExpectedDiagnostics>("expectedDiagnostics");
+ QTest::addColumn<UnexpectedDiagnostics>("unexpectedDiagnostics");
+
+ const Environment defaultEnv;
+ const QString dir1 = testFile(u"ImportPath1"_s);
+ const QString dir2 = testFile(u"ImportPath2"_s);
+ const QString notDir = testFile(u"ImportPath1/SomeModule/qmldir"_s);
+ const QString wrongDir = testFile(u"ImportPathInexistent"_s);
+
+ const QString fileImportingDir1 = testFile(u"sourceFolder/ImportFromImportPath1.qml"_s);
+ const QString fileImportingBothDirs = testFile(u"sourceFolder/ImportFromBothPaths.qml"_s);
+
+ const QString importWarningDir1 = u"Warnings occurred while importing module \"SomeModule\""_s;
+ const QString importWarningDir2 = u"Warnings occurred while importing module \"AnotherModule\""_s;
+
+ const UnexpectedMessages noUnexpectedMessages;
+ const QString warnAboutQmllsIniFiles{
+ u"Using the build directories found in the .qmlls.ini file. Your build folder might not be found if no .qmlls.ini files are present in the root source folder."_s
+ };
+
+ QTest::addRow("2-build-dirs")
+ << QStringList{ u"--build-dir"_s, dir1, u"-b"_s, dir2 } << defaultEnv
+ << fileImportingDir1
+ << ExpectedMessages{ u"Using build directories passed by -b: \"%1\", \"%2\"."_s.arg(
+ dir1, dir2) }
+ << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
+ << UnexpectedDiagnostics{ importWarningDir1 };
+
+ QTest::addRow("build-dir-not-dir")
+ << QStringList{ u"--build-dir"_s, notDir, u"-b"_s, dir2 } << defaultEnv
+ << fileImportingBothDirs
+ << ExpectedMessages{ u"Argument \"%1\" passed to -b is not a directory."_s.arg(notDir) }
+ << UnexpectedMessages{ warnAboutQmllsIniFiles }
+ << ExpectedDiagnostics{ importWarningDir1 }
+ << UnexpectedDiagnostics{ importWarningDir2 };
+
+ QTest::addRow("build-dir-not-existing")
+ << QStringList{ u"--build-dir"_s, wrongDir, u"-b"_s, dir2 } << defaultEnv
+ << fileImportingBothDirs
+ << ExpectedMessages{ u"Argument \"%1\" passed to -b does not exist."_s.arg(wrongDir) }
+ << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
+ << UnexpectedDiagnostics{};
+
+ QTest::addRow("build-dir-from-environment")
+ << QStringList{}
+ << Environment{ { u"QMLLS_BUILD_DIRS"_s,
+ u"%1%2%3"_s.arg(dir1, QDir::listSeparator(), dir2) } }
+ << fileImportingBothDirs
+ << ExpectedMessages{ u"Using build directories passed from environment variable \"QMLLS_BUILD_DIRS\": \"%1\", \"%2\"."_s
+ .arg(dir1, dir2) }
+ << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
+ << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };
+
+ QTest::addRow("build-dir-from-environment-not-existing")
+ << QStringList{}
+ << Environment{ { u"QMLLS_BUILD_DIRS"_s,
+ QStringList{ dir1, wrongDir, notDir }.join(QDir::listSeparator()) } }
+ << fileImportingDir1
+ << ExpectedMessages{ u"Argument \"%1\" from environment variable \"QMLLS_BUILD_DIRS\" does not exist."_s
+ .arg(wrongDir),
+ u"Argument \"%1\" from environment variable \"QMLLS_BUILD_DIRS\" is not a directory."_s
+ .arg(notDir) }
+ << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
+ << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };
+
+ QTest::addRow("ignore-environment-with-option")
+ << QStringList{ u"--build-dir"_s, dir1 }
+ << Environment{ { u"QMLLS_BUILD_DIRS"_s, dir2 } } << fileImportingBothDirs
+ << ExpectedMessages{ u"Using build directories passed by -b: \"%1\"."_s.arg(dir1) }
+ << UnexpectedMessages{ dir2, warnAboutQmllsIniFiles }
+ << ExpectedDiagnostics{ importWarningDir2 }
+ << UnexpectedDiagnostics{ importWarningDir1 };
+
+ QTest::addRow("loadFromConfigFile")
+ << QStringList{} << Environment{} << fileImportingDir1
+ << ExpectedMessages{ warnAboutQmllsIniFiles } << UnexpectedMessages{}
+ << ExpectedDiagnostics{ importWarningDir1 } << UnexpectedDiagnostics{};
+
+ QTest::addRow("2-import-paths")
+ << QStringList{ u"-I"_s, dir1, u"-I"_s, dir2 } << Environment{} << fileImportingBothDirs
+ << ExpectedMessages{ u"Using import directories passed by -I: \"%1\", \"%2\"."_s.arg(
+ dir1, dir2) }
+ << UnexpectedMessages{} << ExpectedDiagnostics{}
+ << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };
+
+ QTest::addRow("import-paths-ignore-env")
+ << QStringList{ u"-I"_s, dir1, } << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } }
+ << fileImportingBothDirs
+ << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1) }
+ << UnexpectedMessages{ u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s.arg(dir2)}
+ << ExpectedDiagnostics{importWarningDir2} << UnexpectedDiagnostics{ importWarningDir1 };
+
+ QTest::addRow("2-import-paths-mixed")
+ << QStringList{ u"-I"_s, dir1, u"-E"_s }
+ << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } } << fileImportingBothDirs
+ << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1),
+ u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s
+ .arg(dir2) }
+ << UnexpectedMessages{} << ExpectedDiagnostics{}
+ << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };
+
+ QTest::addRow("2-import-paths-deprecated")
+ << QStringList{ u"-I"_s, dir1, u"-E"_s }
+ << Environment{ { u"QML2_IMPORT_PATH"_s, dir2 } } << fileImportingBothDirs
+ << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1),
+ u"Using import directories passed from the deprecated environment variable \"QML2_IMPORT_PATH\": \"%1\"."_s
+ .arg(dir2) }
+ << UnexpectedMessages{} << ExpectedDiagnostics{}
+ << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };
+}
+
+auto tst_qmlls_cli::startServerRAII()
+{
+ startServerImpl();
+ return qScopeGuard([this]() { this->stopServerImpl(); });
+}
+
+void tst_qmlls_cli::startServerImpl()
+{
+ m_protocol = std::make_unique<QLanguageServerProtocol>(
+ [this](const QByteArray &data) { m_server.write(data); });
+
+ connect(&m_server, &QProcess::readyReadStandardOutput, this, [this]() {
+ QByteArray data = m_server.readAllStandardOutput();
+ m_protocol->receiveData(data);
+ });
+
+ m_server.start();
+
+ QLspSpecification::InitializeParams clientInfo;
+ clientInfo.rootUri = QUrl::fromLocalFile(dataDirectory() + "/default").toString().toUtf8();
+
+ QLspSpecification::TextDocumentClientCapabilities tDoc;
+ tDoc.typeDefinition = QLspSpecification::TypeDefinitionClientCapabilities{ false, false };
+
+ QLspSpecification::PublishDiagnosticsClientCapabilities pDiag;
+ tDoc.publishDiagnostics = pDiag;
+ pDiag.versionSupport = true;
+ clientInfo.capabilities.textDocument = tDoc;
+ bool didInit = false;
+ m_protocol->requestInitialize(
+ clientInfo, [this, &didInit](const QLspSpecification::InitializeResult &serverInfo) {
+ Q_UNUSED(serverInfo);
+ m_protocol->notifyInitialized(QLspSpecification::InitializedParams());
+ didInit = true;
+ });
+ QTRY_COMPARE_WITH_TIMEOUT(didInit, true, 10000);
+}
+
+void tst_qmlls_cli::stopServerImpl()
+{
+ m_server.closeWriteChannel();
+ m_server.waitForFinished();
+ QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
+ QCOMPARE(m_server.exitStatus(), QProcess::NormalExit);
+}
+
+void tst_qmlls_cli::warnings()
+{
+ QFETCH(QStringList, args);
+ QFETCH(Environment, environment);
+ QFETCH(ExpectedMessages, expectedMessages);
+ QFETCH(UnexpectedMessages, unexpectedMessages);
+ QFETCH(QString, filePath);
+ QFETCH(ExpectedDiagnostics, expectedDiagnostics);
+ QFETCH(UnexpectedDiagnostics, unexpectedDiagnostics);
+
+ QProcessEnvironment processEnvironment = QProcessEnvironment::systemEnvironment();
+ for (const auto &entry : environment)
+ processEnvironment.insert(entry.first, entry.second);
+ m_server.setProcessEnvironment(processEnvironment);
+ m_server.setArguments(args);
+
+ QList<int> countExpectedMessages(expectedMessages.size(), 0);
+ QList<int> countUnexpectedMessages(unexpectedMessages.size(), 0);
+ QList<int> countExpectedDiagnostics(expectedDiagnostics.size(), 0);
+ QList<int> countUnexpectedDiagnostics(unexpectedDiagnostics.size(), 0);
+
+ auto guard = qScopeGuard([this]() {
+ // note: the lambda used in the "connect"-call references local variables, so disconnect the
+ // lambda via QScopedGuard to avoid its captured references to dangle
+ disconnect(&m_server, &QProcess::readyReadStandardOutput, nullptr, nullptr);
+ });
+ connect(&m_server, &QProcess::readyReadStandardError, this,
+ [this, &expectedMessages, &countExpectedMessages, &unexpectedMessages,
+ &countUnexpectedMessages]() {
+ const auto data = QString::fromUtf8(m_server.readAllStandardError());
+ if (data.isEmpty())
+ return;
+
+ for (int i = 0; i < expectedMessages.size(); ++i) {
+ if (data.contains(expectedMessages[i]))
+ ++countExpectedMessages[i];
+ }
+ for (int i = 0; i < unexpectedMessages.size(); ++i) {
+ if (data.contains(unexpectedMessages[i]))
+ ++countUnexpectedMessages[i];
+ }
+ });
+
+ auto guard2 = startServerRAII();
+
+ // each expected message should appear exactly one time
+ QTRY_COMPARE_WITH_TIMEOUT(countExpectedMessages, QList<int>(expectedMessages.size(), 1), 500);
+ // each unexpected message should appear exactly zero times
+ QCOMPARE(countUnexpectedMessages, QList<int>(unexpectedMessages.size(), 0));
+
+ bool diagnosticOk = false;
+ m_protocol->registerPublishDiagnosticsNotificationHandler(
+ [&diagnosticOk, &expectedDiagnostics, &countExpectedDiagnostics, &unexpectedDiagnostics,
+ &countUnexpectedDiagnostics](const QByteArray &,
+ const QLspSpecification::PublishDiagnosticsParams &p) {
+ for (const auto &d : p.diagnostics) {
+ const QString message = QString::fromUtf8(d.message);
+ for (int i = 0; i < expectedDiagnostics.size(); ++i) {
+ if (message.contains(expectedDiagnostics[i]))
+ ++countExpectedDiagnostics[i];
+ }
+ for (int i = 0; i < unexpectedDiagnostics.size(); ++i) {
+ if (message.contains(unexpectedDiagnostics[i]))
+ ++countUnexpectedDiagnostics[i];
+ }
+ }
+ diagnosticOk = true;
+ });
+
+ QFile file(filePath);
+ QVERIFY(file.open(QIODevice::ReadOnly));
+
+ QLspSpecification::DidOpenTextDocumentParams oParams;
+ QLspSpecification::TextDocumentItem textDocument;
+ QByteArray uri = QUrl::fromLocalFile(filePath).toEncoded();
+ textDocument.uri = uri;
+ textDocument.text = file.readAll();
+ oParams.textDocument = textDocument;
+ m_protocol->notifyDidOpenTextDocument(oParams);
+
+ QTRY_VERIFY_WITH_TIMEOUT(diagnosticOk, 3000);
+ // each expected diagnostic should appear exactly one time
+ QCOMPARE(countExpectedDiagnostics, QList<int>(expectedDiagnostics.size(), 1));
+ // each unexpected diagnostic should appear exactly zero times
+ QCOMPARE(countUnexpectedDiagnostics, QList<int>(unexpectedDiagnostics.size(), 0));
+
+}
+
+QTEST_MAIN(tst_qmlls_cli)
diff --git a/tests/auto/qmlls/cli/tst_qmlls_cli.h b/tests/auto/qmlls/cli/tst_qmlls_cli.h
new file mode 100644
index 0000000000..3eab8b5d1b
--- /dev/null
+++ b/tests/auto/qmlls/cli/tst_qmlls_cli.h
@@ -0,0 +1,38 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef TST_QMLLS_CLI_H
+#define TST_QMLLS_CLI_H
+
+#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
+#include <QtQuickTestUtils/private/qmlutils_p.h>
+
+#include <QtCore/qobject.h>
+#include <QtCore/qprocess.h>
+#include <QtCore/qstringlist.h>
+#include <QtCore/qlibraryinfo.h>
+
+#include <QtTest/qtest.h>
+
+class tst_qmlls_cli: public QQmlDataTest
+{
+ Q_OBJECT
+public:
+ tst_qmlls_cli() : QQmlDataTest(QT_QMLTEST_DATADIR) { }
+ [[nodiscard]] auto startServerRAII();
+ void startServerImpl();
+ void stopServerImpl();
+
+private slots:
+ void initTestCase();
+ void cleanup();
+ void warnings_data();
+ void warnings();
+
+public:
+ QProcess m_server;
+ QString m_qmllsPath;
+ std::unique_ptr<QLanguageServerProtocol> m_protocol;
+};
+
+#endif // TST_QMLLS_CLI_H
diff --git a/tools/qmlls/qmllanguageservertool.cpp b/tools/qmlls/qmllanguageservertool.cpp
index 6427c4ac09..34138638b7 100644
--- a/tools/qmlls/qmllanguageservertool.cpp
+++ b/tools/qmlls/qmllanguageservertool.cpp
@@ -8,6 +8,7 @@
#include <QtCore/qfileinfo.h>
#include <QtCore/qcoreapplication.h>
#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
+#include <QtQmlToolingSettings/private/qqmltoolingutils_p.h>
#include <QtCore/qdiriterator.h>
#include <QtCore/qjsonobject.h>
#include <QtCore/qjsonarray.h>
@@ -164,6 +165,17 @@ int main(int argv, char *argc[])
parser.addOption(buildDirOption);
settings.addOption(buildDir);
+ QString qmlImportPath = QStringLiteral(u"qml-import-path");
+ QCommandLineOption qmlImportPathOption(
+ QStringList() << "I", QLatin1String("Look for QML modules in the specified directory"),
+ qmlImportPath);
+ parser.addOption(qmlImportPathOption);
+
+ QCommandLineOption environmentOption(
+ QStringList() << "E",
+ QLatin1String("Use the QML_IMPORT_PATH environment variable to look for QML Modules"));
+ parser.addOption(environmentOption);
+
QCommandLineOption writeDefaultsOption(
QStringList() << "write-defaults",
QLatin1String("Writes defaults settings to .qmlls.ini and exits (Warning: This "
@@ -232,49 +244,60 @@ int main(int argv, char *argc[])
qmlServer.codeModel()->disableCMakeCalls();
}
- const QStringList envPaths =
- qEnvironmentVariable("QMLLS_BUILD_DIRS").split(u',', Qt::SkipEmptyParts);
- for (const QString &envPath : envPaths) {
- QFileInfo info(envPath);
- if (!info.exists()) {
- qWarning() << "Argument" << buildDir << "passed via QMLLS_BUILD_DIRS does not exist.";
- } else if (!info.isDir()) {
- qWarning() << "Argument" << buildDir
- << "passed via QMLLS_BUILD_DIRS is not a directory.";
- }
- }
-
- QStringList buildDirs;
if (parser.isSet(buildDirOption)) {
- buildDirs = parser.values(buildDirOption);
- for (const QString &buildDir : buildDirs) {
- QFileInfo info(buildDir);
- if (!info.exists()) {
- qWarning() << "Argument" << buildDir << "passed to --build-dir does not exist.";
- } else if (!info.isDir()) {
- qWarning() << "Argument" << buildDir << "passed to --build-dir is not a directory.";
- }
- }
- qmlServer.codeModel()->setBuildPathsForRootUrl(QByteArray(), buildDirs);
- }
+ const QStringList dirs =
+ QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, buildDirOption);
+
+ qInfo().nospace().noquote()
+ << "Using build directories passed by -b: \"" << dirs.join(u"\", \""_s) << "\".";
+
+ qmlServer.codeModel()->setBuildPathsForRootUrl(QByteArray(), dirs);
+ } else if (QStringList dirsFromEnv =
+ QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv("QMLLS_BUILD_DIRS");
+ !dirsFromEnv.isEmpty()) {
+
+ // warn now at qmlls startup that those directories will be used later in qqmlcodemodel when
+ // searching for build folders.
+ qInfo().nospace().noquote() << "Using build directories passed from environment variable "
+ "\"QMLLS_BUILD_DIRS\": \""
+ << dirsFromEnv.join(u"\", \""_s) << "\".";
- if (!buildDirs.isEmpty()) {
- qInfo() << "Using the build directories passed via the --build-dir option:"
- << buildDirs.join(", ");
- } else if (!envPaths.isEmpty()) {
- qInfo() << "Using the build directories passed via the QMLLS_BUILD_DIRS environment "
- "variable"
- << buildDirs.join(", ");
} else {
qInfo() << "Using the build directories found in the .qmlls.ini file. Your build folder "
"might not be found if no .qmlls.ini files are present in the root source "
"folder.";
}
+ QStringList importPaths{ QLibraryInfo::path(QLibraryInfo::QmlImportsPath) };
+ if (parser.isSet(qmlImportPathOption)) {
+ const QStringList pathsFromOption =
+ QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, qmlImportPathOption);
+ qInfo().nospace().noquote() << "Using import directories passed by -I: \""
+ << pathsFromOption.join(u"\", \""_s) << "\".";
+ importPaths << pathsFromOption;
+ }
+ if (parser.isSet(environmentOption)) {
+ if (const QStringList dirsFromEnv =
+ QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML_IMPORT_PATH"_s);
+ !dirsFromEnv.isEmpty()) {
+ qInfo().nospace().noquote()
+ << "Using import directories passed from environment variable "
+ "\"QML_IMPORT_PATH\": \""
+ << dirsFromEnv.join(u"\", \""_s) << "\".";
+ importPaths << dirsFromEnv;
+ }
- if (buildDirs.isEmpty() && envPaths.isEmpty()) {
- qInfo() << "Build directory path omitted: Your source folders will be searched for "
- ".qmlls.ini files.";
+ if (const QStringList dirsFromEnv2 =
+ QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML2_IMPORT_PATH"_s);
+ !dirsFromEnv2.isEmpty()) {
+ qInfo().nospace().noquote()
+ << "Using import directories passed from the deprecated environment variable "
+ "\"QML2_IMPORT_PATH\": \""
+ << dirsFromEnv2.join(u"\", \""_s) << "\".";
+ importPaths << dirsFromEnv2;
+ }
}
+ qmlServer.codeModel()->setImportPaths(importPaths);
+
StdinReader r;
QObject::connect(&r, &StdinReader::receivedData,
qmlServer.server(), &QLanguageServer::receiveData);