diff --git a/plugins/clang/codegen/adaptsignatureassistant.cpp b/plugins/clang/codegen/adaptsignatureassistant.cpp index 91644e3db8..ff557f374d 100644 --- a/plugins/clang/codegen/adaptsignatureassistant.cpp +++ b/plugins/clang/codegen/adaptsignatureassistant.cpp @@ -1,318 +1,318 @@ /* Copyright 2009 David Nolden Copyright 2014 Kevin Funk This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "adaptsignatureassistant.h" #include #include #include #include #include #include #include #include #include "../util/clangdebug.h" using namespace KDevelop; namespace { Declaration *getDeclarationAtCursor(const KTextEditor::Cursor &cursor, const QUrl &documentUrl) { ENSURE_CHAIN_READ_LOCKED ReferencedTopDUContext top(DUChainUtils::standardContextForUrl(documentUrl)); if (!top) { clangDebug() << "no context found for document" << documentUrl; return nullptr; } const auto *context = top->findContextAt(top->transformToLocalRevision(cursor), true); return context->type() == DUContext::Function ? context->owner() : nullptr; } bool isConstructor(const Declaration *functionDecl) { - auto classFun = dynamic_cast(functionDecl); + auto classFun = dynamic_cast(DUChainUtils::declarationForDefinition(const_cast(functionDecl))); return classFun && classFun->isConstructor(); } Signature getDeclarationSignature(const Declaration *functionDecl, const DUContext *functionCtxt, bool includeDefaults) { ENSURE_CHAIN_READ_LOCKED int pos = 0; Signature signature; const AbstractFunctionDeclaration* abstractFunDecl = dynamic_cast(functionDecl); foreach(Declaration * parameter, functionCtxt->localDeclarations()) { signature.defaultParams << (includeDefaults ? abstractFunDecl->defaultParameterForArgument(pos).str() : QString()); signature.parameters << qMakePair(parameter->indexedType(), parameter->identifier().identifier().str()); ++pos; } signature.isConst = functionDecl->abstractType() && functionDecl->abstractType()->modifiers() & AbstractType::ConstModifier; if (!isConstructor(functionDecl)) { if (auto funType = functionDecl->type()) { signature.returnType = IndexedType(funType->returnType()); } } return signature; } } AdaptSignatureAssistant::AdaptSignatureAssistant(ILanguageSupport* supportedLanguage) : StaticAssistant(supportedLanguage) { } QString AdaptSignatureAssistant::title() const { return i18n("Adapt Signature"); } void AdaptSignatureAssistant::reset() { doHide(); clearActions(); m_editingDefinition = {}; m_declarationName = {}; m_otherSideId = DeclarationId(); m_otherSideTopContext = {}; m_otherSideContext = {}; m_oldSignature = {}; m_document = nullptr; m_view.clear(); } void AdaptSignatureAssistant::textChanged(KTextEditor::Document* doc, const KTextEditor::Range& invocationRange, const QString& removedText) { reset(); m_document = doc; m_lastEditPosition = invocationRange.end(); KTextEditor::Range sigAssistRange = invocationRange; if (!removedText.isEmpty()) { sigAssistRange.setRange(sigAssistRange.start(), sigAssistRange.start()); } DUChainReadLocker lock(DUChain::lock(), 300); if (!lock.locked()) { clangDebug() << "failed to lock duchain in time"; return; } KTextEditor::Range simpleInvocationRange = KTextEditor::Range(sigAssistRange); Declaration* funDecl = getDeclarationAtCursor(simpleInvocationRange.start(), m_document->url()); if (!funDecl || !funDecl->type()) { clangDebug() << "No function at cursor"; return; } /* TODO: Port? if(QtFunctionDeclaration* classFun = dynamic_cast(funDecl)) { if (classFun->isSignal()) { // do not offer to change signature of a signal, as the implementation will be generated by moc return; } } */ Declaration* otherSide = nullptr; FunctionDefinition* definition = dynamic_cast(funDecl); if (definition) { m_editingDefinition = true; otherSide = definition->declaration(); } else if ((definition = FunctionDefinition::definition(funDecl))) { m_editingDefinition = false; otherSide = definition; } if (!otherSide) { clangDebug() << "no other side for signature found"; return; } m_otherSideContext = DUContextPointer(DUChainUtils::getFunctionContext(otherSide)); if (!m_otherSideContext) { clangDebug() << "no context for other side found"; return; } m_declarationName = funDecl->identifier(); m_otherSideId = otherSide->id(); m_otherSideTopContext = ReferencedTopDUContext(otherSide->topContext()); m_oldSignature = getDeclarationSignature(otherSide, m_otherSideContext.data(), true); //Schedule an update, to make sure the ranges match DUChain::self()->updateContextForUrl(m_otherSideTopContext->url(), TopDUContext::AllDeclarationsAndContexts); } bool AdaptSignatureAssistant::isUseful() const { return !m_declarationName.isEmpty() && m_otherSideId.isValid() && !actions().isEmpty(); } bool AdaptSignatureAssistant::getSignatureChanges(const Signature& newSignature, QList& oldPositions) const { bool changed = false; for (int i = 0; i < newSignature.parameters.size(); ++i) { oldPositions.append(-1); } for (int curNewParam = newSignature.parameters.size() - 1; curNewParam >= 0; --curNewParam) { int foundAt = -1; for (int curOldParam = m_oldSignature.parameters.size() - 1; curOldParam >= 0; --curOldParam) { if (newSignature.parameters[curNewParam].first != m_oldSignature.parameters[curOldParam].first) { continue; //Different type == different parameters } if (newSignature.parameters[curNewParam].second == m_oldSignature.parameters[curOldParam].second || curOldParam == curNewParam) { //given the same type and either the same position or the same name, it's (probably) the same argument foundAt = curOldParam; if (newSignature.parameters[curNewParam].second != m_oldSignature.parameters[curOldParam].second || curOldParam != curNewParam) { changed = true; //Either the name changed at this position, or position of this name has changed } if (newSignature.parameters[curNewParam].second == m_oldSignature.parameters[curOldParam].second) { break; //Found an argument with the same name and type, no need to look further } //else: position/type match, but name match will trump, allowing: (int i=0, int j=1) => (int j=1, int i=0) } } if (foundAt < 0) { changed = true; } oldPositions[curNewParam] = foundAt; } if (newSignature.parameters.size() != m_oldSignature.parameters.size()) { changed = true; } if (newSignature.isConst != m_oldSignature.isConst) { changed = true; } if (newSignature.returnType != m_oldSignature.returnType) { changed = true; } return changed; } void AdaptSignatureAssistant::setDefaultParams(Signature& newSignature, const QList& oldPositions) const { bool hadDefaultParam = false; for (int i = 0; i < newSignature.defaultParams.size(); ++i) { const auto oldPos = oldPositions[i]; if (oldPos == -1) { // default-initialize new argument if we encountered a previous default param if (hadDefaultParam) { newSignature.defaultParams[i] = QStringLiteral("{} /* TODO */"); } } else { newSignature.defaultParams[i] = m_oldSignature.defaultParams[oldPos]; hadDefaultParam = hadDefaultParam || !newSignature.defaultParams[i].isEmpty(); } } } QList AdaptSignatureAssistant::getRenameActions(const Signature &newSignature, const QList &oldPositions) const { ENSURE_CHAIN_READ_LOCKED QList renameActions; if (!m_otherSideContext) { return renameActions; } for (int i = newSignature.parameters.size() - 1; i >= 0; --i) { if (oldPositions[i] == -1) { continue; //new parameter } Declaration *renamedDecl = m_otherSideContext->localDeclarations()[oldPositions[i]]; if (newSignature.parameters[i].second != m_oldSignature.parameters[oldPositions[i]].second) { const auto uses = renamedDecl->uses(); if (!uses.isEmpty()) { renameActions << new RenameAction(renamedDecl->identifier(), newSignature.parameters[i].second, RevisionedFileRanges::convert(uses)); } } } return renameActions; } void AdaptSignatureAssistant::updateReady(const KDevelop::IndexedString& document, const KDevelop::ReferencedTopDUContext& top) { if (!top || !m_document || document.toUrl() != m_document->url() || top->url() != IndexedString(m_document->url())) { return; } clearActions(); DUChainReadLocker lock; Declaration *functionDecl = getDeclarationAtCursor(m_lastEditPosition, m_document->url()); if (!functionDecl || functionDecl->identifier() != m_declarationName) { clangDebug() << "No function found at" << m_document->url() << m_lastEditPosition; return; } DUContext *functionCtxt = DUChainUtils::getFunctionContext(functionDecl); if (!functionCtxt) { clangDebug() << "No function context found for" << functionDecl->toString(); return; } #if 0 // TODO: Port if (QtFunctionDeclaration * classFun = dynamic_cast(functionDecl)) { if (classFun->isSignal()) { // do not offer to change signature of a signal, as the implementation will be generated by moc return; } } #endif //ParseJob having finished, get the signature that was modified Signature newSignature = getDeclarationSignature(functionDecl, functionCtxt, false); //Check for changes between m_oldSignature and newSignature, use oldPositions to store old<->new param index mapping QList oldPositions; if (!getSignatureChanges(newSignature, oldPositions)) { reset(); clangDebug() << "no changes to signature"; return; //No changes to signature } QList renameActions; if (m_editingDefinition) { setDefaultParams(newSignature, oldPositions); //restore default parameters before updating the declarations } else { renameActions = getRenameActions(newSignature, oldPositions); //rename as needed when updating the definition } IAssistantAction::Ptr action(new AdaptSignatureAction(m_otherSideId, m_otherSideTopContext, m_oldSignature, newSignature, m_editingDefinition, renameActions)); connect(action.data(), &IAssistantAction::executed, this, &AdaptSignatureAssistant::reset); addAction(action); emit actionsChanged(); } KTextEditor::Range AdaptSignatureAssistant::displayRange() const { if (!m_document) { return {}; } auto s = m_lastEditPosition; KTextEditor::Range ran = {s.line(), 0, s.line(), m_document->lineLength(s.line())}; return ran; } #include "moc_adaptsignatureassistant.cpp" diff --git a/plugins/clang/tests/test_assistants.cpp b/plugins/clang/tests/test_assistants.cpp index 79ada81a1a..ef48748595 100644 --- a/plugins/clang/tests/test_assistants.cpp +++ b/plugins/clang/tests/test_assistants.cpp @@ -1,792 +1,800 @@ /* This file is part of KDevelop Copyright 2012 Olivier de Gaalon 2014 David Stevens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "test_assistants.h" #include "codegen/clangrefactoring.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; using namespace KTextEditor; QTEST_MAIN(TestAssistants) ForegroundLock *globalTestLock = nullptr; StaticAssistantsManager *staticAssistantsManager() { return Core::self()->languageController()->staticAssistantsManager(); } void TestAssistants::initTestCase() { QLoggingCategory::setFilterRules(QStringLiteral( "*.debug=false\n" "default.debug=true\n" "kdevelop.plugins.clang.debug=true\n" )); QVERIFY(qputenv("KDEV_CLANG_DISPLAY_DIAGS", "1")); AutoTestShell::init({QStringLiteral("kdevclangsupport"), QStringLiteral("kdevproblemreporter")}); TestCore::initialize(); DUChain::self()->disablePersistentStorage(); Core::self()->languageController()->backgroundParser()->setDelay(0); Core::self()->sourceFormatterController()->disableSourceFormatting(true); CodeRepresentation::setDiskChangesForbidden(true); globalTestLock = new ForegroundLock; } void TestAssistants::cleanupTestCase() { Core::self()->cleanup(); delete globalTestLock; globalTestLock = nullptr; } static QUrl createFile(const QString& fileContents, const QString& extension, int id) { static QTemporaryDir tempDirA; Q_ASSERT(tempDirA.isValid()); static QDir dirA(tempDirA.path()); QFile file(dirA.filePath(QString::number(id) + extension)); file.open(QIODevice::WriteOnly | QIODevice::Text); file.write(fileContents.toUtf8()); file.close(); return QUrl::fromLocalFile(file.fileName()); } class Testbed { public: enum TestDoc { HeaderDoc, CppDoc }; enum IncludeBehavior { NoAutoInclude, AutoInclude, }; Testbed(const QString& headerContents, const QString& cppContents, IncludeBehavior include = AutoInclude) : m_includeBehavior(include) { static int i = 0; int id = i; ++i; m_headerDocument.url = createFile(headerContents,QStringLiteral(".h"),id); m_headerDocument.textDoc = openDocument(m_headerDocument.url); QString preamble; if (include == AutoInclude) preamble = QStringLiteral("#include \"%1\"\n").arg(m_headerDocument.url.toLocalFile()); m_cppDocument.url = createFile(preamble + cppContents,QStringLiteral(".cpp"),id); m_cppDocument.textDoc = openDocument(m_cppDocument.url); } ~Testbed() { Core::self()->documentController()->documentForUrl(m_cppDocument.url)->textDocument(); Core::self()->documentController()->documentForUrl(m_cppDocument.url)->close(KDevelop::IDocument::Discard); Core::self()->documentController()->documentForUrl(m_headerDocument.url)->close(KDevelop::IDocument::Discard); } void changeDocument(TestDoc which, Range where, const QString& what, bool waitForUpdate = false) { TestDocument document; if (which == CppDoc) { document = m_cppDocument; if (m_includeBehavior == AutoInclude) { where = Range(where.start().line() + 1, where.start().column(), where.end().line() + 1, where.end().column()); //The include adds a line } } else { document = m_headerDocument; } // we must activate the document, otherwise we cannot find the correct active view auto kdevdoc = ICore::self()->documentController()->documentForUrl(document.url); QVERIFY(kdevdoc); ICore::self()->documentController()->activateDocument(kdevdoc); auto view = ICore::self()->documentController()->activeTextDocumentView(); QCOMPARE(view->document(), document.textDoc); view->setSelection(where); view->removeSelectionText(); view->setCursorPosition(where.start()); view->insertText(what); QCoreApplication::processEvents(); if (waitForUpdate) { DUChain::self()->waitForUpdate(IndexedString(document.url), KDevelop::TopDUContext::AllDeclarationsAndContexts); } } QString documentText(TestDoc which) { if (which == CppDoc) { //The CPP document text shouldn't include the autogenerated include line QString text = m_cppDocument.textDoc->text(); return m_includeBehavior == AutoInclude ? text.mid(text.indexOf(QLatin1String("\n")) + 1) : text; } else return m_headerDocument.textDoc->text(); } QString includeFileName() const { return m_headerDocument.url.toLocalFile(); } KTextEditor::Document *document(TestDoc which) const { return Core::self()->documentController()->documentForUrl( which == CppDoc ? m_cppDocument.url : m_headerDocument.url)->textDocument(); } private: struct TestDocument { QUrl url; Document *textDoc; }; Document* openDocument(const QUrl& url) { Core::self()->documentController()->openDocument(url); DUChain::self()->waitForUpdate(IndexedString(url), KDevelop::TopDUContext::AllDeclarationsAndContexts); return Core::self()->documentController()->documentForUrl(url)->textDocument(); } IncludeBehavior m_includeBehavior; TestDocument m_headerDocument; TestDocument m_cppDocument; }; /** * A StateChange describes an insertion/deletion/replacement and the expected result **/ struct StateChange { StateChange(){}; StateChange(Testbed::TestDoc document, const Range& range, const QString& newText, const QString& result) : document(document) , range(range) , newText(newText) , result(result) { } Testbed::TestDoc document; Range range; QString newText; QString result; }; Q_DECLARE_METATYPE(StateChange) Q_DECLARE_METATYPE(QList) void TestAssistants::testRenameAssistant_data() { QTest::addColumn("fileContents"); QTest::addColumn("oldDeclarationName"); QTest::addColumn >("stateChanges"); QTest::addColumn("finalFileContents"); QTest::newRow("Prepend Text") << "int foo(int i)\n { i = 0; return i; }" << "i" << QList{ StateChange(Testbed::CppDoc, Range(0,12,0,12), "u", "ui"), StateChange(Testbed::CppDoc, Range(0,13,0,13), "z", "uzi"), } << "int foo(int uzi)\n { uzi = 0; return uzi; }"; QTest::newRow("Append Text") << "int foo(int i)\n { i = 0; return i; }" << "i" << (QList() << StateChange(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("d"), QStringLiteral("id"))) << "int foo(int id)\n { id = 0; return id; }"; QTest::newRow("Replace Text") << "int foo(int i)\n { i = 0; return i; }" << "i" << (QList() << StateChange(Testbed::CppDoc, Range(0,12,0,13), QStringLiteral("u"), QStringLiteral("u"))) << "int foo(int u)\n { u = 0; return u; }"; QTest::newRow("Paste Replace") << "int foo(int abg)\n { abg = 0; return abg; }" << "abg" << (QList() << StateChange(Testbed::CppDoc, Range(0,12,0,15), QStringLiteral("abcdefg"), QStringLiteral("abcdefg"))) << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }"; QTest::newRow("Paste Insert") << "int foo(int abg)\n { abg = 0; return abg; }" << "abg" << (QList() << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("cdef"), QStringLiteral("abcdefg"))) << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }"; QTest::newRow("Letter-by-Letter Prepend") << "int foo(int i)\n { i = 0; return i; }" << "i" << (QList() << StateChange(Testbed::CppDoc, Range(0,12,0,12), QStringLiteral("a"), QStringLiteral("ai")) << StateChange(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("b"), QStringLiteral("abi")) << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("c"), QStringLiteral("abci")) ) << "int foo(int abci)\n { abci = 0; return abci; }"; QTest::newRow("Letter-by-Letter Insert") << "int foo(int abg)\n { abg = 0; return abg; }" << "abg" << (QList() << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("c"), QStringLiteral("abcg")) << StateChange(Testbed::CppDoc, Range(0,15,0,15), QStringLiteral("d"), QStringLiteral("abcdg")) << StateChange(Testbed::CppDoc, Range(0,16,0,16), QStringLiteral("e"), QStringLiteral("abcdeg")) << StateChange(Testbed::CppDoc, Range(0,17,0,17), QStringLiteral("f"), QStringLiteral("abcdefg")) ) << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }"; } ProblemPointer findStaticAssistantProblem(const QVector& problems) { const auto renameProblemIt = std::find_if(problems.cbegin(), problems.cend(), [](const ProblemPointer& p) { return dynamic_cast(p.constData()); }); if (renameProblemIt != problems.cend()) return *renameProblemIt; return {}; } void TestAssistants::testRenameAssistant() { QFETCH(QString, fileContents); Testbed testbed(QLatin1String(""), fileContents); const auto document = testbed.document(Testbed::CppDoc); QVERIFY(document); QExplicitlySharedDataPointer assistant; QFETCH(QString, oldDeclarationName); QFETCH(QList, stateChanges); foreach(StateChange stateChange, stateChanges) { testbed.changeDocument(Testbed::CppDoc, stateChange.range, stateChange.newText, true); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); const auto problem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx)); if (problem) assistant = problem->solutionAssistant(); if (stateChange.result.isEmpty()) { QVERIFY(!assistant || !assistant->actions().size()); } else { qWarning() << assistant.data() << stateChange.result; QVERIFY(assistant && assistant->actions().size()); RenameAction *r = qobject_cast(assistant->actions().first().data()); QCOMPARE(r->oldDeclarationName(), oldDeclarationName); QCOMPARE(r->newDeclarationName(), stateChange.result); } } if (assistant && assistant->actions().size()) { assistant->actions().first()->execute(); } QFETCH(QString, finalFileContents); QCOMPARE(testbed.documentText(Testbed::CppDoc), finalFileContents); } void TestAssistants::testRenameAssistantUndoRename() { Testbed testbed(QLatin1String(""), QStringLiteral("int foo(int i)\n { i = 0; return i; }")); testbed.changeDocument(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("d"), true); const auto document = testbed.document(Testbed::CppDoc); QVERIFY(document); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); auto firstProblem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx)); QVERIFY(firstProblem); auto assistant = firstProblem->solutionAssistant(); QVERIFY(assistant); QVERIFY(assistant->actions().size() > 0); RenameAction *r = qobject_cast(assistant->actions().first().data()); qWarning() << topCtx->problems() << assistant->actions().first().data() << assistant->actions().size(); QVERIFY(r); // now rename the variable back to its original identifier testbed.changeDocument(Testbed::CppDoc, Range(0,13,0,14), QLatin1String("")); // there should be no assistant anymore QVERIFY(!assistant || assistant->actions().isEmpty()); } const QString SHOULD_ASSIST = QStringLiteral("SHOULD_ASSIST"); //An assistant will be visible const QString NO_ASSIST = QStringLiteral("NO_ASSIST"); //No assistant visible void TestAssistants::testSignatureAssistant_data() { QTest::addColumn("headerContents"); QTest::addColumn("cppContents"); QTest::addColumn >("stateChanges"); QTest::addColumn("finalHeaderContents"); QTest::addColumn("finalCppContents"); QTest::newRow("change_argument_type") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::HeaderDoc, Range(1,8,1,11), QStringLiteral("char"), SHOULD_ASSIST)) << "class Foo {\nint bar(char a, char* b, int c = 10); \n};" << "int Foo::bar(char a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"; QTest::newRow("prepend_arg_header") << "class Foo { void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 21, 0, 21), QStringLiteral("char c, "), SHOULD_ASSIST)) << "class Foo { void bar(char c, int i); };" << "void Foo::bar(char c, int i)\n{}"; QTest::newRow("prepend_arg_cpp") << "class Foo { void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 14, 0, 14), QStringLiteral("char c, "), SHOULD_ASSIST)) << "class Foo { void bar(char c, int i); };" << "void Foo::bar(char c, int i)\n{}"; QTest::newRow("change_default_parameter") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::HeaderDoc, Range(1,29,1,34), QLatin1String(""), NO_ASSIST)) << "class Foo {\nint bar(int a, char* b, int c); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"; QTest::newRow("change_function_type") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::CppDoc, Range(0,0,0,3), QStringLiteral("char"), SHOULD_ASSIST)) << "class Foo {\nchar bar(int a, char* b, int c = 10); \n};" << "char Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"; QTest::newRow("swap_args_definition_side") << "class Foo {\nint bar(int a, char* b, int c = 10); \n};" << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }" << (QList() << StateChange(Testbed::CppDoc, Range(0,13,0,28), QStringLiteral("char* b, int a,"), SHOULD_ASSIST)) << "class Foo {\nint bar(char* b, int a, int c = 10); \n};" << "int Foo::bar(char* b, int a, int c)\n{ a = c; b = new char; return a + *b; }"; // see https://bugs.kde.org/show_bug.cgi?id=299393 // actually related to the whitespaces in the header... QTest::newRow("change_function_constness") << "class Foo {\nvoid bar(const Foo&) const;\n};" << "void Foo::bar(const Foo&) const\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0,25,0,31), QLatin1String(""), SHOULD_ASSIST)) << "class Foo {\nvoid bar(const Foo&);\n};" << "void Foo::bar(const Foo&)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=356179 QTest::newRow("keep_static_cpp") << "class Foo { static void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 19, 0, 19), QStringLiteral(", char c"), SHOULD_ASSIST)) << "class Foo { static void bar(int i, char c); };" << "void Foo::bar(int i, char c)\n{}"; QTest::newRow("keep_static_header") << "class Foo { static void bar(int i); };" << "void Foo::bar(int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 33, 0, 33), QStringLiteral(", char c"), SHOULD_ASSIST)) << "class Foo { static void bar(int i, char c); };" << "void Foo::bar(int i, char c)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=356178 QTest::newRow("keep_default_args_cpp_before") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 14, 0, 14), QStringLiteral("char c, "), SHOULD_ASSIST)) << "class Foo { void bar(char c, bool b, int i = 0); };" << "void Foo::bar(char c, bool b, int i)\n{}"; QTest::newRow("keep_default_args_cpp_after") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 27, 0, 27), QStringLiteral(", char c"), SHOULD_ASSIST)) << "class Foo { void bar(bool b, int i = 0, char c = {} /* TODO */); };" << "void Foo::bar(bool b, int i, char c)\n{}"; QTest::newRow("keep_default_args_header_before") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 29, 0, 29), QStringLiteral("char c = 'A', "), SHOULD_ASSIST)) << "class Foo { void bar(bool b, char c = 'A', int i = 0); };" << "void Foo::bar(bool b, char c, int i)\n{}"; QTest::newRow("keep_default_args_header_after") << "class Foo { void bar(bool b, int i = 0); };" << "void Foo::bar(bool b, int i)\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 38, 0, 38), QStringLiteral(", char c = 'A'"), SHOULD_ASSIST)) << "class Foo { void bar(bool b, int i = 0, char c = 'A'); };" << "void Foo::bar(bool b, int i, char c)\n{}"; // see https://bugs.kde.org/show_bug.cgi?id=355356 QTest::newRow("no_retval_on_ctor") << "class Foo { Foo(); };" << "Foo::Foo()\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 16, 0, 16), QStringLiteral("char c"), SHOULD_ASSIST)) << "class Foo { Foo(char c); };" << "Foo::Foo(char c)\n{}"; + // see https://bugs.kde.org/show_bug.cgi?id=365420 + QTest::newRow("no_retval_on_ctor_while_editing_definition") + << "class Foo {\nFoo(int a); \n};" + << "Foo::Foo(int a)\n{}" + << (QList() << StateChange(Testbed::CppDoc, Range(0,13,0,14), QStringLiteral("b"), SHOULD_ASSIST)) + << "class Foo {\nFoo(int b); \n};" + << "Foo::Foo(int b)\n{}"; + // see https://bugs.kde.org/show_bug.cgi?id=298511 QTest::newRow("change_return_type_header") << "struct Foo { int bar(); };" << "int Foo::bar()\n{}" << (QList() << StateChange(Testbed::HeaderDoc, Range(0, 13, 0, 16), QStringLiteral("char"), SHOULD_ASSIST)) << "struct Foo { char bar(); };" << "char Foo::bar()\n{}"; QTest::newRow("change_return_type_impl") << "struct Foo { int bar(); };" << "int Foo::bar()\n{}" << (QList() << StateChange(Testbed::CppDoc, Range(0, 0, 0, 3), QStringLiteral("char"), SHOULD_ASSIST)) << "struct Foo { char bar(); };" << "char Foo::bar()\n{}"; } void TestAssistants::testSignatureAssistant() { QFETCH(QString, headerContents); QFETCH(QString, cppContents); Testbed testbed(headerContents, cppContents); QExplicitlySharedDataPointer assistant; QFETCH(QList, stateChanges); foreach (StateChange stateChange, stateChanges) { testbed.changeDocument(stateChange.document, stateChange.range, stateChange.newText, true); const auto document = testbed.document(stateChange.document); QVERIFY(document); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); const auto problem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx)); if (problem) { assistant = problem->solutionAssistant(); } if (stateChange.result == SHOULD_ASSIST) { #if CINDEX_VERSION_MINOR < 35 QEXPECT_FAIL("change_function_type", "Clang sees that return type of out-of-line definition differs from that in the declaration and won't parse the code...", Abort); QEXPECT_FAIL("change_return_type_impl", "Clang sees that return type of out-of-line definition differs from that in the declaration and won't include the function's AST and thus we never get updated about the new return type...", Abort); #endif QVERIFY(assistant && !assistant->actions().isEmpty()); } else { QVERIFY(!assistant || assistant->actions().isEmpty()); } } if (assistant && !assistant->actions().isEmpty()) assistant->actions().first()->execute(); QFETCH(QString, finalHeaderContents); QFETCH(QString, finalCppContents); QCOMPARE(testbed.documentText(Testbed::HeaderDoc), finalHeaderContents); QCOMPARE(testbed.documentText(Testbed::CppDoc), finalCppContents); } enum UnknownDeclarationAction { NoUnknownDeclarationAction = 0x0, ForwardDecls = 0x1, MissingInclude = 0x2 }; Q_DECLARE_FLAGS(UnknownDeclarationActions, UnknownDeclarationAction) Q_DECLARE_METATYPE(UnknownDeclarationActions) void TestAssistants::testUnknownDeclarationAssistant_data() { QTest::addColumn("headerContents"); QTest::addColumn("globalText"); QTest::addColumn("functionText"); QTest::addColumn("actions"); QTest::newRow("unincluded_struct") << "struct test{};" << "" << "test" << UnknownDeclarationActions(ForwardDecls | MissingInclude); QTest::newRow("forward_declared_struct") << "struct test{};" << "struct test;" << "test *f; f->" << UnknownDeclarationActions(MissingInclude); QTest::newRow("unknown_struct") << "" << "" << "test" << UnknownDeclarationActions(); QTest::newRow("not a class type") << "void test();" << "" << "test" << UnknownDeclarationActions(); } void TestAssistants::testUnknownDeclarationAssistant() { QFETCH(QString, headerContents); QFETCH(QString, globalText); QFETCH(QString, functionText); QFETCH(UnknownDeclarationActions, actions); static const auto cppContents = QStringLiteral("%1\nvoid f_u_n_c_t_i_o_n() {\n}"); Testbed testbed(headerContents, cppContents.arg(globalText), Testbed::NoAutoInclude); const auto document = testbed.document(Testbed::CppDoc); QVERIFY(document); const int line = document->lines() - 1; testbed.changeDocument(Testbed::CppDoc, Range(line, 0, line, 0), functionText, true); DUChainReadLocker lock; auto topCtx = DUChain::self()->chainForDocument(document->url()); QVERIFY(topCtx); const auto problems = topCtx->problems(); if (actions == NoUnknownDeclarationAction) { QVERIFY(!problems.isEmpty()); return; } auto firstProblem = problems.first(); auto assistant = firstProblem->solutionAssistant(); QVERIFY(assistant); const auto assistantActions = assistant->actions(); QStringList actionDescriptions; for (auto action: assistantActions) { actionDescriptions << action->description(); } { const bool hasForwardDecls = actionDescriptions.contains(i18n("Forward declare as 'struct'")) || actionDescriptions.contains(i18n("Forward declare as 'class'")); QCOMPARE(hasForwardDecls, static_cast(actions & ForwardDecls)); } { auto fileName = testbed.includeFileName(); fileName = fileName.mid(fileName.lastIndexOf('/') + 1); const auto directive = QStringLiteral("#include \"%1\"").arg(fileName); const auto description = i18n("Insert \'%1\'", directive); const bool hasMissingInclude = actionDescriptions.contains(description); QCOMPARE(hasMissingInclude, static_cast(actions & MissingInclude)); } } void TestAssistants::testMoveIntoSource() { QFETCH(QString, origHeader); QFETCH(QString, origImpl); QFETCH(QString, newHeader); QFETCH(QString, newImpl); QFETCH(QualifiedIdentifier, id); TestFile header(origHeader, QStringLiteral("h")); TestFile impl("#include \"" + header.url().byteArray() + "\"\n" + origImpl, QStringLiteral("cpp"), &header); { TopDUContext* headerCtx = nullptr; { DUChainReadLocker lock; headerCtx = DUChain::self()->chainForDocument(header.url()); } // Here is a problem: when launching tests one by one, we can reuse the same tmp file for headers. // But because of document chain for header wasn't unloaded properly in previous run we reuse it here // Therefore when using headerCtx->findDeclarations below we find declarations from the previous launch -> tests fail if (headerCtx) { // TODO: Investigate why this chain doesn't get updated when parsing source file DUChainWriteLocker lock; DUChain::self()->removeDocumentChain(headerCtx); } } impl.parse(KDevelop::TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(impl.waitForParsed()); IndexedDeclaration declaration; { DUChainReadLocker lock; auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); auto decls = headerCtx->findDeclarations(id); Q_ASSERT(!decls.isEmpty()); declaration = IndexedDeclaration(decls.first()); QVERIFY(declaration.isValid()); } CodeRepresentation::setDiskChangesForbidden(false); ClangRefactoring refactoring; QCOMPARE(refactoring.moveIntoSource(declaration), QString()); CodeRepresentation::setDiskChangesForbidden(true); QCOMPARE(header.fileContents(), newHeader); QVERIFY(impl.fileContents().endsWith(newImpl)); } void TestAssistants::testMoveIntoSource_data() { QTest::addColumn("origHeader"); QTest::addColumn("origImpl"); QTest::addColumn("newHeader"); QTest::addColumn("newImpl"); QTest::addColumn("id"); const QualifiedIdentifier fooId(QStringLiteral("foo")); QTest::newRow("globalfunction") << QStringLiteral("int foo()\n{\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo();\n") << QStringLiteral("\nint foo()\n{\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("staticfunction") << QStringLiteral("static int foo()\n{\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("static int foo();\n") << QStringLiteral("\nint foo()\n{\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("funcsameline") << QStringLiteral("int foo() {\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo();\n") << QStringLiteral("\nint foo() {\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("func-comment") << QStringLiteral("int foo()\n/* foobar */ {\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo()\n/* foobar */;\n") << QStringLiteral("\nint foo() {\n int i = 0;\n return 0;\n}\n") << fooId; QTest::newRow("func-comment2") << QStringLiteral("int foo()\n/*asdf*/\n{\n int i = 0;\n return 0;\n}\n") << QString() << QStringLiteral("int foo()\n/*asdf*/;\n") << QStringLiteral("\nint foo()\n{\n int i = 0;\n return 0;\n}\n") << fooId; const QualifiedIdentifier aFooId(QStringLiteral("a::foo")); QTest::newRow("class-method") << QStringLiteral("class a {\n int foo(){\n return 0;\n }\n};\n") << QString() << QStringLiteral("class a {\n int foo();\n};\n") << QStringLiteral("\nint a::foo() {\n return 0;\n }\n") << aFooId; QTest::newRow("class-method-const") << QStringLiteral("class a {\n int foo() const\n {\n return 0;\n }\n};\n") << QString() << QStringLiteral("class a {\n int foo() const;\n};\n") << QStringLiteral("\nint a::foo() const\n {\n return 0;\n }\n") << aFooId; QTest::newRow("class-method-const-sameline") << QStringLiteral("class a {\n int foo() const{\n return 0;\n }\n};\n") << QString() << QStringLiteral("class a {\n int foo() const;\n};\n") << QStringLiteral("\nint a::foo() const {\n return 0;\n }\n") << aFooId; QTest::newRow("elaborated-type") << QStringLiteral("namespace NS{class C{};} class a {\nint foo(const NS::C c) const{\nreturn 0;\n}\n};\n") << QString() << QStringLiteral("namespace NS{class C{};} class a {\nint foo(const NS::C c) const;\n};\n") << QStringLiteral("\nint a::foo(const NS::C c) const {\nreturn 0;\n}\n") << aFooId; QTest::newRow("add-into-namespace") << QStringLiteral("namespace NS{class a {\nint foo() const {\nreturn 0;\n}\n};\n}") << QStringLiteral("namespace NS{\n}") << QStringLiteral("namespace NS{class a {\nint foo() const;\n};\n}") << QStringLiteral("namespace NS{\n\nint a::foo() const {\nreturn 0;\n}\n}") << QualifiedIdentifier(QStringLiteral("NS::a::foo")); QTest::newRow("class-template-parameter") << QStringLiteral(R"( namespace first { template class Test{}; namespace second { template class List; } class MoveIntoSource { public: void f(const second::List>*>& param){} };} )") << QString() << QStringLiteral(R"( namespace first { template class Test{}; namespace second { template class List; } class MoveIntoSource { public: void f(const second::List>*>& param); };} )") << QStringLiteral("namespace first {\nvoid MoveIntoSource::f(const first::second::List< const volatile first::Test< first::second::List< int* > >* >& param) {}}\n\n") << QualifiedIdentifier(QStringLiteral("first::MoveIntoSource::f")); QTest::newRow("move-unexposed-type") << QStringLiteral("namespace std { template class basic_string; \ntypedef basic_string string;}\n void move(std::string i){}") << QString() << QStringLiteral("namespace std { template class basic_string; \ntypedef basic_string string;}\n void move(std::string i);") << QStringLiteral("void move(std::string i) {}\n") << QualifiedIdentifier(QStringLiteral("move")); QTest::newRow("move-constructor") << QStringLiteral("class Class{Class(){}\n};") << QString() << QStringLiteral("class Class{Class();\n};") << QStringLiteral("Class::Class() {}\n") << QualifiedIdentifier(QStringLiteral("Class::Class")); }