diff --git a/cmake/modules/FindClazyStandalone.cmake b/cmake/modules/FindClazyStandalone.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindClazyStandalone.cmake @@ -0,0 +1,22 @@ +# Find the clazy-standalone executable +# +# Defines the following variables +# ClazyStandalone_EXECUTABLE - path of the clazy-standalone executable + +#============================================================================= +# Copyright 2018 Anton Anikin +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= + +find_program(ClazyStandalone_EXECUTABLE NAMES clazy-standalone) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(ClazyStandalone DEFAULT_MSG ClazyStandalone_EXECUTABLE) + +mark_as_advanced(ClazyStandalone_EXECUTABLE) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -1,4 +1,5 @@ # BEGIN: Analyzers +add_subdirectory(clazy) add_subdirectory(cppcheck) if(UNIX AND NOT (APPLE OR CYGWIN)) add_subdirectory(heaptrack) diff --git a/plugins/clazy/CMakeLists.txt b/plugins/clazy/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/clazy/CMakeLists.txt @@ -0,0 +1,59 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kdevclazy\") + +find_package(ClazyStandalone QUIET) +set_package_properties(ClazyStandalone PROPERTIES + DESCRIPTION "Qt oriented code checker based on clang framework. Krazy's little brother" + URL "https://github.com/KDE/clazy" + PURPOSE "Recommended: required by the non-essential Clazy plugin" + TYPE RUNTIME +) + +set(kdevclazy_core_SRCS + checks_db.cpp + job.cpp + job_parameters.cpp + utils.cpp +) +ecm_qt_declare_logging_category(kdevclazy_core_SRCS + HEADER debug.h + IDENTIFIER KDEV_CLAZY + CATEGORY_NAME "kdevelop.analyzers.clazy" +) +kconfig_add_kcfg_files(kdevclazy_core_SRCS GENERATE_MOC + config/globalsettings.kcfgc +) +kconfig_add_kcfg_files(kdevclazy_core_SRCS + config/projectsettings.kcfgc +) +add_library(kdevclazy_core STATIC + ${kdevclazy_core_SRCS} +) +target_link_libraries(kdevclazy_core + KDev::Project + KDev::Shell +) + +set(kdevclazy_SRCS + plugin.cpp + problem_model.cpp + + config/checks_widget.cpp + config/command_line_widget.cpp + config/globalconfigpage.cpp + config/projectconfigpage.cpp +) +qt5_add_resources(kdevclazy_SRCS + kdevclazy.qrc +) +kdevplatform_add_plugin(kdevclazy + JSON kdevclazy.json + SOURCES ${kdevclazy_SRCS} +) +target_link_libraries(kdevclazy + kdevclazy_core + KF5::ItemViews +) + +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/plugins/clazy/checks_db.h b/plugins/clazy/checks_db.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/checks_db.h @@ -0,0 +1,71 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_CHECKS_DB_H +#define KDEVCLAZY_CHECKS_DB_H + +#include +#include + +namespace Clazy +{ + +struct Level; + +struct Check +{ + const Level* level = nullptr; + QString name; + QString description; + QUrl url; +}; + +struct Level +{ + QString name; + QString displayName; + QString description; + + QMap checks; +}; + +class ChecksDB +{ +public: + explicit ChecksDB(const QUrl& docsPath); + ~ChecksDB(); + + bool isValid() const; + QString error() const; + + const QMap& levels() const; + + const QMap& checks() const; + +protected: + QString m_error; + + QMap m_checks; + QMap m_levels; +}; + +} + +#endif diff --git a/plugins/clazy/checks_db.cpp b/plugins/clazy/checks_db.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/checks_db.cpp @@ -0,0 +1,160 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "checks_db.h" + +#include "globalsettings.h" +#include "utils.h" + +#include + +#include +#include + +namespace Clazy +{ + +ChecksDB::ChecksDB(const QUrl& docsPath) +{ + static const QHash levelName = { + { QStringLiteral("manuallevel"), QStringLiteral("manual") }, + { QStringLiteral("hiddenlevel"), QStringLiteral("manual") } + }; + + static const QHash levelDisplayName = { + { QStringLiteral("level0"), i18n("Level 0") }, + { QStringLiteral("level1"), i18n("Level 1") }, + { QStringLiteral("level2"), i18n("Level 2") }, + { QStringLiteral("level3"), i18n("Level 3") }, + { QStringLiteral("manual"), i18n("Manual Level") } + }; + + static const QHash levelDescription = { + { QStringLiteral("level0"), + i18n("Very stable checks, 99.99% safe, mostly no false-positives, very desirable.") }, + + { QStringLiteral("level1"), + i18n("The default level. Very similar to level 0, slightly more false-positives but very few.") }, + + { QStringLiteral("level2"), + i18n("Also very few false-positives, but contains noisy checks which not everyone agree should be default.") }, + + { QStringLiteral("level3"), + i18n("Contains checks with high rate of false-positives.") }, + + { QStringLiteral("manual"), + i18n("Checks here need to be enabled explicitly, as they don't belong to any level. " + "Checks here are very stable and have very few false-positives.") } + }; + + const QString defaultError = i18n( + "Unable to load clazy checks information from '%1'. Please check your settings.", + docsPath.toLocalFile()); + + QDir docsDir(docsPath.toLocalFile()); + if (!docsDir.exists()) { + m_error = defaultError; + return; + } + + auto levelsDirs = docsDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const auto& levelDir : levelsDirs) { + static const QRegularExpression levelRE(QStringLiteral(".*level.*")); + + auto match = levelRE.match(levelDir); + if (!match.hasMatch()) { + continue; + } + + if (!docsDir.cd(levelDir)) { + continue; + } + + auto level = new Level; + level->name = levelName.value(levelDir, levelDir); + level->displayName = levelDisplayName.value(level->name, levelDir); + level->description = levelDescription.value(level->name, {}); + + auto checksFiles = docsDir.entryList(QDir::Files | QDir::Readable); + for (const auto& checkFile : checksFiles) { + static const QRegularExpression checkRE(QStringLiteral("^README-(.+)\\.md$")); + + auto match = checkRE.match(checkFile); + if (!match.hasMatch()) { + continue; + } + + QFile mdFile(docsDir.absoluteFilePath(checkFile)); + if (!mdFile.open(QIODevice::ReadOnly)) { + continue; + } + + auto check = new Check; + check->level = level; + check->name = match.captured(1); + check->description = markdown2html(mdFile.readAll()); + check->url = QUrl(QStringLiteral( + "https://github.com/KDE/clazy/blob/master/docs/checks/README-%1.md").arg(check->name)); + level->checks[check->name] = check; + + m_checks[check->name] = check; + } + + if (level->checks.isEmpty()) { + delete level; + } else { + m_levels[level->name] = level; + } + + docsDir.cdUp(); + } + + if (m_levels.isEmpty()) { + m_error = defaultError; + } +} + +ChecksDB::~ChecksDB() +{ + qDeleteAll(m_levels); + qDeleteAll(m_checks); +} + +bool ChecksDB::isValid() const +{ + return m_error.isEmpty(); +} + +QString ChecksDB::error() const +{ + return m_error; +} + +const QMap& ChecksDB::levels() const +{ + return m_levels; +} + +const QMap& ChecksDB::checks() const +{ + return m_checks; +} + +} diff --git a/plugins/clazy/config/checks_widget.h b/plugins/clazy/config/checks_widget.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/checks_widget.h @@ -0,0 +1,70 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_CHECKS_WIDGET_H +#define KDEVCLAZY_CHECKS_WIDGET_H + +#include + +class QTreeWidget; +class QTreeWidgetItem; + +namespace Clazy +{ + +class ChecksDB; + +class ChecksWidget : public QWidget +{ + Q_OBJECT + + Q_PROPERTY( + QString checks + READ checks + WRITE setChecks + NOTIFY checksChanged + USER true) + +public: + explicit ChecksWidget(QSharedPointer db, QWidget* parent = nullptr); + + ~ChecksWidget() override; + + QString checks() const; + + void setChecks(const QString& checks); + +Q_SIGNALS: + void checksChanged(const QString& cheks); + +private: + void updateChecks(); + + void setState(QTreeWidgetItem* item, Qt::CheckState state); + + QString m_checks; + + QTreeWidget* m_treeWidget; + QHash m_items; +}; + +} + +#endif diff --git a/plugins/clazy/config/checks_widget.cpp b/plugins/clazy/config/checks_widget.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/checks_widget.cpp @@ -0,0 +1,233 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "checks_widget.h" + +#include "checks_db.h" +#include "debug.h" +#include "job_parameters.h" + +#include +#include +#include +#include +#include + +namespace Clazy +{ + +enum DataRole { + CheckNameRole = Qt::UserRole + 1 +}; + +ChecksWidget::ChecksWidget(QSharedPointer db, QWidget* parent) + : QWidget(parent) +{ + auto mainLayout = new QVBoxLayout(this); + + m_treeWidget = new QTreeWidget; + m_treeWidget->setColumnCount(2); + m_treeWidget->header()->setStretchLastSection(false); + m_treeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); + m_treeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + m_treeWidget->header()->hide(); + mainLayout->addWidget(m_treeWidget); + + auto linkActivated = [](const QString & link) { + QDesktopServices::openUrl(QUrl(link)); + }; + + for (auto level : db->levels()) { + auto levelItem = new QTreeWidgetItem(m_treeWidget, { level->displayName }); + levelItem->setData(0, CheckNameRole, level->name); + levelItem->setToolTip(0, level->description); + levelItem->setCheckState(0, Qt::Unchecked); + + m_items[level->name] = levelItem; + + for (auto check : qAsConst(level->checks)) { + auto checkItem = new QTreeWidgetItem(levelItem, { check->name }); + checkItem->setData(0, CheckNameRole, check->name); + checkItem->setToolTip(0, check->description); + checkItem->setCheckState(0, Qt::Unchecked); + + auto urlLabel = new QLabel; + urlLabel->setText(QStringLiteral("Web Page").arg(check->url.toDisplayString())); + urlLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + m_treeWidget->setItemWidget(checkItem, 1, urlLabel); + + connect(urlLabel, &QLabel::linkActivated, this, linkActivated); + + m_items[check->name] = checkItem; + } + } + + connect(m_treeWidget, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* item) { + setState(item, item->checkState(0)); + updateChecks(); + }); +} + +ChecksWidget::~ChecksWidget() +{ +} + +QString ChecksWidget::checks() const +{ + return m_checks; +} + +void ChecksWidget::setChecks(const QString& checks) +{ + if (m_checks == checks) { + return; + } + + for (int i = 0 ; i < m_treeWidget->topLevelItemCount(); ++i) { + setState(m_treeWidget->topLevelItem(i), Qt::Unchecked); + } + + const QStringList checksList = checks.split(',', QString::SkipEmptyParts); + for (auto checkName : checksList) { + checkName = checkName.trimmed(); + + if (checkName == QStringLiteral("manual")) { + // FIXME add qwarn + continue; + } + + auto state = Qt::Checked; + if (checkName.startsWith(QStringLiteral("no-"))) { + checkName = checkName.mid(3); + state = Qt::Unchecked; + } + + if (auto checkItem = m_items.value(checkName, nullptr)) { + setState(checkItem, state); + } + } + + updateChecks(); +} + +QStringList levelChecks(const QTreeWidgetItem* levelItem, QString& topLevel) +{ + Q_ASSERT(levelItem); + + if (levelItem->checkState(0) == Qt::Unchecked) { + return {}; + } + + auto levelName = levelItem->data(0, CheckNameRole).toString(); + + if (levelItem->checkState(0) == Qt::Checked && + levelName != QStringLiteral("manual") && + topLevel.isEmpty()) { + + topLevel = levelName; + return {}; + } + + QStringList enabled; + QStringList disabled; + + for (int j = 0 ; j < levelItem->childCount(); ++j) { + auto checkItem = levelItem->child(j); + auto checkName = checkItem->data(0, CheckNameRole).toString(); + + if (checkItem->checkState(0) == Qt::Checked) { + enabled += checkName; + } else { + disabled += QStringLiteral("no-%1").arg(checkName); + } + } + + if (levelName == QStringLiteral("manual") || enabled.size() < disabled.size()) { + return enabled; + } + + if (topLevel.isEmpty()) { + topLevel = levelName; + } + + return disabled; +} + +void ChecksWidget::updateChecks() +{ + QStringList checksList; + QString topLevel; + + for (int i = m_treeWidget->topLevelItemCount() - 1; i >= 0; --i) { + checksList += levelChecks(m_treeWidget->topLevelItem(i), topLevel); + } + + checksList.sort(); + if (!topLevel.isEmpty()) { + checksList.prepend(topLevel); + } + + auto checks = checksList.join(','); + + if (checks.isEmpty()) { + checks = JobParameters::defaultChecks(); + } + + if (checks != m_checks) { + m_checks = checks; + emit checksChanged(m_checks); + } +} + +void ChecksWidget::setState(QTreeWidgetItem* item, Qt::CheckState state) +{ + Q_ASSERT(item); + + QSignalBlocker blocker(m_treeWidget); + + item->setCheckState(0, state); + for (int i = 0; i < item->childCount(); ++i) { + item->child(i)->setCheckState(0, state); + } + + auto parent = item->parent(); + if (!parent) { + return; + } + + const int childCount = parent->childCount(); + int checkedCount = 0; + + for (int i = 0; i < childCount; ++i) { + if (parent->child(i)->checkState(0) == Qt::Checked) { + ++checkedCount; + } + } + + if (checkedCount == 0) { + parent->setCheckState(0, Qt::Unchecked); + } else if (checkedCount == childCount) { + parent->setCheckState(0, Qt::Checked); + } else { + parent->setCheckState(0, Qt::PartiallyChecked); + } +} + +} diff --git a/plugins/clazy/config/command_line_widget.h b/plugins/clazy/config/command_line_widget.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/command_line_widget.h @@ -0,0 +1,55 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_COMMAND_LINE_WIDGET_H +#define KDEVCLAZY_COMMAND_LINE_WIDGET_H + +#include + +class QCheckBox; +class QLineEdit; +class QTextEdit; + +namespace Clazy +{ + +class CommandLineWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CommandLineWidget(QWidget* parent = nullptr); + ~CommandLineWidget() override; + + void setText(const QString& text); + +protected: + void updateCommandLine(); + + QString m_text; + + QLineEdit* m_cmdFilter; + QCheckBox* m_cmdBreak; + QTextEdit* m_cmdEdit; +}; + +} + +#endif diff --git a/plugins/clazy/config/command_line_widget.cpp b/plugins/clazy/config/command_line_widget.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/command_line_widget.cpp @@ -0,0 +1,101 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "command_line_widget.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace Clazy +{ + +CommandLineWidget::CommandLineWidget(QWidget* parent) + : QWidget(parent) +{ + auto mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + + auto groupWidget = new QGroupBox(i18n("Command line")); + auto groupLayout = new QVBoxLayout(groupWidget); + mainLayout->addWidget(groupWidget); + + auto filter = new KFilterProxySearchLine; + m_cmdFilter = filter->lineEdit(); + + m_cmdBreak = new QCheckBox(i18n("Break lines")); + + auto cmdFilterLayout = new QHBoxLayout; + cmdFilterLayout->addWidget(filter); + cmdFilterLayout->addWidget(m_cmdBreak); + groupLayout->addLayout(cmdFilterLayout); + + m_cmdEdit = new QTextEdit; + m_cmdEdit->setReadOnly(true); + m_cmdEdit->setFontFamily(QStringLiteral("Monospace")); + groupLayout->addWidget(m_cmdEdit); + + connect(m_cmdFilter, &QLineEdit::textChanged, this, &CommandLineWidget::updateCommandLine); + connect(m_cmdBreak, &QCheckBox::stateChanged, this, &CommandLineWidget::updateCommandLine); +} + +CommandLineWidget::~CommandLineWidget() +{ +} + +void CommandLineWidget::setText(const QString& text) +{ + if (m_text != text) { + m_text = text; + updateCommandLine(); + } +} + +void CommandLineWidget::updateCommandLine() +{ + auto commandLine = m_text; + if (m_cmdBreak->isChecked()) { + commandLine.replace(QLatin1String(" -"), QLatin1String("\n-")); + commandLine.replace(QLatin1String(","), QLatin1String("\n,")); + } + + auto filterText = m_cmdFilter->text(); + if (!filterText.isEmpty()) { + QStringList lines = commandLine.split('\n'); + QMutableStringListIterator i(lines); + + while (i.hasNext()) { + if (!i.next().contains(filterText)) { + i.remove(); + } + } + + commandLine = lines.join('\n'); + } + + m_cmdEdit->setText(commandLine); +} + +} diff --git a/plugins/clazy/config/globalconfigpage.h b/plugins/clazy/config/globalconfigpage.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/globalconfigpage.h @@ -0,0 +1,48 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_GLOBAL_CONFIG_PAGE_H +#define KDEVCLAZY_GLOBAL_CONFIG_PAGE_H + +#include + +namespace Clazy +{ + +class Plugin; + +class GlobalConfigPage: public KDevelop::ConfigPage +{ + Q_OBJECT + +public: + GlobalConfigPage(Plugin* plugin, QWidget* parent); + ~GlobalConfigPage() override; + + KDevelop::ConfigPage::ConfigPageType configPageType() const override; + + QString name() const override; + QString fullName() const override; + QIcon icon() const override; +}; + +} + +#endif diff --git a/plugins/clazy/config/globalconfigpage.cpp b/plugins/clazy/config/globalconfigpage.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/globalconfigpage.cpp @@ -0,0 +1,182 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "globalconfigpage.h" + +#include "checks_db.h" +#include "globalsettings.h" +#include "plugin.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Clazy +{ + +GlobalConfigPage::GlobalConfigPage(Plugin* plugin, QWidget* parent) + : ConfigPage(plugin, GlobalSettings::self(), parent) +{ + auto mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + + // ============================================================================================= + + auto pathBox = new QGroupBox(i18n("Paths")); + auto pathLayout = new QFormLayout(pathBox); + mainLayout->addWidget(pathBox); + + auto executablePath = new KUrlRequester; + executablePath->setObjectName(QStringLiteral("kcfg_executablePath")); + executablePath->setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly); + executablePath->setPlaceholderText(i18n("path to clazy-standalone executable")); + pathLayout->addRow(new QLabel(i18n("clazy-standalone:")), executablePath); + + auto docsPath = new KUrlRequester; + docsPath->setObjectName(QStringLiteral("kcfg_docsPath")); + docsPath->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + docsPath->setPlaceholderText(i18n("path to clazy docs directory")); + pathLayout->addRow(new QLabel(i18n("clazy docs:")), docsPath); + + auto checksInfoLabel = new QLabel; + pathLayout->addRow(nullptr, checksInfoLabel); + + // ============================================================================================= + + auto jobsBox = new QGroupBox; + auto jobsLayout = new QVBoxLayout(jobsBox); + mainLayout->addWidget(jobsBox); + + auto parallelJobsEnabled = new QCheckBox(i18n("Run analysis jobs in parallel")); + parallelJobsEnabled->setObjectName(QStringLiteral("kcfg_parallelJobsEnabled")); + jobsLayout->addWidget(parallelJobsEnabled); + + auto coresLayout = new QHBoxLayout; + jobsLayout->addLayout(coresLayout); + + auto parallelJobsAutoCount = new QCheckBox(i18n("Use all CPU cores")); + parallelJobsAutoCount->setObjectName(QStringLiteral("kcfg_parallelJobsAutoCount")); + coresLayout->addWidget(parallelJobsAutoCount); + coresLayout->addStretch(); + + auto parallelJobsFixedCountLabel = new QLabel(i18n("Maximum number of threads:")); + coresLayout->addWidget(parallelJobsFixedCountLabel); + + auto parallelJobsFixedCount = new QSpinBox; + parallelJobsFixedCount->setObjectName(QStringLiteral("kcfg_parallelJobsFixedCount")); + parallelJobsFixedCount->setMinimum(1); + coresLayout->addWidget(parallelJobsFixedCount); + + auto jobsSettingsChanged = [ + parallelJobsEnabled, + parallelJobsAutoCount, + parallelJobsFixedCountLabel, parallelJobsFixedCount]() { + + const bool jobsEnabled = parallelJobsEnabled->checkState() == Qt::Checked; + const bool autoEnabled = parallelJobsAutoCount->checkState() == Qt::Checked; + + parallelJobsAutoCount->setEnabled(jobsEnabled); + + parallelJobsFixedCount->setEnabled(!autoEnabled); + parallelJobsFixedCountLabel->setEnabled(!autoEnabled); + }; + + connect(parallelJobsEnabled, &QCheckBox::stateChanged, this, jobsSettingsChanged); + connect(parallelJobsAutoCount, &QCheckBox::stateChanged, this, jobsSettingsChanged); + + // ============================================================================================= + + auto outputBox = new QGroupBox(i18n("Output")); + auto outputGroupLayout = new QVBoxLayout(outputBox); + mainLayout->addWidget(outputBox); + + auto hideOutputView = new QCheckBox(i18n("Hide output view during check")); + hideOutputView->setObjectName(QStringLiteral("kcfg_hideOutputView")); + outputGroupLayout->addWidget(hideOutputView); + + // ============================================================================================= + + auto errorWidget = new KMessageWidget; + errorWidget->setCloseButtonVisible(false); + errorWidget->setMessageType(KMessageWidget::Error); + errorWidget->setVisible(false); + errorWidget->setWordWrap(true); + mainLayout->addWidget(errorWidget); + mainLayout->addStretch(); + + // ============================================================================================= + + auto checkUrls = [executablePath, docsPath, errorWidget, checksInfoLabel]() { + ChecksDB db(docsPath->url()); + checksInfoLabel->setText(i18n("%1 checks detected", db.checks().size())); + + JobGlobalParameters params(executablePath->url(), docsPath->url()); + if (!params.isValid()) { + errorWidget->setText(params.error()); + errorWidget->setVisible(true); + return; + } + + if (!db.isValid()) { + errorWidget->setText(db.error()); + errorWidget->setVisible(true); + return; + } + + errorWidget->setVisible(false); + }; + + connect(executablePath, &KUrlRequester::textChanged, this, checkUrls); + connect(docsPath, &KUrlRequester::textChanged, this, checkUrls); + + checkUrls(); +} + +GlobalConfigPage::~GlobalConfigPage() +{ +} + +KDevelop::ConfigPage::ConfigPageType GlobalConfigPage::configPageType() const +{ + return KDevelop::ConfigPage::AnalyzerConfigPage; +} + +QString GlobalConfigPage::name() const +{ + return i18n("Clazy"); +} + +QString GlobalConfigPage::fullName() const +{ + return i18n("Configure Clazy Settings"); +} + +QIcon GlobalConfigPage::icon() const +{ + return QIcon::fromTheme(QStringLiteral("kdevelop")); +} + +} diff --git a/plugins/clazy/config/globalsettings.kcfg b/plugins/clazy/config/globalsettings.kcfg new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/globalsettings.kcfg @@ -0,0 +1,39 @@ + + + + + + "job_parameters.h" + + + + + JobGlobalParameters::defaultExecutablePath() + + + + JobGlobalParameters::defaultDocsPath() + + + + + true + + + + true + + + + 2 + + + + true + + + + diff --git a/plugins/clazy/config/globalsettings.kcfgc b/plugins/clazy/config/globalsettings.kcfgc new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/globalsettings.kcfgc @@ -0,0 +1,4 @@ +File=globalsettings.kcfg +NameSpace=Clazy +ClassName=GlobalSettings +Singleton=true diff --git a/plugins/clazy/config/projectconfigpage.h b/plugins/clazy/config/projectconfigpage.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/projectconfigpage.h @@ -0,0 +1,57 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_PROJECT_CONFIG_PAGE_H +#define KDEVCLAZY_PROJECT_CONFIG_PAGE_H + +#include + +namespace KDevelop +{ + +class IProject; + +} + +namespace Clazy +{ + +class JobParameters; +class Plugin; + +class ProjectConfigPage : public KDevelop::ConfigPage +{ + Q_OBJECT + +public: + ProjectConfigPage(Plugin* plugin, KDevelop::IProject* project, QWidget* parent); + + ~ProjectConfigPage() override; + + QIcon icon() const override; + QString name() const override; + +protected: + QScopedPointer m_parameters; +}; + +} + +#endif diff --git a/plugins/clazy/config/projectconfigpage.cpp b/plugins/clazy/config/projectconfigpage.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/projectconfigpage.cpp @@ -0,0 +1,245 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "projectconfigpage.h" + +#include "job_parameters.h" +#include "projectsettings.h" +#include "checks_db.h" +#include "checks_widget.h" +#include "command_line_widget.h" +#include "plugin.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Clazy +{ + +ProjectConfigPage::ProjectConfigPage(Plugin* plugin, KDevelop::IProject* project, QWidget* parent) + : ConfigPage(plugin, new ProjectSettings, parent) + , m_parameters(new JobParameters(project)) +{ + Q_ASSERT(plugin); + + auto mainLayout = new QVBoxLayout(this); + + if (!plugin->checksDB()->isValid()) { + auto dbError = new KMessageWidget; + dbError->setMessageType(KMessageWidget::Error); + dbError->setCloseButtonVisible(false); + dbError->setWordWrap(true); + dbError->setText(plugin->checksDB()->error()); + mainLayout->addWidget(dbError); + return; + } + + configSkeleton()->setSharedConfig(project->projectConfiguration()); + configSkeleton()->load(); + + auto tabWidget = new QTabWidget; + tabWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + mainLayout->addWidget(tabWidget); + + // ============================================================================================= + + auto checksWidget = new ChecksWidget(plugin->checksDB()); + checksWidget->setObjectName(QStringLiteral("kcfg_checks")); + connect(checksWidget, &ChecksWidget::checksChanged, m_parameters.data(), &JobParameters::setChecks); + tabWidget->addTab(checksWidget, i18n("Checks")); + + // ============================================================================================= + + auto optionsWidget = new QWidget; + auto optionsLayout = new QVBoxLayout(optionsWidget); + + auto commonWidget = new QGroupBox; + auto commonLayout = new QGridLayout(commonWidget); + + auto onlyQt = new QCheckBox(i18n("Only Qt")); + onlyQt->setObjectName(QStringLiteral("kcfg_onlyQt")); + onlyQt->setToolTip(i18n( + "Won't emit warnings for non-Qt files, or in other words, if -DQT_CORE_LIB is missing")); + connect(onlyQt, &QCheckBox::stateChanged, this, [this](int state) { + m_parameters->setOnlyQt(state != Qt::Unchecked); + }); + commonLayout->addWidget(onlyQt, 0, 0); + + auto qtDeveloper = new QCheckBox(i18n("Qt developer")); + qtDeveloper->setObjectName(QStringLiteral("kcfg_qtDeveloper")); + qtDeveloper->setToolTip(i18n( + "For running clazy on Qt itself, optional, but honours specific guidelines")); + connect(qtDeveloper, &QCheckBox::stateChanged, this, [this](int state) { + m_parameters->setQtDeveloper(state != Qt::Unchecked); + }); + commonLayout->addWidget(qtDeveloper, 1, 0); + + auto qt4Compat = new QCheckBox(i18n("Qt4 Compatible")); + qt4Compat->setObjectName(QStringLiteral("kcfg_qt4Compat")); + qt4Compat->setToolTip(i18n("Turns off checks not compatible with Qt 4")); + connect(qt4Compat, &QCheckBox::stateChanged, this, [this](int state) { + m_parameters->setQt4Compat(state != Qt::Unchecked); + }); + commonLayout->addWidget(qt4Compat, 0, 1); + + auto visitImplicitCode = new QCheckBox(i18n("Visit Implicit Code")); + visitImplicitCode->setObjectName(QStringLiteral("kcfg_visitImplicitCode")); + visitImplicitCode->setToolTip(i18n( + "For visiting implicit code like compiler generated constructors.\n" + "None of the built-in checks benefit from this, but can be useful for custom checks.")); + connect(visitImplicitCode, &QCheckBox::stateChanged, this, [this](int state) { + m_parameters->setVisitImplicitCode(state != Qt::Unchecked); + }); + commonLayout->addWidget(visitImplicitCode, 1, 1); + + optionsLayout->addWidget(commonWidget); + + auto headerWidget = new QGroupBox; + auto headerLayout = new QFormLayout(headerWidget); + + auto ignoreIncludedFiles = new QCheckBox(i18n("Ignore Included Files")); + ignoreIncludedFiles->setObjectName(QStringLiteral("kcfg_ignoreIncludedFiles")); + ignoreIncludedFiles->setToolTip(i18n( + "Only emit warnings for the current file being compiled and ignore any includes.\n" + "Useful for performance reasons.")); + connect(ignoreIncludedFiles, &QCheckBox::stateChanged, this, [this](int state) { + m_parameters->setIgnoreIncludedFiles(state != Qt::Unchecked); + }); + headerLayout->addRow(ignoreIncludedFiles); + + auto headerFilterDescription = i18n( + "Regular expression matching the names of the headers to output diagnostics from.\n" + "Diagnostics from the main file of each translation unit are always displayed."); + auto headerFilter = new QLineEdit; + headerFilter->setPlaceholderText(headerFilterDescription); + headerFilter->setToolTip(headerFilterDescription); + headerFilter->setObjectName(QStringLiteral("kcfg_headerFilter")); + connect(headerFilter, &QLineEdit::textChanged, m_parameters.data(), &JobParameters::setHeaderFilter); + headerLayout->addRow(new QLabel(i18n("Header filter:")), headerFilter); + + optionsLayout->addWidget(headerWidget); + + auto fixitsWidget = new QGroupBox; + auto fixitsLayout = new QGridLayout(fixitsWidget); + + auto enableAllFixits = new QCheckBox(i18n("Enable all fixits")); + enableAllFixits->setObjectName(QStringLiteral("kcfg_enableAllFixits")); + enableAllFixits->setToolTip(""); + connect(enableAllFixits, &QCheckBox::stateChanged, this, [this](int state) { + m_parameters->setEnableAllFixits(state != Qt::Unchecked); + }); + fixitsLayout->addWidget(enableAllFixits, 0, 0); + + auto noInplaceFixits = new QCheckBox(i18n("No-inplace fixits")); + noInplaceFixits->setObjectName(QStringLiteral("kcfg_noInplaceFixits")); + noInplaceFixits->setToolTip(i18n( + "Fixits will be applied to a separate file (for unit-test use only)")); + connect(noInplaceFixits, &QCheckBox::stateChanged, this, [this](int state) { + m_parameters->setNoInplaceFixits(state != Qt::Unchecked); + }); + fixitsLayout->addWidget(noInplaceFixits, 0, 1); + + auto fixitsWarning = new KMessageWidget; + fixitsWarning->setMessageType(KMessageWidget::Warning); + fixitsWarning->setCloseButtonVisible(false); + // We don't use setWordWrap(true) here because of sizeHint-problems. When wrap is enabled + // message widget takes too many vertical space which is critical for our config page. + fixitsWarning->setText(i18n( + "WARNING: Backup your code and don't blame me if a fixit is not applied correctly.\n" + "For better results don't use parallel checks, otherwise a fixit being applied " + "in an header file might be done twice.")); + fixitsWarning->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + fixitsLayout->addWidget(fixitsWarning, 1, 0, 1, 2); + + optionsLayout->addWidget(fixitsWidget); + optionsLayout->addStretch(); + + tabWidget->addTab(optionsWidget, i18n("Options")); + + // ============================================================================================= + + auto extraWidget = new QWidget; + auto extraLayout = new QFormLayout(extraWidget); + + auto extraAppendDescription = i18n("Additional parameters to append to the compiler command line"); + auto extraAppend = new QLineEdit; + extraAppend->setPlaceholderText(extraAppendDescription); + extraAppend->setToolTip(extraAppendDescription); + extraAppend->setObjectName(QStringLiteral("kcfg_extraAppend")); + connect(extraAppend, &QLineEdit::textChanged, m_parameters.data(), &JobParameters::setExtraAppend); + extraLayout->addRow(new QLabel(i18n("Compiler append:")), extraAppend); + + auto extraPrependDescription = i18n("Additional parameters to prepend to the compiler command line"); + auto extraPrepend = new QLineEdit; + extraPrepend->setPlaceholderText(extraPrependDescription); + extraPrepend->setToolTip(extraPrependDescription); + extraPrepend->setObjectName(QStringLiteral("kcfg_extraPrepend")); + connect(extraPrepend, &QLineEdit::textChanged, m_parameters.data(), &JobParameters::setExtraPrepend); + extraLayout->addRow(new QLabel(i18n("Compiler prepend:")), extraPrepend); + + auto extraClazyDescription = i18n("Additional parameters to clazy-standalone"); + auto extraClazy = new QLineEdit; + extraClazy->setObjectName(QStringLiteral("kcfg_extraClazy")); + extraClazy->setPlaceholderText(extraClazyDescription); + extraClazy->setToolTip(extraClazyDescription); + connect(extraClazy, &QLineEdit::textChanged, m_parameters.data(), &JobParameters::setExtraClazy); + extraLayout->addRow(new QLabel(i18n("Extra parameters:")), extraClazy); + + tabWidget->addTab(extraWidget, i18n("Extra parameters")); + + // ============================================================================================= + + auto commandLineWidget = new CommandLineWidget; + mainLayout->addWidget(commandLineWidget); + + auto settingsChanged = [this, commandLineWidget]() + { + commandLineWidget->setText(m_parameters->commandLine().join(' ')); + }; + + connect(m_parameters.data(), &JobParameters::changed, this, settingsChanged); + settingsChanged(); +} + +ProjectConfigPage::~ProjectConfigPage() +{ +} + +QIcon ProjectConfigPage::icon() const +{ + return QIcon::fromTheme(QStringLiteral("dialog-ok")); +} + +QString ProjectConfigPage::name() const +{ + return i18n("Clazy"); +} + +} diff --git a/plugins/clazy/config/projectsettings.kcfg b/plugins/clazy/config/projectsettings.kcfg new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/projectsettings.kcfg @@ -0,0 +1,52 @@ + + + + "job_parameters.h" + + + + + JobParameters::defaultChecks() + + + + false + + + + false + + + + false + + + + false + + + + false + + + + + + false + + + + false + + + + + + + + + + diff --git a/plugins/clazy/config/projectsettings.kcfgc b/plugins/clazy/config/projectsettings.kcfgc new file mode 100644 --- /dev/null +++ b/plugins/clazy/config/projectsettings.kcfgc @@ -0,0 +1,3 @@ +File=projectsettings.kcfg +NameSpace=Clazy +ClassName=ProjectSettings diff --git a/plugins/clazy/job.h b/plugins/clazy/job.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/job.h @@ -0,0 +1,71 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_JOB_H +#define KDEVCLAZY_JOB_H + +#include +#include + +class QElapsedTimer; + +namespace Clazy +{ + +class ChecksDB; +class JobParameters; + +class Job : public KDevelop::OutputExecuteJob +{ + Q_OBJECT + +public: + Job(const JobParameters& params, QSharedPointer db); + ~Job() override; + + void start() override; + +Q_SIGNALS: + void problemsDetected(const QVector& problems); + +protected Q_SLOTS: + void postProcessStdout(const QStringList& lines) override; + void postProcessStderr(const QStringList& lines) override; + + void childProcessExited(int exitCode, QProcess::ExitStatus exitStatus) override; + void childProcessError(QProcess::ProcessError processError) override; + +protected: + Job(); + + QString buildMakefile(const JobParameters& params); + + QSharedPointer m_db; + QScopedPointer m_timer; + + QStringList m_standardOutput; + QStringList m_stderrOutput; + + int m_totalCount = 0; + int m_finishedCount = 0; +}; + +} +#endif diff --git a/plugins/clazy/job.cpp b/plugins/clazy/job.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/job.cpp @@ -0,0 +1,256 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "job.h" + +#include "checks_db.h" +#include "debug.h" +#include "globalsettings.h" +#include "plugin.h" +#include "utils.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Clazy +{ + +// Empty constructor which creates invalid Job instance. Used only for testing +Job::Job() + : KDevelop::OutputExecuteJob(nullptr) +{ +} + +Job::Job(const JobParameters& params, QSharedPointer db) + : KDevelop::OutputExecuteJob(nullptr) + , m_db(db) + , m_timer(new QElapsedTimer) +{ + setJobName(i18n("Clazy Analysis (%1)", prettyPathName(params.checkPath()))); + + setCapabilities(KJob::Killable); + setStandardToolView(KDevelop::IOutputView::TestView); + setBehaviours(KDevelop::IOutputView::AutoScroll); + + setProperties(OutputExecuteJob::JobProperty::DisplayStdout); + setProperties(OutputExecuteJob::JobProperty::DisplayStderr); + setProperties(OutputExecuteJob::JobProperty::PostProcessOutput); + + *this << QStringLiteral("make"); + + if (GlobalSettings::parallelJobsEnabled()) { + const int threadsCount = + GlobalSettings::parallelJobsAutoCount() ? + QThread::idealThreadCount() : + GlobalSettings::parallelJobsFixedCount(); + + *this << QStringLiteral("-j%1").arg(threadsCount); + } + + *this << QStringLiteral("-f"); + *this << buildMakefile(params); +} + +Job::~Job() +{ + doKill(); +} + +QString Job::buildMakefile(const JobParameters& params) +{ + const auto makefilePath = QStringLiteral("%1/kdevclazy.makefile").arg(params.projectBuildPath()); + + QFile makefile(makefilePath); + makefile.open(QIODevice::WriteOnly); + + const auto clazySources = params.sources(); + const auto clazyCommand = params.commandLine().join(' '); + + QTextStream scriptStream(&makefile); + + scriptStream << QStringLiteral("SOURCES = %1\n").arg(clazySources.join(' ')); + scriptStream << QStringLiteral("COMMAND = @%2\n").arg(clazyCommand); + + scriptStream << QStringLiteral(".PHONY: all $(SOURCES)\n"); + scriptStream << QStringLiteral("all: $(SOURCES)\n"); + scriptStream << QStringLiteral("$(SOURCES):\n"); + + scriptStream << QStringLiteral("\t@echo 'Clazy check started for $@'\n"); + scriptStream << QStringLiteral("\t$(COMMAND) $@\n"); + scriptStream << QStringLiteral("\t@echo 'Clazy check finished for $@'\n"); + + makefile.close(); + + m_totalCount = clazySources.size(); + + return makefilePath; +} + +void Job::postProcessStdout(const QStringList& lines) +{ + static const auto startedRegex = QRegularExpression("Clazy check started for (.+)$"); + static const auto finishedRegex = QRegularExpression("Clazy check finished for (.+)$"); + + for (const QString & line : lines) { + auto match = startedRegex.match(line); + if (match.hasMatch()) { + emit infoMessage(this, match.captured(1)); + continue; + } + + match = finishedRegex.match(line); + if (match.hasMatch()) { + setPercent(++m_finishedCount/(double)m_totalCount * 100); + continue; + } + } + + m_standardOutput << lines; + + if (status() == KDevelop::OutputExecuteJob::JobStatus::JobRunning) { + OutputExecuteJob::postProcessStdout(lines); + } +} + +void Job::postProcessStderr(const QStringList& lines) +{ + static const auto errorRegex = QRegularExpression( + QStringLiteral("(.+):(\\d+):(\\d+):\\s+warning:\\s+(.+)\\s+\\[-Wclazy-(.+)\\]$")); + + QVector problems; + + for (const QString & line : lines) { + auto match = errorRegex.match(line); + if (match.hasMatch()) { + auto check = m_db ? m_db->checks().value(match.captured(5), nullptr) : nullptr; + + const QString levelName = check ? check->level->displayName : i18n("Unknown Level"); + KDevelop::IProblem::Ptr problem(new KDevelop::DetectedProblem(levelName)); + + problem->setSeverity(KDevelop::IProblem::Warning); + problem->setDescription(match.captured(4)); + if (check) { + problem->setExplanation(check->description); + } + + const int line = match.captured(2).toInt() - 1; + const int column = match.captured(3).toInt() - 1; + + // FIXME add KDevelop::IProblem::FinalLocationMode::ToEnd type ? + KTextEditor::Range range(line, column, line, 1000); + KDevelop::DocumentRange documentRange(KDevelop::IndexedString(match.captured(1)), range); + problem->setFinalLocation(documentRange); + problem->setFinalLocationMode(KDevelop::IProblem::Range); + + problems.append(problem); + } + } + m_stderrOutput << lines; + + if (problems.size()) { + emit problemsDetected(problems); + } + + if (status() == KDevelop::OutputExecuteJob::JobStatus::JobRunning) { + OutputExecuteJob::postProcessStderr(lines); + } +} + +void Job::start() +{ + m_standardOutput.clear(); + m_stderrOutput.clear(); + + qCDebug(KDEV_CLAZY) << "executing:" << commandLine().join(' '); + + m_timer->restart(); + setPercent(0); + m_finishedCount = 0; + + OutputExecuteJob::start(); +} + +void Job::childProcessError(QProcess::ProcessError e) +{ + QString message; + + switch (e) { + case QProcess::FailedToStart: + message = i18n("Failed to start Clazy analysis process."); + break; + + case QProcess::Crashed: + if (status() != KDevelop::OutputExecuteJob::JobStatus::JobCanceled) { + message = i18n("Clazy analysis process crashed."); + } + break; + + case QProcess::Timedout: + message = i18n("Clazy analysis process timed out."); + break; + + case QProcess::WriteError: + message = i18n("Write to Clazy analysis process failed."); + break; + + case QProcess::ReadError: + message = i18n("Read from Clazy analysis process failed."); + break; + + case QProcess::UnknownError: + // errors will be displayed in the output view ? + // don't notify the user + break; + } + + if (!message.isEmpty()) { + KMessageBox::error(qApp->activeWindow(), message, i18n("Clazy Error")); + } + + KDevelop::OutputExecuteJob::childProcessError(e); +} + +void Job::childProcessExited(int exitCode, QProcess::ExitStatus exitStatus) +{ + qCDebug(KDEV_CLAZY) << "Process Finished, exitCode" << exitCode << "process exit status" << exitStatus; + + setPercent(100); + postProcessStdout({QString("Elapsed time: %1 s.").arg(m_timer->elapsed()/1000.0)}); + + if (exitCode != 0) { + qCDebug(KDEV_CLAZY) << "clazy failed"; + qCDebug(KDEV_CLAZY) << "stdout output: "; + qCDebug(KDEV_CLAZY) << m_standardOutput.join('\n'); + qCDebug(KDEV_CLAZY) << "stderr output: "; + qCDebug(KDEV_CLAZY) << m_stderrOutput.join('\n'); + } + + KDevelop::OutputExecuteJob::childProcessExited(exitCode, exitStatus); +} + +} diff --git a/plugins/clazy/job_parameters.h b/plugins/clazy/job_parameters.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/job_parameters.h @@ -0,0 +1,127 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_JOB_PARAMETERS_H +#define KDEVCLAZY_JOB_PARAMETERS_H + +#include +#include +#include + +namespace KDevelop +{ + +class IProject; + +} + +namespace Clazy +{ + +class JobGlobalParameters : public QObject +{ + Q_OBJECT + +public: + JobGlobalParameters(const QUrl& executablePath, const QUrl& docsPath); + ~JobGlobalParameters() override; + + static QUrl defaultExecutablePath(); + static QUrl defaultDocsPath(); + + bool isValid() const; + QString error() const; + +Q_SIGNALS: + void changed(); + +protected: + JobGlobalParameters(); + + QString m_executablePath; + QString m_docsPath; + + QString m_error; +}; + +class JobParameters : public JobGlobalParameters +{ + Q_OBJECT + +public: + JobParameters(KDevelop::IProject* project); + JobParameters(KDevelop::IProject* project, const QString& checkPath); + ~JobParameters() override; + + static QString defaultChecks(); + + QString checkPath() const; + const QStringList& sources() const; + + QString projectBuildPath() const; + + QStringList commandLine() const; + + void setChecks(const QString& checks); + + void setOnlyQt(bool onlyQt); + void setQtDeveloper(bool qtDeveloper); + void setQt4Compat(bool qt4Compat); + void setVisitImplicitCode(bool visitImplicitCode); + + void setIgnoreIncludedFiles(bool ignoreIncludedFiles); + void setHeaderFilter(const QString& headerFilter); + + void setEnableAllFixits(bool enableAllFixits); + void setNoInplaceFixits(bool noInplaceFixits); + + void setExtraAppend(const QString& extraAppend); + void setExtraPrepend(const QString& extraPrepend); + void setExtraClazy(const QString& extraClazy); + +protected: + template + void setValue(T& currentValue, const T& newValue); + + QString m_checkPath; + QStringList m_sources; + + QString m_projectBuildPath; + + QString m_checks; + + bool m_onlyQt; + bool m_qtDeveloper; + bool m_qt4Compat; + bool m_visitImplicitCode; + + bool m_ignoreIncludedFiles; + QString m_headerFilter; + + bool m_enableAllFixits; + bool m_noInplaceFixits; + + QString m_extraAppend; + QString m_extraPrepend; + QString m_extraClazy; +}; + +} +#endif diff --git a/plugins/clazy/job_parameters.cpp b/plugins/clazy/job_parameters.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/job_parameters.cpp @@ -0,0 +1,366 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "job_parameters.h" + +#include "debug.h" +#include "globalsettings.h" +#include "projectsettings.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace Clazy +{ + +JobGlobalParameters::JobGlobalParameters() + : JobGlobalParameters(GlobalSettings::executablePath(), GlobalSettings::docsPath()) +{ +} + +JobGlobalParameters::JobGlobalParameters(const QUrl& executablePath, const QUrl& docsPath) +{ + m_executablePath = executablePath.toLocalFile(); + m_docsPath = docsPath.toLocalFile(); + + QFileInfo info; + + if (m_executablePath.isEmpty()) { + if (defaultExecutablePath().toLocalFile().isEmpty()) { + m_error = i18n( + "clazy-standalone path cannot be detected. " + "Set the path manually if clazy is already installed."); + } else { + m_error = i18n("clazy-standalone path is empty."); + } + return; + } + + info.setFile(m_executablePath); + + if (!info.exists()) { + m_error = i18n("clazy-standalone path '%1' does not exists.", m_executablePath); + return; + } + + if (!info.isFile() || !info.isExecutable()) { + m_error = i18n("clazy-standalone path '%1' is not an executable.", m_executablePath); + return; + } + + // ============================================================================================= + + if (m_docsPath.isEmpty()) { + if (defaultDocsPath().toLocalFile().isEmpty()) { + m_error = i18n( + "clazy docs path cannot be detected. " + "Set the path manually if clazy is already installed."); + } else { + m_error = i18n("clazy docs path is empty."); + } + return; + } + + info.setFile(m_docsPath); + + if (!info.exists()) { + m_error = i18n("clazy docs path '%1' does not exists", m_docsPath); + return; + } + + if (!info.isDir()) { + m_error = i18n("clazy docs path '%1' is not a directory", m_docsPath); + return; + } + + m_error.clear(); +} + +JobGlobalParameters::~JobGlobalParameters() +{ +} + +QUrl JobGlobalParameters::defaultExecutablePath() +{ + return QUrl::fromLocalFile(QStandardPaths::findExecutable(QStringLiteral("clazy-standalone"))); +} + +QUrl JobGlobalParameters::defaultDocsPath() +{ + const auto dataPaths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + for (const auto& dataPath : dataPaths) + { + QDir clazyDir(dataPath); + if (!clazyDir.cd(QStringLiteral("clazy/doc"))) { + continue; + } + + return QUrl::fromLocalFile(clazyDir.canonicalPath()); + } + + return {}; +} + +bool JobGlobalParameters::isValid() const +{ + return m_error.isEmpty(); +} + +QString JobGlobalParameters::error() const +{ + return m_error; +} + +JobParameters::JobParameters(KDevelop::IProject* project) + : JobParameters(project, QStringLiteral("")) +{ +} + +JobParameters::JobParameters(KDevelop::IProject* project, const QString& checkPath) + : m_checkPath(checkPath) +{ + Q_ASSERT(project); // FIXME this break tests + + auto projectRootPath = project->path().toLocalFile(); + + auto buildPath = project->buildSystemManager()->buildDirectory(project->projectItem()); + m_projectBuildPath = buildPath.toLocalFile(); + + buildPath.addPath(QStringLiteral("compile_commands.json")); + + auto commandsFilePath = buildPath.toLocalFile(); + if (!QFile::exists(commandsFilePath)) { + m_error = i18n("%1 does not exists", commandsFilePath); + return; + } + + if (!m_checkPath.isEmpty()) { + const auto allFiles = compileCommandsFiles(commandsFilePath, m_error); + if (!m_error.isEmpty()) { + return; + } + + if (m_checkPath == projectRootPath) { + m_sources = allFiles; + } else { + const bool checkPathIsFile = QFileInfo(m_checkPath).isFile(); + for (auto& file : allFiles) { + if (checkPathIsFile) { + if (file == m_checkPath) { + m_sources.clear(); + m_sources += m_checkPath; + break; + } + } else if (file.startsWith(m_checkPath)) { + m_sources += file; + } + } + } + } + + ProjectSettings projectSettings; + projectSettings.setSharedConfig(project->projectConfiguration()); + projectSettings.load(); + + m_checks = projectSettings.checks(); + + m_onlyQt = projectSettings.onlyQt(); + m_qtDeveloper = projectSettings.qtDeveloper(); + m_qt4Compat = projectSettings.qt4Compat(); + m_visitImplicitCode = projectSettings.visitImplicitCode(); + + m_ignoreIncludedFiles = projectSettings.ignoreIncludedFiles(); + m_headerFilter = projectSettings.headerFilter(); + + m_enableAllFixits = projectSettings.enableAllFixits(); + m_noInplaceFixits = projectSettings.noInplaceFixits(); + + m_extraAppend = projectSettings.extraAppend(); + m_extraPrepend = projectSettings.extraPrepend(); + m_extraClazy = projectSettings.extraClazy(); + + if (m_sources.isEmpty()) { + m_error = i18n("Nothing to check: %1 contains no matching items", commandsFilePath); + } +} + +JobParameters::~JobParameters() +{ +} + +QString JobParameters::defaultChecks() +{ + return QStringLiteral("level1"); +} + +QString JobParameters::checkPath() const +{ + return m_checkPath; +} + +const QStringList& JobParameters::sources() const +{ + return m_sources; +} + +QString JobParameters::projectBuildPath() const +{ + return m_projectBuildPath; +} + +QStringList JobParameters::commandLine() const +{ + QStringList arguments; + + arguments << m_executablePath; + + if (!m_checks.isEmpty()) { + arguments << QStringLiteral("-checks=%1").arg(m_checks); + } + + if (m_onlyQt) { + arguments << QStringLiteral("-only-qt"); + } + + if (m_qtDeveloper) { + arguments << QStringLiteral("-qt-developer"); + } + + if (m_qt4Compat) { + arguments << QStringLiteral("-qt4-compat"); + } + + if (m_visitImplicitCode) { + arguments << QStringLiteral("-visit-implicit-code"); + } + + if (m_ignoreIncludedFiles) { + arguments << QStringLiteral("-ignore-included-files"); + } + + if (!m_headerFilter.isEmpty()) { + arguments << QStringLiteral("-header-filter=%1").arg(m_headerFilter); + } + + if (m_enableAllFixits) { + arguments << QStringLiteral("-enable-all-fixits"); + } + + if (m_noInplaceFixits) { + arguments << QStringLiteral("-no-inplace-fixits"); + } + + if (!m_extraAppend.isEmpty()) { + arguments << QStringLiteral("-extra-arg=%1").arg(m_extraAppend); + } + + if (!m_extraPrepend.isEmpty()) { + arguments << QStringLiteral("-extra-arg-before=%1").arg(m_extraPrepend); + } + + if (!m_extraClazy.isEmpty()) { + arguments << KShell::splitArgs(m_extraClazy); + } + + arguments << QStringLiteral("-p"); + arguments << m_projectBuildPath; + + return arguments; +} + + +template +void JobParameters::setValue(T& currentValue, const T& newValue) +{ + if (currentValue != newValue) { + currentValue = newValue; + emit changed(); + } +} + +void JobParameters::setChecks(const QString& checks) +{ + setValue(m_checks, checks); +} + +void JobParameters::setOnlyQt(bool onlyQt) +{ + setValue(m_onlyQt, onlyQt); +} + +void JobParameters::setQtDeveloper(bool qtDeveloper) +{ + setValue(m_qtDeveloper, qtDeveloper); +} + +void JobParameters::setQt4Compat(bool qt4Compat) +{ + setValue(m_qt4Compat, qt4Compat); +} + +void JobParameters::setVisitImplicitCode(bool visitImplicitCode) +{ + setValue(m_visitImplicitCode, visitImplicitCode); +} + +void JobParameters::setIgnoreIncludedFiles(bool ignoreIncludedFiles) +{ + setValue(m_ignoreIncludedFiles, ignoreIncludedFiles); +} + +void JobParameters::setHeaderFilter(const QString& headerFilter) +{ + setValue(m_headerFilter, headerFilter); +} + +void JobParameters::setEnableAllFixits(bool enableAllFixits) +{ + setValue(m_enableAllFixits, enableAllFixits); +} + +void JobParameters::setNoInplaceFixits(bool noInplaceFixits) +{ + setValue(m_noInplaceFixits, noInplaceFixits); +} + +void JobParameters::setExtraAppend(const QString& extraAppend) +{ + setValue(m_extraAppend, extraAppend); +} + +void JobParameters::setExtraPrepend(const QString& extraPrepend) +{ + setValue(m_extraPrepend, extraPrepend); +} + +void JobParameters::setExtraClazy(const QString& extraClazy) +{ + setValue(m_extraClazy, extraClazy); +} + +} diff --git a/plugins/clazy/kdevclazy.json b/plugins/clazy/kdevclazy.json new file mode 100644 --- /dev/null +++ b/plugins/clazy/kdevclazy.json @@ -0,0 +1,26 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "Anton Anikin", + "Name[x-test]": "xxAnton Anikinxx" + } + ], + "Category": "Analyzers", + "Description": "This plugin integrates Clazy to KDevelop", + "Description[x-test]": "xxThis plugin integrates Clazy to KDevelopxx", + "Icon": "kdevelop", + "Id": "kdevclazy", + "License": "GPL", + "Name": "Clazy Support", + "Name[x-test]": "xxClazy Supportxx", + "ServiceTypes": [ + "KDevelop/Plugin" + ] + }, + "X-KDevelop-Category": "Global", + "X-KDevelop-IRequired": [ + "org.kdevelop.IExecutePlugin" + ], + "X-KDevelop-Mode": "GUI" +} diff --git a/plugins/clazy/kdevclazy.qrc b/plugins/clazy/kdevclazy.qrc new file mode 100644 --- /dev/null +++ b/plugins/clazy/kdevclazy.qrc @@ -0,0 +1,6 @@ + + + + kdevclazy.rc + + diff --git a/plugins/clazy/kdevclazy.rc b/plugins/clazy/kdevclazy.rc new file mode 100644 --- /dev/null +++ b/plugins/clazy/kdevclazy.rc @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/clazy/plugin.h b/plugins/clazy/plugin.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/plugin.h @@ -0,0 +1,96 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_PLUGIN_H +#define KDEVCLAZY_PLUGIN_H + +#include "job.h" + +#include + +class KJob; +class QMenu; + +namespace KDevelop +{ + class IProject; +} + +namespace Clazy +{ + +class ChecksDB; +class Job; +class ProblemModel; + +class Plugin : public KDevelop::IPlugin +{ + Q_OBJECT + +public: + Plugin(QObject* parent, const QVariantList& = QVariantList()); + + ~Plugin() override; + + int configPages() const override { return 1; } + KDevelop::ConfigPage* configPage(int number, QWidget* parent) override; + + int perProjectConfigPages() const override { return 1; } + KDevelop::ConfigPage* perProjectConfigPage(int number, const KDevelop::ProjectConfigOptions& options, QWidget* parent) override; + + KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context, QWidget* parent) override; + + void runClazy(KDevelop::IProject* project, const QString& path); + bool isRunning() const; + + QSharedPointer checksDB() const; + +private: + void killClazy(); + + void raiseProblemsView(); + void raiseOutputView(); + + void updateActions(); + void projectClosed(KDevelop::IProject* project); + + void runClazy(bool checkProject); + + void result(KJob* job); + + void reloadDB(); + + Job* m_job; + + KDevelop::IProject* m_project; + ProblemModel* m_model; + + QAction* m_menuActionFile; + QAction* m_menuActionProject; + QAction* m_contextActionFile; + QAction* m_contextActionProject; + QAction* m_contextActionProjectItem; + + QSharedPointer m_db; +}; + +} + +#endif diff --git a/plugins/clazy/plugin.cpp b/plugins/clazy/plugin.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/plugin.cpp @@ -0,0 +1,292 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "plugin.h" + +#include "checks_db.h" +#include "config/globalconfigpage.h" +#include "config/projectconfigpage.h" +#include "debug.h" +#include "globalsettings.h" +#include "problem_model.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(ClazyFactory, "kdevclazy.json", registerPlugin();) + +namespace Clazy +{ + +Plugin::Plugin(QObject* parent, const QVariantList&) + : IPlugin("kdevclazy", parent) + , m_job(nullptr) + , m_project(nullptr) + , m_model(new ProblemModel(this)) + , m_db(nullptr) +{ + setXMLFile("kdevclazy.rc"); + + reloadDB(); + + m_menuActionFile = new QAction(i18n("Analyze Current File with Clazy"), this); + connect(m_menuActionFile, &QAction::triggered, this, [this](){ + runClazy(false); + }); + actionCollection()->addAction("clazy_file", m_menuActionFile); + + m_contextActionFile = new QAction(i18n("Clazy"), this); + connect(m_contextActionFile, &QAction::triggered, this, [this]() { + runClazy(false); + }); + + m_menuActionProject = new QAction(i18n("Analyze Current Project with Clazy"), this); + connect(m_menuActionProject, &QAction::triggered, this, [this](){ + runClazy(true); + }); + actionCollection()->addAction("clazy_project", m_menuActionProject); + + m_contextActionProject = new QAction(i18n("Clazy"), this); + connect(m_contextActionProject, &QAction::triggered, this, [this]() { + runClazy(true); + }); + + m_contextActionProjectItem = new QAction("Clazy", this); + + connect(core()->documentController(), &KDevelop::IDocumentController::documentClosed, + this, &Plugin::updateActions); + + connect(core()->documentController(), &KDevelop::IDocumentController::documentActivated, + this, &Plugin::updateActions); + + connect(core()->projectController(), &KDevelop::IProjectController::projectOpened, + this, &Plugin::updateActions); + + connect(core()->projectController(), &KDevelop::IProjectController::projectClosed, + this, &Plugin::projectClosed); + + updateActions(); +} + +Plugin::~Plugin() +{ + killClazy(); +} + +bool Plugin::isRunning() const +{ + return m_job; +} + +void Plugin::killClazy() +{ + if (m_job) { + m_job->kill(KJob::EmitResult); + } +} + +void Plugin::raiseProblemsView() +{ + m_model->show(); +} + +void Plugin::raiseOutputView() +{ + core()->uiController()->findToolView( + i18ndc("kdevstandardoutputview", "@title:window", "Test"), + nullptr, + KDevelop::IUiController::FindFlags::Raise); +} + +void Plugin::updateActions() +{ + m_project = nullptr; + + m_menuActionFile->setEnabled(false); + m_menuActionProject->setEnabled(false); + + if (isRunning()) { + return; + } + + auto activeDocument = core()->documentController()->activeDocument(); + if (!activeDocument) { + return; + } + + m_project = core()->projectController()->findProjectForUrl(activeDocument->url()); + if (!m_project) { + return; + } + + m_menuActionFile->setEnabled(true); + m_menuActionProject->setEnabled(true); +} + +void Plugin::projectClosed(KDevelop::IProject* project) +{ + if (project != m_model->project()) { + return; + } + + killClazy(); + m_model->reset(); +} + +void Plugin::runClazy(bool checkProject) +{ + auto doc = core()->documentController()->activeDocument(); + Q_ASSERT(doc); + + if (checkProject) { + runClazy(m_project, m_project->path().toUrl().toLocalFile()); + } else { + runClazy(m_project, doc->url().toLocalFile()); + } +} + +void Plugin::runClazy(KDevelop::IProject* project, const QString& path) +{ + JobParameters params(project, path); + + if (!params.isValid()) { + QString errorMessage; + errorMessage += i18n("Unable to start clazy check for \"%1\":", path); + errorMessage += QStringLiteral("\n\n"); + errorMessage += params.error(); + KMessageBox::error(qApp->activeWindow(), errorMessage, i18n("Clazy Error")); + return; + } + + m_model->reset(project, path); + + m_job = new Job(params, m_db); + + 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, "clazy")); + core()->runController()->registerJob(m_job); + + if (GlobalSettings::hideOutputView()) { + raiseProblemsView(); + } else { + raiseOutputView(); + } + + updateActions(); +} + +void Plugin::result(KJob*) +{ + if (!core()->projectController()->projects().contains(m_model->project())) { + m_model->reset(); + } else { + m_model->setProblems(); + + if (m_job->status() == KDevelop::OutputExecuteJob::JobStatus::JobSucceeded || + m_job->status() == KDevelop::OutputExecuteJob::JobStatus::JobCanceled) { + + raiseProblemsView(); + } else { + raiseOutputView(); + } + } + + m_job = nullptr; // job is automatically deleted later + + updateActions(); +} + +KDevelop::ContextMenuExtension Plugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) +{ + Q_UNUSED(parent); + KDevelop::ContextMenuExtension extension; + + if (context->hasType(KDevelop::Context::EditorContext) && m_project && !isRunning()) { + 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().constFirst(); + 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](){ + runClazy(item->project(), item->path().toLocalFile()); + }); + + extension.addAction(KDevelop::ContextMenuExtension::AnalyzeProjectGroup, m_contextActionProjectItem); + } + + return extension; +} + +KDevelop::ConfigPage* Plugin::perProjectConfigPage(int number, const KDevelop::ProjectConfigOptions& options, QWidget* parent) +{ + return number ? nullptr : new ProjectConfigPage(this, options.project, parent); +} + +KDevelop::ConfigPage* Plugin::configPage(int number, QWidget* parent) +{ + return number ? nullptr : new GlobalConfigPage(this, parent); +} + +QSharedPointer Plugin::checksDB() const +{ + return m_db; +} + +void Plugin::reloadDB() +{ + m_db.reset(new ChecksDB(GlobalSettings::docsPath())); + connect(GlobalSettings::self(), &GlobalSettings::docsPathChanged, this, &Plugin::reloadDB); +} + +} + +#include "plugin.moc" diff --git a/plugins/clazy/problem_model.h b/plugins/clazy/problem_model.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/problem_model.h @@ -0,0 +1,69 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_PROBLEM_MODEL_H +#define KDEVCLAZY_PROBLEM_MODEL_H + +#include + +namespace KDevelop +{ + class IProject; +} + +namespace Clazy +{ + +class Plugin; + +class ProblemModel : public KDevelop::ProblemModel +{ + Q_OBJECT + +public: + explicit ProblemModel(Plugin* plugin); + ~ProblemModel() override; + + KDevelop::IProject* project() const; + + void addProblems(const QVector& problems); + + void setProblems(); + using KDevelop::ProblemModel::setProblems; + + void reset(); + void reset(KDevelop::IProject* project, const QString& path); + + void show(); + + void forceFullUpdate() override; + +private: + Plugin* m_plugin; + + KDevelop::IProject* m_project; + QString m_path; + + QVector m_problems; +}; + +} + +#endif diff --git a/plugins/clazy/problem_model.cpp b/plugins/clazy/problem_model.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/problem_model.cpp @@ -0,0 +1,129 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "problem_model.h" + +#include "plugin.h" +#include "utils.h" + +#include +#include +#include +#include + +namespace Clazy +{ + +inline KDevelop::ProblemModelSet* problemModelSet() +{ + return KDevelop::ICore::self()->languageController()->problemModelSet(); +} + +inline QString problemModelId() +{ + return QStringLiteral("clazy"); +} + +ProblemModel::ProblemModel(Plugin* plugin) + : KDevelop::ProblemModel(plugin) + , m_plugin(plugin) + , m_project(nullptr) +{ + setFeatures(CanDoFullUpdate | + ScopeFilter | + SeverityFilter | + Grouping | + CanByPassScopeFilter| + ShowSource); + + reset(); + + problemModelSet()->addModel(problemModelId(), i18n("Clazy"), this); +} + +ProblemModel::~ProblemModel() +{ + problemModelSet()->removeModel(problemModelId()); +} + +KDevelop::IProject* ProblemModel::project() const +{ + return m_project; +} + +void ProblemModel::addProblems(const QVector& problems) +{ + static int maxLength = 0; + + if (m_problems.isEmpty()) { + maxLength = 0; + } + + m_problems.append(problems); + for (auto p : problems) { + addProblem(p); + + // This performs adjusting of columns width in the ProblemsView + if (maxLength < p->description().length()) { + maxLength = p->description().length(); + setProblems(m_problems); + break; + } + } +} + +void ProblemModel::setProblems() +{ + 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 Clazy Analysis"); + if (m_project) { + tooltip += QString(" (%1)").arg(prettyPathName(m_path)); + } + setFullUpdateTooltip(tooltip); +} + +void ProblemModel::show() +{ + problemModelSet()->showModel(problemModelId()); +} + +void ProblemModel::forceFullUpdate() +{ + if (m_project && !m_plugin->isRunning()) { + m_plugin->runClazy(m_project, m_path); + } +} + +} diff --git a/plugins/clazy/tests/CMakeLists.txt b/plugins/clazy/tests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/clazy/tests/CMakeLists.txt @@ -0,0 +1,6 @@ +ecm_add_test( + test_clazy_job.cpp + + TEST_NAME test_clazy_job + LINK_LIBRARIES kdevclazy_core Qt5::Test KDev::Tests +) diff --git a/plugins/clazy/tests/test_clazy_job.h b/plugins/clazy/tests/test_clazy_job.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/tests/test_clazy_job.h @@ -0,0 +1,37 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_JOB_TEST_H +#define KDEVCLAZY_JOB_TEST_H + +#include + +class TestClazyJob : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void testJob(); +}; + +#endif diff --git a/plugins/clazy/tests/test_clazy_job.cpp b/plugins/clazy/tests/test_clazy_job.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/tests/test_clazy_job.cpp @@ -0,0 +1,168 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "test_clazy_job.h" + +#include "job.h" +#include "job_parameters.h" + +#include +#include +#include + +#include + + +using namespace KDevelop; +using namespace Clazy; + +class JobTester : public Job +{ + Q_OBJECT + +public: + JobTester() + { + connect(this, &JobTester::problemsDetected, + this, [this](const QVector& problems) { + m_problems += problems; + }); + } + + using Job::postProcessStdout; + using Job::postProcessStderr; + + QVector problems() const + { + return m_problems; + } + + int finishedCount() const + { + return m_finishedCount; + } + +protected: + QVector m_problems; +}; + +void TestClazyJob::initTestCase() +{ + AutoTestShell::init({"kdevclazy"}); + TestCore::initialize(Core::NoUi); +} + +void TestClazyJob::cleanupTestCase() +{ + TestCore::shutdown(); +} + +void TestClazyJob::testJob() +{ + JobTester jobTester; + + // test progress parsing ======================================================================= + + static const QStringList stdoutOutput1 = { + QStringLiteral("Clazy check started for source1.cpp"), + QStringLiteral("Clazy check started for source2.cpp"), + QStringLiteral("Clazy check started for source3.cpp") + }; + + static const QStringList stdoutOutput2 = { + QStringLiteral("Clazy check finished for source2.cpp"), + QStringLiteral("Clazy check started for source4.cpp") + }; + + static const QStringList stdoutOutput3 = { + QStringLiteral("Clazy check finished for source1.cpp"), + QStringLiteral("Clazy check finished for source3.cpp"), + QStringLiteral("Clazy check finished for source4.cpp") + }; + + jobTester.postProcessStdout(stdoutOutput1); + QCOMPARE(jobTester.finishedCount(), 0); + + jobTester.postProcessStdout(stdoutOutput2); + QCOMPARE(jobTester.finishedCount(), 1); + + jobTester.postProcessStdout(stdoutOutput3); + QCOMPARE(jobTester.finishedCount(), 4); + + // test errors parsing ========================================================================= + + static const QStringList stderrOutput1 = { + QStringLiteral("source2.cpp:13:10: warning: unused variable 'check' [-Wunused-variable]"), + QStringLiteral(" auto check = db.checks()[\"returning-void-expression\"];") + }; + + static const QStringList stderrOutput2 = { + QStringLiteral("source3.cpp:248:21: warning: Don't call QList::first() on temporary [-Wclazy-detaching-temporary]"), + QStringLiteral(" auto item = pContext->items().first();"), + QStringLiteral(" ^"), + QStringLiteral("1 warning generated.") + }; + + static const QStringList stderrOutput3 = { + QStringLiteral("source4.cpp:47:9: warning: unused QString [-Wclazy-unused-non-trivial-variable]"), + QStringLiteral(" auto test = QString(\"%1 : %2\").arg(\"a\").arg(\"b\");"), + QStringLiteral(" ^"), + QStringLiteral("source4.cpp:47:47: warning: Use multi-arg instead [-Wclazy-qstring-arg]"), + QStringLiteral(" auto test = QString(\"%1 : %2\").arg(\"a\").arg(\"b\");"), + QStringLiteral(" ^"), + QStringLiteral("2 warnings generated.") + }; + + jobTester.postProcessStderr(stderrOutput1); + QCOMPARE(jobTester.problems().size(), 0); + + jobTester.postProcessStderr(stderrOutput2); + QCOMPARE(jobTester.problems().size(), 1); + + jobTester.postProcessStderr(stderrOutput3); + QCOMPARE(jobTester.problems().size(), 3); + + + // test common values + auto problems = jobTester.problems(); + foreach (auto problem, problems) { + QCOMPARE(problem->severity(), KDevelop::IProblem::Warning); + QCOMPARE(problem->source(), KDevelop::IProblem::Plugin); + } + + // test problem location (file) + QCOMPARE(problems[0]->finalLocation().document.str(), QStringLiteral("source3.cpp")); + QCOMPARE(problems[1]->finalLocation().document.str(), QStringLiteral("source4.cpp")); + QCOMPARE(problems[2]->finalLocation().document.str(), QStringLiteral("source4.cpp")); + + // test problem location (line) + QCOMPARE(problems[0]->finalLocation().start().line(), 247); + QCOMPARE(problems[1]->finalLocation().start().line(), 46); + QCOMPARE(problems[2]->finalLocation().start().line(), 46); + + // test problem location (column) + QCOMPARE(problems[0]->finalLocation().start().column(), 20); + QCOMPARE(problems[1]->finalLocation().start().column(), 8); + QCOMPARE(problems[2]->finalLocation().start().column(), 46); +} + +QTEST_GUILESS_MAIN(TestClazyJob) + +#include "test_clazy_job.moc" diff --git a/plugins/clazy/utils.h b/plugins/clazy/utils.h new file mode 100644 --- /dev/null +++ b/plugins/clazy/utils.h @@ -0,0 +1,37 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KDEVCLAZY_UTILS_H +#define KDEVCLAZY_UTILS_H + +#include + +namespace Clazy +{ + +QString prettyPathName(const QString& path); + +QStringList compileCommandsFiles(const QString& jsonFilePath, QString& error); + +QString markdown2html(const QByteArray& markdown); + +} + +#endif diff --git a/plugins/clazy/utils.cpp b/plugins/clazy/utils.cpp new file mode 100644 --- /dev/null +++ b/plugins/clazy/utils.cpp @@ -0,0 +1,242 @@ +/* This file is part of KDevelop + + Copyright 2018 Anton Anikin + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "utils.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Clazy +{ + +QString prettyPathName(const QString& path) +{ + return KDevelop::ICore::self()->projectController()->prettyFileName( + QUrl::fromLocalFile(path), + KDevelop::IProjectController::FormatPlain); +} + +QStringList compileCommandsFiles(const QString& jsonFilePath, QString& error) +{ + QStringList paths; + + QFile jsonFile(jsonFilePath); + if (!jsonFile.open(QFile::ReadOnly | QFile::Text)) { + error = i18n("Unable to open %1 for reading", jsonFilePath); + return paths; + } + + QJsonParseError jsonError; + auto document = QJsonDocument::fromJson(jsonFile.readAll(), &jsonError); + + if (jsonError.error) { + error = i18n("JSON error during parsing %1: %2", jsonFilePath, jsonError.errorString()); + return paths; + } + + if (!document.isArray()) { + error = i18n("JSON error during parsing %1: document is not an array", jsonFilePath); + return paths; + } + + static const QString KEY_FILE = QStringLiteral("file"); + + const auto array = document.array(); + for (const auto& value : array) { + if (!value.isObject()) { + continue; + } + + const QJsonObject entry = value.toObject(); + if (entry.contains(KEY_FILE)) { + auto path = entry[KEY_FILE].toString(); + if (QFile::exists(path)) + { + paths += path; + } + } + } + + return paths; +} + +// Very simple Markdown parser/converter. Does not provide full Markdown language support and +// was tested only with Clazy documentation. +class MarkdownConverter +{ +public: + MarkdownConverter() + { + tagStart.resize(STATE_COUNT); + tagEnd.resize(STATE_COUNT); + + tagStart[EMPTY] = tagEnd[EMPTY] = QStringLiteral(""); + + tagStart[HEADING] = QStringLiteral(""); + tagEnd [HEADING] = QStringLiteral(""); + + tagStart[PARAGRAPH] = QStringLiteral("

"); + tagEnd [PARAGRAPH] = QStringLiteral("

"); + + tagStart[PREFORMATTED] = QStringLiteral("
");
+        tagEnd  [PREFORMATTED] = QStringLiteral("
"); + + tagStart[LIST] = QStringLiteral("
  • "); + tagEnd [LIST] = QStringLiteral("
"); + } + + ~MarkdownConverter() + { + } + + QString toHtml(const QString& markdown) + { + const QRegularExpression hRE(QStringLiteral("(#+) (.+)")); + QRegularExpressionMatch match; + + state = EMPTY; + html.clear(); + html += QStringLiteral(""); + + auto lines = markdown.split('\n'); + for (auto line : lines) { + if (line.isEmpty()) { + setState(EMPTY); + continue; + } + + if (line.startsWith("#")) { + auto match = hRE.match(line); + if (match.hasMatch()) { + setState(HEADING); + html += match.captured(2); + setState(EMPTY); + if (match.captured(1).size() == 1) { + html += QStringLiteral("
"); + } + } + continue; + } + + if (line.startsWith(QStringLiteral("```"))) { + setState((state == PREFORMATTED) ? EMPTY : PREFORMATTED); + continue; + } + + if (line.startsWith(QStringLiteral(" "))) { + if (state == EMPTY) { + setState(PREFORMATTED); + } + } else if ( + line.startsWith(QStringLiteral("- ")) || + line.startsWith(QStringLiteral("* "))) { + // force close and reopen list - this fixes cases when we don't have + // separator line between items + setState(EMPTY); + setState(LIST); + line = line.mid(2); + } + + if (state == EMPTY) { + setState(PARAGRAPH); + } + + processLine(line); + } + setState(EMPTY); + + html += QStringLiteral(""); + return html.join('\n'); + } + +protected: + enum STATE { + EMPTY, + HEADING, + PARAGRAPH, + PREFORMATTED, + LIST, + + STATE_COUNT + }; + + void setState(int newState) + { + if (state == newState) { + return; + } + + if (state != EMPTY) { + html += tagEnd[state]; + } + + if (newState != EMPTY) { + html += tagStart[newState]; + } + + state = newState; + } + + void processLine(QString& line) + { + static const QRegularExpression ttRE(QStringLiteral("`([^`]+)`")); + static const QRegularExpression bdRE(QStringLiteral("\\*\\*([^\\*]+)\\*\\*")); + static const QRegularExpression itRE(QStringLiteral("[^\\*]\\*([^\\*]+)\\*[^\\*]")); + + static auto applyRE = [](const QRegularExpression& re, QString& line, const QString& tag) { + auto i = re.globalMatch(line); + while (i.hasNext()) { + auto match = i.next(); + line.replace(match.captured(0), QStringLiteral("<%1>%2").arg(tag, match.captured(1))); + } + }; + + if (state != PREFORMATTED) { + applyRE(ttRE, line, QStringLiteral("tt")); + applyRE(bdRE, line, QStringLiteral("b")); + applyRE(itRE, line, QStringLiteral("i")); + } + + html += line; + } + + int state; + QVector tagStart; + QVector tagEnd; + QStringList html; +}; + +QString markdown2html(const QByteArray& markdown) +{ + MarkdownConverter converter; + return converter.toHtml(markdown); +} + +}