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
4#include "qqmllintsuggestions_p.h"
5
6#include <QtLanguageServer/private/qlanguageserverspec_p.h>
7#include <QtQmlCompiler/private/qqmljslinter_p.h>
8#include <QtQmlCompiler/private/qqmljslogger_p.h>
9#include <QtCore/qlibraryinfo.h>
10#include <QtCore/qtimer.h>
11#include <QtCore/qdebug.h>
12
13using namespace QLspSpecification;
14using namespace QQmlJS::Dom;
15using namespace Qt::StringLiterals;
16
17Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint")
18
19QT_BEGIN_NAMESPACE
20namespace QmlLsp {
21
22static DiagnosticSeverity severityFromMsgType(QtMsgType t)
23{
24 switch (t) {
25 case QtDebugMsg:
26 return DiagnosticSeverity::Hint;
27 case QtInfoMsg:
28 return DiagnosticSeverity::Information;
29 case QtWarningMsg:
30 return DiagnosticSeverity::Warning;
31 case QtCriticalMsg:
32 case QtFatalMsg:
33 break;
34 }
35 return DiagnosticSeverity::Error;
36}
37
38static void codeActionHandler(
39 const QByteArray &, const CodeActionParams &params,
40 LSPPartialResponse<std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>,
41 QList<std::variant<Command, CodeAction>>> &&response)
42{
43 QList<std::variant<Command, CodeAction>> responseData;
44
45 for (const Diagnostic &diagnostic : params.context.diagnostics) {
46 if (!diagnostic.data.has_value())
47 continue;
48
49 const auto &data = diagnostic.data.value();
50
51 int version = data[u"version"].toInt();
52 QJsonArray suggestions = data[u"suggestions"].toArray();
53
54 QList<TextDocumentEdit> edits;
55 QString message;
56 for (const QJsonValue &suggestion : suggestions) {
57 QString replacement = suggestion[u"replacement"].toString();
58 message += suggestion[u"message"].toString() + u"\n";
59
60 TextEdit textEdit;
61 textEdit.range = { .start: Position { .line: suggestion[u"lspBeginLine"].toInt(),
62 .character: suggestion[u"lspBeginCharacter"].toInt() },
63 .end: Position { .line: suggestion[u"lspEndLine"].toInt(),
64 .character: suggestion[u"lspEndCharacter"].toInt() } };
65 textEdit.newText = replacement.toUtf8();
66
67 TextDocumentEdit textDocEdit;
68 textDocEdit.textDocument = { params.textDocument, .version: version };
69 textDocEdit.edits.append(t: textEdit);
70
71 edits.append(t: textDocEdit);
72 }
73 message.chop(n: 1);
74 WorkspaceEdit edit;
75 edit.documentChanges = edits;
76
77 CodeAction action;
78 // VS Code and QtC ignore everything that is not a 'quickfix'.
79 action.kind = u"quickfix"_s.toUtf8();
80 action.edit = edit;
81 action.title = message.toUtf8();
82
83 responseData.append(t: action);
84 }
85
86 response.sendResponse(r: responseData);
87}
88
89void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
90{
91 protocol->registerCodeActionRequestHandler(handler: &codeActionHandler);
92}
93
94void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &,
95 QLspSpecification::InitializeResult &serverInfo)
96{
97 serverInfo.capabilities.codeActionProvider = true;
98}
99
100QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel)
101 : m_server(server), m_codeModel(codeModel)
102{
103 QObject::connect(sender: m_codeModel, signal: &QmlLsp::QQmlCodeModel::updatedSnapshot, context: this,
104 slot: &QmlLintSuggestions::diagnose, type: Qt::DirectConnection);
105}
106
107void QmlLintSuggestions::diagnose(const QByteArray &url)
108{
109 const int maxInvalidMsec = 4000;
110 qCDebug(lintLog) << "diagnose start";
111 QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUrl(url);
112 QList<Diagnostic> diagnostics;
113 std::optional<int> version;
114 DomItem doc;
115 {
116 QMutexLocker l(&m_mutex);
117 LastLintUpdate &lastUpdate = m_lastUpdate[url];
118 if (lastUpdate.version && *lastUpdate.version == version) {
119 qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc";
120 return;
121 }
122 if (snapshot.validDocVersion
123 && (!lastUpdate.version || *snapshot.validDocVersion > *lastUpdate.version)) {
124 doc = snapshot.validDoc;
125 version = snapshot.validDocVersion;
126 } else if (snapshot.docVersion
127 && (!lastUpdate.version || *snapshot.docVersion > *lastUpdate.version)) {
128 if (!lastUpdate.version || !snapshot.validDocVersion
129 || (lastUpdate.invalidUpdatesSince
130 && lastUpdate.invalidUpdatesSince->msecsTo(QDateTime::currentDateTime())
131 > maxInvalidMsec)) {
132 doc = snapshot.doc;
133 version = snapshot.docVersion;
134 } else if (!lastUpdate.invalidUpdatesSince) {
135 lastUpdate.invalidUpdatesSince = QDateTime::currentDateTime();
136 QTimer::singleShot(interval: maxInvalidMsec, timerType: Qt::VeryCoarseTimer, receiver: this,
137 slot: [this, url]() { diagnose(url); });
138 }
139 }
140 if (doc) {
141 // update immediately, and do not keep track of sent version, thus in extreme cases sent
142 // updates could be out of sync
143 lastUpdate.version = version;
144 lastUpdate.invalidUpdatesSince.reset();
145 }
146 }
147 QString fileContents;
148 if (doc) {
149 qCDebug(lintLog) << "has doc, do real lint";
150 QStringList imports = m_codeModel->buildPathsForFileUrl(url);
151 imports.append(t: QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath));
152 // add m_server->clientInfo().rootUri & co?
153 bool silent = true;
154 QString filename = doc.canonicalFilePath();
155 fileContents = doc.field(name: Fields::code).value().toString();
156 QStringList qmltypesFiles;
157 QStringList resourceFiles;
158 QList<QQmlJS::LoggerCategory> categories;
159
160 QQmlJSLinter linter(imports);
161
162 linter.lintFile(filename, fileContents: &fileContents, silent, json: nullptr, qmlImportPaths: imports, qmldirFiles: qmltypesFiles,
163 resourceFiles, categories);
164 auto addLength = [&fileContents](Position &position, int startOffset, int length) {
165 int i = startOffset;
166 int iEnd = i + length;
167 if (iEnd > int(fileContents.size()))
168 iEnd = fileContents.size();
169 while (i < iEnd) {
170 if (fileContents.at(i) == u'\n') {
171 ++position.line;
172 position.character = 0;
173 if (i + 1 < iEnd && fileContents.at(i) == u'\r')
174 ++i;
175 } else {
176 ++position.character;
177 }
178 ++i;
179 }
180 };
181
182 auto messageToDiagnostic = [&addLength, &version](const Message &message) {
183 Diagnostic diagnostic;
184 diagnostic.severity = severityFromMsgType(t: message.type);
185 Range &range = diagnostic.range;
186 Position &position = range.start;
187
188 QQmlJS::SourceLocation srcLoc = message.loc;
189
190 position.line = srcLoc.isValid() ? srcLoc.startLine - 1 : 0;
191 position.character = srcLoc.isValid() ? srcLoc.startColumn - 1 : 0;
192 range.end = position;
193 addLength(range.end, srcLoc.isValid() ? message.loc.offset : 0, srcLoc.isValid() ? message.loc.length : 0);
194 diagnostic.message = message.message.toUtf8();
195 diagnostic.source = QByteArray("qmllint");
196
197 auto suggestion = message.fixSuggestion;
198 if (suggestion.has_value()) {
199 // We need to interject the information about where the fix suggestions end
200 // here since we don't have access to the textDocument to calculate it later.
201 QJsonArray fixedSuggestions;
202 const QQmlJS::SourceLocation cut = suggestion->location();
203
204 const int line = cut.isValid() ? cut.startLine - 1 : 0;
205 const int column = cut.isValid() ? cut.startColumn - 1 : 0;
206
207 QJsonObject object;
208 object.insert(key: "lspBeginLine"_L1, value: line);
209 object.insert(key: "lspBeginCharacter"_L1, value: column);
210
211 Position end = { .line: line, .character: column };
212
213 addLength(end, srcLoc.isValid() ? cut.offset : 0,
214 srcLoc.isValid() ? cut.length : 0);
215 object.insert(key: "lspEndLine"_L1, value: end.line);
216 object.insert(key: "lspEndCharacter"_L1, value: end.character);
217
218 object.insert(key: "message"_L1, value: suggestion->fixDescription());
219 object.insert(key: "replacement"_L1, value: suggestion->replacement());
220
221 fixedSuggestions << object;
222 QJsonObject data;
223 data[u"suggestions"] = fixedSuggestions;
224
225 Q_ASSERT(version.has_value());
226 data[u"version"] = version.value();
227
228 diagnostic.data = data;
229 }
230 return diagnostic;
231 };
232 doc.iterateErrors(
233 visitor: [&diagnostics, &addLength](DomItem, ErrorMessage msg) {
234 Diagnostic diagnostic;
235 diagnostic.severity = severityFromMsgType(t: QtMsgType(int(msg.level)));
236 // do something with msg.errorGroups ?
237 auto &location = msg.location;
238 Range &range = diagnostic.range;
239 range.start.line = location.startLine - 1;
240 range.start.character = location.startColumn - 1;
241 range.end = range.start;
242 addLength(range.end, location.offset, location.length);
243 diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
244 diagnostic.source = "domParsing";
245 diagnostic.message = msg.message.toUtf8();
246 diagnostics.append(t: diagnostic);
247 return true;
248 },
249 iterate: true);
250
251 if (const QQmlJSLogger *logger = linter.logger()) {
252 qsizetype nDiagnostics = diagnostics.size();
253 for (const auto &messages : { logger->infos(), logger->warnings(), logger->errors() }) {
254 for (const Message &message : messages) {
255 diagnostics.append(t: messageToDiagnostic(message));
256 }
257 }
258 if (diagnostics.size() != nDiagnostics && imports.size() == 1) {
259 Diagnostic diagnostic;
260 diagnostic.severity = DiagnosticSeverity::Warning;
261 Range &range = diagnostic.range;
262 Position &position = range.start;
263 position.line = 0;
264 position.character = 0;
265 Position &positionEnd = range.end;
266 positionEnd.line = 1;
267 diagnostic.message =
268 "qmlls could not find a build directory, without a build directory "
269 "containing a current build there could be spurious warnings, you might "
270 "want to pass the --build-dir <buildDir> option to qmlls, or set the "
271 "environment variable QMLLS_BUILD_DIRS.";
272 diagnostic.source = QByteArray("qmllint");
273 diagnostics.append(t: diagnostic);
274 }
275 }
276 }
277 PublishDiagnosticsParams diagnosticParams;
278 diagnosticParams.uri = url;
279 diagnosticParams.diagnostics = diagnostics;
280 diagnosticParams.version = version;
281
282 m_server->protocol()->notifyPublishDiagnostics(params: diagnosticParams);
283 qCDebug(lintLog) << "lint" << QString::fromUtf8(ba: url) << "found"
284 << diagnosticParams.diagnostics.size() << "issues"
285 << QTypedJson::toJsonValue(params: diagnosticParams);
286}
287
288} // namespace QmlLsp
289QT_END_NAMESPACE
290

source code of qtdeclarative/src/qmlls/qqmllintsuggestions.cpp