diff --git a/languages/clang/codegen/clangrefactoring.cpp b/languages/clang/codegen/clangrefactoring.cpp index 74ebe956b2..7add145c01 100644 --- a/languages/clang/codegen/clangrefactoring.cpp +++ b/languages/clang/codegen/clangrefactoring.cpp @@ -1,235 +1,290 @@ /* * This file is part of KDevelop * * Copyright 2015 Sergey Kalinichev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "clangrefactoring.h" #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include "duchain/clanghelpers.h" #include "duchain/documentfinderhelpers.h" #include "duchain/duchainutils.h" #include "sourcemanipulation.h" #include "util/clangdebug.h" using namespace KDevelop; +namespace { + +bool isDestructor(Declaration* decl) +{ + if (auto functionDef = dynamic_cast(decl)) { + // we found a definition, e.g. "Foo::~Foo()" + const auto functionDecl = functionDef->declaration(decl->topContext()); + if (auto classFunctionDecl = dynamic_cast(functionDecl)) { + return classFunctionDecl->isDestructor(); + } + } + else if (auto classFunctionDecl = dynamic_cast(decl)) { + // we found a declaration, e.g. "~Foo()" + return classFunctionDecl->isDestructor(); + } + + return false; +} + +} + ClangRefactoring::ClangRefactoring(QObject* parent) : BasicRefactoring(parent) { qRegisterMetaType(); } void ClangRefactoring::fillContextMenu(ContextMenuExtension& extension, Context* context) { auto declContext = dynamic_cast(context); if (!declContext) { return; } DUChainReadLocker lock; auto declaration = declContext->declaration().data(); if (!declaration) { return; } QFileInfo fileInfo(declaration->topContext()->url().str()); if (!fileInfo.isWritable()) { return; } auto action = new QAction(i18n("Rename %1", declaration->qualifiedIdentifier().toString()), this); action->setData(QVariant::fromValue(IndexedDeclaration(declaration))); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect(action, &QAction::triggered, this, &ClangRefactoring::executeRenameAction); extension.addAction(ContextMenuExtension::RefactorGroup, action); if (!validCandidateToMoveIntoSource(declaration)) { return; } action = new QAction( i18n("Create separate definition for %1", declaration->qualifiedIdentifier().toString()), this); action->setData(QVariant::fromValue(IndexedDeclaration(declaration))); connect(action, &QAction::triggered, this, &ClangRefactoring::executeMoveIntoSourceAction); extension.addAction(ContextMenuExtension::RefactorGroup, action); } bool ClangRefactoring::validCandidateToMoveIntoSource(Declaration* decl) { if (!decl || !decl->isFunctionDeclaration() || !decl->type()) { return false; } if (!decl->internalContext() || decl->internalContext()->type() != DUContext::Function) { return false; } if (dynamic_cast(decl)) { return false; } auto childCtx = decl->internalContext()->childContexts(); if (childCtx.isEmpty()) { return false; } auto ctx = childCtx.first(); if (!ctx || ctx->type() != DUContext::Other) { return false; } auto functionDecl = dynamic_cast(decl); if (!functionDecl || functionDecl->isInline()) { return false; } return true; } QString ClangRefactoring::moveIntoSource(const IndexedDeclaration& iDecl) { DUChainReadLocker lock; auto decl = iDecl.data(); if (!decl) { return i18n("No declaration under cursor"); } const auto headerUrl = decl->url(); auto targetUrl = DocumentFinderHelpers::sourceForHeader(headerUrl.str()); if (targetUrl.isEmpty() || targetUrl == headerUrl.str()) { // TODO: Create source file if it doesn't exist return i18n("No source file available for %1.", headerUrl.str()); } lock.unlock(); const IndexedString indexedTargetUrl(targetUrl); auto top = DUChain::self()->waitForUpdate(headerUrl, KDevelop::TopDUContext::AllDeclarationsAndContexts); auto targetTopContext = DUChain::self()->waitForUpdate(indexedTargetUrl, KDevelop::TopDUContext::AllDeclarationsAndContexts); lock.lock(); if (!targetTopContext) { return i18n("Failed to update DUChain for %1.", targetUrl); } if (!top || !iDecl.data() || iDecl.data() != decl) { return i18n("Declaration lost while updating."); } clangDebug() << "moving" << decl->qualifiedIdentifier(); if (!validCandidateToMoveIntoSource(decl)) { return i18n("Cannot create definition for this declaration."); } auto otherCtx = decl->internalContext()->childContexts().first(); auto code = createCodeRepresentation(headerUrl); if (!code) { return i18n("No document for %1", headerUrl.str()); } auto bodyRange = otherCtx->rangeInCurrentRevision(); auto prefixRange(ClangIntegration::DUChainUtils::functionSignatureRange(decl)); const auto prefixText = code->rangeText(prefixRange); for (int i = prefixText.length() - 1; i >= 0 && prefixText.at(i).isSpace(); --i) { if (bodyRange.start().column() == 0) { bodyRange.setStart(bodyRange.start() - KTextEditor::Cursor(1, 0)); if (bodyRange.start().line() == prefixRange.start().line()) { bodyRange.setStart(KTextEditor::Cursor(bodyRange.start().line(), prefixRange.start().column() + i)); } else { int lastNewline = prefixText.lastIndexOf(QLatin1Char('\n'), i - 1); bodyRange.setStart(KTextEditor::Cursor(bodyRange.start().line(), i - lastNewline - 1)); } } else { bodyRange.setStart(bodyRange.start() - KTextEditor::Cursor(0, 1)); } } const QString body = code->rangeText(bodyRange); SourceCodeInsertion ins(targetTopContext); auto parentId = decl->internalContext()->parentContext()->scopeIdentifier(false); ins.setSubScope(parentId); Identifier id(IndexedString(decl->qualifiedIdentifier().mid(parentId.count()).toString())); clangDebug() << "id:" << id; if (!ins.insertFunctionDeclaration(decl, id, body)) { return i18n("Insertion failed"); } lock.unlock(); auto applied = ins.changes().applyAllChanges(); if (!applied) { return i18n("Applying changes failed: %1", applied.m_failureReason); } // replace function body with a semicolon DocumentChangeSet changeHeader; changeHeader.addChange(DocumentChange(headerUrl, bodyRange, body, QStringLiteral(";"))); applied = changeHeader.applyAllChanges(); if (!applied) { return i18n("Applying changes failed: %1", applied.m_failureReason); } ICore::self()->languageController()->backgroundParser()->addDocument(headerUrl); ICore::self()->languageController()->backgroundParser()->addDocument(indexedTargetUrl); return {}; } void ClangRefactoring::executeMoveIntoSourceAction() { auto action = qobject_cast(sender()); Q_ASSERT(action); auto iDecl = action->data().value(); if (!iDecl.isValid()) { iDecl = declarationUnderCursor(false); } const auto error = moveIntoSource(iDecl); if (!error.isEmpty()) { KMessageBox::error(nullptr, error); } } + +DocumentChangeSet::ChangeResult ClangRefactoring::applyChangesToDeclarations(const QString& oldName, + const QString& newName, + DocumentChangeSet& changes, + const QList& declarations) +{ + foreach (const IndexedDeclaration decl, declarations) { + Declaration *declaration = decl.data(); + if (!declaration) + continue; + + if (declaration->range().isEmpty()) + clangDebug() << "found empty declaration:" << declaration->toString(); + + // special handling for dtors, their name is not "Foo", but "~Foo" + // see https://bugs.kde.org/show_bug.cgi?id=373452 + QString fixedOldName = oldName; + QString fixedNewName = newName; + + if (isDestructor(declaration)) { + clangDebug() << "found destructor:" << declaration->toString() << "-- making sure we replace the identifier correctly"; + fixedOldName = QLatin1Char('~') + oldName; + fixedNewName = QLatin1Char('~') + newName; + } + + TopDUContext *top = declaration->topContext(); + DocumentChangeSet::ChangeResult result = changes.addChange(DocumentChange(top->url(), declaration->rangeInCurrentRevision(), fixedOldName, fixedNewName)); + if (!result) + return result; + } + + return true; +} diff --git a/languages/clang/codegen/clangrefactoring.h b/languages/clang/codegen/clangrefactoring.h index 47d8cef4be..04d5c7dc03 100644 --- a/languages/clang/codegen/clangrefactoring.h +++ b/languages/clang/codegen/clangrefactoring.h @@ -1,56 +1,63 @@ /* * This file is part of KDevelop * * Copyright 2015 Sergey Kalinichev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifndef SIMPLEREFACTORING_H #define SIMPLEREFACTORING_H #include "clangprivateexport.h" #include +class TestRefactoring; + namespace KDevelop { class Context; class ContextMenuExtension; class Declaration; } class KDEVCLANGPRIVATE_EXPORT ClangRefactoring : public KDevelop::BasicRefactoring { Q_OBJECT public: explicit ClangRefactoring(QObject* parent = 0); void fillContextMenu(KDevelop::ContextMenuExtension& extension, KDevelop::Context* context) override; QString moveIntoSource(const KDevelop::IndexedDeclaration& iDecl); public slots: void executeMoveIntoSourceAction(); +protected: + KDevelop::DocumentChangeSet::ChangeResult applyChangesToDeclarations(const QString& oldName, const QString& newName, KDevelop::DocumentChangeSet& changes, const QList& declarations) override; + private: + friend TestRefactoring; + bool validCandidateToMoveIntoSource(KDevelop::Declaration* decl); }; #endif diff --git a/languages/clang/tests/CMakeLists.txt b/languages/clang/tests/CMakeLists.txt index 3205095217..12770d716f 100644 --- a/languages/clang/tests/CMakeLists.txt +++ b/languages/clang/tests/CMakeLists.txt @@ -1,96 +1,104 @@ add_executable(clang-parser clang-parser.cpp ) target_link_libraries(clang-parser KDev::Tests KDevClangPrivate ) add_library(codecompletiontestbase STATIC codecompletiontestbase.cpp) target_link_libraries(codecompletiontestbase PUBLIC KDev::Tests Qt5::Test KDevClangPrivate ) add_executable(clang-minimal-visitor WIN32 minimal_visitor.cpp ) ecm_mark_nongui_executable(clang-minimal-visitor) target_link_libraries(clang-minimal-visitor ${CLANG_LIBCLANG_LIB} ) ecm_add_test(test_buddies.cpp TEST_NAME test_buddies-clang LINK_LIBRARIES KDev::Tests Qt5::Test ) ecm_add_test(test_codecompletion.cpp TEST_NAME test_codecompletion LINK_LIBRARIES codecompletiontestbase ) ecm_add_test(test_assistants.cpp TEST_NAME test_assistants LINK_LIBRARIES KDev::Tests Qt5::Test KDevClangPrivate ) ecm_add_test(test_clangutils.cpp TEST_NAME test_clangutils LINK_LIBRARIES KDev::Tests Qt5::Test ${CLANG_LIBCLANG_LIB} KDevClangPrivate ) ecm_add_test(test_duchain.cpp TEST_NAME test_duchain-clang LINK_LIBRARIES KDev::Tests Qt5::Test KDevClangPrivate ) +ecm_add_test(test_refactoring.cpp + TEST_NAME test_refactoring-clang + LINK_LIBRARIES + KDev::Tests + Qt5::Test + KDevClangPrivate +) + ecm_add_test(test_duchainutils.cpp TEST_NAME test_duchainutils LINK_LIBRARIES KDev::Tests Qt5::Test KDevClangPrivate ) ecm_add_test(test_problems.cpp TEST_NAME test_problems LINK_LIBRARIES KDev::Tests Qt5::Test KDevClangPrivate ) configure_file("testfilepaths.h.cmake" "testfilepaths.h" ESCAPE_QUOTES) ecm_add_test(test_files.cpp TEST_NAME test_files-clang LINK_LIBRARIES Qt5::Test Qt5::Core KDev::Language KDev::Tests ) if(NOT COMPILER_OPTIMIZATIONS_DISABLED) ecm_add_test(bench_codecompletion.cpp TEST_NAME bench_codecompletion LINK_LIBRARIES codecompletiontestbase ) set_tests_properties(bench_codecompletion PROPERTIES TIMEOUT 30) endif() diff --git a/languages/clang/tests/test_refactoring.cpp b/languages/clang/tests/test_refactoring.cpp new file mode 100644 index 0000000000..af51213266 --- /dev/null +++ b/languages/clang/tests/test_refactoring.cpp @@ -0,0 +1,143 @@ +/* + * Copyright 2017 Kevin Funk + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "test_refactoring.h" + +#include +#include +#include +#include +#include + +#include "codegen/clangrefactoring.h" + +#include +#include +#include + +#include +#include + +QTEST_MAIN(TestRefactoring); + +using namespace KDevelop; + +TestRefactoring::~TestRefactoring() = default; + +void TestRefactoring::initTestCase() +{ + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false\ndefault.debug=true\nkdevelop.plugins.clang.debug=true\n")); + + QVERIFY(qputenv("KDEV_CLANG_DISPLAY_DIAGS", "1")); + + AutoTestShell::init({QStringLiteral("kdevclangsupport")}); + + auto core = TestCore::initialize(); + delete core->projectController(); + m_projectController = new TestProjectController(core); + core->setProjectController(m_projectController); +} + +void TestRefactoring::cleanupTestCase() +{ + TestCore::shutdown(); +} + +void TestRefactoring::cleanup() +{ +} + +void TestRefactoring::init() +{ +} + +void TestRefactoring::testClassRename() +{ + const QString codeBefore(R"( +class Foo { +public: + Foo(); + ~Foo(); +}; +Foo::Foo() { +} +Foo::~Foo() { +} + )"); + + const QString codeAfter(R"( +class FooNew { +public: + FooNew(); + ~FooNew(); +}; +FooNew::FooNew() { +} +FooNew::~FooNew() { +} + )"); + + QTemporaryDir dir; + auto project = new TestProject(Path(dir.path()), this); + m_projectController->addProject(project); + + TestFile file(codeBefore, "cpp", project, dir.path()); + QVERIFY(file.parseAndWait(TopDUContext::AllDeclarationsContextsAndUses)); + + DUChainReadLocker lock; + + auto top = file.topContext(); + QVERIFY(top); + + auto declaration = top->localDeclarations().first(); + QVERIFY(declaration); + + const QString originalName = declaration->identifier().identifier().str(); + const QString newName = QStringLiteral("FooNew"); + + QSharedPointer collector(new BasicRefactoringCollector(declaration)); + + // TODO: Do this without GUI? + UsesWidget uses(declaration, collector); + lock.unlock(); + + for (int i = 0; i < 30000; i += 1000) { + if (collector->isReady()) { + break; + } + QTest::qWait(1000); + } + QVERIFY(collector->isReady()); + + BasicRefactoring::NameAndCollector nameAndCollector{newName, collector}; + + auto languages = ICore::self()->languageController()->languagesForUrl(file.url().toUrl()); + QVERIFY(!languages.isEmpty()); + auto clangLanguageSupport = languages.first(); + QVERIFY(clangLanguageSupport); + auto clangRefactoring = qobject_cast(clangLanguageSupport->refactoring()); + QVERIFY(clangRefactoring); + + clangRefactoring->renameCollectedDeclarations(nameAndCollector.collector.data(), newName, originalName); + QCOMPARE(file.fileContents(), codeAfter); + + m_projectController->closeAllProjects(); +} + diff --git a/languages/clang/codegen/clangrefactoring.h b/languages/clang/tests/test_refactoring.h similarity index 53% copy from languages/clang/codegen/clangrefactoring.h copy to languages/clang/tests/test_refactoring.h index 47d8cef4be..0287ae174f 100644 --- a/languages/clang/codegen/clangrefactoring.h +++ b/languages/clang/tests/test_refactoring.h @@ -1,56 +1,49 @@ /* - * This file is part of KDevelop - * - * Copyright 2015 Sergey Kalinichev + * Copyright 2017 Kevin Funk * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * */ -#ifndef SIMPLEREFACTORING_H -#define SIMPLEREFACTORING_H - -#include "clangprivateexport.h" +#ifndef TEST_REFACTORING_H +#define TEST_REFACTORING_H -#include +#include -namespace KDevelop -{ -class Context; -class ContextMenuExtension; -class Declaration; +namespace KDevelop { +class TestProjectController; } -class KDEVCLANGPRIVATE_EXPORT ClangRefactoring : public KDevelop::BasicRefactoring +class TestRefactoring : public QObject { Q_OBJECT - public: - explicit ClangRefactoring(QObject* parent = 0); + ~TestRefactoring() override; - void fillContextMenu(KDevelop::ContextMenuExtension& extension, KDevelop::Context* context) override; +private slots: + void initTestCase(); + void cleanupTestCase(); - QString moveIntoSource(const KDevelop::IndexedDeclaration& iDecl); + void init(); + void cleanup(); -public slots: - void executeMoveIntoSourceAction(); + void testClassRename(); private: - bool validCandidateToMoveIntoSource(KDevelop::Declaration* decl); + KDevelop::TestProjectController* m_projectController; }; -#endif +#endif // TEST_REFACTORING_H