7#include <QtLanguageServer/private/qlanguageserverspec_p.h>
8#include <QtQmlCompiler/private/qqmljslinter_p.h>
9#include <QtQmlCompiler/private/qqmljslogger_p.h>
10#include <QtQmlCompiler/private/qqmljsutils_p.h>
11#include <QtQmlDom/private/qqmldom_utils_p.h>
12#include <QtQmlDom/private/qqmldomtop_p.h>
13#include <QtCore/qdebug.h>
14#include <QtCore/qdir.h>
15#include <QtCore/qfileinfo.h>
16#include <QtCore/qlibraryinfo.h>
17#include <QtCore/qtimer.h>
18#include <QtCore/qxpfunctional.h>
23Q_STATIC_LOGGING_CATEGORY(lintLog,
"qt.languageserver.lint")
25using namespace QLspSpecification;
26using namespace QQmlJS::Dom;
27using namespace Qt::StringLiterals;
35 return DiagnosticSeverity::Hint;
37 return DiagnosticSeverity::Information;
39 return DiagnosticSeverity::Warning;
44 return DiagnosticSeverity::Error;
48 const QByteArray &,
const CodeActionParams ¶ms,
52 QList<std::variant<Command, CodeAction>> responseData;
54 for (
const Diagnostic &diagnostic : params.context.diagnostics) {
55 if (!diagnostic.data.has_value())
58 const auto &data = diagnostic.data.value();
60 int version = data[u"version"].toInt();
61 QJsonArray suggestions = data[u"suggestions"].toArray();
63 QList<WorkspaceEdit::DocumentChange> edits;
65 for (
const QJsonValue &suggestion : suggestions) {
66 QString replacement = suggestion[u"replacement"].toString();
67 message += suggestion[u"message"].toString() + u"\n";
70 textEdit.range = { Position { suggestion[u"lspBeginLine"].toInt(),
71 suggestion[u"lspBeginCharacter"].toInt() },
72 Position { suggestion[u"lspEndLine"].toInt(),
73 suggestion[u"lspEndCharacter"].toInt() } };
74 textEdit.newText = replacement.toUtf8();
76 TextDocumentEdit textDocEdit;
77 textDocEdit.textDocument = { params.textDocument, version };
78 textDocEdit.edits.append(textEdit);
80 edits.append(textDocEdit);
84 edit.documentChanges = edits;
88 action.kind = u"quickfix"_s.toUtf8();
90 action.title = message.toUtf8();
92 responseData.append(action);
95 response.sendResponse(responseData);
100 protocol->registerCodeActionRequestHandler(&codeActionHandler);
104 QLspSpecification::InitializeResult &serverInfo)
106 serverInfo.capabilities.codeActionProvider =
true;
111 : m_server(server), m_codeModelManager(codeModelManager)
113 QObject::connect(m_codeModelManager, &QmlLsp::QQmlCodeModelManager::updatedSnapshot,
this,
114 &QmlLintSuggestions::diagnose, Qt::DirectConnection);
118 const int startOffset = location.offset;
119 const int length = location.length;
121 int iEnd = i + length;
122 if (iEnd >
int(fileContents.size()))
123 iEnd = fileContents.size();
125 if (fileContents.at(i) == u'\n') {
127 position.character = 0;
128 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
131 ++position.character;
139 Diagnostic diagnostic;
140 diagnostic.severity = DiagnosticSeverity::Warning;
141 Range &range = diagnostic.range;
142 Position &position = range.start;
144 position.character = 0;
145 Position &positionEnd = range.end;
146 positionEnd.line = 1;
148 "qmlls couldn't find a build directory. Pass the \"--build-dir <buildDir>\" option to "
149 "qmlls, set the environment variable \"QMLLS_BUILD_DIRS\", or create a .qmlls.ini "
150 "configuration file with a \"buildDir\" value in your project's source folder to avoid "
152 diagnostic.source = QByteArray(
"qmllint");
158 std::optional<
int> version,
const Message &message)
160 Diagnostic diagnostic;
161 diagnostic.severity = severityFromMsgType(message.type);
162 Range &range = diagnostic.range;
163 Position &position = range.start;
165 QQmlJS::SourceLocation srcLoc = message.loc;
167 if (srcLoc.isValid()) {
168 position.line = srcLoc.startLine - 1;
169 position.character = srcLoc.startColumn - 1;
170 range.end = position;
171 advancePositionPastLocation(message.loc, range.end);
174 if (message.fixSuggestion && !message.fixSuggestion->fixDescription().isEmpty()) {
175 diagnostic.message = u"%1: %2 [%3]"_s.arg(message.message, message.fixSuggestion->fixDescription(), message.id.toString())
179 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
182 diagnostic.source = QByteArray(
"qmllint");
184 auto suggestion = message.fixSuggestion;
185 if (!suggestion.has_value())
190 const QQmlJS::SourceLocation cut = suggestion->location();
192 const int line = cut.isValid() ? cut.startLine - 1 : 0;
193 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
196 object.insert(
"lspBeginLine"_L1, line);
197 object.insert(
"lspBeginCharacter"_L1, column);
199 Position end = { line, column };
201 if (srcLoc.isValid())
202 advancePositionPastLocation(cut, end);
203 object.insert(
"lspEndLine"_L1, end.line);
204 object.insert(
"lspEndCharacter"_L1, end.character);
206 object.insert(
"message"_L1, suggestion->fixDescription());
207 object.insert(
"replacement"_L1, suggestion->replacement());
209 QJsonArray fixedSuggestions;
210 fixedSuggestions.append(object);
212 data[u"suggestions"] = fixedSuggestions;
214 Q_ASSERT(version.has_value());
215 data[u"version"] = version.value();
217 diagnostic.data = data;
225 if (!snapshotVersion)
229 if (!processedVersion || *snapshotVersion > *processedVersion)
234using namespace std::chrono_literals;
236QmlLintSuggestions::VersionToDiagnose
240 const std::chrono::milliseconds maxInvalidTime = 400ms;
246 if (policy !=
ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
247 qCDebug(lspServerLog) <<
"skipped update of " << url <<
"unchanged valid doc";
248 return NoDocumentAvailable{};
252 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
253 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
256 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
257 if (
auto since = lastUpdate.invalidUpdatesSince) {
259 if (
std::chrono::steady_clock::now() - *since > maxInvalidTime) {
260 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
264 lastUpdate.invalidUpdatesSince =
std::chrono::steady_clock::now();
268 return TryAgainLater{ maxInvalidTime };
270 return NoDocumentAvailable{};
273QmlLintSuggestions::VersionToDiagnose
276 QMutexLocker l(&m_mutex);
277 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
278 if (
auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
282 lastUpdate.version = versionedDocument->version;
283 lastUpdate.invalidUpdatesSince.reset();
285 return versionToDiagnose;
290 auto versionedDocument = chooseVersionToDiagnose(url, policy);
292 std::visit(qOverloadedVisitor{
293 [](NoDocumentAvailable) {},
294 [
this, &url, &policy](
const TryAgainLater &tryAgainLater) {
295 QTimer::singleShot(tryAgainLater.time, Qt::VeryCoarseTimer,
this,
296 [
this, url, policy]() { diagnose(url, policy); });
298 [
this, &url](
const VersionedDocument &versionedDocument) {
299 diagnoseHelper(url, versionedDocument);
307 const VersionedDocument &versionedDocument)
309 auto [version, doc] = versionedDocument;
311 PublishDiagnosticsParams diagnosticParams;
312 diagnosticParams.uri = url;
313 diagnosticParams.version = version;
315 qCDebug(lintLog) <<
"has doc, do real lint";
316 QStringList imports = m_codeModelManager->buildPathsForFileUrl(url);
317 const QString filename = doc.canonicalFilePath();
318 imports.append(m_codeModelManager->importPathsForUrl(url));
321 imports.append(QFileInfo(filename).dir().absolutePath());
324 const QString fileContents = doc.field(Fields::code).value().toString();
325 const QStringList qmltypesFiles;
326 const QStringList resourceFiles = QQmlJSUtils::resourceFilesFromBuildFolders(imports);
328 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::defaultCategories();
330 QQmlJSLinter linter(imports);
332 for (
const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
333 for (
const QQmlJS::LoggerCategory &category : plugin.categories())
334 categories.append(category);
337 QQmlToolingSettings settings(QLatin1String(
"qmllint"));
338 if (settings.search(filename).isValid()) {
339 QQmlJS::LoggingUtils::updateLogLevels(categories, settings,
nullptr);
343 linter.lintFile(filename, &fileContents, silent,
nullptr, imports, qmltypesFiles, resourceFiles,
347 auto advancePositionPastLocation = [&fileContents](
const QQmlJS::SourceLocation &location, Position &position)
349 advancePositionPastLocation_helper(fileContents, location, position);
351 auto messageToDiagnostic = [&advancePositionPastLocation,
352 versionedDocument](
const Message &message) {
353 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
357 QList<Diagnostic> diagnostics;
359 [&diagnostics, &advancePositionPastLocation](
const DomItem &,
const ErrorMessage &msg) {
360 Diagnostic diagnostic;
361 diagnostic.severity = severityFromMsgType(QtMsgType(
int(msg.level)));
363 auto &location = msg.location;
364 Range &range = diagnostic.range;
365 range.start.line = location.startLine - 1;
366 range.start.character = location.startColumn - 1;
367 range.end = range.start;
368 advancePositionPastLocation(location, range.end);
369 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
370 diagnostic.source =
"domParsing";
371 diagnostic.message = msg.message.toUtf8();
372 diagnostics.append(diagnostic);
377 if (
const QQmlJSLogger *logger = linter.logger()) {
378 qsizetype nDiagnostics = diagnostics.size();
379 logger->iterateAllMessages([&](
const Message &message) {
380 if (!message.message.contains(u"Failed to import")) {
381 diagnostics.append(messageToDiagnostic(message));
385 Message modified {message};
386 modified.message.append(
387 u" Did you build your project? If yes, did you set the "
388 u"\"QT_QML_GENERATE_QMLLS_INI\" CMake variable on your project to \"ON\"?");
390 diagnostics.append(messageToDiagnostic(modified));
392 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
393 diagnostics.append(createMissingBuildDirDiagnostic());
396 diagnosticParams.diagnostics = diagnostics;
398 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
399 qCDebug(lintLog) <<
"lint" << QString::fromUtf8(url) <<
"found"
400 << diagnosticParams.diagnostics.size() <<
"issues"
401 << QTypedJson::toJsonValue(diagnosticParams);
Implements a server for the language server protocol.
void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, QLspSpecification::InitializeResult &) override
void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override
static Diagnostic messageToDiagnostic_helper(AdvanceFunc advancePositionPastLocation, std::optional< int > version, const Message &message)
static Diagnostic createMissingBuildDirDiagnostic()
static bool isSnapshotNew(std::optional< int > snapshotVersion, std::optional< int > processedVersion, QmlLsp::UpdatePolicy policy)
static void codeActionHandler(const QByteArray &, const CodeActionParams ¶ms, LSPPartialResponse< std::variant< QList< std::variant< Command, CodeAction > >, std::nullptr_t >, QList< std::variant< Command, CodeAction > > > &&response)
static void advancePositionPastLocation_helper(const QString &fileContents, const QQmlJS::SourceLocation &location, Position &position)
static DiagnosticSeverity severityFromMsgType(QtMsgType t)