Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmllintsuggestions.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
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>
19#include <chrono>
20
22
23Q_STATIC_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint")
24
25using namespace QLspSpecification;
26using namespace QQmlJS::Dom;
27using namespace Qt::StringLiterals;
28
29namespace QmlLsp {
30
32{
33 switch (t) {
34 case QtDebugMsg:
35 return DiagnosticSeverity::Hint;
36 case QtInfoMsg:
37 return DiagnosticSeverity::Information;
38 case QtWarningMsg:
39 return DiagnosticSeverity::Warning;
40 case QtCriticalMsg:
41 case QtFatalMsg:
42 break;
43 }
44 return DiagnosticSeverity::Error;
45}
46
48 const QByteArray &, const CodeActionParams &params,
51{
52 QList<std::variant<Command, CodeAction>> responseData;
53
54 for (const Diagnostic &diagnostic : params.context.diagnostics) {
55 if (!diagnostic.data.has_value())
56 continue;
57
58 const auto &data = diagnostic.data.value();
59
60 int version = data[u"version"].toInt();
61 QJsonArray suggestions = data[u"suggestions"].toArray();
62
63 QList<WorkspaceEdit::DocumentChange> edits;
64 QString message;
65 for (const QJsonValue &suggestion : suggestions) {
66 QString replacement = suggestion[u"replacement"].toString();
67 message += suggestion[u"message"].toString() + u"\n";
68
69 TextEdit textEdit;
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();
75
76 TextDocumentEdit textDocEdit;
77 textDocEdit.textDocument = { params.textDocument, version };
78 textDocEdit.edits.append(textEdit);
79
80 edits.append(textDocEdit);
81 }
82 message.chop(1);
83 WorkspaceEdit edit;
84 edit.documentChanges = edits;
85
86 CodeAction action;
87 // VS Code and QtC ignore everything that is not a 'quickfix'.
88 action.kind = u"quickfix"_s.toUtf8();
89 action.edit = edit;
90 action.title = message.toUtf8();
91
92 responseData.append(action);
93 }
94
95 response.sendResponse(responseData);
96}
97
98void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
99{
100 protocol->registerCodeActionRequestHandler(&codeActionHandler);
101}
102
103void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &,
104 QLspSpecification::InitializeResult &serverInfo)
105{
106 serverInfo.capabilities.codeActionProvider = true;
107}
108
109QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server,
110 QmlLsp::QQmlCodeModelManager *codeModelManager)
111 : m_server(server), m_codeModelManager(codeModelManager)
112{
113 QObject::connect(m_codeModelManager, &QmlLsp::QQmlCodeModelManager::updatedSnapshot, this,
114 &QmlLintSuggestions::diagnose, Qt::DirectConnection);
115}
116
117static void advancePositionPastLocation_helper(const QString &fileContents, const QQmlJS::SourceLocation &location, Position &position) {
118 const int startOffset = location.offset;
119 const int length = location.length;
120 int i = startOffset;
121 int iEnd = i + length;
122 if (iEnd > int(fileContents.size()))
123 iEnd = fileContents.size();
124 while (i < iEnd) {
125 if (fileContents.at(i) == u'\n') {
126 ++position.line;
127 position.character = 0;
128 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
129 ++i;
130 } else {
131 ++position.character;
132 }
133 ++i;
134 }
135};
136
138{
139 Diagnostic diagnostic;
140 diagnostic.severity = DiagnosticSeverity::Warning;
141 Range &range = diagnostic.range;
142 Position &position = range.start;
143 position.line = 0;
144 position.character = 0;
145 Position &positionEnd = range.end;
146 positionEnd.line = 1;
147 diagnostic.message =
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 "
151 "spurious warnings";
152 diagnostic.source = QByteArray("qmllint");
153 return diagnostic;
154}
155
157static Diagnostic messageToDiagnostic_helper(AdvanceFunc advancePositionPastLocation,
158 std::optional<int> version, const Message &message)
159{
160 Diagnostic diagnostic;
161 diagnostic.severity = severityFromMsgType(message.type);
162 Range &range = diagnostic.range;
163 Position &position = range.start;
164
165 QQmlJS::SourceLocation srcLoc = message.loc;
166
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);
172 }
173
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())
176 .simplified()
177 .toUtf8();
178 } else {
179 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
180 }
181
182 diagnostic.source = QByteArray("qmllint");
183
184 auto suggestion = message.fixSuggestion;
185 if (!suggestion.has_value())
186 return diagnostic;
187
188 // We need to interject the information about where the fix suggestions end
189 // here since we don't have access to the textDocument to calculate it later.
190 const QQmlJS::SourceLocation cut = suggestion->location();
191
192 const int line = cut.isValid() ? cut.startLine - 1 : 0;
193 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
194
195 QJsonObject object;
196 object.insert("lspBeginLine"_L1, line);
197 object.insert("lspBeginCharacter"_L1, column);
198
199 Position end = { line, column };
200
201 if (srcLoc.isValid())
202 advancePositionPastLocation(cut, end);
203 object.insert("lspEndLine"_L1, end.line);
204 object.insert("lspEndCharacter"_L1, end.character);
205
206 object.insert("message"_L1, suggestion->fixDescription());
207 object.insert("replacement"_L1, suggestion->replacement());
208
209 QJsonArray fixedSuggestions;
210 fixedSuggestions.append(object);
211 QJsonObject data;
212 data[u"suggestions"] = fixedSuggestions;
213
214 Q_ASSERT(version.has_value());
215 data[u"version"] = version.value();
216
217 diagnostic.data = data;
218
219 return diagnostic;
220};
221
222static bool isSnapshotNew(std::optional<int> snapshotVersion, std::optional<int> processedVersion,
223 QmlLsp::UpdatePolicy policy)
224{
225 if (!snapshotVersion)
226 return false;
227 if (policy == ForceUpdate)
228 return true;
229 if (!processedVersion || *snapshotVersion > *processedVersion)
230 return true;
231 return false;
232}
233
234using namespace std::chrono_literals;
235
236QmlLintSuggestions::VersionToDiagnose
237QmlLintSuggestions::chooseVersionToDiagnoseHelper(const QByteArray &url,
238 QmlLsp::UpdatePolicy policy)
239{
240 const std::chrono::milliseconds maxInvalidTime = 400ms;
241 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModelManager->snapshotByUrl(url);
242
243 LastLintUpdate &lastUpdate = m_lastUpdate[url];
244
245 // ignore updates when already processed
246 if (policy != ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
247 qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc";
248 return NoDocumentAvailable{};
249 }
250
251 // try out a valid version, if there is one
252 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
253 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
254
255 // try out an invalid version, if there is one
256 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
257 if (auto since = lastUpdate.invalidUpdatesSince) {
258 // did we wait enough to get a valid document?
259 if (std::chrono::steady_clock::now() - *since > maxInvalidTime) {
260 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
261 }
262 } else {
263 // first time hitting the invalid document:
264 lastUpdate.invalidUpdatesSince = std::chrono::steady_clock::now();
265 }
266
267 // wait some time for extra keystrokes before diagnose
268 return TryAgainLater{ maxInvalidTime };
269 }
270 return NoDocumentAvailable{};
271}
272
273QmlLintSuggestions::VersionToDiagnose
274QmlLintSuggestions::chooseVersionToDiagnose(const QByteArray &url, QmlLsp::UpdatePolicy policy)
275{
276 QMutexLocker l(&m_mutex);
277 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
278 if (auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
279 // update immediately, and do not keep track of sent version, thus in extreme cases sent
280 // updates could be out of sync
281 LastLintUpdate &lastUpdate = m_lastUpdate[url];
282 lastUpdate.version = versionedDocument->version;
283 lastUpdate.invalidUpdatesSince.reset();
284 }
285 return versionToDiagnose;
286}
287
288void QmlLintSuggestions::diagnose(const QByteArray &url, QmlLsp::UpdatePolicy policy)
289{
290 auto versionedDocument = chooseVersionToDiagnose(url, policy);
291
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); });
297 },
298 [this, &url](const VersionedDocument &versionedDocument) {
299 diagnoseHelper(url, versionedDocument);
300 },
301
302 },
303 versionedDocument);
304}
305
306void QmlLintSuggestions::diagnoseHelper(const QByteArray &url,
307 const VersionedDocument &versionedDocument)
308{
309 auto [version, doc] = versionedDocument;
310
311 PublishDiagnosticsParams diagnosticParams;
312 diagnosticParams.uri = url;
313 diagnosticParams.version = version;
314
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));
319 // add source directory as last import as fallback in case there is no qmldir in the build
320 // folder this mimics qmllint behaviors
321 imports.append(QFileInfo(filename).dir().absolutePath());
322 // add m_server->clientInfo().rootUri & co?
323 bool silent = true;
324 const QString fileContents = doc.field(Fields::code).value().toString();
325 const QStringList qmltypesFiles;
326 const QStringList resourceFiles = QQmlJSUtils::resourceFilesFromBuildFolders(imports);
327
328 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::defaultCategories();
329
330 QQmlJSLinter linter(imports);
331
332 for (const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
333 for (const QQmlJS::LoggerCategory &category : plugin.categories())
334 categories.append(category);
335 }
336
337 QQmlToolingSettings settings(QLatin1String("qmllint"));
338 if (settings.search(filename).isValid()) {
339 QQmlJS::LoggingUtils::updateLogLevels(categories, settings, nullptr);
340 }
341
342 // TODO: pass the workspace folders to QQmlJSLinter
343 linter.lintFile(filename, &fileContents, silent, nullptr, imports, qmltypesFiles, resourceFiles,
344 categories);
345
346 // ### TODO: C++20 replace with bind_front
347 auto advancePositionPastLocation = [&fileContents](const QQmlJS::SourceLocation &location, Position &position)
348 {
349 advancePositionPastLocation_helper(fileContents, location, position);
350 };
351 auto messageToDiagnostic = [&advancePositionPastLocation,
352 versionedDocument](const Message &message) {
353 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
354 message);
355 };
356
357 QList<Diagnostic> diagnostics;
358 doc.iterateErrors(
359 [&diagnostics, &advancePositionPastLocation](const DomItem &, const ErrorMessage &msg) {
360 Diagnostic diagnostic;
361 diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level)));
362 // do something with msg.errorGroups ?
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);
373 return true;
374 },
375 true);
376
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));
382 return;
383 }
384
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\"?");
389
390 diagnostics.append(messageToDiagnostic(modified));
391 });
392 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
393 diagnostics.append(createMissingBuildDirDiagnostic());
394 }
395
396 diagnosticParams.diagnostics = diagnostics;
397
398 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
399 qCDebug(lintLog) << "lint" << QString::fromUtf8(url) << "found"
400 << diagnosticParams.diagnostics.size() << "issues"
401 << QTypedJson::toJsonValue(diagnosticParams);
402}
403
404} // namespace QmlLsp
405QT_END_NAMESPACE
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 &params, 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)