From 5a2261d0bdcbd0c2d79124c46750c8eef56d9f86 Mon Sep 17 00:00:00 2001 From: Marco Bubke Date: Mon, 28 Apr 2025 23:44:32 +0200 Subject: [PATCH] QmlDesigner: Refactor escape and deescape functions - Replaced QString::replace with manual text handling to avoid multiple loops. - Changed parameter type from QString to QStringView for better performance. - Improved handling of special characters and reserved memory for efficiency. Change-Id: I984df7b555f6e0fc115575e4e7e0606b55703520 Reviewed-by: Burak Hancerli Reviewed-by: Thomas Hartmann --- .../libs/qmldesignerutils/stringutils.h | 106 +++++++++++--- .../qmldesignerutils/stringutils-test.cpp | 136 ++++++++++++++++++ 2 files changed, 222 insertions(+), 20 deletions(-) diff --git a/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h b/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h index d6e7e72b0fc..4b6c92c9b40 100644 --- a/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h +++ b/src/plugins/qmldesigner/libs/qmldesignerutils/stringutils.h @@ -11,40 +11,106 @@ namespace QmlDesigner::StringUtils { -inline QString escape(const QString &value) +inline QString escape(QStringView text) { using namespace Qt::StringLiterals; - if (value.length() == 6 && value.startsWith("\\u")) //Do not double escape unicode chars - return value; + if (text.size() == 6 && text.startsWith(u"\\u")) //Do not double escape unicode chars + return text.toString(); - QString result = value; + QString escapedText; + escapedText.reserve(text.size() * 2); - result.replace("\\"_L1, "\\\\"_L1); - result.replace("\""_L1, "\\\""_L1); - result.replace("\t"_L1, "\\t"_L1); - result.replace("\r"_L1, "\\r"_L1); - result.replace("\n"_L1, "\\n"_L1); + const auto end = text.end(); + auto current = text.begin(); + QStringView pattern = u"\\\"\t\r\n"; + while (current != end) { + auto found = std::ranges::find_first_of(current, end, pattern.begin(), pattern.end()); + escapedText.append(QStringView{current, found}); - return result; + if (found == end) + break; + + QChar c = *found; + switch (c.unicode()) { + case u'\\': + escapedText.append(u"\\\\"); + break; + case u'\"': + escapedText.append(u"\\\""); + break; + case u'\t': + escapedText.append(u"\\t"); + break; + case u'\r': + escapedText.append(u"\\r"); + break; + case u'\n': + escapedText.append(u"\\n"); + break; + } + + current = std::next(found); + } + + return escapedText; } -inline QString deescape(const QString &value) +inline QString deescape(QStringView text) { using namespace Qt::StringLiterals; - if (value.length() == 6 && value.startsWith("\\u")) //Ignore unicode chars - return value; + if (text.isEmpty() || (text.size() == 6 && text.startsWith(u"\\u"))) //Ignore unicode chars + return text.toString(); - QString result = value; + QString deescapedText; + deescapedText.reserve(text.size()); - result.replace("\\\\"_L1, "\\"_L1); - result.replace("\\\""_L1, "\""_L1); - result.replace("\\t"_L1, "\t"_L1); - result.replace("\\r"_L1, "\r"_L1); - result.replace("\\n"_L1, "\n"_L1); + const auto end = text.end(); + auto current = text.begin(); + while (current != end) { + auto found = std::ranges::find(current, end, u'\\'); + deescapedText.append(QStringView{current, found}); - return result; + if (found == end) + break; + + current = std::next(found); + + if (current == end) { + deescapedText.append(u'\\'); + break; + } + + QChar c = *current; + switch (c.unicode()) { + case u'\\': + deescapedText.append(u'\\'); + current = std::next(current); + break; + case u'\"': + deescapedText.append(u'\"'); + current = std::next(current); + break; + case u't': + deescapedText.append(u'\t'); + current = std::next(current); + break; + case u'r': + deescapedText.append(u'\r'); + current = std::next(current); + break; + case u'n': + deescapedText.append(u'\n'); + current = std::next(current); + break; + default: + deescapedText.append(u'\\'); + break; + } + } + + return deescapedText; } template diff --git a/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp b/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp index bf53f4ed59e..0f1824ebf82 100644 --- a/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp +++ b/tests/unit/tests/unittests/qmldesignerutils/stringutils-test.cpp @@ -6,6 +6,8 @@ namespace { +using namespace Qt::StringLiterals; + using QmlDesigner::StringUtils::split_last; TEST(StringUtils_split_last, leaf_is_empty_for_empty_input) @@ -79,4 +81,138 @@ TEST(StringUtils_split_last, no_steam_for_not_dot) ASSERT_THAT(steam, IsEmpty()); } + +using ConvertFunction = QString (*)(QStringView); + +struct EscapeParameters +{ + QString input; + QString output; + std::string name; + ConvertFunction convert; +}; + +class escaping : public testing::TestWithParam +{ +public: + escaping() + : inputTerm{GetParam().input} + , outputTerm{GetParam().output} + , name{GetParam().name} + , convert{GetParam().convert} + {} + +public: + const QString &inputTerm; + const QString &outputTerm; + const std::string &name; + ConvertFunction convert; +}; + +auto excape_printer = [](const testing::TestParamInfo &info) { + return info.param.name; +}; + +INSTANTIATE_TEST_SUITE_P( + StringUtils, + escaping, + testing::Values( + EscapeParameters(u"\\"_s, u"\\\\"_s, "escape_backslash", QmlDesigner::StringUtils::escape), + EscapeParameters(u"\""_s, u"\\\""_s, "escape_quote", QmlDesigner::StringUtils::escape), + EscapeParameters(u"\t"_s, u"\\t"_s, "escape_tab", QmlDesigner::StringUtils::escape), + EscapeParameters(u"\n"_s, u"\\n"_s, "escape_new_line", QmlDesigner::StringUtils::escape), + EscapeParameters(u"\r"_s, u"\\r"_s, "escape_carriage_return", QmlDesigner::StringUtils::escape), + EscapeParameters(u"\\"_s, u"\\"_s, "deescape_backslash", QmlDesigner::StringUtils::deescape), + EscapeParameters(u"\\\\"_s, u"\\"_s, "deescape_double_backslash", QmlDesigner::StringUtils::deescape), + EscapeParameters(u"\\\""_s, u"\""_s, "deescape_quote", QmlDesigner::StringUtils::deescape), + EscapeParameters(u"\\t"_s, u"\t"_s, "deescape_tab", QmlDesigner::StringUtils::deescape), + EscapeParameters(u"\\n"_s, u"\n"_s, "deescape_new_line", QmlDesigner::StringUtils::deescape), + EscapeParameters( + u"\\r"_s, u"\r"_s, "deescape_carriage_return", QmlDesigner::StringUtils::deescape)), + excape_printer); + +TEST_P(escaping, begin) +{ + QString input = inputTerm + "foo"; + + auto converted = convert(input); + + ASSERT_THAT(converted, outputTerm + "foo"); +} + +TEST_P(escaping, empty) +{ + QString input; + + auto converted = convert(input); + + ASSERT_THAT(converted, IsEmpty()); +} + +TEST_P(escaping, only) +{ + QString input = inputTerm; + + auto converted = convert(input); + + ASSERT_THAT(converted, outputTerm); +} + +TEST_P(escaping, nothing) +{ + QString input = "foobar"; + + auto converted = convert(input); + + ASSERT_THAT(converted, "foobar"); +} + +TEST_P(escaping, end) +{ + QString input = "foo" + inputTerm; + + auto converted = convert(input); + + ASSERT_THAT(converted, "foo" + outputTerm); +} + +TEST_P(escaping, middle) +{ + QString input = "foo" + inputTerm + "bar"; + + auto converted = convert(input); + + ASSERT_THAT(converted, "foo" + outputTerm + "bar"); +} + +TEST_P(escaping, multiple) +{ + QString input = "foo" + inputTerm + "bar" + inputTerm + "foo"; + + auto converted = convert(input); + + ASSERT_THAT(converted, "foo" + outputTerm + "bar" + outputTerm + "foo"); +} + +TEST_P(escaping, multiple_in_row) +{ + if (name == "deescape_backslash") // would be double backslash + return; + QString input = "foo" + inputTerm + inputTerm + "foo"; + + auto converted = convert(input); + + ASSERT_THAT(converted, "foo" + outputTerm + outputTerm + "foo"); +} + +TEST_P(escaping, skip_unicode) +{ + QString input = "\\u" + inputTerm + "foo"; + input.resize(6); + + auto converted = convert(input); + + ASSERT_THAT(converted, input); +} + } // namespace