diff options
Diffstat (limited to 'src/plugins/cppeditor/quickfixes/convertstringliteral.cpp')
-rw-r--r-- | src/plugins/cppeditor/quickfixes/convertstringliteral.cpp | 746 |
1 files changed, 746 insertions, 0 deletions
diff --git a/src/plugins/cppeditor/quickfixes/convertstringliteral.cpp b/src/plugins/cppeditor/quickfixes/convertstringliteral.cpp new file mode 100644 index 0000000000..eff4b3a9ae --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/convertstringliteral.cpp @@ -0,0 +1,746 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "convertstringliteral.h" + +#include "../cppeditordocument.h" +#include "../cppeditortr.h" +#include "../cppeditorwidget.h" +#include "../cpprefactoringchanges.h" +#include "cppquickfix.h" + +#include <QTextDecoder> + +#ifdef WITH_TESTS +#include "cppquickfix_test.h" +#include <QtTest> +#endif + +using namespace CPlusPlus; +using namespace Utils; + +namespace CppEditor::Internal { +namespace { + +enum StringLiteralType { TypeString, TypeObjCString, TypeChar, TypeNone }; + +enum ActionFlags { + EncloseInQLatin1CharAction = 0x1, + EncloseInQLatin1StringAction = 0x2, + EncloseInQStringLiteralAction = 0x4, + EncloseInQByteArrayLiteralAction = 0x8, + EncloseActionMask = EncloseInQLatin1CharAction | EncloseInQLatin1StringAction + | EncloseInQStringLiteralAction | EncloseInQByteArrayLiteralAction, + TranslateTrAction = 0x10, + TranslateQCoreApplicationAction = 0x20, + TranslateNoopAction = 0x40, + TranslationMask = TranslateTrAction | TranslateQCoreApplicationAction | TranslateNoopAction, + RemoveObjectiveCAction = 0x100, + ConvertEscapeSequencesToCharAction = 0x200, + ConvertEscapeSequencesToStringAction = 0x400, + SingleQuoteAction = 0x800, + DoubleQuoteAction = 0x1000 +}; + +static bool isQtStringLiteral(const QByteArray &id) +{ + return id == "QLatin1String" || id == "QLatin1Literal" || id == "QStringLiteral" + || id == "QByteArrayLiteral"; +} + +static bool isQtStringTranslation(const QByteArray &id) +{ + return id == "tr" || id == "trUtf8" || id == "translate" || id == "QT_TRANSLATE_NOOP"; +} + +/* Convert single-character string literals into character literals with some + * special cases "a" --> 'a', "'" --> '\'', "\n" --> '\n', "\"" --> '"'. */ +static QByteArray stringToCharEscapeSequences(const QByteArray &content) +{ + if (content.size() == 1) + return content.at(0) == '\'' ? QByteArray("\\'") : content; + if (content.size() == 2 && content.at(0) == '\\') + return content == "\\\"" ? QByteArray(1, '"') : content; + return QByteArray(); +} + +/* Convert character literal into a string literal with some special cases + * 'a' -> "a", '\n' -> "\n", '\'' --> "'", '"' --> "\"". */ +static QByteArray charToStringEscapeSequences(const QByteArray &content) +{ + if (content.size() == 1) + return content.at(0) == '"' ? QByteArray("\\\"") : content; + if (content.size() == 2) + return content == "\\'" ? QByteArray("'") : content; + return QByteArray(); +} + +static QString msgQtStringLiteralDescription(const QString &replacement) +{ + return Tr::tr("Enclose in %1(...)").arg(replacement); +} + +static QString stringLiteralReplacement(unsigned actions) +{ + if (actions & EncloseInQLatin1CharAction) + return QLatin1String("QLatin1Char"); + if (actions & EncloseInQLatin1StringAction) + return QLatin1String("QLatin1String"); + if (actions & EncloseInQStringLiteralAction) + return QLatin1String("QStringLiteral"); + if (actions & EncloseInQByteArrayLiteralAction) + return QLatin1String("QByteArrayLiteral"); + if (actions & TranslateTrAction) + return QLatin1String("tr"); + if (actions & TranslateQCoreApplicationAction) + return QLatin1String("QCoreApplication::translate"); + if (actions & TranslateNoopAction) + return QLatin1String("QT_TRANSLATE_NOOP"); + return QString(); +} + +static ExpressionAST *analyzeStringLiteral(const QList<AST *> &path, + const CppRefactoringFilePtr &file, StringLiteralType *type, + QByteArray *enclosingFunction = nullptr, + CallAST **enclosingFunctionCall = nullptr) +{ + *type = TypeNone; + if (enclosingFunction) + enclosingFunction->clear(); + if (enclosingFunctionCall) + *enclosingFunctionCall = nullptr; + + if (path.isEmpty()) + return nullptr; + + ExpressionAST *literal = path.last()->asExpression(); + if (literal) { + if (literal->asStringLiteral()) { + // Check for Objective C string (@"bla") + const QChar firstChar = file->charAt(file->startOf(literal)); + *type = firstChar == QLatin1Char('@') ? TypeObjCString : TypeString; + } else if (NumericLiteralAST *numericLiteral = literal->asNumericLiteral()) { + // character ('c') constants are numeric. + if (file->tokenAt(numericLiteral->literal_token).is(T_CHAR_LITERAL)) + *type = TypeChar; + } + } + + if (*type != TypeNone && enclosingFunction && path.size() > 1) { + if (CallAST *call = path.at(path.size() - 2)->asCall()) { + if (call->base_expression) { + if (IdExpressionAST *idExpr = call->base_expression->asIdExpression()) { + if (SimpleNameAST *functionName = idExpr->name->asSimpleName()) { + *enclosingFunction = file->tokenAt(functionName->identifier_token).identifier->chars(); + if (enclosingFunctionCall) + *enclosingFunctionCall = call; + } + } + } + } + } + return literal; +} + +class EscapeStringLiteralOperation: public CppQuickFixOperation +{ +public: + EscapeStringLiteralOperation(const CppQuickFixInterface &interface, + ExpressionAST *literal, bool escape) + : CppQuickFixOperation(interface) + , m_literal(literal) + , m_escape(escape) + { + if (m_escape) { + setDescription(Tr::tr("Escape String Literal as UTF-8")); + } else { + setDescription(Tr::tr("Unescape String Literal as UTF-8")); + } + } + +private: + static inline bool isDigit(quint8 ch, int base) + { + if (base == 8) + return ch >= '0' && ch < '8'; + if (base == 16) + return isxdigit(ch); + return false; + } + + static QByteArrayList escapeString(const QByteArray &contents) + { + QByteArrayList newContents; + QByteArray chunk; + bool wasEscaped = false; + for (const quint8 c : contents) { + const bool needsEscape = !isascii(c) || !isprint(c); + if (!needsEscape && wasEscaped && std::isxdigit(c) && !chunk.isEmpty()) { + newContents << chunk; + chunk.clear(); + } + if (needsEscape) + chunk += QByteArray("\\x") + QByteArray::number(c, 16).rightJustified(2, '0'); + else + chunk += c; + wasEscaped = needsEscape; + } + if (!chunk.isEmpty()) + newContents << chunk; + return newContents; + } + + static QByteArray unescapeString(const QByteArray &contents) + { + QByteArray newContents; + const int len = contents.length(); + for (int i = 0; i < len; ++i) { + quint8 c = contents.at(i); + if (c == '\\' && i < len - 1) { + int idx = i + 1; + quint8 ch = contents.at(idx); + int base = 0; + int maxlen = 0; + if (isDigit(ch, 8)) { + base = 8; + maxlen = 3; + } else if ((ch == 'x' || ch == 'X') && idx < len - 1) { + base = 16; + maxlen = 2; + ch = contents.at(++idx); + } + if (base > 0) { + QByteArray buf; + while (isDigit(ch, base) && idx < len && buf.length() < maxlen) { + buf += ch; + ++idx; + if (idx == len) + break; + ch = contents.at(idx); + } + if (!buf.isEmpty()) { + bool ok; + uint value = buf.toUInt(&ok, base); + // Don't unescape isascii() && !isprint() + if (ok && (!isascii(value) || isprint(value))) { + newContents += value; + i = idx - 1; + continue; + } + } + } + newContents += c; + c = contents.at(++i); + } + newContents += c; + } + return newContents; + } + + // QuickFixOperation interface +public: + void perform() override + { + const int startPos = currentFile()->startOf(m_literal); + const int endPos = currentFile()->endOf(m_literal); + + StringLiteralAST *stringLiteral = m_literal->asStringLiteral(); + QTC_ASSERT(stringLiteral, return); + const QByteArray oldContents(currentFile()->tokenAt(stringLiteral->literal_token). + identifier->chars()); + QByteArrayList newContents; + if (m_escape) + newContents = escapeString(oldContents); + else + newContents = {unescapeString(oldContents)}; + + if (newContents.isEmpty() + || (newContents.size() == 1 && newContents.first() == oldContents)) { + return; + } + + QTextCodec *utf8codec = QTextCodec::codecForName("UTF-8"); + QScopedPointer<QTextDecoder> decoder(utf8codec->makeDecoder()); + ChangeSet changes; + + bool replace = true; + for (const QByteArray &chunk : std::as_const(newContents)) { + const QString str = decoder->toUnicode(chunk); + const QByteArray utf8buf = str.toUtf8(); + if (!utf8codec->canEncode(str) || chunk != utf8buf) + return; + if (replace) + changes.replace(startPos + 1, endPos - 1, str); + else + changes.insert(endPos, "\"" + str + "\""); + replace = false; + } + currentFile()->apply(changes); + } + +private: + ExpressionAST *m_literal; + bool m_escape; +}; + +/// Operation performs the operations of type ActionFlags passed in as actions. +class WrapStringLiteralOp : public CppQuickFixOperation +{ +public: + WrapStringLiteralOp(const CppQuickFixInterface &interface, int priority, + unsigned actions, const QString &description, ExpressionAST *literal, + const QString &translationContext = QString()) + : CppQuickFixOperation(interface, priority), m_actions(actions), m_literal(literal), + m_translationContext(translationContext) + { + setDescription(description); + } + + void perform() override + { + ChangeSet changes; + + const int startPos = currentFile()->startOf(m_literal); + const int endPos = currentFile()->endOf(m_literal); + + // kill leading '@'. No need to adapt endPos, that is done by ChangeSet + if (m_actions & RemoveObjectiveCAction) + changes.remove(startPos, startPos + 1); + + // Fix quotes + if (m_actions & (SingleQuoteAction | DoubleQuoteAction)) { + const QString newQuote((m_actions & SingleQuoteAction) + ? QLatin1Char('\'') : QLatin1Char('"')); + changes.replace(startPos, startPos + 1, newQuote); + changes.replace(endPos - 1, endPos, newQuote); + } + + // Convert single character strings into character constants + if (m_actions & ConvertEscapeSequencesToCharAction) { + StringLiteralAST *stringLiteral = m_literal->asStringLiteral(); + QTC_ASSERT(stringLiteral, return ;); + const QByteArray oldContents(currentFile()->tokenAt(stringLiteral->literal_token).identifier->chars()); + const QByteArray newContents = stringToCharEscapeSequences(oldContents); + QTC_ASSERT(!newContents.isEmpty(), return ;); + if (oldContents != newContents) + changes.replace(startPos + 1, endPos -1, QString::fromLatin1(newContents)); + } + + // Convert character constants into strings constants + if (m_actions & ConvertEscapeSequencesToStringAction) { + NumericLiteralAST *charLiteral = m_literal->asNumericLiteral(); // char 'c' constants are numerical. + QTC_ASSERT(charLiteral, return ;); + const QByteArray oldContents(currentFile()->tokenAt(charLiteral->literal_token).identifier->chars()); + const QByteArray newContents = charToStringEscapeSequences(oldContents); + QTC_ASSERT(!newContents.isEmpty(), return ;); + if (oldContents != newContents) + changes.replace(startPos + 1, endPos -1, QString::fromLatin1(newContents)); + } + + // Enclose in literal or translation function, macro. + if (m_actions & (EncloseActionMask | TranslationMask)) { + changes.insert(endPos, QString(QLatin1Char(')'))); + QString leading = stringLiteralReplacement(m_actions); + leading += QLatin1Char('('); + if (m_actions + & (TranslateQCoreApplicationAction | TranslateNoopAction)) { + leading += QLatin1Char('"'); + leading += m_translationContext; + leading += QLatin1String("\", "); + } + changes.insert(startPos, leading); + } + + currentFile()->apply(changes); + } + +private: + const unsigned m_actions; + ExpressionAST *m_literal; + const QString m_translationContext; +}; + +class ConvertCStringToNSStringOp: public CppQuickFixOperation +{ +public: + ConvertCStringToNSStringOp(const CppQuickFixInterface &interface, int priority, + StringLiteralAST *stringLiteral, CallAST *qlatin1Call) + : CppQuickFixOperation(interface, priority) + , stringLiteral(stringLiteral) + , qlatin1Call(qlatin1Call) + { + setDescription(Tr::tr("Convert to Objective-C String Literal")); + } + + void perform() override + { + ChangeSet changes; + + if (qlatin1Call) { + changes.replace(currentFile()->startOf(qlatin1Call), currentFile()->startOf(stringLiteral), + QLatin1String("@")); + changes.remove(currentFile()->endOf(stringLiteral), currentFile()->endOf(qlatin1Call)); + } else { + changes.insert(currentFile()->startOf(stringLiteral), QLatin1String("@")); + } + + currentFile()->apply(changes); + } + +private: + StringLiteralAST *stringLiteral; + CallAST *qlatin1Call; +}; + +/*! + Replace + "abcd" + QLatin1String("abcd") + QLatin1Literal("abcd") + + With + @"abcd" + + Activates on: the string literal, if the file type is a Objective-C(++) file. +*/ +class ConvertCStringToNSString: public CppQuickFixFactory +{ +#ifdef WITH_TESTS +public: + static QObject *createTest(); +#endif + +private: + void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override + { + CppRefactoringFilePtr file = interface.currentFile(); + + if (!interface.editor()->cppEditorDocument()->isObjCEnabled()) + return; + + StringLiteralType type = TypeNone; + QByteArray enclosingFunction; + CallAST *qlatin1Call; + const QList<AST *> &path = interface.path(); + ExpressionAST *literal = analyzeStringLiteral(path, file, &type, &enclosingFunction, + &qlatin1Call); + if (!literal || type != TypeString) + return; + if (!isQtStringLiteral(enclosingFunction)) + qlatin1Call = nullptr; + + result << new ConvertCStringToNSStringOp(interface, path.size() - 1, literal->asStringLiteral(), + qlatin1Call); + } +}; + +/*! + Replace + "abcd" + + With + tr("abcd") or + QCoreApplication::translate("CONTEXT", "abcd") or + QT_TRANSLATE_NOOP("GLOBAL", "abcd") + + depending on what is available. + + Activates on: the string literal +*/ +class TranslateStringLiteral: public CppQuickFixFactory +{ +#ifdef WITH_TESTS +public: + static QObject *createTest(); +#endif + +private: + void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override + { + // Initialize + StringLiteralType type = TypeNone; + QByteArray enclosingFunction; + const QList<AST *> &path = interface.path(); + CppRefactoringFilePtr file = interface.currentFile(); + ExpressionAST *literal = analyzeStringLiteral(path, file, &type, &enclosingFunction); + if (!literal || type != TypeString + || isQtStringLiteral(enclosingFunction) || isQtStringTranslation(enclosingFunction)) + return; + + QString trContext; + + std::shared_ptr<Control> control = interface.context().bindings()->control(); + const Name *trName = control->identifier("tr"); + + // Check whether we are in a function: + const QString description = Tr::tr("Mark as Translatable"); + for (int i = path.size() - 1; i >= 0; --i) { + if (FunctionDefinitionAST *definition = path.at(i)->asFunctionDefinition()) { + Function *function = definition->symbol; + ClassOrNamespace *b = interface.context().lookupType(function); + if (b) { + // Do we have a tr function? + const QList<LookupItem> items = b->find(trName); + for (const LookupItem &r : items) { + Symbol *s = r.declaration(); + if (s->type()->asFunctionType()) { + // no context required for tr + result << new WrapStringLiteralOp(interface, path.size() - 1, + TranslateTrAction, + description, literal); + return; + } + } + } + // We need to do a QCA::translate, so we need a context. + // Use fully qualified class name: + Overview oo; + const QList<const Name *> names = LookupContext::path(function); + for (const Name *n : names) { + if (!trContext.isEmpty()) + trContext.append(QLatin1String("::")); + trContext.append(oo.prettyName(n)); + } + // ... or global if none available! + if (trContext.isEmpty()) + trContext = QLatin1String("GLOBAL"); + result << new WrapStringLiteralOp(interface, path.size() - 1, + TranslateQCoreApplicationAction, + description, literal, trContext); + return; + } + } + + // We need to use Q_TRANSLATE_NOOP + result << new WrapStringLiteralOp(interface, path.size() - 1, + TranslateNoopAction, + description, literal, trContext); + } +}; + +/*! + Replace + "abcd" -> QLatin1String("abcd") + @"abcd" -> QLatin1String("abcd") (Objective C) + 'a' -> QLatin1Char('a') + 'a' -> "a" + "a" -> 'a' or QLatin1Char('a') (Single character string constants) + "\n" -> '\n', QLatin1Char('\n') + + Except if they are already enclosed in + QLatin1Char, QT_TRANSLATE_NOOP, tr, + trUtf8, QLatin1Literal, QLatin1String + + Activates on: the string or character literal +*/ + +class WrapStringLiteral: public CppQuickFixFactory +{ +#ifdef WITH_TESTS +public: + static QObject *createTest(); +#endif + +private: + void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override + { + StringLiteralType type = TypeNone; + QByteArray enclosingFunction; + const QList<AST *> &path = interface.path(); + CppRefactoringFilePtr file = interface.currentFile(); + ExpressionAST *literal = analyzeStringLiteral(path, file, &type, &enclosingFunction); + if (!literal || type == TypeNone) + return; + if ((type == TypeChar && enclosingFunction == "QLatin1Char") + || isQtStringLiteral(enclosingFunction) + || isQtStringTranslation(enclosingFunction)) + return; + + const int priority = path.size() - 1; // very high priority + if (type == TypeChar) { + unsigned actions = EncloseInQLatin1CharAction; + QString description = msgQtStringLiteralDescription(stringLiteralReplacement(actions)); + result << new WrapStringLiteralOp(interface, priority, actions, description, literal); + if (NumericLiteralAST *charLiteral = literal->asNumericLiteral()) { + const QByteArray contents(file->tokenAt(charLiteral->literal_token).identifier->chars()); + if (!charToStringEscapeSequences(contents).isEmpty()) { + actions = DoubleQuoteAction | ConvertEscapeSequencesToStringAction; + description = Tr::tr("Convert to String Literal"); + result << new WrapStringLiteralOp(interface, priority, actions, + description, literal); + } + } + } else { + const unsigned objectiveCActions = type == TypeObjCString ? + unsigned(RemoveObjectiveCAction) : 0u; + unsigned actions = 0; + if (StringLiteralAST *stringLiteral = literal->asStringLiteral()) { + const QByteArray contents(file->tokenAt(stringLiteral->literal_token).identifier->chars()); + if (!stringToCharEscapeSequences(contents).isEmpty()) { + actions = EncloseInQLatin1CharAction | SingleQuoteAction + | ConvertEscapeSequencesToCharAction | objectiveCActions; + QString description = + Tr::tr("Convert to Character Literal and Enclose in QLatin1Char(...)"); + result << new WrapStringLiteralOp(interface, priority, actions, + description, literal); + actions &= ~EncloseInQLatin1CharAction; + description = Tr::tr("Convert to Character Literal"); + result << new WrapStringLiteralOp(interface, priority, actions, + description, literal); + } + } + actions = EncloseInQLatin1StringAction | objectiveCActions; + result << new WrapStringLiteralOp(interface, priority, actions, + msgQtStringLiteralDescription(stringLiteralReplacement(actions)), literal); + actions = EncloseInQStringLiteralAction | objectiveCActions; + result << new WrapStringLiteralOp(interface, priority, actions, + msgQtStringLiteralDescription(stringLiteralReplacement(actions)), literal); + actions = EncloseInQByteArrayLiteralAction | objectiveCActions; + result << new WrapStringLiteralOp(interface, priority, actions, + msgQtStringLiteralDescription(stringLiteralReplacement(actions)), literal); + } + } +}; + +/*! + Escapes or unescapes a string literal as UTF-8. + + Escapes non-ASCII characters in a string literal to hexadecimal escape sequences. + Unescapes octal or hexadecimal escape sequences in a string literal. + String literals are handled as UTF-8 even if file's encoding is not UTF-8. + */ +class EscapeStringLiteral : public CppQuickFixFactory +{ +#ifdef WITH_TESTS +public: + static QObject *createTest(); +#endif + +private: + void doMatch(const CppQuickFixInterface &interface, TextEditor::QuickFixOperations &result) override + { + const QList<AST *> &path = interface.path(); + if (path.isEmpty()) + return; + + AST * const lastAst = path.last(); + ExpressionAST *literal = lastAst->asStringLiteral(); + if (!literal) + return; + + StringLiteralAST *stringLiteral = literal->asStringLiteral(); + CppRefactoringFilePtr file = interface.currentFile(); + const QByteArray contents(file->tokenAt(stringLiteral->literal_token).identifier->chars()); + + bool canEscape = false; + bool canUnescape = false; + for (int i = 0; i < contents.length(); ++i) { + quint8 c = contents.at(i); + if (!isascii(c) || !isprint(c)) { + canEscape = true; + } else if (c == '\\' && i < contents.length() - 1) { + c = contents.at(++i); + if ((c >= '0' && c < '8') || c == 'x' || c == 'X') + canUnescape = true; + } + } + + if (canEscape) + result << new EscapeStringLiteralOperation(interface, literal, true); + + if (canUnescape) + result << new EscapeStringLiteralOperation(interface, literal, false); + } +}; + +#ifdef WITH_TESTS +using namespace Tests; + +class EscapeStringLiteralTest : public QObject +{ + Q_OBJECT + +private slots: + void test_data() + { + QTest::addColumn<QByteArray>("original"); + QTest::addColumn<QByteArray>("expected"); + + // Escape String Literal as UTF-8 (no-trigger) + QTest::newRow("EscapeStringLiteral_notrigger") + << QByteArray("const char *notrigger = \"@abcdef \\a\\n\\\\\";\n") + << QByteArray(); + + // Escape String Literal as UTF-8 + QTest::newRow("EscapeStringLiteral") + << QByteArray("const char *utf8 = \"@\xe3\x81\x82\xe3\x81\x84\";\n") + << QByteArray("const char *utf8 = \"\\xe3\\x81\\x82\\xe3\\x81\\x84\";\n"); + + // Unescape String Literal as UTF-8 (from hexdecimal escape sequences) + QTest::newRow("UnescapeStringLiteral_hex") + << QByteArray("const char *hex_escaped = \"@\\xe3\\x81\\x82\\xe3\\x81\\x84\";\n") + << QByteArray("const char *hex_escaped = \"\xe3\x81\x82\xe3\x81\x84\";\n"); + + // Unescape String Literal as UTF-8 (from octal escape sequences) + QTest::newRow("UnescapeStringLiteral_oct") + << QByteArray("const char *oct_escaped = \"@\\343\\201\\202\\343\\201\\204\";\n") + << QByteArray("const char *oct_escaped = \"\xe3\x81\x82\xe3\x81\x84\";\n"); + + // Unescape String Literal as UTF-8 (triggered but no change) + QTest::newRow("UnescapeStringLiteral_noconv") + << QByteArray("const char *escaped_ascii = \"@\\x1b\";\n") + << QByteArray("const char *escaped_ascii = \"\\x1b\";\n"); + + // Unescape String Literal as UTF-8 (no conversion because of invalid utf-8) + QTest::newRow("UnescapeStringLiteral_invalid") + << QByteArray("const char *escaped = \"@\\xe3\\x81\";\n") + << QByteArray("const char *escaped = \"\\xe3\\x81\";\n"); + + QTest::newRow("escape string literal: simple case") + << QByteArray(R"(const char *str = @"àxyz";)") + << QByteArray(R"(const char *str = "\xc3\xa0xyz";)"); + QTest::newRow("escape string literal: simple case reverse") + << QByteArray(R"(const char *str = @"\xc3\xa0xyz";)") + << QByteArray(R"(const char *str = "àxyz";)"); + QTest::newRow("escape string literal: raw string literal") + << QByteArray(R"x(const char *str = @R"(àxyz)";)x") + << QByteArray(R"x(const char *str = R"(\xc3\xa0xyz)";)x"); + QTest::newRow("escape string literal: splitting required") + << QByteArray(R"(const char *str = @"àf23бgб1";)") + << QByteArray(R"(const char *str = "\xc3\xa0""f23\xd0\xb1g\xd0\xb1""1";)"); + QTest::newRow("escape string literal: unescape adjacent literals") + << QByteArray(R"(const char *str = @"\xc3\xa0""f23\xd0\xb1g\xd0\xb1""1";)") + << QByteArray(R"(const char *str = "àf23бgб1";)"); + } + + void test() + { + QFETCH(QByteArray, original); + QFETCH(QByteArray, expected); + + EscapeStringLiteral factory; + QuickFixOperationTest(singleDocument(original, expected), &factory); + } +}; + +QObject *EscapeStringLiteral::createTest() { return new EscapeStringLiteralTest; } +QObject *ConvertCStringToNSString::createTest() { return new QObject; } +QObject *WrapStringLiteral::createTest() { return new QObject; } +QObject *TranslateStringLiteral::createTest() { return new QObject; } + +#endif // WITH_TESTS +} // namespace + +void registerConvertStringLiteralQuickfixes() +{ + CppQuickFixFactory::registerFactory<ConvertCStringToNSString>(); + CppQuickFixFactory::registerFactory<EscapeStringLiteral>(); + CppQuickFixFactory::registerFactory<TranslateStringLiteral>(); + CppQuickFixFactory::registerFactory<WrapStringLiteral>(); +} + +} // namespace CppEditor::Internal + +#ifdef WITH_TESTS +#include <convertstringliteral.moc> +#endif |