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