diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 742e5e953f..ce9b8b0db2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,65 +1,66 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevclangtidy\") include_directories( ${Boost_INCLUDE_DIRS} ) set(kdevclangtidy_PART_SRCS ${kdevclangtidy_LOG_SRCS} job.cpp plugin.cpp checkset.cpp + problemmodel.cpp utils.cpp config/clangtidyprojectconfigpage.cpp config/clangtidypreferences.cpp config/checkselection.cpp config/checklistfilterproxysearchline.cpp config/checklistitemproxystyle.cpp config/checklistmodel.cpp config/checkgroup.cpp parsers/clangtidyparser.cpp # disable for now: # CentOS used for appimage does not have string_ref.hpp (Boost >= 1.53) as used by current code # parsers/replacementparser.cpp ) ki18n_wrap_ui(kdevclangtidy_PART_SRCS config/clangtidypreferences.ui config/clangtidyprojectconfigpage.ui ) qt5_add_resources(kdevclangtidy_PART_SRCS kdevclangtidy.qrc ) kconfig_add_kcfg_files(kdevclangtidy_PART_SRCS config/clangtidyconfig.kcfgc config/clangtidyprojectconfig.kcfgc ) kdevplatform_add_plugin(kdevclangtidy JSON kdevclangtidy.json SOURCES ${kdevclangtidy_PART_SRCS} ) target_link_libraries(kdevclangtidy KDev::Interfaces KDev::Project KDev::Language KDev::OutputView KDev::Util KDev::Shell KF5::ItemViews KF5::ConfigCore KF5::I18n ) if(Qt5_VERSION VERSION_LESS "5.10.0") target_link_libraries(kdevclangtidy KF5::ItemModels ) endif() install(FILES org.kde.kdev-clang-tidy.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) diff --git a/src/job.cpp b/src/job.cpp index eea4543ae2..cc9f6da798 100644 --- a/src/job.cpp +++ b/src/job.cpp @@ -1,284 +1,278 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * Copyright 2018 Friedrich W. H. Kossebau * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "job.h" // plugin -#include "parsers/clangtidyparser.h" #include // KF #include #include // Qt #include #include #include namespace ClangTidy { // uses ' for quoting QString inlineYaml(const Job::Parameters& parameters) { QString result; result.append(QLatin1String("{Checks: '") + parameters.enabledChecks + QLatin1Char('\'')); if (!parameters.headerFilter.isEmpty()) { // TODO: the regex might need escpaing for potential quotes of all kinds result.append(QLatin1String(", HeaderFilterRegex: '") + parameters.headerFilter + QLatin1Char('\'')); } result.append(QLatin1Char('}')); return result; } // uses " for quoting QStringList commandLineArgs(const Job::Parameters& parameters) { QStringList args{ parameters.executablePath, QLatin1String("-p=\"") + parameters.buildDir + QLatin1Char('\"'), // don't add statistics we are not interested in to parse anyway QStringLiteral("-quiet"), }; if (!parameters.additionalParameters.isEmpty()) { args << parameters.additionalParameters; } if (parameters.checkSystemHeaders) { args << QStringLiteral("--system-headers"); } if (!parameters.useConfigFile) { args << QLatin1String("--config=\"") + inlineYaml(parameters) + QLatin1Char('\"'); } return args; } Job::Job(const Parameters& params, QObject* parent) : KDevelop::OutputExecuteJob(parent) , m_parameters(params) { setJobName(i18n("Clang-Tidy Analysis")); setCapabilities(KJob::Killable); setStandardToolView(KDevelop::IOutputView::TestView); setBehaviours(KDevelop::IOutputView::AutoScroll); setProperties(KDevelop::OutputExecuteJob::JobProperty::DisplayStdout | KDevelop::OutputExecuteJob::JobProperty::DisplayStderr | KDevelop::OutputExecuteJob::JobProperty::PostProcessOutput); m_totalCount = params.filePaths.size(); + connect(&m_parser, &ClangTidyParser::problemsDetected, + this, &Job::problemsDetected); + // TODO: check success of creation generateMakefile(); *this << QStringList{ QStringLiteral("make"), QStringLiteral("-f"), m_makeFilePath, }; qCDebug(KDEV_CLANGTIDY) << "checking files" << params.filePaths; } Job::~Job() { doKill(); if (!m_makeFilePath.isEmpty()) { QFile::remove(m_makeFilePath); } } void Job::generateMakefile() { m_makeFilePath = m_parameters.buildDir + QLatin1String("/kdevclangtidy.makefile"); QFile makefile(m_makeFilePath); makefile.open(QIODevice::WriteOnly); QTextStream scriptStream(&makefile); scriptStream << QStringLiteral("SOURCES ="); for (const auto& source : qAsConst(m_parameters.filePaths)) { // TODO: how to escape " in a filename, for those people who like to go extreme? scriptStream << QLatin1String(" \\\n\t\"") + source + QLatin1Char('\"'); } scriptStream << QLatin1Char('\n'); scriptStream << QStringLiteral("COMMAND ="); const auto commandLine = commandLineArgs(m_parameters); for (const auto& commandPart : commandLine) { scriptStream << QLatin1Char(' ') << commandPart; } scriptStream << QLatin1Char('\n'); scriptStream << QStringLiteral(".PHONY: all $(SOURCES)\n"); scriptStream << QStringLiteral("all: $(SOURCES)\n"); scriptStream << QStringLiteral("$(SOURCES):\n"); scriptStream << QStringLiteral("\t@echo 'Clang-Tidy check started for $@'\n"); scriptStream << QStringLiteral("\t$(COMMAND) $@\n"); scriptStream << QStringLiteral("\t@echo 'Clang-Tidy check finished for $@'\n"); makefile.close(); } void Job::processStdoutLines(const QStringList& lines) { static const auto startedRegex = QRegularExpression(QStringLiteral("Clang-Tidy check started for (.+)$")); static const auto finishedRegex = QRegularExpression(QStringLiteral("Clang-Tidy check finished for (.+)$")); for (const auto& line : lines) { auto match = startedRegex.match(line); if (match.hasMatch()) { emit infoMessage(this, match.captured(1)); continue; } match = finishedRegex.match(line); if (match.hasMatch()) { ++m_finishedCount; setPercent(static_cast(m_finishedCount)/m_totalCount * 100); continue; } } + m_parser.addData(lines); m_standardOutput << lines; } void Job::processStderrLines(const QStringList& lines) { static const auto xmlStartRegex = QRegularExpression(QStringLiteral("\\s*<")); for (const QString& line : lines) { // unfortunately sometime clangtidy send non-XML messages to stderr. // For example, if we pass '-I /missing_include_dir' to the argument list, // then stderr output will contains such line (tested on clangtidy 1.72): // // (information) Couldn't find path given by -I '/missing_include_dir' // // Therefore we must 'move' such messages to m_standardOutput. if (line.indexOf(xmlStartRegex) != -1) { // the line contains XML m_xmlOutput << line; } else { m_standardOutput << line; } } } void Job::postProcessStdout(const QStringList& lines) { processStdoutLines(lines); KDevelop::OutputExecuteJob::postProcessStdout(lines); } void Job::postProcessStderr(const QStringList& lines) { processStderrLines(lines); KDevelop::OutputExecuteJob::postProcessStderr(lines); } void Job::start() { m_standardOutput.clear(); m_xmlOutput.clear(); qCDebug(KDEV_CLANGTIDY) << "executing:" << commandLine().join(QLatin1Char(' ')); setPercent(0); m_finishedCount = 0; KDevelop::OutputExecuteJob::start(); } -QVector Job::problems() const -{ - return m_problems; -} - void Job::childProcessError(QProcess::ProcessError processError) { QString message; switch (processError) { case QProcess::FailedToStart: { message = i18n("Failed to start Clang-Tidy process."); break; } case QProcess::Crashed: message = i18n("Clang-tidy crashed."); break; case QProcess::Timedout: message = i18n("Clang-tidy process timed out."); break; case QProcess::WriteError: message = i18n("Write to Clang-tidy process failed."); break; case QProcess::ReadError: message = i18n("Read from Clang-tidy process failed."); break; case QProcess::UnknownError: // current clangtidy errors will be displayed in the output view // don't notify the user break; } if (!message.isEmpty()) { KMessageBox::error(qApp->activeWindow(), message, i18n("Clang-tidy Error")); } KDevelop::OutputExecuteJob::childProcessError(processError); } void Job::childProcessExited(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(KDEV_CLANGTIDY) << "Process Finished, exitCode" << exitCode << "process exit status" << exitStatus; setPercent(100); if (exitCode != 0) { qCDebug(KDEV_CLANGTIDY) << "clang-tidy failed, standard output: "; qCDebug(KDEV_CLANGTIDY) << m_standardOutput.join(QLatin1Char('\n')); qCDebug(KDEV_CLANGTIDY) << "clang-tidy failed, XML output: "; qCDebug(KDEV_CLANGTIDY) << m_xmlOutput.join(QLatin1Char('\n')); - } else { - ClangTidyParser parser; - parser.addData(m_standardOutput); - parser.parse(); - m_problems = parser.problems(); } KDevelop::OutputExecuteJob::childProcessExited(exitCode, exitStatus); } + } // namespace ClangTidy diff --git a/src/job.h b/src/job.h index 2f1b157ccc..0d2862fc99 100644 --- a/src/job.h +++ b/src/job.h @@ -1,92 +1,96 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef CLANGTIDY_JOB_H #define CLANGTIDY_JOB_H // plugin +#include "parsers/clangtidyparser.h" #include // KDevPlatform #include #include namespace ClangTidy { /** * \class * \brief specializes a KJob for running clang-tidy. */ class Job : public KDevelop::OutputExecuteJob { Q_OBJECT public: /** * \class * \brief command line parameters. */ struct Parameters { QString projectRootDir; QString executablePath; QStringList filePaths; QString buildDir; QString additionalParameters; QString enabledChecks; bool useConfigFile; QString headerFilter; bool checkSystemHeaders; }; explicit Job(const Parameters& params, QObject* parent = nullptr); ~Job() override; public: // KJob API void start() override; -public: - QVector problems() const; +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: void processStdoutLines(const QStringList& lines); void processStderrLines(const QStringList& lines); private: void generateMakefile(); protected: + ClangTidyParser m_parser; QStringList m_standardOutput; QStringList m_xmlOutput; const Job::Parameters m_parameters; QString m_makeFilePath; int m_finishedCount = 0; int m_totalCount = 0; QVector m_problems; }; + } + #endif diff --git a/src/parsers/clangtidyparser.cpp b/src/parsers/clangtidyparser.cpp index 78f3ec9645..35b518318a 100644 --- a/src/parsers/clangtidyparser.cpp +++ b/src/parsers/clangtidyparser.cpp @@ -1,106 +1,112 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "clangtidyparser.h" // plugin #include // KDevPlatform #include #include #include namespace ClangTidy { using KDevelop::IProblem; using KDevelop::DetectedProblem; using KDevelop::DocumentRange; using KDevelop::IndexedString; /** * Convert the value of attribute of element from clang-tidy's * XML-output to 'good-looking' HTML-version. This is necessary because the * displaying of the original message is performed without line breaks - such * tooltips are uncomfortable to read, and large messages will not fit into the * screen. * * This function put the original message into \ tag that automatically * provides line wrapping by builtin capabilities of Qt library. The source text * also can contain tokens '\012' (line break) - they are present in the case of * source code examples. In such cases, the entire text between the first and * last tokens (i.e. source code) is placed into \ tag. * * @param[in] input the original value of attribute * @return HTML version for displaying in problem's tooltip */ QString verboseMessageToHtml(const QString& input) { QString output(QStringLiteral("%1").arg(input.toHtmlEscaped())); output.replace(QStringLiteral("\\012"), QStringLiteral("\n")); if (output.count(QLatin1Char('\n')) >= 2) { output.replace(output.indexOf(QLatin1Char('\n')), 1, QStringLiteral("
"));
         output.replace(output.lastIndexOf(QLatin1Char('\n')), 1, QStringLiteral("

")); } return output; } ClangTidyParser::ClangTidyParser(QObject* parent) : QObject(parent) // (1filename ) (2lin) (3col) (4s) (5d) (6explain) , m_hitRegExp(QStringLiteral("(\\/.+\\.[ch]{1,2}[px]{0,2}):(\\d+):(\\d+): (.+): (.+) (\\[.+\\])")) { } -void ClangTidyParser::parse() +void ClangTidyParser::addData(const QStringList& stdoutList) { - for (const auto& line : qAsConst(m_stdout)) { + QVector problems; + + for (const auto& line : qAsConst(stdoutList)) { auto smatch = m_hitRegExp.match(line); if (!smatch.hasMatch()) { continue; } IProblem::Ptr problem(new DetectedProblem()); problem->setSource(IProblem::Plugin); problem->setDescription(smatch.captured(5)); problem->setExplanation(smatch.captured(6)); DocumentRange range; range.document = IndexedString(smatch.captured(1)); range.setBothColumns(smatch.capturedRef(3).toInt() - 1); range.setBothLines(smatch.capturedRef(2).toInt() - 1); problem->setFinalLocation(range); const auto sev = smatch.capturedRef(4); const IProblem::Severity erity = (sev == QLatin1String("error")) ? IProblem::Error : (sev == QLatin1String("warning")) ? IProblem::Warning : (sev == QLatin1String("note")) ? IProblem::Hint : /* else */ IProblem::NoSeverity; problem->setSeverity(erity); - m_problems.append(problem); + problems.append(problem); + } + + if (!problems.isEmpty()) { + emit problemsDetected(problems); } } } // namespace ClangTidy diff --git a/src/parsers/clangtidyparser.h b/src/parsers/clangtidyparser.h index e71fa6062e..76209b948e 100644 --- a/src/parsers/clangtidyparser.h +++ b/src/parsers/clangtidyparser.h @@ -1,65 +1,61 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef _CLANGTIDYPARSER_H_ #define _CLANGTIDYPARSER_H_ // KDevPlatform #include // Qt #include #include namespace ClangTidy { /** * \class * \brief Implements a parser for clang-tidy's standard output. */ class ClangTidyParser : public QObject { + Q_OBJECT + public: explicit ClangTidyParser(QObject* parent = nullptr); ~ClangTidyParser() = default; public: - /** - * \return all problems collected from clang-tidy stdout. - */ - QVector problems() const { return m_problems; } - - void parse(); - /** * \brief meant to be used by Job class to pass the standard output to be parsed. */ - void addData(const QStringList& stdoutList) { m_stdout = stdoutList; } + void addData(const QStringList& stdoutList); + +Q_SIGNALS: + void problemsDetected(const QVector& problems); private: const QRegularExpression m_hitRegExp; - QVector m_problems; ///< problems collected from clang-tidy stdout. - QStringList m_stdout; }; } // namespace ClangTidy #endif diff --git a/src/plugin.cpp b/src/plugin.cpp index 3428092079..bd6e5c8c83 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -1,323 +1,332 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * Copyright 2018 Friedrich W. H. Kossebau * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "plugin.h" // plugin #include #include #include "config/clangtidypreferences.h" #include "config/clangtidyprojectconfigpage.h" #include "job.h" +#include "problemmodel.h" #include "utils.h" // KDevPlatform #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include // KF #include #include #include #include // Qt #include #include #include #include using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(ClangTidyFactory, "kdevclangtidy.json", registerPlugin();) namespace ClangTidy { namespace Strings { QString modelId() { return QStringLiteral("ClangTidy"); } } static bool isSupportedMimeType(const QMimeType& mimeType) { const QString mime = mimeType.name(); return (mime == QLatin1String("text/x-c++src") || mime == QLatin1String("text/x-csrc")); } Plugin::Plugin(QObject* parent, const QVariantList& /*unused*/) : IPlugin(QStringLiteral("kdevclangtidy"), parent) - , m_model(new KDevelop::ProblemModel(parent)) + , m_model(new ProblemModel(this, this)) { setXMLFile(QStringLiteral("kdevclangtidy.rc")); m_checkFileAction = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Analyze Current File with Clang-Tidy"), this); connect(m_checkFileAction, &QAction::triggered, this, &Plugin::runClangTidyFile); actionCollection()->addAction(QStringLiteral("clangtidy_file"), m_checkFileAction); m_checkProjectAction = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Analyze Current Project with Clang-Tidy"), this); connect(m_checkProjectAction, &QAction::triggered, this, &Plugin::runClangTidyAll); actionCollection()->addAction(QStringLiteral("clangtidy_project"), m_checkProjectAction); ProblemModelSet* pms = core()->languageController()->problemModelSet(); pms->addModel(Strings::modelId(), i18n("Clang-Tidy"), m_model.data()); auto clangTidyPath = KDevelop::Path(ClangTidySettings::clangtidyPath()).toLocalFile(); // TODO: not only collect on plugin loading, but also on every change in the settings // TODO: should also check version on every job start to see if there was an update // behind our back while kdevelop running // TODO: collect in async job m_checkSet.setClangTidyPath(clangTidyPath); 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); updateActions(); } Plugin::~Plugin() = default; void Plugin::unload() { ProblemModelSet* pms = core()->languageController()->problemModelSet(); pms->removeModel(Strings::modelId()); } void Plugin::updateActions() { m_checkFileAction->setEnabled(false); m_checkProjectAction->setEnabled(false); if (isRunning()) { return; } KDevelop::IDocument* activeDocument = core()->documentController()->activeDocument(); if (!activeDocument) { return; } auto currentProject = core()->projectController()->findProjectForUrl(activeDocument->url()); if (!currentProject) { return; } if (isSupportedMimeType(activeDocument->mimeType())) { m_checkFileAction->setEnabled(true); } m_checkProjectAction->setEnabled(true); } void Plugin::runClangTidy(bool allFiles) { auto doc = core()->documentController()->activeDocument(); if (doc == nullptr) { QMessageBox::critical(nullptr, i18n("Error starting clang-tidy"), i18n("No suitable active file, unable to deduce project.")); return; } runClangTidy(doc->url(), allFiles); } void Plugin::runClangTidy(const QUrl& url, bool allFiles) { KDevelop::IProject* project = core()->projectController()->findProjectForUrl(url); if (project == nullptr) { QMessageBox::critical(nullptr, i18n("Error starting clang-tidy"), i18n("Active file isn't in a project")); return; } + m_model->reset(project, url, allFiles); + const auto buildDir = project->buildSystemManager()->buildDirectory(project->projectItem()); QString error; const auto filePaths = Utils::filesFromCompilationDatabase(buildDir, url, allFiles, error); ClangTidyProjectSettings projectSettings; projectSettings.setSharedConfig(project->projectConfiguration()); projectSettings.load(); Job::Parameters params; params.projectRootDir = project->path().toLocalFile(); auto clangTidyPath = KDevelop::Path(ClangTidySettings::clangtidyPath()).toLocalFile(); params.executablePath = clangTidyPath; params.filePaths = filePaths; params.buildDir = buildDir.toLocalFile(); params.additionalParameters = projectSettings.additionalParameters(); const auto enabledChecks = projectSettings.enabledChecks(); if (!enabledChecks.isEmpty()) { params.enabledChecks = enabledChecks; } else { // make sure the defaults are up-to-date TODO: make async m_checkSet.setClangTidyPath(clangTidyPath); params.enabledChecks = m_checkSet.defaults().join(QLatin1Char(',')); } params.useConfigFile = projectSettings.useConfigFile(); params.headerFilter = projectSettings.headerFilter(); params.checkSystemHeaders = projectSettings.checkSystemHeaders(); auto job = new ClangTidy::Job(params, this); + connect(job, &Job::problemsDetected, m_model.data(), &ProblemModel::addProblems); connect(job, &KJob::finished, this, &Plugin::result); core()->uiController()->registerStatus(new KDevelop::JobStatus(job, QStringLiteral("clang-tidy"))); core()->runController()->registerJob(job); m_runningJob = job; updateActions(); + showModel(); +} + +void Plugin::showModel() +{ + ProblemModelSet* pms = core()->languageController()->problemModelSet(); + pms->showModel(Strings::modelId()); } bool Plugin::isRunning() const { return !m_runningJob.isNull(); } void Plugin::runClangTidyFile() { bool allFiles = false; runClangTidy(allFiles); } void Plugin::runClangTidyAll() { bool allFiles = true; runClangTidy(allFiles); } void Plugin::result(KJob* job) { auto* aj = qobject_cast(job); if (aj == nullptr) { return; } + m_model->finishAddProblems(); + if (aj->status() == KDevelop::OutputExecuteJob::JobStatus::JobSucceeded || aj->status() == KDevelop::OutputExecuteJob::JobStatus::JobCanceled) { - m_model->setProblems(aj->problems()); - - ProblemModelSet* pms = core()->languageController()->problemModelSet(); - pms->showModel(Strings::modelId()); + showModel(); } else { core()->uiController()->findToolView(i18ndc("kdevstandardoutputview", "@title:window", "Test"), nullptr, KDevelop::IUiController::FindFlags::Raise); } updateActions(); } ContextMenuExtension Plugin::contextMenuExtension(Context* context, QWidget* parent) { ContextMenuExtension extension = KDevelop::IPlugin::contextMenuExtension(context, parent); if (context->hasType(KDevelop::Context::EditorContext) && !isRunning()) { IDocument* doc = core()->documentController()->activeDocument(); if (isSupportedMimeType(doc->mimeType())) { auto action = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Clang-Tidy"), parent); connect(action, &QAction::triggered, this, &Plugin::runClangTidyFile); extension.addAction(KDevelop::ContextMenuExtension::AnalyzeFileGroup, action); } auto action = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Clang-Tidy"), parent); connect(action, &QAction::triggered, this, &Plugin::runClangTidyAll); extension.addAction(KDevelop::ContextMenuExtension::AnalyzeProjectGroup, action); } if (context->hasType(KDevelop::Context::ProjectItemContext) && !isRunning()) { auto pContext = dynamic_cast(context); const auto items = pContext->items(); if (items.size() != 1) { return extension; } const auto item = items.first(); const auto itemType = item->type(); if ((itemType != KDevelop::ProjectBaseItem::File) && (itemType != KDevelop::ProjectBaseItem::Folder) && (itemType != KDevelop::ProjectBaseItem::BuildFolder)) { return extension; } if (itemType == KDevelop::ProjectBaseItem::File) { const QMimeType mimetype = QMimeDatabase().mimeTypeForUrl(item->path().toUrl()); if (!isSupportedMimeType(mimetype)) { return extension; } } auto action = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Clang-Tidy"), parent); connect(action, &QAction::triggered, this, [this, item]() { runClangTidy(item->path().toUrl()); }); extension.addAction(KDevelop::ContextMenuExtension::AnalyzeProjectGroup, action); } return extension; } KDevelop::ConfigPage* Plugin::perProjectConfigPage(int number, const ProjectConfigOptions& options, QWidget* parent) { if (number != 0) { return nullptr; } // ensure checkset is up-to-date TODO: async auto clangTidyPath = KDevelop::Path(ClangTidySettings::clangtidyPath()).toLocalFile(); m_checkSet.setClangTidyPath(clangTidyPath); return new ProjectConfigPage(this, options.project, &m_checkSet, parent); } KDevelop::ConfigPage* Plugin::configPage(int number, QWidget* parent) { if (number != 0) { return nullptr; } return new ClangTidyPreferences(this, parent); } } // namespace ClangTidy #include "plugin.moc" diff --git a/src/plugin.h b/src/plugin.h index 6203aff7aa..1ce521ffea 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -1,103 +1,102 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef CLANGTIDY_PLUGIN_H #define CLANGTIDY_PLUGIN_H // plugin #include "checkset.h" #include // KDevPlatform #include // Qt #include #include class KJob; class QAction; -namespace KDevelop +namespace ClangTidy { + class ProblemModel; -} -namespace ClangTidy -{ /** * \class * \brief implements the support for clang-tidy inside KDevelop by using the IPlugin interface. */ class Plugin : public KDevelop::IPlugin { Q_OBJECT public: explicit Plugin(QObject* parent, const QVariantList& = QVariantList()); ~Plugin(); public: // KDevelop::IPlugin API void unload() override; KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context, QWidget* parent) override; int configPages() const override { return 1; } /** * \function * \return the session configuration page for clang-tidy plugin. */ KDevelop::ConfigPage* configPage(int number, QWidget* parent) override; int perProjectConfigPages() const override { return 1; } /** * \function * \return the clang-tidy configuration page for the current project. */ KDevelop::ConfigPage* perProjectConfigPage(int number, const KDevelop::ProjectConfigOptions& options, QWidget* parent) override; public: /** *\function *\returns all available checks, obtained from clang-tidy program, ran with with "--checks=* --list-checks" * parameters. */ QStringList allAvailableChecks() { return m_checkSet.all(); } + bool isRunning() const; + void runClangTidy(const QUrl& url, bool allFiles = false); private Q_SLOTS: void runClangTidy(bool allFiles = false); - void runClangTidy(const QUrl& url, bool allFiles = false); void runClangTidyFile(); void runClangTidyAll(); void result(KJob* job); void updateActions(); private: - bool isRunning() const; + void showModel(); private: QPointer m_runningJob; QAction* m_checkFileAction; QAction* m_checkProjectAction; - QScopedPointer m_model; + QScopedPointer m_model; CheckSet m_checkSet; }; } // namespace ClangTidy #endif // CLANGTIDY_PLUGIN_H diff --git a/src/problemmodel.cpp b/src/problemmodel.cpp new file mode 100644 index 0000000000..ce14c03867 --- /dev/null +++ b/src/problemmodel.cpp @@ -0,0 +1,133 @@ +/* + * This file is part of KDevelop + * + * Copyright 2018 Anton Anikin + * Copyright 2018 Friedrich W. H. Kossebau + * + * 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; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "problemmodel.h" + +// plugin +#include "utils.h" +#include "plugin.h" +// KDevPlatform +#include +// KF +#include + +namespace ClangTidy +{ + +ProblemModel::ProblemModel(Plugin* plugin, QObject* parent) + : KDevelop::ProblemModel(parent) + , m_plugin(plugin) +{ + setFeatures(CanDoFullUpdate | + ScopeFilter | + SeverityFilter | + Grouping | + CanByPassScopeFilter); +} + +ProblemModel::~ProblemModel() = default; + +void ProblemModel::forceFullUpdate() +{ + if (m_url.isValid() && !m_plugin->isRunning()) { + m_plugin->runClangTidy(m_url, m_allFiles); + } +} + +void ProblemModel::reset(KDevelop::IProject* project, const QUrl& url, bool allFiles) +{ + m_url = url; + m_allFiles = allFiles; + const auto path = url.toLocalFile(); + + clearProblems(); + m_problems.clear(); + + QString tooltip; + if (project) { + setMessage(i18n("Analysis started...")); + tooltip = i18nc("@info:tooltip %1 is the path of the file", "Re-run last Clang-Tidy analysis (%1)", Utils::prettyPathName(path)); + } else { + tooltip = i18nc("@info:tooltip", "Re-run last Clang-Tidy analysis"); + } + + setFullUpdateTooltip(tooltip); +} + +void ProblemModel::setMessage(const QString& message) +{ + KDevelop::DocumentRange pathLocation(KDevelop::DocumentRange::invalid()); + pathLocation.document = KDevelop::IndexedString(m_url.toLocalFile()); + setPlaceholderText(message, pathLocation, i18n("Clang-Tidy")); +} + +// The code is adapted version of cppcheck::ProblemModel::problemExists() +// TODO Add into KDevelop::ProblemModel class ? +bool ProblemModel::problemExists(KDevelop::IProblem::Ptr newProblem) +{ + for (const auto& problem : qAsConst(m_problems)) { + if (newProblem->source() == problem->source() && + newProblem->sourceString() == problem->sourceString() && + newProblem->severity() == problem->severity() && + newProblem->finalLocation() == problem->finalLocation() && + newProblem->description() == problem->description() && + newProblem->explanation() == problem->explanation()) + return true; + } + + return false; +} + +// The code is adapted version of cppcheck::ProblemModel::addProblems() +// TODO Add into KDevelop::ProblemModel class ? +void ProblemModel::addProblems(const QVector& problems) +{ + if (m_problems.isEmpty()) { + m_maxProblemDescriptionLength = 0; + } + + for (const auto& problem : problems) { + if (problemExists(problem)) { + continue; + } + + m_problems.append(problem); + addProblem(problem); + + // This performs adjusting of columns width in the ProblemsView + if (m_maxProblemDescriptionLength < problem->description().length()) { + m_maxProblemDescriptionLength = problem->description().length(); + setProblems(m_problems); + } + } +} + +void ProblemModel::finishAddProblems() +{ + if (m_problems.isEmpty()) { + setMessage(i18n("Analysis completed, no problems detected.")); + } else { + setProblems(m_problems); + } +} + +} diff --git a/src/problemmodel.h b/src/problemmodel.h new file mode 100644 index 0000000000..dc20ba444f --- /dev/null +++ b/src/problemmodel.h @@ -0,0 +1,68 @@ +/* + * This file is part of KDevelop + * + * Copyright 2018 Friedrich W. H. Kossebau + * + * 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; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#ifndef CLANGTIDY_PROBLEMMODEL_H +#define CLANGTIDY_PROBLEMMODEL_H + +// KDevPlatfrom +#include + +namespace KDevelop { class IProject; } + +namespace ClangTidy +{ + +class Plugin; + +class ProblemModel : public KDevelop::ProblemModel +{ + Q_OBJECT + +public: + explicit ProblemModel(Plugin* plugin, QObject* parent); + ~ProblemModel() override; + +public: // KDevelop::ProblemModel API + void forceFullUpdate() override; + +public: + void addProblems(const QVector& problems); + + void finishAddProblems(); + + void reset(KDevelop::IProject* project, const QUrl& url, bool allFiles); + +private: + void setMessage(const QString& message); + bool problemExists(KDevelop::IProblem::Ptr newProblem); + +private: + Plugin* const m_plugin; + QUrl m_url; + bool m_allFiles = false; + + QVector m_problems; + int m_maxProblemDescriptionLength = 0; +}; + +} + +#endif diff --git a/src/utils.cpp b/src/utils.cpp index 541beb0581..231f6719cd 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,116 +1,127 @@ /* * This file is part of KDevelop * * Copyright 2018 Friedrich W. H. Kossebau * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "utils.h" // plugin #include // KDevPlatform +#include +#include #include // KF #include // Qt #include +#include #include #include #include #include namespace ClangTidy { namespace Utils { +QString prettyPathName(const QString& path) +{ + auto* projectController = KDevelop::ICore::self()->projectController(); + return projectController->prettyFileName(QUrl::fromLocalFile(path), + KDevelop::IProjectController::FormatPlain); +} + + QString findExecutable(const QString& fallbackExecutablePath) { const QString executablePath = QStandardPaths::findExecutable(fallbackExecutablePath); return executablePath.isEmpty() ? fallbackExecutablePath : executablePath; } QStringList filesFromCompilationDatabase(const KDevelop::Path& buildPath, const QUrl& urlToCheck, bool allFiles, QString& error) { QStringList result; const auto commandsFilePath = KDevelop::Path(buildPath, QStringLiteral("compile_commands.json")).toLocalFile(); if (!QFile::exists(commandsFilePath)) { error = i18n("Compilation database file not found: '%1'", commandsFilePath); return result; } const auto pathToCheck = urlToCheck.toLocalFile(); if (pathToCheck.isEmpty()) { return result; } QFile commandsFile(commandsFilePath); if (!commandsFile.open(QFile::ReadOnly | QFile::Text)) { error = i18n("Could not open compilation database file for reading: '%1'", commandsFilePath); return result; } QJsonParseError jsonError; const auto commandsDocument = QJsonDocument::fromJson(commandsFile.readAll(), &jsonError); if (jsonError.error) { error = i18n("JSON error during parsing compilation database file '%1': %2", commandsFilePath, jsonError.errorString()); return result; } if (!commandsDocument.isArray()) { error = i18n("JSON error during parsing compilation database file '%1': document is not an array.", commandsFilePath); return result; } const auto fileDataArray = commandsDocument.array(); for (const auto& value : fileDataArray) { if (!value.isObject()) { continue; } const auto entry = value.toObject(); const auto it = entry.find(QLatin1String("file")); if (it != entry.end()) { auto path = it->toString(); if (QFile::exists(path)) { if (allFiles) { result += path; } else { if (path == pathToCheck) { result = QStringList{path}; break; } else if (path.startsWith(pathToCheck)) { result.append(path); } } } } } return result; } } } diff --git a/src/utils.h b/src/utils.h index e687c66120..8abfa9593d 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,44 +1,45 @@ /* * This file is part of KDevelop * * Copyright 2018 Friedrich W. H. Kossebau * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef CLANGTIDY_UTILS_H #define CLANGTIDY_UTILS_H namespace KDevelop { class Path; } class QUrl; class QString; class QStringList; namespace ClangTidy { namespace Utils { +QString prettyPathName(const QString& path); QString findExecutable(const QString& fallbackExecutablePath); QStringList filesFromCompilationDatabase(const KDevelop::Path& buildPath, const QUrl& urlToCheck, bool allFiles, QString& error); } } #endif diff --git a/tests/test_clangtidyjob.cpp b/tests/test_clangtidyjob.cpp index 0a2333a2b9..1e2101c65a 100644 --- a/tests/test_clangtidyjob.cpp +++ b/tests/test_clangtidyjob.cpp @@ -1,97 +1,103 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "test_clangtidyjob.h" // SUT #include "job.h" // KDevPlatform #include #include #include // Qt #include +#include using namespace KDevelop; using namespace ClangTidy; class JobTester : public Job { public: JobTester(Job::Parameters params) : Job(params) { } using Job::processStdoutLines; using Job::processStderrLines; using Job::childProcessExited; QString standardOutput() const { return m_standardOutput.join('\n'); } }; void TestClangTidyJob::initTestCase() { AutoTestShell::init({ "kdevclangtidy" }); TestCore::initialize(Core::NoUi); } void TestClangTidyJob::cleanupTestCase() { TestCore::shutdown(); } void TestClangTidyJob::testJob() { QFile output_example_file("data/output_example"); output_example_file.open(QIODevice::ReadOnly); QTextStream ios(&output_example_file); QStringList stdoutOutput; QString line; while (ios.readLineInto(&line)) { stdoutOutput << line; } QVERIFY(!stdoutOutput.isEmpty()); Job::Parameters jobParams; JobTester jobTester(jobParams); + qRegisterMetaType>(); + QSignalSpy problemsSpy(&jobTester, &JobTester::problemsDetected); + jobTester.processStdoutLines(stdoutOutput); QCOMPARE(jobTester.standardOutput(), stdoutOutput.join('\n')); jobTester.childProcessExited(0, QProcess::NormalExit); - auto problems = jobTester.problems(); + + QCOMPARE(problemsSpy.count(), 1); + const auto problems = qvariant_cast>(problemsSpy.at(0).at(0)); QVERIFY(problems[0]->finalLocation().document.str().contains(QStringLiteral("/kdev-clang-tidy/src/plugin.cpp"))); QVERIFY( problems[0]->explanation().startsWith(QStringLiteral("[cppcoreguidelines-pro-bounds-array-to-pointer-decay]"))); QVERIFY(problems[1]->finalLocation().document.str().contains(QStringLiteral("/kdev-clang-tidy/src/plugin.cpp"))); QVERIFY( problems[1]->explanation().startsWith(QStringLiteral("[cppcoreguidelines-pro-bounds-array-to-pointer-decay]"))); QVERIFY(problems[2]->finalLocation().document.str().contains(QStringLiteral("/kdev-clang-tidy/src/plugin.cpp"))); QVERIFY( problems[2]->explanation().startsWith(QStringLiteral("[cppcoreguidelines-pro-bounds-array-to-pointer-decay]"))); } QTEST_GUILESS_MAIN(TestClangTidyJob); diff --git a/tests/test_clangtidyparser.cpp b/tests/test_clangtidyparser.cpp index 4d04c8b3b5..d27a552a3d 100644 --- a/tests/test_clangtidyparser.cpp +++ b/tests/test_clangtidyparser.cpp @@ -1,103 +1,108 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * * 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; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "test_clangtidyparser.h" // SUT #include "parsers/clangtidyparser.h" // KDevPlatform #include #include #include #include // Qt #include #include #include +#include using namespace KDevelop; using namespace ClangTidy; void TestClangTidyParser::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); } void TestClangTidyParser::cleanupTestCase() { TestCore::shutdown(); } void TestClangTidyParser::testParser() { // prepare QStringList from file to be parsed. QFile output_example_file("data/output_example"); output_example_file.open(QIODevice::ReadOnly); QTextStream ios(&output_example_file); QStringList clangtidy_example_output; QString line; while (ios.readLineInto(&line)) { clangtidy_example_output << line; } QVERIFY(!clangtidy_example_output.isEmpty()); ClangTidy::ClangTidyParser parser; + + qRegisterMetaType>(); + QSignalSpy problemsSpy(&parser, &ClangTidyParser::problemsDetected); + parser.addData(clangtidy_example_output); - parser.parse(); - const auto problems = parser.problems(); + QCOMPARE(problemsSpy.count(), 1); + const auto problems = qvariant_cast>(problemsSpy.at(0).at(0)); QVERIFY(!problems.empty()); IProblem::Ptr p = problems[0]; QCOMPARE(p->description(), QStringLiteral("do not implicitly decay an array into a pointer; consider using " "gsl::array_view or an explicit cast instead")); QVERIFY(p->explanation().startsWith(QStringLiteral("[cppcoreguidelines-pro-bounds-array-to-pointer-decay]"))); QVERIFY(p->finalLocation().document.str().contains(QStringLiteral("/kdev-clang-tidy/src/plugin.cpp"))); QCOMPARE(p->finalLocation().start().line() + 1, 80); QCOMPARE(p->finalLocation().start().column() + 1, 5); QCOMPARE(p->severity(), IProblem::Warning); QCOMPARE(p->source(), IProblem::Plugin); // test problem with 2 elements p = problems[1]; QCOMPARE(p->description(), QStringLiteral("do not implicitly decay an array into a pointer; consider using " "gsl::array_view or an explicit cast instead")); QVERIFY(p->explanation().startsWith(QStringLiteral("[cppcoreguidelines-pro-bounds-array-to-pointer-decay]"))); QVERIFY(p->finalLocation().document.str().contains(QStringLiteral("/kdev-clang-tidy/src/plugin.cpp"))); QCOMPARE(p->finalLocation().start().line() + 1, 145); QCOMPARE(p->finalLocation().start().column() + 1, 9); QCOMPARE(p->severity(), IProblem::Warning); QCOMPARE(p->source(), IProblem::Plugin); // test problem with '\\012' tokens in verbose message p = problems[2]; QCOMPARE(p->description(), QStringLiteral("do not implicitly decay an array into a pointer; consider using " "gsl::array_view or an explicit cast instead")); QVERIFY(p->explanation().startsWith(QStringLiteral("[cppcoreguidelines-pro-bounds-array-to-pointer-decay]"))); QVERIFY(p->finalLocation().document.str().contains(QStringLiteral("/kdev-clang-tidy/src/plugin.cpp"))); QCOMPARE(p->finalLocation().start().line() + 1, 151); QCOMPARE(p->finalLocation().start().column() + 1, 9); QCOMPARE(p->severity(), IProblem::Warning); QCOMPARE(p->source(), IProblem::Plugin); } QTEST_GUILESS_MAIN(TestClangTidyParser);