diff options
Diffstat (limited to 'src/plugins/cppeditor/quickfixes/completeswitchstatement.cpp')
-rw-r--r-- | src/plugins/cppeditor/quickfixes/completeswitchstatement.cpp | 793 |
1 files changed, 793 insertions, 0 deletions
diff --git a/src/plugins/cppeditor/quickfixes/completeswitchstatement.cpp b/src/plugins/cppeditor/quickfixes/completeswitchstatement.cpp new file mode 100644 index 0000000000..15b8e592ed --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/completeswitchstatement.cpp @@ -0,0 +1,793 @@ +// 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 "completeswitchstatement.h" + +#include "../cppeditortr.h" +#include "../cpprefactoringchanges.h" +#include "cppquickfix.h" + +#include <cplusplus/Overview.h> +#include <cplusplus/TypeOfExpression.h> + +#ifdef WITH_TESTS +#include "cppquickfix_test.h" +#include <QtTest> +#endif + +using namespace CPlusPlus; +using namespace Utils; + +namespace CppEditor::Internal { +namespace { + +class CaseStatementCollector : public ASTVisitor +{ +public: + CaseStatementCollector(Document::Ptr document, const Snapshot &snapshot, + Scope *scope) + : ASTVisitor(document->translationUnit()), + document(document), + scope(scope) + { + typeOfExpression.init(document, snapshot); + } + + QStringList operator ()(AST *ast) + { + values.clear(); + foundCaseStatementLevel = false; + accept(ast); + return values; + } + + bool preVisit(AST *ast) override { + if (CaseStatementAST *cs = ast->asCaseStatement()) { + foundCaseStatementLevel = true; + if (ExpressionAST *csExpression = cs->expression) { + if (ExpressionAST *expression = csExpression->asIdExpression()) { + QList<LookupItem> candidates = typeOfExpression(expression, document, scope); + if (!candidates.isEmpty() && candidates.first().declaration()) { + Symbol *decl = candidates.first().declaration(); + values << prettyPrint.prettyName(LookupContext::fullyQualifiedName(decl)); + } + } + } + return true; + } else if (foundCaseStatementLevel) { + return false; + } + return true; + } + + Overview prettyPrint; + bool foundCaseStatementLevel = false; + QStringList values; + TypeOfExpression typeOfExpression; + Document::Ptr document; + Scope *scope; +}; + +class CompleteSwitchCaseStatementOp: public CppQuickFixOperation +{ +public: + CompleteSwitchCaseStatementOp(const CppQuickFixInterface &interface, + int priority, CompoundStatementAST *compoundStatement, const QStringList &values) + : CppQuickFixOperation(interface, priority) + , compoundStatement(compoundStatement) + , values(values) + { + setDescription(Tr::tr("Complete Switch Statement")); + } + + void perform() override + { + currentFile()->apply(ChangeSet::makeInsert( + currentFile()->endOf(compoundStatement->lbrace_token), + QLatin1String("\ncase ") + values.join(QLatin1String(":\nbreak;\ncase ")) + + QLatin1String(":\nbreak;"))); + } + + CompoundStatementAST *compoundStatement; + QStringList values; +}; + +static Enum *findEnum(const QList<LookupItem> &results, const LookupContext &ctxt) +{ + for (const LookupItem &result : results) { + const FullySpecifiedType fst = result.type(); + + Type *type = result.declaration() ? result.declaration()->type().type() + : fst.type(); + + if (!type) + continue; + if (Enum *e = type->asEnumType()) + return e; + if (const NamedType *namedType = type->asNamedType()) { + if (ClassOrNamespace *con = ctxt.lookupType(namedType->name(), result.scope())) { + QList<Enum *> enums = con->unscopedEnums(); + const QList<Symbol *> symbols = con->symbols(); + for (Symbol * const s : symbols) { + if (const auto e = s->asEnum()) + enums << e; + } + const Name *referenceName = namedType->name(); + if (const QualifiedNameId *qualifiedName = referenceName->asQualifiedNameId()) + referenceName = qualifiedName->name(); + for (Enum *e : std::as_const(enums)) { + if (const Name *candidateName = e->name()) { + if (candidateName->match(referenceName)) + return e; + } + } + } + } + } + + return nullptr; +} + +Enum *conditionEnum(const CppQuickFixInterface &interface, SwitchStatementAST *statement) +{ + Block *block = statement->symbol; + Scope *scope = interface.semanticInfo().doc->scopeAt(block->line(), block->column()); + TypeOfExpression typeOfExpression; + typeOfExpression.setExpandTemplates(true); + typeOfExpression.init(interface.semanticInfo().doc, interface.snapshot()); + const QList<LookupItem> results = typeOfExpression(statement->condition, + interface.semanticInfo().doc, + scope); + + return findEnum(results, typeOfExpression.context()); +} + +//! Adds missing case statements for "switch (enumVariable)" +class CompleteSwitchStatement: public CppQuickFixFactory +{ +public: + CompleteSwitchStatement() { setClangdReplacement({12}); } +#ifdef WITH_TESTS + static QObject *createTest(); +#endif + +private: + void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override + { + const QList<AST *> &path = interface.path(); + + if (path.isEmpty()) + return; + + // look for switch statement + for (int depth = path.size() - 1; depth >= 0; --depth) { + AST *ast = path.at(depth); + SwitchStatementAST *switchStatement = ast->asSwitchStatement(); + if (switchStatement) { + if (!switchStatement->statement || !switchStatement->symbol) + return; + CompoundStatementAST *compoundStatement = switchStatement->statement->asCompoundStatement(); + if (!compoundStatement) // we ignore pathologic case "switch (t) case A: ;" + return; + // look if the condition's type is an enum + if (Enum *e = conditionEnum(interface, switchStatement)) { + // check the possible enum values + QStringList values; + Overview prettyPrint; + for (int i = 0; i < e->memberCount(); ++i) { + if (Declaration *decl = e->memberAt(i)->asDeclaration()) + values << prettyPrint.prettyName(LookupContext::fullyQualifiedName(decl)); + } + // Get the used values + Block *block = switchStatement->symbol; + CaseStatementCollector caseValues(interface.semanticInfo().doc, interface.snapshot(), + interface.semanticInfo().doc->scopeAt(block->line(), block->column())); + const QStringList usedValues = caseValues(switchStatement); + // save the values that would be added + for (const QString &usedValue : usedValues) + values.removeAll(usedValue); + if (!values.isEmpty()) + result << new CompleteSwitchCaseStatementOp(interface, depth, + compoundStatement, values); + return; + } + + return; + } + } + } +}; + +#ifdef WITH_TESTS +using namespace Tests; + +class CompleteSwitchStatementTest : public QObject +{ + Q_OBJECT + +private slots: + void test_data() + { + QTest::addColumn<QByteArray>("original"); + QTest::addColumn<QByteArray>("expected"); + + using QByteArray = QByteArray; + + // Checks: All enum values are added as case statements for a blank switch. + QTest::newRow("basic1") + << QByteArray( + "enum EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "enum EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " case V1:\n" + " break;\n" + " case V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("basic1_enum class") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " case EnumType::V1:\n" + " break;\n" + " case EnumType::V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above with the cursor somewhere in the body. + QTest::newRow("basic1_enum class, cursor in the body") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " @}\n" + "}\n") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " case EnumType::V1:\n" + " break;\n" + " case EnumType::V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: All enum values are added as case statements for a blank switch when + // the variable is declared alongside the enum definition. + QTest::newRow("basic1_enum_with_declaration") + << QByteArray( + "enum EnumType { V1, V2 } t;\n" + "\n" + "void f()\n" + "{\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "enum EnumType { V1, V2 } t;\n" + "\n" + "void f()\n" + "{\n" + " switch (t) {\n" + " case V1:\n" + " break;\n" + " case V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("basic1_enum_with_declaration_enumClass") + << QByteArray( + "enum class EnumType { V1, V2 } t;\n" + "\n" + "void f()\n" + "{\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "enum class EnumType { V1, V2 } t;\n" + "\n" + "void f()\n" + "{\n" + " switch (t) {\n" + " case EnumType::V1:\n" + " break;\n" + " case EnumType::V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: All enum values are added as case statements for a blank switch + // for anonymous enums. + QTest::newRow("basic1_anonymous_enum") + << QByteArray( + "enum { V1, V2 } t;\n" + "\n" + "void f()\n" + "{\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "enum { V1, V2 } t;\n" + "\n" + "void f()\n" + "{\n" + " switch (t) {\n" + " case V1:\n" + " break;\n" + " case V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: All enum values are added as case statements for a blank switch with a default case. + QTest::newRow("basic2") + << QByteArray( + "enum EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " @switch (t) {\n" + " default:\n" + " break;\n" + " }\n" + "}\n") + << QByteArray( + "enum EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " case V1:\n" + " break;\n" + " case V2:\n" + " break;\n" + " default:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("basic2_enumClass") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " @switch (t) {\n" + " default:\n" + " break;\n" + " }\n" + "}\n") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " case EnumType::V1:\n" + " break;\n" + " case EnumType::V2:\n" + " break;\n" + " default:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: Enum type in class is found. + QTest::newRow("enumTypeInClass") + << QByteArray( + "struct C { enum EnumType { V1, V2 }; };\n" + "\n" + "void f(C::EnumType t) {\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "struct C { enum EnumType { V1, V2 }; };\n" + "\n" + "void f(C::EnumType t) {\n" + " switch (t) {\n" + " case C::V1:\n" + " break;\n" + " case C::V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("enumClassInClass") + << QByteArray( + "struct C { enum class EnumType { V1, V2 }; };\n" + "\n" + "void f(C::EnumType t) {\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "struct C { enum class EnumType { V1, V2 }; };\n" + "\n" + "void f(C::EnumType t) {\n" + " switch (t) {\n" + " case C::EnumType::V1:\n" + " break;\n" + " case C::EnumType::V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: Enum type in namespace is found. + QTest::newRow("enumTypeInNamespace") + << QByteArray( + "namespace N { enum EnumType { V1, V2 }; };\n" + "\n" + "void f(N::EnumType t) {\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "namespace N { enum EnumType { V1, V2 }; };\n" + "\n" + "void f(N::EnumType t) {\n" + " switch (t) {\n" + " case N::V1:\n" + " break;\n" + " case N::V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("enumClassInNamespace") + << QByteArray( + "namespace N { enum class EnumType { V1, V2 }; };\n" + "\n" + "void f(N::EnumType t) {\n" + " @switch (t) {\n" + " }\n" + "}\n") + << QByteArray( + "namespace N { enum class EnumType { V1, V2 }; };\n" + "\n" + "void f(N::EnumType t) {\n" + " switch (t) {\n" + " case N::EnumType::V1:\n" + " break;\n" + " case N::EnumType::V2:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: The missing enum value is added. + QTest::newRow("oneValueMissing") + << QByteArray( + "enum EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " @switch (t) {\n" + " case V2:\n" + " break;\n" + " default:\n" + " break;\n" + " }\n" + "}\n") + << QByteArray( + "enum EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " case V1:\n" + " break;\n" + " case V2:\n" + " break;\n" + " default:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: Same as above for enum class. + QTest::newRow("oneValueMissing_enumClass") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " @switch (t) {\n" + " case EnumType::V2:\n" + " break;\n" + " default:\n" + " break;\n" + " }\n" + "}\n") + << QByteArray( + "enum class EnumType { V1, V2 };\n" + "\n" + "void f()\n" + "{\n" + " EnumType t;\n" + " switch (t) {\n" + " case EnumType::V1:\n" + " break;\n" + " case EnumType::V2:\n" + " break;\n" + " default:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: Find the correct enum type despite there being a declaration with the same name. + QTest::newRow("QTCREATORBUG10366_1") + << QByteArray( + "enum test { TEST_1, TEST_2 };\n" + "\n" + "void f() {\n" + " enum test test;\n" + " @switch (test) {\n" + " }\n" + "}\n") + << QByteArray( + "enum test { TEST_1, TEST_2 };\n" + "\n" + "void f() {\n" + " enum test test;\n" + " switch (test) {\n" + " case TEST_1:\n" + " break;\n" + " case TEST_2:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("QTCREATORBUG10366_1_enumClass") + << QByteArray( + "enum class test { TEST_1, TEST_2 };\n" + "\n" + "void f() {\n" + " enum test test;\n" + " @switch (test) {\n" + " }\n" + "}\n") + << QByteArray( + "enum class test { TEST_1, TEST_2 };\n" + "\n" + "void f() {\n" + " enum test test;\n" + " switch (test) {\n" + " case test::TEST_1:\n" + " break;\n" + " case test::TEST_2:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: Find the correct enum type despite there being a declaration with the same name. + QTest::newRow("QTCREATORBUG10366_2") + << QByteArray( + "enum test1 { Wrong11, Wrong12 };\n" + "enum test { Right1, Right2 };\n" + "enum test2 { Wrong21, Wrong22 };\n" + "\n" + "int main() {\n" + " enum test test;\n" + " @switch (test) {\n" + " }\n" + "}\n") + << QByteArray( + "enum test1 { Wrong11, Wrong12 };\n" + "enum test { Right1, Right2 };\n" + "enum test2 { Wrong21, Wrong22 };\n" + "\n" + "int main() {\n" + " enum test test;\n" + " switch (test) {\n" + " case Right1:\n" + " break;\n" + " case Right2:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("QTCREATORBUG10366_2_enumClass") + << QByteArray( + "enum class test1 { Wrong11, Wrong12 };\n" + "enum class test { Right1, Right2 };\n" + "enum class test2 { Wrong21, Wrong22 };\n" + "\n" + "int main() {\n" + " enum test test;\n" + " @switch (test) {\n" + " }\n" + "}\n") + << QByteArray( + "enum class test1 { Wrong11, Wrong12 };\n" + "enum class test { Right1, Right2 };\n" + "enum class test2 { Wrong21, Wrong22 };\n" + "\n" + "int main() {\n" + " enum test test;\n" + " switch (test) {\n" + " case test::Right1:\n" + " break;\n" + " case test::Right2:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: Do not crash on incomplete case statetement. + QTest::newRow("doNotCrashOnIncompleteCase") + << QByteArray( + "enum E {};\n" + "void f(E o)\n" + "{\n" + " @switch (o)\n" + " {\n" + " case\n" + " }\n" + "}\n") + << QByteArray(); + + // Same as above for enum class. + QTest::newRow("doNotCrashOnIncompleteCase_enumClass") + << QByteArray( + "enum class E {};\n" + "void f(E o)\n" + "{\n" + " @switch (o)\n" + " {\n" + " case\n" + " }\n" + "}\n") + << QByteArray(); + + // Checks: complete switch statement where enum is goes via a template type parameter + QTest::newRow("QTCREATORBUG-24752") + << QByteArray( + "enum E {EA, EB};\n" + "template<typename T> struct S {\n" + " static T theType() { return T(); }\n" + "};\n" + "int main() {\n" + " @switch (S<E>::theType()) {\n" + " }\n" + "}\n") + << QByteArray( + "enum E {EA, EB};\n" + "template<typename T> struct S {\n" + " static T theType() { return T(); }\n" + "};\n" + "int main() {\n" + " switch (S<E>::theType()) {\n" + " case EA:\n" + " break;\n" + " case EB:\n" + " break;\n" + " }\n" + "}\n"); + + // Same as above for enum class. + QTest::newRow("QTCREATORBUG-24752_enumClass") + << QByteArray( + "enum class E {A, B};\n" + "template<typename T> struct S {\n" + " static T theType() { return T(); }\n" + "};\n" + "int main() {\n" + " @switch (S<E>::theType()) {\n" + " }\n" + "}\n") + << QByteArray( + "enum class E {A, B};\n" + "template<typename T> struct S {\n" + " static T theType() { return T(); }\n" + "};\n" + "int main() {\n" + " switch (S<E>::theType()) {\n" + " case E::A:\n" + " break;\n" + " case E::B:\n" + " break;\n" + " }\n" + "}\n"); + + // Checks: Complete switch statement where enum is return type of a template function + // which is outside the scope of the return value. + // TODO: Type minimization. + QTest::newRow("QTCREATORBUG-25998") + << QByteArray( + "template <typename T> T enumCast(int value) { return static_cast<T>(value); }\n" + "class Test {\n" + " enum class E { V1, V2 };" + " void func(int i) {\n" + " @switch (enumCast<E>(i)) {\n" + " }\n" + " }\n" + "};\n") + << QByteArray( + "template <typename T> T enumCast(int value) { return static_cast<T>(value); }\n" + "class Test {\n" + " enum class E { V1, V2 };" + " void func(int i) {\n" + " switch (enumCast<E>(i)) {\n" + " case Test::E::V1:\n" + " break;\n" + " case Test::E::V2:\n" + " break;\n" + " }\n" + " }\n" + "};\n"); + } + + void test() + { + QFETCH(QByteArray, original); + QFETCH(QByteArray, expected); + CompleteSwitchStatement factory; + QuickFixOperationTest(singleDocument(original, expected), &factory); + } +}; + +QObject *CompleteSwitchStatement::createTest() { return new CompleteSwitchStatementTest; } + +#endif // WITH_TESTS +} // namespace + +void registerCompleteSwitchStatementQuickfix() +{ + CppQuickFixFactory::registerFactory<CompleteSwitchStatement>(); +} + +} // namespace CppEditor::Internal + +#ifdef WITH_TESTS +#include <completeswitchstatement.moc> +#endif |