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 }