diff --git a/src/plugins/cppeditor/CMakeLists.txt b/src/plugins/cppeditor/CMakeLists.txt index 1011af9eedf..23e6fe7faf3 100644 --- a/src/plugins/cppeditor/CMakeLists.txt +++ b/src/plugins/cppeditor/CMakeLists.txt @@ -100,6 +100,7 @@ add_qtc_plugin(CppEditor quickfixes/completeswitchstatement.cpp quickfixes/completeswitchstatement.h quickfixes/convertqt4connect.cpp quickfixes/convertqt4connect.h quickfixes/convertstringliteral.cpp quickfixes/convertstringliteral.h + quickfixes/converttometamethodcall.cpp quickfixes/converttometamethodcall.h quickfixes/cppcodegenerationquickfixes.cpp quickfixes/cppcodegenerationquickfixes.h quickfixes/cppinsertvirtualmethods.cpp quickfixes/cppinsertvirtualmethods.h quickfixes/cppquickfix.cpp quickfixes/cppquickfix.h diff --git a/src/plugins/cppeditor/cppeditor.qbs b/src/plugins/cppeditor/cppeditor.qbs index 6065b517f82..562d9d474ac 100644 --- a/src/plugins/cppeditor/cppeditor.qbs +++ b/src/plugins/cppeditor/cppeditor.qbs @@ -231,6 +231,8 @@ QtcPlugin { "convertqt4connect.h", "convertstringliteral.cpp", "convertstringliteral.h", + "converttometamethodcall.cpp", + "converttometamethodcall.h", "cppcodegenerationquickfixes.cpp", "cppcodegenerationquickfixes.h", "cppinsertvirtualmethods.cpp", diff --git a/src/plugins/cppeditor/quickfixes/converttometamethodcall.cpp b/src/plugins/cppeditor/quickfixes/converttometamethodcall.cpp new file mode 100644 index 00000000000..95c250595be --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/converttometamethodcall.cpp @@ -0,0 +1,274 @@ +// 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 "converttometamethodcall.h" + +#include "../cppeditortr.h" +#include "../cpprefactoringchanges.h" +#include "cppquickfix.h" +#include "cppquickfixhelpers.h" + +#include +#include +#include + +#ifdef WITH_TESTS +#include "cppquickfix_test.h" +#include +#endif + +using namespace CPlusPlus; +using namespace Utils; + +namespace CppEditor::Internal { +namespace { + +class ConvertToMetaMethodCallOp : public CppQuickFixOperation +{ +public: + ConvertToMetaMethodCallOp(const CppQuickFixInterface &interface, CallAST *callAst) + : CppQuickFixOperation(interface), m_callAst(callAst) + { + setDescription(Tr::tr("Convert Function Call to Qt Meta-Method Invocation")); + } + +private: + void perform() override + { + // Construct the argument list. + Overview ov; + QStringList arguments; + for (ExpressionListAST *it = m_callAst->expression_list; it; it = it->next) { + if (!it->value) + return; + const FullySpecifiedType argType + = typeOfExpr(it->value, currentFile(), snapshot(), context()); + if (!argType.isValid()) + return; + arguments << QString::fromUtf8("Q_ARG(%1, %2)") + .arg(ov.prettyType(argType), currentFile()->textOf(it->value)); + } + QString argsString = arguments.join(", "); + if (!argsString.isEmpty()) + argsString.prepend(", "); + + // Construct the replace string. + const auto memberAccessAst = m_callAst->base_expression->asMemberAccess(); + QTC_ASSERT(memberAccessAst, return); + QString baseExpr = currentFile()->textOf(memberAccessAst->base_expression); + const FullySpecifiedType baseExprType + = typeOfExpr(memberAccessAst->base_expression, currentFile(), snapshot(), context()); + if (!baseExprType.isValid()) + return; + if (!baseExprType->asPointerType()) + baseExpr.prepend('&'); + const QString functionName = currentFile()->textOf(memberAccessAst->member_name); + const QString qMetaObject = "QMetaObject"; + const QString newCall = QString::fromUtf8("%1::invokeMethod(%2, \"%3\"%4)") + .arg(qMetaObject, baseExpr, functionName, argsString); + + // Determine the start and end positions of the replace operation. + // If the call is preceded by an "emit" keyword, that one has to be removed as well. + int firstToken = m_callAst->firstToken(); + if (firstToken > 0) + switch (semanticInfo().doc->translationUnit()->tokenKind(firstToken - 1)) { + case T_EMIT: case T_Q_EMIT: --firstToken; break; + default: break; + } + const TranslationUnit *const tu = semanticInfo().doc->translationUnit(); + const int startPos = tu->getTokenPositionInDocument(firstToken, textDocument()); + const int endPos = tu->getTokenPositionInDocument(m_callAst->lastToken(), textDocument()); + + // Replace the old call with the new one. + ChangeSet changes; + changes.replace(startPos, endPos, newCall); + + // Insert include for QMetaObject, if necessary. + const Identifier qMetaObjectId(qPrintable(qMetaObject), qMetaObject.size()); + Scope * const scope = currentFile()->scopeAt(firstToken); + const QList results = context().lookup(&qMetaObjectId, scope); + bool isDeclared = false; + for (const LookupItem &item : results) { + if (Symbol *declaration = item.declaration(); declaration && declaration->asClass()) { + isDeclared = true; + break; + } + } + if (!isDeclared) { + insertNewIncludeDirective('<' + qMetaObject + '>', currentFile(), semanticInfo().doc, + changes); + } + + // Apply the changes. + currentFile()->setChangeSet(changes); + currentFile()->apply(); + } + + const CallAST * const m_callAst; +}; + +//! Converts a normal function call into a meta method invocation, if the functions is +//! marked as invokable. +class ConvertToMetaMethodCall : public CppQuickFixFactory +{ +#ifdef WITH_TESTS +public: + static QObject *createTest(); +#endif +private: + void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override + { + const Document::Ptr &cppDoc = interface.currentFile()->cppDocument(); + const QList path = ASTPath(cppDoc)(interface.cursor()); + if (path.isEmpty()) + return; + + // Are we on a member function call? + CallAST *callAst = nullptr; + for (auto it = path.crbegin(); it != path.crend(); ++it) { + if ((callAst = (*it)->asCall())) + break; + } + if (!callAst || !callAst->base_expression) + return; + ExpressionAST *baseExpr = nullptr; + const NameAST *nameAst = nullptr; + if (const MemberAccessAST * const ast = callAst->base_expression->asMemberAccess()) { + baseExpr = ast->base_expression; + nameAst = ast->member_name; + } + if (!baseExpr || !nameAst || !nameAst->name) + return; + + // Locate called function and check whether it is invokable. + Scope *scope = cppDoc->globalNamespace(); + for (auto it = path.crbegin(); it != path.crend(); ++it) { + if (const CompoundStatementAST * const stmtAst = (*it)->asCompoundStatement()) { + scope = stmtAst->symbol; + break; + } + } + const LookupContext context(cppDoc, interface.snapshot()); + TypeOfExpression exprType; + exprType.setExpandTemplates(true); + exprType.init(cppDoc, interface.snapshot()); + const QList typeMatches = exprType(callAst->base_expression, cppDoc, scope); + for (const LookupItem &item : typeMatches) { + if (const auto func = item.type()->asFunctionType(); func && func->methodKey()) { + result << new ConvertToMetaMethodCallOp(interface, callAst); + return; + } + } + } +}; + +#ifdef WITH_TESTS +using namespace Tests; + +class ConvertToMetaMethodCallTest : public QObject +{ + Q_OBJECT + +private slots: + void test_data() + { + QTest::addColumn("input"); + QTest::addColumn("expected"); + + // ^ marks the cursor locations. + // $ marks the replacement regions. + // The quoted string in the comment is the data tag. + // The rest of the comment is the replacement string. + const QByteArray allCases = R"( +class C { +public: + C() { + $this->^aSignal()$; // "signal from region on pointer to object" QMetaObject::invokeMethod(this, "aSignal") + C c; + $c.^aSignal()$; // "signal from region on object value" QMetaObject::invokeMethod(&c, "aSignal") + $(new C)->^aSignal()$; // "signal from region on expression" QMetaObject::invokeMethod((new C), "aSignal") + $emit this->^aSignal()$; // "signal from region, with emit" QMetaObject::invokeMethod(this, "aSignal") + $Q_EMIT this->^aSignal()$; // "signal from region, with Q_EMIT" QMetaObject::invokeMethod(this, "aSignal") + $this->^aSlot()$; // "slot from region" QMetaObject::invokeMethod(this, "aSlot") + $this->^noArgs()$; // "Q_SIGNAL, no arguments" QMetaObject::invokeMethod(this, "noArgs") + $this->^oneArg(0)$; // "Q_SLOT, one argument" QMetaObject::invokeMethod(this, "oneArg", Q_ARG(int, 0)) + $this->^twoArgs(0, c)$; // "Q_INVOKABLE, two arguments" QMetaObject::invokeMethod(this, "twoArgs", Q_ARG(int, 0), Q_ARG(C, c)) + this->^notInvokable(); // "not invokable" + } + +signals: + void aSignal(); + +private slots: + void aSlot(); + +private: + Q_SIGNAL void noArgs(); + Q_SLOT void oneArg(int index); + Q_INVOKABLE void twoArgs(int index, const C &value); + void notInvokable(); +}; +)"; + + qsizetype nextCursor = allCases.indexOf('^'); + while (nextCursor != -1) { + const int commentStart = allCases.indexOf("//", nextCursor); + QVERIFY(commentStart != -1); + const int tagStart = allCases.indexOf('"', commentStart + 2); + QVERIFY(tagStart != -1); + const int tagEnd = allCases.indexOf('"', tagStart + 1); + QVERIFY(tagEnd != -1); + QByteArray input = allCases; + QByteArray output = allCases; + input.replace(nextCursor, 1, "@"); + const QByteArray tag = allCases.mid(tagStart + 1, tagEnd - tagStart - 1); + const int prevNewline = allCases.lastIndexOf('\n', nextCursor); + const int regionStart = allCases.lastIndexOf('$', nextCursor); + bool hasReplacement = false; + if (regionStart != -1 && regionStart > prevNewline) { + const int regionEnd = allCases.indexOf('$', regionStart + 1); + QVERIFY(regionEnd != -1); + const int nextNewline = allCases.indexOf('\n', tagEnd); + QVERIFY(nextNewline != -1); + const QByteArray replacement + = allCases.mid(tagEnd + 1, nextNewline - tagEnd - 1).trimmed(); + output.replace(regionStart, regionEnd - regionStart, replacement); + hasReplacement = true; + } + static const auto matcher = [](char c) { return c == '^' || c == '$'; }; + input.removeIf(matcher); + if (hasReplacement) { + output.removeIf(matcher); + output.prepend("#include \n\n"); + } else { + output.clear(); + } + QTest::newRow(tag.data()) << input << output; + nextCursor = allCases.indexOf('^', nextCursor + 1); + } + } + + void test() + { + QFETCH(QByteArray, input); + QFETCH(QByteArray, expected); + ConvertToMetaMethodCall factory; + QuickFixOperationTest({CppTestDocument::create("file.cpp", input, expected)}, &factory); + } +}; + +QObject *ConvertToMetaMethodCall::createTest() { return new ConvertToMetaMethodCallTest; } + +#endif // WITH_TESTS +} // namespace + +void registerConvertToMetaMethodCallQuickfix() +{ + CppQuickFixFactory::registerFactory(); +} + +} // namespace CppEditor::Internal + +#ifdef WITH_TESTS +#include +#endif diff --git a/src/plugins/cppeditor/quickfixes/converttometamethodcall.h b/src/plugins/cppeditor/quickfixes/converttometamethodcall.h new file mode 100644 index 00000000000..e6c309ad134 --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/converttometamethodcall.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 registerConvertToMetaMethodCallQuickfix(); +} // namespace CppEditor::Internal diff --git a/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp b/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp index f949ed1b025..41df3b423a2 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp +++ b/src/plugins/cppeditor/quickfixes/cppquickfix_test.cpp @@ -350,91 +350,4 @@ CppCodeStyleSettings CppCodeStyleSettingsChanger::currentSettings() return CppToolsSettings::cppCodeStyle()->currentDelegate()->value().value(); } -void QuickfixTest::testConvertToMetaMethodInvocation_data() -{ - QTest::addColumn("input"); - QTest::addColumn("expected"); - - // ^ marks the cursor locations. - // $ marks the replacement regions. - // The quoted string in the comment is the data tag. - // The rest of the comment is the replacement string. - const QByteArray allCases = R"( -class C { -public: - C() { - $this->^aSignal()$; // "signal from region on pointer to object" QMetaObject::invokeMethod(this, "aSignal") - C c; - $c.^aSignal()$; // "signal from region on object value" QMetaObject::invokeMethod(&c, "aSignal") - $(new C)->^aSignal()$; // "signal from region on expression" QMetaObject::invokeMethod((new C), "aSignal") - $emit this->^aSignal()$; // "signal from region, with emit" QMetaObject::invokeMethod(this, "aSignal") - $Q_EMIT this->^aSignal()$; // "signal from region, with Q_EMIT" QMetaObject::invokeMethod(this, "aSignal") - $this->^aSlot()$; // "slot from region" QMetaObject::invokeMethod(this, "aSlot") - $this->^noArgs()$; // "Q_SIGNAL, no arguments" QMetaObject::invokeMethod(this, "noArgs") - $this->^oneArg(0)$; // "Q_SLOT, one argument" QMetaObject::invokeMethod(this, "oneArg", Q_ARG(int, 0)) - $this->^twoArgs(0, c)$; // "Q_INVOKABLE, two arguments" QMetaObject::invokeMethod(this, "twoArgs", Q_ARG(int, 0), Q_ARG(C, c)) - this->^notInvokable(); // "not invokable" - } - -signals: - void aSignal(); - -private slots: - void aSlot(); - -private: - Q_SIGNAL void noArgs(); - Q_SLOT void oneArg(int index); - Q_INVOKABLE void twoArgs(int index, const C &value); - void notInvokable(); -}; -)"; - - qsizetype nextCursor = allCases.indexOf('^'); - while (nextCursor != -1) { - const int commentStart = allCases.indexOf("//", nextCursor); - QVERIFY(commentStart != -1); - const int tagStart = allCases.indexOf('"', commentStart + 2); - QVERIFY(tagStart != -1); - const int tagEnd = allCases.indexOf('"', tagStart + 1); - QVERIFY(tagEnd != -1); - QByteArray input = allCases; - QByteArray output = allCases; - input.replace(nextCursor, 1, "@"); - const QByteArray tag = allCases.mid(tagStart + 1, tagEnd - tagStart - 1); - const int prevNewline = allCases.lastIndexOf('\n', nextCursor); - const int regionStart = allCases.lastIndexOf('$', nextCursor); - bool hasReplacement = false; - if (regionStart != -1 && regionStart > prevNewline) { - const int regionEnd = allCases.indexOf('$', regionStart + 1); - QVERIFY(regionEnd != -1); - const int nextNewline = allCases.indexOf('\n', tagEnd); - QVERIFY(nextNewline != -1); - const QByteArray replacement - = allCases.mid(tagEnd + 1, nextNewline - tagEnd - 1).trimmed(); - output.replace(regionStart, regionEnd - regionStart, replacement); - hasReplacement = true; - } - static const auto matcher = [](char c) { return c == '^' || c == '$'; }; - input.removeIf(matcher); - if (hasReplacement) { - output.removeIf(matcher); - output.prepend("#include \n\n"); - } else { - output.clear(); - } - QTest::newRow(tag.data()) << input << output; - nextCursor = allCases.indexOf('^', nextCursor + 1); - } -} - -void QuickfixTest::testConvertToMetaMethodInvocation() -{ - QFETCH(QByteArray, input); - QFETCH(QByteArray, expected); - - ConvertToMetaMethodCall factory; - QuickFixOperationTest({CppTestDocument::create("file.cpp", input, expected)}, &factory); -} - } // namespace CppEditor::Internal::Tests diff --git a/src/plugins/cppeditor/quickfixes/cppquickfix_test.h b/src/plugins/cppeditor/quickfixes/cppquickfix_test.h index 504ed30c1df..0bead0a5b1e 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfix_test.h +++ b/src/plugins/cppeditor/quickfixes/cppquickfix_test.h @@ -96,9 +96,6 @@ class QuickfixTest : public QObject private slots: void testGeneric_data(); void testGeneric(); - - void testConvertToMetaMethodInvocation_data(); - void testConvertToMetaMethodInvocation(); }; } // namespace Tests diff --git a/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp b/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp index 5c29d10713d..05cc6909cef 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp +++ b/src/plugins/cppeditor/quickfixes/cppquickfixes.cpp @@ -17,6 +17,7 @@ #include "bringidentifierintoscope.h" #include "completeswitchstatement.h" #include "convertfromandtopointer.h" +#include "converttometamethodcall.h" #include "cppcodegenerationquickfixes.h" #include "cppinsertvirtualmethods.h" #include "cppquickfixassistant.h" @@ -740,138 +741,6 @@ void ExtraRefactoringOperations::doMatch(const CppQuickFixInterface &interface, } } -namespace { -class ConvertToMetaMethodCallOp : public CppQuickFixOperation -{ -public: - ConvertToMetaMethodCallOp(const CppQuickFixInterface &interface, CallAST *callAst) - : CppQuickFixOperation(interface), m_callAst(callAst) - { - setDescription(Tr::tr("Convert Function Call to Qt Meta-Method Invocation")); - } - -private: - void perform() override - { - // Construct the argument list. - Overview ov; - QStringList arguments; - for (ExpressionListAST *it = m_callAst->expression_list; it; it = it->next) { - if (!it->value) - return; - const FullySpecifiedType argType - = typeOfExpr(it->value, currentFile(), snapshot(), context()); - if (!argType.isValid()) - return; - arguments << QString::fromUtf8("Q_ARG(%1, %2)") - .arg(ov.prettyType(argType), currentFile()->textOf(it->value)); - } - QString argsString = arguments.join(", "); - if (!argsString.isEmpty()) - argsString.prepend(", "); - - // Construct the replace string. - const auto memberAccessAst = m_callAst->base_expression->asMemberAccess(); - QTC_ASSERT(memberAccessAst, return); - QString baseExpr = currentFile()->textOf(memberAccessAst->base_expression); - const FullySpecifiedType baseExprType - = typeOfExpr(memberAccessAst->base_expression, currentFile(), snapshot(), context()); - if (!baseExprType.isValid()) - return; - if (!baseExprType->asPointerType()) - baseExpr.prepend('&'); - const QString functionName = currentFile()->textOf(memberAccessAst->member_name); - const QString qMetaObject = "QMetaObject"; - const QString newCall = QString::fromUtf8("%1::invokeMethod(%2, \"%3\"%4)") - .arg(qMetaObject, baseExpr, functionName, argsString); - - // Determine the start and end positions of the replace operation. - // If the call is preceded by an "emit" keyword, that one has to be removed as well. - int firstToken = m_callAst->firstToken(); - if (firstToken > 0) - switch (semanticInfo().doc->translationUnit()->tokenKind(firstToken - 1)) { - case T_EMIT: case T_Q_EMIT: --firstToken; break; - default: break; - } - const TranslationUnit *const tu = semanticInfo().doc->translationUnit(); - const int startPos = tu->getTokenPositionInDocument(firstToken, textDocument()); - const int endPos = tu->getTokenPositionInDocument(m_callAst->lastToken(), textDocument()); - - // Replace the old call with the new one. - ChangeSet changes; - changes.replace(startPos, endPos, newCall); - - // Insert include for QMetaObject, if necessary. - const Identifier qMetaObjectId(qPrintable(qMetaObject), qMetaObject.size()); - Scope * const scope = currentFile()->scopeAt(firstToken); - const QList results = context().lookup(&qMetaObjectId, scope); - bool isDeclared = false; - for (const LookupItem &item : results) { - if (Symbol *declaration = item.declaration(); declaration && declaration->asClass()) { - isDeclared = true; - break; - } - } - if (!isDeclared) { - insertNewIncludeDirective('<' + qMetaObject + '>', currentFile(), semanticInfo().doc, - changes); - } - - // Apply the changes. - currentFile()->setChangeSet(changes); - currentFile()->apply(); - } - - const CallAST * const m_callAst; -}; -} // namespace - -void ConvertToMetaMethodCall::doMatch(const CppQuickFixInterface &interface, - TextEditor::QuickFixOperations &result) -{ - const Document::Ptr &cppDoc = interface.currentFile()->cppDocument(); - const QList path = ASTPath(cppDoc)(interface.cursor()); - if (path.isEmpty()) - return; - - // Are we on a member function call? - CallAST *callAst = nullptr; - for (auto it = path.crbegin(); it != path.crend(); ++it) { - if ((callAst = (*it)->asCall())) - break; - } - if (!callAst || !callAst->base_expression) - return; - ExpressionAST *baseExpr = nullptr; - const NameAST *nameAst = nullptr; - if (const MemberAccessAST * const ast = callAst->base_expression->asMemberAccess()) { - baseExpr = ast->base_expression; - nameAst = ast->member_name; - } - if (!baseExpr || !nameAst || !nameAst->name) - return; - - // Locate called function and check whether it is invokable. - Scope *scope = cppDoc->globalNamespace(); - for (auto it = path.crbegin(); it != path.crend(); ++it) { - if (const CompoundStatementAST * const stmtAst = (*it)->asCompoundStatement()) { - scope = stmtAst->symbol; - break; - } - } - const LookupContext context(cppDoc, interface.snapshot()); - TypeOfExpression exprType; - exprType.setExpandTemplates(true); - exprType.init(cppDoc, interface.snapshot()); - const QList typeMatches = exprType(callAst->base_expression, cppDoc, scope); - for (const LookupItem &item : typeMatches) { - if (const auto func = item.type()->asFunctionType(); func && func->methodKey()) { - result << new ConvertToMetaMethodCallOp(interface, callAst); - return; - } - } -} - void createCppQuickFixes() { new ConvertToCamelCase; @@ -903,10 +772,9 @@ void createCppQuickFixes() registerConvertFromAndToPointerQuickfix(); registerAssignToLocalVariableQuickfix(); registerCompleteSwitchStatementQuickfix(); + registerConvertToMetaMethodCallQuickfix(); new ExtraRefactoringOperations; - - new ConvertToMetaMethodCall; } void destroyCppQuickFixes() diff --git a/src/plugins/cppeditor/quickfixes/cppquickfixes.h b/src/plugins/cppeditor/quickfixes/cppquickfixes.h index 3e7ba78bc92..e7c12fefecb 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfixes.h +++ b/src/plugins/cppeditor/quickfixes/cppquickfixes.h @@ -122,14 +122,5 @@ public: void doMatch(const CppQuickFixInterface &interface, TextEditor::QuickFixOperations &result) override; }; -//! Converts a normal function call into a meta method invocation, if the functions is -//! marked as invokable. -class ConvertToMetaMethodCall : public CppQuickFixFactory -{ -private: - void doMatch(const CppQuickFixInterface &interface, - TextEditor::QuickFixOperations &result) override; -}; - } // namespace Internal } // namespace CppEditor