1 /*
2     SPDX-FileCopyrightText: 2012 Olivier de Gaalon <olivier.jg@gmail.com>
3     SPDX-FileCopyrightText: 2014 David Stevens <dgedstevens@gmail.com>
4 
5     SPDX-License-Identifier: LGPL-2.0-only
6 */
7 
8 #include "test_assistants.h"
9 
10 #include "codegen/clangrefactoring.h"
11 #include "codegen/adaptsignatureassistant.h"
12 
13 #include <QTest>
14 #include <QLoggingCategory>
15 #include <QTemporaryDir>
16 
17 #include <KLocalizedString>
18 
19 #include <KTextEditor/View>
20 #include <KTextEditor/Document>
21 
22 #include <tests/autotestshell.h>
23 #include <tests/testcore.h>
24 #include <tests/testfile.h>
25 
26 #include <util/foregroundlock.h>
27 
28 #include <interfaces/idocumentcontroller.h>
29 #include <interfaces/ilanguagecontroller.h>
30 #include <interfaces/isourceformattercontroller.h>
31 
32 #include <language/assistant/staticassistant.h>
33 #include <language/assistant/staticassistantsmanager.h>
34 #include <language/assistant/renameaction.h>
35 #include <language/assistant/renameassistant.h>
36 #include <language/backgroundparser/backgroundparser.h>
37 #include <language/duchain/duchain.h>
38 #include <language/duchain/duchainlock.h>
39 #include <language/duchain/duchainutils.h>
40 #include <language/codegen/coderepresentation.h>
41 
42 #include <shell/documentcontroller.h>
43 
44 #include <clang-c/Index.h>
45 
46 using namespace KDevelop;
47 using namespace KTextEditor;
48 
49 QTEST_MAIN(TestAssistants)
50 
51 ForegroundLock *globalTestLock = nullptr;
staticAssistantsManager()52 StaticAssistantsManager *staticAssistantsManager() { return Core::self()->languageController()->staticAssistantsManager(); }
53 
initTestCase()54 void TestAssistants::initTestCase()
55 {
56     QLoggingCategory::setFilterRules(QStringLiteral(
57         "*.debug=false\n"
58         "default.debug=true\n"
59         "kdevelop.plugins.clang.debug=true\n"
60     ));
61     QVERIFY(qputenv("KDEV_CLANG_DISPLAY_DIAGS", "1"));
62     AutoTestShell::init({QStringLiteral("kdevclangsupport"), QStringLiteral("kdevproblemreporter")});
63     TestCore::initialize();
64     DUChain::self()->disablePersistentStorage();
65     Core::self()->languageController()->backgroundParser()->setDelay(0);
66     Core::self()->sourceFormatterController()->disableSourceFormatting(true);
67     CodeRepresentation::setDiskChangesForbidden(true);
68 
69     globalTestLock = new ForegroundLock;
70 }
71 
cleanupTestCase()72 void TestAssistants::cleanupTestCase()
73 {
74     Core::self()->cleanup();
75     delete globalTestLock;
76     globalTestLock = nullptr;
77 }
78 
createFile(const QString & fileContents,const QString & extension,int id)79 static QUrl createFile(const QString& fileContents, const QString& extension, int id)
80 {
81     static QTemporaryDir tempDirA;
82     Q_ASSERT(tempDirA.isValid());
83     static QDir dirA(tempDirA.path());
84     QFile file(dirA.filePath(QString::number(id) + extension));
85     file.open(QIODevice::WriteOnly | QIODevice::Text);
86     file.write(fileContents.toUtf8());
87     file.close();
88     return QUrl::fromLocalFile(file.fileName());
89 }
90 
91 class Testbed
92 {
93 public:
94     enum TestDoc
95     {
96         HeaderDoc,
97         CppDoc
98     };
99 
100     enum IncludeBehavior
101     {
102         NoAutoInclude,
103         AutoInclude,
104     };
105 
Testbed(const QString & headerContents,const QString & cppContents,IncludeBehavior include=AutoInclude)106     Testbed(const QString& headerContents, const QString& cppContents, IncludeBehavior include = AutoInclude)
107         : m_includeBehavior(include)
108     {
109         static int i = 0;
110         int id = i;
111         ++i;
112         m_headerDocument.url = createFile(headerContents,QStringLiteral(".h"),id);
113         m_headerDocument.textDoc = openDocument(m_headerDocument.url);
114 
115         QString preamble;
116         if (include == AutoInclude)
117             preamble = QStringLiteral("#include \"%1\"\n").arg(m_headerDocument.url.toLocalFile());
118         m_cppDocument.url = createFile(preamble + cppContents,QStringLiteral(".cpp"),id);
119         m_cppDocument.textDoc = openDocument(m_cppDocument.url);
120     }
~Testbed()121     ~Testbed()
122     {
123         Core::self()->documentController()->documentForUrl(m_cppDocument.url)->textDocument();
124         Core::self()->documentController()->documentForUrl(m_cppDocument.url)->close(KDevelop::IDocument::Discard);
125         Core::self()->documentController()->documentForUrl(m_headerDocument.url)->close(KDevelop::IDocument::Discard);
126     }
127 
changeDocument(TestDoc which,Range where,const QString & what,bool waitForUpdate=false)128     void changeDocument(TestDoc which, Range where, const QString& what, bool waitForUpdate = false)
129     {
130         TestDocument document;
131         if (which == CppDoc)
132         {
133             document = m_cppDocument;
134             if (m_includeBehavior == AutoInclude) {
135                 where = Range(where.start().line() + 1, where.start().column(),
136                               where.end().line() + 1, where.end().column()); //The include adds a line
137             }
138         }
139         else {
140             document = m_headerDocument;
141         }
142         // we must activate the document, otherwise we cannot find the correct active view
143         auto kdevdoc = ICore::self()->documentController()->documentForUrl(document.url);
144         QVERIFY(kdevdoc);
145         ICore::self()->documentController()->activateDocument(kdevdoc);
146         auto view = ICore::self()->documentController()->activeTextDocumentView();
147         QCOMPARE(view->document(), document.textDoc);
148 
149         view->setSelection(where);
150         view->removeSelectionText();
151         view->setCursorPosition(where.start());
152         view->insertText(what);
153         QCoreApplication::processEvents();
154         if (waitForUpdate) {
155             DUChain::self()->waitForUpdate(IndexedString(document.url), KDevelop::TopDUContext::AllDeclarationsAndContexts);
156         }
157     }
158 
documentText(TestDoc which)159     QString documentText(TestDoc which)
160     {
161         if (which == CppDoc)
162         {
163             //The CPP document text shouldn't include the autogenerated include line
164             QString text = m_cppDocument.textDoc->text();
165             return m_includeBehavior == AutoInclude ? text.mid(text.indexOf(QLatin1String("\n")) + 1) : text;
166         }
167         else
168             return m_headerDocument.textDoc->text();
169     }
170 
includeFileName() const171     QString includeFileName() const
172     {
173         return m_headerDocument.url.toLocalFile();
174     }
175 
document(TestDoc which) const176     KTextEditor::Document *document(TestDoc which) const
177     {
178         return Core::self()->documentController()->documentForUrl(
179             which == CppDoc ? m_cppDocument.url : m_headerDocument.url)->textDocument();
180     }
181 
182 private:
183     struct TestDocument {
184         QUrl url;
185         Document *textDoc;
186     };
187 
openDocument(const QUrl & url)188     Document* openDocument(const QUrl& url)
189     {
190         Core::self()->documentController()->openDocument(url);
191         DUChain::self()->waitForUpdate(IndexedString(url), KDevelop::TopDUContext::AllDeclarationsAndContexts);
192         return Core::self()->documentController()->documentForUrl(url)->textDocument();
193     }
194 
195     IncludeBehavior m_includeBehavior;
196     TestDocument m_headerDocument;
197     TestDocument m_cppDocument;
198 };
199 
200 
201 /**
202  * A StateChange describes an insertion/deletion/replacement and the expected result
203 **/
204 struct StateChange
205 {
StateChangeStateChange206     StateChange(){};
StateChangeStateChange207     StateChange(Testbed::TestDoc document, const Range& range, const QString& newText, const QString& result)
208         : document(document)
209         , range(range)
210         , newText(newText)
211         , result(result)
212     {
213     }
214     Testbed::TestDoc document;
215     Range range;
216     QString newText;
217     QString result;
218 };
219 
Q_DECLARE_METATYPE(StateChange)220 Q_DECLARE_METATYPE(StateChange)
221 
222 void TestAssistants::testRenameAssistant_data()
223 {
224     QTest::addColumn<QString>("fileContents");
225     QTest::addColumn<QString>("oldDeclarationName");
226     QTest::addColumn<QList<StateChange> >("stateChanges");
227     QTest::addColumn<QString>("finalFileContents");
228 
229     QTest::newRow("Prepend Text")
230         << "int foo(int i)\n { i = 0; return i; }"
231         << "i"
232         << QList<StateChange>{
233             StateChange(Testbed::CppDoc, Range(0,12,0,12), "u", "ui"),
234             StateChange(Testbed::CppDoc, Range(0,13,0,13), "z", "uzi"),
235         }
236         << "int foo(int uzi)\n { uzi = 0; return uzi; }";
237 
238     QTest::newRow("Append Text")
239         << "int foo(int i)\n { i = 0; return i; }"
240         << "i"
241         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("d"), QStringLiteral("id")))
242         << "int foo(int id)\n { id = 0; return id; }";
243 
244     QTest::newRow("Replace Text")
245         << "int foo(int i)\n { i = 0; return i; }"
246         << "i"
247         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,12,0,13), QStringLiteral("u"), QStringLiteral("u")))
248         << "int foo(int u)\n { u = 0; return u; }";
249 
250     QTest::newRow("Paste Replace")
251         << "int foo(int abg)\n { abg = 0; return abg; }"
252         << "abg"
253         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,12,0,15), QStringLiteral("abcdefg"), QStringLiteral("abcdefg")))
254         << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }";
255 
256     QTest::newRow("Paste Insert")
257         << "int foo(int abg)\n { abg = 0; return abg; }"
258         << "abg"
259         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("cdef"), QStringLiteral("abcdefg")))
260         << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }";
261 
262     QTest::newRow("Letter-by-Letter Prepend")
263         << "int foo(int i)\n { i = 0; return i; }"
264         << "i"
265         << (QList<StateChange>()
266             << StateChange(Testbed::CppDoc, Range(0,12,0,12), QStringLiteral("a"), QStringLiteral("ai"))
267             << StateChange(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("b"), QStringLiteral("abi"))
268             << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("c"), QStringLiteral("abci"))
269         )
270         << "int foo(int abci)\n { abci = 0; return abci; }";
271     QTest::newRow("Letter-by-Letter Insert")
272         << "int foo(int abg)\n { abg = 0; return abg; }"
273         << "abg"
274         << (QList<StateChange>()
275             << StateChange(Testbed::CppDoc, Range(0,14,0,14), QStringLiteral("c"), QStringLiteral("abcg"))
276             << StateChange(Testbed::CppDoc, Range(0,15,0,15), QStringLiteral("d"), QStringLiteral("abcdg"))
277             << StateChange(Testbed::CppDoc, Range(0,16,0,16), QStringLiteral("e"), QStringLiteral("abcdeg"))
278             << StateChange(Testbed::CppDoc, Range(0,17,0,17), QStringLiteral("f"), QStringLiteral("abcdefg"))
279         )
280         << "int foo(int abcdefg)\n { abcdefg = 0; return abcdefg; }";
281 }
282 
findStaticAssistantProblem(const QVector<ProblemPointer> & problems)283 ProblemPointer findStaticAssistantProblem(const QVector<ProblemPointer>& problems)
284 {
285     const auto renameProblemIt = std::find_if(problems.cbegin(), problems.cend(), [](const ProblemPointer& p) {
286         return dynamic_cast<const StaticAssistantProblem*>(p.constData());
287     });
288     if (renameProblemIt != problems.cend())
289         return *renameProblemIt;
290 
291     return {};
292 }
293 
294 template <typename T>
findProblemWithAssistant(const QVector<ProblemPointer> & problems)295 ProblemPointer findProblemWithAssistant(const QVector<ProblemPointer>& problems)
296 {
297     const auto problemIterator = std::find_if(problems.cbegin(), problems.cend(), [](const ProblemPointer& p) {
298         return dynamic_cast<const T*>(p->solutionAssistant().constData());
299     });
300     if (problemIterator != problems.cend())
301         return *problemIterator;
302 
303     return {};
304 }
305 
testRenameAssistant()306 void TestAssistants::testRenameAssistant()
307 {
308     QFETCH(QString, fileContents);
309     Testbed testbed(QString(), fileContents);
310 
311     const auto document = testbed.document(Testbed::CppDoc);
312     QVERIFY(document);
313 
314     QExplicitlySharedDataPointer<IAssistant> assistant;
315 
316     QFETCH(QString, oldDeclarationName);
317     QFETCH(QList<StateChange>, stateChanges);
318     for (const StateChange& stateChange : qAsConst(stateChanges)) {
319         testbed.changeDocument(Testbed::CppDoc, stateChange.range, stateChange.newText, true);
320 
321         DUChainReadLocker lock;
322 
323         auto topCtx = DUChain::self()->chainForDocument(document->url());
324         QVERIFY(topCtx);
325 
326         const auto problem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx));
327         if (problem)
328             assistant = problem->solutionAssistant();
329 
330         if (stateChange.result.isEmpty()) {
331             QVERIFY(!assistant || !assistant->actions().size());
332         } else {
333             qWarning() << assistant.data() << stateChange.result;
334             QVERIFY(assistant && assistant->actions().size());
335             auto *r = qobject_cast<RenameAction*>(assistant->actions().first().data());
336             QCOMPARE(r->oldDeclarationName(), oldDeclarationName);
337             QCOMPARE(r->newDeclarationName(), stateChange.result);
338         }
339     }
340 
341     if (assistant && assistant->actions().size()) {
342         assistant->actions().first()->execute();
343     }
344     QFETCH(QString, finalFileContents);
345     QCOMPARE(testbed.documentText(Testbed::CppDoc), finalFileContents);
346 }
347 
testRenameAssistantUndoRename()348 void TestAssistants::testRenameAssistantUndoRename()
349 {
350     Testbed testbed(QString(), QStringLiteral("int foo(int i)\n { i = 0; return i; }"));
351     testbed.changeDocument(Testbed::CppDoc, Range(0,13,0,13), QStringLiteral("d"), true);
352 
353     const auto document = testbed.document(Testbed::CppDoc);
354     QVERIFY(document);
355 
356     DUChainReadLocker lock;
357     auto topCtx = DUChain::self()->chainForDocument(document->url());
358     QVERIFY(topCtx);
359 
360     auto firstProblem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(topCtx));
361     QVERIFY(firstProblem);
362     auto assistant = firstProblem->solutionAssistant();
363     QVERIFY(assistant);
364 
365     QVERIFY(assistant->actions().size() > 0);
366     auto *r = qobject_cast<RenameAction*>(assistant->actions().first().data());
367     qWarning() << topCtx->problems() << assistant->actions().first().data() << assistant->actions().size();
368     QVERIFY(r);
369 
370     // now rename the variable back to its original identifier
371     testbed.changeDocument(Testbed::CppDoc, Range(0,13,0,14), QString());
372     // there should be no assistant anymore
373     QVERIFY(!assistant || assistant->actions().isEmpty());
374 }
375 
376 const QString SHOULD_ASSIST = QStringLiteral("SHOULD_ASSIST"); //An assistant will be visible
377 const QString NO_ASSIST = QStringLiteral("NO_ASSIST");               //No assistant visible
378 
testSignatureAssistant_data()379 void TestAssistants::testSignatureAssistant_data()
380 {
381     QTest::addColumn<QString>("headerContents");
382     QTest::addColumn<QString>("cppContents");
383     QTest::addColumn<QList<StateChange> >("stateChanges");
384     QTest::addColumn<QString>("finalHeaderContents");
385     QTest::addColumn<QString>("finalCppContents");
386 
387     QTest::newRow("change_argument_type")
388       << "class Foo {\nint bar(int a, char* b, int c = 10); \n};"
389       << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"
390       << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(1,8,1,11), QStringLiteral("char"), SHOULD_ASSIST))
391       << "class Foo {\nint bar(char a, char* b, int c = 10); \n};"
392       << "int Foo::bar(char a, char* b, int c)\n{ a = c; b = new char; return a + *b; }";
393 
394     QTest::newRow("prepend_arg_header")
395       << "class Foo { void bar(int i); };"
396       << "void Foo::bar(int i)\n{}"
397       << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(0, 21, 0, 21), QStringLiteral("char c, "), SHOULD_ASSIST))
398       << "class Foo { void bar(char c, int i); };"
399       << "void Foo::bar(char c, int i)\n{}";
400 
401     QTest::newRow("prepend_arg_cpp")
402       << "class Foo { void bar(int i); };"
403       << "void Foo::bar(int i)\n{}"
404       << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0, 14, 0, 14), QStringLiteral("char c, "), SHOULD_ASSIST))
405       << "class Foo { void bar(char c, int i); };"
406       << "void Foo::bar(char c, int i)\n{}";
407 
408     QTest::newRow("change_default_parameter")
409         << "class Foo {\nint bar(int a, char* b, int c = 10); \n};"
410         << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"
411         << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(1,29,1,34), QString(), NO_ASSIST))
412         << "class Foo {\nint bar(int a, char* b, int c); \n};"
413         << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }";
414 
415     QTest::newRow("change_function_type")
416         << "class Foo {\nint bar(int a, char* b, int c = 10); \n};"
417         << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"
418         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,0,0,3), QStringLiteral("char"), SHOULD_ASSIST))
419         << "class Foo {\nchar bar(int a, char* b, int c = 10); \n};"
420         << "char Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }";
421 
422     QTest::newRow("swap_args_definition_side")
423         << "class Foo {\nint bar(int a, char* b, int c = 10); \n};"
424         << "int Foo::bar(int a, char* b, int c)\n{ a = c; b = new char; return a + *b; }"
425         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,13,0,28), QStringLiteral("char* b, int a,"), SHOULD_ASSIST))
426         << "class Foo {\nint bar(char* b, int a, int c = 10); \n};"
427         << "int Foo::bar(char* b, int a, int c)\n{ a = c; b = new char; return a + *b; }";
428 
429     // see https://bugs.kde.org/show_bug.cgi?id=299393
430     // actually related to the whitespaces in the header...
431     QTest::newRow("change_function_constness")
432         << "class Foo {\nvoid bar(const Foo&) const;\n};"
433         << "void Foo::bar(const Foo&) const\n{}"
434         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,25,0,31), QString(), SHOULD_ASSIST))
435         << "class Foo {\nvoid bar(const Foo&);\n};"
436         << "void Foo::bar(const Foo&)\n{}";
437 
438     // see https://bugs.kde.org/show_bug.cgi?id=356179
439     QTest::newRow("keep_static_cpp")
440         << "class Foo { static void bar(int i); };"
441         << "void Foo::bar(int i)\n{}"
442         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0, 19, 0, 19), QStringLiteral(", char c"), SHOULD_ASSIST))
443         << "class Foo { static void bar(int i, char c); };"
444         << "void Foo::bar(int i, char c)\n{}";
445     QTest::newRow("keep_static_header")
446         << "class Foo { static void bar(int i); };"
447         << "void Foo::bar(int i)\n{}"
448         << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(0, 33, 0, 33), QStringLiteral(", char c"), SHOULD_ASSIST))
449         << "class Foo { static void bar(int i, char c); };"
450         << "void Foo::bar(int i, char c)\n{}";
451 
452     // see https://bugs.kde.org/show_bug.cgi?id=356178
453     QTest::newRow("keep_default_args_cpp_before")
454         << "class Foo { void bar(bool b, int i = 0); };"
455         << "void Foo::bar(bool b, int i)\n{}"
456         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0, 14, 0, 14), QStringLiteral("char c, "), SHOULD_ASSIST))
457         << "class Foo { void bar(char c, bool b, int i = 0); };"
458         << "void Foo::bar(char c, bool b, int i)\n{}";
459     QTest::newRow("keep_default_args_cpp_after")
460         << "class Foo { void bar(bool b, int i = 0); };"
461         << "void Foo::bar(bool b, int i)\n{}"
462         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0, 27, 0, 27), QStringLiteral(", char c"), SHOULD_ASSIST))
463         << "class Foo { void bar(bool b, int i = 0, char c = {} /* TODO */); };"
464         << "void Foo::bar(bool b, int i, char c)\n{}";
465     QTest::newRow("keep_default_args_header_before")
466         << "class Foo { void bar(bool b, int i = 0); };"
467         << "void Foo::bar(bool b, int i)\n{}"
468         << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(0, 29, 0, 29), QStringLiteral("char c = 'A', "), SHOULD_ASSIST))
469         << "class Foo { void bar(bool b, char c = 'A', int i = 0); };"
470         << "void Foo::bar(bool b, char c, int i)\n{}";
471     QTest::newRow("keep_default_args_header_after")
472         << "class Foo { void bar(bool b, int i = 0); };"
473         << "void Foo::bar(bool b, int i)\n{}"
474         << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(0, 38, 0, 38), QStringLiteral(", char c = 'A'"), SHOULD_ASSIST))
475         << "class Foo { void bar(bool b, int i = 0, char c = 'A'); };"
476         << "void Foo::bar(bool b, int i, char c)\n{}";
477 
478     // see https://bugs.kde.org/show_bug.cgi?id=355356
479     QTest::newRow("no_retval_on_ctor")
480         << "class Foo { Foo(); };"
481         << "Foo::Foo()\n{}"
482         << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(0, 16, 0, 16), QStringLiteral("char c"), SHOULD_ASSIST))
483         << "class Foo { Foo(char c); };"
484         << "Foo::Foo(char c)\n{}";
485 
486     // see https://bugs.kde.org/show_bug.cgi?id=365420
487     QTest::newRow("no_retval_on_ctor_while_editing_definition")
488         << "class Foo {\nFoo(int a); \n};"
489         << "Foo::Foo(int a)\n{}"
490         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0,13,0,14), QStringLiteral("b"), SHOULD_ASSIST))
491         << "class Foo {\nFoo(int b); \n};"
492         << "Foo::Foo(int b)\n{}";
493 
494     // see https://bugs.kde.org/show_bug.cgi?id=298511
495     QTest::newRow("change_return_type_header")
496         << "struct Foo { int bar(); };"
497         << "int Foo::bar()\n{}"
498         << (QList<StateChange>() << StateChange(Testbed::HeaderDoc, Range(0, 13, 0, 16), QStringLiteral("char"), SHOULD_ASSIST))
499         << "struct Foo { char bar(); };"
500         << "char Foo::bar()\n{}";
501     QTest::newRow("change_return_type_impl")
502         << "struct Foo { int bar(); };"
503         << "int Foo::bar()\n{}"
504         << (QList<StateChange>() << StateChange(Testbed::CppDoc, Range(0, 0, 0, 3), QStringLiteral("char"), SHOULD_ASSIST))
505         << "struct Foo { char bar(); };"
506         << "char Foo::bar()\n{}";
507 }
508 
testSignatureAssistant()509 void TestAssistants::testSignatureAssistant()
510 {
511     QFETCH(QString, headerContents);
512     QFETCH(QString, cppContents);
513     Testbed testbed(headerContents, cppContents);
514 
515     QExplicitlySharedDataPointer<IAssistant> assistant;
516 
517     QFETCH(QList<StateChange>, stateChanges);
518     for (const StateChange& stateChange : qAsConst(stateChanges)) {
519         testbed.changeDocument(stateChange.document, stateChange.range, stateChange.newText, true);
520 
521         const auto document = testbed.document(stateChange.document);
522         QVERIFY(document);
523 
524         DUChainReadLocker lock;
525 
526         auto topCtx = DUChain::self()->chainForDocument(document->url());
527         QVERIFY(topCtx);
528 
529         const auto problem = findProblemWithAssistant<AdaptSignatureAssistant>(DUChainUtils::allProblemsForContext(topCtx));
530         if (problem) {
531             assistant = problem->solutionAssistant();
532         }
533 
534         if (stateChange.result == SHOULD_ASSIST) {
535 #if CINDEX_VERSION_MINOR < 35
536             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);
537             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);
538 #endif
539             QVERIFY(assistant && !assistant->actions().isEmpty());
540         } else {
541             QVERIFY(!assistant || assistant->actions().isEmpty());
542         }
543     }
544 
545     if (assistant && !assistant->actions().isEmpty())
546         assistant->actions().first()->execute();
547 
548     QFETCH(QString, finalHeaderContents);
549     QFETCH(QString, finalCppContents);
550     QCOMPARE(testbed.documentText(Testbed::HeaderDoc), finalHeaderContents);
551     QCOMPARE(testbed.documentText(Testbed::CppDoc), finalCppContents);
552 }
553 
554 enum UnknownDeclarationAction
555 {
556     NoUnknownDeclarationAction = 0x0,
557     ForwardDecls = 0x1,
558     MissingInclude = 0x2
559 };
Q_DECLARE_FLAGS(UnknownDeclarationActions,UnknownDeclarationAction)560 Q_DECLARE_FLAGS(UnknownDeclarationActions, UnknownDeclarationAction)
561 Q_DECLARE_METATYPE(UnknownDeclarationActions)
562 
563 void TestAssistants::testUnknownDeclarationAssistant_data()
564 {
565     QTest::addColumn<QString>("headerContents");
566     QTest::addColumn<QString>("globalText");
567     QTest::addColumn<QString>("functionText");
568     QTest::addColumn<UnknownDeclarationActions>("actions");
569 
570     QTest::newRow("unincluded_struct") << "struct test{};" << "" << "test"
571         << UnknownDeclarationActions(ForwardDecls | MissingInclude);
572     QTest::newRow("forward_declared_struct") << "struct test{};" << "struct test;" << "test *f; f->"
573         << UnknownDeclarationActions(MissingInclude);
574     QTest::newRow("unknown_struct") << "" << "" << "test"
575         << UnknownDeclarationActions();
576     QTest::newRow("not a class type") << "void test();" << "" << "test"
577         << UnknownDeclarationActions();
578 }
579 
testUnknownDeclarationAssistant()580 void TestAssistants::testUnknownDeclarationAssistant()
581 {
582     QFETCH(QString, headerContents);
583     QFETCH(QString, globalText);
584     QFETCH(QString, functionText);
585     QFETCH(UnknownDeclarationActions, actions);
586 
587     static const auto cppContents = QStringLiteral("%1\nvoid f_u_n_c_t_i_o_n() {\n}");
588     Testbed testbed(headerContents, cppContents.arg(globalText), Testbed::NoAutoInclude);
589     const auto document = testbed.document(Testbed::CppDoc);
590     QVERIFY(document);
591     const int line = document->lines() - 1;
592     testbed.changeDocument(Testbed::CppDoc, Range(line, 0, line, 0), functionText, true);
593 
594     DUChainReadLocker lock;
595 
596     auto topCtx = DUChain::self()->chainForDocument(document->url());
597     QVERIFY(topCtx);
598 
599     const auto problems = topCtx->problems();
600 
601     if (actions == NoUnknownDeclarationAction) {
602         QVERIFY(!problems.isEmpty());
603         return;
604     }
605 
606     auto firstProblem = problems.first();
607     auto assistant = firstProblem->solutionAssistant();
608     QVERIFY(assistant);
609     const auto assistantActions = assistant->actions();
610     QStringList actionDescriptions;
611     for (auto action: assistantActions) {
612         actionDescriptions << action->description();
613     }
614 
615     {
616         const bool hasForwardDecls =
617             actionDescriptions.contains(i18n("Forward declare as 'struct'")) ||
618             actionDescriptions.contains(i18n("Forward declare as 'class'"));
619         QCOMPARE(hasForwardDecls, static_cast<bool>(actions & ForwardDecls));
620     }
621 
622     {
623         auto fileName = testbed.includeFileName();
624         fileName.remove(0, fileName.lastIndexOf('/') + 1);
625         const auto directive = QStringLiteral("#include \"%1\"").arg(fileName);
626         const auto description = i18n("Insert \'%1\'", directive);
627         const bool hasMissingInclude = actionDescriptions.contains(description);
628         QCOMPARE(hasMissingInclude, static_cast<bool>(actions & MissingInclude));
629     }
630 }
631 
testMoveIntoSource()632 void TestAssistants::testMoveIntoSource()
633 {
634     QFETCH(QString, origHeader);
635     QFETCH(QString, origImpl);
636     QFETCH(QString, newHeader);
637     QFETCH(QString, newImpl);
638     QFETCH(QualifiedIdentifier, id);
639 
640     TestFile header(origHeader, QStringLiteral("h"));
641     TestFile impl("#include \"" + header.url().byteArray() + "\"\n" + origImpl, QStringLiteral("cpp"), &header);
642 
643     {
644         TopDUContext* headerCtx = nullptr;
645         {
646             DUChainReadLocker lock;
647             headerCtx = DUChain::self()->chainForDocument(header.url());
648         }
649         // Here is a problem: when launching tests one by one, we can reuse the same tmp file for headers.
650         // But because of document chain for header wasn't unloaded properly in previous run we reuse it here
651         // Therefore when using headerCtx->findDeclarations below we find declarations from the previous launch -> tests fail
652         if (headerCtx) {
653             // TODO: Investigate why this chain doesn't get updated when parsing source file
654             DUChainWriteLocker lock;
655             DUChain::self()->removeDocumentChain(headerCtx);
656         }
657     }
658 
659     impl.parse(KDevelop::TopDUContext::AllDeclarationsContextsAndUses);
660     QVERIFY(impl.waitForParsed());
661 
662     IndexedDeclaration declaration;
663     {
664         DUChainReadLocker lock;
665         auto headerCtx = DUChain::self()->chainForDocument(header.url());
666         QVERIFY(headerCtx);
667         auto decls = headerCtx->findDeclarations(id);
668         Q_ASSERT(!decls.isEmpty());
669         declaration = IndexedDeclaration(decls.first());
670         QVERIFY(declaration.isValid());
671     }
672     CodeRepresentation::setDiskChangesForbidden(false);
673     ClangRefactoring refactoring;
674     QCOMPARE(refactoring.moveIntoSource(declaration), QString());
675     CodeRepresentation::setDiskChangesForbidden(true);
676 
677     QCOMPARE(header.fileContents(), newHeader);
678     QVERIFY(impl.fileContents().endsWith(newImpl));
679 }
680 
testMoveIntoSource_data()681 void TestAssistants::testMoveIntoSource_data()
682 {
683     QTest::addColumn<QString>("origHeader");
684     QTest::addColumn<QString>("origImpl");
685     QTest::addColumn<QString>("newHeader");
686     QTest::addColumn<QString>("newImpl");
687     QTest::addColumn<QualifiedIdentifier>("id");
688 
689     const QualifiedIdentifier fooId(QStringLiteral("foo"));
690 
691     QTest::newRow("globalfunction") << QStringLiteral("int foo()\n{\n    int i = 0;\n    return 0;\n}\n")
692                                     << QString()
693                                     << QStringLiteral("int foo();\n")
694                                     << QStringLiteral("\nint foo()\n{\n    int i = 0;\n    return 0;\n}\n")
695                                     << fooId;
696 
697     QTest::newRow("staticfunction") << QStringLiteral("static int foo()\n{\n    int i = 0;\n    return 0;\n}\n")
698                                     << QString()
699                                     << QStringLiteral("static int foo();\n")
700                                     << QStringLiteral("\nint foo()\n{\n    int i = 0;\n    return 0;\n}\n")
701                                     << fooId;
702 
703     QTest::newRow("funcsameline") << QStringLiteral("int foo() {\n    int i = 0;\n    return 0;\n}\n")
704                                     << QString()
705                                     << QStringLiteral("int foo();\n")
706                                     << QStringLiteral("\nint foo() {\n    int i = 0;\n    return 0;\n}\n")
707                                     << fooId;
708 
709     QTest::newRow("func-comment") << QStringLiteral("int foo()\n/* foobar */ {\n    int i = 0;\n    return 0;\n}\n")
710                                     << QString()
711                                     << QStringLiteral("int foo()\n/* foobar */;\n")
712                                     << QStringLiteral("\nint foo() {\n    int i = 0;\n    return 0;\n}\n")
713                                     << fooId;
714 
715     QTest::newRow("func-comment2") << QStringLiteral("int foo()\n/*asdf*/\n{\n    int i = 0;\n    return 0;\n}\n")
716                                     << QString()
717                                     << QStringLiteral("int foo()\n/*asdf*/;\n")
718                                     << QStringLiteral("\nint foo()\n{\n    int i = 0;\n    return 0;\n}\n")
719                                     << fooId;
720 
721     const QualifiedIdentifier aFooId(QStringLiteral("a::foo"));
722     QTest::newRow("class-method") << QStringLiteral("class a {\n    int foo(){\n        return 0;\n    }\n};\n")
723                                     << QString()
724                                     << QStringLiteral("class a {\n    int foo();\n};\n")
725                                     << QStringLiteral("\nint a::foo() {\n        return 0;\n    }\n")
726                                     << aFooId;
727 
728     QTest::newRow("class-method-const") << QStringLiteral("class a {\n    int foo() const\n    {\n        return 0;\n    }\n};\n")
729                                     << QString()
730                                     << QStringLiteral("class a {\n    int foo() const;\n};\n")
731                                     << QStringLiteral("\nint a::foo() const\n    {\n        return 0;\n    }\n")
732                                     << aFooId;
733 
734     QTest::newRow("class-method-const-sameline") << QStringLiteral("class a {\n    int foo() const{\n        return 0;\n    }\n};\n")
735                                     << QString()
736                                     << QStringLiteral("class a {\n    int foo() const;\n};\n")
737                                     << QStringLiteral("\nint a::foo() const {\n        return 0;\n    }\n")
738                                     << aFooId;
739     QTest::newRow("elaborated-type") << QStringLiteral("namespace NS{class C{};} class a {\nint foo(const NS::C c) const{\nreturn 0;\n}\n};\n")
740                                     << QString()
741                                     << QStringLiteral("namespace NS{class C{};} class a {\nint foo(const NS::C c) const;\n};\n")
742                                     << QStringLiteral("\nint a::foo(const NS::C c) const {\nreturn 0;\n}\n")
743                                     << aFooId;
744     QTest::newRow("add-into-namespace") << QStringLiteral("namespace NS{class a {\nint foo() const {\nreturn 0;\n}\n};\n}")
745                                     << QStringLiteral("namespace NS{\n}")
746                                     << QStringLiteral("namespace NS{class a {\nint foo() const;\n};\n}")
747                                     << QStringLiteral("namespace NS{\n\nint a::foo() const {\nreturn 0;\n}\n}")
748                                     << QualifiedIdentifier(QStringLiteral("NS::a::foo"));
749     QTest::newRow("class-template-parameter")
750         << QStringLiteral(R"(
751             namespace first {
752             template <typename T>
753             class Test{};
754 
755             namespace second {
756                 template <typename T>
757                 class List;
758             }
759 
760             class MoveIntoSource
761             {
762             public:
763                 void f(const second::List<const volatile Test<first::second::List<int*>>*>& param){}
764             };}
765         )")
766         << QString()
767         << QStringLiteral(R"(
768             namespace first {
769             template <typename T>
770             class Test{};
771 
772             namespace second {
773                 template <typename T>
774                 class List;
775             }
776 
777             class MoveIntoSource
778             {
779             public:
780                 void f(const second::List<const volatile Test<first::second::List<int*>>*>& param);
781             };}
782         )")
783         << QStringLiteral("namespace first {\nvoid MoveIntoSource::f(const first::second::List< const volatile first::Test< first::second::List< int* > >* >& param) {}}\n\n")
784         << QualifiedIdentifier(QStringLiteral("first::MoveIntoSource::f"));
785 
786         QTest::newRow("move-unexposed-type")
787             << QStringLiteral("namespace std { template<typename _CharT> class basic_string; \ntypedef basic_string<char> string;}\n void move(std::string i){}")
788             << QString()
789             << QStringLiteral("namespace std { template<typename _CharT> class basic_string; \ntypedef basic_string<char> string;}\n void move(std::string i);")
790             << QStringLiteral("void move(std::string i) {}\n")
791             << QualifiedIdentifier(QStringLiteral("move"));
792         QTest::newRow("move-constructor")
793             << QStringLiteral("class Class{Class(){}\n};")
794             << QString()
795             << QStringLiteral("class Class{Class();\n};")
796             << QStringLiteral("Class::Class() {}\n")
797             << QualifiedIdentifier(QStringLiteral("Class::Class"));
798 }
799 
testHeaderGuardAssistant()800 void TestAssistants::testHeaderGuardAssistant()
801 {
802     CodeRepresentation::setDiskChangesForbidden(false);
803 
804     QFETCH(QString, filename);
805     QFETCH(QString, code);
806     QFETCH(QString, pragmaExpected);
807     QFETCH(QString, macroExpected);
808 
809     TestFile pragmaFile(code, QStringLiteral("h"));
810     TestFile macroFile(code, QStringLiteral("h"), filename);
811     TestFile impl("#include \"" + pragmaFile.url().str() + "\"\n"
812                   "#include \"" + macroFile.url().str() + "\"\n", QStringLiteral("cpp"));
813 
814     QExplicitlySharedDataPointer<IAssistant> pragmaAssistant;
815     QExplicitlySharedDataPointer<IAssistant> macroAssistant;
816 
817     QVERIFY(impl.parseAndWait(TopDUContext::Empty));
818 
819     DUChainReadLocker lock;
820     QVERIFY(impl.topContext());
821 
822     const auto pragmaTopContext = DUChain::self()->chainForDocument(pragmaFile.url());
823     const auto macroTopContext = DUChain::self()->chainForDocument(macroFile.url());
824     QVERIFY(pragmaTopContext);
825     QVERIFY(macroTopContext);
826 
827     const auto pragmaProblem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(pragmaTopContext));
828     const auto macroProblem = findStaticAssistantProblem(DUChainUtils::allProblemsForContext(macroTopContext));
829     QVERIFY(pragmaProblem && macroProblem);
830     pragmaAssistant = pragmaProblem->solutionAssistant();
831     macroAssistant = macroProblem->solutionAssistant();
832     QVERIFY(pragmaAssistant && macroAssistant);
833 
834     pragmaAssistant->actions()[0]->execute();
835     macroAssistant->actions()[1]->execute();
836 
837     QCOMPARE(pragmaFile.fileContents(), pragmaExpected);
838     QCOMPARE(macroFile.fileContents(), macroExpected);
839 
840     CodeRepresentation::setDiskChangesForbidden(true);
841 }
842 
testHeaderGuardAssistant_data()843 void TestAssistants::testHeaderGuardAssistant_data()
844 {
845     QTest::addColumn<QString>("filename");
846     QTest::addColumn<QString>("code");
847     QTest::addColumn<QString>("pragmaExpected");
848     QTest::addColumn<QString>("macroExpected");
849 
850     QTest::newRow("simple") << QStringLiteral("simpleheaderguard")
851         << QStringLiteral("int main()\n{\nreturn 0;\n}\n")
852         << QStringLiteral("#pragma once\n\nint main()\n{\nreturn 0;\n}\n")
853         << QStringLiteral(
854             "#ifndef SIMPLEHEADERGUARD_H_INCLUDED\n"
855             "#define SIMPLEHEADERGUARD_H_INCLUDED\n\n"
856             "int main()\n{\nreturn 0;\n}\n\n"
857             "#endif // SIMPLEHEADERGUARD_H_INCLUDED"
858         );
859 
860     QTest::newRow("licensed") << QStringLiteral("licensed-headerguard")
861         << QStringLiteral("/* Copyright 3019 John Doe\n */\n// Some comment\n"
862                           "int main()\n{\nreturn 0;\n}\n")
863         << QStringLiteral("/* Copyright 3019 John Doe\n */\n// Some comment\n"
864                           "#pragma once\n\n"
865                           "int main()\n{\nreturn 0;\n}\n")
866         << QStringLiteral(
867             "/* Copyright 3019 John Doe\n */\n// Some comment\n"
868             "#ifndef LICENSED_HEADERGUARD_H_INCLUDED\n"
869             "#define LICENSED_HEADERGUARD_H_INCLUDED\n\n"
870             "int main()\n{\nreturn 0;\n}\n\n"
871             "#endif // LICENSED_HEADERGUARD_H_INCLUDED"
872         );
873 
874     QTest::newRow("empty") << QStringLiteral("empty-file")
875         << QStringLiteral("")
876         << QStringLiteral("#pragma once\n\n")
877         << QStringLiteral("#ifndef EMPTY_FILE_H_INCLUDED\n"
878                           "#define EMPTY_FILE_H_INCLUDED\n\n\n"
879                           "#endif // EMPTY_FILE_H_INCLUDED"
880         );
881 
882     QTest::newRow("no-trailinig-newline") << QStringLiteral("no-endline-file")
883         << QStringLiteral("int foo;")
884         << QStringLiteral("#pragma once\n\nint foo;")
885         << QStringLiteral("#ifndef NO_ENDLINE_FILE_H_INCLUDED\n"
886                             "#define NO_ENDLINE_FILE_H_INCLUDED\n\n"
887                             "int foo;\n"
888                             "#endif // NO_ENDLINE_FILE_H_INCLUDED"
889         );
890 
891     QTest::newRow("whitespace-at-start") << QStringLiteral("whitespace-at-start")
892         << QStringLiteral("\nint foo;")
893         << QStringLiteral("#pragma once\n\n\nint foo;")
894         << QStringLiteral(
895             "#ifndef WHITESPACE_AT_START_H_INCLUDED\n"
896             "#define WHITESPACE_AT_START_H_INCLUDED\n\n"
897             "\nint foo;\n"
898             "#endif // WHITESPACE_AT_START_H_INCLUDED"
899         );
900 }
901