diff --git a/addons/project/CMakeLists.txt b/addons/project/CMakeLists.txt --- a/addons/project/CMakeLists.txt +++ b/addons/project/CMakeLists.txt @@ -26,6 +26,7 @@ kateprojectcodeanalysistool.cpp tools/kateprojectcodeanalysistoolcppcheck.cpp tools/kateprojectcodeanalysistoolflake8.cpp + tools/kateprojectcodeanalysistoolshellcheck.cpp tools/kateprojectcodeanalysisselector.cpp ) diff --git a/addons/project/autotests/CMakeLists.txt b/addons/project/autotests/CMakeLists.txt --- a/addons/project/autotests/CMakeLists.txt +++ b/addons/project/autotests/CMakeLists.txt @@ -5,7 +5,12 @@ ) # Project Plugin -set(ProjectPluginSrc test1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../fileutil.cpp) +set(ProjectPluginSrc + test1.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../fileutil.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../kateprojectcodeanalysistool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../tools/kateprojectcodeanalysistoolshellcheck.cpp +) add_executable(projectplugin_test ${ProjectPluginSrc}) add_test(plugin-project_test projectplugin_test) target_link_libraries(projectplugin_test kdeinit_kate Qt5::Test) diff --git a/addons/project/autotests/test1.h b/addons/project/autotests/test1.h --- a/addons/project/autotests/test1.h +++ b/addons/project/autotests/test1.h @@ -33,6 +33,7 @@ private Q_SLOTS: void testCommonParent(); + void testShellCheckParsing(); }; #endif diff --git a/addons/project/autotests/test1.cpp b/addons/project/autotests/test1.cpp --- a/addons/project/autotests/test1.cpp +++ b/addons/project/autotests/test1.cpp @@ -20,9 +20,12 @@ #include "test1.h" #include "fileutil.h" +#include "tools/kateprojectcodeanalysistoolshellcheck.h" #include +#include + QTEST_MAIN(Test1) void Test1::initTestCase() @@ -40,4 +43,13 @@ QCOMPARE(FileUtil::commonParent(QLatin1String("~/dev/proj1"), QLatin1String("~/dev/proj222")), QLatin1String("~/dev/")); } +void Test1::testShellCheckParsing() +{ + QString line = QStringLiteral("script.sh:3:11: note: Use ./*glob* or -- *glob* so ... options. [SC2035]"); + KateProjectCodeAnalysisToolShellcheck sc(nullptr); + QStringList outList = sc.parseLine(line); + //qDebug() << outList; + QCOMPARE(outList.size(), 4); +} + // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/project/kateprojectcodeanalysistool.h b/addons/project/kateprojectcodeanalysistool.h --- a/addons/project/kateprojectcodeanalysistool.h +++ b/addons/project/kateprojectcodeanalysistool.h @@ -92,10 +92,36 @@ */ virtual QStringList parseLine(const QString &line) = 0; + /** + * Tells the tool runner if the returned process exit code + * was a successful one. + * + * The default implementation returns true on exitCode 0. + * + * Override this method for a tool that use a non-zero exit code + * e.g. if the processing itself was successful but not all files + * had no linter errors. + */ + virtual bool isSuccessfulExitCode(int exitCode); + /** * @return messages passed to the tool through stdin */ virtual QString stdinMessages() = 0; + + /** + * @returns the number of files to be processed after the filter + * has been applied + */ + int getActualFilesCount(); + + /** + * To be called by derived classes + */ + void setActualFilesCount(int count); + +private: + int m_filesCount = 0; }; Q_DECLARE_METATYPE(KateProjectCodeAnalysisTool*) diff --git a/addons/project/kateprojectcodeanalysistool.cpp b/addons/project/kateprojectcodeanalysistool.cpp --- a/addons/project/kateprojectcodeanalysistool.cpp +++ b/addons/project/kateprojectcodeanalysistool.cpp @@ -26,11 +26,26 @@ { } +KateProjectCodeAnalysisTool::~KateProjectCodeAnalysisTool() +{ +} + void KateProjectCodeAnalysisTool::setProject(KateProject *project) { m_project = project; } -KateProjectCodeAnalysisTool::~KateProjectCodeAnalysisTool() +bool KateProjectCodeAnalysisTool::isSuccessfulExitCode(int exitCode) +{ + return exitCode == 0; +} + +int KateProjectCodeAnalysisTool::getActualFilesCount() +{ + return m_filesCount; +} + +void KateProjectCodeAnalysisTool::setActualFilesCount(int count) { + m_filesCount = count; } diff --git a/addons/project/kateprojectinfoviewcodeanalysis.cpp b/addons/project/kateprojectinfoviewcodeanalysis.cpp --- a/addons/project/kateprojectinfoviewcodeanalysis.cpp +++ b/addons/project/kateprojectinfoviewcodeanalysis.cpp @@ -126,7 +126,7 @@ m_model->removeRows(0, m_model->rowCount(), QModelIndex()); /** - * launch cppcheck + * launch selected tool */ m_analyzer = new QProcess(this); m_analyzer->setProcessChannelMode(QProcess::MergedChannels); @@ -152,6 +152,9 @@ m_messageWidget->animatedShow(); return; } + + m_startStopAnalysis->setEnabled(false); + /** * write files list and close write channel */ @@ -230,19 +233,22 @@ void KateProjectInfoViewCodeAnalysis::finished(int exitCode, QProcess::ExitStatus) { + m_startStopAnalysis->setEnabled(true); + m_messageWidget = new KMessageWidget(); m_messageWidget->setCloseButtonVisible(true); m_messageWidget->setWordWrap(false); - if (exitCode == 0) { + if (m_analysisTool->isSuccessfulExitCode(exitCode)) { + // normally 0 is successful but there are exceptions m_messageWidget->setMessageType(KMessageWidget::Information); - m_messageWidget->setText(i18n("Analysis finished.")); + m_messageWidget->setText(i18n("Analysis on %1 file(s) finished.", m_analysisTool->getActualFilesCount())); } else { // unfortunately, output was eaten by slotReadyRead() // TODO: get stderr output, show it here m_messageWidget->setMessageType(KMessageWidget::Warning); - m_messageWidget->setText(i18n("Analysis failed!")); + m_messageWidget->setText(i18n("Analysis on %1 file(s) failed with exit code %2.", m_analysisTool->getActualFilesCount(), exitCode)); } - static_cast(layout ())->addWidget(m_messageWidget); - m_messageWidget->animatedShow (); + static_cast(layout())->addWidget(m_messageWidget); + m_messageWidget->animatedShow(); } diff --git a/addons/project/tools/kateprojectcodeanalysisselector.cpp b/addons/project/tools/kateprojectcodeanalysisselector.cpp --- a/addons/project/tools/kateprojectcodeanalysisselector.cpp +++ b/addons/project/tools/kateprojectcodeanalysisselector.cpp @@ -22,6 +22,7 @@ #include "kateprojectcodeanalysistoolcppcheck.h" #include "kateprojectcodeanalysistoolflake8.h" +#include "kateprojectcodeanalysistoolshellcheck.h" QStandardItemModel *KateProjectCodeAnalysisSelector::model(QObject *parent) { @@ -34,7 +35,9 @@ // cppcheck, for C++ new KateProjectCodeAnalysisToolCppcheck(model), // flake8, for Python - new KateProjectCodeAnalysisToolFlake8(model) + new KateProjectCodeAnalysisToolFlake8(model), + // ShellCheck, for sh/bash scripts + new KateProjectCodeAnalysisToolShellcheck(model) }; QList column; diff --git a/addons/project/tools/kateprojectcodeanalysistoolcppcheck.cpp b/addons/project/tools/kateprojectcodeanalysistoolcppcheck.cpp --- a/addons/project/tools/kateprojectcodeanalysistoolcppcheck.cpp +++ b/addons/project/tools/kateprojectcodeanalysistoolcppcheck.cpp @@ -36,7 +36,7 @@ QString KateProjectCodeAnalysisToolCppcheck::name() { - return i18n("cppcheck"); + return i18n("cppcheck (C++)"); } QString KateProjectCodeAnalysisToolCppcheck::fileExtensions() @@ -88,5 +88,7 @@ return QString(); } - return filter(m_project->files()).join(QStringLiteral("\n")); + auto&& fileList = filter(m_project->files()); + setActualFilesCount(fileList.size()); + return fileList.join(QStringLiteral("\n")); } diff --git a/addons/project/tools/kateprojectcodeanalysistoolflake8.cpp b/addons/project/tools/kateprojectcodeanalysistoolflake8.cpp --- a/addons/project/tools/kateprojectcodeanalysistoolflake8.cpp +++ b/addons/project/tools/kateprojectcodeanalysistoolflake8.cpp @@ -36,7 +36,7 @@ QString KateProjectCodeAnalysisToolFlake8::name() { - return i18n("flake8"); + return i18n("flake8 (Python)"); } QString KateProjectCodeAnalysisToolFlake8::fileExtensions() @@ -72,7 +72,9 @@ << QStringLiteral("--format=%(path)s////%(row)d////%(code)s////%(text)s"); if (m_project) { - _args.append(filter(m_project->files())); + auto&& fileList = filter(m_project->files()); + setActualFilesCount(fileList.size()); + _args.append(fileList); } return _args; diff --git a/addons/project/tools/kateprojectcodeanalysistoolshellcheck.h b/addons/project/tools/kateprojectcodeanalysistoolshellcheck.h new file mode 100644 --- /dev/null +++ b/addons/project/tools/kateprojectcodeanalysistoolshellcheck.h @@ -0,0 +1,52 @@ +/* This file is part of the Kate project. + * + * Copyright (C) 2018 Gregor Mi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "../kateprojectcodeanalysistool.h" + +/** + * Information provider for shellcheck + */ +class KateProjectCodeAnalysisToolShellcheck : public KateProjectCodeAnalysisTool +{ +public: + explicit KateProjectCodeAnalysisToolShellcheck(QObject *parent = nullptr); + + virtual ~KateProjectCodeAnalysisToolShellcheck() override; + + virtual QString name() override; + + virtual QString fileExtensions() override; + + virtual QStringList filter(const QStringList &files) override; + + virtual QString path() override; + + virtual QStringList arguments() override; + + virtual QString notInstalledMessage() override; + + virtual QStringList parseLine(const QString &line) override; + + virtual bool isSuccessfulExitCode(int exitCode) override; + + virtual QString stdinMessages() override; +}; diff --git a/addons/project/tools/kateprojectcodeanalysistoolshellcheck.cpp b/addons/project/tools/kateprojectcodeanalysistoolshellcheck.cpp new file mode 100644 --- /dev/null +++ b/addons/project/tools/kateprojectcodeanalysistoolshellcheck.cpp @@ -0,0 +1,118 @@ +/* This file is part of the Kate project. + * + * Copyright (C) 2018 Gregor Mi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "kateprojectcodeanalysistoolshellcheck.h" + +#include +#include + +KateProjectCodeAnalysisToolShellcheck::KateProjectCodeAnalysisToolShellcheck(QObject *parent) + : KateProjectCodeAnalysisTool(parent) +{ + +} + +KateProjectCodeAnalysisToolShellcheck::~KateProjectCodeAnalysisToolShellcheck() +{ + +} + +QString KateProjectCodeAnalysisToolShellcheck::name() +{ + // "ShellCheck is a static analysis and linting tool for sh/bash scripts" + return i18n("ShellCheck (sh/bash)"); +} + +QString KateProjectCodeAnalysisToolShellcheck::fileExtensions() +{ + // TODO: How to also handle files with no file extension? + return QStringLiteral("sh|bash"); +} + +QStringList KateProjectCodeAnalysisToolShellcheck::filter(const QStringList &files) +{ + // for now we expect files with extension + return files.filter(QRegularExpression(QStringLiteral("\\.(") + fileExtensions() + QStringLiteral(")$"))); +} + +QString KateProjectCodeAnalysisToolShellcheck::path() +{ + return QStringLiteral("shellcheck"); +} + +QStringList KateProjectCodeAnalysisToolShellcheck::arguments() +{ + QStringList _args; + + // shellcheck --format=gcc script.sh + // Example output: +// script.sh:2:12: note: Use ./*glob* or -- *glob* so names with dashes won't become options. [SC2035] +// script.sh:3:11: note: Use ./*glob* or -- *glob* so names with dashes won't become options. [SC2035] +// script.sh:3:20: warning: podir is referenced but not assigned. [SC2154] +// script.sh:3:20: note: Double quote to prevent globbing and word splitting. [SC2086] + + _args << QStringLiteral("--format=gcc"); + + if (m_project) { + auto&& fileList = filter(m_project->files()); + setActualFilesCount(fileList.size()); + _args.append(fileList); + } + + return _args; +} + +QString KateProjectCodeAnalysisToolShellcheck::notInstalledMessage() +{ + return i18n("Please install ShellCheck (see https://www.shellcheck.net)."); +} + +QStringList KateProjectCodeAnalysisToolShellcheck::parseLine(const QString &line) +{ + // Example: + // IN: + // script.sh:3:11: note: Use ./*glob* or -- *glob* so names with dashes won't become options. [SC2035] + // OUT: + // file, line, severity, message + // "script.sh", "3", "note", "... ..." + + QRegularExpression regex(QStringLiteral("([^:]+):(\\d+):\\d+: (\\w+): (.*)")); + QRegularExpressionMatch match = regex.match(line); + QStringList outList = match.capturedTexts(); + outList.erase(outList.begin()); // remove first element + if (outList.size() != 4) { + // if parsing fails we clear the list + outList.clear(); + } + + return outList; +} + +bool KateProjectCodeAnalysisToolShellcheck::isSuccessfulExitCode(int exitCode) +{ + // "0: All files successfully scanned with no issues." + // "1: All files successfully scanned with some issues." + return exitCode == 0 || exitCode == 1; +} + +QString KateProjectCodeAnalysisToolShellcheck::stdinMessages() +{ + return QString(); +}