File indexing completed on 2024-05-26 05:51:52
0001 /* 0002 SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com> 0003 SPDX-License-Identifier: LGPL-2.0-or-later 0004 */ 0005 #include "EslintPlugin.h" 0006 0007 #include <KLocalizedString> 0008 #include <KPluginFactory> 0009 #include <KSharedConfig> 0010 #include <KTextEditor/Application> 0011 #include <KTextEditor/Editor> 0012 #include <KTextEditor/View> 0013 #include <KXMLGUIFactory> 0014 0015 #include <QFileInfo> 0016 #include <QJsonArray> 0017 #include <QJsonDocument> 0018 #include <QJsonObject> 0019 #include <QPointer> 0020 0021 #include <hostprocess.h> 0022 #include <ktexteditor_utils.h> 0023 0024 K_PLUGIN_FACTORY_WITH_JSON(ESLintPluginFactory, "EslintPlugin.json", registerPlugin<ESLintPlugin>();) 0025 0026 ESLintPlugin::ESLintPlugin(QObject *parent, const QVariantList &) 0027 : KTextEditor::Plugin(parent) 0028 { 0029 } 0030 0031 QObject *ESLintPlugin::createView(KTextEditor::MainWindow *mainWindow) 0032 { 0033 return new ESLintPluginView(mainWindow); 0034 } 0035 0036 ESLintPluginView::ESLintPluginView(KTextEditor::MainWindow *mainWin) 0037 : QObject(mainWin) 0038 , m_mainWindow(mainWin) 0039 , m_provider(mainWin, this) 0040 { 0041 m_provider.setObjectName(QStringLiteral("ESLintDiagnosticProvider")); 0042 m_provider.name = i18n("ESLint"); 0043 0044 connect(mainWin, &KTextEditor::MainWindow::viewChanged, this, &ESLintPluginView::onActiveViewChanged); 0045 connect(&m_eslintProcess, &QProcess::readyReadStandardOutput, this, &ESLintPluginView::onReadyRead); 0046 connect(&m_eslintProcess, &QProcess::readyReadStandardError, this, &ESLintPluginView::onError); 0047 connect(&m_provider, &DiagnosticsProvider::requestFixes, this, &ESLintPluginView::onFixesRequested); 0048 0049 m_mainWindow->guiFactory()->addClient(this); 0050 } 0051 0052 ESLintPluginView::~ESLintPluginView() 0053 { 0054 disconnect(&m_eslintProcess, &QProcess::readyReadStandardOutput, this, &ESLintPluginView::onReadyRead); 0055 disconnect(&m_eslintProcess, &QProcess::readyReadStandardError, this, &ESLintPluginView::onError); 0056 if (m_eslintProcess.state() == QProcess::Running) { 0057 m_eslintProcess.kill(); 0058 m_eslintProcess.waitForFinished(); 0059 } 0060 disconnect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &ESLintPluginView::onActiveViewChanged); 0061 m_mainWindow->guiFactory()->removeClient(this); 0062 } 0063 0064 void ESLintPluginView::onActiveViewChanged(KTextEditor::View *v) 0065 { 0066 if (v && v->document() == m_activeDoc) { 0067 return; 0068 } 0069 0070 if (m_activeDoc) { 0071 disconnect(m_activeDoc, &KTextEditor::Document::documentSavedOrUploaded, this, &ESLintPluginView::onSaved); 0072 } 0073 0074 m_activeDoc = v ? v->document() : nullptr; 0075 0076 if (m_activeDoc) { 0077 connect(m_activeDoc, &KTextEditor::Document::documentSavedOrUploaded, this, &ESLintPluginView::onSaved, Qt::QueuedConnection); 0078 } 0079 } 0080 0081 static bool canLintDoc(KTextEditor::Document *d) 0082 { 0083 if (!d || !d->url().isLocalFile()) { 0084 return false; 0085 } 0086 const auto mode = d->highlightingMode().toLower(); 0087 return (mode == QStringLiteral("javascript") || mode == QStringLiteral("typescript") || mode == QStringLiteral("typescript react (tsx)") 0088 || mode == QStringLiteral("javascript react (jsx)")); 0089 } 0090 0091 void ESLintPluginView::onSaved(KTextEditor::Document *d) 0092 { 0093 m_diagsWithFix.clear(); 0094 if (!canLintDoc(d)) { 0095 return; 0096 } 0097 0098 if (m_eslintProcess.state() == QProcess::Running) { 0099 return; 0100 } 0101 0102 const auto name = safeExecutableName(QStringLiteral("npx")); 0103 if (name.isEmpty()) { 0104 // TODO: error 0105 return; 0106 } 0107 const QStringList args{QStringLiteral("eslint"), QStringLiteral("-f"), QStringLiteral("json"), {d->url().toLocalFile()}}; 0108 startHostProcess(m_eslintProcess, name, args); 0109 } 0110 0111 static FileDiagnostics parseLine(const QString &line, std::vector<DiagnosticWithFix> &diagWithFix) 0112 { 0113 QJsonParseError e; 0114 QJsonDocument d = QJsonDocument::fromJson(line.toUtf8(), &e); 0115 if (e.error != QJsonParseError::NoError) { 0116 return {}; 0117 } 0118 0119 const auto arr = d.array(); 0120 if (arr.empty()) { 0121 return {}; 0122 } 0123 auto obj = arr.at(0).toObject(); 0124 QUrl uri = QUrl::fromLocalFile(obj.value(QStringLiteral("filePath")).toString()); 0125 if (!uri.isValid()) { 0126 return {}; 0127 } 0128 0129 FileDiagnostics fd; 0130 fd.uri = uri; 0131 const auto messages = obj.value(QStringLiteral("messages")).toArray(); 0132 if (messages.empty()) { 0133 // No errors in this file 0134 return fd; 0135 } 0136 0137 QList<Diagnostic> diags; 0138 diags.reserve(messages.size()); 0139 for (const auto &m : messages) { 0140 const auto msg = m.toObject(); 0141 if (msg.isEmpty()) { 0142 continue; 0143 } 0144 0145 const int startLine = msg.value(QStringLiteral("line")).toInt() - 1; 0146 const int startColumn = msg.value(QStringLiteral("column")).toInt() - 1; 0147 const int endLine = msg.value(QStringLiteral("endLine")).toInt() - 1; 0148 const int endColumn = msg.value(QStringLiteral("endColumn")).toInt() - 1; 0149 Diagnostic d; 0150 d.range = {startLine, startColumn, endLine, endColumn}; 0151 if (!d.range.isValid()) { 0152 continue; 0153 } 0154 d.code = msg.value(QStringLiteral("ruleId")).toString(); 0155 d.message = msg.value(QStringLiteral("message")).toString(); 0156 d.source = QStringLiteral("eslint"); 0157 const int severity = msg.value(QStringLiteral("severity")).toInt(); 0158 if (severity == 1) { 0159 d.severity = DiagnosticSeverity::Warning; 0160 } else if (severity == 2) { 0161 d.severity = DiagnosticSeverity::Error; 0162 } else { 0163 // fallback, even though there is no other severity 0164 d.severity = DiagnosticSeverity::Information; 0165 } 0166 0167 auto fixObject = msg.value(QStringLiteral("fix")).toObject(); 0168 if (!fixObject.isEmpty()) { 0169 const auto rangeArray = fixObject.value(QStringLiteral("range")).toArray(); 0170 int s, e; 0171 if (rangeArray.size() == 2) { 0172 s = rangeArray[0].toInt(-1); 0173 e = rangeArray[1].toInt(-1); 0174 auto v = fixObject.value(QStringLiteral("text")); 0175 if (!v.isUndefined()) { 0176 DiagnosticWithFix df; 0177 d.message += QStringLiteral(" (fix available)"); 0178 df.diag = d; 0179 df.fix = {s, e, v.toString()}; 0180 diagWithFix.push_back(df); 0181 } 0182 } 0183 } 0184 0185 diags << d; 0186 } 0187 return {uri, diags}; 0188 } 0189 0190 void ESLintPluginView::onReadyRead() 0191 { 0192 /** 0193 * get results of analysis 0194 */ 0195 QHash<QUrl, QList<Diagnostic>> fileDiagnostics; 0196 const auto out = m_eslintProcess.readAllStandardOutput().split('\n'); 0197 for (const auto &rawLine : out) { 0198 /** 0199 * get one line, split it, skip it, if too few elements 0200 */ 0201 QString line = QString::fromLocal8Bit(rawLine); 0202 FileDiagnostics fd = parseLine(line, m_diagsWithFix); 0203 if (!fd.uri.isValid()) { 0204 continue; 0205 } 0206 fileDiagnostics[fd.uri] << fd.diagnostics; 0207 } 0208 0209 for (auto it = fileDiagnostics.cbegin(); it != fileDiagnostics.cend(); ++it) { 0210 m_provider.diagnosticsAdded(FileDiagnostics{it.key(), it.value()}); 0211 } 0212 } 0213 0214 void ESLintPluginView::onError() 0215 { 0216 const QString err = QString::fromUtf8(m_eslintProcess.readAllStandardError()); 0217 if (!err.isEmpty()) { 0218 const QString message = i18n("Eslint failed, error: %1", err); 0219 Utils::showMessage(message, {}, i18n("ESLint"), MessageType::Warning, m_mainWindow); 0220 } 0221 } 0222 0223 void ESLintPluginView::onFixesRequested(const QUrl &u, const Diagnostic &d, const QVariant &v) 0224 { 0225 if (m_diagsWithFix.empty()) { 0226 return; 0227 } 0228 0229 for (const auto &fd : m_diagsWithFix) { 0230 const auto &diag = fd.diag; 0231 if (diag.range == d.range && diag.code == d.code && diag.message == d.message) { 0232 DiagnosticFix f; 0233 f.fixTitle = QStringLiteral("replace with %1").arg(fd.fix.text); 0234 f.fixCallback = [u, fix = fd.fix, this] { 0235 fixDiagnostic(u, fix); 0236 }; 0237 Q_EMIT m_provider.fixesAvailable({f}, v); 0238 } 0239 } 0240 } 0241 0242 void ESLintPluginView::fixDiagnostic(const QUrl &url, const DiagnosticWithFix::Fix &fix) 0243 { 0244 KTextEditor::Document *d = nullptr; 0245 if (m_activeDoc && m_activeDoc->url() == url) { 0246 d = m_activeDoc; 0247 } else { 0248 d = KTextEditor::Editor::instance()->application()->findUrl(url); 0249 } 0250 if (!d) { 0251 const QString message = i18n("Failed to find doc with url %1", url.toLocalFile()); 0252 Utils::showMessage(message, {}, i18n("ESLint"), MessageType::Info, m_mainWindow); 0253 return; 0254 } 0255 0256 KTextEditor::Cursor s = d->offsetToCursor(fix.rangeStart); 0257 KTextEditor::Cursor e = d->offsetToCursor(fix.rangeEnd); 0258 if (s.isValid() && e.isValid()) { 0259 d->replaceText({s, e}, fix.text); 0260 } 0261 } 0262 0263 #include "EslintPlugin.moc" 0264 #include "moc_EslintPlugin.cpp"