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 QmlLsp::QQmlCodeModelManager *codeModelManager)
49 : m_server(server), m_codeModelManager(codeModelManager)
51 QObject::connect(m_codeModelManager, &QmlLsp::QQmlCodeModelManager::updatedSnapshot,
this,
52 &QmlLintSuggestions::diagnose, Qt::DirectConnection);
56 const int startOffset = location.offset;
57 const int length = location.length;
59 int iEnd = i + length;
60 if (iEnd >
int(fileContents.size()))
61 iEnd = fileContents.size();
63 if (fileContents.at(i) == u'\n') {
65 position.character = 0;
66 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
77 Diagnostic diagnostic;
78 diagnostic.severity = DiagnosticSeverity::Warning;
79 Range &range = diagnostic.range;
80 Position &position = range.start;
82 position.character = 0;
83 Position &positionEnd = range.end;
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 "
90 diagnostic.source = QByteArray(
"qmllint");
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);
109 if (!message.fixSuggestion.has_value())
112 const QQmlJSFixSuggestion &suggestion = message.fixSuggestion.value();
113 QJsonArray documentEditsJson;
114 const auto &documentEdits = suggestion.documentEdits();
115 for (
const auto &documentEdit : documentEdits) {
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;
122 Position end = { line, column };
124 advancePositionPastLocation(cut, end);
126 QJsonObject documentEditJson{
127 {
"filename"_L1, QUrl::fromLocalFile(documentEdit.m_filename).toString() },
128 {
"replacement"_L1, documentEdit.m_replacement }
130 addLocationToJsonObject(documentEdit.m_location, documentEditJson);
131 documentEditsJson.append(documentEditJson);
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();
140 QJsonArray fixSuggestionsJson;
141 fixSuggestionsJson.append(fixSuggestionJson);
142 return fixSuggestionsJson;
146 std::optional<
int> version,
const Message &message)
148 Diagnostic diagnostic;
149 diagnostic.severity = severityFromMsgType(message.type);
150 Range &range = diagnostic.range;
151 Position &position = range.start;
153 QQmlJS::SourceLocation srcLoc = message.loc;
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);
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())
167 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
170 diagnostic.source = QByteArray(
"qmllint");
172 auto suggestion = message.fixSuggestion;
173 if (!suggestion.has_value())
177 data[u"suggestions"] = suggestionToJson(advancePositionPastLocation, message);
178 Q_ASSERT(version.has_value());
179 data[u"version"] = version.value();
181 diagnostic.data = data;
186 QmlLsp::UpdatePolicy policy)
188 if (!snapshotVersion)
190 if (policy == ForceUpdate)
192 if (!processedVersion || *snapshotVersion > *processedVersion)
197using namespace std::chrono_literals;
199QmlLintSuggestions::VersionToDiagnose
201 QmlLsp::UpdatePolicy policy)
203 const std::chrono::milliseconds maxInvalidTime = 400ms;
204 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModelManager->snapshotByUrl(url);
209 if (policy != ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
210 qCDebug(lspServerLog) <<
"skipped update of " << url <<
"unchanged valid doc";
211 return NoDocumentAvailable{};
215 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
216 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
219 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
220 if (
auto since = lastUpdate.invalidUpdatesSince) {
222 if (
std::chrono::steady_clock::now() - *since > maxInvalidTime) {
223 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
227 lastUpdate.invalidUpdatesSince =
std::chrono::steady_clock::now();
231 return TryAgainLater{ maxInvalidTime };
233 return NoDocumentAvailable{};
236QmlLintSuggestions::VersionToDiagnose
237QmlLintSuggestions::chooseVersionToDiagnose(
const QByteArray &url, QmlLsp::UpdatePolicy policy)
239 QMutexLocker l(&m_mutex);
240 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
241 if (
auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
245 lastUpdate.version = versionedDocument->version;
246 lastUpdate.invalidUpdatesSince.reset();
248 return versionToDiagnose;
253 auto versionedDocument = chooseVersionToDiagnose(url, policy);
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); });
261 [
this, &url](
const VersionedDocument &versionedDocument) {
262 diagnoseHelper(url, versionedDocument);
270 const VersionedDocument &versionedDocument)
272 auto [version, doc] = versionedDocument;
274 PublishDiagnosticsParams diagnosticParams;
275 diagnosticParams.uri = url;
276 diagnosticParams.version = version;
278 qCDebug(lintLog) <<
"has doc, do real lint";
279 QStringList imports = m_codeModelManager->importPathsForUrl(url);
280 const QString filename = doc.canonicalFilePath();
283 imports.append(QFileInfo(filename).dir().absolutePath());
286 const QString fileContents = doc.field(Fields::code).value().toString();
287 const QStringList qmltypesFiles;
288 const QStringList resourceFiles = m_codeModelManager->resourceFilesForFileUrl(url);
290 QList<QQmlJS::LoggerCategory> categories = QQmlJSLogger::builtinCategories();
292 QQmlJSLinter linter(imports);
294 for (
const QQmlJSLinter::Plugin &plugin : linter.plugins()) {
295 for (
const QQmlJS::LoggerCategory &category : plugin.categories())
296 categories.append(category);
299 QQmlToolingSettings settings(QLatin1String(
"qmllint"));
300 if (settings.search(filename).isValid()) {
301 QQmlJS::LoggingUtils::updateLogSeverities(categories, settings,
nullptr);
305 linter.lintFile(filename, &fileContents, silent,
nullptr, imports, qmltypesFiles, resourceFiles,
309 auto advancePositionPastLocation = [&fileContents](
const QQmlJS::SourceLocation &location, Position &position)
311 advancePositionPastLocation_helper(fileContents, location, position);
313 auto messageToDiagnostic = [&advancePositionPastLocation,
314 versionedDocument](
const Message &message) {
315 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
319 QList<Diagnostic> diagnostics;
321 [&diagnostics, &advancePositionPastLocation](
const DomItem &,
const ErrorMessage &msg) {
322 Diagnostic diagnostic;
323 diagnostic.severity = severityFromMsgType(QtMsgType(
int(msg.level)));
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);
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));
347 Message modified {message};
348 modified.message.append(u" Did you build your project?");
350 diagnostics.append(messageToDiagnostic(modified));
352 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
353 diagnostics.append(createMissingBuildDirDiagnostic());
356 diagnosticParams.diagnostics = diagnostics;
358 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
359 qCDebug(lintLog) <<
"lint" << QString::fromUtf8(url) <<
"found"
360 << diagnosticParams.diagnostics.size() <<
"issues"
361 << QTypedJson::toJsonValue(diagnosticParams);
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)