1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Sergio Martins <sergio.martins@kdab.com> |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the plugins 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 <QtTest/QtTest> |
30 | #include <QProcess> |
31 | #include <QString> |
32 | |
33 | #include <util.h> |
34 | |
35 | class TestQmllint: public QQmlDataTest |
36 | { |
37 | Q_OBJECT |
38 | |
39 | private Q_SLOTS: |
40 | void initTestCase() override; |
41 | |
42 | void testUnqualified(); |
43 | void testUnqualified_data(); |
44 | |
45 | void cleanQmlCode_data(); |
46 | void cleanQmlCode(); |
47 | |
48 | void dirtyQmlCode_data(); |
49 | void dirtyQmlCode(); |
50 | |
51 | void testUnknownCausesFail(); |
52 | |
53 | private: |
54 | QString runQmllint(const QString &fileToLint, bool shouldSucceed); |
55 | |
56 | QString m_qmllintPath; |
57 | }; |
58 | |
59 | void TestQmllint::initTestCase() |
60 | { |
61 | QQmlDataTest::initTestCase(); |
62 | m_qmllintPath = QLibraryInfo::location(QLibraryInfo::BinariesPath) + QLatin1String("/qmllint" ); |
63 | #ifdef Q_OS_WIN |
64 | m_qmllintPath += QLatin1String(".exe" ); |
65 | #endif |
66 | if (!QFileInfo(m_qmllintPath).exists()) { |
67 | QString message = QStringLiteral("qmllint executable not found (looked for %0)" ).arg(a: m_qmllintPath); |
68 | QFAIL(qPrintable(message)); |
69 | } |
70 | } |
71 | |
72 | void TestQmllint::testUnqualified() |
73 | { |
74 | QFETCH(QString, filename); |
75 | QFETCH(QString, warningMessage); |
76 | QFETCH(int, warningLine); |
77 | QFETCH(int, warningColumn); |
78 | |
79 | const QString output = runQmllint(fileToLint: filename, shouldSucceed: false); |
80 | QVERIFY(output.contains(QString::asprintf("Warning: unqualified access at %d:%d" , warningLine, warningColumn))); |
81 | QVERIFY(output.contains(warningMessage)); |
82 | } |
83 | |
84 | void TestQmllint::testUnqualified_data() |
85 | { |
86 | QTest::addColumn<QString>(name: "filename" ); |
87 | QTest::addColumn<QString>(name: "warningMessage" ); |
88 | QTest::addColumn<int>(name: "warningLine" ); |
89 | QTest::addColumn<int>(name: "warningColumn" ); |
90 | |
91 | // check for false positive due to and warning about with statement |
92 | QTest::newRow(dataTag: "WithStatement" ) << QStringLiteral("WithStatement.qml" ) << QStringLiteral("with statements are strongly discouraged" ) << 10 << 25; |
93 | // id from nowhere (as with setContextProperty) |
94 | QTest::newRow(dataTag: "IdFromOuterSpaceDirect" ) << QStringLiteral("IdFromOuterSpace.qml" ) << "alien.x" << 4 << 8; |
95 | QTest::newRow(dataTag: "IdFromOuterSpaceAccess" ) << QStringLiteral("IdFromOuterSpace.qml" ) << "console.log(alien)" << 7 << 21; |
96 | // access property of root object |
97 | QTest::newRow(dataTag: "FromRootDirect" ) << QStringLiteral("FromRoot.qml" ) << QStringLiteral("x: root.unqualified" ) << 9 << 16; // new property |
98 | QTest::newRow(dataTag: "FromRootAccess" ) << QStringLiteral("FromRoot.qml" ) << QStringLiteral("property int check: root.x" ) << 13 << 33; // builtin property |
99 | // access injected name from signal |
100 | QTest::newRow(dataTag: "SignalHandler1" ) << QStringLiteral("SignalHandler.qml" ) << QStringLiteral("onDoubleClicked: function(mouse) {..." ) << 5 << 21; |
101 | QTest::newRow(dataTag: "SignalHandler2" ) << QStringLiteral("SignalHandler.qml" ) << QStringLiteral("onPositionChanged: function(mouse) {..." ) << 10 << 21; |
102 | QTest::newRow(dataTag: "SignalHandlerShort1" ) << QStringLiteral("SignalHandler.qml" ) << QStringLiteral("onClicked: (mouse) => {..." ) << 8 << 29; |
103 | QTest::newRow(dataTag: "SignalHandlerShort2" ) << QStringLiteral("SignalHandler.qml" ) << QStringLiteral("onPressAndHold: (mouse) => {..." ) << 12 << 34; |
104 | // access catch identifier outside catch block |
105 | QTest::newRow(dataTag: "CatchStatement" ) << QStringLiteral("CatchStatement.qml" ) << QStringLiteral("err" ) << 6 << 21; |
106 | |
107 | QTest::newRow(dataTag: "NonSpuriousParent" ) << QStringLiteral("nonSpuriousParentWarning.qml" ) << QStringLiteral("property int x: <id>.parent.x" ) << 6 << 25; |
108 | } |
109 | |
110 | void TestQmllint::testUnknownCausesFail() |
111 | { |
112 | const QString unknownNotFound = runQmllint(fileToLint: "unknownElement.qml" , shouldSucceed: false); |
113 | QVERIFY(unknownNotFound.contains( |
114 | QStringLiteral("warning: Unknown was not found. Did you add all import paths?" ))); |
115 | } |
116 | |
117 | void TestQmllint::dirtyQmlCode_data() |
118 | { |
119 | QTest::addColumn<QString>(name: "filename" ); |
120 | QTest::addColumn<QString>(name: "warningMessage" ); |
121 | QTest::addColumn<QString>(name: "notContained" ); |
122 | |
123 | QTest::newRow(dataTag: "Invalid_syntax_QML" ) |
124 | << QStringLiteral("failure1.qml" ) |
125 | << QStringLiteral("failure1.qml:4 : Expected token `:'" ) |
126 | << QString(); |
127 | QTest::newRow(dataTag: "Invalid_syntax_JS" ) |
128 | << QStringLiteral("failure1.js" ) |
129 | << QStringLiteral("failure1.js:4 : Expected token `;'" ) |
130 | << QString(); |
131 | QTest::newRow(dataTag: "AutomatchedSignalHandler" ) |
132 | << QStringLiteral("AutomatchedSignalHandler.qml" ) |
133 | << QString("Warning: unqualified access at 12:36" ) |
134 | << QStringLiteral("no matching signal found" ); |
135 | QTest::newRow(dataTag: "MemberNotFound" ) |
136 | << QStringLiteral("memberNotFound.qml" ) |
137 | << QString("Warning: Property \"foo\" not found on type \"QtObject\" at 6:31" ) |
138 | << QString(); |
139 | QTest::newRow(dataTag: "UnknownJavascriptMethd" ) |
140 | << QStringLiteral("unknownJavascriptMethod.qml" ) |
141 | << QString("Warning: Property \"foo2\" not found on type \"Methods\" at 5:25" ) |
142 | << QString(); |
143 | QTest::newRow(dataTag: "badAlias" ) |
144 | << QStringLiteral("badAlias.qml" ) |
145 | << QString("Warning: unqualified access at 4:27" ) |
146 | << QString(); |
147 | QTest::newRow(dataTag: "badAliasProperty" ) |
148 | << QStringLiteral("badAliasProperty.qml" ) |
149 | << QString("Warning: Property \"nowhere\" not found on type \"QtObject\" at 5:32" ) |
150 | << QString(); |
151 | QTest::newRow(dataTag: "badParent" ) |
152 | << QStringLiteral("badParent.qml" ) |
153 | << QString("Warning: Property \"rrr\" not found on type \"Item\" at 5:34" ) |
154 | << QString(); |
155 | QTest::newRow(dataTag: "parentIsComponent" ) |
156 | << QStringLiteral("parentIsComponent.qml" ) |
157 | << QString("Warning: Property \"progress\" not found on type \"QQuickItem\" at 7:39" ) |
158 | << QString(); |
159 | QTest::newRow(dataTag: "badTypeAssertion" ) |
160 | << QStringLiteral("badTypeAssertion.qml" ) |
161 | << QString("Warning: Property \"rrr\" not found on type \"Item\" at 5:39" ) |
162 | << QString(); |
163 | QTest::newRow(dataTag: "incompleteQmltypes" ) |
164 | << QStringLiteral("incompleteQmltypes.qml" ) |
165 | << QString("Warning: Type \"QPalette\" of member \"palette\" not found at 5:26" ) |
166 | << QString(); |
167 | QTest::newRow(dataTag: "inheritanceCylce" ) |
168 | << QStringLiteral("Cycle1.qml" ) |
169 | << QString("Warning: Cycle2 is part of an inheritance cycle: Cycle2 -> Cycle3 -> Cycle1 -> Cycle2" ) |
170 | << QString(); |
171 | } |
172 | |
173 | void TestQmllint::dirtyQmlCode() |
174 | { |
175 | QFETCH(QString, filename); |
176 | QFETCH(QString, warningMessage); |
177 | QFETCH(QString, notContained); |
178 | |
179 | const QString output = runQmllint(fileToLint: filename, shouldSucceed: false); |
180 | QVERIFY(output.contains(warningMessage)); |
181 | if (!notContained.isEmpty()) |
182 | QVERIFY(!output.contains(notContained)); |
183 | } |
184 | |
185 | void TestQmllint::cleanQmlCode_data() |
186 | { |
187 | QTest::addColumn<QString>(name: "filename" ); |
188 | QTest::newRow(dataTag: "Simple_QML" ) << QStringLiteral("Simple.qml" ); |
189 | QTest::newRow(dataTag: "QML_importing_JS" ) << QStringLiteral("importing_js.qml" ); |
190 | QTest::newRow(dataTag: "JS_with_pragma_and_import" ) << QStringLiteral("QTBUG-45916.js" ); |
191 | QTest::newRow(dataTag: "uiQml" ) << QStringLiteral("FormUser.qml" ); |
192 | QTest::newRow(dataTag: "methodInScope" ) << QStringLiteral("MethodInScope.qml" ); |
193 | QTest::newRow(dataTag: "importWithPrefix" ) << QStringLiteral("ImportWithPrefix.qml" ); |
194 | QTest::newRow(dataTag: "catchIdentifier" ) << QStringLiteral("catchIdentifierNoWarning.qml" ); |
195 | QTest::newRow(dataTag: "qmldirAndQmltypes" ) << QStringLiteral("qmldirAndQmltypes.qml" ); |
196 | QTest::newRow(dataTag: "forLoop" ) << QStringLiteral("forLoop.qml" ); |
197 | QTest::newRow(dataTag: "esmodule" ) << QStringLiteral("esmodule.mjs" ); |
198 | QTest::newRow(dataTag: "methodsInJavascript" ) << QStringLiteral("javascriptMethods.qml" ); |
199 | QTest::newRow(dataTag: "goodAlias" ) << QStringLiteral("goodAlias.qml" ); |
200 | QTest::newRow(dataTag: "goodParent" ) << QStringLiteral("goodParent.qml" ); |
201 | QTest::newRow(dataTag: "goodTypeAssertion" ) << QStringLiteral("goodTypeAssertion.qml" ); |
202 | QTest::newRow(dataTag: "AttachedProps" ) << QStringLiteral("AttachedProps.qml" ); |
203 | QTest::newRow(dataTag: "unknownBuiltinFont" ) << QStringLiteral("ButtonLoader.qml" ); |
204 | QTest::newRow(dataTag: "confusingImport" ) << QStringLiteral("Dialog.qml" ); |
205 | QTest::newRow(dataTag: "qualifiedAttached" ) << QStringLiteral("Drawer.qml" ); |
206 | } |
207 | |
208 | void TestQmllint::cleanQmlCode() |
209 | { |
210 | QFETCH(QString, filename); |
211 | const QString warnings = runQmllint(fileToLint: filename, shouldSucceed: true); |
212 | QVERIFY(warnings.isEmpty()); |
213 | } |
214 | |
215 | QString TestQmllint::runQmllint(const QString &fileToLint, bool shouldSucceed) |
216 | { |
217 | auto qmlImportDir = QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath); |
218 | QStringList args; |
219 | args << QStringLiteral("-U" ) << testFile(fileName: fileToLint) |
220 | << QStringLiteral("-I" ) << qmlImportDir |
221 | << QStringLiteral("-I" ) << dataDirectory() |
222 | << QStringLiteral("--silent" ); |
223 | QString errors; |
224 | auto verify = [&](bool isSilent) { |
225 | QProcess process; |
226 | process.start(program: m_qmllintPath, arguments: args); |
227 | QVERIFY(process.waitForFinished()); |
228 | QCOMPARE(process.exitStatus(), QProcess::NormalExit); |
229 | if (shouldSucceed) |
230 | QCOMPARE(process.exitCode(), 0); |
231 | else |
232 | QVERIFY(process.exitCode() != 0); |
233 | errors = process.readAllStandardError(); |
234 | |
235 | if (isSilent) |
236 | QVERIFY(errors.isEmpty()); |
237 | }; |
238 | verify(true); |
239 | args.removeLast(); |
240 | verify(false); |
241 | return errors; |
242 | } |
243 | |
244 | QTEST_MAIN(TestQmllint) |
245 | #include "tst_qmllint.moc" |
246 | |