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 <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(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
156static Diagnostic messageToDiagnostic_helper(AdvanceFunc advancePositionPastLocation,
157 std::optional<int> version, const Message &message)
158{
159 Diagnostic diagnostic;
160 diagnostic.severity = severityFromMsgType(message.type);
161 Range &range = diagnostic.range;
162 Position &position = range.start;
163
164 QQmlJS::SourceLocation srcLoc = message.loc;
165
166 if (srcLoc.isValid()) {
167 position.line = srcLoc.startLine - 1;
168 position.character = srcLoc.startColumn - 1;
169 range.end = position;
170 advancePositionPastLocation(message.loc, range.end);
171 }
172
173 if (message.fixSuggestion && !message.fixSuggestion->description().isEmpty()) {
174 diagnostic.message = u"%1: %2 [%3]"_s.arg(message.message, message.fixSuggestion->description(), message.id.toString())
175 .simplified()
176 .toUtf8();
177 } else {
178 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
179 }
180
181 diagnostic.source = QByteArray("qmllint");
182
183 auto suggestion = message.fixSuggestion;
184 if (!suggestion.has_value())
185 return diagnostic;
186
187 // We need to interject the information about where the fix suggestions end
188 // here since we don't have access to the textDocument to calculate it later.
189 const QQmlJS::SourceLocation cut = suggestion->location();
190
191 const int line = cut.isValid() ? cut.startLine - 1 : 0;
192 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
193
194 QJsonObject object;
195 object.insert("lspBeginLine"_L1, line);
196 object.insert("lspBeginCharacter"_L1, column);
197
198 Position end = { line, column };
199
200 if (srcLoc.isValid())
201 advancePositionPastLocation(cut, end);
202 object.insert("lspEndLine"_L1, end.line);
203 object.insert("lspEndCharacter"_L1, end.character);
204
205 object.insert("message"_L1, suggestion->description());
206 object.insert("replacement"_L1, suggestion->replacement());
207
208 QJsonArray fixedSuggestions;
209 fixedSuggestions.append(object);
210 QJsonObject data;
211 data[u"suggestions"] = fixedSuggestions;
212
213 Q_ASSERT(version.has_value());
214 data[u"version"] = version.value();
215
216 diagnostic.data = data;
217
218 return diagnostic;
219};
220
221static bool isSnapshotNew(std::optional<int> snapshotVersion, std::optional<int> processedVersion,
222 QmlLsp::UpdatePolicy policy)
223{
224 if (!snapshotVersion)
225 return false;
226 if (policy == ForceUpdate)
227 return true;
228 if (!processedVersion || *snapshotVersion > *processedVersion)
229 return true;
230 return false;
231}
232
233using namespace std::chrono_literals;
234
235QmlLintSuggestions::VersionToDiagnose
236QmlLintSuggestions::chooseVersionToDiagnoseHelper(const QByteArray &url,
237 QmlLsp::UpdatePolicy policy)
238{
239 const std::chrono::milliseconds maxInvalidTime = 400ms;
240 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModelManager->snapshotByUrl(url);
241
242 LastLintUpdate &lastUpdate = m_lastUpdate[url];
243
244 // ignore updates when already processed
245 if (policy != ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
246 qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc";
247 return NoDocumentAvailable{};
248 }
249
250 // try out a valid version, if there is one
251 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
252 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
253
254 // try out an invalid version, if there is one
255 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
256 if (auto since = lastUpdate.invalidUpdatesSince) {
257 // did we wait enough to get a valid document?
258 if (std::chrono::steady_clock::now() - *since > maxInvalidTime) {
259 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
260 }
261 } else {
262 // first time hitting the invalid document:
263 lastUpdate.invalidUpdatesSince = std::chrono::steady_clock::now();
264 }
265
266 // wait some time for extra keystrokes before diagnose
267 return TryAgainLater{ maxInvalidTime };
268 }
269 return NoDocumentAvailable{};
270}
271
272QmlLintSuggestions::VersionToDiagnose
273QmlLintSuggestions::chooseVersionToDiagnose(const QByteArray &url, QmlLsp::UpdatePolicy policy)
274{
275 QMutexLocker l(&m_mutex);
276 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
277 if (auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
278 // update immediately, and do not keep track of sent version, thus in extreme cases sent
279 // updates could be out of sync
280 LastLintUpdate &lastUpdate = m_lastUpdate[url];
281 lastUpdate.version = versionedDocument->version;
282 lastUpdate.invalidUpdatesSince.reset();
283 }
284 return versionToDiagnose;
285}
286
287void QmlLintSuggestions::diagnose(const QByteArray &url, QmlLsp::UpdatePolicy policy)
288{
289 auto versionedDocument = chooseVersionToDiagnose(url, policy);
290
291 std::visit(qOverloadedVisitor{
292 [](NoDocumentAvailable) {},
293 [this, &url, &policy](const TryAgainLater &tryAgainLater) {
294 QTimer::singleShot(tryAgainLater.time, Qt::VeryCoarseTimer, this,
295 [this, url, policy]() { diagnose(url, policy); });
296 },
297 [this, &url](const VersionedDocument &versionedDocument) {
298 diagnoseHelper(url, versionedDocument);
299 },
300
301 },
302 versionedDocument);
303}
304
305void QmlLintSuggestions::diagnoseHelper(const QByteArray &url,
306 const VersionedDocument &versionedDocument)
307{
308 auto [version, doc] = versionedDocument;
309
310 PublishDiagnosticsParams diagnosticParams;
311 diagnosticParams.uri = url;
312 diagnosticParams.version = version;
313
314 qCDebug(lintLog) << "has doc, do real lint";
315 QStringList imports = m_codeModelManager->importPathsForUrl(url);
316 const QString filename = doc.canonicalFilePath();
317 // add source directory as last import as fallback in case there is no qmldir in the build
318 // folder this mimics qmllint behaviors
319 imports.append(QFileInfo(filename).dir().absolutePath());
320 // add m_server->clientInfo().rootUri & co?
321 bool silent = true;
322 const QString fileContents = doc.field(Fields::code).value().toString();
323 const QStringList qmltypesFiles;
324 const QStringList resourceFiles = m_codeModelManager->resourceFilesForFileUrl(url);
325
326 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::builtinCategories();
327
328 QQmlJSLinter linter(imports);
329
330 for (const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
331 for (const QQmlJS::LoggerCategory &category : plugin.categories())
332 categories.append(category);
333 }
334
335 QQmlToolingSettings settings(QLatin1String("qmllint"));
336 if (settings.search(filename).isValid()) {
337 QQmlJS::LoggingUtils::updateLogSeverities(categories, settings, nullptr);
338 }
339
340 // TODO: pass the workspace folders to QQmlJSLinter
341 linter.lintFile(filename, &fileContents, silent, nullptr, imports, qmltypesFiles, resourceFiles,
342 categories);
343
344 // ### TODO: C++20 replace with bind_front
345 auto advancePositionPastLocation = [&fileContents](const QQmlJS::SourceLocation &location, Position &position)
346 {
347 advancePositionPastLocation_helper(fileContents, location, position);
348 };
349 auto messageToDiagnostic = [&advancePositionPastLocation,
350 versionedDocument](const Message &message) {
351 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
352 message);
353 };
354
355 QList<Diagnostic> diagnostics;
356 doc.iterateErrors(
357 [&diagnostics, &advancePositionPastLocation](const DomItem &, const ErrorMessage &msg) {
358 Diagnostic diagnostic;
359 diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level)));
360 // do something with msg.errorGroups ?
361 auto &location = msg.location;
362 Range &range = diagnostic.range;
363 range.start.line = location.startLine - 1;
364 range.start.character = location.startColumn - 1;
365 range.end = range.start;
366 advancePositionPastLocation(location, range.end);
367 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
368 diagnostic.source = "domParsing";
369 diagnostic.message = msg.message.toUtf8();
370 diagnostics.append(diagnostic);
371 return true;
372 },
373 true);
374
375 if (const QQmlJSLogger *logger = linter.logger()) {
376 qsizetype nDiagnostics = diagnostics.size();
377 logger->iterateAllMessages([&](const Message &message) {
378 if (!message.message.contains(u"Failed to import")) {
379 diagnostics.append(messageToDiagnostic(message));
380 return;
381 }
382
383 Message modified {message};
384 modified.message.append(u" Did you build your project?");
385
386 diagnostics.append(messageToDiagnostic(modified));
387 });
388 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
389 diagnostics.append(createMissingBuildDirDiagnostic());
390 }
391
392 diagnosticParams.diagnostics = diagnostics;
393
394 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
395 qCDebug(lintLog) << "lint" << QString::fromUtf8(url) << "found"
396 << diagnosticParams.diagnostics.size() << "issues"
397 << QTypedJson::toJsonValue(diagnosticParams);
398}
399
400} // namespace QmlLsp
401QT_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)
static DiagnosticSeverity severityFromMsgType(QtMsgType t)