diff --git a/CMakeLists.txt b/CMakeLists.txt index 21873f3..d9d2592 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,59 +1,60 @@ cmake_minimum_required(VERSION 2.8.12) project(kdevgoplugin) set(VERSION_MAJOR 1) set(VERSION_MINOR 90) set(VERSION_PATCH 90) # KDevplatform dependency version set(KDEVPLATFORM_VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") find_package(ECM 0.0.9 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) include(ECMAddTests) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings) include(GenerateExportHeader) find_package(Qt5 REQUIRED Core Widgets Test) find_package(KF5 REQUIRED COMPONENTS ItemModels ThreadWeaver TextEditor I18n) find_package(KDevPlatform ${KDEVPLATFORM_VERSION} REQUIRED) find_package(KDevelop-PG-Qt REQUIRED) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR} ) enable_testing() add_subdirectory(app_templates) add_subdirectory(buildsystem) add_subdirectory(parser) add_subdirectory(duchain) add_subdirectory(codecompletion) +add_subdirectory(gometalinter) include_directories( ${KDEVPGQT_INCLUDE_DIR} ) kdevplatform_add_plugin(kdevgoplugin JSON kdevgo.json SOURCES kdevgoplugin.cpp golangparsejob.cpp gohighlighting.cpp godebug.cpp ) target_link_libraries(kdevgoplugin KDev::Interfaces KDev::Language KF5::ThreadWeaver KF5::TextEditor kdevgoparser kdevgoduchain kdevgocompletion ) install(FILES builtins.go DESTINATION ${DATA_INSTALL_DIR}/kdev-go) diff --git a/gometalinter/CMakeLists.txt b/gometalinter/CMakeLists.txt new file mode 100644 index 0000000..2ebc6a4 --- /dev/null +++ b/gometalinter/CMakeLists.txt @@ -0,0 +1,18 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"gometalinter\") + +set(gometalinter_SRCS plugin.cpp job.cpp problemmodel.cpp) + +qt5_add_resources(gometalinter_SRCS + gometalinter.qrc +) +kdevplatform_add_plugin(gometalinter + JSON gometalinter.json + SOURCES ${gometalinter_SRCS} +) + +target_link_libraries(gometalinter + KDev::Language + KDev::Project + KDev::Shell + KF5::ItemViews +) diff --git a/gometalinter/gometalinter.json b/gometalinter/gometalinter.json new file mode 100644 index 0000000..08d3f03 --- /dev/null +++ b/gometalinter/gometalinter.json @@ -0,0 +1,27 @@ +{ + "GenericName": "Gometalinter Support", + "GenericName[x-test]": "xxGometalinter Supportxx", + "KPlugin": { + "Authors": [ + { + "Name": "Mikhail Ivchenko", + "Name[x-test]": "xxMikhail Ivchenkoxx" + } + ], + "Category": "Analyzers", + "Description": "This plugin integrates gometalinter (static analysis tool) to KDevelop", + "Description[x-test]": "xxThis plugin integrates gometalinter (static analysis tool) to KDevelopxx", + "Id": "gometalinter", + "License": "GPL", + "Name": "Gometalinter Support", + "Name[x-test]": "xxGometalinter Supportxx", + "ServiceTypes": [ + "KDevelop/Plugin" + ] + }, + "X-KDevelop-Category": "Global", + "X-KDevelop-IRequired": [ + "org.kdevelop.IExecutePlugin" + ], + "X-KDevelop-Mode": "GUI" +} diff --git a/gometalinter/gometalinter.qrc b/gometalinter/gometalinter.qrc new file mode 100644 index 0000000..43e6ea7 --- /dev/null +++ b/gometalinter/gometalinter.qrc @@ -0,0 +1,6 @@ + + + + gometalinter.rc + + diff --git a/gometalinter/gometalinter.rc b/gometalinter/gometalinter.rc new file mode 100644 index 0000000..ce9990b --- /dev/null +++ b/gometalinter/gometalinter.rc @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/gometalinter/job.cpp b/gometalinter/job.cpp new file mode 100644 index 0000000..12cfb71 --- /dev/null +++ b/gometalinter/job.cpp @@ -0,0 +1,135 @@ +/* KDevelop gometalinter support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#include "job.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace GoMetaLinter +{ + + +Job::Job(const QUrl &workingDirectory, const QString &path, QObject *parent) + : KDevelop::OutputExecuteJob(parent) + , m_timer(new QElapsedTimer) +{ + setJobName(i18n("Go Meta Linter Analysis")); + + setCapabilities(KJob::Killable); + setStandardToolView(KDevelop::IOutputView::TestView); + setBehaviours(KDevelop::IOutputView::AutoScroll); + + setProperties(KDevelop::OutputExecuteJob::JobProperty::DisplayStdout); + setProperties(KDevelop::OutputExecuteJob::JobProperty::DisplayStderr); + setProperties(KDevelop::OutputExecuteJob::JobProperty::PostProcessOutput); + + setWorkingDirectory(workingDirectory); + QStringList commandLine = {"gometalinter", "--aggregate", "--sort=path", path}; + *this << commandLine; + m_projectRootPath = KDevelop::Path(workingDirectory); +} + +Job::~Job() +{ + doKill(); +} + +void Job::postProcessStdout(const QStringList& lines) +{ + static const auto problemRegex = QRegularExpression(QStringLiteral("^([^:]*):([^:]*):([^:]*):([^:]*): ([^:]*)$")); + + QRegularExpressionMatch match; + + QVector problems; + + foreach (const QString & line, lines) { + match = problemRegex.match(line); + if (match.hasMatch()) { + KDevelop::IProblem::Ptr problem(new KDevelop::DetectedProblem(i18n("Go Meta Linter"))); + if(match.captured(4) == QStringLiteral("warning")) + { + problem->setSeverity(KDevelop::IProblem::Warning); + } + if(match.captured(4) == QStringLiteral("error")) + { + problem->setSeverity(KDevelop::IProblem::Error); + } + problem->setDescription(match.captured(5)); + KDevelop::DocumentRange range; + range.document = KDevelop::IndexedString(KDevelop::Path(m_projectRootPath, match.captured(1)).toLocalFile()); + range.setBothLines(match.captured(2).toInt() - 1); + if(!match.captured(3).isEmpty()) + { + range.setBothColumns(match.captured(3).toInt() - 1); + } + problem->setFinalLocation(range); + problems.append(problem); + } + } + + emit problemsDetected(problems); + + if (status() == KDevelop::OutputExecuteJob::JobStatus::JobRunning) { + KDevelop::OutputExecuteJob::postProcessStdout(lines); + } +} + +void Job::start() +{ + m_timer->restart(); + KDevelop::OutputExecuteJob::start(); +} + +void Job::childProcessError(QProcess::ProcessError e) +{ + QString message; + + switch (e) { + case QProcess::FailedToStart: + message = i18n("Failed to start Go Meta Linter from \"%1\".", commandLine()[0]); + break; + + case QProcess::Crashed: + if (status() != KDevelop::OutputExecuteJob::JobStatus::JobCanceled) { + message = i18n("Go Meta Linter crashed."); + } + break; + + case QProcess::Timedout: + message = i18n("Go Meta Linter process timed out."); + break; + + case QProcess::WriteError: + message = i18n("Write to Go Meta Linter process failed."); + break; + + case QProcess::ReadError: + message = i18n("Read from Go Meta Linter process failed."); + break; + + case QProcess::UnknownError: + break; + } + + if (!message.isEmpty()) { + KMessageBox::error(qApp->activeWindow(), message, i18n("Go Meta Linter Error")); + } + + KDevelop::OutputExecuteJob::childProcessError(e); +} + +} diff --git a/gometalinter/job.h b/gometalinter/job.h new file mode 100644 index 0000000..08795b2 --- /dev/null +++ b/gometalinter/job.h @@ -0,0 +1,49 @@ +/* KDevelop gometalinter support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#ifndef GOMETALINTER_JOB_H +#define GOMETALINTER_JOB_H + +#include +#include +#include + +class QElapsedTimer; + +namespace GoMetaLinter +{ + +class Job : public KDevelop::OutputExecuteJob +{ + Q_OBJECT + +public: + explicit Job(const QUrl &workingDirectory, const QString &path, QObject *parent = nullptr); + ~Job() override; + + void start() override; + +Q_SIGNALS: + void problemsDetected(const QVector& problems); + +protected slots: + void postProcessStdout(const QStringList& lines) override; + + void childProcessError(QProcess::ProcessError processError) override; + +protected: + QScopedPointer m_timer; + + KDevelop::Path m_projectRootPath; +}; + +} + +#endif diff --git a/gometalinter/plugin.cpp b/gometalinter/plugin.cpp new file mode 100644 index 0000000..5ccdbc0 --- /dev/null +++ b/gometalinter/plugin.cpp @@ -0,0 +1,200 @@ +/* KDevelop gometalinter support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#include "plugin.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(GoMetaLinterFactory, "gometalinter.json", registerPlugin();) + +namespace GoMetaLinter +{ + +Plugin::Plugin(QObject *parent, const QVariantList&) + : IPlugin(QStringLiteral("gometalinter"), parent), + m_currentProject(nullptr), + m_model(new ProblemModel(this)), + m_job(nullptr) +{ + setXMLFile(QStringLiteral("gometalinter.rc")); + + auto analyzeFile = [this](){ run(false); }; + auto analyzeProject = [this](){ run(true); }; + + m_menuActionFile = new QAction(i18n("Analyze Current File with Go Meta Linter"), this); + connect(m_menuActionFile, &QAction::triggered, this, analyzeFile); + actionCollection()->addAction(QStringLiteral("gometalinter_file"), m_menuActionFile); + + m_menuActionProject = new QAction(i18n("Analyze Current Project with Go Meta Linter"), this); + connect(m_menuActionProject, &QAction::triggered, this, analyzeProject); + actionCollection()->addAction(QStringLiteral("gometalinter_project"), m_menuActionProject); + + auto documentController = core()->documentController(); + connect(documentController, &KDevelop::IDocumentController::documentClosed, this, &Plugin::updateActions); + connect(documentController, &KDevelop::IDocumentController::documentActivated, this, &Plugin::updateActions); + + auto projectController = core()->projectController(); + connect(projectController, &KDevelop::IProjectController::projectOpened, this, &Plugin::updateActions); + connect(projectController, &KDevelop::IProjectController::projectClosed, this, &Plugin::projectClosed); + + m_contextActionFile = new QAction(i18n("Go Meta Linter"), this); + connect(m_contextActionFile, &QAction::triggered, this, analyzeFile); + + m_contextActionProject = new QAction(i18n("Go Meta Linter"), this); + connect(m_contextActionProject, &QAction::triggered, this, analyzeProject); + + m_contextActionProjectItem = new QAction(i18n("Go Meta Linter"), this); + + updateActions(); +} + +Plugin::~Plugin() +{ + killJob(); +} + +void Plugin::run(bool checkProject) +{ + auto path = checkProject ? m_currentProject->path().toUrl() : core()->documentController()->activeDocument()->url(); + run(m_currentProject, path.toLocalFile()); +} + +void Plugin::run(KDevelop::IProject* project, const QString &path) +{ + auto workingDirectory = project->path().toUrl(); + m_model->reset(project, path); + + m_job = new Job(workingDirectory, path); + connect(m_job, &Job::problemsDetected, m_model, &ProblemModel::addProblems); + connect(m_job, &Job::finished, this, &Plugin::result); + + core()->uiController()->registerStatus(new KDevelop::JobStatus(m_job, QStringLiteral("Go Meta Linter"))); + core()->runController()->registerJob(m_job); + + m_model->show(); + + updateActions(); +} + +void Plugin::updateActions() +{ + m_currentProject = nullptr; + m_menuActionFile->setEnabled(false); + m_menuActionProject->setEnabled(false); + + KDevelop::IDocument* activeDocument = core()->documentController()->activeDocument(); + + if (!activeDocument) { + return; + } + + m_currentProject = core()->projectController()->findProjectForUrl(activeDocument->url()); + + m_menuActionFile->setEnabled(m_currentProject); + m_menuActionProject->setEnabled(m_currentProject); +} + +void Plugin::projectClosed(KDevelop::IProject *project) +{ + if (project != m_model->project()) { + return; + } + + killJob(); + m_model->reset(); +} + +void Plugin::killJob() +{ + if(isRunning()) + { + m_job->kill(KJob::EmitResult); + } +} + +void Plugin::result(KJob*) +{ + if (!core()->projectController()->projects().contains(m_model->project())) { + m_model->reset(); + } else { + m_model->show(); + } + + m_job = nullptr; + + updateActions(); +} + +KDevelop::ContextMenuExtension Plugin::contextMenuExtension(KDevelop::Context* context) +{ + KDevelop::ContextMenuExtension extension = KDevelop::IPlugin::contextMenuExtension(context); + + if (context->hasType(KDevelop::Context::EditorContext) && m_currentProject && !isRunning()) { + auto eContext = dynamic_cast(context); + QMimeDatabase db; + const auto mime = db.mimeTypeForUrl(eContext->url()); + + if (mime.name() == QLatin1String("text/x-go")) { + extension.addAction(KDevelop::ContextMenuExtension::AnalyzeFileGroup, m_contextActionFile); + extension.addAction(KDevelop::ContextMenuExtension::AnalyzeProjectGroup, m_contextActionProject); + } + } + + if (context->hasType(KDevelop::Context::ProjectItemContext) && !isRunning()) { + auto pContext = dynamic_cast(context); + if (pContext->items().size() != 1) { + return extension; + } + + auto item = pContext->items().first(); + + switch (item->type()) { + case KDevelop::ProjectBaseItem::File: + case KDevelop::ProjectBaseItem::Folder: + case KDevelop::ProjectBaseItem::BuildFolder: + break; + + default: + return extension; + } + + m_contextActionProjectItem->disconnect(); + connect(m_contextActionProjectItem, &QAction::triggered, this, [this, item](){ + run(item->project(), item->path().toLocalFile()); + }); + + extension.addAction(KDevelop::ContextMenuExtension::AnalyzeProjectGroup, m_contextActionProjectItem); + } + + return extension; +} + +bool Plugin::isRunning() +{ + return m_job; +} + +} + +#include "plugin.moc" \ No newline at end of file diff --git a/gometalinter/plugin.h b/gometalinter/plugin.h new file mode 100644 index 0000000..f30729d --- /dev/null +++ b/gometalinter/plugin.h @@ -0,0 +1,55 @@ +/* KDevelop gometalinter support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#ifndef GOMETALINTER_PLUGIN_H +#define GOMETALINTER_PLUGIN_H + +#include "problemmodel.h" +#include "job.h" + +#include +#include +#include + +namespace GoMetaLinter +{ + +class Plugin : public KDevelop::IPlugin +{ + Q_OBJECT +public: + explicit Plugin(QObject *parent, const QVariantList&); + ~Plugin() override; + KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context) override; + void run(KDevelop::IProject* project, const QString &path); + bool isRunning(); +private: + void updateActions(); + void projectClosed(KDevelop::IProject* project); + void killJob(); + void result(KJob* job); + void run(bool checkProject); + + KDevelop::IProject* m_currentProject; + + QAction* m_menuActionFile; + QAction* m_menuActionProject; + QAction* m_contextActionFile; + QAction* m_contextActionProject; + QAction* m_contextActionProjectItem; + + ProblemModel* m_model; + Job* m_job; +}; + +} + + +#endif //KDEVGOPLUGIN_PLUGIN_H diff --git a/gometalinter/problemmodel.cpp b/gometalinter/problemmodel.cpp new file mode 100644 index 0000000..4db1d5a --- /dev/null +++ b/gometalinter/problemmodel.cpp @@ -0,0 +1,120 @@ +/* KDevelop gometalinter support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#include "problemmodel.h" + +#include "plugin.h" + +#include +#include +#include +#include + +#include + +namespace GoMetaLinter +{ + +inline KDevelop::ProblemModelSet* problemModelSet() +{ + return KDevelop::ICore::self()->languageController()->problemModelSet(); +} + +static const QString problemModelId = QStringLiteral("Go Meta Linter"); + +ProblemModel::ProblemModel(Plugin* plugin) + : KDevelop::ProblemModel(plugin), + m_plugin(plugin), + m_project(nullptr) +{ + setFeatures(CanDoFullUpdate | ScopeFilter | SeverityFilter | Grouping | CanByPassScopeFilter); + reset(); + problemModelSet()->addModel(problemModelId, i18n("Go Meta Linter"), this); +} + +ProblemModel::~ProblemModel() +{ + problemModelSet()->removeModel(problemModelId); +} + +KDevelop::IProject* ProblemModel::project() const +{ + return m_project; +} + +bool ProblemModel::problemExists(KDevelop::IProblem::Ptr newProblem) +{ + for (auto problem : m_problems) { + if (newProblem->source() == problem->source() && + newProblem->severity() == problem->severity() && + newProblem->finalLocation() == problem->finalLocation() && + newProblem->description() == problem->description() && + newProblem->explanation() == problem->explanation()) + return true; + } + + return false; +} + +void ProblemModel::addProblems(const QVector& problems) +{ + static int maxLength = 0; + + if (m_problems.isEmpty()) { + maxLength = 0; + } + + for (auto problem : problems) { + + if (problemExists(problem)) { + continue; + } + + m_problems.append(problem); + addProblem(problem); + + // This performs adjusting of columns width in the ProblemsView + if (maxLength < problem->description().length()) { + maxLength = problem->description().length(); + setProblems(m_problems); + } + } +} + +void ProblemModel::reset() +{ + reset(nullptr, QString()); +} + +void ProblemModel::reset(KDevelop::IProject* project, const QString& path) +{ + m_project = project; + m_path = path; + + clearProblems(); + m_problems.clear(); + + QString tooltip = i18nc("@info:tooltip", "Re-Run Last Go Meta Linter Analysis"); + setFullUpdateTooltip(tooltip); +} + +void ProblemModel::show() +{ + problemModelSet()->showModel(problemModelId); +} + +void ProblemModel::forceFullUpdate() +{ + if (m_project && !m_plugin->isRunning()) { + m_plugin->run(m_project, m_path); + } +} + +} diff --git a/gometalinter/problemmodel.h b/gometalinter/problemmodel.h new file mode 100644 index 0000000..5ec9999 --- /dev/null +++ b/gometalinter/problemmodel.h @@ -0,0 +1,58 @@ +/* KDevelop gometalinter support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#ifndef GOMETALINTER_PROBLEMMODEL_H +#define GOMETALINTER_PROBLEMMODEL_H + +#include + +namespace KDevelop +{ + class IProject; +} + +namespace GoMetaLinter +{ + +class Plugin; + +class ProblemModel : public KDevelop::ProblemModel +{ +public: + explicit ProblemModel(Plugin* plugin); + ~ProblemModel() override; + + KDevelop::IProject* project() const; + + void addProblems(const QVector& problems); + + void reset(); + void reset(KDevelop::IProject* project, const QString& path); + + void show(); + + void forceFullUpdate() override; + +private: + bool problemExists(KDevelop::IProblem::Ptr newProblem); + + using KDevelop::ProblemModel::setProblems; + + Plugin* m_plugin; + + KDevelop::IProject* m_project; + QString m_path; + + QVector m_problems; +}; + +} + +#endif \ No newline at end of file