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
4#include "qqmllintsuggestions_p.h"
5
6#include <QtLanguageServer/private/qlanguageserverspec_p.h>
7#include <QtQmlCompiler/private/qqmljslinter_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 <QtCore/qdebug.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qfileinfo.h>
15#include <QtCore/qlibraryinfo.h>
16#include <QtCore/qtimer.h>
17#include <QtCore/qxpfunctional.h>
18#include <chrono>
19
20using namespace QLspSpecification;
21using namespace QQmlJS::Dom;
22using namespace Qt::StringLiterals;
23
24Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint")
25
26QT_BEGIN_NAMESPACE
27namespace QmlLsp {
28
29static DiagnosticSeverity severityFromMsgType(QtMsgType t)
30{
31 switch (t) {
32 case QtDebugMsg:
33 return DiagnosticSeverity::Hint;
34 case QtInfoMsg:
35 return DiagnosticSeverity::Information;
36 case QtWarningMsg:
37 return DiagnosticSeverity::Warning;
38 case QtCriticalMsg:
39 case QtFatalMsg:
40 break;
41 }
42 return DiagnosticSeverity::Error;
43}
44
45static void codeActionHandler(
46 const QByteArray &, const CodeActionParams &params,
47 LSPPartialResponse<std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>,
48 QList<std::variant<Command, CodeAction>>> &&response)
49{
50 QList<std::variant<Command, CodeAction>> responseData;
51
52 for (const Diagnostic &diagnostic : params.context.diagnostics) {
53 if (!diagnostic.data.has_value())
54 continue;
55
56 const auto &data = diagnostic.data.value();
57
58 int version = data[u"version"].toInt();
59 QJsonArray suggestions = data[u"suggestions"].toArray();
60
61 QList<WorkspaceEdit::DocumentChange> edits;
62 QString message;
63 for (const QJsonValue &suggestion : suggestions) {
64 QString replacement = suggestion[u"replacement"].toString();
65 message += suggestion[u"message"].toString() + u"\n";
66
67 TextEdit textEdit;
68 textEdit.range = { .start: Position { .line: suggestion[u"lspBeginLine"].toInt(),
69 .character: suggestion[u"lspBeginCharacter"].toInt() },
70 .end: Position { .line: suggestion[u"lspEndLine"].toInt(),
71 .character: suggestion[u"lspEndCharacter"].toInt() } };
72 textEdit.newText = replacement.toUtf8();
73
74 TextDocumentEdit textDocEdit;
75 textDocEdit.textDocument = { params.textDocument, .version: version };
76 textDocEdit.edits.append(t: textEdit);
77
78 edits.append(t: textDocEdit);
79 }
80 message.chop(n: 1);
81 WorkspaceEdit edit;
82 edit.documentChanges = edits;
83
84 CodeAction action;
85 // VS Code and QtC ignore everything that is not a 'quickfix'.
86 action.kind = u"quickfix"_s.toUtf8();
87 action.edit = edit;
88 action.title = message.toUtf8();
89
90 responseData.append(t: action);
91 }
92
93 response.sendResponse(r: responseData);
94}
95
96void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
97{
98 protocol->registerCodeActionRequestHandler(handler: &codeActionHandler);
99}
100
101void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &,
102 QLspSpecification::InitializeResult &serverInfo)
103{
104 serverInfo.capabilities.codeActionProvider = true;
105}
106
107QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel)
108 : m_server(server), m_codeModel(codeModel)
109{
110 QObject::connect(sender: m_codeModel, signal: &QmlLsp::QQmlCodeModel::updatedSnapshot, context: this,
111 slot: &QmlLintSuggestions::diagnose, type: Qt::DirectConnection);
112}
113
114static void advancePositionPastLocation_helper(const QString &fileContents, const QQmlJS::SourceLocation &location, Position &position) {
115 const int startOffset = location.offset;
116 const int length = location.length;
117 int i = startOffset;
118 int iEnd = i + length;
119 if (iEnd > int(fileContents.size()))
120 iEnd = fileContents.size();
121 while (i < iEnd) {
122 if (fileContents.at(i) == u'\n') {
123 ++position.line;
124 position.character = 0;
125 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
126 ++i;
127 } else {
128 ++position.character;
129 }
130 ++i;
131 }
132};
133
134static Diagnostic createMissingBuildDirDiagnostic()
135{
136 Diagnostic diagnostic;
137 diagnostic.severity = DiagnosticSeverity::Warning;
138 Range &range = diagnostic.range;
139 Position &position = range.start;
140 position.line = 0;
141 position.character = 0;
142 Position &positionEnd = range.end;
143 positionEnd.line = 1;
144 diagnostic.message =
145 "qmlls couldn't find a build directory. Pass the \"--build-dir <buildDir>\" option to "
146 "qmlls, set the environment variable \"QMLLS_BUILD_DIRS\", or create a .qmlls.ini "
147 "configuration file with a \"buildDir\" value in your project's source folder to avoid "
148 "spurious warnings";
149 diagnostic.source = QByteArray("qmllint");
150 return diagnostic;
151}
152
153using AdvanceFunc = qxp::function_ref<void(const QQmlJS::SourceLocation &, Position &)>;
154static Diagnostic messageToDiagnostic_helper(AdvanceFunc advancePositionPastLocation,
155 std::optional<int> version, const Message &message)
156{
157 Diagnostic diagnostic;
158 diagnostic.severity = severityFromMsgType(t: message.type);
159 Range &range = diagnostic.range;
160 Position &position = range.start;
161
162 QQmlJS::SourceLocation srcLoc = message.loc;
163
164 if (srcLoc.isValid()) {
165 position.line = srcLoc.startLine - 1;
166 position.character = srcLoc.startColumn - 1;
167 range.end = position;
168 advancePositionPastLocation(message.loc, range.end);
169 }
170
171 if (message.fixSuggestion && !message.fixSuggestion->fixDescription().isEmpty()) {
172 diagnostic.message = u"%1: %2 [%3]"_s.arg(args: message.message, args: message.fixSuggestion->fixDescription(), args: message.id.toString())
173 .simplified()
174 .toUtf8();
175 } else {
176 diagnostic.message = u"%1 [%2]"_s.arg(args: message.message, args: message.id.toString()).toUtf8();
177 }
178
179 diagnostic.source = QByteArray("qmllint");
180
181 auto suggestion = message.fixSuggestion;
182 if (!suggestion.has_value())
183 return diagnostic;
184
185 // We need to interject the information about where the fix suggestions end
186 // here since we don't have access to the textDocument to calculate it later.
187 const QQmlJS::SourceLocation cut = suggestion->location();
188
189 const int line = cut.isValid() ? cut.startLine - 1 : 0;
190 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
191
192 QJsonObject object;
193 object.insert(key: "lspBeginLine"_L1, value: line);
194 object.insert(key: "lspBeginCharacter"_L1, value: column);
195
196 Position end = { .line: line, .character: column };
197
198 if (srcLoc.isValid())
199 advancePositionPastLocation(cut, end);
200 object.insert(key: "lspEndLine"_L1, value: end.line);
201 object.insert(key: "lspEndCharacter"_L1, value: end.character);
202
203 object.insert(key: "message"_L1, value: suggestion->fixDescription());
204 object.insert(key: "replacement"_L1, value: suggestion->replacement());
205
206 QJsonArray fixedSuggestions;
207 fixedSuggestions.append(value: object);
208 QJsonObject data;
209 data[u"suggestions"] = fixedSuggestions;
210
211 Q_ASSERT(version.has_value());
212 data[u"version"] = version.value();
213
214 diagnostic.data = data;
215
216 return diagnostic;
217};
218
219static bool isSnapshotNew(std::optional<int> snapshotVersion, std::optional<int> processedVersion)
220{
221 if (!snapshotVersion)
222 return false;
223 if (!processedVersion || *snapshotVersion > *processedVersion)
224 return true;
225 return false;
226}
227
228using namespace std::chrono_literals;
229
230QmlLintSuggestions::VersionToDiagnose
231QmlLintSuggestions::chooseVersionToDiagnoseHelper(const QByteArray &url)
232{
233 const std::chrono::milliseconds maxInvalidTime = 400ms;
234 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUrl(url);
235
236 LastLintUpdate &lastUpdate = m_lastUpdate[url];
237
238 // ignore updates when already processed
239 if (lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
240 qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc";
241 return NoDocumentAvailable{};
242 }
243
244 // try out a valid version, if there is one
245 if (isSnapshotNew(snapshotVersion: snapshot.validDocVersion, processedVersion: lastUpdate.version))
246 return VersionedDocument{ .version: snapshot.validDocVersion, .item: snapshot.validDoc };
247
248 // try out an invalid version, if there is one
249 if (isSnapshotNew(snapshotVersion: snapshot.docVersion, processedVersion: lastUpdate.version)) {
250 if (auto since = lastUpdate.invalidUpdatesSince) {
251 // did we wait enough to get a valid document?
252 if (std::chrono::steady_clock::now() - *since > maxInvalidTime) {
253 return VersionedDocument{ .version: snapshot.docVersion, .item: snapshot.doc };
254 }
255 } else {
256 // first time hitting the invalid document:
257 lastUpdate.invalidUpdatesSince = std::chrono::steady_clock::now();
258 }
259
260 // wait some time for extra keystrokes before diagnose
261 return TryAgainLater{ .time: maxInvalidTime };
262 }
263 return NoDocumentAvailable{};
264}
265
266QmlLintSuggestions::VersionToDiagnose
267QmlLintSuggestions::chooseVersionToDiagnose(const QByteArray &url)
268{
269 QMutexLocker l(&m_mutex);
270 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url);
271 if (auto versionedDocument = std::get_if<VersionedDocument>(ptr: &versionToDiagnose)) {
272 // update immediately, and do not keep track of sent version, thus in extreme cases sent
273 // updates could be out of sync
274 LastLintUpdate &lastUpdate = m_lastUpdate[url];
275 lastUpdate.version = versionedDocument->version;
276 lastUpdate.invalidUpdatesSince.reset();
277 }
278 return versionToDiagnose;
279}
280
281void QmlLintSuggestions::diagnose(const QByteArray &url)
282{
283 auto versionedDocument = chooseVersionToDiagnose(url);
284
285 std::visit(visitor: qOverloadedVisitor{
286 [](NoDocumentAvailable) {},
287 [this, &url](const TryAgainLater &tryAgainLater) {
288 QTimer::singleShot(interval: tryAgainLater.time, timerType: Qt::VeryCoarseTimer, receiver: this,
289 slot: [this, url]() { diagnose(url); });
290 },
291 [this, &url](const VersionedDocument &versionedDocument) {
292 diagnoseHelper(uri: url, document: versionedDocument);
293 },
294
295 },
296 variants&: versionedDocument);
297}
298
299void QmlLintSuggestions::diagnoseHelper(const QByteArray &url,
300 const VersionedDocument &versionedDocument)
301{
302 auto [version, doc] = versionedDocument;
303
304 PublishDiagnosticsParams diagnosticParams;
305 diagnosticParams.uri = url;
306 diagnosticParams.version = version;
307
308 qCDebug(lintLog) << "has doc, do real lint";
309 QStringList imports = m_codeModel->buildPathsForFileUrl(url);
310 imports.append(other: m_codeModel->importPaths());
311 const QString filename = doc.canonicalFilePath();
312 // add source directory as last import as fallback in case there is no qmldir in the build
313 // folder this mimics qmllint behaviors
314 imports.append(t: QFileInfo(filename).dir().absolutePath());
315 // add m_server->clientInfo().rootUri & co?
316 bool silent = true;
317 const QString fileContents = doc.field(name: Fields::code).value().toString();
318 const QStringList qmltypesFiles;
319 const QStringList resourceFiles = QQmlJSUtils::resourceFilesFromBuildFolders(buildFolders: imports);
320
321 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::defaultCategories();
322
323 QQmlJSLinter linter(imports);
324
325 for (const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
326 for (const QQmlJS::LoggerCategory &category : plugin.categories())
327 categories.append(t: category);
328 }
329
330 QQmlToolingSettings settings(QLatin1String("qmllint"));
331 if (settings.search(path: filename)) {
332 QQmlJS::LoggingUtils::updateLogLevels(categories, settings, parser: nullptr);
333 }
334
335 linter.lintFile(filename, fileContents: &fileContents, silent, json: nullptr, qmlImportPaths: imports, qmldirFiles: qmltypesFiles,
336 resourceFiles, categories);
337
338 // ### TODO: C++20 replace with bind_front
339 auto advancePositionPastLocation = [&fileContents](const QQmlJS::SourceLocation &location, Position &position)
340 {
341 advancePositionPastLocation_helper(fileContents, location, position);
342 };
343 auto messageToDiagnostic = [&advancePositionPastLocation,
344 versionedDocument](const Message &message) {
345 return messageToDiagnostic_helper(advancePositionPastLocation, version: versionedDocument.version,
346 message);
347 };
348
349 QList<Diagnostic> diagnostics;
350 doc.iterateErrors(
351 visitor: [&diagnostics, &advancePositionPastLocation](const DomItem &, const ErrorMessage &msg) {
352 Diagnostic diagnostic;
353 diagnostic.severity = severityFromMsgType(t: QtMsgType(int(msg.level)));
354 // do something with msg.errorGroups ?
355 auto &location = msg.location;
356 Range &range = diagnostic.range;
357 range.start.line = location.startLine - 1;
358 range.start.character = location.startColumn - 1;
359 range.end = range.start;
360 advancePositionPastLocation(location, range.end);
361 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
362 diagnostic.source = "domParsing";
363 diagnostic.message = msg.message.toUtf8();
364 diagnostics.append(t: diagnostic);
365 return true;
366 },
367 iterate: true);
368
369 if (const QQmlJSLogger *logger = linter.logger()) {
370 qsizetype nDiagnostics = diagnostics.size();
371 for (const auto &messages : { logger->infos(), logger->warnings(), logger->errors() })
372 for (const Message &message : messages)
373 diagnostics.append(t: messageToDiagnostic(message));
374 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
375 diagnostics.append(t: createMissingBuildDirDiagnostic());
376 }
377
378 diagnosticParams.diagnostics = diagnostics;
379
380 m_server->protocol()->notifyPublishDiagnostics(params: diagnosticParams);
381 qCDebug(lintLog) << "lint" << QString::fromUtf8(ba: url) << "found"
382 << diagnosticParams.diagnostics.size() << "issues"
383 << QTypedJson::toJsonValue(params: diagnosticParams);
384}
385
386} // namespace QmlLsp
387QT_END_NAMESPACE
388

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtdeclarative/src/qmlls/qqmllintsuggestions.cpp