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