diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2024-05-15 17:07:03 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2024-05-17 09:36:13 +0000 |
commit | b3e4d552d3c9198dbb569b6b7287c2ff83ce84ae (patch) | |
tree | e671836183300bbf3eaf1d55851cd8b5d351e9d6 /src/plugins | |
parent | c5325effc83c92844ded84153fa1bb03babb9206 (diff) |
CppEditor: Move quickfixes for string literals into dedicated files
Change-Id: I60d9d30981a68a6393ba39f566bd174b0f391793
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
Diffstat (limited to 'src/plugins')
-rw-r--r-- | src/plugins/cppeditor/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/plugins/cppeditor/cppeditor.qbs | 2 | ||||
-rw-r--r-- | src/plugins/cppeditor/quickfixes/convertstringliteral.cpp | 758 | ||||
-rw-r--r-- | src/plugins/cppeditor/quickfixes/convertstringliteral.h | 8 | ||||
-rw-r--r-- | src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp | 56 | ||||
-rw-r--r-- | src/plugins/cppeditor/quickfixes/cppquickfixes.cpp | 585 | ||||
-rw-r--r-- | src/plugins/cppeditor/quickfixes/cppquickfixes.h | 70 |
7 files changed, 772 insertions, 710 deletions
diff --git a/src/plugins/cppeditor/CMakeLists.txt b/src/plugins/cppeditor/CMakeLists.txt index 507ed0d046..ede4e3d661 100644 --- a/src/plugins/cppeditor/CMakeLists.txt +++ b/src/plugins/cppeditor/CMakeLists.txt @@ -96,6 +96,8 @@ add_qtc_plugin(CppEditor projectinfo.cpp projectinfo.h projectpart.cpp projectpart.h quickfixes/bringidentifierintoscope.cpp quickfixes/bringidentifierintoscope.h + quickfixes/convertqt4connect.cpp quickfixes/convertqt4connect.h + quickfixes/convertstringliteral.cpp quickfixes/convertstringliteral.h quickfixes/cppcodegenerationquickfixes.cpp quickfixes/cppcodegenerationquickfixes.h quickfixes/cppinsertvirtualmethods.cpp quickfixes/cppinsertvirtualmethods.h quickfixes/cppquickfix.cpp quickfixes/cppquickfix.h @@ -107,7 +109,6 @@ add_qtc_plugin(CppEditor quickfixes/cppquickfixsettings.cpp quickfixes/cppquickfixsettings.h quickfixes/cppquickfixsettingspage.cpp quickfixes/cppquickfixsettingspage.h quickfixes/cppquickfixsettingswidget.cpp quickfixes/cppquickfixsettingswidget.h - quickfixes/convertqt4connect.cpp quickfixes/convertqt4connect.h quickfixes/insertfunctiondefinition.cpp quickfixes/insertfunctiondefinition.h quickfixes/moveclasstoownfile.cpp quickfixes/moveclasstoownfile.h quickfixes/movefunctiondefinition.cpp quickfixes/movefunctiondefinition.h diff --git a/src/plugins/cppeditor/cppeditor.qbs b/src/plugins/cppeditor/cppeditor.qbs index 4ea91a8a04..fa4f40f633 100644 --- a/src/plugins/cppeditor/cppeditor.qbs +++ b/src/plugins/cppeditor/cppeditor.qbs @@ -223,6 +223,8 @@ QtcPlugin { "bringidentifierintoscope.h", "convertqt4connect.cpp", "convertqt4connect.h", + "convertstringliteral.cpp", + "convertstringliteral.h", "cppcodegenerationquickfixes.cpp", "cppcodegenerationquickfixes.h", "cppinsertvirtualmethods.cpp", diff --git a/src/plugins/cppeditor/quickfixes/convertstringliteral.cpp b/src/plugins/cppeditor/quickfixes/convertstringliteral.cpp new file mode 100644 index 0000000000..c65ff5107b --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/convertstringliteral.cpp @@ -0,0 +1,758 @@ +// 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 + { + CppRefactoringChanges refactoring(snapshot()); + CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath()); + + 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->setChangeSet(changes); + currentFile->apply(); + } + +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 + { + CppRefactoringChanges refactoring(snapshot()); + CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath()); + + 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->setChangeSet(changes); + currentFile->apply(); + } + +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 + { + CppRefactoringChanges refactoring(snapshot()); + CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath()); + + 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->setChangeSet(changes); + currentFile->apply(); + } + +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 diff --git a/src/plugins/cppeditor/quickfixes/convertstringliteral.h b/src/plugins/cppeditor/quickfixes/convertstringliteral.h new file mode 100644 index 0000000000..16cefacae0 --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/convertstringliteral.h @@ -0,0 +1,8 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +namespace CppEditor::Internal { +void registerConvertStringLiteralQuickfixes(); +} // namespace CppEditor::Internal diff --git a/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp b/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp index 04b9020b66..786df948e0 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp +++ b/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp @@ -1238,42 +1238,6 @@ void QuickfixTest::testGeneric_data() << _("void foo() {fo@r (int i = 0; i < -3; ++i) {}}\n") << _(); - // Escape String Literal as UTF-8 (no-trigger) - QTest::newRow("EscapeStringLiteral_notrigger") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _("const char *notrigger = \"@abcdef \\a\\n\\\\\";\n") - << _(); - - // Escape String Literal as UTF-8 - QTest::newRow("EscapeStringLiteral") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _("const char *utf8 = \"@\xe3\x81\x82\xe3\x81\x84\";\n") - << _("const char *utf8 = \"\\xe3\\x81\\x82\\xe3\\x81\\x84\";\n"); - - // Unescape String Literal as UTF-8 (from hexdecimal escape sequences) - QTest::newRow("UnescapeStringLiteral_hex") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _("const char *hex_escaped = \"@\\xe3\\x81\\x82\\xe3\\x81\\x84\";\n") - << _("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") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _("const char *oct_escaped = \"@\\343\\201\\202\\343\\201\\204\";\n") - << _("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") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _("const char *escaped_ascii = \"@\\x1b\";\n") - << _("const char *escaped_ascii = \"\\x1b\";\n"); - - // Unescape String Literal as UTF-8 (no conversion because of invalid utf-8) - QTest::newRow("UnescapeStringLiteral_invalid") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _("const char *escaped = \"@\\xe3\\x81\";\n") - << _("const char *escaped = \"\\xe3\\x81\";\n"); - QTest::newRow("ConvertFromPointer") << CppQuickFixFactoryPtr(new ConvertFromAndToPointer) << _("void foo() {\n" @@ -1602,26 +1566,6 @@ void QuickfixTest::testGeneric_data() << CppQuickFixFactoryPtr(new ConvertToCamelCase(true)) << _("void @WhAt_TODO_hErE();\n") << _("void WhAtTODOHErE();\n"); - QTest::newRow("escape string literal: simple case") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _(R"(const char *str = @"àxyz";)") - << _(R"(const char *str = "\xc3\xa0xyz";)"); - QTest::newRow("escape string literal: simple case reverse") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _(R"(const char *str = @"\xc3\xa0xyz";)") - << _(R"(const char *str = "àxyz";)"); - QTest::newRow("escape string literal: raw string literal") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _(R"x(const char *str = @R"(àxyz)";)x") - << _(R"x(const char *str = R"(\xc3\xa0xyz)";)x"); - QTest::newRow("escape string literal: splitting required") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _(R"(const char *str = @"àf23бgб1";)") - << _(R"(const char *str = "\xc3\xa0""f23\xd0\xb1g\xd0\xb1""1";)"); - QTest::newRow("escape string literal: unescape adjacent literals") - << CppQuickFixFactoryPtr(new EscapeStringLiteral) - << _(R"(const char *str = @"\xc3\xa0""f23\xd0\xb1g\xd0\xb1""1";)") - << _(R"(const char *str = "àf23бgб1";)"); QTest::newRow("AddLocalDeclaration_QTCREATORBUG-26004") << CppQuickFixFactoryPtr(new AddDeclarationForUndeclaredIdentifier) << _("void func() {\n" diff --git a/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp b/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp index 02b5ce716f..8b04089883 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp +++ b/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp @@ -22,6 +22,7 @@ #include "cppquickfixhelpers.h" #include "cppquickfixprojectsettings.h" #include "convertqt4connect.h" +#include "convertstringliteral.h" #include "insertfunctiondefinition.h" #include "moveclasstoownfile.h" #include "movefunctiondefinition.h" @@ -131,17 +132,6 @@ namespace Internal { // different quick fixes. namespace { -inline bool isQtStringLiteral(const QByteArray &id) -{ - return id == "QLatin1String" || id == "QLatin1Literal" || id == "QStringLiteral" - || id == "QByteArrayLiteral"; -} - -inline bool isQtStringTranslation(const QByteArray &id) -{ - return id == "tr" || id == "trUtf8" || id == "translate" || id == "QT_TRANSLATE_NOOP"; -} - QString nameString(const NameAST *name) { return CppCodeStyleSettings::currentProjectCodeStyleOverview().prettyName(name->name); @@ -1012,389 +1002,6 @@ void SplitIfStatement::doMatch(const CppQuickFixInterface &interface, QuickFixOp } } -/* Analze a string/character literal like "x", QLatin1String("x") and return the literal - * (StringLiteral or NumericLiteral for characters) and its type - * and the enclosing function (QLatin1String, tr...) */ - -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 -}; - -/* 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; -} - -namespace { - -/// 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 - { - CppRefactoringChanges refactoring(snapshot()); - CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath()); - - 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->setChangeSet(changes); - currentFile->apply(); - } - -private: - const unsigned m_actions; - ExpressionAST *m_literal; - const QString m_translationContext; -}; - -} // anonymous namespace - -void WrapStringLiteral::doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) -{ - 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); - } -} - -void TranslateStringLiteral::doMatch(const CppQuickFixInterface &interface, - QuickFixOperations &result) -{ - // 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); -} - -namespace { - -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 - { - CppRefactoringChanges refactoring(snapshot()); - CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath()); - - 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->setChangeSet(changes); - currentFile->apply(); - } - -private: - StringLiteralAST *stringLiteral; - CallAST *qlatin1Call; -}; - -} // anonymous namespace - -void ConvertCStringToNSString::doMatch(const CppQuickFixInterface &interface, - QuickFixOperations &result) -{ - 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); -} - namespace { class ConvertNumericLiteralOp: public CppQuickFixOperation @@ -4315,190 +3922,6 @@ void OptimizeForLoop::doMatch(const CppQuickFixInterface &interface, QuickFixOpe } } -namespace { - -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 - { - CppRefactoringChanges refactoring(snapshot()); - CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath()); - - 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->setChangeSet(changes); - currentFile->apply(); - } - -private: - ExpressionAST *m_literal; - bool m_escape; -}; - -} // anonymous namespace - -void EscapeStringLiteral::doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) -{ - 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); -} - void ExtraRefactoringOperations::doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) { @@ -5075,10 +4498,7 @@ void createCppQuickFixes() new ConvertToCamelCase; - new ConvertCStringToNSString; new ConvertNumericLiteral; - new TranslateStringLiteral; - new WrapStringLiteral; new MoveDeclarationOutOfIf; new MoveDeclarationOutOfWhile; @@ -5109,11 +4529,10 @@ void createCppQuickFixes() registerMoveFunctionDefinitionQuickfixes(); registerInsertFunctionDefinitionQuickfixes(); registerBringIdentifierIntoScopeQuickfixes(); + registerConvertStringLiteralQuickfixes(); new OptimizeForLoop; - new EscapeStringLiteral; - new ExtraRefactoringOperations; new ConvertCommentStyle; diff --git a/src/plugins/cppeditor/quickfixes/cppquickfixes.h b/src/plugins/cppeditor/quickfixes/cppquickfixes.h index 845ed8d0e2..580718eddd 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfixes.h +++ b/src/plugins/cppeditor/quickfixes/cppquickfixes.h @@ -72,22 +72,6 @@ public: void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override; }; -/*! - 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 -{ -public: - void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override; -}; /*! Base class for converting numeric literals between decimal, octal and hex. @@ -115,47 +99,6 @@ public: }; /*! - 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 -{ -public: - void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override; -}; - -/*! - 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 -{ -public: - void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override; -}; - -/*! Turns "an_example_symbol" into "anExampleSymbol" and "AN_EXAMPLE_SYMBOL" into "AnExampleSymbol". @@ -413,19 +356,6 @@ public: void doMatch(const CppQuickFixInterface &interface, TextEditor::QuickFixOperations &result) override; }; -/*! - 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 -{ -public: - void doMatch(const CppQuickFixInterface &interface, TextEditor::QuickFixOperations &result) override; -}; - //! Converts C-style to C++-style comments and vice versa class ConvertCommentStyle : public CppQuickFixFactory { |