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/qqmljslogger_p.h>
9#include <QtQmlCompiler/private/qqmljsutils_p.h>
10#include <QtQmlDom/private/qqmldom_utils_p.h>
11#include <QtQmlDom/private/qqmldomtop_p.h>
12#include <QtQmlLint/private/qqmljslinter_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 : std::as_const(suggestions)) {
66 message += suggestion[u"message"_s].toString() + u'\n';
67 const auto &documentEdits = suggestion[u"documentEdits"_s].toArray();
68 TextDocumentEdit textDocEdit;
69 for (const auto &documentEdit : documentEdits) {
70 TextEdit textEdit;
71 textEdit.range = { Position{ documentEdit[u"lspBeginLine"].toInt(),
72 documentEdit[u"lspBeginCharacter"].toInt() },
73 Position{ documentEdit[u"lspEndLine"].toInt(),
74 documentEdit[u"lspEndCharacter"].toInt() } };
75 textEdit.newText = documentEdit[u"replacement"_s].toString().toUtf8();
76 QString filename = documentEdit[u"filename"_s].toString();
77 textDocEdit.textDocument = { { filename.toUtf8() }, 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(QLspSpecification::ServerCapabilities &caps)
104{
105 caps.codeActionProvider = true;
106}
107
108QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server,
109 QmlLsp::QQmlCodeModelManager *codeModelManager)
110 : m_server(server), m_codeModelManager(codeModelManager)
111{
112 QObject::connect(m_codeModelManager, &QmlLsp::QQmlCodeModelManager::updatedSnapshot, this,
113 &QmlLintSuggestions::diagnose, Qt::DirectConnection);
114}
115
116static void advancePositionPastLocation_helper(const QString &fileContents, const QQmlJS::SourceLocation &location, Position &position) {
117 const int startOffset = location.offset;
118 const int length = location.length;
119 int i = startOffset;
120 int iEnd = i + length;
121 if (iEnd > int(fileContents.size()))
122 iEnd = fileContents.size();
123 while (i < iEnd) {
124 if (fileContents.at(i) == u'\n') {
125 ++position.line;
126 position.character = 0;
127 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
128 ++i;
129 } else {
130 ++position.character;
131 }
132 ++i;
133 }
134};
135
137{
138 Diagnostic diagnostic;
139 diagnostic.severity = DiagnosticSeverity::Warning;
140 Range &range = diagnostic.range;
141 Position &position = range.start;
142 position.line = 0;
143 position.character = 0;
144 Position &positionEnd = range.end;
145 positionEnd.line = 1;
146 diagnostic.message =
147 "qmlls couldn't find a build directory. Pass the \"--build-dir <buildDir>\" option to "
148 "qmlls, set the environment variable \"QMLLS_BUILD_DIRS\", or create a .qmlls.ini "
149 "configuration file with a \"buildDir\" value in your project's source folder to avoid "
150 "spurious warnings";
151 diagnostic.source = QByteArray("qmllint");
152 return diagnostic;
153}
154
156QJsonArray suggestionToJson(AdvanceFunc advancePositionPastLocation, const Message &message)
157{
158 const auto addLocationToJsonObject = [&](const auto &location, QJsonObject &object) {
159 const int line = location.isValid() ? location.startLine - 1 : 0;
160 const int column = location.isValid() ? location.startColumn - 1 : 0;
161 Position end = { line, column };
162 if (location.isValid())
163 advancePositionPastLocation(location, end);
164 object["lspBeginLine"_L1] = line;
165 object["lspBeginCharacter"_L1] = column;
166 object["lspEndLine"_L1] = end.line;
167 object["lspEndCharacter"_L1] = end.character;
168 };
169
170 if (!message.fixSuggestion.has_value())
171 return QJsonArray();
172
173 const QQmlJSFixSuggestion &suggestion = message.fixSuggestion.value();
174 QJsonArray documentEditsJson;
175 const auto &documentEdits = suggestion.documentEdits();
176 for (const auto &documentEdit : documentEdits) {
177 // We need to interject the information about where the fix suggestions end
178 // here since we don't have access to the textDocument to calculate it later.
179 const QQmlJS::SourceLocation cut = suggestion.location();
180 const int line = cut.isValid() ? cut.startLine - 1 : 0;
181 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
182
183 Position end = { line, column };
184 if (cut.isValid())
185 advancePositionPastLocation(cut, end);
186
187 QJsonObject documentEditJson{
188 { "filename"_L1, QUrl::fromLocalFile(documentEdit.m_filename).toString() },
189 { "replacement"_L1, documentEdit.m_replacement }
190 };
191 addLocationToJsonObject(documentEdit.m_location, documentEditJson);
192 documentEditsJson.append(documentEditJson);
193 }
194
195 QJsonObject fixSuggestionJson;
196 fixSuggestionJson["message"_L1] = message.fixSuggestion->description();
197 addLocationToJsonObject(message.fixSuggestion->location(), fixSuggestionJson);
198 fixSuggestionJson["documentEdits"_L1] = documentEditsJson;
199 fixSuggestionJson["autoApplicable"_L1] = message.fixSuggestion->isAutoApplicable();
200
201 QJsonArray fixSuggestionsJson;
202 fixSuggestionsJson.append(fixSuggestionJson);
203 return fixSuggestionsJson;
204}
205
206static Diagnostic messageToDiagnostic_helper(AdvanceFunc advancePositionPastLocation,
207 std::optional<int> version, const Message &message)
208{
209 Diagnostic diagnostic;
210 diagnostic.severity = severityFromMsgType(message.type);
211 Range &range = diagnostic.range;
212 Position &position = range.start;
213
214 QQmlJS::SourceLocation srcLoc = message.loc;
215
216 if (srcLoc.isValid()) {
217 position.line = srcLoc.startLine - 1;
218 position.character = srcLoc.startColumn - 1;
219 range.end = position;
220 advancePositionPastLocation(message.loc, range.end);
221 }
222
223 if (message.fixSuggestion && !message.fixSuggestion->description().isEmpty()) {
224 diagnostic.message = u"%1: %2 [%3]"_s.arg(message.message, message.fixSuggestion->description(), message.id.toString())
225 .simplified()
226 .toUtf8();
227 } else {
228 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
229 }
230
231 diagnostic.source = QByteArray("qmllint");
232
233 auto suggestion = message.fixSuggestion;
234 if (!suggestion.has_value())
235 return diagnostic;
236
237 QJsonObject data;
238 data[u"suggestions"] = suggestionToJson(advancePositionPastLocation, message);
239 Q_ASSERT(version.has_value());
240 data[u"version"] = version.value();
241
242 diagnostic.data = data;
243 return diagnostic;
244};
245
246static bool isSnapshotNew(std::optional<int> snapshotVersion, std::optional<int> processedVersion,
247 QmlLsp::UpdatePolicy policy)
248{
249 if (!snapshotVersion)
250 return false;
251 if (policy == ForceUpdate)
252 return true;
253 if (!processedVersion || *snapshotVersion > *processedVersion)
254 return true;
255 return false;
256}
257
258using namespace std::chrono_literals;
259
260QmlLintSuggestions::VersionToDiagnose
261QmlLintSuggestions::chooseVersionToDiagnoseHelper(const QByteArray &url,
262 QmlLsp::UpdatePolicy policy)
263{
264 const std::chrono::milliseconds maxInvalidTime = 400ms;
265 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModelManager->snapshotByUrl(url);
266
267 LastLintUpdate &lastUpdate = m_lastUpdate[url];
268
269 // ignore updates when already processed
270 if (policy != ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
271 qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc";
272 return NoDocumentAvailable{};
273 }
274
275 // try out a valid version, if there is one
276 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
277 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
278
279 // try out an invalid version, if there is one
280 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
281 if (auto since = lastUpdate.invalidUpdatesSince) {
282 // did we wait enough to get a valid document?
283 if (std::chrono::steady_clock::now() - *since > maxInvalidTime) {
284 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
285 }
286 } else {
287 // first time hitting the invalid document:
288 lastUpdate.invalidUpdatesSince = std::chrono::steady_clock::now();
289 }
290
291 // wait some time for extra keystrokes before diagnose
292 return TryAgainLater{ maxInvalidTime };
293 }
294 return NoDocumentAvailable{};
295}
296
297QmlLintSuggestions::VersionToDiagnose
298QmlLintSuggestions::chooseVersionToDiagnose(const QByteArray &url, QmlLsp::UpdatePolicy policy)
299{
300 QMutexLocker l(&m_mutex);
301 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
302 if (auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
303 // update immediately, and do not keep track of sent version, thus in extreme cases sent
304 // updates could be out of sync
305 LastLintUpdate &lastUpdate = m_lastUpdate[url];
306 lastUpdate.version = versionedDocument->version;
307 lastUpdate.invalidUpdatesSince.reset();
308 }
309 return versionToDiagnose;
310}
311
312void QmlLintSuggestions::diagnose(const QByteArray &url, QmlLsp::UpdatePolicy policy)
313{
314 auto versionedDocument = chooseVersionToDiagnose(url, policy);
315
316 std::visit(qOverloadedVisitor{
317 [](NoDocumentAvailable) {},
318 [this, &url, &policy](const TryAgainLater &tryAgainLater) {
319 QTimer::singleShot(tryAgainLater.time, Qt::VeryCoarseTimer, this,
320 [this, url, policy]() { diagnose(url, policy); });
321 },
322 [this, &url](const VersionedDocument &versionedDocument) {
323 diagnoseHelper(url, versionedDocument);
324 },
325
326 },
327 versionedDocument);
328}
329
330void QmlLintSuggestions::diagnoseHelper(const QByteArray &url,
331 const VersionedDocument &versionedDocument)
332{
333 auto [version, doc] = versionedDocument;
334
335 PublishDiagnosticsParams diagnosticParams;
336 diagnosticParams.uri = url;
337 diagnosticParams.version = version;
338
339 qCDebug(lintLog) << "has doc, do real lint";
340 QStringList imports = m_codeModelManager->importPathsForUrl(url);
341 const QString filename = doc.canonicalFilePath();
342 // add source directory as last import as fallback in case there is no qmldir in the build
343 // folder this mimics qmllint behaviors
344 imports.append(QFileInfo(filename).dir().absolutePath());
345 // add m_server->clientInfo().rootUri & co?
346 bool silent = true;
347 const QString fileContents = doc.field(Fields::code).value().toString();
348 const QStringList qmltypesFiles;
349 const QStringList resourceFiles = m_codeModelManager->resourceFilesForFileUrl(url);
350
351 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::builtinCategories();
352
353 QQmlJSLinter linter(imports);
354
355 for (const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
356 for (const QQmlJS::LoggerCategory &category : plugin.categories())
357 categories.append(category);
358 }
359
360 QQmlToolingSettings settings(QLatin1String("qmllint"));
361 if (settings.search(filename).isValid()) {
362 QQmlJS::LoggingUtils::updateLogSeverities(categories, settings, nullptr);
363 }
364
365 // TODO: pass the workspace folders to QQmlJSLinter
366 linter.lintFile(filename, &fileContents, silent, nullptr, imports, qmltypesFiles, resourceFiles,
367 categories);
368
369 // ### TODO: C++20 replace with bind_front
370 auto advancePositionPastLocation = [&fileContents](const QQmlJS::SourceLocation &location, Position &position)
371 {
372 advancePositionPastLocation_helper(fileContents, location, position);
373 };
374 auto messageToDiagnostic = [&advancePositionPastLocation,
375 versionedDocument](const Message &message) {
376 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
377 message);
378 };
379
380 QList<Diagnostic> diagnostics;
381 doc.iterateErrors(
382 [&diagnostics, &advancePositionPastLocation](const DomItem &, const ErrorMessage &msg) {
383 Diagnostic diagnostic;
384 diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level)));
385 // do something with msg.errorGroups ?
386 auto &location = msg.location;
387 Range &range = diagnostic.range;
388 range.start.line = location.startLine - 1;
389 range.start.character = location.startColumn - 1;
390 range.end = range.start;
391 advancePositionPastLocation(location, range.end);
392 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
393 diagnostic.source = "domParsing";
394 diagnostic.message = msg.message.toUtf8();
395 diagnostics.append(diagnostic);
396 return true;
397 },
398 true);
399
400 if (const QQmlJSLogger *logger = linter.logger()) {
401 qsizetype nDiagnostics = diagnostics.size();
402 logger->iterateAllMessages([&](const Message &message) {
403 if (!message.message.contains(u"Failed to import")) {
404 diagnostics.append(messageToDiagnostic(message));
405 return;
406 }
407
408 Message modified {message};
409 modified.message.append(u" Did you build your project?");
410
411 diagnostics.append(messageToDiagnostic(modified));
412 });
413 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
414 diagnostics.append(createMissingBuildDirDiagnostic());
415 }
416
417 diagnosticParams.diagnostics = diagnostics;
418
419 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
420 qCDebug(lintLog) << "lint" << QString::fromUtf8(url) << "found"
421 << diagnosticParams.diagnostics.size() << "issues"
422 << QTypedJson::toJsonValue(diagnosticParams);
423}
424
425} // namespace QmlLsp
426QT_END_NAMESPACE
void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override
void setupCapabilities(QLspSpecification::ServerCapabilities &caps) override
Combined button and popup list for selecting options.
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)
QJsonArray suggestionToJson(AdvanceFunc advancePositionPastLocation, const Message &message)
static DiagnosticSeverity severityFromMsgType(QtMsgType t)