diff --git a/util/CMakeLists.txt b/util/CMakeLists.txt --- a/util/CMakeLists.txt +++ b/util/CMakeLists.txt @@ -52,10 +52,11 @@ KF5::TextEditor KF5::GuiAddons ) - install( FILES kdevplatform_shell_environment.sh DESTINATION bin PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ) -install( FILES kdev_format_source DESTINATION bin PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ) +add_executable(kdev_format_source kdevformatsource.cpp kdevformatfile.cpp) +target_link_libraries(kdev_format_source Qt5::Core) +install(TARGETS kdev_format_source DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### diff --git a/util/kdev_format_source b/util/kdev_format_source deleted file mode 100644 --- a/util/kdev_format_source +++ /dev/null @@ -1,132 +0,0 @@ -#!/bin/bash - -# Author: David Nolden -# This script is made available under the GPLv2 licence. -# -# This script formats a given source-file automatically by -# using formatting scripts as defined in specific meta-information -# "format_sources" files contained in the file system hierarchy. -# -# The rules within a "format_sources" file apply to all subdirectories, -# and follow this syntax: -# Each line defines a list of wildcards followed by a colon and the formatting-command. -# Example: "*.cpp *.h : my_custom_formatting_script.sh $TMPFILE" -# -# The file must be terminated by a trailing newline. -# -# If no colon and no wildcards are given, the command is -# used for everything, equivalently to the "*" wildcard. -# -# The contents is processed in linear order, and the first matching command is used. - -ORIGFILE=$1 -TMPFILE=$2 - -if ! [ "$ORIGFILE" ]; then - echo "Usage: $(basename $0) [FILE] [TEMPFILE]" - echo "" - echo "Where FILE represents the original location of the formatted contents," - echo "and TEMPFILE is used as the actual, potentially different," - echo "contents of the file." - exit -fi - -ORIGFILE=$(readlink -f $ORIGFILE) - -if ! [ $TMPFILE ]; then - echo "No tempfile given, formatting the original file" - TMPFILE=$ORIGFILE -else - TMPFILE=$(readlink -f $TMPFILE) -fi - -# Helper: Returns the relative path from a given source directory to a target path -function relativePath { - source=$1 - target=$2 - - common_part=$source - back= - while [ "${target#$common_part}" = "${target}" ]; do - common_part=$(dirname $common_part) - back="../${back}" - done - - echo ${back}${target#$common_part/} -} - -# Go to the directory of the original file, and start searching for "format_sources" files upwards -cd $(dirname $ORIGFILE) - -while ! [ "$(pwd)" == "/" ]; do - - if [ -e "format_sources" ]; then - echo "found $(readlink -f format_sources)" - - # Read line by line - while read line - do -# echo "Line: $line" - # Split by the ":" which is the delimiter between wildcards - IFS="\:" - array= - pos=0 - - # remove leading whitespace - line="${line#"${line%%[![:space:]]*}"}" - - if [[ "$line" == \#* ]] || ! [ "$line" ]; then - # Ignore lines starting with # - # Those can be used for comments. - # Also ignore empty lines - continue - fi - - for item in $line; - do - array[$pos]=$item - pos=$(($pos+1)) - done - - if [ $pos == "2" ]; then - # We found the correct syntax with "wildcards : command" - WILDCARDS=${array[0]} - COMMAND=${array[1]} - - MATCHED=0 - -# echo "wildcards: $WILDCARDS" - - RELATIVE_ORIGFILE=$(relativePath $(pwd) $ORIGFILE) -# echo "relative path: $RELATIVE_ORIGFILE" - - IFS=" " - set -f # This disables the wildcard expansion, we don't want it - for WILDCARD in $WILDCARDS; do - set +f - # This if-command does wildcard matching -# echo "matching $RELATIVE_ORIGFILE and $WILDCARD" - if [[ "$RELATIVE_ORIGFILE" == $WILDCARD ]]; then - echo "matched $RELATIVE_ORIGFILE with wildcard $WILDCARD, using command \"$COMMAND\"" - eval $COMMAND - exit - fi - set -f - done - set +f - fi - - if [ $pos == "1" ]; then - # We found the simple syntax without wildcards, and only with the command - COMMAND=${array[0]} - echo "matched without wildcard, using command $COMMAND" - eval $COMMAND - exit - fi - - done < format_sources - fi - - - cd .. -done diff --git a/util/kdevformatfile.h b/util/kdevformatfile.h new file mode 100644 --- /dev/null +++ b/util/kdevformatfile.h @@ -0,0 +1,75 @@ +/* + Copyright 2016 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; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include +#include + +namespace KDevelop { + +struct KDevFormatLine +{ + QStringList wildcards; + QString command; +}; + +class KDevFormatFile +{ +public: + KDevFormatFile(const QString& origFilePath, const QString& tempFilePath); + + bool find(); + bool read(); + bool apply(); + +private: + bool executeCommand(QString command); + + QString m_origFilePath; + QString m_tempFilePath; + + QVector m_formatLines; +}; + +class AutoFlushingQTextStream : public QTextStream +{ +public: + AutoFlushingQTextStream(FILE* f, QIODevice::OpenMode o) + : QTextStream(f, o) + { + } + + template + AutoFlushingQTextStream& operator << (T&& s) + { + *((QTextStream*) this) << std::forward(s); + flush(); + return *this; + } +}; + +inline AutoFlushingQTextStream& qStdOut() +{ + static AutoFlushingQTextStream s{stdout, QIODevice::WriteOnly | QIODevice::Text}; + return s; +} + +} + diff --git a/util/kdevformatfile.cpp b/util/kdevformatfile.cpp new file mode 100644 --- /dev/null +++ b/util/kdevformatfile.cpp @@ -0,0 +1,153 @@ +/* + Copyright 2016 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; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "kdevformatfile.h" + +#include +#include +#include +#include +#include + +namespace KDevelop { + +static const QString formatFileName = QStringLiteral("format_sources"); + +KDevFormatFile::KDevFormatFile(const QString& origFilePath, const QString& tempFilePath) + : m_origFilePath(origFilePath) + , m_tempFilePath(tempFilePath) +{ +} + +bool KDevFormatFile::find() +{ + QDir srcDir(QFileInfo(m_origFilePath).canonicalPath()); + + while (!srcDir.isRoot()) { + if (srcDir.exists(formatFileName)) { + QDir::setCurrent(srcDir.canonicalPath()); + + qStdOut() << "found \"" + << QFileInfo(srcDir.canonicalPath() + "/" + formatFileName).canonicalFilePath() + << "\"\n"; + return true; + } + + srcDir.cdUp(); + } + + return false; +} + +bool KDevFormatFile::read() +{ + static const QChar delimeter(':'); + + QFile formatFile(formatFileName); + if (!formatFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qStdOut() << "unable to open \"" << formatFileName << "\"\n"; + return false; + } + + int lineNumber = 0; + QString line; + QStringList wildcards; + QString command; + + while (!formatFile.atEnd()) { + ++lineNumber; + + line = formatFile.readLine().trimmed(); + if (line.isEmpty() || line.startsWith('#')) + continue; + + if (line.indexOf(delimeter) < 0) { + // We found the simple syntax without wildcards, and only with the command + + wildcards.clear(); + m_formatLines.append({wildcards, line}); + } else { + // We found the correct syntax with "wildcards : command" + + wildcards = line.section(delimeter, 0, 0).split(' ', QString::SkipEmptyParts); + command = line.section(delimeter, 1).trimmed(); + + if (wildcards.isEmpty()) { + qStdOut() << formatFileName << ":" << lineNumber + << ": error: empty wildcard, skip the line\n"; + continue; + } + m_formatLines.append({wildcards, command}); + } + } + + if (m_formatLines.isEmpty()) { + qStdOut() << formatFileName << ": error: no commands are found\n"; + return false; + } + + return true; +} + +bool KDevFormatFile::apply() +{ + foreach (const KDevFormatLine& formatLine, m_formatLines) { + if (formatLine.wildcards.isEmpty()) { + qStdOut() << "matched \"" << m_origFilePath << "\" without wildcard"; + return executeCommand(formatLine.command); + } + + foreach(const QString& wildcard, formatLine.wildcards) { + if (QDir::match(QDir::current().canonicalPath() + "/" + wildcard.trimmed(), m_origFilePath)) { + qStdOut() << "matched \"" << m_origFilePath << "\" with wildcard \"" << wildcard << "\""; + return executeCommand(formatLine.command); + } + } + } + + qStdOut() << formatFileName << ": error: no commands applicable to \"" << m_origFilePath << "\"\n"; + return false; +} + +bool KDevFormatFile::executeCommand(QString command) +{ + qStdOut() << ", using command \"" << command << "\"\n"; + + command.replace(QStringLiteral("$ORIGFILE"), m_origFilePath); + command.replace(QStringLiteral("$TMPFILE"), m_tempFilePath); + +#ifdef Q_OS_WIN + int execResult = QProcess::execute("cmd", {"/c", command}); +#else + int execResult = QProcess::execute("sh", {"-c", command}); +#endif + + if (execResult == -2) { + qStdOut() << "command \"" + command + "\" failed to start\n"; + return false; + } + + if (execResult == -1) { + qStdOut() << "command \"" + command + "\" crashed\n"; + return false; + } + + return true; +} + +} diff --git a/util/kdevformatsource.cpp b/util/kdevformatsource.cpp new file mode 100644 --- /dev/null +++ b/util/kdevformatsource.cpp @@ -0,0 +1,63 @@ +/* + Copyright 2016 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; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "kdevformatfile.h" + +#include + +int main(int argc, char** argv) +{ + using namespace KDevelop; + + if (argc == 1) { + qStdOut() << "Usage:" << argv[0] << "ORIGFILE [TMPFILE]\n\n"; + qStdOut() << "Where ORIGFILE represents the original location of the formatted contents,\n"; + qStdOut() << "and TMPFILE is used as the actual, potentially different,\n"; + qStdOut() << "contents of the file.\n"; + return EXIT_FAILURE; + } + + QFileInfo origFileInfo(argv[1]); + if (!origFileInfo.exists()) { + qStdOut() << "orig file \"" << origFileInfo.absoluteFilePath() << "\" does not exits\n"; + return EXIT_FAILURE; + } + + QString origFilePath = origFileInfo.canonicalFilePath(); + QString tempFilePath; + + if (argc > 2) + tempFilePath = QFileInfo(argv[2]).canonicalFilePath(); + else { + tempFilePath = origFilePath; + qStdOut() << "no temp file given, formatting the original file\n"; + } + + KDevFormatFile formatFile(origFilePath, tempFilePath); + + if (!formatFile.find()) + return EXIT_FAILURE; + + if (!formatFile.read()) + return EXIT_FAILURE; + + if (!formatFile.apply()) + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} diff --git a/util/tests/CMakeLists.txt b/util/tests/CMakeLists.txt --- a/util/tests/CMakeLists.txt +++ b/util/tests/CMakeLists.txt @@ -24,3 +24,11 @@ ecm_add_test(test_environment.cpp LINK_LIBRARIES Qt5::Test KDev::Util) + +ecm_add_test( + ../kdevformatfile.cpp + test_kdevformatsource.cpp + + TEST_NAME test_kdevformatsource + LINK_LIBRARIES Qt5::Test +) diff --git a/util/tests/test_kdevformatsource.h b/util/tests/test_kdevformatsource.h new file mode 100644 --- /dev/null +++ b/util/tests/test_kdevformatsource.h @@ -0,0 +1,63 @@ +/* + Copyright 2016 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; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include + +namespace KDevelop +{ + +struct Source +{ + QString path; + QStringList lines; +}; + +class TestKdevFormatSource: public QObject { + Q_OBJECT + +private slots: + void testNotFound(); + void testNotFound_data(); + + void testNoCommands(); + void testNoCommands_data(); + + void testNotMatch(); + void testNotMatch_data(); + + void testMatch1(); + void testMatch1_data(); + + void testMatch2(); + void testMatch2_data(); + +private: + bool initTest(const QStringList& formatFileData); + void runTest() const; + + bool mkPath(const QString& path) const; + bool writeLines(const QString& path, const QStringList& lines) const; + bool readLines(const QString& path, QStringList& lines) const; + + QVector m_sources; +}; + +} diff --git a/util/tests/test_kdevformatsource.cpp b/util/tests/test_kdevformatsource.cpp new file mode 100644 --- /dev/null +++ b/util/tests/test_kdevformatsource.cpp @@ -0,0 +1,255 @@ +/* + Copyright 2016 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; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "test_kdevformatsource.h" +#include "../kdevformatfile.h" + +#include + +#include + +QTEST_MAIN(KDevelop::TestKdevFormatSource) + +using namespace KDevelop; + +void TestKdevFormatSource::testNotFound_data() +{ + static const QStringList formatFileData = {}; + + QCOMPARE(initTest(formatFileData), true); + + for (const Source& source : m_sources) { + QTest::newRow(source.path.toUtf8()) << source.path << false << false << false << source.lines; + } +} + +void TestKdevFormatSource::testNotFound() +{ + runTest(); +} + +void TestKdevFormatSource::testNoCommands_data() +{ + static const QStringList formatFileData = {QStringLiteral("# some comment")}; + + QCOMPARE(initTest(formatFileData), true); + + for (const Source& source : m_sources) { + QTest::newRow(source.path.toUtf8()) << source.path << true << false << false << source.lines; + } +} + +void TestKdevFormatSource::testNoCommands() +{ + runTest(); +} + +void TestKdevFormatSource::testNotMatch_data() +{ + static const QStringList formatFileData = {QStringLiteral("notmatched.cpp : unused_command")}; + + QCOMPARE(initTest(formatFileData), true); + + for (const Source& source : m_sources) { + QTest::newRow(source.path.toUtf8()) << source.path << true << true << false << source.lines; + } +} + +void TestKdevFormatSource::testNotMatch() +{ + runTest(); +} + +void TestKdevFormatSource::testMatch1_data() +{ + static const QStringList formatFileData({ + QStringLiteral("src1/source_1.cpp : cat $ORIGFILE | sed 's/foo/FOO/' > tmp && mv tmp $ORIGFILE"), + QStringLiteral("src2/source_2.cpp : cat $ORIGFILE | sed 's/sqrt/std::sqrt/' > tmp && mv tmp $ORIGFILE"), + QStringLiteral("*.cpp : cat $ORIGFILE | sed 's/z/Z/' > tmp && mv tmp $ORIGFILE"), + QStringLiteral("notmatched.cpp : unused_command"), + }); + + QCOMPARE(initTest(formatFileData), true); + + m_sources[0].lines.replaceInStrings("foo", "FOO"); + m_sources[1].lines.replaceInStrings("sqrt", "std::sqrt"); + m_sources[2].lines.replaceInStrings("z", "Z"); + + for (const Source& source : m_sources) { + QTest::newRow(source.path.toUtf8()) << source.path << true << true << true << source.lines; + } +} + +void TestKdevFormatSource::testMatch1() +{ + runTest(); +} + +void TestKdevFormatSource::testMatch2_data() +{ + static const QStringList formatFileData({QStringLiteral("cat $ORIGFILE | sed 's/;/;;/' > tmp && mv tmp $ORIGFILE")}); + + QCOMPARE(initTest(formatFileData), true); + + for (Source& source : m_sources) { + source.lines.replaceInStrings(";", ";;"); + QTest::newRow(source.path.toUtf8()) << source.path << true << true << true << source.lines; + } + +} + +void TestKdevFormatSource::testMatch2() +{ + runTest(); +} + +bool TestKdevFormatSource::initTest(const QStringList& formatFileData) +{ + QTest::addColumn("path"); + QTest::addColumn("isFound"); + QTest::addColumn("isRead"); + QTest::addColumn("isApplied"); + QTest::addColumn("lines"); + + QString workPath = QStandardPaths::standardLocations(QStandardPaths::TempLocation).first(); + workPath += "/test_kdevformatsource/"; + + if (QDir(workPath).exists() && !QDir(workPath).removeRecursively()) { + qDebug() << "unable to remove existing directory" << workPath; + return false; + } + + if (!mkPath(workPath)) + return false; + + if (!mkPath(workPath + "src1")) + return false; + + if (!mkPath(workPath + "src2")) + return false; + + if (!QDir::setCurrent(workPath)) { + qDebug() << "unable to set current directory to" << workPath; + return false; + } + + m_sources.resize(3); + + m_sources[0].path = workPath + "src1/source_1.cpp"; + m_sources[0].lines = QStringList({ + QStringLiteral("void foo(int x) {"), + QStringLiteral(" printf(\"squared x = %d\\n\", x * x);"), + QStringLiteral("}") + }); + + m_sources[1].path = workPath + "src2/source_2.cpp"; + m_sources[1].lines = QStringList({ + QStringLiteral("void bar(double x) {"), + QStringLiteral(" x = sqrt(x);"), + QStringLiteral(" printf(\"sqrt(x) = %e\\n\", x);"), + QStringLiteral("}") + }); + + m_sources[2].path = workPath + "source_3.cpp"; + m_sources[2].lines = QStringList({ + QStringLiteral("void baz(double x, double y) {"), + QStringLiteral(" double z = pow(x, y);"), + QStringLiteral(" printf(\"x^y = %e\\n\", z);"), + QStringLiteral("}") + }); + + for (const Source& source : m_sources) { + if (!writeLines(source.path, source.lines)) + return false; + } + + if (!formatFileData.isEmpty() && !writeLines(QStringLiteral("format_sources"), formatFileData)) + return false; + + return true; +} + +void TestKdevFormatSource::runTest() const +{ + QFETCH(QString, path); + QFETCH(bool, isFound); + QFETCH(bool, isRead); + QFETCH(bool, isApplied); + QFETCH(QStringList, lines); + + KDevFormatFile formatFile(path, path); + + QCOMPARE(formatFile.find(), isFound); + + if (isFound) + QCOMPARE(formatFile.read(), isRead); + + if (isRead) + QCOMPARE(formatFile.apply(), isApplied); + + QStringList processedLines; + QCOMPARE(readLines(path, processedLines), true); + + QCOMPARE(processedLines, lines); +} + +bool TestKdevFormatSource::mkPath(const QString& path) const +{ + if (!QDir().exists(path) && !QDir().mkpath(path)) { + qDebug() << "unable to create directory" << path; + return false; + } + + return true; +} + +bool TestKdevFormatSource::writeLines(const QString& path, const QStringList& lines) const +{ + QFile outFile(path); + if (!outFile.open(QIODevice::WriteOnly)) { + qDebug() << "unable to open file" << path << "for writing"; + return false; + } + + QTextStream outStream(&outFile); + for (const QString& line : lines) { + outStream << line << "\n"; + } + outFile.close(); + + return true; +} + +bool TestKdevFormatSource::readLines(const QString& path, QStringList& lines) const +{ + QFile inFile(path); + if (!inFile.open(QIODevice::ReadOnly)) { + qDebug() << "unable to open file" << path << "for reading"; + return false; + } + + lines.clear(); + + QTextStream inStream(&inFile); + while (!inStream.atEnd()) { + lines += inStream.readLine(); + } + inFile.close(); + + return true; +}