From fbd350f31f35c56e7eccb833c2943870f93e0c89 Mon Sep 17 00:00:00 2001 From: Nikolai Kosjar Date: Mon, 28 Oct 2019 16:25:07 +0100 Subject: [PATCH] ClangTools: Introduce an info bar ...displaying status information and errors. Change-Id: I4f86b440b28e82786299700dee572e77de7334f3 Reviewed-by: Cristian Adam --- src/plugins/clangtools/clangtool.cpp | 590 ++++++++++++++---- src/plugins/clangtools/clangtool.h | 38 +- .../clangtools/clangtoolruncontrol.cpp | 112 ++-- src/plugins/clangtools/clangtoolruncontrol.h | 11 +- .../clangtools/clangtoolsdiagnosticmodel.cpp | 63 +- .../clangtools/clangtoolsdiagnosticmodel.h | 19 +- src/plugins/clangtools/clangtoolsutils.cpp | 2 +- .../projectexplorer/projectexplorer.cpp | 12 + src/plugins/projectexplorer/projectexplorer.h | 1 + 9 files changed, 618 insertions(+), 230 deletions(-) diff --git a/src/plugins/clangtools/clangtool.cpp b/src/plugins/clangtools/clangtool.cpp index bad06028518..decad6ca1de 100644 --- a/src/plugins/clangtools/clangtool.cpp +++ b/src/plugins/clangtools/clangtool.cpp @@ -59,13 +59,17 @@ #include #include +#include #include #include +#include +#include #include #include #include #include +#include #include #include #include @@ -81,6 +85,145 @@ namespace Internal { static ClangTool *s_instance; +static QString makeLink(const QString &text) +{ + return QString("%1").arg(text); +} + +class IconAndLabel : public QWidget +{ + Q_OBJECT + +public: + IconAndLabel(const QPixmap &pixmap, const QString &text = {}) + : m_icon(new QLabel) + , m_label(new QLabel) + { + QSizePolicy minSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + minSizePolicy.setHorizontalStretch(0); + + m_icon->setPixmap(pixmap); + m_icon->setSizePolicy(minSizePolicy); + + m_label->setSizePolicy(minSizePolicy); + m_label->setText(text); + m_label->setTextInteractionFlags(Qt::TextBrowserInteraction); + + QHBoxLayout *layout = new QHBoxLayout; + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_icon); + layout->addWidget(m_label); + setLayout(layout); + + connect(m_label, &QLabel::linkActivated, this, &IconAndLabel::linkActivated); + } + + void setIcon(const QPixmap &pixmap) { + m_icon->setPixmap(pixmap); + m_icon->setVisible(!pixmap.isNull()); + } + + QString text() const { return m_label->text(); } + void setText(const QString &text) + { + m_label->setText(text); + setVisible(!text.isEmpty()); + } + +signals: + void linkActivated(const QString &link); + +private: + QLabel *m_icon; + QLabel *m_label; +}; + +class InfoBarWidget : public QFrame +{ + Q_OBJECT + +public: + InfoBarWidget() + : m_progressIndicator(new Utils::ProgressIndicator(ProgressIndicatorSize::Small)) + , m_info(new IconAndLabel(Utils::Icons::INFO.pixmap())) + , m_error(new IconAndLabel(Utils::Icons::WARNING.pixmap())) + , m_diagStats(new QLabel) + { + m_diagStats->setTextInteractionFlags(Qt::TextBrowserInteraction); + + QHBoxLayout *layout = new QHBoxLayout; + layout->setContentsMargins(5, 5, 5, 5); + layout->addWidget(m_progressIndicator); + layout->addWidget(m_info); + layout->addWidget(m_error); + layout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum)); + layout->addWidget(m_diagStats); + setLayout(layout); + + QPalette pal; + pal.setColor(QPalette::Window, Utils::creatorTheme()->color(Utils::Theme::InfoBarBackground)); + pal.setColor(QPalette::WindowText, Utils::creatorTheme()->color(Utils::Theme::InfoBarText)); + setPalette(pal); + + setAutoFillBackground(true); + } + + // Info + enum InfoIconType { ProgressIcon, InfoIcon }; + void setInfoIcon(InfoIconType type) + { + const bool showProgress = type == ProgressIcon; + m_progressIndicator->setVisible(showProgress); + m_info->setIcon(showProgress ? QPixmap() : Utils::Icons::INFO.pixmap()); + } + QString infoText() const { return m_info->text(); } + void setInfoText(const QString &text) + { + m_info->setText(text); + evaluateVisibility(); + } + + // Error + using OnLinkActivated = std::function; + enum IssueType { Warning, Error }; + + QString errorText() const { return m_error->text(); } + void setError(IssueType type, + const QString &text, + const OnLinkActivated &linkAction = OnLinkActivated()) + { + m_error->setText(text); + m_error->setIcon(type == Warning ? Utils::Icons::WARNING.pixmap() + : Utils::Icons::CRITICAL.pixmap()); + m_error->disconnect(); + if (linkAction) + connect(m_error, &IconAndLabel::linkActivated, this, linkAction); + evaluateVisibility(); + } + + // Diag stats + void setDiagText(const QString &text) { m_diagStats->setText(text); } + + void reset() + { + setInfoIcon(InfoIcon); + setInfoText({}); + setError(Warning, {}, {}); + setDiagText({}); + } + + void evaluateVisibility() + { + setVisible(!infoText().isEmpty() || !errorText().isEmpty()); + } + +private: + Utils::ProgressIndicator *m_progressIndicator; + IconAndLabel *m_info; + IconAndLabel *m_error; + QLabel *m_diagStats; +}; + class SelectFixitsCheckBox : public QCheckBox { Q_OBJECT @@ -257,11 +400,12 @@ static FileInfos sortedFileInfos(const QVector &proj static RunSettings runSettings() { - Project *project = SessionManager::startupProject(); - auto *projectSettings = ClangToolsProjectSettingsManager::getSettings(project); - if (projectSettings->useGlobalSettings()) - return ClangToolsSettings::instance()->runSettings(); - return projectSettings->runSettings(); + if (Project *project = SessionManager::startupProject()) { + auto *projectSettings = ClangToolsProjectSettingsManager::getSettings(project); + if (!projectSettings->useGlobalSettings()) + return projectSettings->runSettings(); + } + return ClangToolsSettings::instance()->runSettings(); } static ClangDiagnosticConfig diagnosticConfig(const Core::Id &diagConfigId) @@ -310,25 +454,25 @@ ClangTool::ClangTool() m_diagnosticFilterModel->setSourceModel(m_diagnosticModel); m_diagnosticFilterModel->setDynamicSortFilter(true); + m_infoBarWidget = new InfoBarWidget; + m_diagnosticView = new DiagnosticView; initDiagnosticView(); m_diagnosticView->setModel(m_diagnosticFilterModel); m_diagnosticView->setSortingEnabled(true); m_diagnosticView->sortByColumn(Debugger::DetailedErrorView::DiagnosticColumn, Qt::AscendingOrder); - m_diagnosticView->setObjectName(QLatin1String("ClangTidyClazyIssuesView")); - m_diagnosticView->setWindowTitle(tr("Clang-Tidy and Clazy Diagnostics")); foreach (auto * const model, QList({m_diagnosticModel, m_diagnosticFilterModel})) { connect(model, &QAbstractItemModel::rowsInserted, - this, &ClangTool::handleStateUpdate); + this, &ClangTool::updateForCurrentState); connect(model, &QAbstractItemModel::rowsRemoved, - this, &ClangTool::handleStateUpdate); + this, &ClangTool::updateForCurrentState); connect(model, &QAbstractItemModel::modelReset, - this, &ClangTool::handleStateUpdate); + this, &ClangTool::updateForCurrentState); connect(model, &QAbstractItemModel::layoutChanged, // For QSortFilterProxyModel::invalidate() - this, &ClangTool::handleStateUpdate); + this, &ClangTool::updateForCurrentState); } // Go to previous diagnostic @@ -359,10 +503,9 @@ ClangTool::ClangTool() action->setDisabled(true); action->setIcon(Utils::Icons::CLEAN_TOOLBAR.icon()); action->setToolTip(tr("Clear")); - connect(action, &QAction::triggered, [this](){ - m_clear->setEnabled(false); - m_diagnosticModel->clear(); - Debugger::showPermanentStatusMessage(QString()); + connect(action, &QAction::triggered, this, [this]() { + reset(); + update(); }); m_clear = action; @@ -410,18 +553,20 @@ ClangTool::ClangTool() connect(m_diagnosticModel, &ClangToolsDiagnosticModel::fixitStatusChanged, m_diagnosticFilterModel, &DiagnosticFilterModel::onFixitStatusChanged); - connect(m_diagnosticFilterModel, &DiagnosticFilterModel::fixitStatisticsChanged, + connect(m_diagnosticFilterModel, &DiagnosticFilterModel::fixitCountersChanged, this, - [this](int scheduled, int scheduableTotal){ - m_selectFixitsCheckBox->setEnabled(scheduableTotal > 0); + [this](int scheduled, int scheduable){ + m_selectFixitsCheckBox->setEnabled(scheduable > 0); m_applyFixitsButton->setEnabled(scheduled > 0); if (scheduled == 0) m_selectFixitsCheckBox->setCheckState(Qt::Unchecked); - else if (scheduled == scheduableTotal) + else if (scheduled == scheduable) m_selectFixitsCheckBox->setCheckState(Qt::Checked); else m_selectFixitsCheckBox->setCheckState(Qt::PartiallyChecked); + + updateForCurrentState(); }); connect(m_applyFixitsButton, &QToolButton::clicked, [this]() { QVector diagnosticItems; @@ -445,7 +590,17 @@ ClangTool::ClangTool() const QString toolTip = tr("Clang-Tidy and Clazy use a customized Clang executable from the " "Clang project to search for diagnostics."); - m_perspective.addWindow(m_diagnosticView, Perspective::SplitVertical, nullptr); + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(1); + mainLayout->addWidget(m_infoBarWidget); + mainLayout->addWidget(m_diagnosticView); + auto mainWidget = new QWidget; + mainWidget->setObjectName("ClangTidyClazyIssuesView"); + mainWidget->setWindowTitle(tr("Clang-Tidy and Clazy")); + mainWidget->setLayout(mainLayout); + + m_perspective.addWindow(mainWidget, Perspective::SplitVertical, nullptr); action = new QAction(tr("Clang-Tidy and Clazy..."), this); action->setToolTip(toolTip); @@ -479,10 +634,14 @@ ClangTool::ClangTool() m_perspective.addToolBarWidget(m_selectFixitsCheckBox); m_perspective.addToolBarWidget(m_applyFixitsButton); - updateRunActions(); + update(); connect(ProjectExplorerPlugin::instance(), &ProjectExplorerPlugin::updateRunActions, - this, &ClangTool::updateRunActions); + this, &ClangTool::update); + connect(CppModelManager::instance(), &CppModelManager::projectPartsUpdated, + this, &ClangTool::update); + connect(ClangToolsSettings::instance(), &ClangToolsSettings::changed, + this, &ClangTool::update); } ClangTool::~ClangTool() @@ -501,6 +660,33 @@ void ClangTool::startTool(ClangTool::FileSelection fileSelection) startTool(fileSelection, theRunSettings, diagnosticConfig(theRunSettings.diagnosticConfigId())); } +static bool continueDespiteReleaseBuild(const QString &toolName) +{ + const QString wrongMode = ClangTool::tr("Release"); + const QString title = ClangTool::tr("Run %1 in %2 Mode?").arg(toolName, wrongMode); + const QString problem + = ClangTool::tr( + "You are trying to run the tool \"%1\" on an application in %2 mode. The tool is " + "designed to be used in Debug mode since enabled assertions can reduce the number of " + "false positives.") + .arg(toolName, wrongMode); + const QString question = ClangTool::tr( + "Do you want to continue and run the tool in %1 mode?") + .arg(wrongMode); + const QString message = QString("" + "

%1

" + "

%2

" + "") + .arg(problem, question); + return CheckableMessageBox::doNotAskAgainQuestion(ICore::mainWindow(), + title, + message, + ICore::settings(), + "ClangToolsCorrectModeWarning") + == QDialogButtonBox::Yes; +} + + void ClangTool::startTool(ClangTool::FileSelection fileSelection, const RunSettings &runSettings, const CppTools::ClangDiagnosticConfig &diagnosticConfig) @@ -509,51 +695,58 @@ void ClangTool::startTool(ClangTool::FileSelection fileSelection, QTC_ASSERT(project, return); QTC_ASSERT(project->activeTarget(), return); - auto runControl = new RunControl(Constants::CLANGTIDYCLAZY_RUN_MODE); - runControl->setDisplayName(tr("Clang-Tidy and Clazy")); - runControl->setIcon(ProjectExplorer::Icons::ANALYZER_START_SMALL_TOOLBAR); - runControl->setTarget(project->activeTarget()); + // Continue despite release mode? + if (BuildConfiguration *bc = project->activeTarget()->activeBuildConfiguration()) { + if (bc->buildType() == BuildConfiguration::Release) + if (!continueDespiteReleaseBuild(m_name)) + return; + } + // Collect files to analyze const FileInfos fileInfos = collectFileInfos(project, fileSelection); if (fileInfos.empty()) return; - const bool preventBuild = fileSelection == FileSelection::CurrentFile; - auto clangTool = new ClangToolRunWorker(runControl, - runSettings, - diagnosticConfig, - fileInfos, - preventBuild); + // Reset + reset(); + // Run control + m_runControl = new RunControl(Constants::CLANGTIDYCLAZY_RUN_MODE); + m_runControl->setDisplayName(tr("Clang-Tidy and Clazy")); + m_runControl->setIcon(ProjectExplorer::Icons::ANALYZER_START_SMALL_TOOLBAR); + m_runControl->setTarget(project->activeTarget()); m_stopAction->disconnect(); - connect(m_stopAction, &QAction::triggered, runControl, [runControl] { - runControl->appendMessage(tr("Clang-Tidy and Clazy tool stopped by user."), - NormalMessageFormat); - runControl->initiateStop(); + connect(m_stopAction, &QAction::triggered, m_runControl, [this] { + m_runControl->appendMessage(tr("Clang-Tidy and Clazy tool stopped by user."), + NormalMessageFormat); + m_runControl->initiateStop(); + setState(State::StoppedByUser); }); + connect(m_runControl, &RunControl::stopped, this, &ClangTool::onRunControlStopped); - connect(runControl, &RunControl::stopped, this, [this, clangTool] { - bool success = clangTool->success(); - setToolBusy(false); - m_running = false; - handleStateUpdate(); - updateRunActions(); - emit finished(success); - }); - - m_perspective.select(); - m_diagnosticModel->clear(); + // Run worker + const bool preventBuild = fileSelection == FileSelection::CurrentFile; + const bool buildBeforeAnalysis = !preventBuild && runSettings.buildBeforeAnalysis(); + m_runWorker = new ClangToolRunWorker(m_runControl, + runSettings, + diagnosticConfig, + fileInfos, + buildBeforeAnalysis); + connect(m_runWorker, &ClangToolRunWorker::buildFailed,this, &ClangTool::onBuildFailed); + connect(m_runWorker, &ClangToolRunWorker::startFailed, this, &ClangTool::onStartFailed); + connect(m_runWorker, &ClangToolRunWorker::started, this, &ClangTool::onStarted); + connect(m_runWorker, &ClangToolRunWorker::runnerFinished, + this, &ClangTool::updateForCurrentState); + // More init and UI update m_diagnosticFilterModel->setProject(project); - m_selectFixitsCheckBox->setEnabled(false); - m_applyFixitsButton->setEnabled(false); - m_running = true; + m_perspective.select(); + if (buildBeforeAnalysis) + m_infoBarWidget->setInfoText("Waiting for build to finish..."); + setState(State::PreparationStarted); - setToolBusy(true); - handleStateUpdate(); - updateRunActions(); - - ProjectExplorerPlugin::startRunControl(runControl); + // Start + ProjectExplorerPlugin::startRunControl(m_runControl); } Diagnostics ClangTool::read(OutputFileFormat outputFileFormat, @@ -656,12 +849,145 @@ void ClangTool::loadDiagnosticsFromFiles() } // Show errors - if (!errors.isEmpty()) + if (!errors.isEmpty()) { AsynchronousMessageBox::critical(tr("Error Loading Diagnostics"), errors); + return; + } // Show imported - m_diagnosticModel->clear(); + reset(); onNewDiagnosticsAvailable(diagnostics); + setState(State::ImportFinished); +} + +void ClangTool::showOutputPane() +{ + ProjectExplorerPlugin::showOutputPaneForRunControl(m_runControl); +} + +void ClangTool::reset() +{ + m_clear->setEnabled(false); + m_selectFixitsCheckBox->setEnabled(false); + m_applyFixitsButton->setEnabled(false); + + m_diagnosticModel->clear(); + m_diagnosticFilterModel->resetCounters(); + + m_infoBarWidget->reset(); + + m_state = State::Initial; + m_runControl = nullptr; + m_runWorker = nullptr; +} + +static bool canAnalyzeProject(Project *project) +{ + if (const Target *target = project->activeTarget()) { + const Core::Id c = ProjectExplorer::Constants::C_LANGUAGE_ID; + const Core::Id cxx = ProjectExplorer::Constants::CXX_LANGUAGE_ID; + const bool projectSupportsLanguage = project->projectLanguages().contains(c) + || project->projectLanguages().contains(cxx); + return projectSupportsLanguage + && CppModelManager::instance()->projectInfo(project).isValid() + && ToolChainKitAspect::toolChain(target->kit(), cxx); + } + return false; +} + +struct CheckResult { + enum { + InvalidTidyExecutable, + InvalidClazyExecutable, + ProjectNotOpen, + ProjectNotSuitable, + ReadyToAnalyze, + } kind; + QString errorText; +}; + +static CheckResult canAnalyze() +{ + const ClangDiagnosticConfig config = diagnosticConfig(runSettings().diagnosticConfigId()); + + if (config.isClangTidyEnabled() && !isFileExecutable(clangTidyExecutable())) { + return {CheckResult::InvalidTidyExecutable, + ClangTool::tr("Set a valid Clang-Tidy executable.")}; + } + + if (config.isClazyEnabled() && !isFileExecutable(clazyStandaloneExecutable())) { + return {CheckResult::InvalidClazyExecutable, + ClangTool::tr("Set a valid Clazy-Standalone executable.")}; + } + + if (Project *project = SessionManager::startupProject()) { + if (!canAnalyzeProject(project)) { + return {CheckResult::ProjectNotSuitable, + ClangTool::tr("Project \"%1\" is not a C/C++ project.") + .arg(project->displayName())}; + } + } else { + return {CheckResult::ProjectNotOpen, + ClangTool::tr("Open a C/C++ project to start analyzing.")}; + } + + return {CheckResult::ReadyToAnalyze, {}}; +} + +void ClangTool::updateForInitialState() +{ + if (m_state != State::Initial) + return; + + m_infoBarWidget->reset(); + + const CheckResult result = canAnalyze(); + switch (result.kind) + case CheckResult::InvalidTidyExecutable: { + case CheckResult::InvalidClazyExecutable: + m_infoBarWidget->setError(InfoBarWidget::Warning, + makeLink(result.errorText), + [](){ ICore::showOptionsDialog(Constants::SETTINGS_PAGE_ID); }); + break; + case CheckResult::ProjectNotSuitable: + case CheckResult::ProjectNotOpen: + case CheckResult::ReadyToAnalyze: + break; + } +} + +void ClangTool::onBuildFailed() +{ + m_infoBarWidget->setError(InfoBarWidget::Error, + tr("Failed to build the project."), + [this]() { showOutputPane(); }); + setState(State::PreparationFailed); +} + +void ClangTool::onStartFailed() +{ + m_infoBarWidget->setError(InfoBarWidget::Error, + makeLink(tr("Failed to start the analyzer.")), + [this]() { showOutputPane(); }); + setState(State::PreparationFailed); +} + +void ClangTool::onStarted() +{ + setState(State::AnalyzerRunning); +} + +void ClangTool::onRunControlStopped() +{ + if (m_state != State::StoppedByUser && m_state != State::PreparationFailed) + setState(State::AnalyzerFinished); + emit finished(m_runWorker->success()); +} + +void ClangTool::update() +{ + updateForInitialState(); + updateForCurrentState(); } using DocumentPredicate = std::function; @@ -726,6 +1052,12 @@ FileInfoProviders ClangTool::fileInfoProviders(ProjectExplorer::Project *project }; } +void ClangTool::setState(ClangTool::State state) +{ + m_state = state; + updateForCurrentState(); +} + QSet ClangTool::diagnostics() const { return Utils::filtered(m_diagnosticModel->diagnostics(), [](const Diagnostic &diagnostic) { @@ -742,82 +1074,98 @@ void ClangTool::onNewDiagnosticsAvailable(const Diagnostics &diagnostics) m_diagnosticFilterModel->invalidateFilter(); } -void ClangTool::updateRunActions() +void ClangTool::updateForCurrentState() { - if (m_toolBusy) { - QString tooltipText = tr("Clang-Tidy and Clazy are still running."); - - m_startAction->setEnabled(false); - m_startAction->setToolTip(tooltipText); - - m_startOnCurrentFileAction->setEnabled(false); - m_startOnCurrentFileAction->setToolTip(tooltipText); - - m_stopAction->setEnabled(true); - m_loadExported->setEnabled(false); - m_clear->setEnabled(false); - } else { - QString toolTipStart = m_startAction->text(); - QString toolTipStartOnCurrentFile = m_startOnCurrentFileAction->text(); - - Project *project = SessionManager::startupProject(); - Target *target = project ? project->activeTarget() : nullptr; - const Core::Id cxx = ProjectExplorer::Constants::CXX_LANGUAGE_ID; - bool canRun = target && project->projectLanguages().contains(cxx) - && ToolChainKitAspect::toolChain(target->kit(), cxx); - if (!canRun) - toolTipStart = toolTipStartOnCurrentFile = tr("This is not a C/C++ project."); - - m_startAction->setEnabled(canRun); - m_startAction->setToolTip(toolTipStart); - - m_startOnCurrentFileAction->setEnabled(canRun); - m_startOnCurrentFileAction->setToolTip(toolTipStartOnCurrentFile); - - m_stopAction->setEnabled(false); - m_loadExported->setEnabled(true); - m_clear->setEnabled(m_diagnosticModel->diagnostics().count()); + // Actions + bool canStart = false; + const bool isPreparing = m_state == State::PreparationStarted; + const bool isRunning = m_state == State::AnalyzerRunning; + QString startActionToolTip = m_startAction->text(); + QString startOnCurrentToolTip = m_startOnCurrentFileAction->text(); + if (!isRunning) { + const CheckResult result = canAnalyze(); + canStart = result.kind == CheckResult::ReadyToAnalyze; + if (!canStart) { + startActionToolTip = result.errorText; + startOnCurrentToolTip = result.errorText; + } } -} - -void ClangTool::handleStateUpdate() -{ - QTC_ASSERT(m_goBack, return); - QTC_ASSERT(m_goNext, return); - QTC_ASSERT(m_diagnosticModel, return); - QTC_ASSERT(m_diagnosticFilterModel, return); + m_startAction->setEnabled(canStart); + m_startAction->setToolTip(startActionToolTip); + m_startOnCurrentFileAction->setEnabled(canStart); + m_startOnCurrentFileAction->setToolTip(startOnCurrentToolTip); + m_stopAction->setEnabled(isPreparing || isRunning); const int issuesFound = m_diagnosticModel->diagnostics().count(); - const int issuesVisible = m_diagnosticFilterModel->rowCount(); + const int issuesVisible = m_diagnosticFilterModel->diagnostics(); m_goBack->setEnabled(issuesVisible > 1); m_goNext->setEnabled(issuesVisible > 1); - m_clear->setEnabled(issuesFound > 0); + m_clear->setEnabled(!isRunning); m_expandCollapse->setEnabled(issuesVisible); + m_loadExported->setEnabled(!isRunning); - m_loadExported->setEnabled(!m_running); + // Diagnostic view + m_diagnosticView->setCursor(isRunning ? Qt::BusyCursor : Qt::ArrowCursor); - QString message; - if (m_running) { - if (issuesFound) - message = tr("Running - %n diagnostics", nullptr, issuesFound); - else - message = tr("Running - No diagnostics"); - } else { - if (issuesFound) - message = tr("Finished - %n diagnostics", nullptr, issuesFound); - else - message = tr("Finished - No diagnostics"); + // Info bar: errors + const bool hasErrorText = !m_infoBarWidget->errorText().isEmpty(); + const bool hasErrors = m_runWorker && m_runWorker->filesNotAnalyzed() > 0; + if (hasErrors && !hasErrorText) { + const QString text = makeLink( tr("Failed to analyze %1 files.").arg(m_runWorker->filesNotAnalyzed())); + m_infoBarWidget->setError(InfoBarWidget::Warning, text, [this]() { showOutputPane(); }); } - Debugger::showPermanentStatusMessage(message); -} + // Info bar: info + bool showProgressIcon = false; + QString infoText; + switch (m_state) { + case State::Initial: + infoText = m_infoBarWidget->infoText(); + break; + case State::AnalyzerRunning: + showProgressIcon = true; + if (m_runWorker->totalFilesToAnalyze() == 0) { + infoText = tr("Analyzing..."); // Not yet fully started/initialized + } else { + infoText = tr("Analyzing... %1 of %2 files processed.") + .arg(m_runWorker->filesAnalyzed() + m_runWorker->filesNotAnalyzed()) + .arg(m_runWorker->totalFilesToAnalyze()); + } + break; + case State::PreparationStarted: + showProgressIcon = true; + infoText = m_infoBarWidget->infoText(); + break; + case State::PreparationFailed: + break; // OK, we just show an error. + case State::StoppedByUser: + infoText = tr("Analysis stopped by user."); + break; + case State::AnalyzerFinished: + infoText = tr("Finished processing %1 files.").arg(m_runWorker->totalFilesToAnalyze()); + break; + case State::ImportFinished: + infoText = tr("Diagnostics imported."); + break; + } + m_infoBarWidget->setInfoText(infoText); + m_infoBarWidget->setInfoIcon(showProgressIcon ? InfoBarWidget::ProgressIcon + : InfoBarWidget::InfoIcon); -void ClangTool::setToolBusy(bool busy) -{ - QTC_ASSERT(m_diagnosticView, return); - QCursor cursor(busy ? Qt::BusyCursor : Qt::ArrowCursor); - m_diagnosticView->setCursor(cursor); - m_toolBusy = busy; + // Info bar: diagnostic stats + QString diagText; + if (issuesFound) { + diagText = tr("%1 diagnostics. %2 fixits, %4 selected.") + .arg(issuesVisible) + .arg(m_diagnosticFilterModel->fixitsScheduable()) + .arg(m_diagnosticFilterModel->fixitsScheduled()); + } else if (m_state != State::AnalyzerRunning + && m_state != State::Initial + && m_state != State::PreparationStarted + && m_state != State::PreparationFailed) { + diagText = tr("No diagnostics."); + } + m_infoBarWidget->setDiagText(diagText); } } // namespace Internal diff --git a/src/plugins/clangtools/clangtool.h b/src/plugins/clangtools/clangtool.h index 7835bdcd678..2630c0d5101 100644 --- a/src/plugins/clangtools/clangtool.h +++ b/src/plugins/clangtools/clangtool.h @@ -35,6 +35,7 @@ #include QT_BEGIN_NAMESPACE +class QFrame; class QToolButton; QT_END_NAMESPACE @@ -44,6 +45,9 @@ class ClangDiagnosticConfig; namespace Debugger { class DetailedErrorView; } +namespace ProjectExplorer { +class RunControl; +} namespace Utils { class FilePath; class FancyLineEdit; @@ -52,7 +56,9 @@ class FancyLineEdit; namespace ClangTools { namespace Internal { +class InfoBarWidget; class ClangToolsDiagnosticModel; +class ClangToolRunWorker; class Diagnostic; class DiagnosticFilterModel; class RunSettings; @@ -105,25 +111,47 @@ signals: void finished(bool success); // For testing. private: - void updateRunActions(); - void handleStateUpdate(); + enum class State { + Initial, + PreparationStarted, + PreparationFailed, + AnalyzerRunning, + StoppedByUser, + AnalyzerFinished, + ImportFinished, + }; + void setState(State state); + void update(); + void updateForCurrentState(); + void updateForInitialState(); - void setToolBusy(bool busy); + void onBuildFailed(); + void onStartFailed(); + void onStarted(); + void onRunControlStopped(); void initDiagnosticView(); void loadDiagnosticsFromFiles(); + void showOutputPane(); + + void reset(); + FileInfoProviders fileInfoProviders(ProjectExplorer::Project *project, const FileInfos &allFileInfos); ClangToolsDiagnosticModel *m_diagnosticModel = nullptr; + ProjectExplorer::RunControl *m_runControl = nullptr; + ClangToolRunWorker *m_runWorker = nullptr; + + InfoBarWidget *m_infoBarWidget = nullptr; QPointer m_diagnosticView; QAction *m_startAction = nullptr; QAction *m_startOnCurrentFileAction = nullptr; QAction *m_stopAction = nullptr; - bool m_running = false; - bool m_toolBusy = false; + + State m_state = State::Initial; DiagnosticFilterModel *m_diagnosticFilterModel = nullptr; diff --git a/src/plugins/clangtools/clangtoolruncontrol.cpp b/src/plugins/clangtools/clangtoolruncontrol.cpp index 82c2ba0b38c..1503961aab8 100644 --- a/src/plugins/clangtools/clangtoolruncontrol.cpp +++ b/src/plugins/clangtools/clangtoolruncontrol.cpp @@ -56,11 +56,9 @@ #include #include #include -#include #include #include -#include #include #include @@ -137,29 +135,6 @@ private: Target *target = runControl()->target(); QTC_ASSERT(target, reportFailure(); return); - if (runControl()->buildType() == BuildConfiguration::Release) { - const QString wrongMode = ClangToolRunWorker::tr("Release"); - const QString toolName = tool()->name(); - const QString title = ClangToolRunWorker::tr("Run %1 in %2 Mode?").arg(toolName, wrongMode); - const QString problem = ClangToolRunWorker::tr( - "You are trying to run the tool \"%1\" on an application in %2 mode. The tool is " - "designed to be used in Debug mode since enabled assertions can reduce the number of " - "false positives.").arg(toolName, wrongMode); - const QString question = ClangToolRunWorker::tr( - "Do you want to continue and run the tool in %1 mode?").arg(wrongMode); - const QString message = QString("" - "

%1

" - "

%2

" - "").arg(problem, question); - if (Utils::CheckableMessageBox::doNotAskAgainQuestion(Core::ICore::mainWindow(), - title, message, Core::ICore::settings(), - "ClangToolsCorrectModeWarning") != QDialogButtonBox::Yes) - { - reportFailure(); - return; - } - } - connect(BuildManager::instance(), &BuildManager::buildQueueFinished, this, &ProjectBuilder::onBuildFinished, Qt::QueuedConnection); @@ -225,7 +200,7 @@ ClangToolRunWorker::ClangToolRunWorker(RunControl *runControl, const RunSettings &runSettings, const CppTools::ClangDiagnosticConfig &diagnosticConfig, const FileInfos &fileInfos, - bool preventBuild) + bool buildBeforeAnalysis) : RunWorker(runControl) , m_runSettings(runSettings) , m_diagnosticConfig(diagnosticConfig) @@ -235,7 +210,7 @@ ClangToolRunWorker::ClangToolRunWorker(RunControl *runControl, setId("ClangTidyClazyRunner"); setSupportsReRunning(false); - if (!preventBuild && runSettings.buildBeforeAnalysis()) { + if (buildBeforeAnalysis) { m_projectBuilder = new ProjectBuilder(runControl); addStartDependency(m_projectBuilder); } @@ -273,11 +248,11 @@ QList ClangToolRunWorker::runnerCreators() void ClangToolRunWorker::start() { - TaskHub::clearTasks(Debugger::Constants::ANALYZERTASK_ID); ProjectExplorerPlugin::saveModifiedFiles(); if (m_projectBuilder && !m_projectBuilder->success()) { - reportFailure(); + emit buildFailed(); + reportFailure(tr("Failed to build the project.")); return; } @@ -286,13 +261,23 @@ void ClangToolRunWorker::start() m_projectInfo = CppTools::CppModelManager::instance()->projectInfo(project); m_projectFiles = Utils::toSet(project->files(Project::AllFiles)); - // Some projects provides CompilerCallData once a build is finished, + // Project changed in the mean time? if (m_projectInfo.configurationOrFilesChanged(m_projectInfoBeforeBuild)) { // If it's more than a release/debug build configuration change, e.g. // a version control checkout, files might be not valid C++ anymore // or even gone, so better stop here. reportFailure(tr("The project configuration changed since the start of " - "the %1. Please re-run with current configuration.").arg(toolName)); + "the %1. Please re-run with current configuration.") + .arg(toolName)); + emit startFailed(); + return; + } + + // Create log dir + if (!m_temporaryDir.isValid()) { + reportFailure( + tr("Failed to create temporary directory: %1.").arg(m_temporaryDir.errorString())); + emit startFailed(); return; } @@ -303,17 +288,6 @@ void ClangToolRunWorker::start() .arg(m_diagnosticConfig.displayName()), Utils::NormalMessageFormat); - // Create log dir - if (!m_temporaryDir.isValid()) { - const QString errorMessage - = tr("%1: Failed to create temporary directory. Stopped.").arg(toolName); - appendMessage(errorMessage, Utils::ErrorMessageFormat); - TaskHub::addTask(Task::Error, errorMessage, Debugger::Constants::ANALYZERTASK_ID); - TaskHub::requestPopup(); - reportFailure(errorMessage); - return; - } - // Collect files const AnalyzeUnits unitsToProcess = unitsToAnalyze(); qCDebug(LOG) << "Files to process:" << unitsToProcess; @@ -389,22 +363,14 @@ void ClangToolRunWorker::analyzeNextFile() ClangToolRunner *runner = queueItem.runnerCreator(); m_runners.insert(runner); - const QString executable = runner->executable(); - if (!isFileExecutable(executable)) { - const QString errorMessage = tr("%1: Invalid executable \"%2\". Stopped.") - .arg(runner->name(), executable); - TaskHub::addTask(Task::Error, errorMessage, Debugger::Constants::ANALYZERTASK_ID); - TaskHub::requestPopup(); - reportFailure(errorMessage); + if (runner->run(unit.file, unit.arguments)) { + const QString filePath = FilePath::fromString(unit.file).toUserOutput(); + appendMessage(tr("Analyzing \"%1\" [%2].").arg(filePath, runner->name()), + Utils::StdOutFormat); + } else { + reportFailure(tr("Failed to start runner \"%1\".").arg(runner->name())); stop(); - return; } - - QTC_ASSERT(runner->run(unit.file, unit.arguments), return); - - appendMessage(tr("Analyzing \"%1\" [%2].") - .arg(FilePath::fromString(unit.file).toUserOutput(), runner->name()), - Utils::StdOutFormat); } void ClangToolRunWorker::onRunnerFinishedWithSuccess(const QString &filePath) @@ -413,6 +379,8 @@ void ClangToolRunWorker::onRunnerFinishedWithSuccess(const QString &filePath) const QString outputFilePath = runner->outputFilePath(); qCDebug(LOG) << "onRunnerFinishedWithSuccess:" << outputFilePath; + emit runnerFinished(); + QString errorMessage; const Diagnostics diagnostics = tool()->read(runner->outputFileFormat(), outputFilePath, @@ -444,6 +412,8 @@ void ClangToolRunWorker::onRunnerFinishedWithFailure(const QString &errorMessage qCDebug(LOG).noquote() << "onRunnerFinishedWithFailure:" << errorMessage << '\n' << errorDetails; + emit runnerFinished(); + auto *toolRunner = qobject_cast(sender()); const QString fileToAnalyze = toolRunner->fileToAnalyze(); const QString outputFilePath = toolRunner->outputFilePath(); @@ -458,7 +428,6 @@ void ClangToolRunWorker::onRunnerFinishedWithFailure(const QString &errorMessage const QString message = tr("Failed to analyze \"%1\": %2").arg(fileToAnalyze, errorMessage); appendMessage(message, Utils::StdErrFormat); appendMessage(errorDetails, Utils::StdErrFormat); - TaskHub::addTask(Task::Error, message, Debugger::Constants::ANALYZERTASK_ID); handleFinished(); } @@ -484,6 +453,19 @@ void ClangToolRunWorker::updateProgressValue() void ClangToolRunWorker::finalize() { const QString toolName = tool()->name(); + if (m_filesNotAnalyzed.size() != 0) { + appendMessage(tr("Error: Failed to analyze %1 files.").arg(m_filesAnalyzed.size()), + ErrorMessageFormat); + Target *target = runControl()->target(); + if (target && target->activeBuildConfiguration() && !target->activeBuildConfiguration()->buildDirectory().exists() + && !m_runSettings.buildBeforeAnalysis()) { + appendMessage( + tr("Note: You might need to build the project to generate or update source " + "files. To build automatically, enable \"Build the project before analysis\"."), + NormalMessageFormat); + } + } + appendMessage(tr("%1 finished: " "Processed %2 files successfully, %3 failed.") .arg(toolName) @@ -491,22 +473,6 @@ void ClangToolRunWorker::finalize() .arg(m_filesNotAnalyzed.size()), Utils::NormalMessageFormat); - if (m_filesNotAnalyzed.size() != 0) { - QString msg = tr("%1: Not all files could be analyzed.").arg(toolName); - TaskHub::addTask(Task::Error, msg, Debugger::Constants::ANALYZERTASK_ID); - Target *target = runControl()->target(); - if (target && target->activeBuildConfiguration() && !target->activeBuildConfiguration()->buildDirectory().exists() - && !m_runSettings.buildBeforeAnalysis()) { - msg = tr("%1: You might need to build the project to generate or update source " - "files. To build automatically, enable \"Build the project before starting " - "analysis\".") - .arg(toolName); - TaskHub::addTask(Task::Error, msg, Debugger::Constants::ANALYZERTASK_ID); - } - - TaskHub::requestPopup(); - } - m_progress.reportFinished(); runControl()->initiateStop(); } diff --git a/src/plugins/clangtools/clangtoolruncontrol.h b/src/plugins/clangtools/clangtoolruncontrol.h index d1c97c206c8..5a9fbbbdd54 100644 --- a/src/plugins/clangtools/clangtoolruncontrol.h +++ b/src/plugins/clangtools/clangtoolruncontrol.h @@ -70,10 +70,19 @@ public: const RunSettings &runSettings, const CppTools::ClangDiagnosticConfig &diagnosticConfig, const FileInfos &fileInfos, - bool preventBuild); + bool buildBeforeAnalysis); bool success() const { return m_success; } // For testing. + int filesAnalyzed() const { return m_filesAnalyzed.size(); } + int filesNotAnalyzed() const { return m_filesNotAnalyzed.size(); } + int totalFilesToAnalyze() const { return m_fileInfos.size(); } + +signals: + void buildFailed(); + void runnerFinished(); + void startFailed(); + protected: void onRunnerFinishedWithSuccess(const QString &filePath); void onRunnerFinishedWithFailure(const QString &errorMessage, const QString &errorDetails); diff --git a/src/plugins/clangtools/clangtoolsdiagnosticmodel.cpp b/src/plugins/clangtools/clangtoolsdiagnosticmodel.cpp index f45a7abef4d..1e8780a1cc3 100644 --- a/src/plugins/clangtools/clangtoolsdiagnosticmodel.cpp +++ b/src/plugins/clangtools/clangtoolsdiagnosticmodel.cpp @@ -553,19 +553,22 @@ DiagnosticFilterModel::DiagnosticFilterModel(QObject *parent) setProject(project); }); connect(this, &QAbstractItemModel::modelReset, this, [this]() { - m_fixItsScheduled = 0; - m_fixItsScheduableInTotal = 0; - emit fixitStatisticsChanged(m_fixItsScheduled, m_fixItsScheduableInTotal); + resetCounters(); + emit fixitCountersChanged(m_fixitsScheduled, m_fixitsScheduable); }); connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { - m_fixItsScheduableInTotal += diagnosticsWithFixits(parent, first, last); - emit fixitStatisticsChanged(m_fixItsScheduled, m_fixItsScheduableInTotal); + const Counters counters = countDiagnostics(parent, first, last); + m_diagnostics += counters.diagnostics; + m_fixitsScheduable += counters.fixits; + emit fixitCountersChanged(m_fixitsScheduled, m_fixitsScheduable); }); connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) { - m_fixItsScheduableInTotal -= diagnosticsWithFixits(parent, first, last); - emit fixitStatisticsChanged(m_fixItsScheduled, m_fixItsScheduableInTotal); + const Counters counters = countDiagnostics(parent, first, last); + m_diagnostics -= counters.diagnostics; + m_fixitsScheduable -= counters.fixits; + emit fixitCountersChanged(m_fixitsScheduled, m_fixitsScheduable); }); } @@ -605,34 +608,46 @@ void DiagnosticFilterModel::onFixitStatusChanged(const QModelIndex &sourceIndex, return; if (newStatus == FixitStatus::Scheduled) - ++m_fixItsScheduled; + ++m_fixitsScheduled; else if (oldStatus == FixitStatus::Scheduled) { - --m_fixItsScheduled; + --m_fixitsScheduled; if (newStatus != FixitStatus::NotScheduled) - --m_fixItsScheduableInTotal; + --m_fixitsScheduable; } - emit fixitStatisticsChanged(m_fixItsScheduled, m_fixItsScheduableInTotal); + emit fixitCountersChanged(m_fixitsScheduled, m_fixitsScheduable); } -int DiagnosticFilterModel::diagnosticsWithFixits(const QModelIndex &parent, - int first, - int last) const +void DiagnosticFilterModel::resetCounters() { - if (!parent.isValid()) - return 0; + m_fixitsScheduled = 0; + m_fixitsScheduable = 0; + m_diagnostics = 0; +} + +DiagnosticFilterModel::Counters DiagnosticFilterModel::countDiagnostics(const QModelIndex &parent, + int first, + int last) const +{ + Counters counters; + const auto countItem = [&](Utils::TreeItem *item){ + if (!mapFromSource(item->index()).isValid()) + return; // Do not count filtered out items. + ++counters.diagnostics; + if (static_cast(item)->diagnostic().hasFixits) + ++counters.fixits; + }; - int count = 0; auto model = static_cast(sourceModel()); - for (int idx = first; idx <= last; ++idx) { - Utils::TreeItem *treeItem = model->itemForIndex(mapToSource(index(idx, 0, parent))); - if (treeItem->level() == 2) { - if (static_cast(treeItem)->diagnostic().hasFixits) - ++count; - } + for (int row = first; row <= last; ++row) { + Utils::TreeItem *treeItem = model->itemForIndex(mapToSource(index(row, 0, parent))); + if (treeItem->level() == 1) + static_cast(treeItem)->forChildrenAtLevel(1, countItem); + else if (treeItem->level() == 2) + countItem(treeItem); } - return count; + return counters; } bool DiagnosticFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const diff --git a/src/plugins/clangtools/clangtoolsdiagnosticmodel.h b/src/plugins/clangtools/clangtoolsdiagnosticmodel.h index 1ecfc0e7913..c6ca1a6e319 100644 --- a/src/plugins/clangtools/clangtoolsdiagnosticmodel.h +++ b/src/plugins/clangtools/clangtoolsdiagnosticmodel.h @@ -161,22 +161,31 @@ public: FixitStatus oldStatus, FixitStatus newStatus); + void resetCounters(); + int diagnostics() const { return m_diagnostics; } + int fixitsScheduable() const { return m_fixitsScheduable; } + int fixitsScheduled() const { return m_fixitsScheduled; } + signals: - void fixitStatisticsChanged(int scheduled, int scheduableTotal); + void fixitCountersChanged(int scheduled, int scheduableTotal); private: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; bool lessThan(const QModelIndex &l, const QModelIndex &r) const override; - - int diagnosticsWithFixits(const QModelIndex &parent, int first, int last) const; + struct Counters { + int diagnostics = 0; + int fixits = 0; + }; + Counters countDiagnostics(const QModelIndex &parent, int first, int last) const; void handleSuppressedDiagnosticsChanged(); QPointer m_project; Utils::FilePath m_lastProjectDirectory; SuppressedDiagnosticsList m_suppressedDiagnostics; - int m_fixItsScheduableInTotal = 0; - int m_fixItsScheduled = 0; + int m_diagnostics = 0; + int m_fixitsScheduable = 0; + int m_fixitsScheduled = 0; }; } // namespace Internal diff --git a/src/plugins/clangtools/clangtoolsutils.cpp b/src/plugins/clangtools/clangtoolsutils.cpp index fed839d1166..333a573161b 100644 --- a/src/plugins/clangtools/clangtoolsutils.cpp +++ b/src/plugins/clangtools/clangtoolsutils.cpp @@ -80,7 +80,7 @@ bool isFileExecutable(const QString &filePath) return false; const QFileInfo fileInfo(filePath); - return fileInfo.isFile() && fileInfo.isExecutable(); + return fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable(); } QString shippedClangTidyExecutable() diff --git a/src/plugins/projectexplorer/projectexplorer.cpp b/src/plugins/projectexplorer/projectexplorer.cpp index 0b64c69ce7d..dead62ae052 100644 --- a/src/plugins/projectexplorer/projectexplorer.cpp +++ b/src/plugins/projectexplorer/projectexplorer.cpp @@ -368,6 +368,7 @@ public: void addToRecentProjects(const QString &fileName, const QString &displayName); void startRunControl(RunControl *runControl); + void showOutputPaneForRunControl(RunControl *runControl); void updateActions(); void updateContext(); @@ -2342,6 +2343,11 @@ void ProjectExplorerPlugin::startRunControl(RunControl *runControl) dd->startRunControl(runControl); } +void ProjectExplorerPlugin::showOutputPaneForRunControl(RunControl *runControl) +{ + dd->showOutputPaneForRunControl(runControl); +} + void ProjectExplorerPluginPrivate::startRunControl(RunControl *runControl) { m_outputPane.createNewOutputWindow(runControl); @@ -2361,6 +2367,12 @@ void ProjectExplorerPluginPrivate::startRunControl(RunControl *runControl) emit m_instance->updateRunActions(); } +void ProjectExplorerPluginPrivate::showOutputPaneForRunControl(RunControl *runControl) +{ + m_outputPane.showTabFor(runControl); + m_outputPane.popup(IOutputPane::NoModeSwitch | IOutputPane::WithFocus); +} + void ProjectExplorerPluginPrivate::checkForShutdown() { --m_activeRunControlCount; diff --git a/src/plugins/projectexplorer/projectexplorer.h b/src/plugins/projectexplorer/projectexplorer.h index 488e905ce1f..93d2c141b6c 100644 --- a/src/plugins/projectexplorer/projectexplorer.h +++ b/src/plugins/projectexplorer/projectexplorer.h @@ -141,6 +141,7 @@ public: static void showQtSettings(); static void startRunControl(RunControl *runControl); + static void showOutputPaneForRunControl(RunControl *runControl); static void showRunErrorMessage(const QString &errorMessage); // internal public for FlatModel