// 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 "synchronizememberfunctionorder.h" #include "../cppeditortr.h" #include "../cppeditorwidget.h" #include "../cpprefactoringchanges.h" #include "cppquickfix.h" #include "cppquickfixhelpers.h" #include #include #include #include #include #ifdef WITH_TESTS #include "cppquickfix_test.h" #include #include #include #endif #include using namespace Core; using namespace CPlusPlus; using namespace Utils; namespace CppEditor::Internal { namespace { class SynchronizeMemberFunctionOrderOp : public CppQuickFixOperation { public: SynchronizeMemberFunctionOrderOp( const CppQuickFixInterface &interface, const QList &decls) : CppQuickFixOperation(interface), m_state(std::make_shared()) { setDescription( Tr::tr("Re-order Member Function Definitions According to Declaration Order")); m_state->decls = decls; m_state->currentFile = currentFile(); } private: struct DefLocation { Symbol *decl = nullptr; Link defLoc; bool operator==(const DefLocation &other) const { return decl == other.decl && defLoc == other.defLoc; } }; using DefLocations = QList; struct State { using Ptr = std::shared_ptr; void insertSorted(Symbol *decl, const Link &link) { DefLocations &dl = defLocations[link.targetFilePath]; DefLocation newElem{decl, link}; const auto cmp = [](const DefLocation &elem, const DefLocation &value) { if (elem.defLoc.target.line < value.defLoc.target.line) return true; if (elem.defLoc.target.line > value.defLoc.target.line) return false; return elem.defLoc.target.column < value.defLoc.target.column; }; dl.insert(std::lower_bound(dl.begin(), dl.end(), newElem, cmp), newElem); } QList decls; QHash defLocations; CppRefactoringFilePtr currentFile; int remainingFollowSymbolOps = 0; }; void perform() override { for (Symbol * const decl : std::as_const(m_state->decls)) { QTextCursor cursor(currentFile()->document()->begin()); TranslationUnit * const tu = currentFile()->cppDocument()->translationUnit(); const int declPos = tu->getTokenPositionInDocument(decl->sourceLocation(), currentFile()->document()); cursor.setPosition(declPos); const CursorInEditor cursorInEditor( cursor, decl->filePath(), qobject_cast(currentFile()->editor()), currentFile()->editor()->textDocument(), currentFile()->cppDocument()); const auto callback = [decl, declPos, doc = cursor.document(), state = m_state]( const Link &link) { class FinishedChecker { public: FinishedChecker(const State::Ptr &state) : m_state(state) {} ~FinishedChecker() { if (--m_state->remainingFollowSymbolOps == 0) finish(m_state); }; private: const State::Ptr &m_state; } finishedChecker(state); if (!link.hasValidTarget()) return; if (decl->filePath() == link.targetFilePath) { const int linkPos = link.target.toPositionInDocument(doc); if (linkPos == declPos) return; } state->insertSorted(decl, link); }; ++m_state->remainingFollowSymbolOps; // Force queued execution, as the built-in editor can run the callback synchronously. const auto followSymbol = [cursorInEditor, callback] { NonInteractiveFollowSymbolMarker niMarker; CppModelManager::followSymbol( cursorInEditor, callback, true, false, FollowSymbolMode::Exact); }; QMetaObject::invokeMethod(CppModelManager::instance(), followSymbol, Qt::QueuedConnection); } } static void finish(const State::Ptr &state) { CppRefactoringChanges factory{CppModelManager::snapshot()}; const auto findAstRange = [](const CppRefactoringFile &file, const DefLocation &defLoc) { const QList astPath = ASTPath( file.cppDocument())(defLoc.defLoc.target.line, defLoc.defLoc.target.column + 1); for (auto it = astPath.rbegin(); it != astPath.rend(); ++it) { if (const auto funcDef = (*it)->asFunctionDefinition()) { AST *ast = funcDef; for (auto next = std::next(it); next != astPath.rend() && (*next)->asTemplateDeclaration(); ++next) { ast = *next; } const QList commentTokens = commentsForDeclaration( defLoc.decl, ast, *file.document(), file.cppDocument()); const int start = commentTokens.isEmpty() ? file.startOf(ast) : file.startOf(commentTokens.first()); return ChangeSet::Range{start, file.endOf(ast)}; } } return ChangeSet::Range(); }; for (auto it = state->defLocations.cbegin(); it != state->defLocations.cend(); ++it) { const DefLocations &defLocsActualOrder = it.value(); const DefLocations defLocsExpectedOrder = Utils::sorted( defLocsActualOrder, [](const DefLocation &loc1, const DefLocation &loc2) { return loc1.decl->sourceLocation() < loc2.decl->sourceLocation(); }); if (defLocsExpectedOrder == defLocsActualOrder) continue; CppRefactoringFilePtr file = it.key() == state->currentFile->filePath() ? state->currentFile : factory.cppFile(it.key()); ChangeSet changes; for (int i = 0; i < defLocsActualOrder.size(); ++i) { const DefLocation &actualLoc = defLocsActualOrder[i]; int expectedPos = -1; for (int j = 0; j < defLocsExpectedOrder.size(); ++j) { if (defLocsExpectedOrder[j].decl == actualLoc.decl) { expectedPos = j; break; } } if (expectedPos == i) continue; const ChangeSet::Range actualRange = findAstRange(*file, actualLoc); const ChangeSet::Range expectedRange = findAstRange(*file, defLocsActualOrder[expectedPos]); if (actualRange.end > actualRange.start && expectedRange.end > expectedRange.start) changes.move(actualRange, expectedRange.start); } QTC_ASSERT(!changes.hadErrors(), continue); file->setChangeSet(changes); file->apply(); } } const State::Ptr m_state; }; //! Ensures relative order of member function implementations is the same as declaration order. class SynchronizeMemberFunctionOrder : public CppQuickFixFactory { #ifdef WITH_TESTS public: static QObject *createTest(); #endif private: void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override { ClassSpecifierAST * const classAst = astForClassOperations(interface); if (!classAst || !classAst->symbol) return; QList memberFunctions; const TranslationUnit * const tu = interface.currentFile()->cppDocument()->translationUnit(); for (int i = 0; i < classAst->symbol->memberCount(); ++i) { Symbol *member = classAst->symbol->memberAt(i); // Skip macros if (tu->tokenAt(member->sourceLocation()).expanded()) continue; if (const auto templ = member->asTemplate()) member = templ->declaration(); if (member->type()->asFunctionType() && !member->asFunction()) memberFunctions << member; } if (!memberFunctions.isEmpty()) result << new SynchronizeMemberFunctionOrderOp(interface, memberFunctions); } }; #ifdef WITH_TESTS using namespace Tests; class SynchronizeMemberFunctionOrderTest : public QObject { Q_OBJECT private slots: void test_data() { QTest::addColumn("projectName"); QTest::addColumn("expectChanges"); QTest::newRow("no out-of-line definitions") << "no-out-of-line" << false; QTest::newRow("already sorted") << "already-sorted" << false; QTest::newRow("different impl locations") << "different-locations" << true; QTest::newRow("templates") << "templates" << true; } void test() { QFETCH(QString, projectName); QFETCH(bool, expectChanges); using namespace CppEditor::Tests; using namespace ProjectExplorer; using namespace TextEditor; // Set up project. Kit * const kit = Utils::findOr(KitManager::kits(), nullptr, [](const Kit *k) { return k->isValid() && !k->hasWarning() && k->value("QtSupport.QtInformation").isValid(); }); if (!kit) QSKIP("The test requires at least one valid kit with a valid Qt"); const auto projectDir = std::make_unique( ":/cppeditor/testcases/reorder-member-impls/" + projectName); SourceFilesRefreshGuard refreshGuard; ProjectOpenerAndCloser projectMgr; QVERIFY(projectMgr.open(projectDir->absolutePath(projectName + ".pro"), true, kit)); QVERIFY(refreshGuard.wait()); // Open header file and locate class. const auto headerFilePath = projectDir->absolutePath("header.h"); QVERIFY2(headerFilePath.exists(), qPrintable(headerFilePath.toUserOutput())); const auto editor = qobject_cast(EditorManager::openEditor(headerFilePath)); QVERIFY(editor); const auto doc = qobject_cast(editor->document()); QVERIFY(doc); QTextCursor classCursor = doc->document()->find("struct S"); QVERIFY(!classCursor.isNull()); editor->setCursorPosition(classCursor.position()); const auto editorWidget = qobject_cast(editor->editorWidget()); QVERIFY(editorWidget); QVERIFY(TestCase::waitForRehighlightedSemanticDocument(editorWidget)); // Query factory. SynchronizeMemberFunctionOrder factory; CppQuickFixInterface quickFixInterface(editorWidget, ExplicitlyInvoked); QuickFixOperations operations; factory.match(quickFixInterface, operations); operations.first()->perform(); if (expectChanges) QVERIFY(waitForSignalOrTimeout(doc, &IDocument::saved, 30000)); QTest::qWait(1000); // Compare all files. const FileFilter filter({"*_expected"}, QDir::Files); const FilePaths expectedDocuments = projectDir->filePath().dirEntries(filter); QVERIFY(!expectedDocuments.isEmpty()); for (const FilePath &expected : expectedDocuments) { static const QString suffix = "_expected"; const FilePath actual = expected.parentDir() .pathAppended(expected.fileName().chopped(suffix.size())); QVERIFY(actual.exists()); const auto actualContents = actual.fileContents(); QVERIFY(actualContents); const auto expectedContents = expected.fileContents(); const QByteArrayList actualLines = actualContents->split('\n'); const QByteArrayList expectedLines = expectedContents->split('\n'); if (actualLines.size() != expectedLines.size()) { qDebug().noquote().nospace() << "---\n" << *expectedContents << "EOF"; qDebug().noquote().nospace() << "+++\n" << *actualContents << "EOF"; } QCOMPARE(actualLines.size(), expectedLines.size()); for (int i = 0; i < actualLines.size(); ++i) { const QByteArray actualLine = actualLines.at(i); const QByteArray expectedLine = expectedLines.at(i); if (actualLine != expectedLine) qDebug() << "Unexpected content in line" << (i + 1) << "of file" << actual.fileName(); QCOMPARE(actualLine, expectedLine); } } } }; QObject *SynchronizeMemberFunctionOrder::createTest() { return new SynchronizeMemberFunctionOrderTest; } #endif } // namespace void registerSynchronizeMemberFunctionOrderQuickfix() { CppQuickFixFactory::registerFactory(); } } // namespace CppEditor::Internal #ifdef WITH_TESTS #include #endif