1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Contact: http://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL3$ |
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 http://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at http://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPLv3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or later as published by the Free |
28 | ** Software Foundation and appearing in the file LICENSE.GPL included in |
29 | ** the packaging of this file. Please review the following information to |
30 | ** ensure the GNU General Public License version 2.0 requirements will be |
31 | ** met: http://www.gnu.org/licenses/gpl-2.0.html. |
32 | ** |
33 | ** $QT_END_LICENSE$ |
34 | ** |
35 | ****************************************************************************/ |
36 | |
37 | #include <QtTest> |
38 | #include <QtQml> |
39 | #include <QtCore/private/qhooks_p.h> |
40 | #include <QtQml/private/qqmljsengine_p.h> |
41 | #include <QtQml/private/qqmljslexer_p.h> |
42 | #include <QtQml/private/qqmljsparser_p.h> |
43 | #include <QtQml/private/qqmljsast_p.h> |
44 | #include <QtQml/private/qqmljsastvisitor_p.h> |
45 | #include <QtQml/private/qqmlmetatype_p.h> |
46 | #include "../../auto/shared/visualtestutil.h" |
47 | |
48 | using namespace QQuickVisualTestUtil; |
49 | |
50 | Q_GLOBAL_STATIC(QObjectList, qt_qobjects) |
51 | |
52 | extern "C" Q_DECL_EXPORT void qt_addQObject(QObject *object) |
53 | { |
54 | qt_qobjects->append(t: object); |
55 | } |
56 | |
57 | extern "C" Q_DECL_EXPORT void qt_removeQObject(QObject *object) |
58 | { |
59 | qt_qobjects->removeAll(t: object); |
60 | } |
61 | |
62 | class tst_Sanity : public QObject |
63 | { |
64 | Q_OBJECT |
65 | |
66 | private slots: |
67 | void init(); |
68 | void cleanup(); |
69 | void initTestCase(); |
70 | |
71 | void jsFiles(); |
72 | void functions(); |
73 | void functions_data(); |
74 | void signalHandlers(); |
75 | void signalHandlers_data(); |
76 | void anchors(); |
77 | void anchors_data(); |
78 | void attachedObjects(); |
79 | void attachedObjects_data(); |
80 | void ids(); |
81 | void ids_data(); |
82 | |
83 | private: |
84 | QQmlEngine engine; |
85 | QMap<QString, QString> files; |
86 | }; |
87 | |
88 | void tst_Sanity::init() |
89 | { |
90 | qtHookData[QHooks::AddQObject] = reinterpret_cast<quintptr>(&qt_addQObject); |
91 | qtHookData[QHooks::RemoveQObject] = reinterpret_cast<quintptr>(&qt_removeQObject); |
92 | } |
93 | |
94 | void tst_Sanity::cleanup() |
95 | { |
96 | qt_qobjects->clear(); |
97 | qtHookData[QHooks::AddQObject] = 0; |
98 | qtHookData[QHooks::RemoveQObject] = 0; |
99 | } |
100 | |
101 | class BaseValidator : public QQmlJS::AST::Visitor |
102 | { |
103 | public: |
104 | QString errors() const { return m_errors.join(sep: ", " ); } |
105 | |
106 | bool validate(const QString& filePath) |
107 | { |
108 | m_errors.clear(); |
109 | m_fileName = QFileInfo(filePath).fileName(); |
110 | |
111 | QFile file(filePath); |
112 | if (!file.open(flags: QFile::ReadOnly)) { |
113 | m_errors += QString("%1: failed to open (%2)" ).arg(args&: m_fileName, args: file.errorString()); |
114 | return false; |
115 | } |
116 | |
117 | QQmlJS::Engine engine; |
118 | QQmlJS::Lexer lexer(&engine); |
119 | lexer.setCode(code: QString::fromUtf8(str: file.readAll()), /*line = */ lineno: 1); |
120 | |
121 | QQmlJS::Parser parser(&engine); |
122 | if (!parser.parse()) { |
123 | const auto diagnosticMessages = parser.diagnosticMessages(); |
124 | for (const QQmlJS::DiagnosticMessage &msg : diagnosticMessages) |
125 | #if Q_QML_PRIVATE_API_VERSION >= 8 |
126 | m_errors += QString("%s:%d : %s" ).arg(m_fileName).arg(msg.loc.startLine).arg(msg.message); |
127 | #else |
128 | m_errors += QString("%s:%d : %s" ).arg(m_fileName).arg(msg.line).arg(msg.message); |
129 | #endif |
130 | return false; |
131 | } |
132 | |
133 | QQmlJS::AST::UiProgram* ast = parser.ast(); |
134 | ast->accept(visitor: this); |
135 | return m_errors.isEmpty(); |
136 | } |
137 | |
138 | protected: |
139 | void addError(const QString& error, QQmlJS::AST::Node *node) |
140 | { |
141 | m_errors += QString("%1:%2 : %3" ).arg(a: m_fileName).arg(a: node->firstSourceLocation().startLine).arg(a: error); |
142 | } |
143 | |
144 | void throwRecursionDepthError() final |
145 | { |
146 | m_errors += QString::fromLatin1(str: "%1: Maximum statement or expression depth exceeded" ) |
147 | .arg(a: m_fileName); |
148 | } |
149 | |
150 | private: |
151 | QString m_fileName; |
152 | QStringList m_errors; |
153 | }; |
154 | |
155 | void tst_Sanity::initTestCase() |
156 | { |
157 | QQmlEngine engine; |
158 | QQmlComponent component(&engine); |
159 | component.setData(QString("import QtQuick.Templates 2.%1; Control { }" ).arg(QT_VERSION_MINOR - 7).toUtf8(), baseUrl: QUrl()); |
160 | |
161 | const QStringList qmlTypeNames = QQmlMetaType::qmlTypeNames(); |
162 | |
163 | QDirIterator it(QQC2_IMPORT_PATH, QStringList() << "*.qml" << "*.js" , QDir::Files, QDirIterator::Subdirectories); |
164 | while (it.hasNext()) { |
165 | it.next(); |
166 | QFileInfo info = it.fileInfo(); |
167 | if (qmlTypeNames.contains(QStringLiteral("QtQuick.Templates/" ) + info.baseName())) |
168 | files.insert(key: info.dir().dirName() + "/" + info.fileName(), value: info.filePath()); |
169 | } |
170 | } |
171 | |
172 | void tst_Sanity::jsFiles() |
173 | { |
174 | QMap<QString, QString>::const_iterator it; |
175 | for (it = files.constBegin(); it != files.constEnd(); ++it) { |
176 | if (QFileInfo(it.value()).suffix() == QStringLiteral("js" )) |
177 | QFAIL(qPrintable(it.value() + ": JS files are not allowed" )); |
178 | } |
179 | } |
180 | |
181 | class FunctionValidator : public BaseValidator |
182 | { |
183 | protected: |
184 | virtual bool visit(QQmlJS::AST::FunctionDeclaration *node) |
185 | { |
186 | addError(error: "function declarations are not allowed" , node); |
187 | return true; |
188 | } |
189 | }; |
190 | |
191 | void tst_Sanity::functions() |
192 | { |
193 | QFETCH(QString, control); |
194 | QFETCH(QString, filePath); |
195 | |
196 | FunctionValidator validator; |
197 | if (!validator.validate(filePath)) |
198 | QFAIL(qPrintable(validator.errors())); |
199 | } |
200 | |
201 | void tst_Sanity::functions_data() |
202 | { |
203 | QTest::addColumn<QString>(name: "control" ); |
204 | QTest::addColumn<QString>(name: "filePath" ); |
205 | |
206 | QMap<QString, QString>::const_iterator it; |
207 | for (it = files.constBegin(); it != files.constEnd(); ++it) |
208 | QTest::newRow(qPrintable(it.key())) << it.key() << it.value(); |
209 | } |
210 | |
211 | class SignalHandlerValidator : public BaseValidator |
212 | { |
213 | protected: |
214 | static bool isSignalHandler(const QStringRef &name) |
215 | { |
216 | return name.length() > 2 && name.startsWith(s: "on" ) && name.at(i: 2).isUpper(); |
217 | } |
218 | |
219 | virtual bool visit(QQmlJS::AST::UiScriptBinding *node) |
220 | { |
221 | QQmlJS::AST::UiQualifiedId* id = node->qualifiedId; |
222 | if ((id && isSignalHandler(name: id->name)) || (id && id->next && isSignalHandler(name: id->next->name))) |
223 | addError(error: "signal handlers are not allowed" , node); |
224 | return true; |
225 | } |
226 | }; |
227 | |
228 | void tst_Sanity::signalHandlers() |
229 | { |
230 | QFETCH(QString, control); |
231 | QFETCH(QString, filePath); |
232 | |
233 | SignalHandlerValidator validator; |
234 | if (!validator.validate(filePath)) |
235 | QFAIL(qPrintable(validator.errors())); |
236 | } |
237 | |
238 | void tst_Sanity::signalHandlers_data() |
239 | { |
240 | QTest::addColumn<QString>(name: "control" ); |
241 | QTest::addColumn<QString>(name: "filePath" ); |
242 | |
243 | QMap<QString, QString>::const_iterator it; |
244 | for (it = files.constBegin(); it != files.constEnd(); ++it) |
245 | QTest::newRow(qPrintable(it.key())) << it.key() << it.value(); |
246 | } |
247 | |
248 | class AnchorValidator : public BaseValidator |
249 | { |
250 | protected: |
251 | virtual bool visit(QQmlJS::AST::UiScriptBinding *node) |
252 | { |
253 | QQmlJS::AST::UiQualifiedId* id = node->qualifiedId; |
254 | if (id && id->name == QStringLiteral("anchors" )) |
255 | addError(error: "anchors are not allowed" , node); |
256 | return true; |
257 | } |
258 | }; |
259 | |
260 | void tst_Sanity::anchors() |
261 | { |
262 | QFETCH(QString, control); |
263 | QFETCH(QString, filePath); |
264 | |
265 | AnchorValidator validator; |
266 | if (!validator.validate(filePath)) |
267 | QFAIL(qPrintable(validator.errors())); |
268 | } |
269 | |
270 | void tst_Sanity::anchors_data() |
271 | { |
272 | QTest::addColumn<QString>(name: "control" ); |
273 | QTest::addColumn<QString>(name: "filePath" ); |
274 | |
275 | QMap<QString, QString>::const_iterator it; |
276 | for (it = files.constBegin(); it != files.constEnd(); ++it) |
277 | QTest::newRow(qPrintable(it.key())) << it.key() << it.value(); |
278 | } |
279 | |
280 | class IdValidator : public BaseValidator |
281 | { |
282 | public: |
283 | IdValidator() : m_depth(0) { } |
284 | |
285 | protected: |
286 | bool visit(QQmlJS::AST::UiObjectBinding *) override |
287 | { |
288 | ++m_depth; |
289 | return true; |
290 | } |
291 | |
292 | void endVisit(QQmlJS::AST::UiObjectBinding *) override |
293 | { |
294 | --m_depth; |
295 | } |
296 | |
297 | bool visit(QQmlJS::AST::UiScriptBinding *node) override |
298 | { |
299 | if (m_depth == 0) |
300 | return true; |
301 | |
302 | QQmlJS::AST::UiQualifiedId *id = node->qualifiedId; |
303 | if (id && id->name == QStringLiteral("id" )) |
304 | addError(error: QString("Internal IDs are not allowed (%1)" ).arg(a: extractName(statement: node->statement)), node); |
305 | return true; |
306 | } |
307 | |
308 | private: |
309 | QString (QQmlJS::AST::Statement *statement) |
310 | { |
311 | QQmlJS::AST::ExpressionStatement *expressionStatement = static_cast<QQmlJS::AST::ExpressionStatement *>(statement); |
312 | if (!expressionStatement) |
313 | return QString(); |
314 | |
315 | QQmlJS::AST::IdentifierExpression *expression = static_cast<QQmlJS::AST::IdentifierExpression *>(expressionStatement->expression); |
316 | if (!expression) |
317 | return QString(); |
318 | |
319 | return expression->name.toString(); |
320 | } |
321 | |
322 | int m_depth; |
323 | }; |
324 | |
325 | void tst_Sanity::ids() |
326 | { |
327 | QFETCH(QString, control); |
328 | QFETCH(QString, filePath); |
329 | |
330 | IdValidator validator; |
331 | if (!validator.validate(filePath)) |
332 | QFAIL(qPrintable(validator.errors())); |
333 | } |
334 | |
335 | void tst_Sanity::ids_data() |
336 | { |
337 | QTest::addColumn<QString>(name: "control" ); |
338 | QTest::addColumn<QString>(name: "filePath" ); |
339 | |
340 | QMap<QString, QString>::const_iterator it; |
341 | for (it = files.constBegin(); it != files.constEnd(); ++it) |
342 | QTest::newRow(qPrintable(it.key())) << it.key() << it.value(); |
343 | } |
344 | |
345 | void tst_Sanity::attachedObjects() |
346 | { |
347 | QFETCH(QUrl, url); |
348 | |
349 | QQmlComponent component(&engine); |
350 | component.loadUrl(url); |
351 | |
352 | QSet<QString> classNames; |
353 | QScopedPointer<QObject> object(component.create()); |
354 | QVERIFY2(object.data(), qPrintable(component.errorString())); |
355 | for (QObject *object : qAsConst(t&: *qt_qobjects)) { |
356 | if (object->parent() == &engine) |
357 | continue; // allow "global" instances |
358 | QString className = object->metaObject()->className(); |
359 | if (className.endsWith(s: "Attached" ) || className.endsWith(s: "Style" )) |
360 | QVERIFY2(!classNames.contains(className), qPrintable(QString("Multiple %1 instances" ).arg(className))); |
361 | classNames.insert(value: className); |
362 | } |
363 | } |
364 | |
365 | void tst_Sanity::attachedObjects_data() |
366 | { |
367 | QTest::addColumn<QUrl>(name: "url" ); |
368 | addTestRowForEachControl(engine: &engine, sourcePath: "calendar" , targetPath: "Qt/labs/calendar" ); |
369 | addTestRowForEachControl(engine: &engine, sourcePath: "controls" , targetPath: "QtQuick/Controls.2" ); |
370 | addTestRowForEachControl(engine: &engine, sourcePath: "controls/fusion" , targetPath: "QtQuick/Controls.2" , skiplist: QStringList() << "CheckIndicator" << "RadioIndicator" << "SliderGroove" << "SliderHandle" << "SwitchIndicator" ); |
371 | addTestRowForEachControl(engine: &engine, sourcePath: "controls/material" , targetPath: "QtQuick/Controls.2/Material" , skiplist: QStringList() << "Ripple" << "SliderHandle" << "CheckIndicator" << "RadioIndicator" << "SwitchIndicator" << "BoxShadow" << "ElevationEffect" << "CursorDelegate" ); |
372 | addTestRowForEachControl(engine: &engine, sourcePath: "controls/universal" , targetPath: "QtQuick/Controls.2/Universal" , skiplist: QStringList() << "CheckIndicator" << "RadioIndicator" << "SwitchIndicator" ); |
373 | } |
374 | |
375 | QTEST_MAIN(tst_Sanity) |
376 | |
377 | #include "tst_sanity.moc" |
378 | |