qtdeclarative/tests/auto/qmlls/completions/tst_qmllscompletions.cpp

501 lines
22 KiB
C++

// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtJsonRpc/private/qjsonrpcprotocol_p.h>
#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtCore/private/qduplicatetracker_p.h>
#include <QtCore/qobject.h>
#include <QtCore/qprocess.h>
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qstringlist.h>
#include <QtTest/qtest.h>
#include <QtQmlLS/private/qlspcustomtypes_p.h>
#include <iostream>
#include <variant>
// Check if QTest already has a QTEST_CHECKED macro
#ifndef QTEST_CHECKED
#define QTEST_CHECKED(...) \
do { \
__VA_ARGS__; \
if (QTest::currentTestFailed()) \
return; \
} while (false)
#endif
QT_USE_NAMESPACE
using namespace Qt::StringLiterals;
using namespace QLspSpecification;
class tst_QmllsCompletions : public QQmlDataTest
{
using ExpectedCompletion = QPair<QString, CompletionItemKind>;
using ExpectedCompletions = QList<ExpectedCompletion>;
using ExpectedDocumentation = std::tuple<QString, QString, QString>;
using ExpectedDocumentations = QList<ExpectedDocumentation>;
Q_OBJECT
public:
tst_QmllsCompletions();
void checkCompletions(QByteArray uri, int lineNr, int character, ExpectedCompletions expected,
QStringList notExpected);
private slots:
void initTestCase() final;
void completions_data();
void completions();
void function_documentations_data();
void function_documentations();
void buildDir();
void cleanupTestCase();
private:
QProcess m_server;
QLanguageServerProtocol m_protocol;
QString m_qmllsPath;
QList<QByteArray> m_uriToClose;
};
tst_QmllsCompletions::tst_QmllsCompletions()
: QQmlDataTest(QT_QMLTEST_DATADIR),
m_protocol([this](const QByteArray &data) { m_server.write(data); })
{
connect(&m_server, &QProcess::readyReadStandardOutput, this, [this]() {
QByteArray data = m_server.readAllStandardOutput();
m_protocol.receiveData(data);
});
connect(&m_server, &QProcess::readyReadStandardError, this,
[this]() {
QProcess::ProcessChannel tmp = m_server.readChannel();
m_server.setReadChannel(QProcess::StandardError);
while (m_server.canReadLine())
std::cerr << m_server.readLine().constData();
m_server.setReadChannel(tmp);
});
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);
m_protocol.registerPublishDiagnosticsNotificationHandler([](const QByteArray &, auto) {
// ignoring qmlint notifications
});
}
void tst_QmllsCompletions::initTestCase()
{
QQmlDataTest::initTestCase();
if (!QFileInfo::exists(m_qmllsPath)) {
QString message =
QStringLiteral("qmlls executable not found (looked for %0)").arg(m_qmllsPath);
QSKIP(qPrintable(message)); // until we add a feature for this we avoid failing here
}
m_server.start();
InitializeParams clientInfo;
clientInfo.rootUri = QUrl::fromLocalFile(dataDirectory() + "/default").toString().toUtf8();
TextDocumentClientCapabilities tDoc;
PublishDiagnosticsClientCapabilities pDiag;
tDoc.publishDiagnostics = pDiag;
pDiag.versionSupport = true;
clientInfo.capabilities.textDocument = tDoc;
bool didInit = false;
m_protocol.requestInitialize(clientInfo, [this, &didInit](const InitializeResult &serverInfo) {
Q_UNUSED(serverInfo);
m_protocol.notifyInitialized(InitializedParams());
didInit = true;
});
QTRY_COMPARE_WITH_TIMEOUT(didInit, true, 10000);
for (const QString &filePath :
QStringList({ u"completions/Yyy.qml"_s, u"completions/fromBuildDir.qml"_s })) {
QFile file(testFile(filePath));
QVERIFY(file.open(QIODevice::ReadOnly));
DidOpenTextDocumentParams oParams;
TextDocumentItem textDocument;
QByteArray uri = testFileUrl(filePath).toString().toUtf8();
textDocument.uri = uri;
textDocument.text = file.readAll();
oParams.textDocument = textDocument;
m_protocol.notifyDidOpenTextDocument(oParams);
m_uriToClose.append(uri);
}
}
void tst_QmllsCompletions::completions_data()
{
QTest::addColumn<QByteArray>("uri");
QTest::addColumn<int>("lineNr");
QTest::addColumn<int>("character");
QTest::addColumn<ExpectedCompletions>("expected");
QTest::addColumn<QStringList>("notExpected");
QByteArray uri = testFileUrl("completions/Yyy.qml").toString().toUtf8();
QTest::newRow("objEmptyLine") << uri << 8 << 0
<< ExpectedCompletions({
{ u"Rectangle"_s, CompletionItemKind::Class },
{ u"property"_s, CompletionItemKind::Keyword },
{ u"width"_s, CompletionItemKind::Property },
{ u"function"_s, CompletionItemKind::Keyword },
})
<< QStringList({ u"QtQuick"_s, u"vector4d"_s });
QTest::newRow("inBindingLabel") << uri << 5 << 9
<< ExpectedCompletions({
{ u"Rectangle"_s, CompletionItemKind::Class },
{ u"property"_s, CompletionItemKind::Keyword },
{ u"width"_s, CompletionItemKind::Property },
})
<< QStringList({ u"QtQuick"_s, u"vector4d"_s });
QTest::newRow("afterBinding") << uri << 5 << 10
<< ExpectedCompletions({
{ u"Rectangle"_s, CompletionItemKind::Field },
{ u"width"_s, CompletionItemKind::Field },
{ u"vector4d"_s, CompletionItemKind::Field },
})
<< QStringList({ u"QtQuick"_s, u"property"_s });
// suppress?
QTest::newRow("afterId") << uri << 4 << 7
<< ExpectedCompletions({
{ u"import"_s, CompletionItemKind::Keyword },
})
<< QStringList({ u"QtQuick"_s, u"property"_s, u"Rectangle"_s,
u"width"_s, u"vector4d"_s });
QTest::newRow("fileStart") << uri << 0 << 0
<< ExpectedCompletions({
{ u"Rectangle"_s, CompletionItemKind::Class },
{ u"import"_s, CompletionItemKind::Keyword },
})
<< QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s });
QTest::newRow("importImport") << uri << 0 << 3
<< ExpectedCompletions({
{ u"Rectangle"_s, CompletionItemKind::Class },
{ u"import"_s, CompletionItemKind::Keyword },
})
<< QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s });
QTest::newRow("importModuleStart")
<< uri << 0 << 7
<< ExpectedCompletions({
{ u"QtQuick"_s, CompletionItemKind::Module },
})
<< QStringList({ u"vector4d"_s, u"width"_s, u"Rectangle"_s, u"import"_s });
QTest::newRow("importVersionStart")
<< uri << 0 << 15
<< ExpectedCompletions({
{ u"2"_s, CompletionItemKind::Constant },
{ u"as"_s, CompletionItemKind::Keyword },
})
<< QStringList({ u"Rectangle"_s, u"import"_s, u"vector4d"_s, u"width"_s });
// QTest::newRow("importVersionMinor")
// << uri << 0 << 17
// << ExpectedCompletions({
// { u"15"_s, CompletionItemKind::Constant },
// })
// << QStringList({ u"as"_s, u"Rectangle"_s, u"import"_s, u"vector4d"_s, u"width"_s });
QTest::newRow("inScript") << uri << 6 << 14
<< ExpectedCompletions({
{ u"Rectangle"_s, CompletionItemKind::Field },
{ u"vector4d"_s, CompletionItemKind::Field },
{ u"lala()"_s, CompletionItemKind::Function },
{ u"longfunction()"_s, CompletionItemKind::Function },
{ u"documentedFunction()"_s,
CompletionItemKind::Function },
{ u"lala()"_s, CompletionItemKind { 0 } },
{ u"width"_s, CompletionItemKind::Field },
})
<< QStringList({ u"import"_s });
QTest::newRow("expandBase1") << uri << 9 << 23
<< ExpectedCompletions({
{ u"width"_s, CompletionItemKind::Field },
{ u"foo"_s, CompletionItemKind::Field },
})
<< QStringList({ u"import"_s, u"Rectangle"_s });
QTest::newRow("expandBase2") << uri << 10 << 29
<< ExpectedCompletions({
{ u"width"_s, CompletionItemKind::Field },
{ u"color"_s, CompletionItemKind::Field },
})
<< QStringList({ u"foo"_s, u"import"_s, u"Rectangle"_s });
QTest::newRow("asCompletions")
<< uri << 25 << 8
<< ExpectedCompletions({
{ u"Rectangle"_s, CompletionItemKind::Field },
})
<< QStringList({ u"foo"_s, u"import"_s, u"lala()"_s, u"width"_s });
}
void tst_QmllsCompletions::checkCompletions(QByteArray uri, int lineNr, int character,
ExpectedCompletions expected, QStringList notExpected)
{
CompletionParams cParams;
cParams.position.line = lineNr;
cParams.position.character = character;
cParams.textDocument.uri = uri;
std::shared_ptr<bool> didFinish = std::make_shared<bool>(false);
auto clean = [didFinish]() { *didFinish = true; };
m_protocol.requestCompletion(
cParams,
[clean, uri, expected, notExpected](auto res) {
QScopeGuard cleanup(clean);
const QList<CompletionItem> *cItems = std::get_if<QList<CompletionItem>>(&res);
if (!cItems) {
return;
}
QSet<QString> labels;
QDuplicateTracker<QByteArray> modulesTracker;
QDuplicateTracker<QByteArray> keywordsTracker;
QDuplicateTracker<QByteArray> classesTracker;
QDuplicateTracker<QByteArray> fieldsTracker;
QDuplicateTracker<QByteArray> propertiesTracker;
for (const CompletionItem &c : *cItems) {
if (c.kind->toInt() == int(CompletionItemKind::Module)) {
QVERIFY2(!modulesTracker.hasSeen(c.label), "Duplicate module: " + c.label);
} else if (c.kind->toInt() == int(CompletionItemKind::Keyword)) {
QVERIFY2(!keywordsTracker.hasSeen(c.label),
"Duplicate keyword: " + c.label);
} else if (c.kind->toInt() == int(CompletionItemKind::Class)) {
QVERIFY2(!classesTracker.hasSeen(c.label), "Duplicate class: " + c.label);
} else if (c.kind->toInt() == int(CompletionItemKind::Field)) {
QVERIFY2(!fieldsTracker.hasSeen(c.label), "Duplicate field: " + c.label);
} else if (c.kind->toInt() == int(CompletionItemKind::Property)) {
QVERIFY2(!propertiesTracker.hasSeen(c.label),
"Duplicate property: " + c.label);
QVERIFY2(c.insertText == c.label + u": "_s,
"a property should end with a colon with a space for "
"'insertText', for better coding experience");
}
labels << c.label;
}
for (const ExpectedCompletion &exp : expected) {
QVERIFY2(labels.contains(exp.first),
u"no %1 in %2"_s
.arg(exp.first,
QStringList(labels.begin(), labels.end()).join(u", "_s))
.toUtf8());
if (labels.contains(exp.first)) {
for (const CompletionItem &c : *cItems) {
const auto kind = static_cast<CompletionItemKind>(c.kind->toInt());
bool foundEntry = false;
bool hasCorrectKind = false;
for (const ExpectedCompletion &e : expected) {
if (c.label == e.first) {
foundEntry = true;
hasCorrectKind |= kind == e.second;
}
}
// Ignore QVERIFY for those completions not in the expected list.
if (!foundEntry)
continue;
QVERIFY2(hasCorrectKind,
qPrintable(
QString::fromLatin1(
"Completion item '%1' has wrong kind '%2'")
.arg(c.label)
.arg(QMetaEnum::fromType<CompletionItemKind>()
.valueToKey(int(kind)))));
}
}
}
for (const QString &nexp : notExpected) {
QVERIFY2(!labels.contains(nexp),
u"found unexpected completion %1"_s.arg(nexp).toUtf8());
}
},
[clean](const ResponseError &err) {
QScopeGuard cleanup(clean);
ProtocolBase::defaultResponseErrorHandler(err);
QVERIFY2(false, "error computing the completion");
});
QTRY_VERIFY_WITH_TIMEOUT(*didFinish, 30000);
}
void tst_QmllsCompletions::completions()
{
QFETCH(QByteArray, uri);
QFETCH(int, lineNr);
QFETCH(int, character);
QFETCH(ExpectedCompletions, expected);
QFETCH(QStringList, notExpected);
QTEST_CHECKED(checkCompletions(uri, lineNr, character, expected, notExpected));
}
void tst_QmllsCompletions::function_documentations_data()
{
QTest::addColumn<QByteArray>("uri");
QTest::addColumn<int>("lineNr");
QTest::addColumn<int>("character");
QTest::addColumn<ExpectedDocumentations>("expectedDocs");
QByteArray uri = testFileUrl("completions/Yyy.qml").toString().toUtf8();
QTest::newRow("longfunction")
<< uri << 5 << 14
<< ExpectedDocumentations{
std::make_tuple(u"lala()"_s, u"returns void"_s, u"lala()"_s),
std::make_tuple(u"longfunction()"_s, u"returns string"_s,
uR"(longfunction(a, b, c = "c", d = "d"))"_s),
std::make_tuple(u"documentedFunction()"_s, u"returns string"_s,
uR"(// documentedFunction: is documented
// returns 'Good'
documentedFunction(arg1, arg2 = "Qt"))"_s),
};
}
void tst_QmllsCompletions::function_documentations()
{
QFETCH(QByteArray, uri);
QFETCH(int, lineNr);
QFETCH(int, character);
QFETCH(ExpectedDocumentations, expectedDocs);
CompletionParams cParams;
cParams.position.line = lineNr;
cParams.position.character = character;
cParams.textDocument.uri = uri;
std::shared_ptr<bool> didFinish = std::make_shared<bool>(false);
auto clean = [didFinish]() { *didFinish = true; };
m_protocol.requestCompletion(
cParams,
[clean, uri, expectedDocs](auto res) {
const QList<CompletionItem> *cItems = std::get_if<QList<CompletionItem>>(&res);
if (!cItems) {
return;
}
for (const ExpectedDocumentation &exp : expectedDocs) {
bool hasFoundExpected = false;
const auto expectedLabel = std::get<0>(exp);
for (const CompletionItem &c : *cItems) {
if (c.kind->toInt() != int(CompletionItemKind::Function)) {
// Only check functions.
continue;
}
if (c.label == expectedLabel) {
hasFoundExpected = true;
}
}
QVERIFY2(hasFoundExpected,
qPrintable(u"expected completion label '%1' wasn't found"_s.arg(
expectedLabel)));
}
for (const CompletionItem &c : *cItems) {
if (c.kind->toInt() != int(CompletionItemKind::Function)) {
// Only check functions.
continue;
}
QVERIFY(c.documentation != std::nullopt);
// We currently don't support 'MarkupContent', change this when we do.
QVERIFY(c.documentation->index() == 0);
const QByteArray cDoc = std::get<0>(*c.documentation);
for (const ExpectedDocumentation &exp : expectedDocs) {
const auto &[label, details, docs] = exp;
if (c.label != label)
continue;
QVERIFY2(c.detail == details,
qPrintable(u"Completion item '%1' has wrong details '%2'"_s
.arg(label).arg(*c.detail)));
QVERIFY2(cDoc == docs,
qPrintable(u"Completion item '%1' has wrong documentation '%2'"_s
.arg(label).arg(cDoc)));
}
}
clean();
},
[clean](const ResponseError &err) {
ProtocolBase::defaultResponseErrorHandler(err);
QVERIFY2(false, "error computing the completion");
clean();
});
QTRY_VERIFY_WITH_TIMEOUT(*didFinish, 30000);
}
void tst_QmllsCompletions::buildDir()
{
QString filePath = u"completions/fromBuildDir.qml"_s;
QByteArray uri = testFileUrl(filePath).toString().toUtf8();
QTEST_CHECKED(checkCompletions(uri, 3, 0,
ExpectedCompletions({
{ u"property"_s, CompletionItemKind::Keyword },
{ u"function"_s, CompletionItemKind::Keyword },
{ u"Rectangle"_s, CompletionItemKind::Class },
}),
QStringList({ u"BuildDirType"_s, u"QtQuick"_s, u"width"_s, u"vector4d"_s })));
Notifications::AddBuildDirsParams bDirs;
UriToBuildDirs ub;
ub.baseUri = uri;
ub.buildDirs.append(testFile("buildDir").toUtf8());
bDirs.buildDirsToSet.append(ub);
m_protocol.typedRpc()->sendNotification(QByteArray(Notifications::AddBuildDirsMethod), bDirs);
DidChangeTextDocumentParams didChange;
didChange.textDocument.uri = uri;
didChange.textDocument.version = 2;
TextDocumentContentChangeEvent change;
QFile file(testFile(filePath));
QVERIFY(file.open(QIODevice::ReadOnly));
change.text = file.readAll();
didChange.contentChanges.append(change);
m_protocol.notifyDidChangeTextDocument(didChange);
QTEST_CHECKED(checkCompletions(uri, 3, 0,
ExpectedCompletions({
{ u"BuildDirType"_s, CompletionItemKind::Class },
{ u"Rectangle"_s, CompletionItemKind::Class },
{ u"property"_s, CompletionItemKind::Keyword },
{ u"width"_s, CompletionItemKind::Property },
{ u"function"_s, CompletionItemKind::Keyword },
}),
QStringList({ u"QtQuick"_s, u"vector4d"_s })));
}
void tst_QmllsCompletions::cleanupTestCase()
{
for (const QByteArray &uri : m_uriToClose) {
DidCloseTextDocumentParams closeP;
closeP.textDocument.uri = uri;
m_protocol.notifyDidCloseTextDocument(closeP);
}
m_server.closeWriteChannel();
QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
QCOMPARE(m_server.exitStatus(), QProcess::NormalExit);
}
QTEST_MAIN(tst_QmllsCompletions)
#include <tst_qmllscompletions.moc>