File indexing completed on 2025-01-05 05:20:14
0001 /** 0002 * SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com> 0003 * SPDX-License-Identifier: LGPL-2.0-or-later 0004 */ 0005 #include "clippy.h" 0006 #include "kateproject.h" 0007 0008 #include <KLocalizedString> 0009 #include <KTextEditor/Document> 0010 #include <KTextEditor/MainWindow> 0011 #include <KTextEditor/View> 0012 0013 #include <QDir> 0014 #include <QJsonArray> 0015 #include <QJsonDocument> 0016 #include <QRegularExpression> 0017 #include <QStringLiteral> 0018 0019 Clippy::Clippy(QObject *parent) 0020 : KateProjectCodeAnalysisTool(parent) 0021 { 0022 } 0023 0024 QString Clippy::name() const 0025 { 0026 return i18n("Clippy (Rust)"); 0027 } 0028 0029 QString Clippy::description() const 0030 { 0031 return i18n("Clippy is a static analysis tool for Rust code."); 0032 } 0033 0034 QString Clippy::fileExtensions() const 0035 { 0036 return QStringLiteral("rs"); 0037 } 0038 0039 QStringList Clippy::filter(const QStringList &files) const 0040 { 0041 // js/ts files 0042 return files.filter( 0043 QRegularExpression(QStringLiteral("\\.(") + fileExtensions().replace(QStringLiteral("+"), QStringLiteral("\\+")) + QStringLiteral(")$"))); 0044 } 0045 0046 QString Clippy::path() const 0047 { 0048 return QStringLiteral("cargo"); 0049 } 0050 0051 static QString getNearestManifestPath(KTextEditor::MainWindow *mainWindow) 0052 { 0053 if (auto v = mainWindow->activeView()) { 0054 QString path = v->document()->url().toLocalFile(); 0055 if (!path.isEmpty()) { 0056 QDir d(path); 0057 while (d.cdUp()) { 0058 if (d.exists(QStringLiteral("Cargo.toml"))) { 0059 return d.absoluteFilePath(QStringLiteral("Cargo.toml")); 0060 } 0061 } 0062 } 0063 } 0064 return {}; 0065 } 0066 0067 QStringList Clippy::arguments() 0068 { 0069 if (!m_project) { 0070 return {}; 0071 } 0072 0073 QStringList args; 0074 QString manifestPath = getNearestManifestPath(m_mainWindow); 0075 0076 args << QStringLiteral("clippy"); 0077 if (!manifestPath.isEmpty()) { 0078 args << QStringLiteral("--manifest-path"); 0079 args << manifestPath; 0080 } 0081 args << QStringLiteral("--message-format"); 0082 args << QStringLiteral("json"); 0083 args << QStringLiteral("--quiet"); 0084 args << QStringLiteral("--no-deps"); 0085 args << QStringLiteral("--offline"); 0086 0087 setActualFilesCount(m_project->files().size()); 0088 0089 return args; 0090 } 0091 0092 QString Clippy::notInstalledMessage() const 0093 { 0094 return i18n("Please install 'cargo'."); 0095 } 0096 0097 std::pair<QString, KTextEditor::Range> sourceLocationFromSpans(const QJsonArray &spans) 0098 { 0099 int lineStart = -1; 0100 int lineEnd = -1; 0101 int colStart = -1; 0102 int colEnd = -1; 0103 QString file; 0104 for (const auto span : spans) { 0105 auto obj = span.toObject(); 0106 lineStart = obj.value(u"line_start").toInt() - 1; 0107 lineEnd = obj.value(u"line_end").toInt() - 1; 0108 colStart = obj.value(u"column_start").toInt() - 1; 0109 colEnd = obj.value(u"column_end").toInt() - 1; 0110 file = obj.value(u"file_name").toString(); 0111 break; 0112 } 0113 return {file, KTextEditor::Range(lineStart, colStart, lineEnd, colEnd)}; 0114 } 0115 0116 FileDiagnostics Clippy::parseLine(const QString &line) const 0117 { 0118 QJsonParseError err; 0119 QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &err); 0120 if (err.error != QJsonParseError::NoError) { 0121 qDebug() << "ERROR:" << err.errorString(); 0122 printf("%s\n", line.toUtf8().data()); 0123 return {}; 0124 } 0125 0126 auto topLevelObj = doc.object(); 0127 auto reason = topLevelObj.value(QLatin1String("reason")); 0128 if (reason.isNull() || reason.isUndefined() || reason.toString() != QStringLiteral("compiler-message")) { 0129 return {}; 0130 } 0131 0132 QDir manifest_path = topLevelObj.value(u"manifest_path").toString(); 0133 manifest_path.cdUp(); 0134 if (!manifest_path.exists()) { 0135 qDebug() << "invalid uri"; 0136 return {}; 0137 } 0138 0139 const auto message = topLevelObj.value(QLatin1String("message")).toObject(); 0140 if (message.isEmpty()) { 0141 qDebug() << "invalid message"; 0142 return {}; 0143 } 0144 0145 QString code = message.value(u"code").toObject().value(u"code").toString(); 0146 QString level = message.value(u"level").toString().toLower(); 0147 QString diagMessage = message.value(u"message").toString(); 0148 const auto spans = message.value(u"spans").toArray(); 0149 const auto [fileName, range] = sourceLocationFromSpans(spans); 0150 0151 Diagnostic diag; 0152 diag.severity = [&level]() { 0153 if (level == u"warning") { 0154 return DiagnosticSeverity::Warning; 0155 } 0156 if (level == u"error") { 0157 return DiagnosticSeverity::Error; 0158 } 0159 return DiagnosticSeverity::Warning; 0160 }(); 0161 diag.code = code; 0162 diag.message = diagMessage; 0163 diag.range = range; 0164 if (!diag.range.isValid()) { 0165 return {}; 0166 } 0167 0168 QUrl uri = QUrl::fromLocalFile(manifest_path.absoluteFilePath(fileName)); 0169 if (!uri.isValid()) { 0170 return {}; 0171 } 0172 0173 const auto children = message.value(QLatin1String("children")).toArray(); 0174 for (const auto &child : children) { 0175 auto obj = child.toObject(); 0176 QString msg = obj.value(u"message").toString(); 0177 if (msg.isEmpty()) { 0178 continue; 0179 } 0180 auto spans = obj.value(u"spans").toArray(); 0181 auto [_, range] = sourceLocationFromSpans(spans); 0182 QUrl u = uri; 0183 if (!spans.isEmpty()) { 0184 auto rep = spans.first().toObject().value(u"suggested_replacement").toString(); 0185 if (!rep.isEmpty()) { 0186 msg += QLatin1String(": `"); 0187 msg += spans.first().toObject().value(u"suggested_replacement").toString(); 0188 msg += QLatin1String("`"); 0189 } 0190 } 0191 0192 if (!range.isValid()) { 0193 range = diag.range; 0194 } 0195 diag.relatedInformation.push_back(DiagnosticRelatedInformation{{u, range}, msg}); 0196 } 0197 0198 // qDebug() << uri << diag.message << diag.range; 0199 return {uri, {diag}}; 0200 } 0201 0202 QString Clippy::stdinMessages() 0203 { 0204 return QString(); 0205 }