diff --git a/plugins/clazy/config/checkswidget.cpp b/plugins/clazy/config/checkswidget.cpp index de8141a4bd..bf41438f07 100644 --- a/plugins/clazy/config/checkswidget.cpp +++ b/plugins/clazy/config/checkswidget.cpp @@ -1,298 +1,300 @@ /* 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 "checkswidget.h" #include "ui_checkswidget.h" #include "checksdb.h" #include "debug.h" +#include + #include #include namespace Clazy { enum DataRole { CheckRole = Qt::UserRole + 1, DescriptionRole = Qt::UserRole + 2 }; enum ItemType { LevelType, CheckType }; ChecksWidget::ChecksWidget(QSharedPointer db, QWidget* parent) : QWidget(parent) , m_ui(new Ui::ChecksWidget) { m_ui->setupUi(this); m_ui->filterEdit->addTreeWidget(m_ui->checksTree); m_ui->filterEdit->setPlaceholderText(i18n("Search checks...")); connect(m_ui->filterEdit, &KTreeWidgetSearchLine::searchUpdated, this, &ChecksWidget::searchUpdated); auto resetMenu = new QMenu(this); m_ui->resetButton->setMenu(resetMenu); for (auto level : db->levels()) { auto levelItem = new QTreeWidgetItem(m_ui->checksTree, { level->displayName }, LevelType); levelItem->setData(0, CheckRole, level->name); levelItem->setData(0, DescriptionRole, level->description); levelItem->setCheckState(0, Qt::Unchecked); m_items[level->name] = levelItem; auto levelAction = resetMenu->addAction(level->displayName); connect(levelAction, &QAction::triggered, this, [this, level, levelItem]() { { // Block QLineEdit::textChanged() signal, which is used by KTreeWidgetSearchLine to // start delayed search. QSignalBlocker blocker(m_ui->filterEdit); m_ui->filterEdit->clear(); } m_ui->filterEdit->updateSearch(); setChecks(level->name); m_ui->checksTree->setCurrentItem(levelItem); }); for (auto check : qAsConst(level->checks)) { auto checkItem = new QTreeWidgetItem(levelItem, { check->name }, CheckType); checkItem->setData(0, CheckRole, check->name); checkItem->setData(0, DescriptionRole, check->description); checkItem->setCheckState(0, Qt::Unchecked); m_items[check->name] = checkItem; } } connect(m_ui->checksTree, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* item) { setState(item, item->checkState(0)); updateChecks(); }); connect(m_ui->checksTree, &QTreeWidget::currentItemChanged, this, [this, db](QTreeWidgetItem* current) { if (current) { m_ui->descriptionView->setText(current->data(0, DescriptionRole).toString()); } else { m_ui->descriptionView->clear(); } }); } ChecksWidget::~ChecksWidget() = default; QString ChecksWidget::checks() const { return m_checks; } void ChecksWidget::setChecks(const QString& checks) { if (m_checks == checks) { return; } // Clear all selections for (int i = 0 ; i < m_ui->checksTree->topLevelItemCount(); ++i) { setState(m_ui->checksTree->topLevelItem(i), Qt::Unchecked); } const QStringList checksList = checks.split(',', QString::SkipEmptyParts); for (auto checkName : checksList) { checkName = checkName.trimmed(); if (checkName == QStringLiteral("manual")) { 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(); m_ui->checksTree->setCurrentItem(nullptr); } QStringList levelChecks( const QTreeWidget* checksTree, const QString& levelName, const QList& levelItems) { QStringList checksList; if (!levelName.isEmpty()) { checksList += levelName; } for (int i = 0; i < checksTree->topLevelItemCount(); ++i) { const auto levelItem = checksTree->topLevelItem(i); const bool insideLevel = levelItems.contains(levelItem); for (int j = 0; j < levelItem->childCount(); ++j) { auto checkItem = levelItem->child(j); auto checkName = checkItem->data(0, CheckRole).toString(); if (insideLevel) { if (checkItem->checkState(0) == Qt::Unchecked) { checksList += QStringLiteral("no-%1").arg(checkName); } } else { if (checkItem->checkState(0) == Qt::Checked) { checksList += checkName; } } } } return checksList; } void ChecksWidget::updateChecks() { QStringList checksList; QList levelItems; // Here we try to find "best" (shortest) checks representation. To do this we build checks list // for every level and test it's size. for (int i = 0; i < m_ui->checksTree->topLevelItemCount(); ++i) { auto levelItem = m_ui->checksTree->topLevelItem(i); auto levelName = levelItem->data(0, CheckRole).toString(); if (levelName == QStringLiteral("manual")) { // Manual level is "fake level" so we clear the name and will store only // selected checks. levelItems.clear(); levelName.clear(); } else { levelItems += levelItem; } auto levelList = levelChecks(m_ui->checksTree, levelName, levelItems); if (checksList.isEmpty() || checksList.size() > levelList.size()) { checksList = levelList; } } m_ui->messageLabel->setVisible(checksList.isEmpty()); auto checks = checksList.join(QLatin1Char(',')); if (m_checks != checks) { m_checks = checks; emit checksChanged(m_checks); } } void ChecksWidget::setState(QTreeWidgetItem* item, Qt::CheckState state, bool force) { Q_ASSERT(item); QSignalBlocker blocker(m_ui->checksTree); if (item->type() == LevelType) { if (state == Qt::Checked) { // When we enable some non-manual level item, we should also try to enable all // upper level items. We enable upper item only when it's state is Qt::Unchecked. // If the state is Qt::PartiallyChecked we assume that it was configured earlier and // we should skip the item to keep user's checks selection. const int index = m_ui->checksTree->indexOfTopLevelItem(item); if (index > 0 && index < (m_ui->checksTree->topLevelItemCount() - 1)) { setState(m_ui->checksTree->topLevelItem(index - 1), state, false); } if (item->checkState(0) != Qt::Unchecked && !force) { return; } } item->setCheckState(0, state); if (state != Qt::PartiallyChecked) { for (int i = 0; i < item->childCount(); ++i) { item->child(i)->setCheckState(0, state); } } return; } item->setCheckState(0, state); auto levelItem = item->parent(); Q_ASSERT(levelItem); const int childCount = levelItem->childCount(); int checkedCount = 0; for (int i = 0; i < childCount; ++i) { if (levelItem->child(i)->checkState(0) == Qt::Checked) { ++checkedCount; } } if (checkedCount == 0) { setState(levelItem, Qt::Unchecked); } else if (checkedCount == childCount) { setState(levelItem, Qt::Checked); } else { setState(levelItem, Qt::PartiallyChecked); } } void ChecksWidget::searchUpdated(const QString& searchString) { if (searchString.isEmpty()) { m_ui->checksTree->collapseAll(); m_ui->checksTree->setCurrentItem(nullptr); return; } m_ui->checksTree->expandAll(); QTreeWidgetItem* firstVisibleLevel = nullptr; for (int i = 0; i < m_ui->checksTree->topLevelItemCount(); ++i) { auto levelItem = m_ui->checksTree->topLevelItem(i); if (levelItem->isHidden()) { continue; } if (!firstVisibleLevel) { firstVisibleLevel = levelItem; } for (int j = 0; j < levelItem->childCount(); ++j) { auto checkItem = levelItem->child(j); if (!checkItem->isHidden()) { m_ui->checksTree->setCurrentItem(checkItem); return; } } } m_ui->checksTree->setCurrentItem(firstVisibleLevel); } } diff --git a/plugins/clazy/job.cpp b/plugins/clazy/job.cpp index 2fc47d57f1..7f1be69019 100644 --- a/plugins/clazy/job.cpp +++ b/plugins/clazy/job.cpp @@ -1,276 +1,285 @@ /* 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 "checksdb.h" #include "debug.h" #include "globalsettings.h" #include "plugin.h" #include "utils.h" #include #include #include #include #include #include #include #include #include namespace Clazy { Job::Job() : KDevelop::OutputExecuteJob(nullptr) + , m_db(nullptr) + , m_timer(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(); } inline QString spaceEscapedString(const QString& s) { return QString(s).replace(QLatin1Char(' '), QLatin1String("\\ ")); } QString Job::buildMakefile(const JobParameters& params) { const auto makefilePath = QStringLiteral("%1/kdevclazy.makefile").arg(params.projectBuildPath()); QFile makefile(makefilePath); makefile.open(QIODevice::WriteOnly); QTextStream scriptStream(&makefile); // Since GNU make (and maybe other make versions) fails on files/paths with whitespaces // we should perform space-escaping procedure for all potential strings. scriptStream << QStringLiteral("SOURCES ="); for (const QString& source : params.sources()) { scriptStream << QStringLiteral(" %1").arg(spaceEscapedString(source)); } scriptStream << QLatin1Char('\n'); scriptStream << QStringLiteral("COMMAND ="); if (!GlobalSettings::verboseOutput()) { scriptStream << QLatin1Char('@'); } const auto commandLine = params.commandLine(); for (const QString& commandPart : commandLine) { scriptStream << QStringLiteral(" %1").arg(spaceEscapedString(commandPart)); } scriptStream << QLatin1Char('\n'); scriptStream << QStringLiteral(".PHONY: all $(SOURCES)\n"); scriptStream << QStringLiteral("all: $(SOURCES)\n"); scriptStream << QStringLiteral("$(SOURCES):\n"); scriptStream << QStringLiteral("\t@echo 'Clazy check started for $@'\n"); // Wrap filename ($@) with quotas to handle "whitespaced" file names. scriptStream << QStringLiteral("\t$(COMMAND) '$@'\n"); scriptStream << QStringLiteral("\t@echo 'Clazy check finished for $@'\n"); makefile.close(); m_totalCount = params.sources().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 auto document = QFileInfo(match.captured(1)).canonicalFilePath(); + // Sometimes warning/error file path contains "." or ".." elements so we should fix + // it and take "real" (canonical) path value. But QFileInfo::canonicalFilePath() + // returns empty string when file does not exists. Unfortunately we can't pass some + // real file path from unit tests, therefore we should skip canonicalFilePath() step. + // To detect such testing cases we are check m_timer value, which is not-null only for + // "real" jobs, created with public constructor. + const auto document = m_timer.isNull() ? match.captured(1) : QFileInfo(match.captured(1)).canonicalFilePath(); + const int line = match.captured(2).toInt() - 1; const int column = match.captured(3).toInt() - 1; // TODO add KDevelop::IProblem::FinalLocationMode::ToEnd type ? KTextEditor::Range range(line, column, line, 1000); KDevelop::DocumentRange documentRange(KDevelop::IndexedString(document), 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(QLatin1Char(' ')); 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({QStringLiteral("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(QLatin1Char('\n')); qCDebug(KDEV_CLAZY) << "stderr output: "; qCDebug(KDEV_CLAZY) << m_stderrOutput.join(QLatin1Char('\n')); } KDevelop::OutputExecuteJob::childProcessExited(exitCode, exitStatus); } } diff --git a/plugins/clazy/kdevclazy.json b/plugins/clazy/kdevclazy.json index 4b4abad262..97c773a9de 100644 --- a/plugins/clazy/kdevclazy.json +++ b/plugins/clazy/kdevclazy.json @@ -1,23 +1,20 @@ { "KPlugin": { "Authors": [ { "Name": "Anton Anikin" } ], "Category": "Analyzers", "Description": "This plugin integrates Clazy to KDevelop", "Icon": "clazy", "Id": "kdevclazy", "License": "GPL", "Name": "Clazy Support", "ServiceTypes": [ "KDevelop/Plugin" ] }, "X-KDevelop-Category": "Global", - "X-KDevelop-IRequired": [ - "org.kdevelop.IExecutePlugin" - ], "X-KDevelop-Mode": "GUI" } diff --git a/plugins/clazy/problemmodel.cpp b/plugins/clazy/problemmodel.cpp index c55b847c3c..9259dbda50 100644 --- a/plugins/clazy/problemmodel.cpp +++ b/plugins/clazy/problemmodel.cpp @@ -1,168 +1,169 @@ /* 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 "problemmodel.h" #include "plugin.h" #include "utils.h" #include #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) , m_pathLocation(KDevelop::DocumentRange::invalid()) { 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::setMessage(const QString& message) { setPlaceholderText(message, m_pathLocation, i18n("Clazy")); } // 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) { static int maxLength = 0; if (m_problems.isEmpty()) { maxLength = 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 (maxLength < problem->description().length()) { maxLength = problem->description().length(); setProblems(m_problems); } } } void ProblemModel::setProblems() { if (m_problems.isEmpty()) { setMessage(i18n("Analysis completed, no problems detected.")); } else { setProblems(m_problems); } } void ProblemModel::reset() { reset(nullptr, QString()); } void ProblemModel::reset(KDevelop::IProject* project, const QString& path) { m_project = project; m_path = path; m_pathLocation.document = KDevelop::IndexedString(path); clearProblems(); m_problems.clear(); QString tooltip; if (m_project) { setMessage(i18n("Analysis started...")); tooltip = i18nc("@info:tooltip %1 is the path of the file", "Re-run last Clazy analysis (%1)", prettyPathName(m_path)); } else { tooltip = i18nc("@info:tooltip", "Re-run last Clazy analysis"); } setFullUpdateTooltip(tooltip); } void ProblemModel::show() { problemModelSet()->showModel(problemModelId()); } void ProblemModel::forceFullUpdate() { if (m_project && !m_plugin->isRunning()) { m_plugin->runClazy(m_project, m_path); } } }