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