qmlls: fix the order in which the build directory is obtained

Make sure that the order in which the build directory is obtained is
intuitive. The preference should be:

1. Command line option
2. If there is no command line option, search for the environment
   variable.
3. If there environment variable, check for .qmlls.ini files

Add a test to test all combinations of this.

Check that the paths passed via the --build-dir argument to qmlls does
point to a directory and complain when it does not. Same for the paths
passed via the QMLLS_BUILD_DIRS environment variable. Also inform the
user when none of --build-dir nor QMLLS_BUILD_DIRS was passed that
the build dir will be set by the .qmlls.ini files.

Also, remove the broken #if QT_CONFIG(commandlineparser)-block in the
code: the parser defined in this #if-block is also used outside the #if.
Instead, do not compile qmlls if the commandlineparser feature is
disabled.

Also use platform-specific separators when passing multiple build
directories.

[ChangeLog][Qmlls] Qmlls warns when inexisting build folder
paths are passed to the --build-dir argument or the QMLLS_BUILD_DIRS
environment variable. Also, it will inform the user if the build
directory was set using the command line option, the environment
variable or an .qmlls.ini settings file, if existent in the source
folder.

Pick-to: 6.6
Task-number: QTBUG-114474
Change-Id: I6cb4f2db47945f8ea9549b7519a92f9d15d5ce66
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
This commit is contained in:
Sami Shalayel 2023-06-20 13:50:14 +02:00
parent da38eaf433
commit 13dc5b3c9d
7 changed files with 190 additions and 15 deletions

View File

@ -556,22 +556,32 @@ QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url)
}
}
QString path = url2Path(url);
// fallback to the empty root, if is has an entry.
// This is the buildPath that is passed to qmlls via --build-dir.
if (buildPaths.isEmpty()) {
buildPaths += buildPathsForRootUrl(QByteArray());
}
// look in the QMLLS_BUILD_DIRS environment variable
if (buildPaths.isEmpty()) {
QStringList envPaths = qEnvironmentVariable("QMLLS_BUILD_DIRS")
.split(QDir::listSeparator(), Qt::SkipEmptyParts);
buildPaths += envPaths;
}
// look in the settings.
// This is the one that is passed via the .qmlls.ini file.
if (buildPaths.isEmpty() && m_settings) {
// look in the settings
m_settings->search(path);
QString buildDir = QStringLiteral(u"buildDir");
if (m_settings->isSet(buildDir))
buildPaths += m_settings->value(buildDir).toString().split(u',', Qt::SkipEmptyParts);
buildPaths += m_settings->value(buildDir).toString().split(QDir::listSeparator(),
Qt::SkipEmptyParts);
}
// heuristic to find build directory
if (buildPaths.isEmpty()) {
// default values
buildPaths += buildPathsForRootUrl(QByteArray());
}
// env variable
QStringList envPaths = qEnvironmentVariable("QMLLS_BUILD_DIRS").split(u',', Qt::SkipEmptyParts);
buildPaths += envPaths;
if (buildPaths.isEmpty()) {
// heuristic to find build dir
QDir d(path);
d.setNameFilters(QStringList({ u"build*"_s }));
const int maxDirDepth = 8;

View File

@ -7,5 +7,6 @@ if (TARGET Qt::LanguageServerPrivate AND TARGET Qt::qmlls)
add_subdirectory(qmlls)
add_subdirectory(utils)
add_subdirectory(modules)
add_subdirectory(qqmlcodemodel)
endif()
endif()

View File

@ -0,0 +1,27 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
file(GLOB_RECURSE test_data_glob
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
data)
list(APPEND test_data ${test_data_glob})
qt_internal_add_test(tst_qmlls_qqmlcodemodel
SOURCES
tst_qmlls_qqmlcodemodel.cpp tst_qmlls_qqmlcodemodel.h
DEFINES
QT_QMLLS_QQMLCODEMODEL_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
LIBRARIES
Qt::Core
Qt::QmlDomPrivate
Qt::LanguageServerPrivate
Qt::Test
Qt::QuickTestUtilsPrivate
Qt::QmlLSPrivate
TESTDATA ${test_data}
)
qt_internal_extend_target(tst_qmlls_qqmlcodemodel CONDITION ANDROID OR IOS
DEFINES
QT_QMLLS_QQMLCODEMODEL_DATADIR=":/domdata"
)

View File

@ -0,0 +1,63 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "tst_qmlls_qqmlcodemodel.h"
#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
#include <QtQmlLS/private/qqmlcodemodel_p.h>
void tst_qmlls_qqmlcodemodel::buildPathsForFileUrl_data()
{
QTest::addColumn<QString>("pathFromIniFile");
QTest::addColumn<QString>("pathFromEnvironmentVariable");
QTest::addColumn<QString>("pathFromCommandLine");
QTest::addColumn<QString>("expectedPath");
const QString path1 = u"/Users/helloWorld/build-myProject"_s;
const QString path2 = u"/Users/helloWorld/build-custom"_s;
const QString path3 = u"/Users/helloWorld/build-12345678"_s;
QTest::addRow("justCommandLine") << QString() << QString() << path1 << path1;
QTest::addRow("all3") << path1 << path2 << path3 << path3;
QTest::addRow("commandLineOverridesEnvironmentVariable")
<< QString() << path2 << path3 << path3;
QTest::addRow("commandLineOverridesIniFile") << path2 << QString() << path3 << path3;
QTest::addRow("EnvironmentVariableOverridesIniFile") << path1 << path2 << QString() << path2;
QTest::addRow("iniFile") << path1 << QString() << QString() << path1;
QTest::addRow("environmentVariable") << QString() << path3 << QString() << path3;
}
void tst_qmlls_qqmlcodemodel::buildPathsForFileUrl()
{
QFETCH(QString, pathFromIniFile);
QFETCH(QString, pathFromEnvironmentVariable);
QFETCH(QString, pathFromCommandLine);
QFETCH(QString, expectedPath);
QQmlToolingSettings settings(u"qmlls"_s);
if (!pathFromIniFile.isEmpty())
settings.addOption("buildDir", pathFromIniFile);
constexpr char environmentVariable[] = "QMLLS_BUILD_DIRS";
qunsetenv(environmentVariable);
if (!pathFromEnvironmentVariable.isEmpty()) {
qputenv(environmentVariable, pathFromEnvironmentVariable.toUtf8());
}
QmlLsp::QQmlCodeModel model(nullptr, &settings);
if (!pathFromCommandLine.isEmpty())
model.setBuildPathsForRootUrl(QByteArray(), QStringList{ pathFromCommandLine });
// use nonexistent path to avoid loading random .qmlls.ini files that might be laying around.
// in this case, it should abort the search and the standard value we set in the settings
const QByteArray nonExistentUrl =
QUrl::fromLocalFile(u"./___thispathdoesnotexist123___/abcdefghijklmnop"_s).toEncoded();
QStringList result = model.buildPathsForFileUrl(nonExistentUrl);
QCOMPARE(result.size(), 1);
QCOMPARE(result.front(), expectedPath);
}
QTEST_MAIN(tst_qmlls_qqmlcodemodel)

View File

@ -0,0 +1,33 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#ifndef TST_QMLLS_QQMLCODEMODEL_H
#define TST_QMLLS_QQMLCODEMODEL_H
#include <QtJsonRpc/private/qjsonrpcprotocol_p.h>
#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtCore/qobject.h>
#include <QtCore/qprocess.h>
#include <QtCore/qlibraryinfo.h>
#include <QtTest/qtest.h>
#include <iostream>
using namespace Qt::StringLiterals;
class tst_qmlls_qqmlcodemodel : public QQmlDataTest
{
Q_OBJECT
public:
tst_qmlls_qqmlcodemodel() : QQmlDataTest(QT_QMLLS_QQMLCODEMODEL_DATADIR) { }
private slots:
void buildPathsForFileUrl_data();
void buildPathsForFileUrl();
};
#endif // TST_QMLLS_QQMLCODEMODEL_H

View File

@ -11,7 +11,7 @@ if(QT_FEATURE_commandlineparser)
endif()
add_subdirectory(qmlimportscanner)
add_subdirectory(qmlformat)
if (TARGET Qt::LanguageServerPrivate
if (TARGET Qt::LanguageServerPrivate AND QT_FEATURE_commandlineparser
AND NOT WASM AND NOT IOS AND NOT ANDROID AND NOT QNX AND NOT INTEGRITY AND NOT WEBOS)
add_subdirectory(qmlls)
endif()

View File

@ -132,7 +132,7 @@ int main(int argv, char *argc[])
QCoreApplication app(argv, argc);
QCoreApplication::setApplicationName("qmlls");
QCoreApplication::setApplicationVersion(QT_VERSION_STR);
#if QT_CONFIG(commandlineparser)
QCommandLineParser parser;
QQmlToolingSettings settings(QLatin1String("qmlls"));
parser.setApplicationDescription(QLatin1String(R"(QML languageserver)"));
@ -204,7 +204,6 @@ int main(int argv, char *argc[])
QThread::sleep(waitSeconds);
qDebug() << "starting";
}
#endif
QMutex writeMutex;
QQmlLanguageServer qmlServer(
[&writeMutex](const QByteArray &data) {
@ -213,8 +212,50 @@ int main(int argv, char *argc[])
std::cout.flush();
},
(parser.isSet(ignoreSettings) ? nullptr : &settings));
if (parser.isSet(buildDirOption))
qmlServer.codeModel()->setBuildPathsForRootUrl(QByteArray(), parser.values(buildDirOption));
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);
}
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.";
}
if (buildDirs.isEmpty() && envPaths.isEmpty()) {
qInfo() << "Build directory path omitted: Your source folders will be searched for "
".qmlls.ini files.";
}
StdinReader r;
QObject::connect(&r, &StdinReader::receivedData,
qmlServer.server(), &QLanguageServer::receiveData);