Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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
5
6#include <QtLanguageServer/private/qlanguageserverspec_p.h>
7#include <QtQmlCompiler/private/qqmljslinter_p.h>
8#include <QtQmlCompiler/private/qqmljslogger_p.h>
9#include <QtQmlDom/private/qqmldom_utils_p.h>
10#include <QtQmlDom/private/qqmldomtop_p.h>
11#include <QtCore/qdebug.h>
12#include <QtCore/qdir.h>
13#include <QtCore/qfileinfo.h>
14#include <QtCore/qlibraryinfo.h>
15#include <QtCore/qtimer.h>
16#include <QtCore/qxpfunctional.h>
17#include <chrono>
18
19using namespace QLspSpecification;
20using namespace QQmlJS::Dom;
21using namespace Qt::StringLiterals;
22
23Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint")
24
26namespace QmlLsp {
27
28static DiagnosticSeverity severityFromMsgType(QtMsgType t)
29{
30 switch (t) {
31 case QtDebugMsg:
32 return DiagnosticSeverity::Hint;
33 case QtInfoMsg:
34 return DiagnosticSeverity::Information;
35 case QtWarningMsg:
36 return DiagnosticSeverity::Warning;
37 case QtCriticalMsg:
38 case QtFatalMsg:
39 break;
40 }
41 return DiagnosticSeverity::Error;
42}
43
45 const QByteArray &, const CodeActionParams &params,
46 LSPPartialResponse<std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>,
47 QList<std::variant<Command, CodeAction>>> &&response)
48{
49 QList<std::variant<Command, CodeAction>> responseData;
50
51 for (const Diagnostic &diagnostic : params.context.diagnostics) {
52 if (!diagnostic.data.has_value())
53 continue;
54
55 const auto &data = diagnostic.data.value();
56
57 int version = data[u"version"].toInt();
58 QJsonArray suggestions = data[u"suggestions"].toArray();
59
60 QList<WorkspaceEdit::DocumentChange> edits;
62 for (const QJsonValue &suggestion : suggestions) {
63 QString replacement = suggestion[u"replacement"].toString();
64 message += suggestion[u"message"].toString() + u"\n";
65
67 textEdit.range = { Position { suggestion[u"lspBeginLine"].toInt(),
68 suggestion[u"lspBeginCharacter"].toInt() },
69 Position { suggestion[u"lspEndLine"].toInt(),
70 suggestion[u"lspEndCharacter"].toInt() } };
71 textEdit.newText = replacement.toUtf8();
72
73 TextDocumentEdit textDocEdit;
74 textDocEdit.textDocument = { params.textDocument, version };
75 textDocEdit.edits.append(textEdit);
76
77 edits.append(textDocEdit);
78 }
79 message.chop(1);
80 WorkspaceEdit edit;
81 edit.documentChanges = edits;
82
83 CodeAction action;
84 // VS Code and QtC ignore everything that is not a 'quickfix'.
85 action.kind = u"quickfix"_s.toUtf8();
86 action.edit = edit;
87 action.title = message.toUtf8();
88
89 responseData.append(action);
90 }
91
92 response.sendResponse(responseData);
93}
94
95void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
96{
97 protocol->registerCodeActionRequestHandler(&codeActionHandler);
98}
99
100void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &,
101 QLspSpecification::InitializeResult &serverInfo)
102{
103 serverInfo.capabilities.codeActionProvider = true;
104}
105
106QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel)
107 : m_server(server), m_codeModel(codeModel)
108{
111}
112
113static void advancePositionPastLocation_helper(const QString &fileContents, const QQmlJS::SourceLocation &location, Position &position) {
114 const int startOffset = location.offset;
115 const int length = location.length;
116 int i = startOffset;
117 int iEnd = i + length;
118 if (iEnd > int(fileContents.size()))
119 iEnd = fileContents.size();
120 while (i < iEnd) {
121 if (fileContents.at(i) == u'\n') {
122 ++position.line;
123 position.character = 0;
124 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
125 ++i;
126 } else {
127 ++position.character;
128 }
129 ++i;
130 }
131};
132
134{
135 Diagnostic diagnostic;
136 diagnostic.severity = DiagnosticSeverity::Warning;
137 Range &range = diagnostic.range;
138 Position &position = range.start;
139 position.line = 0;
140 position.character = 0;
141 Position &positionEnd = range.end;
142 positionEnd.line = 1;
143 diagnostic.message =
144 "qmlls could not find a build directory, without a build directory "
145 "containing a current build there could be spurious warnings, you might "
146 "want to pass the --build-dir <buildDir> option to qmlls, or set the "
147 "environment variable QMLLS_BUILD_DIRS.";
148 diagnostic.source = QByteArray("qmllint");
149 return diagnostic;
150}
151
153static Diagnostic messageToDiagnostic_helper(AdvanceFunc advancePositionPastLocation,
154 std::optional<int> version, const Message &message)
155{
156 Diagnostic diagnostic;
157 diagnostic.severity = severityFromMsgType(message.type);
158 Range &range = diagnostic.range;
159 Position &position = range.start;
160
161 QQmlJS::SourceLocation srcLoc = message.loc;
162
163 if (srcLoc.isValid()) {
164 position.line = srcLoc.startLine - 1;
165 position.character = srcLoc.startColumn - 1;
166 range.end = position;
167 advancePositionPastLocation(message.loc, range.end);
168 }
169
170 if (message.fixSuggestion && !message.fixSuggestion->fixDescription().isEmpty()) {
171 diagnostic.message = QString(message.message)
172 .append(u": "_s)
173 .append(message.fixSuggestion->fixDescription())
174 .simplified()
175 .toUtf8();
176 } else {
177 diagnostic.message = message.message.toUtf8();
178 }
179
180 diagnostic.source = QByteArray("qmllint");
181
182 auto suggestion = message.fixSuggestion;
183 if (!suggestion.has_value())
184 return diagnostic;
185
186 // We need to interject the information about where the fix suggestions end
187 // here since we don't have access to the textDocument to calculate it later.
188 const QQmlJS::SourceLocation cut = suggestion->location();
189
190 const int line = cut.isValid() ? cut.startLine - 1 : 0;
191 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
192
194 object.insert("lspBeginLine"_L1, line);
195 object.insert("lspBeginCharacter"_L1, column);
196
197 Position end = { line, column };
198
199 if (srcLoc.isValid())
200 advancePositionPastLocation(cut, end);
201 object.insert("lspEndLine"_L1, end.line);
202 object.insert("lspEndCharacter"_L1, end.character);
203
204 object.insert("message"_L1, suggestion->fixDescription());
205 object.insert("replacement"_L1, suggestion->replacement());
206
207 QJsonArray fixedSuggestions;
208 fixedSuggestions.append(object);
210 data[u"suggestions"] = fixedSuggestions;
211
212 Q_ASSERT(version.has_value());
213 data[u"version"] = version.value();
214
215 diagnostic.data = data;
216
217 return diagnostic;
218};
219
220static bool isSnapshotNew(std::optional<int> snapshotVersion, std::optional<int> processedVersion)
221{
222 if (!snapshotVersion)
223 return false;
224 if (!processedVersion || *snapshotVersion > *processedVersion)
225 return true;
226 return false;
227}
228
229using namespace std::chrono_literals;
230
231QmlLintSuggestions::VersionToDiagnose
232QmlLintSuggestions::chooseVersionToDiagnoseHelper(const QByteArray &url)
233{
234 const std::chrono::milliseconds maxInvalidTime = 400ms;
235 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUrl(url);
236
237 LastLintUpdate &lastUpdate = m_lastUpdate[url];
238
239 // ignore updates when already processed
240 if (lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
241 qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc";
242 return NoDocumentAvailable{};
243 }
244
245 // try out a valid version, if there is one
246 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version))
247 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
248
249 // try out an invalid version, if there is one
250 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version)) {
251 if (auto since = lastUpdate.invalidUpdatesSince) {
252 // did we wait enough to get a valid document?
253 if (std::chrono::steady_clock::now() - *since > maxInvalidTime) {
254 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
255 }
256 } else {
257 // first time hitting the invalid document:
258 lastUpdate.invalidUpdatesSince = std::chrono::steady_clock::now();
259 }
260
261 // wait some time for extra keystrokes before diagnose
262 return TryAgainLater{ maxInvalidTime };
263 }
264 return NoDocumentAvailable{};
265}
266
267QmlLintSuggestions::VersionToDiagnose
268QmlLintSuggestions::chooseVersionToDiagnose(const QByteArray &url)
269{
270 QMutexLocker l(&m_mutex);
271 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url);
272 if (auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
273 // update immediately, and do not keep track of sent version, thus in extreme cases sent
274 // updates could be out of sync
275 LastLintUpdate &lastUpdate = m_lastUpdate[url];
276 lastUpdate.version = versionedDocument->version;
277 lastUpdate.invalidUpdatesSince.reset();
278 }
279 return versionToDiagnose;
280}
281
283{
284 auto versionedDocument = chooseVersionToDiagnose(url);
285
286 std::visit(qOverloadedVisitor{
287 [](NoDocumentAvailable) {},
288 [this, &url](const TryAgainLater &tryAgainLater) {
289 QTimer::singleShot(tryAgainLater.time, Qt::VeryCoarseTimer, this,
290 [this, url]() { diagnose(url); });
291 },
292 [this, &url](const VersionedDocument &versionedDocument) {
293 diagnoseHelper(url, versionedDocument);
294 },
295
296 },
297 versionedDocument);
298}
299
300void QmlLintSuggestions::diagnoseHelper(const QByteArray &url,
301 const VersionedDocument &versionedDocument)
302{
303 auto [version, doc] = versionedDocument;
304
305 PublishDiagnosticsParams diagnosticParams;
306 diagnosticParams.uri = url;
307 diagnosticParams.version = version;
308
309 qCDebug(lintLog) << "has doc, do real lint";
310 QStringList imports = m_codeModel->buildPathsForFileUrl(url);
311 imports.append(m_codeModel->importPaths());
312 const QString filename = doc.canonicalFilePath();
313 // add source directory as last import as fallback in case there is no qmldir in the build
314 // folder this mimics qmllint behaviors
315 imports.append(QFileInfo(filename).dir().absolutePath());
316 // add m_server->clientInfo().rootUri & co?
317 bool silent = true;
318 const QString fileContents = doc.field(Fields::code).value().toString();
319 const QStringList qmltypesFiles;
320 const QStringList resourceFiles = resourceFilesFromBuildFolders(imports);
321
322 QList<QQmlJS::LoggerCategory> categories;
323
324 QQmlJSLinter linter(imports);
325
326 linter.lintFile(filename, &fileContents, silent, nullptr, imports, qmltypesFiles,
327 resourceFiles, categories);
328
329 // ### TODO: C++20 replace with bind_front
330 auto advancePositionPastLocation = [&fileContents](const QQmlJS::SourceLocation &location, Position &position)
331 {
333 };
334 auto messageToDiagnostic = [&advancePositionPastLocation,
335 versionedDocument](const Message &message) {
336 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
337 message);
338 };
339
340 QList<Diagnostic> diagnostics;
341 doc.iterateErrors(
342 [&diagnostics, &advancePositionPastLocation](const DomItem &, const ErrorMessage &msg) {
343 Diagnostic diagnostic;
344 diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level)));
345 // do something with msg.errorGroups ?
346 auto &location = msg.location;
347 Range &range = diagnostic.range;
348 range.start.line = location.startLine - 1;
349 range.start.character = location.startColumn - 1;
350 range.end = range.start;
351 advancePositionPastLocation(location, range.end);
352 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
353 diagnostic.source = "domParsing";
354 diagnostic.message = msg.message.toUtf8();
355 diagnostics.append(diagnostic);
356 return true;
357 },
358 true);
359
360 if (const QQmlJSLogger *logger = linter.logger()) {
361 qsizetype nDiagnostics = diagnostics.size();
362 for (const auto &messages : { logger->infos(), logger->warnings(), logger->errors() })
363 for (const Message &message : messages)
364 diagnostics.append(messageToDiagnostic(message));
365 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
366 diagnostics.append(createMissingBuildDirDiagnostic());
367 }
368
369 diagnosticParams.diagnostics = diagnostics;
370
371 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
372 qCDebug(lintLog) << "lint" << QString::fromUtf8(url) << "found"
373 << diagnosticParams.diagnostics.size() << "issues"
374 << QTypedJson::toJsonValue(diagnosticParams);
375}
376
377} // namespace QmlLsp
[custom type definition]
\inmodule QtCore
Definition qbytearray.h:57
void chop(qsizetype n)
Removes n bytes from the end of the byte array.
QByteArray & append(char c)
This is an overloaded member function, provided for convenience. It differs from the above function o...
\inmodule QtCore\reentrant
Definition qjsonarray.h:18
void append(const QJsonValue &value)
Inserts value at the end of the array.
\inmodule QtCore\reentrant
Definition qjsonobject.h:20
iterator insert(const QString &key, const QJsonValue &value)
Inserts a new item with the key key and a value of value.
\inmodule QtCore\reentrant
Definition qjsonvalue.h:25
Implements a server for the language server protocol.
QLanguageServerProtocol * protocol()
constexpr const char * data() const noexcept
constexpr qsizetype size() const noexcept
Definition qlist.h:75
\inmodule QtCore
Definition qmutex.h:313
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2960
Represents an error message connected to the dom.
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:6018
QString simplified() const &
Definition qstring.h:451
QString & append(QChar c)
Definition qstring.cpp:3252
QByteArray toUtf8() const &
Definition qstring.h:634
bool singleShot
whether the timer is a single-shot timer
Definition qtimer.h:22
std::optional< int > docVersion
std::optional< int > validDocVersion
QQmlJS::Dom::DomItem validDoc
OpenDocumentSnapshot snapshotByUrl(const QByteArray &url)
QStringList importPaths() const
QStringList buildPathsForFileUrl(const QByteArray &url)
void updatedSnapshot(const QByteArray &url)
void diagnose(const QByteArray &uri)
list append(new Employee("Blackpool", "Stephen"))
QStringList resourceFilesFromBuildFolders(const QStringList &buildFolders)
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 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 bool isSnapshotNew(std::optional< int > snapshotVersion, std::optional< int > processedVersion)
static DiagnosticSeverity severityFromMsgType(QtMsgType t)
@ VeryCoarseTimer
@ DirectConnection
static jboolean cut(JNIEnv *, jobject)
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char return DBusMessage return DBusMessage const char return DBusMessage dbus_bool_t return DBusMessage dbus_uint32_t return DBusMessage void
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char return DBusMessage return DBusMessage const char return DBusMessage dbus_bool_t return DBusMessage dbus_uint32_t return DBusMessage return DBusPendingCall DBusPendingCall return DBusPendingCall return dbus_int32_t return DBusServer * server
typedef QByteArray(EGLAPIENTRYP PFNQGSGETDISPLAYSPROC)()
QtMsgType
Definition qlogging.h:29
@ QtCriticalMsg
Definition qlogging.h:32
@ QtInfoMsg
Definition qlogging.h:34
@ QtWarningMsg
Definition qlogging.h:31
@ QtFatalMsg
Definition qlogging.h:33
@ QtDebugMsg
Definition qlogging.h:30
#define Q_LOGGING_CATEGORY(name,...)
#define qCDebug(category,...)
GLint location
GLuint GLuint end
GLenum GLuint GLenum GLsizei length
GLuint object
[3]
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLsizei range
GLuint GLsizei const GLchar * message
void ** params
GLenum GLenum GLsizei void GLsizei void * column
GLsizei GLenum * categories
GLdouble GLdouble t
Definition qopenglext.h:243
static QString absolutePath(const QString &path)
static qreal position(const QQuickItem *item, QQuickAnchors::Anchor anchorLine)
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
ptrdiff_t qsizetype
Definition qtypes.h:165
QUrl url("example.com")
[constructor-url-reference]
QString dir
[11]
QGraphicsWidget * textEdit