// 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 "rewritecontrolstatements.h" #include "../cppcodestylesettings.h" #include "../cppeditortr.h" #include "../cppeditorwidget.h" #include "../cpprefactoringchanges.h" #include "cppquickfix.h" #include #include #include #ifdef WITH_TESTS #include "cppquickfix_test.h" #endif using namespace CPlusPlus; using namespace TextEditor; using namespace Utils; namespace CppEditor::Internal { namespace { template Statement *asControlStatement(AST *node) { if constexpr (std::is_same_v) return node->asIfStatement(); if constexpr (std::is_same_v) return node->asWhileStatement(); if constexpr (std::is_same_v) return node->asForStatement(); if constexpr (std::is_same_v) return node->asRangeBasedForStatement(); if constexpr (std::is_same_v) return node->asDoStatement(); return nullptr; } template QList triggerTokens(const Statement *statement) { if constexpr (std::is_same_v) return {statement->if_token, statement->else_token}; if constexpr (std::is_same_v) return {statement->while_token}; if constexpr (std::is_same_v) return {statement->do_token}; if constexpr (std::is_same_v || std::is_same_v) { return {statement->for_token}; } } template int tokenToInsertOpeningBraceAfter(const Statement *statement) { if constexpr (std::is_same_v) return statement->do_token; return statement->rparen_token; } template class AddBracesToControlStatementOp : public CppQuickFixOperation { public: AddBracesToControlStatementOp(const CppQuickFixInterface &interface, const QList &statements, StatementAST *elseStatement, int elseToken) : CppQuickFixOperation(interface, 0) , m_statements(statements), m_elseStatement(elseStatement), m_elseToken(elseToken) { setDescription(Tr::tr("Add Curly Braces")); } void perform() override { ChangeSet changes; for (Statement * const statement : m_statements) { const int start = currentFile()->endOf(tokenToInsertOpeningBraceAfter(statement)); changes.insert(start, QLatin1String(" {")); if constexpr (std::is_same_v) { const int end = currentFile()->startOf(statement->while_token); changes.insert(end, QLatin1String("} ")); } else if constexpr (std::is_same_v) { if (statement->else_statement) { changes.insert(currentFile()->startOf(statement->else_token), "} "); } else { changes.insert(currentFile()->endOf(statement->statement->lastToken() - 1), "\n}"); } } else { const int end = currentFile()->endOf(statement->statement->lastToken() - 1); changes.insert(end, QLatin1String("\n}")); } } if (m_elseStatement) { changes.insert(currentFile()->endOf(m_elseToken), " {"); changes.insert(currentFile()->endOf(m_elseStatement->lastToken() - 1), "\n}"); } currentFile()->setChangeSet(changes); currentFile()->apply(); } private: const QList m_statements; StatementAST * const m_elseStatement; const int m_elseToken; }; template class RemoveBracesFromControlStatementOp : public CppQuickFixOperation { public: RemoveBracesFromControlStatementOp(const CppQuickFixInterface &interface, const QList &statements, StatementAST *elseStatement, int elseToken) : CppQuickFixOperation(interface, 0) , m_statements(statements), m_elseStatement(elseStatement) { Q_UNUSED(elseToken) setDescription(Tr::tr("Remove Curly Braces")); } void perform() override { ChangeSet changes; const auto findNewline = [&](int bracePos, int diff, int &newlinePos, int *nextNonSpacePos) { for (int i = bracePos + diff; true; i += diff) { const QChar &c = currentFile()->charAt(i); if (c == '\n' || c == QChar::ParagraphSeparator) { newlinePos = i; break; } if (!c.isSpace()) break; if (nextNonSpacePos) ++*nextNonSpacePos; } }; const auto removeBraceAndPossiblyLine = [&](int braceToken, bool removeTrailingSpace) { const int bracePos = currentFile()->startOf(braceToken); int prevNewline = -1; int nextNewline = -1; int start = bracePos; int end = bracePos + 1; findNewline(bracePos, -1, prevNewline, nullptr); findNewline(bracePos, 1, nextNewline, removeTrailingSpace ? &end : nullptr); if (prevNewline != -1 && nextNewline != -1) { start = prevNewline; end = nextNewline; } changes.remove(start, end); }; const auto apply = [&](const CompoundStatementAST *stmt) { QTC_ASSERT(stmt, return); removeBraceAndPossiblyLine(stmt->lbrace_token, false); removeBraceAndPossiblyLine(stmt->rbrace_token, std::is_same_v || std::is_same_v); if (!stmt->statement_list) changes.insert(currentFile()->endOf(stmt), "\n;"); }; for (Statement * const statement : m_statements) apply(statement->statement->asCompoundStatement()); if (m_elseStatement) apply(m_elseStatement->asCompoundStatement()); currentFile()->setChangeSet(changes); currentFile()->apply(); } private: const QList m_statements; StatementAST * const m_elseStatement; }; using StmtConstraint = std::function; template typename Op, typename Statement> bool checkControlStatementsHelper( const CppQuickFixInterface &interface, const StmtConstraint &constraint, QuickFixOperations &result) { Statement * const statement = asControlStatement(interface.path().last()); if (!statement) return false; QList statements; if (!Utils::anyOf(triggerTokens(statement), [&](int tok) { return interface.isCursorOn(tok); })) return false; bool abort = false; if (statement->statement && constraint(statement->statement, abort)) statements << statement; if (abort) return false; StatementAST *elseStmt = nullptr; int elseToken = 0; if constexpr (std::is_same_v) { IfStatementAST *currentIfStmt = statement; for (elseStmt = currentIfStmt->else_statement, elseToken = currentIfStmt->else_token; elseStmt && (currentIfStmt = elseStmt->asIfStatement()); elseStmt = currentIfStmt->else_statement, elseToken = currentIfStmt->else_token) { if (currentIfStmt->statement && constraint(currentIfStmt->statement, abort)) statements << currentIfStmt; if (abort) return false; } if (elseStmt && (elseStmt->asIfStatement() || !constraint(elseStmt, abort))) { if (abort) return false; elseStmt = nullptr; elseToken = 0; } } if (!statements.isEmpty() || elseStmt) { result << new Op(interface, statements, elseStmt, elseToken); return false; } return true; } template typename Op, typename... Statements> void checkControlStatements( const CppQuickFixInterface &interface, const StmtConstraint &constraint, QuickFixOperations &result) { (... || checkControlStatementsHelper(interface, constraint, result)); } class MoveDeclarationOutOfIfOp: public CppQuickFixOperation { public: MoveDeclarationOutOfIfOp(const CppQuickFixInterface &interface) : CppQuickFixOperation(interface) { setDescription(Tr::tr("Move Declaration out of Condition")); reset(); } void reset() { condition = mk.Condition(); pattern = mk.IfStatement(condition); } void perform() override { ChangeSet changes; changes.copy(currentFile()->range(core), currentFile()->startOf(condition)); int insertPos = currentFile()->startOf(pattern); changes.move(currentFile()->range(condition), insertPos); changes.insert(insertPos, QLatin1String(";\n")); currentFile()->apply(changes); } ASTMatcher matcher; ASTPatternBuilder mk; ConditionAST *condition = nullptr; IfStatementAST *pattern = nullptr; CoreDeclaratorAST *core = nullptr; }; class MoveDeclarationOutOfWhileOp: public CppQuickFixOperation { public: MoveDeclarationOutOfWhileOp(const CppQuickFixInterface &interface) : CppQuickFixOperation(interface) { setDescription(Tr::tr("Move Declaration out of Condition")); reset(); } void reset() { condition = mk.Condition(); pattern = mk.WhileStatement(condition); } void perform() override { ChangeSet changes; changes.insert(currentFile()->startOf(condition), QLatin1String("(")); changes.insert(currentFile()->endOf(condition), QLatin1String(") != 0")); int insertPos = currentFile()->startOf(pattern); const int conditionStart = currentFile()->startOf(condition); changes.move(conditionStart, currentFile()->startOf(core), insertPos); changes.copy(currentFile()->range(core), insertPos); changes.insert(insertPos, QLatin1String(";\n")); currentFile()->apply(changes); } ASTMatcher matcher; ASTPatternBuilder mk; ConditionAST *condition = nullptr; WhileStatementAST *pattern = nullptr; CoreDeclaratorAST *core = nullptr; }; class SplitIfStatementOp: public CppQuickFixOperation { public: SplitIfStatementOp(const CppQuickFixInterface &interface, int priority, IfStatementAST *pattern, BinaryExpressionAST *condition) : CppQuickFixOperation(interface, priority) , pattern(pattern) , condition(condition) { setDescription(Tr::tr("Split if Statement")); } void perform() override { const Token binaryToken = currentFile()->tokenAt(condition->binary_op_token); if (binaryToken.is(T_AMPER_AMPER)) splitAndCondition(); else splitOrCondition(); } void splitAndCondition() const { ChangeSet changes; int startPos = currentFile()->startOf(pattern); changes.insert(startPos, QLatin1String("if (")); changes.move(currentFile()->range(condition->left_expression), startPos); changes.insert(startPos, QLatin1String(") {\n")); const int lExprEnd = currentFile()->endOf(condition->left_expression); changes.remove(lExprEnd, currentFile()->startOf(condition->right_expression)); changes.insert(currentFile()->endOf(pattern), QLatin1String("\n}")); currentFile()->apply(changes); } void splitOrCondition() const { ChangeSet changes; StatementAST *ifTrueStatement = pattern->statement; CompoundStatementAST *compoundStatement = ifTrueStatement->asCompoundStatement(); int insertPos = currentFile()->endOf(ifTrueStatement); if (compoundStatement) changes.insert(insertPos, QLatin1String(" ")); else changes.insert(insertPos, QLatin1String("\n")); changes.insert(insertPos, QLatin1String("else if (")); const int rExprStart = currentFile()->startOf(condition->right_expression); changes.move(rExprStart, currentFile()->startOf(pattern->rparen_token), insertPos); changes.insert(insertPos, QLatin1String(")")); const int rParenEnd = currentFile()->endOf(pattern->rparen_token); changes.copy(rParenEnd, currentFile()->endOf(pattern->statement), insertPos); const int lExprEnd = currentFile()->endOf(condition->left_expression); changes.remove(lExprEnd, currentFile()->startOf(condition->right_expression)); currentFile()->apply(changes); } private: IfStatementAST *pattern; BinaryExpressionAST *condition; }; class OptimizeForLoopOperation: public CppQuickFixOperation { public: OptimizeForLoopOperation(const CppQuickFixInterface &interface, const ForStatementAST *forAst, const bool optimizePostcrement, const ExpressionAST *expression, const FullySpecifiedType &type) : CppQuickFixOperation(interface) , m_forAst(forAst) , m_optimizePostcrement(optimizePostcrement) , m_expression(expression) , m_type(type) { setDescription(Tr::tr("Optimize for-Loop")); } void perform() override { QTC_ASSERT(m_forAst, return); const CppRefactoringFilePtr file = currentFile(); ChangeSet change; // Optimize post (in|de)crement operator to pre (in|de)crement operator if (m_optimizePostcrement && m_forAst->expression) { PostIncrDecrAST *incrdecr = m_forAst->expression->asPostIncrDecr(); if (incrdecr && incrdecr->base_expression && incrdecr->incr_decr_token) { change.flip(file->range(incrdecr->base_expression), file->range(incrdecr->incr_decr_token)); } } // Optimize Condition int renamePos = -1; if (m_expression) { QString varName = QLatin1String("total"); if (file->textOf(m_forAst->initializer).size() == 1) { Overview oo = CppCodeStyleSettings::currentProjectCodeStyleOverview(); const QString typeAndName = oo.prettyType(m_type, varName); renamePos = file->endOf(m_forAst->initializer) - 1 + typeAndName.size(); change.insert(file->endOf(m_forAst->initializer) - 1, // "-1" because of ";" typeAndName + QLatin1String(" = ") + file->textOf(m_expression)); } else { // Check if varName is already used if (DeclarationStatementAST *ds = m_forAst->initializer->asDeclarationStatement()) { if (DeclarationAST *decl = ds->declaration) { if (SimpleDeclarationAST *sdecl = decl->asSimpleDeclaration()) { for (;;) { bool match = false; for (DeclaratorListAST *it = sdecl->declarator_list; it; it = it->next) { if (file->textOf(it->value->core_declarator) == varName) { varName += QLatin1Char('X'); match = true; break; } } if (!match) break; } } } } renamePos = file->endOf(m_forAst->initializer) + 1; change.insert(file->endOf(m_forAst->initializer) - 1, // "-1" because of ";" QLatin1String(", ") + varName + QLatin1String(" = ") + file->textOf(m_expression)); } ChangeSet::Range exprRange(file->startOf(m_expression), file->endOf(m_expression)); change.replace(exprRange, varName); } file->apply(change); // Select variable name and trigger symbol rename if (renamePos != -1) { QTextCursor c = file->cursor(); c.setPosition(renamePos); editor()->setTextCursor(c); editor()->renameSymbolUnderCursor(); c.select(QTextCursor::WordUnderCursor); editor()->setTextCursor(c); } } private: const ForStatementAST *m_forAst; const bool m_optimizePostcrement; const ExpressionAST *m_expression; const FullySpecifiedType m_type; }; /*! Replace if (Type name = foo()) {...} With Type name = foo(); if (name) {...} Activates on: the name of the introduced variable */ class MoveDeclarationOutOfIf: public CppQuickFixFactory { void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { const QList &path = interface.path(); using Ptr = QSharedPointer; Ptr op(new MoveDeclarationOutOfIfOp(interface)); int index = path.size() - 1; for (; index != -1; --index) { if (IfStatementAST *statement = path.at(index)->asIfStatement()) { if (statement->match(op->pattern, &op->matcher) && op->condition->declarator) { DeclaratorAST *declarator = op->condition->declarator; op->core = declarator->core_declarator; if (!op->core) return; if (interface.isCursorOn(op->core)) { op->setPriority(index); result.append(op); return; } op->reset(); } } } } }; /*! Replace while (Type name = foo()) {...} With Type name; while ((name = foo()) != 0) {...} Activates on: the name of the introduced variable */ class MoveDeclarationOutOfWhile: public CppQuickFixFactory { void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { const QList &path = interface.path(); QSharedPointer op(new MoveDeclarationOutOfWhileOp(interface)); int index = path.size() - 1; for (; index != -1; --index) { if (WhileStatementAST *statement = path.at(index)->asWhileStatement()) { if (statement->match(op->pattern, &op->matcher) && op->condition->declarator) { DeclaratorAST *declarator = op->condition->declarator; op->core = declarator->core_declarator; if (!op->core) return; if (!declarator->equal_token) return; if (!declarator->initializer) return; if (interface.isCursorOn(op->core)) { op->setPriority(index); result.append(op); return; } op->reset(); } } } } }; /*! Replace if (something && something_else) { } with if (something) if (something_else) { } } and if (something || something_else) x; with if (something) x; else if (something_else) x; Activates on: && or || */ class SplitIfStatement: public CppQuickFixFactory { #ifdef WITH_TESTS public: static QObject *createTest() { return new QObject; } #endif private: void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { IfStatementAST *pattern = nullptr; const QList &path = interface.path(); int index = path.size() - 1; for (; index != -1; --index) { AST *node = path.at(index); if (IfStatementAST *stmt = node->asIfStatement()) { pattern = stmt; break; } } if (!pattern || !pattern->statement) return; unsigned splitKind = 0; for (++index; index < path.size(); ++index) { AST *node = path.at(index); BinaryExpressionAST *condition = node->asBinaryExpression(); if (!condition) return; Token binaryToken = interface.currentFile()->tokenAt(condition->binary_op_token); // only accept a chain of ||s or &&s - no mixing if (!splitKind) { splitKind = binaryToken.kind(); if (splitKind != T_AMPER_AMPER && splitKind != T_PIPE_PIPE) return; // we can't reliably split &&s in ifs with an else branch if (splitKind == T_AMPER_AMPER && pattern->else_statement) return; } else if (splitKind != binaryToken.kind()) { return; } if (interface.isCursorOn(condition->binary_op_token)) { result << new SplitIfStatementOp(interface, index, pattern, condition); return; } } } }; /*! Add curly braces to a control statement that doesn't already contain a compound statement. I.e. if (a) b; becomes if (a) { b; } Activates on: the keyword */ class AddBracesToControlStatement : public CppQuickFixFactory { void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { if (interface.path().isEmpty()) return; const auto constraint = [](AST *ast, bool &) { return !ast->asCompoundStatement(); }; checkControlStatements(interface, constraint, result); } }; /*! * The reverse of AddBracesToControlStatement */ class RemoveBracesFromControlStatement : public CppQuickFixFactory { void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { if (interface.path().isEmpty()) return; const auto constraint = [&](AST *ast, bool &abort) { if (const auto compoundStmt = ast->asCompoundStatement()) { if (!compoundStmt->statement_list || !compoundStmt->statement_list->value) return true; // No statements. if (compoundStmt->statement_list->next) { abort = true; return false; // More than one statement. } // We have exactly one statement. Check whether it spans more than one line. const CppRefactoringFilePtr file = interface.currentFile(); const ChangeSet::Range stmtRange = file->range(compoundStmt->statement_list->value); int startLine, startColumn, endLine, endColumn; file->lineAndColumn(stmtRange.start, &startLine, &startColumn); file->lineAndColumn(stmtRange.end, &endLine, &endColumn); if (startLine == endLine) return true; abort = true; return false; } return false; }; checkControlStatements(interface, constraint, result); } }; /*! Optimizes a for loop to avoid permanent condition check and forces to use preincrement or predecrement operators in the expression of the for loop. */ class OptimizeForLoop : public CppQuickFixFactory { void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { const QList path = interface.path(); ForStatementAST *forAst = nullptr; if (!path.isEmpty()) forAst = path.last()->asForStatement(); if (!forAst || !interface.isCursorOn(forAst)) return; // Check for optimizing a postcrement const CppRefactoringFilePtr file = interface.currentFile(); bool optimizePostcrement = false; if (forAst->expression) { if (PostIncrDecrAST *incrdecr = forAst->expression->asPostIncrDecr()) { const Token t = file->tokenAt(incrdecr->incr_decr_token); if (t.is(T_PLUS_PLUS) || t.is(T_MINUS_MINUS)) optimizePostcrement = true; } } // Check for optimizing condition bool optimizeCondition = false; FullySpecifiedType conditionType; ExpressionAST *conditionExpression = nullptr; if (forAst->initializer && forAst->condition) { if (BinaryExpressionAST *binary = forAst->condition->asBinaryExpression()) { // Get the expression against which we should evaluate IdExpressionAST *conditionId = binary->left_expression->asIdExpression(); if (conditionId) { conditionExpression = binary->right_expression; } else { conditionId = binary->right_expression->asIdExpression(); conditionExpression = binary->left_expression; } if (conditionId && conditionExpression && !(conditionExpression->asNumericLiteral() || conditionExpression->asStringLiteral() || conditionExpression->asIdExpression() || conditionExpression->asUnaryExpression())) { // Determine type of for initializer FullySpecifiedType initializerType; if (DeclarationStatementAST *stmt = forAst->initializer->asDeclarationStatement()) { if (stmt->declaration) { if (SimpleDeclarationAST *decl = stmt->declaration->asSimpleDeclaration()) { if (decl->symbols) { if (Symbol *symbol = decl->symbols->value) initializerType = symbol->type(); } } } } // Determine type of for condition TypeOfExpression typeOfExpression; typeOfExpression.init(interface.semanticInfo().doc, interface.snapshot(), interface.context().bindings()); typeOfExpression.setExpandTemplates(true); Scope *scope = file->scopeAt(conditionId->firstToken()); const QList conditionItems = typeOfExpression( conditionId, interface.semanticInfo().doc, scope); if (!conditionItems.isEmpty()) conditionType = conditionItems.first().type(); if (conditionType.isValid() && (file->textOf(forAst->initializer) == QLatin1String(";") || initializerType == conditionType)) { optimizeCondition = true; } } } } if (optimizePostcrement || optimizeCondition) { result << new OptimizeForLoopOperation(interface, forAst, optimizePostcrement, optimizeCondition ? conditionExpression : nullptr, conditionType); } } }; #ifdef WITH_TESTS class AddBracesToControlStatementTest : public Tests::CppQuickFixTestObject { Q_OBJECT public: using CppQuickFixTestObject::CppQuickFixTestObject; }; class RemoveBracesFromControlStatementTest : public Tests::CppQuickFixTestObject { Q_OBJECT public: using CppQuickFixTestObject::CppQuickFixTestObject; }; class MoveDeclarationOutOfIfTest : public Tests::CppQuickFixTestObject { Q_OBJECT public: using CppQuickFixTestObject::CppQuickFixTestObject; }; class MoveDeclarationOutOfWhileTest : public Tests::CppQuickFixTestObject { Q_OBJECT public: using CppQuickFixTestObject::CppQuickFixTestObject; }; class OptimizeForLoopTest : public Tests::CppQuickFixTestObject { Q_OBJECT public: using CppQuickFixTestObject::CppQuickFixTestObject; }; #endif } // namespace void registerRewriteControlStatementQuickfixes() { REGISTER_QUICKFIX_FACTORY_WITH_STANDARD_TEST(AddBracesToControlStatement); REGISTER_QUICKFIX_FACTORY_WITH_STANDARD_TEST(RemoveBracesFromControlStatement); REGISTER_QUICKFIX_FACTORY_WITH_STANDARD_TEST(MoveDeclarationOutOfIf); REGISTER_QUICKFIX_FACTORY_WITH_STANDARD_TEST(MoveDeclarationOutOfWhile); REGISTER_QUICKFIX_FACTORY_WITH_STANDARD_TEST(OptimizeForLoop); CppQuickFixFactory::registerFactory(); } } // namespace CppEditor::Internal #ifdef WITH_TESTS #include #endif