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>
24Q_STATIC_LOGGING_CATEGORY(lintLog,
"qt.languageserver.lint")
26using namespace QLspSpecification;
27using namespace QQmlJS::Dom;
28using namespace Qt::StringLiterals;
36 return DiagnosticSeverity::Hint;
38 return DiagnosticSeverity::Information;
40 return DiagnosticSeverity::Warning;
45 return DiagnosticSeverity::Error;
49 QmlLsp::QQmlCodeModelManager *codeModelManager)
50 : m_server(server), m_codeModelManager(codeModelManager)
52 QObject::connect(m_codeModelManager, &QmlLsp::QQmlCodeModelManager::updatedSnapshot,
this,
53 &QmlLintSuggestions::diagnose, Qt::DirectConnection);
57 const int startOffset = location.offset;
58 const int length = location.length;
60 int iEnd = i + length;
61 if (iEnd >
int(fileContents.size()))
62 iEnd = fileContents.size();
64 if (fileContents.at(i) == u'\n') {
66 position.character = 0;
67 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
78 Diagnostic diagnostic;
79 diagnostic.severity = DiagnosticSeverity::Warning;
80 Range &range = diagnostic.range;
81 Position &position = range.start;
83 position.character = 0;
84 Position &positionEnd = range.end;
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 "
91 diagnostic.source = QByteArray(
"qmllint");
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);
110 if (!message.fixSuggestion.has_value())
113 const QQmlJSFixSuggestion &suggestion = message.fixSuggestion.value();
114 QJsonArray documentEditsJson;
115 const auto &documentEdits = suggestion.documentEdits();
116 for (
const auto &documentEdit : documentEdits) {
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;
123 Position end = { line, column };
125 advancePositionPastLocation(cut, end);
127 QJsonObject documentEditJson{
128 {
"filename"_L1, QUrl::fromLocalFile(documentEdit.m_filename).toString() },
129 {
"replacement"_L1, documentEdit.m_replacement }
131 addLocationToJsonObject(documentEdit.m_location, documentEditJson);
132 documentEditsJson.append(documentEditJson);
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();
141 QJsonArray fixSuggestionsJson;
142 fixSuggestionsJson.append(fixSuggestionJson);
143 return fixSuggestionsJson;
147 std::optional<
int> version,
const Message &message)
149 Diagnostic diagnostic;
150 diagnostic.severity = severityFromMsgType(message.type);
151 Range &range = diagnostic.range;
152 Position &position = range.start;
154 QQmlJS::SourceLocation srcLoc = message.loc;
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);
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())
168 diagnostic.message = u"%1 [%2]"_s.arg(message.message, message.id.toString()).toUtf8();
171 diagnostic.source = QByteArray(
"qmllint");
173 auto suggestion = message.fixSuggestion;
174 if (!suggestion.has_value())
178 data[u"suggestions"] = suggestionToJson(advancePositionPastLocation, message);
179 Q_ASSERT(version.has_value());
180 data[u"version"] = version.value();
182 diagnostic.data = data;
187 QmlLsp::UpdatePolicy policy)
189 if (!snapshotVersion)
191 if (policy == ForceUpdate)
193 if (!processedVersion || *snapshotVersion > *processedVersion)
198using namespace std::chrono_literals;
200QmlLintSuggestions::VersionToDiagnose
202 QmlLsp::UpdatePolicy policy)
204 const std::chrono::milliseconds maxInvalidTime = 400ms;
205 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModelManager->snapshotByUrl(url);
210 if (policy != ForceUpdate && lastUpdate.version && *lastUpdate.version == snapshot.docVersion) {
211 qCDebug(lspServerLog) <<
"skipped update of " << url <<
"unchanged valid doc";
212 return NoDocumentAvailable{};
216 if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version, policy))
217 return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc };
220 if (isSnapshotNew(snapshot.docVersion, lastUpdate.version, policy)) {
221 if (
auto since = lastUpdate.invalidUpdatesSince) {
223 if (
std::chrono::steady_clock::now() - *since > maxInvalidTime) {
224 return VersionedDocument{ snapshot.docVersion, snapshot.doc };
228 lastUpdate.invalidUpdatesSince =
std::chrono::steady_clock::now();
232 return TryAgainLater{ maxInvalidTime };
234 return NoDocumentAvailable{};
237QmlLintSuggestions::VersionToDiagnose
238QmlLintSuggestions::chooseVersionToDiagnose(
const QByteArray &url, QmlLsp::UpdatePolicy policy)
240 QMutexLocker l(&m_mutex);
241 auto versionToDiagnose = chooseVersionToDiagnoseHelper(url, policy);
242 if (
auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) {
246 lastUpdate.version = versionedDocument->version;
247 lastUpdate.invalidUpdatesSince.reset();
249 return versionToDiagnose;
254 auto versionedDocument = chooseVersionToDiagnose(url, policy);
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); });
262 [
this, &url](
const VersionedDocument &versionedDocument) {
263 diagnoseHelper(url, versionedDocument);
271 const VersionedDocument &versionedDocument)
273 auto [version, doc] = versionedDocument;
275 PublishDiagnosticsParams diagnosticParams;
276 diagnosticParams.uri = url;
277 diagnosticParams.version = version;
279 qCDebug(lintLog) <<
"has doc, do real lint";
280 QStringList imports = m_codeModelManager->importPathsForUrl(url);
281 const QString filename = doc.canonicalFilePath();
284 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 QQmlLintSettings settings;
300 if (settings.search(filename).isValid()) {
301 QQmlJS::LoggingUtils::updateLogSeverities(categories, settings,
nullptr);
305 QQmlJSLinter::LintOptions lintOptions;
306 lintOptions.setFlag(QQmlJSLinter::Silent);
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);
315 auto advancePositionPastLocation = [&fileContents](
const QQmlJS::SourceLocation &location, Position &position)
317 advancePositionPastLocation_helper(fileContents, location, position);
319 auto messageToDiagnostic = [&advancePositionPastLocation,
320 versionedDocument](
const Message &message) {
321 return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version,
325 QList<Diagnostic> diagnostics;
327 [&diagnostics, &advancePositionPastLocation](
const DomItem &,
const ErrorMessage &msg) {
328 Diagnostic diagnostic;
329 diagnostic.severity = severityFromMsgType(QtMsgType(
int(msg.level)));
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);
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));
353 Message modified {message};
354 modified.message.append(u" Did you build your project?");
356 diagnostics.append(messageToDiagnostic(modified));
358 if (diagnostics.size() != nDiagnostics && imports.size() == 1)
359 diagnostics.append(createMissingBuildDirDiagnostic());
362 diagnosticParams.diagnostics = diagnostics;
364 m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
365 qCDebug(lintLog) <<
"lint" << QString::fromUtf8(url) <<
"found"
366 << diagnosticParams.diagnostics.size() <<
"issues"
367 << 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)