diff --git a/kdevplatform/util/environmentprofilelist.cpp b/kdevplatform/util/environmentprofilelist.cpp index 6a183e86d9..353a28413f 100644 --- a/kdevplatform/util/environmentprofilelist.cpp +++ b/kdevplatform/util/environmentprofilelist.cpp @@ -1,310 +1,309 @@ /* This file is part of KDevelop Copyright 2007 Andreas Pakulat 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 "environmentprofilelist.h" +#include "kdevstringhandler.h" #include "debug.h" #include #include #include #include -#include #include namespace KDevelop { class EnvironmentProfileListPrivate { public: QMap> m_profiles; QString m_defaultProfileName; }; } using namespace KDevelop; namespace { namespace Strings { // TODO: migrate to more consistent key term "Default Environment Profile" inline QString defaultEnvProfileKey() { return QStringLiteral("Default Environment Group"); } inline QString envGroup() { return QStringLiteral("Environment Settings"); } // TODO: migrate to more consistent key term "Profile List" inline QString profileListKey() { return QStringLiteral("Group List"); } inline QString defaultProfileName() { return QStringLiteral("default"); } } void decode(KConfig* config, EnvironmentProfileListPrivate* d) { KConfigGroup cfg(config, Strings::envGroup()); d->m_defaultProfileName = cfg.readEntry(Strings::defaultEnvProfileKey(), Strings::defaultProfileName()); const QStringList profileNames = cfg.readEntry(Strings::profileListKey(), QStringList{Strings::defaultProfileName()}); for (const auto& profileName : profileNames) { KConfigGroup envgrp(&cfg, profileName); QMap variables; const auto varNames = envgrp.keyList(); for (const QString& varname : varNames) { variables[varname] = envgrp.readEntry(varname, QString()); } d->m_profiles.insert(profileName, variables); } } void encode(KConfig* config, const EnvironmentProfileListPrivate* d) { KConfigGroup cfg(config, Strings::envGroup()); cfg.writeEntry(Strings::defaultEnvProfileKey(), d->m_defaultProfileName); cfg.writeEntry(Strings::profileListKey(), d->m_profiles.keys()); const auto oldGroupList = cfg.groupList(); for (const QString& group : oldGroupList) { if (!d->m_profiles.contains(group)) { cfg.deleteGroup(group); } } for (auto it = d->m_profiles.cbegin(), itEnd = d->m_profiles.cend(); it != itEnd; ++it) { KConfigGroup envgrp(&cfg, it.key()); envgrp.deleteGroup(); const auto val = it.value(); for (auto it2 = val.cbegin(), it2End = val.cend(); it2 != it2End; ++it2) { envgrp.writeEntry(it2.key(), *it2); } } cfg.sync(); } } EnvironmentProfileList::EnvironmentProfileList(const EnvironmentProfileList& rhs) : d_ptr(new EnvironmentProfileListPrivate(*rhs.d_ptr)) { } EnvironmentProfileList& EnvironmentProfileList::operator=(const EnvironmentProfileList& rhs) { Q_D(EnvironmentProfileList); *d = *rhs.d_ptr; return *this; } EnvironmentProfileList::EnvironmentProfileList(const KSharedConfigPtr& config) : d_ptr(new EnvironmentProfileListPrivate) { Q_D(EnvironmentProfileList); decode(config.data(), d); } EnvironmentProfileList::EnvironmentProfileList(KConfig* config) : d_ptr(new EnvironmentProfileListPrivate) { Q_D(EnvironmentProfileList); decode(config, d); } EnvironmentProfileList::~EnvironmentProfileList() = default; QMap EnvironmentProfileList::variables(const QString& profileName) const { Q_D(const EnvironmentProfileList); return d->m_profiles.value(profileName.isEmpty() ? d->m_defaultProfileName : profileName); } QMap& EnvironmentProfileList::variables(const QString& profileName) { Q_D(EnvironmentProfileList); return d->m_profiles[profileName.isEmpty() ? d->m_defaultProfileName : profileName]; } QString EnvironmentProfileList::defaultProfileName() const { Q_D(const EnvironmentProfileList); return d->m_defaultProfileName; } void EnvironmentProfileList::setDefaultProfile(const QString& profileName) { Q_D(EnvironmentProfileList); if (profileName.isEmpty() || !d->m_profiles.contains(profileName)) { return; } d->m_defaultProfileName = profileName; } void EnvironmentProfileList::saveSettings(KConfig* config) const { Q_D(const EnvironmentProfileList); encode(config, d); config->sync(); } void EnvironmentProfileList::loadSettings(KConfig* config) { Q_D(EnvironmentProfileList); d->m_profiles.clear(); decode(config, d); } QStringList EnvironmentProfileList::profileNames() const { Q_D(const EnvironmentProfileList); return d->m_profiles.keys(); } void EnvironmentProfileList::removeProfile(const QString& profileName) { Q_D(EnvironmentProfileList); d->m_profiles.remove(profileName); } EnvironmentProfileList::EnvironmentProfileList() : d_ptr(new EnvironmentProfileListPrivate) { } QStringList EnvironmentProfileList::createEnvironment(const QString& profileName, const QStringList& defaultEnvironment) const { QMap retMap; for (const QString& line : defaultEnvironment) { QString varName = line.section(QLatin1Char('='), 0, 0); QString varValue = line.section(QLatin1Char('='), 1); retMap.insert(varName, varValue); } if (!profileName.isEmpty()) { const auto userMap = variables(profileName); for (QMap::const_iterator it = userMap.constBegin(); it != userMap.constEnd(); ++it) { retMap.insert(it.key(), it.value()); } } QStringList env; env.reserve(retMap.size()); for (QMap::const_iterator it = retMap.constBegin(); it != retMap.constEnd(); ++it) { env << it.key() + QLatin1Char('=') + it.value(); } return env; } static QString expandVariable(const QString &key, const QString &value, QMap &output, const QMap &input, const QProcessEnvironment &environment) { if (value.isEmpty()) return QString(); auto it = output.constFind(key); if (it != output.constEnd()) { // nothing to do, value was expanded already return *it; } // not yet expanded, do that now auto variableValue = [&](const QString &variable) { if (environment.contains(variable)) { return environment.value(variable); } else if (variable == key) { qCWarning(UTIL) << "recursive variable expansion" << variable; return QString(); } else if (input.contains(variable)) { return expandVariable(variable, input.value(variable), output, input, environment); } else { qCWarning(UTIL) << "Couldn't find replacement for" << variable; return QString(); } }; constexpr ushort escapeChar{'\\'}; constexpr ushort variableStartChar{'$'}; const auto isSpecialSymbol = [](QChar c) { return c.unicode() == escapeChar || c.unicode() == variableStartChar; }; - static const QRegularExpression variableNameRegexp(QStringLiteral("\\A\\w+")); auto& expanded = output[key]; expanded.reserve(value.size()); const int lastIndex = value.size() - 1; int i = 0; // Never treat value.back() as a special symbol (nothing to escape or start). while (i < lastIndex) { const auto currentChar = value[i]; switch (currentChar.unicode()) { case escapeChar: { const auto nextChar = value[i+1]; if (!isSpecialSymbol(nextChar)) { expanded += currentChar; // Nothing to escape => keep the escapeChar. } expanded += nextChar; i += 2; break; } case variableStartChar: { ++i; - const auto m = variableNameRegexp.match(value.midRef(i)); - if (m.hasMatch()) { - expanded += variableValue(m.captured()); - i += m.capturedEnd(); + const auto match = matchPossiblyBracedAsciiVariable(value.midRef(i)); + if (match.length == 0) { + expanded += currentChar; // Not a variable name start. } else { - expanded += currentChar; // Not a variable start. + expanded += variableValue(match.name); + i += match.length; } break; } default: expanded += currentChar; ++i; } } if (i == lastIndex) { expanded += value[i]; } return expanded; } void KDevelop::expandVariables(QMap& variables, const QProcessEnvironment& environment) { QMap expanded; for (auto it = variables.cbegin(), end = variables.cend(); it != end; ++it) { expandVariable(it.key(), it.value(), expanded, variables, environment); } variables = expanded; } diff --git a/kdevplatform/util/kdevstringhandler.cpp b/kdevplatform/util/kdevstringhandler.cpp index efa73af72f..bc155d821e 100644 --- a/kdevplatform/util/kdevstringhandler.cpp +++ b/kdevplatform/util/kdevstringhandler.cpp @@ -1,244 +1,294 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat 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. This file mostly code takes from Qt's QSettings class, the copyright header from that file follows: **************************************************************************** ** ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the QtCore module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial Usage ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain ** additional rights. These rights are described in the Nokia Qt LGPL ** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this ** package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://www.qtsoftware.com/contact. ** $QT_END_LICENSE$ ** **************************************************************************** */ #include "kdevstringhandler.h" #include #include +#include #include #include #include #include #include #include +#include +#include + namespace KDevelop { QString joinWithEscaping(const QStringList& input, QChar joinchar, QChar escapechar) { QStringList tmp = input; return tmp.replaceInStrings(joinchar, QString(joinchar) + QString(escapechar)).join(joinchar); } QStringList splitWithEscaping(const QString& input, QChar splitchar, QChar escapechar) { enum State { Normal, SeenEscape } state; state = Normal; QStringList result; QString currentstring; for (const QChar c : input) { switch (state) { case Normal: if (c == escapechar) { state = SeenEscape; } else if (c == splitchar) { result << currentstring; currentstring.clear(); } else { currentstring += c; } break; case SeenEscape: currentstring += c; state = Normal; break; } } if (!currentstring.isEmpty()) { result << currentstring; } return result; } QVariant stringToQVariant(const QString& s) { // Taken from qsettings.cpp, stringToVariant() if (s.startsWith(QLatin1Char('@'))) { if (s.endsWith(QLatin1Char(')'))) { if (s.startsWith(QLatin1String("@Variant("))) { QByteArray a(s.toLatin1().mid(9)); QDataStream stream(&a, QIODevice::ReadOnly); stream.setVersion(QDataStream::Qt_4_4); QVariant result; stream >> result; return result; } } } return QVariant(); } QString qvariantToString(const QVariant& variant) { // Taken from qsettings.cpp, variantToString() QByteArray a; { QDataStream s(&a, QIODevice::WriteOnly); s.setVersion(QDataStream::Qt_4_4); s << variant; } QString result = QLatin1String("@Variant(") + QString::fromLatin1(a.constData(), a.size()) + QLatin1Char(')'); return result; } QString htmlToPlainText(const QString& s, HtmlToPlainTextMode mode) { switch (mode) { case FastMode: { QString result(s); result.remove(QRegExp(QStringLiteral("<[^>]+>"))); return result; } case CompleteMode: { QTextDocument doc; doc.setHtml(s); return doc.toPlainText(); } } return QString(); // never reached } } +int KDevelop::findAsciiIdentifierLength(const QStringRef& str) +{ + if (str.isEmpty()) { + return 0; + } + + constexpr ushort maxAscii{127}; + const auto firstChar = str[0].unicode(); + const bool isIdentifier = firstChar <= maxAscii + && (std::isalpha(firstChar) || firstChar == '_'); + if (!isIdentifier) { + return 0; + } + + const auto partOfIdentifier = [](QChar character) { + const auto u = character.unicode(); + return u <= maxAscii && (std::isalnum(u) || u == '_'); + }; + return std::find_if_not(str.cbegin() + 1, str.cend(), partOfIdentifier) - str.cbegin(); +} + +KDevelop::VariableMatch KDevelop::matchPossiblyBracedAsciiVariable(const QStringRef& str) +{ + if (str.isEmpty()) { + return {}; + } + + if (str[0].unicode() == '{') { + const auto nameLength = findAsciiIdentifierLength(str.mid(1)); + if (nameLength == 0) { + return {}; + } + const auto closingBraceIndex = 1 + nameLength; + if (closingBraceIndex < str.size() && str[closingBraceIndex].unicode() == '}') { + return {nameLength + 2, str.mid(1, nameLength).toString()}; + } + } else { + const auto nameLength = findAsciiIdentifierLength(str); + if (nameLength != 0) { + return {nameLength, str.left(nameLength).toString()}; + } + } + + return {}; +} + QString KDevelop::stripAnsiSequences(const QString& str) { if (str.isEmpty()) { return QString(); // fast path } enum { PLAIN, ANSI_START, ANSI_CSI, ANSI_SEQUENCE, ANSI_WAITING_FOR_ST, ANSI_ST_STARTED } state = PLAIN; QString result; result.reserve(str.count()); for (const QChar c : str) { const auto val = c.unicode(); switch (state) { case PLAIN: if (val == 27) // 'ESC' state = ANSI_START; else if (val == 155) // equivalent to 'ESC'-'[' state = ANSI_CSI; else result.append(c); break; case ANSI_START: if (val == 91) // [ state = ANSI_CSI; else if (val == 80 || val == 93 || val == 94 || val == 95) // 'P', ']', '^' and '_' state = ANSI_WAITING_FOR_ST; else if (val >= 64 && val <= 95) state = PLAIN; else state = ANSI_SEQUENCE; break; case ANSI_CSI: if (val >= 64 && val <= 126) // Anything between '@' and '~' state = PLAIN; break; case ANSI_SEQUENCE: if (val >= 64 && val <= 95) // Anything between '@' and '_' state = PLAIN; break; case ANSI_WAITING_FOR_ST: if (val == 7) // 'BEL' state = PLAIN; else if (val == 27) // 'ESC' state = ANSI_ST_STARTED; break; case ANSI_ST_STARTED: if (val == 92) // '\' state = PLAIN; else state = ANSI_WAITING_FOR_ST; break; } } return result; } void KDevelop::normalizeLineEndings(QByteArray& text) { for (int i = 0, s = text.size(); i < s; ++i) { if (text[i] != '\r') { continue; } if (i + 1 < s && text[i + 1] == '\n') { text.remove(i, 1); } else { text[i] = '\n'; } } } diff --git a/kdevplatform/util/kdevstringhandler.h b/kdevplatform/util/kdevstringhandler.h index fd76d26870..203cf3eb94 100644 --- a/kdevplatform/util/kdevstringhandler.h +++ b/kdevplatform/util/kdevstringhandler.h @@ -1,77 +1,98 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat 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. */ #ifndef KDEVPLATFORM_KDEVSTRINGHANDLER_H #define KDEVPLATFORM_KDEVSTRINGHANDLER_H #include "utilexport.h" -class QString; +#include + +class QStringRef; class QByteArray; class QChar; class QStringList; class QVariant; namespace KDevelop { KDEVPLATFORMUTIL_EXPORT QStringList splitWithEscaping(const QString& input, QChar splitChar, QChar escapeChar); KDEVPLATFORMUTIL_EXPORT QString joinWithEscaping(const QStringList& input, QChar joinChar, QChar escapeChar); /** * convert the @p variant into a string which can then be stored * easily in a KConfig entry. This supports any QVariant type (including custom types) * for which there is a QDataStream operator defined * @returns a QString containing the data from the QVariant. */ KDEVPLATFORMUTIL_EXPORT QString qvariantToString(const QVariant& variant); /** * convert the @p s into a QVariant, usually the string is read from KConfig. * This supports any QVariant type (including custom types) * for which there is a QDataStream operator defined * @returns a QVariant created from the bytearray */ KDEVPLATFORMUTIL_EXPORT QVariant stringToQVariant(const QString& s); enum HtmlToPlainTextMode { FastMode, /**< Fast (conversion via regular expression) */ CompleteMode, /**< Slower, but with expected behavior (conversion via QTextDocument::toPlainText). This also replaces
with newline chars, for example. */ }; /** * Strip HTML tags from string @p s * * @return String no longer containing any HTML tags */ KDEVPLATFORMUTIL_EXPORT QString htmlToPlainText(const QString& s, HtmlToPlainTextMode mode = FastMode); +/** + * Match a prefix of @p str to an ASCII-only identifier, i.e. [a-zA-Z_][a-zA-Z0-9_]* + * + * @return The length of the matched prefix or 0 if there is no match + */ +KDEVPLATFORMUTIL_EXPORT int findAsciiIdentifierLength(const QStringRef& str); + +struct KDEVPLATFORMUTIL_EXPORT VariableMatch { + int length; ///< The length of the matched substring in the source string + QString name; ///< The name of the matched variable +}; + +/** + * Match a prefix of @p str to an ASCII-only identifier or {identifier} + * + * @return The matching result or {} if there is no match + */ +KDEVPLATFORMUTIL_EXPORT VariableMatch matchPossiblyBracedAsciiVariable(const QStringRef& str); + /** * Strip ANSI sequences from string @p str */ KDEVPLATFORMUTIL_EXPORT QString stripAnsiSequences(const QString& str); /** * Replace all occurrences of "\r" or "\r\n" in @p text with "\n". */ KDEVPLATFORMUTIL_EXPORT void normalizeLineEndings(QByteArray& text); } #endif // KDEVPLATFORM_KDEVSTRINGHANDLER_H diff --git a/kdevplatform/util/tests/test_environment.cpp b/kdevplatform/util/tests/test_environment.cpp index 50bc5e792c..b24676b5f7 100644 --- a/kdevplatform/util/tests/test_environment.cpp +++ b/kdevplatform/util/tests/test_environment.cpp @@ -1,179 +1,199 @@ /* * This file is part of KDevelop * * Copyright 2015 Artur Puzio * * This program 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 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_environment.h" #include "util/environmentprofilelist.h" #include #include QTEST_MAIN(TestEnvironment) using ProcEnv = QMap; void TestEnvironment::testExpandVariables_data() { QTest::addColumn("env"); QTest::addColumn("expectedEnv"); QTest::newRow("no variables") << ProcEnv({}) << ProcEnv({}); QTest::newRow("simple variables") << ProcEnv{ {"VAR1", "data"}, {"Var2", "some other data"} } << ProcEnv({ {"VAR1", "data"}, {"Var2", "some other data"} }); QTest::newRow("PATH append and prepend") << ProcEnv({ {"PATH", "/home/usr/bin:$PATH:/home/user/folder"} }) << ProcEnv({ {"PATH", "/home/usr/bin:/bin:/usr/bin:/home/user/folder"} }); QTest::newRow("\\$VAR") << ProcEnv({ {"MY_VAR", "\\$PATH something \\$HOME"} }) << ProcEnv({ {"MY_VAR", "$PATH something $HOME"} }); QTest::newRow("spaces, \\$VAR after $VAR") << ProcEnv({ {"MY_VAR", "$PATH:$HOME something \\$HOME"} }) << ProcEnv({ {"MY_VAR", "/bin:/usr/bin:/home/tom something $HOME"} }); QTest::newRow("VAR2=$VAR1") << ProcEnv({ {"VAR1", "/some/path"}, {"VAR2", "$VAR1"} }) << ProcEnv({ {"VAR1", "/some/path"}, {"VAR2", "/some/path"} }); QTest::newRow("expanding with not yet expanded variable") << ProcEnv({ {"VAR1", "$VAR2"}, {"VAR2", "$PATH"} }) << ProcEnv({ {"VAR1", "/bin:/usr/bin"}, {"VAR2", "/bin:/usr/bin"} }); QTest::newRow("adding to a variable not in the environment") << ProcEnv({ {"VAR1", "data:$VAR1"} }) << ProcEnv({ {"VAR1", "data:"} }); QTest::newRow("cycle") << ProcEnv({ {"VAR1", "$VAR2"}, {"VAR2", "/usr/$VAR2"} }) << ProcEnv({ {"VAR1", "/usr/"}, {"VAR2", "/usr/"} }); QTest::newRow("order") << ProcEnv({ {"VAR1", "$VAR3"}, {"VAR2", "foo"}, {"VAR3", "$VAR2"}, }) << ProcEnv({ {"VAR1", "foo"}, {"VAR2", "foo"}, {"VAR3", "foo"}, }); QTest::newRow("escaped backslash before dollar") << ProcEnv({ {"var1", "\\\\$var2"}, {"var2", "x"}, }) << ProcEnv({ {"var1", "\\x"}, {"var2", "x"}, }); QTest::newRow("non-escaping backslashes") << ProcEnv({ {"var1", "\\abc\\def\\"}, }) << ProcEnv({ {"var1", "\\abc\\def\\"}, }); QTest::newRow("verbatim dollars") << ProcEnv({ {"var1", "a\\$b$.$\\c$"}, }) << ProcEnv({ {"var1", "a$b$.$\\c$"}, }); QTest::newRow("expansion priority") << ProcEnv({ {"A", "$PATH"}, {"HOME", "my$HOME"}, {"PATH", "."}, {"X", "$A$HOME$Z"}, {"Z", "-$PATH+"}, }) << ProcEnv({ {"A", "/bin:/usr/bin"}, {"HOME", "my/home/tom"}, {"PATH", "."}, {"X", "/bin:/usr/bin/home/tom-/bin:/usr/bin+"}, {"Z", "-/bin:/usr/bin+"}, }); + QTest::newRow("braced variable") << ProcEnv({ + {"u", "${PATH}"}, + }) << ProcEnv({ + {"u", "/bin:/usr/bin"}, + }); + QTest::newRow("braced variables") << ProcEnv({ + {"u", "${PATH}${vw}"}, + {"vw", "${MISSING_VAR}/w"}, + }) << ProcEnv({ + {"u", "/bin:/usr/bin/w"}, + {"vw", "/w"}, + }); + QTest::newRow("braced non-variables") << ProcEnv({ + {"u", "\\${PATH}${v.w}{u}"}, + {"w", "$ {PATH}/${:} {}-${} ${2}"}, + }) << ProcEnv({ + {"u", "${PATH}${v.w}{u}"}, + {"w", "$ {PATH}/${:} {}-${} ${2}"}, + }); + // The mutual recursion tests below process unreasonable input. The purpose // of these tests is to verify that KDevelop does not crash or hang with // such input. The actual results are unimportant. QTest::newRow("mutual recursion") << ProcEnv({ {"VAR1", "$VAR2"}, {"VAR2", "$VAR1"}, }) << ProcEnv({ {"VAR1", ""}, {"VAR2", ""}, }); QTest::newRow("mutual recursion - prefix") << ProcEnv({ {"VAR1", "/bin/$VAR2"}, {"VAR2", "/usr/$VAR1"}, }) << ProcEnv({ {"VAR1", "/bin//usr//bin/"}, {"VAR2", "/usr//bin/"}, }); QTest::newRow("mutual recursion - suffix") << ProcEnv({ {"VAR1", "$VAR2/"}, {"VAR2", "$VAR1."}, }) << ProcEnv({ {"VAR1", "./"}, {"VAR2", "."}, }); QTest::newRow("mutual recursion - middle") << ProcEnv({ {"VAR1", "a$VAR2."}, {"VAR2", "b$VAR1,"}, }) << ProcEnv({ {"VAR1", "aba,."}, {"VAR2", "ba,"}, }); QTest::newRow("3-way recursion") << ProcEnv({ {"a", "1$b-2"}, {"b", "3$c-4"}, {"c", "5$a-6"}, }) << ProcEnv({ {"a", "1351-6-4-2"}, {"b", "351-6-4"}, {"c", "51-6"}, }); } void TestEnvironment::testExpandVariables() { QFETCH(ProcEnv, env); QFETCH(ProcEnv, expectedEnv); QProcessEnvironment fakeSysEnv; fakeSysEnv.insert(QStringLiteral("PATH"), QStringLiteral("/bin:/usr/bin")); fakeSysEnv.insert(QStringLiteral("HOME"), QStringLiteral("/home/tom")); KDevelop::expandVariables(env, fakeSysEnv); for (auto it = expectedEnv.cbegin(); it != expectedEnv.cend(); ++it) { QCOMPARE(env.value(it.key()), it.value()); } } diff --git a/kdevplatform/util/tests/test_stringhandler.cpp b/kdevplatform/util/tests/test_stringhandler.cpp index 1768fbb7d6..b670a4f4e0 100644 --- a/kdevplatform/util/tests/test_stringhandler.cpp +++ b/kdevplatform/util/tests/test_stringhandler.cpp @@ -1,103 +1,273 @@ /* * Copyright 2014 Kevin Funk * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . * */ #include "test_stringhandler.h" #include "kdevstringhandler.h" #include QTEST_MAIN(TestStringHandler) using namespace KDevelop; Q_DECLARE_METATYPE(HtmlToPlainTextMode) void TestStringHandler::testHtmlToPlainText() { QFETCH(QString, html); QFETCH(HtmlToPlainTextMode, mode); QFETCH(QString, expectedPlainText); QString plainText = htmlToPlainText(html, mode); QCOMPARE(plainText, expectedPlainText); } void TestStringHandler::testHtmlToPlainText_data() { QTest::addColumn("html"); QTest::addColumn("mode"); QTest::addColumn("expectedPlainText"); QTest::newRow("simple-fast") << "

bar()

a
foo
" << KDevelop::FastMode << "bar() a foo"; QTest::newRow("simple-complete") << "

bar()

a
foo
" << KDevelop::CompleteMode << "bar() \na\nfoo"; } +namespace { +void addAsciiIdentifierData() +{ + QTest::addColumn("str"); + QTest::addColumn("identifierBegin"); + QTest::addColumn("identifierEnd"); + + QString str; + + str = "u0_cBQp.ad"; + const auto indexOfPeriod = str.indexOf('.'); + QTest::newRow("identifier at index 0") << str << 0 << indexOfPeriod; + QTest::newRow("identifier ends at string end") << str << indexOfPeriod + 1 << str.size(); + QTest::newRow("one letter at string end") << str << indexOfPeriod + 2 << str.size(); + QTest::newRow("string end") << str << str.size() << str.size(); + + str = "AbcD901z95-24"; + QTest::newRow("identifier after a letter") << str << 2 << str.indexOf('-'); + + str = "a=$B+c"; + QTest::newRow("identifier after a dollar") << str << 3 << str.indexOf('+'); + + str = "-_,"; + QTest::newRow("underscore identifier") << str << 1 << 2; + + str = "_aBt_9"; + QTest::newRow("entire-string identifier") << str << 0 << str.size(); +} + +void addUnmatchedAsciiIdentifierData() +{ + QTest::addColumn("str"); + QTest::addColumn("unmatchedCharsBegin"); + QTest::addColumn("unmatchedCharsEnd"); + + QString str; + + str = "12970abC3D5"; + QTest::newRow("a digit") << str << 0 << str.indexOf('a'); + auto indexOfDigit = str.indexOf('3'); + QTest::newRow("a digit between letters") << str << indexOfDigit << indexOfDigit + 1; + indexOfDigit = str.indexOf('5'); + QTest::newRow("a digit at string end") << str << indexOfDigit << indexOfDigit + 1; + + str = ".,-[/\\]$&{(=*+!#\r\n)~%`}\0\t |@^?;:<>\"'"; + QTest::newRow("a non-ID character") << str << 0 << str.size(); + + str = QString::fromUtf8("\u5c07\u4f0a\u00f8\u00e5\ub418\uc9c0\u0414\u041b\u0407" + "\u062c\u0628\u062a\u044a\u044b\u043c\u00ae\u00bf\u00ab" + "\u00bb\u00b6\u00fc\u00a7\u201c\u201d\u6d77\u9b5a\u300d\u3232"); + QTest::newRow("a non-ASCII character") << str << 0 << str.size(); +} +} + +void TestStringHandler::testFindAsciiIdentifierLength() +{ + QFETCH(QString, str); + QFETCH(int, identifierBegin); + QFETCH(int, identifierEnd); + + const auto length = identifierEnd - identifierBegin; + Q_ASSERT_X(length >= 0, Q_FUNC_INFO, "Wrong data."); + QCOMPARE(findAsciiIdentifierLength(str.midRef(identifierBegin)), length); +} + +void TestStringHandler::testFindAsciiIdentifierLength_data() +{ + addAsciiIdentifierData(); +} + +void TestStringHandler::testFindAsciiIdentifierLengthNoMatch() +{ + QFETCH(QString, str); + QFETCH(int, unmatchedCharsBegin); + QFETCH(int, unmatchedCharsEnd); + + Q_ASSERT_X(unmatchedCharsBegin < unmatchedCharsEnd, Q_FUNC_INFO, + "Nothing to test. A mistake in our data?"); + for (int i = unmatchedCharsBegin; i < unmatchedCharsEnd; ++i) { + QCOMPARE(findAsciiIdentifierLength(str.midRef(i)), 0); + } +} + +void TestStringHandler::testFindAsciiIdentifierLengthNoMatch_data() +{ + addUnmatchedAsciiIdentifierData(); +} + +void TestStringHandler::testMatchUnbracedAsciiVariable() +{ + QFETCH(QString, str); + QFETCH(int, identifierBegin); + QFETCH(int, identifierEnd); + + const auto length = identifierEnd - identifierBegin; + Q_ASSERT_X(length >= 0, Q_FUNC_INFO, "Wrong data."); + const auto match = matchPossiblyBracedAsciiVariable(str.midRef(identifierBegin)); + QCOMPARE(match.length, length); + QCOMPARE(match.name, str.mid(identifierBegin, length)); +} + +void TestStringHandler::testMatchUnbracedAsciiVariable_data() +{ + addAsciiIdentifierData(); + + QString str; + + str = "a{b"; + QTest::newRow("no closing brace") << str << 1 << 1; + + str = "1x}"; + QTest::newRow("no opening brace") << str << 1 << 2; + + str = "{{u}}"; + QTest::newRow("nested braces") << str << 0 << 0; +} + +void TestStringHandler::testUnmatchedAsciiVariable() +{ + QFETCH(QString, str); + QFETCH(int, unmatchedCharsBegin); + QFETCH(int, unmatchedCharsEnd); + + Q_ASSERT_X(unmatchedCharsBegin < unmatchedCharsEnd, Q_FUNC_INFO, + "Nothing to test. A mistake in our data?"); + for (int i = unmatchedCharsBegin; i < unmatchedCharsEnd; ++i) { + const auto match = matchPossiblyBracedAsciiVariable(str.midRef(i)); + QCOMPARE(match.length, 0); + QCOMPARE(match.name, QString{}); + } +} + +void TestStringHandler::testUnmatchedAsciiVariable_data() +{ + addUnmatchedAsciiIdentifierData(); + + QTest::newRow("empty braces") << QStringLiteral("{}a}") << 0 << 2; + QTest::newRow("a digit inside braces") << QStringLiteral("{4}") << 0 << 3; + QTest::newRow("a non-ID character inside braces") << QStringLiteral("a{!}b") << 1 << 4; +} + +void TestStringHandler::testMatchBracedAsciiVariable() +{ + QFETCH(QString, str); + QFETCH(int, openingBraceIndex); + QFETCH(int, closingBraceIndex); + + const auto variableLength = closingBraceIndex - openingBraceIndex - 1; + Q_ASSERT_X(variableLength > 0, Q_FUNC_INFO, "Wrong data."); + const auto match = matchPossiblyBracedAsciiVariable(str.midRef(openingBraceIndex)); + QCOMPARE(match.length, variableLength + 2); + QCOMPARE(match.name, str.mid(openingBraceIndex + 1, variableLength)); +} + +void TestStringHandler::testMatchBracedAsciiVariable_data() +{ + QTest::addColumn("str"); + QTest::addColumn("openingBraceIndex"); + QTest::addColumn("closingBraceIndex"); + + QString str; + + str = "{a1}-a{_}"; + QTest::newRow("variable at index 0") << str << 0 << str.indexOf('}'); + QTest::newRow("variable ends at string end") << str << str.lastIndexOf('{') << str.size() - 1; + + str = "{a2_bDX__45Ek}"; + QTest::newRow("entire-string variable") << str << 0 << str.size() - 1; +} + void TestStringHandler::testStripAnsiSequences() { QFETCH(QString, input); QFETCH(QString, expectedOutput); const auto output = stripAnsiSequences(input); QCOMPARE(output, expectedOutput); } void TestStringHandler::testStripAnsiSequences_data() { QTest::addColumn("input"); QTest::addColumn("expectedOutput"); QTest::newRow("simple") << QStringLiteral("foo bar:") << "foo bar:"; } void TestStringHandler::testNormalizeLineEndings() { QFETCH(QByteArray, text); QFETCH(QByteArray, expectedOutput); normalizeLineEndings(text); QCOMPARE(text, expectedOutput); } void TestStringHandler::testNormalizeLineEndings_data() { QTest::addColumn("text"); QTest::addColumn("expectedOutput"); QTest::newRow("trivial") << QByteArray("foo\nbar\n") << QByteArray("foo\nbar\n"); QTest::newRow("dos") << QByteArray("foo\r\nbar\r\n") << QByteArray("foo\nbar\n"); QTest::newRow("macos_classic") << QByteArray("foo\rbar\r") << QByteArray("foo\nbar\n"); QTest::newRow("mess") << QByteArray("\r\n\n\r\r\r\n\r") << QByteArray("\n\n\n\n\n\n"); } diff --git a/kdevplatform/util/tests/test_stringhandler.h b/kdevplatform/util/tests/test_stringhandler.h index 24e9139058..435ff6c16f 100644 --- a/kdevplatform/util/tests/test_stringhandler.h +++ b/kdevplatform/util/tests/test_stringhandler.h @@ -1,42 +1,54 @@ /* * Copyright 2014 Kevin Funk * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . * */ #ifndef TESTSTRINGHANDLER_H #define TESTSTRINGHANDLER_H #include class TestStringHandler : public QObject { Q_OBJECT private Q_SLOTS: void testHtmlToPlainText(); void testHtmlToPlainText_data(); + void testFindAsciiIdentifierLength(); + void testFindAsciiIdentifierLength_data(); + void testFindAsciiIdentifierLengthNoMatch(); + void testFindAsciiIdentifierLengthNoMatch_data(); + + void testMatchUnbracedAsciiVariable(); + void testMatchUnbracedAsciiVariable_data(); + void testUnmatchedAsciiVariable(); + void testUnmatchedAsciiVariable_data(); + void testMatchBracedAsciiVariable(); + void testMatchBracedAsciiVariable_data(); + void testStripAnsiSequences(); void testStripAnsiSequences_data(); void testNormalizeLineEndings(); void testNormalizeLineEndings_data(); }; #endif // TESTSTRINGHANDLER_H