Qt 6.x
The Qt SDK
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 <QtCore/qlibraryinfo.h>
10#include <QtCore/qtimer.h>
11#include <QtCore/qdebug.h>
12
13using namespace QLspSpecification;
14using namespace QQmlJS::Dom;
15using namespace Qt::StringLiterals;
16
17Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint")
18
20namespace QmlLsp {
21
22static DiagnosticSeverity severityFromMsgType(QtMsgType t)
23{
24 switch (t) {
25 case QtDebugMsg:
26 return DiagnosticSeverity::Hint;
27 case QtInfoMsg:
28 return DiagnosticSeverity::Information;
29 case QtWarningMsg:
30 return DiagnosticSeverity::Warning;
31 case QtCriticalMsg:
32 case QtFatalMsg:
33 break;
34 }
35 return DiagnosticSeverity::Error;
36}
37
39 const QByteArray &, const CodeActionParams &params,
40 LSPPartialResponse<std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>,
41 QList<std::variant<Command, CodeAction>>> &&response)
42{
44
45 for (const Diagnostic &diagnostic : params.context.diagnostics) {
46 if (!diagnostic.data.has_value())
47 continue;
48
49 const auto &data = diagnostic.data.value();
50
51 int version = data[u"version"].toInt();
52 QJsonArray suggestions = data[u"suggestions"].toArray();
53
56 for (const QJsonValue &suggestion : suggestions) {
57 QString replacement = suggestion[u"replacement"].toString();
58 message += suggestion[u"message"].toString() + u"\n";
59
61 textEdit.range = { Position { suggestion[u"lspBeginLine"].toInt(),
62 suggestion[u"lspBeginCharacter"].toInt() },
63 Position { suggestion[u"lspEndLine"].toInt(),
64 suggestion[u"lspEndCharacter"].toInt() } };
65 textEdit.newText = replacement.toUtf8();
66
67 TextDocumentEdit textDocEdit;
68 textDocEdit.textDocument = { params.textDocument, version };
69 textDocEdit.edits.append(textEdit);
70
71 edits.append(textDocEdit);
72 }
73 message.chop(1);
74 WorkspaceEdit edit;
75 edit.documentChanges = edits;
76
77 CodeAction action;
78 action.kind = u"refactor.rewrite"_s.toUtf8();
79 action.edit = edit;
80 action.title = message.toUtf8();
81
82 responseData.append(action);
83 }
84
85 response.sendResponse(responseData);
86}
87
88void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
89{
90 protocol->registerCodeActionRequestHandler(&codeActionHandler);
91}
92
93void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &,
94 QLspSpecification::InitializeResult &serverInfo)
95{
96 serverInfo.capabilities.codeActionProvider = true;
97}
98
99QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel)
100 : m_server(server), m_codeModel(codeModel)
101{
104}
105
107{
108 const int maxInvalidMsec = 4000;
109 qCDebug(lintLog) << "diagnose start";
110 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUrl(url);
111 QList<Diagnostic> diagnostics;
112 std::optional<int> version;
113 DomItem doc;
114 {
115 QMutexLocker l(&m_mutex);
116 LastLintUpdate &lastUpdate = m_lastUpdate[url];
117 if (lastUpdate.version && *lastUpdate.version == version) {
118 qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc";
119 return;
120 }
121 if (snapshot.validDocVersion
122 && (!lastUpdate.version || *snapshot.validDocVersion > *lastUpdate.version)) {
123 doc = snapshot.validDoc;
124 version = snapshot.validDocVersion;
125 } else if (snapshot.docVersion
126 && (!lastUpdate.version || *snapshot.docVersion > *lastUpdate.version)) {
127 if (!lastUpdate.version || !snapshot.validDocVersion
128 || (lastUpdate.invalidUpdatesSince
129 && lastUpdate.invalidUpdatesSince->msecsTo(QDateTime::currentDateTime())
130 > maxInvalidMsec)) {
131 doc = snapshot.doc;
132 version = snapshot.docVersion;
133 } else if (!lastUpdate.invalidUpdatesSince) {
135 QTimer::singleShot(maxInvalidMsec, Qt::VeryCoarseTimer, this,
136 [this, url]() { diagnose(url); });
137 }
138 }
139 if (doc) {
140 // update immediately, and do not keep track of sent version, thus in extreme cases sent
141 // updates could be out of sync
142 lastUpdate.version = version;
143 lastUpdate.invalidUpdatesSince.reset();
144 }
145 }
146 QString fileContents;
147 if (doc) {
148 qCDebug(lintLog) << "has doc, do real lint";
149 QStringList imports = m_codeModel->buildPathsForFileUrl(url);
151 // add m_server->clientInfo().rootUri & co?
152 bool silent = true;
153 QString filename = doc.canonicalFilePath();
154 fileContents = doc.field(Fields::code).value().toString();
155 QStringList qmltypesFiles;
156 QStringList resourceFiles;
158
159 QQmlJSLinter linter(imports);
160
161 linter.lintFile(filename, &fileContents, silent, nullptr, imports, qmltypesFiles,
162 resourceFiles, categories);
163 auto addLength = [&fileContents](Position &position, int startOffset, int length) {
164 int i = startOffset;
165 int iEnd = i + length;
166 if (iEnd > int(fileContents.size()))
167 iEnd = fileContents.size();
168 while (i < iEnd) {
169 if (fileContents.at(i) == u'\n') {
170 ++position.line;
171 position.character = 0;
172 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
173 ++i;
174 } else {
175 ++position.character;
176 }
177 ++i;
178 }
179 };
180
181 auto messageToDiagnostic = [&addLength, &version](const Message &message) {
182 Diagnostic diagnostic;
183 diagnostic.severity = severityFromMsgType(message.type);
184 Range &range = diagnostic.range;
185 Position &position = range.start;
186
187 QQmlJS::SourceLocation srcLoc = message.loc;
188
189 position.line = srcLoc.isValid() ? srcLoc.startLine - 1 : 0;
190 position.character = srcLoc.isValid() ? srcLoc.startColumn - 1 : 0;
191 range.end = position;
192 addLength(range.end, srcLoc.isValid() ? message.loc.offset : 0, srcLoc.isValid() ? message.loc.length : 0);
193 diagnostic.message = message.message.toUtf8();
194 diagnostic.source = QByteArray("qmllint");
195
196 auto suggestion = message.fixSuggestion;
197 if (suggestion.has_value()) {
198 // We need to interject the information about where the fix suggestions end
199 // here since we don't have access to the textDocument to calculate it later.
200 QJsonArray fixedSuggestions;
201 const QQmlJS::SourceLocation cut = suggestion->location();
202
203 const int line = cut.isValid() ? cut.startLine - 1 : 0;
204 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
205
207 object.insert("lspBeginLine"_L1, line);
208 object.insert("lspBeginCharacter"_L1, column);
209
210 Position end = { line, column };
211
212 addLength(end, srcLoc.isValid() ? cut.offset : 0,
213 srcLoc.isValid() ? cut.length : 0);
214 object.insert("lspEndLine"_L1, end.line);
215 object.insert("lspEndCharacter"_L1, end.character);
216
217 object.insert("message"_L1, suggestion->fixDescription());
218 object.insert("replacement"_L1, suggestion->replacement());
219
220 fixedSuggestions << object;
222 data[u"suggestions"] = fixedSuggestions;
223
224 Q_ASSERT(version.has_value());
225 data[u"version"] = version.value();
226
227 diagnostic.data = data;
228 }
229 return diagnostic;
230 };
231 doc.iterateErrors(
232 [&diagnostics, &addLength](DomItem, ErrorMessage msg) {
233 Diagnostic diagnostic;
234 diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level)));
235 // do something with msg.errorGroups ?
236 auto &location = msg.location;
237 Range &range = diagnostic.range;
238 range.start.line = location.startLine - 1;
239 range.start.character = location.startColumn - 1;
240 range.end = range.start;
241 addLength(range.end, location.offset, location.length);
242 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
243 diagnostic.source = "domParsing";
244 diagnostic.message = msg.message.toUtf8();
245 diagnostics.append(diagnostic);
246 return true;
247 },
248 true);
249
250 if (const QQmlJSLogger *logger = linter.logger()) {
251 qsizetype nDiagnostics = diagnostics.size();
252 for (const auto &messages : { logger->infos(), logger->warnings(), logger->errors() }) {
253 for (const Message &message : messages) {
254 diagnostics.append(messageToDiagnostic(message));
255 }
256 }
257 if (diagnostics.size() != nDiagnostics && imports.size() == 1) {
258 Diagnostic diagnostic;
259 diagnostic.severity = DiagnosticSeverity::Warning;
260 Range &range = diagnostic.range;
261 Position &position = range.start;
262 position.line = 0;
263 position.character = 0;
264 Position &positionEnd = range.end;
265 positionEnd.line = 1;
266 diagnostic.message =
267 "qmlls could not find a build directory, without a build directory "
268 "containing a current build there could be spurious warnings, you might "
269 "want to pass the --build-dir <buildDir> option to qmlls, or set the "
270 "environment variable QMLLS_BUILD_DIRS.";
271 diagnostic.source = QByteArray("qmllint");
272 diagnostics.append(diagnostic);
273 }
274 }
275 }
276 PublishDiagnosticsParams diagnosticParams;
277 diagnosticParams.uri = url;
278 diagnosticParams.diagnostics = diagnostics;
279 diagnosticParams.version = version;
280
281 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
282 qCDebug(lintLog) << "lint" << QString::fromUtf8(url) << "found"
283 << diagnosticParams.diagnostics.size() << "issues"
284 << QTypedJson::toJsonValue(diagnosticParams);
285}
286
287} // namespace QmlLsp
\inmodule QtCore
Definition qbytearray.h:57
QByteArray & append(char c)
This is an overloaded member function, provided for convenience. It differs from the above function o...
QString toString(const QString &defaultValue={}) const
Returns the string value stored in this QCborValue, if it is of the string type.
static QDateTime currentDateTime()
This is an overloaded member function, provided for convenience. It differs from the above function o...
\inmodule QtCore\reentrant
Definition qjsonarray.h:18
\inmodule QtCore\reentrant
Definition qjsonobject.h:20
\inmodule QtCore\reentrant
Definition qjsonvalue.h:24
Implements a server for the language server protocol.
QLanguageServerProtocol * protocol()
constexpr const char * data() const noexcept
constexpr qsizetype size() const noexcept
static QString path(LibraryPath p)
Definition qlist.h:74
qsizetype size() const noexcept
Definition qlist.h:386
pointer data()
Definition qlist.h:414
void append(parameter_type t)
Definition qlist.h:441
\inmodule QtCore
Definition qmutex.h:317
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2823
const QQmlJSLogger * logger() const
LintResult lintFile(const QString &filename, const QString *fileContents, const bool silent, QJsonArray *json, const QStringList &qmlImportPaths, const QStringList &qmldirFiles, const QStringList &resourceFiles, const QList< QQmlJS::LoggerCategory > &categories)
bool iterateErrors(function_ref< bool(DomItem source, ErrorMessage msg)> visitor, bool iterate, Path inPath=Path())
QCborValue value()
QString canonicalFilePath()
DomItem field(QStringView name)
Represents an error message connected to the dom.
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
qsizetype size() const
Returns the number of characters in this string.
Definition qstring.h:182
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:5857
const QChar at(qsizetype i) const
Returns the character at the given index position in the string.
Definition qstring.h:1079
QString & insert(qsizetype i, QChar c)
Definition qstring.cpp:3110
QByteArray toUtf8() const &
Definition qstring.h:563
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 buildPathsForFileUrl(const QByteArray &url)
void updatedSnapshot(const QByteArray &url)
void diagnose(const QByteArray &uri)
Combined button and popup list for selecting options.
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 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 return DBusPendingCall DBusPendingCall return DBusPendingCall return dbus_int32_t return DBusServer * server
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]
GLsizei range
GLuint GLsizei const GLchar * message
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
void ** params
GLenum GLenum GLsizei void GLsizei void * column
GLsizei GLenum * categories
GLdouble GLdouble t
Definition qopenglext.h:243
static qreal position(const QQuickItem *item, QQuickAnchors::Anchor anchorLine)
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
ptrdiff_t qsizetype
Definition qtypes.h:70
QUrl url("example.com")
[constructor-url-reference]
QGraphicsWidget * textEdit
std::optional< QDateTime > invalidUpdatesSince
std::optional< int > version