1 /*
2     SPDX-FileCopyrightText: 2012 Sven Brauch <svenbrauch@googlemail.com>
3     SPDX-FileCopyrightText: 2014 Denis Steckelmacher <steckdenis@yahoo.fr>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "test_qmljscompletion.h"
9 
10 #include <language/duchain/declaration.h>
11 #include <language/duchain/duchain.h>
12 #include <language/codegen/coderepresentation.h>
13 #include <language/codecompletion/codecompletiontesthelper.h>
14 #include <language/codecompletion/codecompletioncontext.h>
15 #include <language/backgroundparser/backgroundparser.h>
16 
17 #include <interfaces/ilanguagecontroller.h>
18 
19 #include <tests/testcore.h>
20 #include <tests/autotestshell.h>
21 #include <tests/testfile.h>
22 
23 #include <QTest>
24 
25 #include "../context.h"
26 #include "../model.h"
27 
28 using namespace KDevelop;
29 using namespace QmlJS;
30 
31 QTEST_MAIN(QmlJS::QmlCompletionTest)
32 
33 using CompletionContextPtr = QSharedPointer<QmlJS::CodeCompletionContext>;
34 
35 namespace {
36 
37 struct CompletionParameters
38 {
39     QSharedPointer<TestFile> file;
40     DUContextPointer contextAtCursor;
41     QString snip;
42     QString remaining;
43     CursorInRevision cursorAt;
44 
45     CompletionContextPtr completionContext;
46     QList<CompletionTreeItemPointer> completionItems;
47 };
48 
fakeModel()49 QStandardItemModel& fakeModel() {
50   static QStandardItemModel model;
51   model.setColumnCount(10);
52   model.setRowCount(10);
53   return model;
54 }
55 
56 
runCompletion(CompletionParameters * parameters)57 void runCompletion(CompletionParameters* parameters)
58 {
59     parameters->completionContext = CompletionContextPtr(new QmlJS::CodeCompletionContext(parameters->contextAtCursor,
60                                                                parameters->snip,
61                                                                parameters->cursorAt));
62     bool abort = false;
63 
64     parameters->completionItems = parameters->completionContext->completionItems(abort, true);
65 }
66 
prepareCompletion(const QString & initCode,const QString & invokeCode,bool qml)67 CompletionParameters prepareCompletion(const QString& initCode, const QString& invokeCode, bool qml)
68 {
69     CompletionParameters completion_data;
70 
71     // Simulate that the user has entered invokeCode where %INVOKE is, put
72     // the cursor where %CURSOR is, and then asked for completions
73     Q_ASSERT(initCode.indexOf("%INVOKE") != -1);
74 
75     // Create a file containing the given code, with "%INVOKE" removed
76     completion_data.file = QSharedPointer<TestFile>(new TestFile(QString(initCode).remove(QLatin1String("%INVOKE")),
77                                                     qml ? "qml" : "js"));
78 
79     completion_data.file->parse();
80     completion_data.file->waitForParsed();
81     // wait for this fail and all dependencies, like modules and such
82     while (!ICore::self()->languageController()->backgroundParser()->isIdle()) {
83         QTest::qWait(100);
84     }
85 
86     if (!completion_data.file->topContext()) {
87       qWarning() << "file contents are: " << completion_data.file->fileContents();
88       Q_ASSERT_X(false, Q_FUNC_INFO, "Failed to parse initCode.");
89     }
90 
91     QString allCode = QString(initCode).replace(QLatin1String("%INVOKE"), invokeCode);
92 
93     QStringList lines = allCode.split('\n');
94     completion_data.cursorAt = CursorInRevision::invalid();
95     for ( int i = 0; i < lines.length(); i++ ) {
96         int j = lines.at(i).indexOf(QLatin1String("%CURSOR"));
97         if ( j != -1 ) {
98             completion_data.cursorAt = CursorInRevision(i, j);
99             break;
100         }
101     }
102     Q_ASSERT(completion_data.cursorAt.isValid());
103 
104     // codeCompletionContext only gets passed the text until the place where completion is invoked
105     completion_data.snip = allCode.mid(0, allCode.indexOf(QLatin1String("%CURSOR")));
106     completion_data.remaining = allCode.mid(allCode.indexOf(QLatin1String("%CURSOR")) + 7);
107 
108     DUChainReadLocker lock;
109     completion_data.contextAtCursor = DUContextPointer(completion_data.file->topContext()->findContextAt(completion_data.cursorAt, true));
110     Q_ASSERT(completion_data.contextAtCursor);
111 
112     runCompletion(&completion_data);
113 
114     return completion_data;
115 }
116 
containsItemForDeclarationNamed(const CompletionParameters & params,const QString & itemName)117 bool containsItemForDeclarationNamed(const CompletionParameters& params, const QString& itemName)
118 {
119     DUChainReadLocker lock;
120 
121     foreach (const CompletionTreeItemPointer& ptr, params.completionItems) {
122         if (ptr->declaration()) {
123             if (ptr->declaration()->identifier().toString() == itemName) {
124                 return true;
125             }
126         }
127     }
128 
129     return false;
130 }
131 
containsItemForText(const CompletionParameters & params,const QString & item)132 bool containsItemForText(const CompletionParameters& params, const QString& item)
133 {
134     QModelIndex idx = fakeModel().index(0, KDevelop::CodeCompletionModel::Name);
135 
136     for (auto ptr : params.completionItems) {
137         if (ptr->data(idx, Qt::DisplayRole, nullptr).toString() == item) {
138             return true;
139         }
140     }
141 
142     return false;
143 }
144 
declarationInCompletionList(const QString & initCode,const QString & invokeCode,const QString & itemName,bool qml)145 bool declarationInCompletionList(const QString& initCode, const QString& invokeCode, const QString& itemName, bool qml)
146 {
147     return containsItemForDeclarationNamed(
148         prepareCompletion(initCode, invokeCode, qml),
149         itemName
150     );
151 }
152 
itemInCompletionList(const QString & initCode,const QString & invokeCode,const QString & itemName,bool qml)153 bool itemInCompletionList(const QString& initCode, const QString& invokeCode, const QString& itemName, bool qml)
154 {
155     return containsItemForText(
156         prepareCompletion(initCode, invokeCode, qml),
157         itemName
158     );
159 }
160 
161 }
162 
163 namespace QmlJS {
164 
initTestCase()165 void QmlCompletionTest::initTestCase()
166 {
167     AutoTestShell::init({"kdevqmljs"});
168     TestCore::initialize(Core::NoUi);
169     DUChain::self()->disablePersistentStorage();
170     CodeRepresentation::setDiskChangesForbidden(true);
171 }
172 
cleanupTestCase()173 void QmlCompletionTest::cleanupTestCase()
174 {
175   TestCore::shutdown();
176 }
177 
testContainsDeclaration()178 void QmlCompletionTest::testContainsDeclaration()
179 {
180     QFETCH(QString, invokeCode);
181     QFETCH(QString, completionCode);
182     QFETCH(QString, expectedItem);
183     QFETCH(bool, qml);
184 
185     QVERIFY(declarationInCompletionList(invokeCode, completionCode, expectedItem, qml));
186 }
187 
testContainsDeclaration_data()188 void QmlCompletionTest::testContainsDeclaration_data()
189 {
190     QTest::addColumn<QString>("invokeCode");
191     QTest::addColumn<QString>("completionCode");
192     QTest::addColumn<QString>("expectedItem");
193     QTest::addColumn<bool>("qml");
194 
195     // Comments and strings
196     QTest::newRow("js_outside_single_line_comment") << "var a // this is a comment;\n%INVOKE" << "%CURSOR" << "a" << false;
197     QTest::newRow("js_outside_multi_line_comment") << "var a;\n%INVOKE" << "/* comment */ %CURSOR" << "a" << false;
198     QTest::newRow("js_outside_string") << "var a;\n%INVOKE" << "var b = 'hello' + %CURSOR" << "a" << false;
199 
200     // Basic JS tests
201     QTest::newRow("js_basic_variable") << "var a;\n %INVOKE" << "%CURSOR" << "a" << false;
202     QTest::newRow("js_basic_function") << "function f();\n %INVOKE" << "%CURSOR" << "f" << false;
203 
204     // Object members
205     QTest::newRow("js_object_members") << "var a = {b: 0};\n %INVOKE" << "a.%CURSOR" << "b" << false;
206     QTest::newRow("js_array_subscript") << "var a = {b: 0};\n %INVOKE" << "a[%CURSOR" << "b" << false;
207     QTest::newRow("js_skip_separators") << "var a = {b: 0};\n %INVOKE" << "foo(false, a.%CURSOR" << "b" << false;
208 
209     // Javascript classes
210     QTest::newRow("js_object") << "var o = {};\n %INVOKE" << "o.%CURSOR" << "toString" << false;
211     QTest::newRow("js_builtin_object") << "var a;\n %INVOKE" << "a.%CURSOR" << "toString" << false;
212     QTest::newRow("js_builtin_string") << "var a = '';\n %INVOKE" << "a.%CURSOR" << "split" << false;
213     QTest::newRow("js_builtin_number") << "var a = 2;\n %INVOKE" << "a.%CURSOR" << "toFixed" << false;
214     QTest::newRow("js_builtin_boolean") << "var a = true;\n %INVOKE" << "a.%CURSOR" << "valueOf" << false;
215     QTest::newRow("js_builtin_array") << "var a = [];\n %INVOKE" << "a.%CURSOR" << "slice" << false;
216     QTest::newRow("js_builtin_function") << "var a = function(){};\n %INVOKE" << "a.%CURSOR" << "apply" << false;
217     QTest::newRow("js_builtin_regexp") << "var a = /.*/;\n %INVOKE" << "a.%CURSOR" << "exec" << false;
218     QTest::newRow("js_dom_document") << "%INVOKE" << "%CURSOR" << "document" << false;
219 
220     // Basic QML tests
221     QTest::newRow("qml_basic_property") << "Item { id: foo\n property int prop\n %INVOKE }" << "%CURSOR" << "prop" << true;
222     QTest::newRow("qml_basic_instance") << "Item { id: foo\n %INVOKE }" << "onTest: %CURSOR" << "foo" << true;
223     QTest::newRow("qml_skip_separators") << "Item { id: foo\n Item { id: bar\n property int prop }\n %INVOKE" << "onTest: bar.%CURSOR" << "prop" << true;
224 
225     // QML inheritance
226     QTest::newRow("qml_inheritance") <<
227         "Module {\n"
228         " Component {\n"
229         "  name: \"TestComponent\"\n"
230         "  Property {\n"
231         "   name: \"prop\"\n"
232         "   type: \"int\"\n"
233         "  }\n"
234         " }\n"
235         " TestComponent {\n"
236         "  id: foo\n"
237         "  %INVOKE\n"
238         " }\n"
239         "}" << "%CURSOR" << "prop" << true;
240 
241     // QML parent
242     QTest::newRow("qml_parent") <<
243         "Item {\n"
244         " id: a\n"
245         " property var prop\n"
246         " Item {\n"
247         "  id: b\n"
248         "  %INVOKE\n"
249         " }\n"
250         "}\n" << "onTest: parent.%CURSOR" << "prop" << true;
251 
252     // This declaration must be in QtQuick 2.2 but not 2.0 (tested in testDoesNotContainDeclaration)
253     QTest::newRow("qml_module_version_2.2") << "import QtQuick 2.2\n Item { id: a\n %INVOKE }" << "%CURSOR" << "OpacityAnimator" << true;
254     QTest::newRow("qml_module_alias") << "import QtQuick 2.2 as Foo\n Item { id: a\n %INVOKE }" << "Foo.%CURSOR" << "OpacityAnimator" << true;
255 
256     // Built-in QML types
257     QTest::newRow("qml_builtin_types") << "import QtQuick 2.0\n"
258                                           "\n"
259                                           "Text {\n"
260                                           " id: foo\n"
261                                           " %INVOKE\n"
262                                           "}\n"
263                                        << "font.%CURSOR"
264                                        << "family" << true;
265 }
266 
testDoesNotContainDeclaration()267 void QmlCompletionTest::testDoesNotContainDeclaration()
268 {
269     QFETCH(QString, invokeCode);
270     QFETCH(QString, completionCode);
271     QFETCH(QString, item);
272     QFETCH(bool, qml);
273 
274     QVERIFY(!declarationInCompletionList(invokeCode, completionCode, item, qml));
275 }
276 
testDoesNotContainDeclaration_data()277 void QmlCompletionTest::testDoesNotContainDeclaration_data()
278 {
279     QTest::addColumn<QString>("invokeCode");
280     QTest::addColumn<QString>("completionCode");
281     QTest::addColumn<QString>("item");
282     QTest::addColumn<bool>("qml");
283 
284     // Comments and strings
285     QTest::newRow("js_in_single_line_comment") << "var a;\n%INVOKE" << "// %CURSOR" << "a" << false;
286     QTest::newRow("js_in_multi_line_comment") << "var a;\n%INVOKE" << "/* %CURSOR" << "a" << false;
287     QTest::newRow("js_in_string") << "var a;\n%INVOKE" << "var b = 'hello \\'%CURSOR" << "a" << false;
288     QTest::newRow("js_useless_completions") << "var a;\n%INVOKE" << "var %CURSOR" << "a" << false;
289     QTest::newRow("js_useless_completions") << "var a;\n%INVOKE" << "function %CURSOR" << "a" << false;
290     QTest::newRow("js_useless_completions") << "var a;\n%INVOKE" << "function f(%CURSOR" << "a" << false;
291     QTest::newRow("js_useless_completions") << "var a;\n%INVOKE" << "var o = {id: %CURSOR" << "a" << false;
292 
293     // Don't show unreachable declarations when providing code-completions for object members
294     QTest::newRow("js_object_member_not_surrounding") << "var a; var b = {c: 0};%INVOKE" << "b.%CURSOR" << "a" << false;
295     QTest::newRow("js_object_member_local") << "var a = {b: 0};%INVOKE" << "%CURSOR" << "b" << false;
296 
297     // When providing completions for script bindings, don't propose script bindings
298     // for properties/signals of the surrounding components
299     QTest::newRow("qml_script_binding_not_surrounding") << "Item { property int foo; Item { %INVOKE } }" << "%CURSOR" << "foo" << false;
300 
301     // Don't list the declarations that are not in a namespace but are imported from it
302     QTest::newRow("qml_namespace_js_builtins") << "import org.kde.kdevplatform 1.0 as KDev\n Item { id: a\n %INVOKE }" << "KDev.%CURSOR" << "String" << true;
303 }
304 
testContainsText()305 void QmlCompletionTest::testContainsText()
306 {
307     QFETCH(QString, invokeCode);
308     QFETCH(QString, completionCode);
309     QFETCH(QString, item);
310     QFETCH(bool, qml);
311 
312     QVERIFY(itemInCompletionList(invokeCode, completionCode, item, qml));
313 }
314 
testContainsText_data()315 void QmlCompletionTest::testContainsText_data()
316 {
317     QTest::addColumn<QString>("invokeCode");
318     QTest::addColumn<QString>("completionCode");
319     QTest::addColumn<QString>("item");
320     QTest::addColumn<bool>("qml");
321 
322     QTest::newRow("qml_import") << "%INVOKE" << "import %CURSOR" << "QtQuick" << true;
323     QTest::newRow("qml_import_prefix") << "%INVOKE\nItem {}" << "import QtQuick.%CURSOR" << "QtQuick.Controls" << true;
324 
325     QTest::newRow("js_node_require") << "%INVOKE" << "var m = require(%CURSOR" << "tls" << false;
326 }
327 
328 
329 }
330