7#include <QtLanguageServer/private/qlanguageserverspec_p.h>
8#include <private/qqmljslinter_p.h>
9#include <QtQmlCompiler/private/qqmljslogger_p.h>
10#include <QtQmlCompiler/private/qqmljsutils_p.h>
11#include <QtQmlDom/private/qqmldom_utils_p.h>
12#include <QtQmlDom/private/qqmldomtop_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 : suggestions) {
66 QString replacement = suggestion[u"replacement"].toString();
67 message += suggestion[u"message"].toString() + u"\n";
70 textEdit.range = { Position { suggestion[u"lspBeginLine"].toInt(),
71 suggestion[u"lspBeginCharacter"].toInt() },
72 Position { suggestion[u"lspEndLine"].toInt(),
73 suggestion[u"lspEndCharacter"].toInt() } };
74 textEdit.newText = replacement.toUtf8();
76 TextDocumentEdit textDocEdit;
77 textDocEdit.textDocument = { params.textDocument, 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");
157 std::optional<
int> version,
const Message &message)
159 Diagnostic diagnostic;
160 diagnostic.severity = severityFromMsgType(message.type);
161 Range &range = diagnostic.range;
162 Position &position = range.start;
164 QQmlJS::SourceLocation srcLoc = message.loc;
166 if (srcLoc.isValid()) {
167 position.line = srcLoc.startLine - 1;
168 position.character = srcLoc.startColumn - 1;
169 range.end = position;
170 advancePositionPastLocation(message.loc, range.end);
173 if (message.fixSuggestion && !message.fixSuggestion->description().isEmpty()) {
174 diagnostic.message = u"%1: %2 [%3]"_s.arg(message.message, message.fixSuggestion->description(), message.id.toString())
178 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
181 diagnostic.source = QByteArray(
"qmllint");
183 auto suggestion = message.fixSuggestion;
184 if (!suggestion.has_value())
189 const QQmlJS::SourceLocation cut = suggestion->location();
191 const int line = cut.isValid() ? cut.startLine - 1 : 0;
192 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
195 object.insert(
"lspBeginLine"_L1, line);
196 object.insert(
"lspBeginCharacter"_L1, column);
198 Position end = { line, column };
200 if (srcLoc.isValid())
201 advancePositionPastLocation(cut, end);
202 object.insert(
"lspEndLine"_L1, end.line);
203 object.insert(
"lspEndCharacter"_L1, end.character);
205 object.insert(
"message"_L1, suggestion->description());
206 object.insert(
"replacement"_L1, suggestion->replacement());
208 QJsonArray fixedSuggestions;
209 fixedSuggestions.append(object);
211 data[u"suggestions"] = fixedSuggestions;
213 Q_ASSERT(version.has_value());
214 data[u"version"] = version.value();
216 diagnostic.data = data;
222 QmlLsp::UpdatePolicy policy)
224 if (!snapshotVersion)
226 if (policy == ForceUpdate)
228 if (!processedVersion || *snapshotVersion > *processedVersion)
233using namespace std::chrono_literals;
235QmlLintSuggestions::VersionToDiagnose
237 QmlLsp::UpdatePolicy policy)
239 const std::chrono::milliseconds maxInvalidTime = 400ms;
240 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModelManager->snapshotByUrl(url);
245 if (policy != ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
246 qCDebug(lspServerLog) <<
"skipped update of " << url <<
"unchanged valid doc";
247 return NoDocumentAvailable{};
251 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
252 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
255 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
256 if (
auto since = lastUpdate.invalidUpdatesSince) {
258 if (
std::chrono::steady_clock::now() - *since > maxInvalidTime) {
259 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
263 lastUpdate.invalidUpdatesSince =
std::chrono::steady_clock::now();
267 return TryAgainLater{ maxInvalidTime };
269 return NoDocumentAvailable{};
272QmlLintSuggestions::VersionToDiagnose
273QmlLintSuggestions::chooseVersionToDiagnose(
const QByteArray &url, QmlLsp::UpdatePolicy policy)
275 QMutexLocker l(&m_mutex);
276 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
277 if (
auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
281 lastUpdate.version = versionedDocument->version;
282 lastUpdate.invalidUpdatesSince.reset();
284 return versionToDiagnose;
289 auto versionedDocument = chooseVersionToDiagnose(url, policy);
291 std::visit(qOverloadedVisitor{
292 [](NoDocumentAvailable) {},
293 [
this, &url, &policy](
const TryAgainLater &tryAgainLater) {
294 QTimer::singleShot(tryAgainLater.time, Qt::VeryCoarseTimer,
this,
295 [
this, url, policy]() { diagnose(url, policy); });
297 [
this, &url](
const VersionedDocument &versionedDocument) {
298 diagnoseHelper(url, versionedDocument);
306 const VersionedDocument &versionedDocument)
308 auto [version, doc] = versionedDocument;
310 PublishDiagnosticsParams diagnosticParams;
311 diagnosticParams.uri = url;
312 diagnosticParams.version = version;
314 qCDebug(lintLog) <<
"has doc, do real lint";
315 QStringList imports = m_codeModelManager->importPathsForUrl(url);
316 const QString filename = doc.canonicalFilePath();
319 imports.append(QFileInfo(filename).dir().absolutePath());
322 const QString fileContents = doc.field(Fields::code).value().toString();
323 const QStringList qmltypesFiles;
324 const QStringList resourceFiles = m_codeModelManager->resourceFilesForFileUrl(url);
326 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::builtinCategories();
328 QQmlJSLinter linter(imports);
330 for (
const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
331 for (
const QQmlJS::LoggerCategory &category : plugin.categories())
332 categories.append(category);
335 QQmlToolingSettings settings(QLatin1String(
"qmllint"));
336 if (settings.search(filename).isValid()) {
337 QQmlJS::LoggingUtils::updateLogSeverities(categories, settings,
nullptr);
341 linter.lintFile(filename, &fileContents, silent,
nullptr, imports, qmltypesFiles, resourceFiles,
345 auto advancePositionPastLocation = [&fileContents](
const QQmlJS::SourceLocation &location, Position &position)
347 advancePositionPastLocation_helper(fileContents, location, position);
349 auto messageToDiagnostic = [&advancePositionPastLocation,
350 versionedDocument](
const Message &message) {
351 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
355 QList<Diagnostic> diagnostics;
357 [&diagnostics, &advancePositionPastLocation](
const DomItem &,
const ErrorMessage &msg) {
358 Diagnostic diagnostic;
359 diagnostic.severity = severityFromMsgType(QtMsgType(
int(msg.level)));
361 auto &location = msg.location;
362 Range &range = diagnostic.range;
363 range.start.line = location.startLine - 1;
364 range.start.character = location.startColumn - 1;
365 range.end = range.start;
366 advancePositionPastLocation(location, range.end);
367 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
368 diagnostic.source =
"domParsing";
369 diagnostic.message = msg.message.toUtf8();
370 diagnostics.append(diagnostic);
375 if (
const QQmlJSLogger *logger = linter.logger()) {
376 qsizetype nDiagnostics = diagnostics.size();
377 logger->iterateAllMessages([&](
const Message &message) {
378 if (!message.message.contains(u"Failed to import")) {
379 diagnostics.append(messageToDiagnostic(message));
383 Message modified {message};
384 modified.message.append(u" Did you build your project?");
386 diagnostics.append(messageToDiagnostic(modified));
388 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
389 diagnostics.append(createMissingBuildDirDiagnostic());
392 diagnosticParams.diagnostics = diagnostics;
394 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
395 qCDebug(lintLog) <<
"lint" << QString::fromUtf8(url) <<
"found"
396 << diagnosticParams.diagnostics.size() <<
"issues"
397 << 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)
static DiagnosticSeverity severityFromMsgType(QtMsgType t)