1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <private/qqmljsengine_p.h>
30#include <private/qqmljsparser_p.h>
31#include <private/qqmljslexer_p.h>
32#include <private/qqmljsastvisitor_p.h>
33#include <private/qqmljsast_p.h>
34
35#include "../../shared/util.h"
36#include "../../shared/qqmljsastdumper.h"
37
38#include <qtest.h>
39#include <QDir>
40#include <QDebug>
41#include <cstdlib>
42
43class tst_qqmlparser : public QQmlDataTest
44{
45 Q_OBJECT
46public:
47 tst_qqmlparser();
48
49private slots:
50 void initTestCase();
51#if !defined(QTEST_CROSS_COMPILED) // sources not available when cross compiled
52 void qmlParser_data();
53 void qmlParser();
54#endif
55 void invalidEscapeSequence();
56 void stringLiteral();
57 void noSubstitutionTemplateLiteral();
58 void templateLiteral();
59 void leadingSemicolonInClass();
60 void templatedReadonlyProperty();
61 void qmlImportInJSRequiresFullVersion();
62 void typeAnnotations_data();
63 void typeAnnotations();
64 void disallowedTypeAnnotations_data();
65 void disallowedTypeAnnotations();
66 void semicolonPartOfExpressionStatement();
67 void typeAssertion_data();
68 void typeAssertion();
69 void annotations_data();
70 void annotations();
71
72private:
73 QStringList excludedDirs;
74
75 QStringList findFiles(const QDir &);
76};
77
78namespace check {
79
80using namespace QQmlJS;
81
82class Check: public AST::Visitor
83{
84 QList<AST::Node *> nodeStack;
85
86public:
87 void operator()(AST::Node *node)
88 {
89 AST::Node::accept(node, visitor: this);
90 }
91
92 void checkNode(AST::Node *node)
93 {
94 if (! nodeStack.isEmpty()) {
95 AST::Node *parent = nodeStack.last();
96 const quint32 parentBegin = parent->firstSourceLocation().begin();
97 const quint32 parentEnd = parent->lastSourceLocation().end();
98
99 if (node->firstSourceLocation().begin() < parentBegin)
100 qDebug() << "first source loc failed: node:" << node->kind << "at" << node->firstSourceLocation().startLine << "/" << node->firstSourceLocation().startColumn
101 << "parent" << parent->kind << "at" << parent->firstSourceLocation().startLine << "/" << parent->firstSourceLocation().startColumn;
102 if (node->lastSourceLocation().end() > parentEnd)
103 qDebug() << "last source loc failed: node:" << node->kind << "at" << node->lastSourceLocation().startLine << "/" << node->lastSourceLocation().startColumn
104 << "parent" << parent->kind << "at" << parent->lastSourceLocation().startLine << "/" << parent->lastSourceLocation().startColumn;
105
106 QVERIFY(node->firstSourceLocation().begin() >= parentBegin);
107 QVERIFY(node->lastSourceLocation().end() <= parentEnd);
108 }
109 }
110
111 virtual bool preVisit(AST::Node *node)
112 {
113 checkNode(node);
114 nodeStack.append(t: node);
115 return true;
116 }
117
118 virtual void postVisit(AST::Node *)
119 {
120 nodeStack.removeLast();
121 }
122
123 void throwRecursionDepthError() final
124 {
125 QFAIL("Maximum statement or expression depth exceeded");
126 }
127};
128
129struct TypeAnnotationObserver: public AST::Visitor
130{
131 bool typeAnnotationSeen = false;
132
133 void operator()(AST::Node *node)
134 {
135 AST::Node::accept(node, visitor: this);
136 }
137
138 virtual bool visit(AST::TypeAnnotation *)
139 {
140 typeAnnotationSeen = true;
141 return true;
142 }
143
144 void throwRecursionDepthError() final
145 {
146 QFAIL("Maximum statement or expression depth exceeded");
147 }
148};
149
150struct ExpressionStatementObserver: public AST::Visitor
151{
152 int expressionsSeen = 0;
153 bool endsWithSemicolon = true;
154
155 void operator()(AST::Node *node)
156 {
157 AST::Node::accept(node, visitor: this);
158 }
159
160 virtual bool visit(AST::ExpressionStatement *statement)
161 {
162 ++expressionsSeen;
163 endsWithSemicolon = endsWithSemicolon
164 && (statement->lastSourceLocation().end() == statement->semicolonToken.end());
165 return true;
166 }
167
168 void throwRecursionDepthError() final
169 {
170 QFAIL("Maximum statement or expression depth exceeded");
171 }
172};
173
174}
175
176tst_qqmlparser::tst_qqmlparser()
177{
178}
179
180void tst_qqmlparser::initTestCase()
181{
182 QQmlDataTest::initTestCase();
183 // Add directories you want excluded here
184
185 // These snippets are not expected to run on their own.
186 excludedDirs << "doc/src/snippets/qml/visualdatamodel_rootindex";
187 excludedDirs << "doc/src/snippets/qml/qtbinding";
188 excludedDirs << "doc/src/snippets/qml/imports";
189 excludedDirs << "doc/src/snippets/qtquick1/visualdatamodel_rootindex";
190 excludedDirs << "doc/src/snippets/qtquick1/qtbinding";
191 excludedDirs << "doc/src/snippets/qtquick1/imports";
192}
193
194QStringList tst_qqmlparser::findFiles(const QDir &d)
195{
196 for (int ii = 0; ii < excludedDirs.count(); ++ii) {
197 QString s = excludedDirs.at(i: ii);
198 if (d.absolutePath().endsWith(s))
199 return QStringList();
200 }
201
202 QStringList rv;
203
204 QStringList files = d.entryList(nameFilters: QStringList() << QLatin1String("*.qml") << QLatin1String("*.js"),
205 filters: QDir::Files);
206 foreach (const QString &file, files) {
207 rv << d.absoluteFilePath(fileName: file);
208 }
209
210 QStringList dirs = d.entryList(filters: QDir::Dirs | QDir::NoDotAndDotDot |
211 QDir::NoSymLinks);
212 foreach (const QString &dir, dirs) {
213 QDir sub = d;
214 sub.cd(dirName: dir);
215 rv << findFiles(d: sub);
216 }
217
218 return rv;
219}
220
221/*
222This test checks all the qml and js files in the QtQml UI source tree
223and ensures that the subnode's source locations are inside parent node's source locations
224*/
225
226#if !defined(QTEST_CROSS_COMPILED) // sources not available when cross compiled
227void tst_qqmlparser::qmlParser_data()
228{
229 QTest::addColumn<QString>(name: "file");
230
231 QString examples = QLatin1String(SRCDIR) + "/../../../../examples/";
232 QString tests = QLatin1String(SRCDIR) + "/../../../../tests/";
233
234 QStringList files;
235 files << findFiles(d: QDir(examples));
236 files << findFiles(d: QDir(tests));
237
238 foreach (const QString &file, files)
239 QTest::newRow(qPrintable(file)) << file;
240}
241#endif
242
243#if !defined(QTEST_CROSS_COMPILED) // sources not available when cross compiled
244void tst_qqmlparser::qmlParser()
245{
246 QFETCH(QString, file);
247
248 using namespace QQmlJS;
249
250 QString code;
251
252 QFile f(file);
253 if (f.open(flags: QFile::ReadOnly))
254 code = QString::fromUtf8(str: f.readAll());
255
256 const bool qmlMode = file.endsWith(s: QLatin1String(".qml"));
257
258 Engine engine;
259 Lexer lexer(&engine);
260 lexer.setCode(code, lineno: 1, qmlMode);
261 Parser parser(&engine);
262 bool ok = qmlMode ? parser.parse() : parser.parseProgram();
263
264 if (ok) {
265 check::Check chk;
266 chk(parser.rootNode());
267 }
268}
269#endif
270
271void tst_qqmlparser::invalidEscapeSequence()
272{
273 using namespace QQmlJS;
274
275 Engine engine;
276 Lexer lexer(&engine);
277 lexer.setCode(code: QLatin1String("\"\\"), lineno: 1);
278 Parser parser(&engine);
279 parser.parse();
280}
281
282void tst_qqmlparser::stringLiteral()
283{
284 using namespace QQmlJS;
285
286 Engine engine;
287 Lexer lexer(&engine);
288 QLatin1String code("'hello string'");
289 lexer.setCode(code , lineno: 1);
290 Parser parser(&engine);
291 QVERIFY(parser.parseExpression());
292 AST::ExpressionNode *expression = parser.expression();
293 QVERIFY(expression);
294 auto *literal = QQmlJS::AST::cast<QQmlJS::AST::StringLiteral *>(ast: expression);
295 QVERIFY(literal);
296 QCOMPARE(literal->value, "hello string");
297 QCOMPARE(literal->firstSourceLocation().begin(), 0u);
298 QCOMPARE(literal->lastSourceLocation().end(), quint32(code.size()));
299}
300
301void tst_qqmlparser::noSubstitutionTemplateLiteral()
302{
303 using namespace QQmlJS;
304
305 Engine engine;
306 Lexer lexer(&engine);
307 QLatin1String code("`hello template`");
308 lexer.setCode(code, lineno: 1);
309 Parser parser(&engine);
310 QVERIFY(parser.parseExpression());
311 AST::ExpressionNode *expression = parser.expression();
312 QVERIFY(expression);
313
314 auto *literal = QQmlJS::AST::cast<QQmlJS::AST::TemplateLiteral *>(ast: expression);
315 QVERIFY(literal);
316
317 QCOMPARE(literal->value, "hello template");
318 QCOMPARE(literal->firstSourceLocation().begin(), 0u);
319 QCOMPARE(literal->lastSourceLocation().end(), quint32(code.size()));
320}
321
322void tst_qqmlparser::templateLiteral()
323{
324 using namespace QQmlJS;
325
326 Engine engine;
327 Lexer lexer(&engine);
328 QLatin1String code("`one plus one equals ${1+1}!`");
329 lexer.setCode(code, lineno: 1);
330 Parser parser(&engine);
331 QVERIFY(parser.parseExpression());
332 AST::ExpressionNode *expression = parser.expression();
333 QVERIFY(expression);
334
335 auto *templateLiteral = QQmlJS::AST::cast<QQmlJS::AST::TemplateLiteral *>(ast: expression);
336 QVERIFY(templateLiteral);
337
338 QCOMPARE(templateLiteral->firstSourceLocation().begin(), 0u);
339 auto *e = templateLiteral->expression;
340 QVERIFY(e);
341}
342
343void tst_qqmlparser::leadingSemicolonInClass()
344{
345 QQmlJS::Engine engine;
346 QQmlJS::Lexer lexer(&engine);
347 lexer.setCode(code: QLatin1String("class X{;n(){}}"), lineno: 1);
348 QQmlJS::Parser parser(&engine);
349 QVERIFY(parser.parseProgram());
350}
351
352void tst_qqmlparser::templatedReadonlyProperty()
353{
354 QQmlJS::Engine engine;
355 QQmlJS::Lexer lexer(&engine);
356 lexer.setCode(code: QLatin1String("A { readonly property list<B> listfoo: [ C{} ] }"), lineno: 1);
357 QQmlJS::Parser parser(&engine);
358 QVERIFY(parser.parse());
359}
360
361void tst_qqmlparser::qmlImportInJSRequiresFullVersion()
362{
363 {
364 QQmlJS::Engine engine;
365 QQmlJS::Lexer lexer(&engine);
366 lexer.setCode(code: QLatin1String(".import Test 1.0 as T"), lineno: 0, qmlMode: false);
367 QQmlJS::Parser parser(&engine);
368 bool b = parser.parseProgram();
369 qDebug() << parser.errorMessage();
370 QVERIFY(b);
371 }
372 {
373 QQmlJS::Engine engine;
374 QQmlJS::Lexer lexer(&engine);
375 lexer.setCode(code: QLatin1String(".import Test 1 as T"), lineno: 0, qmlMode: false);
376 QQmlJS::Parser parser(&engine);
377 QVERIFY(!parser.parseProgram());
378 }
379 {
380 QQmlJS::Engine engine;
381 QQmlJS::Lexer lexer(&engine);
382 lexer.setCode(code: QLatin1String(".import Test 1 as T"), lineno: 0, qmlMode: false);
383 QQmlJS::Parser parser(&engine);
384 QVERIFY(!parser.parseProgram());
385 }
386 {
387 QQmlJS::Engine engine;
388 QQmlJS::Lexer lexer(&engine);
389 lexer.setCode(code: QLatin1String(".import Test as T"), lineno: 0, qmlMode: false);
390 QQmlJS::Parser parser(&engine);
391 QVERIFY(!parser.parseProgram());
392 }
393}
394
395void tst_qqmlparser::typeAnnotations_data()
396{
397 QTest::addColumn<QString>(name: "file");
398
399 QString tests = dataDirectory() + "/typeannotations/";
400
401 QStringList files;
402 files << findFiles(d: QDir(tests));
403
404 for (const QString &file: qAsConst(t&: files))
405 QTest::newRow(qPrintable(file)) << file;
406}
407
408void tst_qqmlparser::typeAnnotations()
409{
410 using namespace QQmlJS;
411
412 QFETCH(QString, file);
413
414 QString code;
415
416 QFile f(file);
417 if (f.open(flags: QFile::ReadOnly))
418 code = QString::fromUtf8(str: f.readAll());
419
420 const bool qmlMode = file.endsWith(s: QLatin1String(".qml"));
421
422 Engine engine;
423 Lexer lexer(&engine);
424 lexer.setCode(code, lineno: 1, qmlMode);
425 Parser parser(&engine);
426 bool ok = qmlMode ? parser.parse() : parser.parseProgram();
427 QVERIFY(ok);
428
429 check::TypeAnnotationObserver observer;
430 observer(parser.rootNode());
431
432 QVERIFY(observer.typeAnnotationSeen);
433}
434
435void tst_qqmlparser::disallowedTypeAnnotations_data()
436{
437 QTest::addColumn<QString>(name: "file");
438
439 QString tests = dataDirectory() + "/disallowedtypeannotations/";
440
441 QStringList files;
442 files << findFiles(d: QDir(tests));
443
444 for (const QString &file: qAsConst(t&: files))
445 QTest::newRow(qPrintable(file)) << file;
446}
447
448void tst_qqmlparser::disallowedTypeAnnotations()
449{
450 using namespace QQmlJS;
451
452 QFETCH(QString, file);
453
454 QString code;
455
456 QFile f(file);
457 if (f.open(flags: QFile::ReadOnly))
458 code = QString::fromUtf8(str: f.readAll());
459
460 const bool qmlMode = file.endsWith(s: QLatin1String(".qml"));
461
462 Engine engine;
463 Lexer lexer(&engine);
464 lexer.setCode(code, lineno: 1, qmlMode);
465 Parser parser(&engine);
466 bool ok = qmlMode ? parser.parse() : parser.parseProgram();
467 QVERIFY(!ok);
468 QVERIFY2(parser.errorMessage().startsWith("Type annotations are not permitted "), qPrintable(parser.errorMessage()));
469}
470
471void tst_qqmlparser::semicolonPartOfExpressionStatement()
472{
473 QQmlJS::Engine engine;
474 QQmlJS::Lexer lexer(&engine);
475 lexer.setCode(code: QLatin1String("A { property int x: 1+1; property int y: 2+2 \n"
476 "tt: {'a': 5, 'b': 6}; ff: {'c': 'rrr'}}"), lineno: 1);
477 QQmlJS::Parser parser(&engine);
478 QVERIFY(parser.parse());
479
480 check::ExpressionStatementObserver observer;
481 observer(parser.rootNode());
482
483 QCOMPARE(observer.expressionsSeen, 4);
484 QVERIFY(observer.endsWithSemicolon);
485}
486
487void tst_qqmlparser::typeAssertion_data()
488{
489 QTest::addColumn<QString>(name: "expression");
490 QTest::addRow(format: "as A")
491 << QString::fromLatin1(str: "A { onStuff: (b as A).happen() }");
492 QTest::addRow(format: "as double paren")
493 << QString::fromLatin1(str: "A { onStuff: console.log((12 as double)); }");
494 QTest::addRow(format: "as double noparen")
495 << QString::fromLatin1(str: "A { onStuff: console.log(12 as double); }");
496 QTest::addRow(format: "property as double")
497 << QString::fromLatin1(str: "A { prop: (12 as double); }");
498 QTest::addRow(format: "property noparen as double")
499 << QString::fromLatin1(str: "A { prop: 12 as double; }");
500
501 // rabbits cannot be discerned from types on a syntactical level.
502 // We could detect this on a semantical level, once we implement type assertions there.
503
504 QTest::addRow(format: "as rabbit")
505 << QString::fromLatin1(str: "A { onStuff: (b as rabbit).happen() }");
506 QTest::addRow(format: "as rabbit paren")
507 << QString::fromLatin1(str: "A { onStuff: console.log((12 as rabbit)); }");
508 QTest::addRow(format: "as rabbit noparen")
509 << QString::fromLatin1(str: "A { onStuff: console.log(12 as rabbit); }");
510 QTest::addRow(format: "property as rabbit")
511 << QString::fromLatin1(str: "A { prop: (12 as rabbit); }");
512 QTest::addRow(format: "property noparen as rabbit")
513 << QString::fromLatin1(str: "A { prop: 12 as rabbit; }");
514}
515
516void tst_qqmlparser::typeAssertion()
517{
518 QFETCH(QString, expression);
519
520 QQmlJS::Engine engine;
521 QQmlJS::Lexer lexer(&engine);
522 lexer.setCode(code: expression, lineno: 1);
523 QQmlJS::Parser parser(&engine);
524 QVERIFY(parser.parse());
525}
526
527void tst_qqmlparser::annotations_data()
528{
529 QTest::addColumn<QString>(name: "file");
530 QTest::addColumn<QString>(name: "refFile");
531
532 QString tests = dataDirectory() + "/annotations/";
533 QString compare = dataDirectory() + "/noannotations/";
534
535 QStringList files;
536 files << findFiles(d: QDir(tests));
537
538 QStringList refFiles;
539 refFiles << findFiles(d: QDir(compare));
540
541 for (const QString &file: qAsConst(t&: files)) {
542 auto fileNameStart = file.lastIndexOf(c: QDir::separator());
543 QStringRef fileName(&file, fileNameStart, file.length()-fileNameStart);
544 auto ref=std::find_if(first: refFiles.constBegin(),last: refFiles.constEnd(), pred: [fileName](const QString &s){ return s.endsWith(s: fileName); });
545 if (ref != refFiles.constEnd())
546 QTest::newRow(qPrintable(file)) << file << *ref;
547 else
548 QTest::newRow(qPrintable(file)) << file << QString();
549 }
550}
551
552void tst_qqmlparser::annotations()
553{
554 using namespace QQmlJS;
555
556 QFETCH(QString, file);
557 QFETCH(QString, refFile);
558
559 QString code;
560 QString refCode;
561
562 QFile f(file);
563 if (f.open(flags: QFile::ReadOnly))
564 code = QString::fromUtf8(str: f.readAll());
565 QFile refF(refFile);
566 if (!refFile.isEmpty() && refF.open(flags: QFile::ReadOnly))
567 refCode = QString::fromUtf8(str: refF.readAll());
568
569 const bool qmlMode = true;
570
571 Engine engine;
572 Lexer lexer(&engine);
573 lexer.setCode(code, lineno: 1, qmlMode);
574 Parser parser(&engine);
575 QVERIFY(parser.parse());
576
577 if (!refCode.isEmpty()) {
578 Engine engine2;
579 Lexer lexer2(&engine2);
580 lexer2.setCode(code: refCode, lineno: 1, qmlMode);
581 Parser parser2(&engine2);
582 QVERIFY(parser2.parse());
583
584 QCOMPARE(AstDumper::diff(parser.ast(), parser2.rootNode(), 3, DumperOptions::NoAnnotations | DumperOptions::NoLocations), QString());
585 }
586}
587
588QTEST_MAIN(tst_qqmlparser)
589
590#include "tst_qqmlparser.moc"
591

source code of qtdeclarative/tests/auto/qml/qqmlparser/tst_qqmlparser.cpp