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