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>
23Q_STATIC_LOGGING_CATEGORY(lintLog,
"qt.languageserver.lint")
25using namespace QLspSpecification;
26using namespace QQmlJS::Dom;
27using namespace Qt::StringLiterals;
35 return DiagnosticSeverity::Hint;
37 return DiagnosticSeverity::Information;
39 return DiagnosticSeverity::Warning;
44 return DiagnosticSeverity::Error;
48 const QByteArray &,
const CodeActionParams ¶ms,
52 QList<std::variant<Command, CodeAction>> responseData;
54 for (
const Diagnostic &diagnostic : params.context.diagnostics) {
55 if (!diagnostic.data.has_value())
58 const auto &data = diagnostic.data.value();
60 int version = data[u"version"].toInt();
61 QJsonArray suggestions = data[u"suggestions"].toArray();
63 QList<WorkspaceEdit::DocumentChange> edits;
65 for (
const QJsonValue &suggestion : std::as_const(suggestions)) {
66 message += suggestion[u"message"_s].toString() + u'\n';
67 const auto &documentEdits = suggestion[u"documentEdits"_s].toArray();
68 TextDocumentEdit textDocEdit;
69 for (
const auto &documentEdit : documentEdits) {
71 textEdit.range = { Position{ documentEdit[u"lspBeginLine"].toInt(),
72 documentEdit[u"lspBeginCharacter"].toInt() },
73 Position{ documentEdit[u"lspEndLine"].toInt(),
74 documentEdit[u"lspEndCharacter"].toInt() } };
75 textEdit.newText = documentEdit[u"replacement"_s].toString().toUtf8();
76 QString filename = documentEdit[u"filename"_s].toString();
77 textDocEdit.textDocument = { { filename.toUtf8() }, version };
78 textDocEdit.edits.append(textEdit);
80 edits.append(textDocEdit);
84 edit.documentChanges = edits;
88 action.kind = u"quickfix"_s.toUtf8();
90 action.title = message.toUtf8();
92 responseData.append(action);
95 response.sendResponse(responseData);
100 protocol->registerCodeActionRequestHandler(&codeActionHandler);
105 caps.codeActionProvider =
true;
109 QmlLsp::QQmlCodeModelManager *codeModelManager)
110 : m_server(server), m_codeModelManager(codeModelManager)
112 QObject::connect(m_codeModelManager, &QmlLsp::QQmlCodeModelManager::updatedSnapshot,
this,
113 &QmlLintSuggestions::diagnose, Qt::DirectConnection);
117 const int startOffset = location.offset;
118 const int length = location.length;
120 int iEnd = i + length;
121 if (iEnd >
int(fileContents.size()))
122 iEnd = fileContents.size();
124 if (fileContents.at(i) == u'\n') {
126 position.character = 0;
127 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
130 ++position.character;
138 Diagnostic diagnostic;
139 diagnostic.severity = DiagnosticSeverity::Warning;
140 Range &range = diagnostic.range;
141 Position &position = range.start;
143 position.character = 0;
144 Position &positionEnd = range.end;
145 positionEnd.line = 1;
147 "qmlls couldn't find a build directory. Pass the \"--build-dir <buildDir>\" option to "
148 "qmlls, set the environment variable \"QMLLS_BUILD_DIRS\", or create a .qmlls.ini "
149 "configuration file with a \"buildDir\" value in your project's source folder to avoid "
151 diagnostic.source = QByteArray(
"qmllint");
158 const auto addLocationToJsonObject = [&](
const auto &location, QJsonObject &object) {
159 const int line = location.isValid() ? location.startLine - 1 : 0;
160 const int column = location.isValid() ? location.startColumn - 1 : 0;
161 Position end = { line, column };
162 if (location.isValid())
163 advancePositionPastLocation(location, end);
164 object[
"lspBeginLine"_L1] = line;
165 object[
"lspBeginCharacter"_L1] = column;
166 object[
"lspEndLine"_L1] = end.line;
167 object[
"lspEndCharacter"_L1] = end.character;
170 if (!message.fixSuggestion.has_value())
173 const QQmlJSFixSuggestion &suggestion = message.fixSuggestion.value();
174 QJsonArray documentEditsJson;
175 const auto &documentEdits = suggestion.documentEdits();
176 for (
const auto &documentEdit : documentEdits) {
179 const QQmlJS::SourceLocation cut = suggestion.location();
180 const int line = cut.isValid() ? cut.startLine - 1 : 0;
181 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
183 Position end = { line, column };
185 advancePositionPastLocation(cut, end);
187 QJsonObject documentEditJson{
188 {
"filename"_L1, QUrl::fromLocalFile(documentEdit.m_filename).toString() },
189 {
"replacement"_L1, documentEdit.m_replacement }
191 addLocationToJsonObject(documentEdit.m_location, documentEditJson);
192 documentEditsJson.append(documentEditJson);
195 QJsonObject fixSuggestionJson;
196 fixSuggestionJson[
"message"_L1] = message.fixSuggestion->description();
197 addLocationToJsonObject(message.fixSuggestion->location(), fixSuggestionJson);
198 fixSuggestionJson[
"documentEdits"_L1] = documentEditsJson;
199 fixSuggestionJson[
"autoApplicable"_L1] = message.fixSuggestion->isAutoApplicable();
201 QJsonArray fixSuggestionsJson;
202 fixSuggestionsJson.append(fixSuggestionJson);
203 return fixSuggestionsJson;
207 std::optional<
int> version,
const Message &message)
209 Diagnostic diagnostic;
210 diagnostic.severity = severityFromMsgType(message.type);
211 Range &range = diagnostic.range;
212 Position &position = range.start;
214 QQmlJS::SourceLocation srcLoc = message.loc;
216 if (srcLoc.isValid()) {
217 position.line = srcLoc.startLine - 1;
218 position.character = srcLoc.startColumn - 1;
219 range.end = position;
220 advancePositionPastLocation(message.loc, range.end);
223 if (message.fixSuggestion && !message.fixSuggestion->description().isEmpty()) {
224 diagnostic.message = u"%1: %2 [%3]"_s.arg(message.message, message.fixSuggestion->description(), message.id.toString())
228 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
231 diagnostic.source = QByteArray(
"qmllint");
233 auto suggestion = message.fixSuggestion;
234 if (!suggestion.has_value())
238 data[u"suggestions"] = suggestionToJson(advancePositionPastLocation, message);
239 Q_ASSERT(version.has_value());
240 data[u"version"] = version.value();
242 diagnostic.data = data;
247 QmlLsp::UpdatePolicy policy)
249 if (!snapshotVersion)
251 if (policy == ForceUpdate)
253 if (!processedVersion || *snapshotVersion > *processedVersion)
258using namespace std::chrono_literals;
260QmlLintSuggestions::VersionToDiagnose
262 QmlLsp::UpdatePolicy policy)
264 const std::chrono::milliseconds maxInvalidTime = 400ms;
265 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModelManager->snapshotByUrl(url);
270 if (policy != ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
271 qCDebug(lspServerLog) <<
"skipped update of " << url <<
"unchanged valid doc";
272 return NoDocumentAvailable{};
276 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
277 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
280 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
281 if (
auto since = lastUpdate.invalidUpdatesSince) {
283 if (
std::chrono::steady_clock::now() - *since > maxInvalidTime) {
284 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
288 lastUpdate.invalidUpdatesSince =
std::chrono::steady_clock::now();
292 return TryAgainLater{ maxInvalidTime };
294 return NoDocumentAvailable{};
297QmlLintSuggestions::VersionToDiagnose
298QmlLintSuggestions::chooseVersionToDiagnose(
const QByteArray &url, QmlLsp::UpdatePolicy policy)
300 QMutexLocker l(&m_mutex);
301 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
302 if (
auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
306 lastUpdate.version = versionedDocument->version;
307 lastUpdate.invalidUpdatesSince.reset();
309 return versionToDiagnose;
314 auto versionedDocument = chooseVersionToDiagnose(url, policy);
316 std::visit(qOverloadedVisitor{
317 [](NoDocumentAvailable) {},
318 [
this, &url, &policy](
const TryAgainLater &tryAgainLater) {
319 QTimer::singleShot(tryAgainLater.time, Qt::VeryCoarseTimer,
this,
320 [
this, url, policy]() { diagnose(url, policy); });
322 [
this, &url](
const VersionedDocument &versionedDocument) {
323 diagnoseHelper(url, versionedDocument);
331 const VersionedDocument &versionedDocument)
333 auto [version, doc] = versionedDocument;
335 PublishDiagnosticsParams diagnosticParams;
336 diagnosticParams.uri = url;
337 diagnosticParams.version = version;
339 qCDebug(lintLog) <<
"has doc, do real lint";
340 QStringList imports = m_codeModelManager->importPathsForUrl(url);
341 const QString filename = doc.canonicalFilePath();
344 imports.append(QFileInfo(filename).dir().absolutePath());
347 const QString fileContents = doc.field(Fields::code).value().toString();
348 const QStringList qmltypesFiles;
349 const QStringList resourceFiles = m_codeModelManager->resourceFilesForFileUrl(url);
351 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::builtinCategories();
353 QQmlJSLinter linter(imports);
355 for (
const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
356 for (
const QQmlJS::LoggerCategory &category : plugin.categories())
357 categories.append(category);
360 QQmlToolingSettings settings(QLatin1String(
"qmllint"));
361 if (settings.search(filename).isValid()) {
362 QQmlJS::LoggingUtils::updateLogSeverities(categories, settings,
nullptr);
366 linter.lintFile(filename, &fileContents, silent,
nullptr, imports, qmltypesFiles, resourceFiles,
370 auto advancePositionPastLocation = [&fileContents](
const QQmlJS::SourceLocation &location, Position &position)
372 advancePositionPastLocation_helper(fileContents, location, position);
374 auto messageToDiagnostic = [&advancePositionPastLocation,
375 versionedDocument](
const Message &message) {
376 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
380 QList<Diagnostic> diagnostics;
382 [&diagnostics, &advancePositionPastLocation](
const DomItem &,
const ErrorMessage &msg) {
383 Diagnostic diagnostic;
384 diagnostic.severity = severityFromMsgType(QtMsgType(
int(msg.level)));
386 auto &location = msg.location;
387 Range &range = diagnostic.range;
388 range.start.line = location.startLine - 1;
389 range.start.character = location.startColumn - 1;
390 range.end = range.start;
391 advancePositionPastLocation(location, range.end);
392 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
393 diagnostic.source =
"domParsing";
394 diagnostic.message = msg.message.toUtf8();
395 diagnostics.append(diagnostic);
400 if (
const QQmlJSLogger *logger = linter.logger()) {
401 qsizetype nDiagnostics = diagnostics.size();
402 logger->iterateAllMessages([&](
const Message &message) {
403 if (!message.message.contains(u"Failed to import")) {
404 diagnostics.append(messageToDiagnostic(message));
408 Message modified {message};
409 modified.message.append(u" Did you build your project?");
411 diagnostics.append(messageToDiagnostic(modified));
413 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
414 diagnostics.append(createMissingBuildDirDiagnostic());
417 diagnosticParams.diagnostics = diagnostics;
419 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
420 qCDebug(lintLog) <<
"lint" << QString::fromUtf8(url) <<
"found"
421 << diagnosticParams.diagnostics.size() <<
"issues"
422 << QTypedJson::toJsonValue(diagnosticParams);
void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override
void setupCapabilities(QLspSpecification::ServerCapabilities &caps) override
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 codeActionHandler(const QByteArray &, const CodeActionParams ¶ms, 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)
QJsonArray suggestionToJson(AdvanceFunc advancePositionPastLocation, const Message &message)
static DiagnosticSeverity severityFromMsgType(QtMsgType t)