/**************************************************************************** ** ** Copyright (C) 2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Sergio Martins ** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include class TestQmllint: public QQmlDataTest { Q_OBJECT public: TestQmllint(); struct Message { QString text = QString(); quint32 line = 0, column = 0; QtMsgType severity = QtWarningMsg; }; struct Result { enum Flag { ExitsNormally = 0x1, NoMessages = 0x2 }; Q_DECLARE_FLAGS(Flags, Flag) static Result clean() { return Result { {}, {}, {}, { NoMessages, ExitsNormally } }; } QList expectedMessages = {}; QList badMessages = {}; QList expectedReplacements = {}; Flags flags = {}; }; private Q_SLOTS: void initTestCase() override; void testUnqualified(); void testUnqualified_data(); void cleanQmlCode_data(); void cleanQmlCode(); void dirtyQmlCode_data(); void dirtyQmlCode(); void compilerWarnings_data(); void compilerWarnings(); void controlsSanity_data(); void controlsSanity(); void testUnknownCausesFail(); void directoryPassedAsQmlTypesFile(); void oldQmltypes(); void qmltypes_data(); void qmltypes(); #ifdef QT_QMLJSROOTGEN_PRESENT void verifyJsRoot(); #endif void autoqmltypes(); void resources(); void requiredProperty(); void settingsFile(); void additionalImplicitImport(); void attachedPropertyReuse(); void missingBuiltinsNoCrash(); void absolutePath(); void importMultipartUri(); private: enum DefaultImportOption { NoDefaultImports, UseDefaultImports }; enum ContainOption { StringNotContained, StringContained }; enum ReplacementOption { NoReplacementSearch, DoReplacementSearch, }; QString runQmllint(const QString &fileToLint, std::function handleResult, const QStringList &extraArgs = QStringList(), bool ignoreSettings = true, bool addImportDirs = true, bool absolutePath = true); QString runQmllint(const QString &fileToLint, bool shouldSucceed, const QStringList &extraArgs = QStringList(), bool ignoreSettings = true, bool addImportDirs = true, bool absolutePath = true); void callQmllint(const QString &fileToLint, bool shouldSucceed, QJsonArray *warnings = nullptr, QStringList importDirs = {}, QStringList qmltypesFiles = {}, QStringList resources = {}, DefaultImportOption defaultImports = UseDefaultImports, QMap *options = nullptr); void searchWarnings(const QJsonArray &warnings, const QString &string, QtMsgType type = QtWarningMsg, quint32 line = 0, quint32 column = 0, ContainOption shouldContain = StringContained, ReplacementOption searchReplacements = NoReplacementSearch); template void checkResult(const QJsonArray &warnings, const Result &result, ExpectedMessageFailureHandler onExpectedMessageFailures, BadMessageFailureHandler onBadMessageFailures); void checkResult(const QJsonArray &warnings, const Result &result) { checkResult( warnings, result, [] {}, [] {}); } void runTest(const QString &testFile, const Result &result, QStringList importDirs = {}, QStringList qmltypesFiles = {}, QStringList resources = {}, DefaultImportOption defaultImports = UseDefaultImports, QMap *options = nullptr); QString m_qmllintPath; QString m_qmljsrootgenPath; QString m_qmltyperegistrarPath; QStringList m_defaultImportPaths; QQmlJSLinter m_linter; }; Q_DECLARE_METATYPE(TestQmllint::Result) TestQmllint::TestQmllint() : QQmlDataTest(QT_QMLTEST_DATADIR), m_defaultImportPaths({ QLibraryInfo::path(QLibraryInfo::QmlImportsPath), dataDirectory() }), m_linter(m_defaultImportPaths) { } void TestQmllint::initTestCase() { QQmlDataTest::initTestCase(); m_qmllintPath = QLibraryInfo::path(QLibraryInfo::BinariesPath) + QLatin1String("/qmllint"); m_qmljsrootgenPath = QLibraryInfo::path(QLibraryInfo::LibraryExecutablesPath) + QLatin1String("/qmljsrootgen"); m_qmltyperegistrarPath = QLibraryInfo::path(QLibraryInfo::LibraryExecutablesPath) + QLatin1String("/qmltyperegistrar"); #ifdef Q_OS_WIN m_qmllintPath += QLatin1String(".exe"); m_qmljsrootgenPath += QLatin1String(".exe"); m_qmltyperegistrarPath += QLatin1String(".exe"); #endif if (!QFileInfo(m_qmllintPath).exists()) { QString message = QStringLiteral("qmllint executable not found (looked for %0)").arg(m_qmllintPath); QFAIL(qPrintable(message)); } #ifdef QT_QMLJSROOTGEN_PRESENT if (!QFileInfo(m_qmljsrootgenPath).exists()) { QString message = QStringLiteral("qmljsrootgen executable not found (looked for %0)").arg(m_qmljsrootgenPath); QFAIL(qPrintable(message)); } if (!QFileInfo(m_qmltyperegistrarPath).exists()) { QString message = QStringLiteral("qmltypesregistrar executable not found (looked for %0)").arg(m_qmltyperegistrarPath); QFAIL(qPrintable(message)); } #endif } void TestQmllint::testUnqualified() { QFETCH(QString, filename); QFETCH(QString, warningMessage); QFETCH(int, warningLine); QFETCH(int, warningColumn); const QString output = runQmllint(filename, false); QVERIFY(output.contains(QStringLiteral("%1:%2:%3: Unqualified access").arg(testFile(filename)).arg(warningLine).arg(warningColumn))); QVERIFY(output.contains(warningMessage)); } void TestQmllint::testUnqualified_data() { QTest::addColumn("filename"); QTest::addColumn("warningMessage"); QTest::addColumn("warningLine"); QTest::addColumn("warningColumn"); // id from nowhere (as with setContextProperty) QTest::newRow("IdFromOuterSpaceDirect") << QStringLiteral("IdFromOuterSpace.qml") << "alien.x" << 4 << 8; QTest::newRow("IdFromOuterSpaceAccess") << QStringLiteral("IdFromOuterSpace.qml") << "console.log(alien)" << 7 << 21; // access property of root object QTest::newRow("FromRootDirect") << QStringLiteral("FromRoot.qml") << QStringLiteral("x: root.unqualified") << 9 << 16; // new property QTest::newRow("FromRootAccess") << QStringLiteral("FromRoot.qml") << QStringLiteral("property int check: root.x") << 13 << 33; // builtin property // access injected name from signal QTest::newRow("SignalHandler1") << QStringLiteral("SignalHandler.qml") << QStringLiteral("onDoubleClicked: function(mouse) {") << 5 << 21; QTest::newRow("SignalHandler2") << QStringLiteral("SignalHandler.qml") << QStringLiteral("onPositionChanged: function(mouse) {") << 10 << 21; QTest::newRow("SignalHandlerShort1") << QStringLiteral("SignalHandler.qml") << QStringLiteral("onClicked: (mouse) => ") << 8 << 29; QTest::newRow("SignalHandlerShort2") << QStringLiteral("SignalHandler.qml") << QStringLiteral("onPressAndHold: (mouse) => ") << 12 << 34; // access catch identifier outside catch block QTest::newRow("CatchStatement") << QStringLiteral("CatchStatement.qml") << QStringLiteral("err") << 6 << 21; QTest::newRow("NonSpuriousParent") << QStringLiteral("nonSpuriousParentWarning.qml") << QStringLiteral("property int x: .parent.x") << 6 << 25; QTest::newRow("crashConnections") << QStringLiteral("crashConnections.qml") << QStringLiteral("target: FirstRunDialog") << 4 << 13; } void TestQmllint::testUnknownCausesFail() { runTest("unknownElement.qml", Result { { Message { QStringLiteral("Unknown was not found. Did you add all import paths?"), 4, 5, QtWarningMsg } } }); runTest("TypeWithUnknownPropertyType.qml", Result { { Message { QStringLiteral("Something was not found. Did you add all import paths?"), 4, 5, QtWarningMsg } } }); } void TestQmllint::directoryPassedAsQmlTypesFile() { runTest("unknownElement.qml", Result { { Message { QStringLiteral("QML types file cannot be a directory: ") + dataDirectory() } } }, {}, { dataDirectory() }); } void TestQmllint::oldQmltypes() { runTest("oldQmltypes.qml", Result { { Message { QStringLiteral("typeinfo not declared in qmldir file") }, Message { QStringLiteral("Found deprecated dependency specifications") }, Message { QStringLiteral( "Meta object revision and export version differ.") }, Message { QStringLiteral( "Revision 0 corresponds to version 0.0; it should be 1.0.") }, }, { Message { QStringLiteral( "QQuickItem was not found. Did you add all import paths?") } } }); } void TestQmllint::qmltypes_data() { QTest::addColumn("file"); const QString importsPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath); QDirIterator it(importsPath, { "*.qmltypes" }, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) QTest::addRow("%s", qPrintable(it.next().mid(importsPath.length()))) << it.filePath(); } void TestQmllint::qmltypes() { QFETCH(QString, file); callQmllint(file, true); } #ifdef QT_QMLJSROOTGEN_PRESENT void TestQmllint::verifyJsRoot() { QProcess process; const QString importsPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath); QDirIterator it(importsPath, { "jsroot.qmltypes" }, QDir::Files, QDirIterator::Subdirectories); QVERIFY(it.hasNext()); QString currentJsRootPath = it.next(); QTemporaryDir dir; QProcess jsrootProcess; connect(&jsrootProcess, &QProcess::errorOccurred, [&](QProcess::ProcessError error) { qWarning() << error << jsrootProcess.errorString(); }); jsrootProcess.setWorkingDirectory(dir.path()); jsrootProcess.start(m_qmljsrootgenPath, {"jsroot.json"}); jsrootProcess.waitForFinished(); QCOMPARE(jsrootProcess.exitStatus(), QProcess::NormalExit); QCOMPARE(jsrootProcess.exitCode(), 0); QProcess typeregistrarProcess; typeregistrarProcess.setWorkingDirectory(dir.path()); typeregistrarProcess.start(m_qmltyperegistrarPath, {"jsroot.json", "--generate-qmltypes", "jsroot.qmltypes"}); typeregistrarProcess.waitForFinished(); QCOMPARE(typeregistrarProcess.exitStatus(), QProcess::NormalExit); QCOMPARE(typeregistrarProcess.exitCode(), 0); QString currentJsRootContent, generatedJsRootContent; QFile currentJsRoot(currentJsRootPath); QVERIFY(currentJsRoot.open(QFile::ReadOnly)); currentJsRootContent = QString::fromUtf8(currentJsRoot.readAll()); currentJsRoot.close(); QFile generatedJsRoot(dir.path() + QDir::separator() + "jsroot.qmltypes"); QVERIFY(generatedJsRoot.open(QFile::ReadOnly)); generatedJsRootContent = QString::fromUtf8(generatedJsRoot.readAll()); generatedJsRoot.close(); // If any of the following asserts fail you need to update jsroot.qmltypes using the following commands: // // qmljsrootgen jsroot.json // qmltyperegistrar jsroot.json --generate-qmltypes src/imports/builtins/jsroot.qmltypes QStringList currentLines = currentJsRootContent.split(QLatin1Char('\n')); QStringList generatedLines = generatedJsRootContent.split(QLatin1Char('\n')); QCOMPARE(currentLines.count(), generatedLines.count()); for (qsizetype i = 0; i < currentLines.count(); i++) { QCOMPARE(currentLines[i], generatedLines[i]); } } #endif void TestQmllint::autoqmltypes() { QProcess process; process.setWorkingDirectory(testFile("autoqmltypes")); process.start(m_qmllintPath, { QStringLiteral("test.qml") }); process.waitForFinished(); QCOMPARE(process.exitStatus(), QProcess::NormalExit); QVERIFY(process.exitCode() != 0); QVERIFY(process.readAllStandardError() .contains("is not a qmldir file. Assuming qmltypes")); QVERIFY(process.readAllStandardOutput().isEmpty()); } void TestQmllint::resources() { callQmllint(testFile("resource.qml"), true, nullptr, {}, {}, { testFile("resource.qrc") }); callQmllint(testFile("badResource.qml"), false, nullptr, {}, {}, { testFile("resource.qrc") }); callQmllint(testFile("resource.qml"), false); callQmllint(testFile("badResource.qml"), true); callQmllint(testFile("T/b.qml"), true, nullptr, {}, {}, { testFile("T/a.qrc") }); } void TestQmllint::dirtyQmlCode_data() { QTest::addColumn("filename"); QTest::addColumn("result"); QTest::newRow("Invalid_syntax_QML") << QStringLiteral("failure1.qml") << Result { { Message { QStringLiteral("Expected token `:'"), 4, 8, QtCriticalMsg } } }; QTest::newRow("Invalid_syntax_JS") << QStringLiteral("failure1.js") << Result { { Message { QStringLiteral("Expected token `;'"), 4, 12, QtCriticalMsg } } }; QTest::newRow("AutomatchedSignalHandler") << QStringLiteral("AutomatchedSignalHandler.qml") << Result { { Message { QStringLiteral("Unqualified access"), 12, 36 } } }; QTest::newRow("AutomatchedSignalHandler2") << QStringLiteral("AutomatchedSignalHandler.qml") << Result { { Message { QStringLiteral("Implicitly defining onClicked as signal handler"), 0, 0, QtInfoMsg } } }; QTest::newRow("MemberNotFound") << QStringLiteral("memberNotFound.qml") << Result { { Message { QStringLiteral("Property \"foo\" not found on type \"QtObject\""), 6, 31 } } }; QTest::newRow("UnknownJavascriptMethd") << QStringLiteral("unknownJavascriptMethod.qml") << Result { { Message { QStringLiteral("Property \"foo2\" not found on type \"Methods\""), 5, 25 } } }; QTest::newRow("badAlias") << QStringLiteral("badAlias.qml") << Result { { Message { QStringLiteral("Cannot resolve alias \"wrong\""), 3, 1 } } }; QTest::newRow("badAliasProperty1") << QStringLiteral("badAliasProperty.qml") << Result { { Message { QStringLiteral("Cannot resolve alias \"wrong\""), 3, 1 } } }; QTest::newRow("badAliasExpression") << QStringLiteral("badAliasExpression.qml") << Result { { Message { QStringLiteral("Invalid alias expression. Only IDs and field member " "expressions can be aliased"), 5, 26 } } }; QTest::newRow("aliasCycle1") << QStringLiteral("aliasCycle.qml") << Result { { Message { QStringLiteral("Alias \"b\" is part of an alias cycle"), 3, 1 } } }; QTest::newRow("aliasCycle2") << QStringLiteral("aliasCycle.qml") << Result { { Message { QStringLiteral("Alias \"a\" is part of an alias cycle"), 3, 1 } } }; QTest::newRow("badParent") << QStringLiteral("badParent.qml") << Result { { Message { QStringLiteral("Property \"rrr\" not found on type \"Item\""), 5, 34 } } }; QTest::newRow("parentIsComponent") << QStringLiteral("parentIsComponent.qml") << Result { { Message { QStringLiteral("Property \"progress\" not found on type \"QQuickItem\""), 7, 39 } } }; QTest::newRow("badTypeAssertion") << QStringLiteral("badTypeAssertion.qml") << Result { { Message { QStringLiteral("Property \"rrr\" not found on type \"QQuickItem\""), 5, 39 } } }; QTest::newRow("incompleteQmltypes") << QStringLiteral("incompleteQmltypes.qml") << Result { { Message { QStringLiteral("Type \"QPalette\" of property \"palette\" not found"), 5, 26 } } }; QTest::newRow("incompleteQmltypes2") << QStringLiteral("incompleteQmltypes2.qml") << Result { { Message { QStringLiteral("Property \"weDontKnowIt\" " "not found on type \"CustomPalette\""), 5, 35 } } }; QTest::newRow("incompleteQmltypes3") << QStringLiteral("incompleteQmltypes3.qml") << Result { { Message { QStringLiteral("Type \"QPalette\" of property \"palette\" not found"), 5, 21 } } }; QTest::newRow("inheritanceCycle") << QStringLiteral("Cycle1.qml") << Result { { Message { QStringLiteral("Cycle2 is part of an inheritance cycle: Cycle2 -> Cycle3 " "-> Cycle1 -> Cycle2"), 2, 1 } } }; QTest::newRow("badQmldirImportAndDepend") << QStringLiteral("qmldirImportAndDepend/bad.qml") << Result { { Message { QStringLiteral("Item was not found. Did you add all import paths?"), 3, 1 } } }; QTest::newRow("javascriptMethodsInModule") << QStringLiteral("javascriptMethodsInModuleBad.qml") << Result { { Message { QStringLiteral("Property \"unknownFunc\" not found on type \"Foo\""), 5, 21 } } }; QTest::newRow("badEnumFromQtQml") << QStringLiteral("badEnumFromQtQml.qml") << Result { { Message { QStringLiteral("Property \"Linear123\" not " "found on type \"QQmlEasingEnums\""), 4, 30 } } }; QTest::newRow("anchors3") << QStringLiteral("anchors3.qml") << Result { { Message { QStringLiteral( "Cannot assign binding of type QQuickItem to QQuickAnchorLine") } } }; QTest::newRow("nanchors1") << QStringLiteral("nanchors1.qml") << Result { { Message { QStringLiteral( "unknown grouped property scope nanchors.") } } }; QTest::newRow("nanchors2") << QStringLiteral("nanchors2.qml") << Result { { Message { QStringLiteral( "unknown grouped property scope nanchors.") } } }; QTest::newRow("nanchors3") << QStringLiteral("nanchors3.qml") << Result { { Message { QStringLiteral( "unknown grouped property scope nanchors.") } } }; QTest::newRow("badAliasObject") << QStringLiteral("badAliasObject.qml") << Result { { Message { QStringLiteral("Property \"wrongwrongwrong\" not " "found on type \"QtObject\""), 8, 40 } } }; QTest::newRow("badScript") << QStringLiteral("badScript.qml") << Result { { Message { QStringLiteral( "Property \"stuff\" not found on type \"Empty\""), 5, 21 } } }; QTest::newRow("badScriptOnAttachedProperty") << QStringLiteral("badScript.attached.qml") << Result { { Message { QStringLiteral("Unqualified access"), 3, 26 } } }; QTest::newRow("brokenNamespace") << QStringLiteral("brokenNamespace.qml") << Result { { Message { QStringLiteral("Type not found in namespace"), 4, 19 } } }; QTest::newRow("segFault (bad)") << QStringLiteral("SegFault.bad.qml") << Result { { Message { QStringLiteral( "Property \"foobar\" not found on type \"QQuickScreenAttached\"") } } }; QTest::newRow("VariableUsedBeforeDeclaration") << QStringLiteral("useBeforeDeclaration.qml") << Result { { Message { QStringLiteral("Variable \"argq\" is used here before its declaration. " "The declaration is at 6:13."), 5, 9 } } }; QTest::newRow("SignalParameterMismatch") << QStringLiteral("namedSignalParameters.qml") << Result { { Message { QStringLiteral( "Parameter 1 to signal handler for \"onSig\" is called \"argarg\". " "The signal has a parameter of the same name in position 2.") } }, { Message { QStringLiteral("onSig2") } } }; QTest::newRow("TooManySignalParameters") << QStringLiteral("tooManySignalParameters.qml") << Result { { Message { QStringLiteral("Signal handler for \"onSig\" has more formal parameters " "than the signal it handles.") } } }; QTest::newRow("OnAssignment") << QStringLiteral("onAssignment.qml") << Result { { Message { QStringLiteral( "Property \"loops\" not found on type \"bool\"") } } }; QTest::newRow("BadAttached") << QStringLiteral("badAttached.qml") << Result { { Message { QStringLiteral( "unknown attached property scope WrongAttached.") } } }; QTest::newRow("BadBinding") << QStringLiteral("badBinding.qml") << Result { { Message { QStringLiteral( "Binding assigned to \"doesNotExist\", but no property " "\"doesNotExist\" exists in the current element.") } } }; QTest::newRow("bad template literal (simple)") << QStringLiteral("badTemplateStringSimple.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type string to int") } } }; QTest::newRow("bad template literal (substitution)") << QStringLiteral("badTemplateStringSubstitution.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type QString to int") } } }; QTest::newRow("bad constant number to string") << QStringLiteral("numberToStringProperty.qml") << Result { { Message { QStringLiteral( "Cannot assign a numeric constant to a string property") } } }; QTest::newRow("bad unary minus to string") << QStringLiteral("unaryMinusToStringProperty.qml") << Result { { Message { QStringLiteral( "Cannot assign a numeric constant to a string property") } } }; QTest::newRow("bad tranlsation binding (qsTr)") << QStringLiteral("bad_qsTr.qml") << Result {}; QTest::newRow("bad string binding (QT_TR_NOOP)") << QStringLiteral("bad_QT_TR_NOOP.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type string to int") } } }; QTest::newRow("BadScriptBindingOnGroup") << QStringLiteral("badScriptBinding.group.qml") << Result { { Message { QStringLiteral("Binding assigned to \"bogusProperty\", but no " "property \"bogusProperty\" exists in the current element."), 3, 10 } } }; QTest::newRow("BadScriptBindingOnAttachedType") << QStringLiteral("badScriptBinding.attached.qml") << Result { { Message { QStringLiteral("Binding assigned to \"bogusProperty\", but no " "property \"bogusProperty\" exists in the current element."), 5, 12 } } }; QTest::newRow("BadScriptBindingOnAttachedSignalHandler") << QStringLiteral("badScriptBinding.attachedSignalHandler.qml") << Result { { Message { QStringLiteral("no matching signal found for handler \"onBogusSignal\""), 3, 10 } } }; QTest::newRow("BadPropertyType") << QStringLiteral("badPropertyType.qml") << Result { { Message { QStringLiteral( "No type found for property \"bad\". This may be due to a missing " "import statement or incomplete qmltypes files.") } } }; QTest::newRow("Deprecation (Property, with reason)") << QStringLiteral("deprecatedPropertyReason.qml") << Result { { Message { QStringLiteral("Property \"deprecated\" is deprecated (Reason: Test)") } } }; QTest::newRow("Deprecation (Property, no reason)") << QStringLiteral("deprecatedProperty.qml") << Result { { Message { QStringLiteral("Property \"deprecated\" is deprecated") } } }; QTest::newRow("Deprecation (Property binding, with reason)") << QStringLiteral("deprecatedPropertyBindingReason.qml") << Result { { Message { QStringLiteral( "Binding on deprecated property \"deprecatedReason\" (Reason: Test)") } } }; QTest::newRow("Deprecation (Property binding, no reason)") << QStringLiteral("deprecatedPropertyBinding.qml") << Result { { Message { QStringLiteral("Binding on deprecated property \"deprecated\"") } } }; QTest::newRow("Deprecation (Type, with reason)") << QStringLiteral("deprecatedTypeReason.qml") << Result { { Message { QStringLiteral( "Type \"TypeDeprecatedReason\" is deprecated (Reason: Test)") } } }; QTest::newRow("Deprecation (Type, no reason)") << QStringLiteral("deprecatedType.qml") << Result { { Message { QStringLiteral("Type \"TypeDeprecated\" is deprecated") } } }; QTest::newRow("MissingDefaultProperty") << QStringLiteral("defaultPropertyWithoutKeyword.qml") << Result { { Message { QStringLiteral("Cannot assign to non-existent default property") } } }; QTest::newRow("MissingDefaultPropertyDefinedInTheSameType") << QStringLiteral("defaultPropertyWithinTheSameType.qml") << Result { { Message { QStringLiteral("Cannot assign to non-existent default property") } } }; QTest::newRow("DoubleAssignToDefaultProperty") << QStringLiteral("defaultPropertyWithDoubleAssignment.qml") << Result { { Message { QStringLiteral( "Cannot assign multiple objects to a default non-list property") } } }; QTest::newRow("DefaultPropertyWithWrongType(string)") << QStringLiteral("defaultPropertyWithWrongType.qml") << Result { { Message { QStringLiteral( "Cannot assign to default property of incompatible type") } }, { Message { QStringLiteral( "Cannot assign to non-existent default property") } } }; QTest::newRow("MultiDefaultPropertyWithWrongType") << QStringLiteral("multiDefaultPropertyWithWrongType.qml") << Result { { Message { QStringLiteral( "Cannot assign to default property of incompatible type") } }, { Message { QStringLiteral( "Cannot assign to non-existent default property") } } }; QTest::newRow("InvalidImport") << QStringLiteral("invalidImport.qml") << Result { { Message { QStringLiteral( "Failed to import FooBar. Are your import paths set up properly?") } } }; QTest::newRow("Unused Import (simple)") << QStringLiteral("unused_simple.qml") << Result { { Message { QStringLiteral("Unused import at"), 1, 1, QtInfoMsg } }, {}, {}, Result::ExitsNormally }; QTest::newRow("Unused Import (prefix)") << QStringLiteral("unused_prefix.qml") << Result { { Message { QStringLiteral("Unused import at"), 1, 1, QtInfoMsg } }, {}, {}, Result::ExitsNormally }; QTest::newRow("TypePropertAccess") << QStringLiteral("typePropertyAccess.qml") << Result {}; QTest::newRow("badAttachedProperty") << QStringLiteral("badAttachedProperty.qml") << Result { { Message { QStringLiteral("Property \"progress\" not found on type \"TestType\"") } } }; QTest::newRow("badAttachedPropertyNested") << QStringLiteral("badAttachedPropertyNested.qml") << Result { { Message { QStringLiteral( "Property \"progress\" not found on type \"QObject\""), 12, 41 } }, { Message { QString("Property \"progress\" not found on type \"QObject\""), 6, 37 } } }; QTest::newRow("badAttachedPropertyTypeString") << QStringLiteral("badAttachedPropertyTypeString.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type string to int") } } }; QTest::newRow("badAttachedPropertyTypeQtObject") << QStringLiteral("badAttachedPropertyTypeQtObject.qml") << Result { { Message { QStringLiteral( "Property \"count\" of type \"int\" is assigned an incompatible type " "\"QtObject\"") } } }; // should succeed, but it does not: QTest::newRow("attachedPropertyAccess") << QStringLiteral("goodAttachedPropertyAccess.qml") << Result::clean(); // should succeed, but it does not: QTest::newRow("attachedPropertyNested") << QStringLiteral("goodAttachedPropertyNested.qml") << Result::clean(); QTest::newRow("deprecatedFunction") << QStringLiteral("deprecatedFunction.qml") << Result { { Message { QStringLiteral( "Method \"deprecated(foobar)\" is deprecated (Reason: No particular " "reason.)") } } }; QTest::newRow("deprecatedFunctionInherited") << QStringLiteral("deprecatedFunctionInherited.qml") << Result { { Message { QStringLiteral( "Method \"deprecatedInherited(c, d)\" is deprecated (Reason: This " "deprecation should be visible!)") } } }; QTest::newRow("duplicated id") << QStringLiteral("duplicateId.qml") << Result { { Message { QStringLiteral("Found a duplicated id. id root was first declared ") } } }; QTest::newRow("string as id") << QStringLiteral("stringAsId.qml") << Result { { Message { QStringLiteral( "ids do not need quotation marks") } } }; QTest::newRow("stringIdUsedInWarning") << QStringLiteral("stringIdUsedInWarning.qml") << Result { { Message { QStringLiteral("i is a member of a parent element"), } }, {}, { Message { QStringLiteral("stringy.") } } }; QTest::newRow("Invalid id (expression)") << QStringLiteral("invalidId1.qml") << Result { { Message { QStringLiteral("Failed to parse id") } } }; QTest::newRow("multilineString") << QStringLiteral("multilineString.qml") << Result { { Message { QStringLiteral("String contains unescaped line terminator " "which is deprecated."), 0, 0, QtInfoMsg } }, {}, {}, Result::ExitsNormally }; QTest::newRow("multilineStringTortureQuote") << QStringLiteral("multilineStringTortureQuote.qml") << Result { { Message { QStringLiteral("String contains unescaped line terminator which is deprecated."), 0, 0, QtInfoMsg }}, {}, {Message {R"(` quote: " \\" \\\\" ticks: \` \` \\\` \\\` singleTicks: ' \' \\' \\\' expression: \${expr} \${expr} \\\${expr} \\\${expr} `)"}}, Result::ExitsNormally }; QTest::newRow("multilineStringTortureTick") << QStringLiteral("multilineStringTortureTick.qml") << Result { { Message { QStringLiteral("String contains unescaped line terminator which is deprecated."), 0, 0, QtInfoMsg } }, {}, {Message { R"(` quote: " \" \\" \\\" ticks: \` \` \\\` \\\` singleTicks: ' \\' \\\\' expression: \${expr} \${expr} \\\${expr} \\\${expr} `)" }}, Result::ExitsNormally}; QTest::newRow("unresolvedType") << QStringLiteral("unresolvedType.qml") << Result { { Message { QStringLiteral( "UnresolvedType was not found. Did you add all import paths?") } }, { Message { QStringLiteral("incompatible type") } } }; QTest::newRow("invalidInterceptor") << QStringLiteral("invalidInterceptor.qml") << Result { { Message { QStringLiteral( "On-binding for property \"angle\" has wrong type \"Item\"") } } }; QTest::newRow("2Interceptors") << QStringLiteral("2interceptors.qml") << Result { { Message { QStringLiteral("Duplicate interceptor on property \"x\"") } } }; QTest::newRow("ValueSource+2Interceptors") << QStringLiteral("valueSourceBetween2interceptors.qml") << Result { { Message { QStringLiteral("Duplicate interceptor on property \"x\"") } } }; QTest::newRow("2ValueSources") << QStringLiteral("2valueSources.qml") << Result { { Message { QStringLiteral( "Duplicate value source on property \"x\"") } } }; QTest::newRow("ValueSource+Value") << QStringLiteral("valueSource_Value.qml") << Result { { Message { QStringLiteral( "Cannot combine value source and binding on property \"obj\"") } } }; QTest::newRow("ValueSource+ListValue") << QStringLiteral("valueSource_listValue.qml") << Result { { Message { QStringLiteral( "Cannot combine value source and binding on property \"objs\"") } } }; QTest::newRow("NonExistentListProperty") << QStringLiteral("nonExistentListProperty.qml") << Result { { Message { QStringLiteral("Property \"objs\" is invalid or does not exist") } } }; QTest::newRow("QtQuick.Window 2.0") << QStringLiteral("qtquickWindow20.qml") << Result { { Message { QStringLiteral( "Property \"window\" not found on type \"QQuickWindow\"") } } }; QTest::newRow("unresolvedAttachedType") << QStringLiteral("unresolvedAttachedType.qml") << Result { { Message { QStringLiteral( "unknown attached property scope UnresolvedAttachedType.") } }, { Message { QStringLiteral( "Property \"property\" is invalid or does not exist") } } }; QTest::newRow("nestedInlineComponents") << QStringLiteral("nestedInlineComponents.qml") << Result { { Message { QStringLiteral("Nested inline components are not supported") } } }; QTest::newRow("WithStatement") << QStringLiteral("WithStatement.qml") << Result { { Message { QStringLiteral( "with statements are strongly discouraged") } } }; QTest::newRow("BindingTypeMismatch") << QStringLiteral("bindingTypeMismatch.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type QString to int") } } }; QTest::newRow("BindingTypeMismatchFunction") << QStringLiteral("bindingTypeMismatchFunction.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type QString to int") } } }; QTest::newRow("BadLiteralBinding") << QStringLiteral("badLiteralBinding.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type string to int") } } }; QTest::newRow("BadLiteralBindingDate") << QStringLiteral("badLiteralBindingDate.qml") << Result { { Message { QStringLiteral("Cannot assign binding of type QString to QDateTime") } } }; QTest::newRow("BadModulePrefix") << QStringLiteral("badModulePrefix.qml") << Result { { Message { QStringLiteral("Cannot load singleton as property of object") } } }; QTest::newRow("BadModulePrefix2") << QStringLiteral("badModulePrefix2.qml") << Result { { Message { QStringLiteral("Cannot use non-reference type QRectF as base " "of namespaced attached type") } } }; QTest::newRow("AssignToReadOnlyProperty") << QStringLiteral("assignToReadOnlyProperty.qml") << Result { { Message { QStringLiteral("Cannot assign to read-only property activeFocus") } } }; QTest::newRow("AssignToReadOnlyProperty") << QStringLiteral("assignToReadOnlyProperty2.qml") << Result { { Message { QStringLiteral("Cannot assign to read-only property activeFocus") } } }; QTest::newRow("DeferredPropertyID") << QStringLiteral("deferredPropertyID.qml") << Result { { Message { QStringLiteral( "Cannot defer property assignment to " "\"contentData\". Assigning an id to an object or one of its sub-objects " "bound to a deferred property will make the assignment immediate.") } } }; QTest::newRow("DeferredPropertyNestedID") << QStringLiteral("deferredPropertyNestedID.qml") << Result { { Message { QStringLiteral( "Cannot defer property assignment to " "\"contentData\". Assigning an id to an object or one of its sub-objects " "bound to a deferred property will make the assignment immediate.") } } }; QTest::newRow("cachedDependency") << QStringLiteral("cachedDependency.qml") << Result { { Message { QStringLiteral("Unused import at"), 1, 1, QtInfoMsg } }, { Message { QStringLiteral( "Cannot assign binding of type QQuickItem to QObject") } }, {}, Result::ExitsNormally }; QTest::newRow("cycle in import") << QStringLiteral("cycleHead.qml") << Result { { Message { QStringLiteral( "MenuItem is part of an inheritance cycle: MenuItem -> MenuItem") } } }; QTest::newRow("badGeneralizedGroup1") << QStringLiteral("badGeneralizedGroup1.qml") << Result { { Message { QStringLiteral( "Binding assigned to \"aaaa\", " "but no property \"aaaa\" exists in the current element") } } }; QTest::newRow("badGeneralizedGroup2") << QStringLiteral("badGeneralizedGroup2.qml") << Result { { Message { QStringLiteral("unknown grouped property scope aself") } } }; QTest::newRow("missingQmltypes") << QStringLiteral("missingQmltypes.qml") << Result { { Message { QStringLiteral("QML types file does not exist") } } }; QTest::newRow("enumInvalid") << QStringLiteral("enumInvalid.qml") << Result { { Message { QStringLiteral("Property \"red\" not found on type \"QtObject\"") } } }; QTest::newRow("inaccessibleId") << QStringLiteral("inaccessibleId.qml") << Result { { Message { QStringLiteral("Property \"objectName\" not found on type \"int\"") } } }; QTest::newRow("inaccessibleId2") << QStringLiteral("inaccessibleId2.qml") << Result { { Message { QStringLiteral("Property \"objectName\" not found on type \"int\"") } } }; QTest::newRow("unknownTypeCustomParser") << QStringLiteral("unknownTypeCustomParser.qml") << Result { { Message { QStringLiteral("TypeDoesNotExist was not found.") } } }; QTest::newRow("nonNullStored") << QStringLiteral("nonNullStored.qml") << Result { { Message { QStringLiteral( "Property \"objectName\" not found on type \"Foozle\"") } }, { Message { QStringLiteral("Unqualified access") } } }; QTest::newRow("cppPropertyChangeHandlers-wrong-parameters-size-bindable") << QStringLiteral("badCppPropertyChangeHandlers1.qml") << Result { { Message { QStringLiteral( "Signal handler for \"onAChanged\" has more formal parameters than " "the signal it handles") } } }; QTest::newRow("cppPropertyChangeHandlers-wrong-parameters-size-notify") << QStringLiteral("badCppPropertyChangeHandlers2.qml") << Result { { Message { QStringLiteral( "Signal handler for \"onBChanged\" has more formal parameters than " "the signal it handles") } } }; QTest::newRow("cppPropertyChangeHandlers-no-property") << QStringLiteral("badCppPropertyChangeHandlers3.qml") << Result { { Message { QStringLiteral("no matching signal found for handler \"onXChanged\"") } } }; QTest::newRow("cppPropertyChangeHandlers-not-a-signal") << QStringLiteral("badCppPropertyChangeHandlers4.qml") << Result { { Message { QStringLiteral( "no matching signal found for handler \"onWannabeSignal\"") } } }; QTest::newRow("didYouMean(binding)") << QStringLiteral("didYouMeanBinding.qml") << Result { { Message { QStringLiteral( "Binding assigned to \"witdh\", but no property \"witdh\" exists in " "the current element.") } }, {}, { Message { QStringLiteral("width") } } }; QTest::newRow("didYouMean(unqualified)") << QStringLiteral("didYouMeanUnqualified.qml") << Result { { Message { QStringLiteral("Unqualified access") } }, {}, { Message { QStringLiteral("height") } } }; QTest::newRow("didYouMean(unqualifiedCall)") << QStringLiteral("didYouMeanUnqualifiedCall.qml") << Result { { Message { QStringLiteral("Unqualified access") } }, {}, { Message { QStringLiteral("func") } } }; QTest::newRow("didYouMean(property)") << QStringLiteral("didYouMeanProperty.qml") << Result { { Message { QStringLiteral( "Property \"hoight\" not found on type \"Rectangle\"") }, {}, { Message { QStringLiteral("height") } } } }; QTest::newRow("didYouMean(propertyCall)") << QStringLiteral("didYouMeanPropertyCall.qml") << Result { { Message { QStringLiteral("Property \"lgg\" not found on type \"Console\"") }, {}, { Message { QStringLiteral("log") } } } }; QTest::newRow("didYouMean(component)") << QStringLiteral("didYouMeanComponent.qml") << Result { { Message { QStringLiteral( "Itym was not found. Did you add all import paths?") }, {}, { Message { QStringLiteral("Item") } } } }; QTest::newRow("nullBinding") << QStringLiteral("nullBinding.qml") << Result { { Message { QStringLiteral( "Cannot assign binding of type $internal$.std::nullptr_t to double") } } }; QTest::newRow("nullBindingFunction") << QStringLiteral("nullBindingFunction.qml") << Result { { Message { QStringLiteral( "Cannot assign binding of type std::nullptr_t to double") } } }; QTest::newRow("missingRequiredAlias") << QStringLiteral("missingRequiredAlias.qml") << Result { { Message { QStringLiteral("Component is missing required property requiredAlias from " "RequiredWithRootLevelAlias") } } }; QTest::newRow("missingSingletonPragma") << QStringLiteral("missingSingletonPragma.qml") << Result { { Message { QStringLiteral( "Type MissingPragma declared as singleton in qmldir but missing " "pragma Singleton") } } }; QTest::newRow("missingSingletonQmldir") << QStringLiteral("missingSingletonQmldir.qml") << Result { { Message { QStringLiteral( "Type MissingQmldirSingleton not declared as singleton in qmldir but using " "pragma Singleton") } } }; QTest::newRow("jsVarDeclarationsWriteConst") << QStringLiteral("jsVarDeclarationsWriteConst.qml") << Result { { Message { QStringLiteral("Cannot assign to read-only property constProp") } } }; QTest::newRow("shadowedSignal") << QStringLiteral("shadowedSignal.qml") << Result { { Message { QStringLiteral("Signal \"pressed\" is shadowed by a property.") } } }; QTest::newRow("shadowedSignalWithId") << QStringLiteral("shadowedSignalWithId.qml") << Result { { Message { QStringLiteral("Signal \"pressed\" is shadowed by a property") } } }; QTest::newRow("shadowedSlot") << QStringLiteral("shadowedSlot.qml") << Result { { Message { QStringLiteral( "Slot \"move\" is shadowed by a property") } } }; QTest::newRow("shadowedMethod") << QStringLiteral("shadowedMethod.qml") << Result { { Message { QStringLiteral( "Method \"foo\" is shadowed by a property.") } } }; QTest::newRow("callVarProp") << QStringLiteral("callVarProp.qml") << Result { { Message { QStringLiteral( "Property \"foo\" is a variant property. It may or may not be a " "method. Use a regular function instead.") } } }; QTest::newRow("callJSValue") << QStringLiteral("callJSValueProp.qml") << Result { { Message { QStringLiteral( "Property \"callLater\" is a QJSValue property. It may or may not be " "a method. Use a regular Q_INVOKABLE instead.") } } }; } void TestQmllint::dirtyQmlCode() { QFETCH(QString, filename); QFETCH(Result, result); QJsonArray warnings; QEXPECT_FAIL("attachedPropertyAccess", "We cannot discern between types and instances", Abort); QEXPECT_FAIL("attachedPropertyNested", "We cannot discern between types and instances", Abort); QEXPECT_FAIL("BadLiteralBindingDate", "We're currently not able to verify any non-trivial QString conversion that " "requires QQmlStringConverters", Abort); QEXPECT_FAIL("bad tranlsation binding (qsTr)", "We currently do not check translation binding", Abort); callQmllint(filename, result.flags.testFlag(Result::ExitsNormally), &warnings); checkResult( warnings, result, [] { QEXPECT_FAIL("BadLiteralBindingDate", "We're currently not able to verify any non-trivial QString " "conversion that " "requires QQmlStringConverters", Abort); }, [] { QEXPECT_FAIL("badAttachedPropertyNested", "We cannot discern between types and instances", Abort); }); } void TestQmllint::cleanQmlCode_data() { QTest::addColumn("filename"); QTest::newRow("Simple_QML") << QStringLiteral("Simple.qml"); QTest::newRow("QML_importing_JS") << QStringLiteral("importing_js.qml"); QTest::newRow("JS_with_pragma_and_import") << QStringLiteral("QTBUG-45916.js"); QTest::newRow("uiQml") << QStringLiteral("FormUser.qml"); QTest::newRow("methodInScope") << QStringLiteral("MethodInScope.qml"); QTest::newRow("importWithPrefix") << QStringLiteral("ImportWithPrefix.qml"); QTest::newRow("catchIdentifier") << QStringLiteral("catchIdentifierNoWarning.qml"); QTest::newRow("qmldirAndQmltypes") << QStringLiteral("qmldirAndQmltypes.qml"); QTest::newRow("forLoop") << QStringLiteral("forLoop.qml"); QTest::newRow("esmodule") << QStringLiteral("esmodule.mjs"); QTest::newRow("methodsInJavascript") << QStringLiteral("javascriptMethods.qml"); QTest::newRow("goodAlias") << QStringLiteral("goodAlias.qml"); QTest::newRow("goodParent") << QStringLiteral("goodParent.qml"); QTest::newRow("goodTypeAssertion") << QStringLiteral("goodTypeAssertion.qml"); QTest::newRow("AttachedProps") << QStringLiteral("AttachedProps.qml"); QTest::newRow("unknownBuiltinFont") << QStringLiteral("ButtonLoader.qml"); QTest::newRow("confusingImport") << QStringLiteral("Dialog.qml"); QTest::newRow("qualifiedAttached") << QStringLiteral("Drawer.qml"); QTest::newRow("EnumAccess1") << QStringLiteral("EnumAccess1.qml"); QTest::newRow("EnumAccess2") << QStringLiteral("EnumAccess2.qml"); QTest::newRow("ListProperty") << QStringLiteral("ListProperty.qml"); QTest::newRow("AttachedType") << QStringLiteral("AttachedType.qml"); QTest::newRow("qmldirImportAndDepend") << QStringLiteral("qmldirImportAndDepend/good.qml"); QTest::newRow("ParentEnum") << QStringLiteral("parentEnum.qml"); QTest::newRow("Signals") << QStringLiteral("Signal.qml"); QTest::newRow("javascriptMethodsInModule") << QStringLiteral("javascriptMethodsInModuleGood.qml"); QTest::newRow("enumFromQtQml") << QStringLiteral("enumFromQtQml.qml"); QTest::newRow("anchors1") << QStringLiteral("anchors1.qml"); QTest::newRow("anchors2") << QStringLiteral("anchors2.qml"); QTest::newRow("defaultImport") << QStringLiteral("defaultImport.qml"); QTest::newRow("goodAliasObject") << QStringLiteral("goodAliasObject.qml"); QTest::newRow("jsmoduleimport") << QStringLiteral("jsmoduleimport.qml"); QTest::newRow("overridescript") << QStringLiteral("overridescript.qml"); QTest::newRow("multiExtension") << QStringLiteral("multiExtension.qml"); QTest::newRow("segFault") << QStringLiteral("SegFault.qml"); QTest::newRow("grouped scope failure") << QStringLiteral("groupedScope.qml"); QTest::newRow("layouts depends quick") << QStringLiteral("layouts.qml"); QTest::newRow("attached") << QStringLiteral("attached.qml"); QTest::newRow("enumProperty") << QStringLiteral("enumProperty.qml"); QTest::newRow("externalEnumProperty") << QStringLiteral("externalEnumProperty.qml"); QTest::newRow("shapes") << QStringLiteral("shapes.qml"); QTest::newRow("var") << QStringLiteral("var.qml"); QTest::newRow("defaultProperty") << QStringLiteral("defaultProperty.qml"); QTest::newRow("defaultPropertyList") << QStringLiteral("defaultPropertyList.qml"); QTest::newRow("defaultPropertyComponent") << QStringLiteral("defaultPropertyComponent.qml"); QTest::newRow("defaultPropertyComponent2") << QStringLiteral("defaultPropertyComponent.2.qml"); QTest::newRow("defaultPropertyListModel") << QStringLiteral("defaultPropertyListModel.qml"); QTest::newRow("defaultPropertyVar") << QStringLiteral("defaultPropertyVar.qml"); QTest::newRow("multiDefaultProperty") << QStringLiteral("multiDefaultPropertyOk.qml"); QTest::newRow("propertyDelegate") << QStringLiteral("propertyDelegate.qml"); QTest::newRow("duplicateQmldirImport") << QStringLiteral("qmldirImport/duplicate.qml"); QTest::newRow("Used imports") << QStringLiteral("used.qml"); QTest::newRow("Unused imports (multi)") << QStringLiteral("unused_multi.qml"); QTest::newRow("Unused static module") << QStringLiteral("unused_static.qml"); QTest::newRow("compositeSingleton") << QStringLiteral("compositesingleton.qml"); QTest::newRow("stringLength") << QStringLiteral("stringLength.qml"); QTest::newRow("stringLength2") << QStringLiteral("stringLength2.qml"); QTest::newRow("stringLength3") << QStringLiteral("stringLength3.qml"); QTest::newRow("attachedPropertyAssignments") << QStringLiteral("attachedPropertyAssignments.qml"); QTest::newRow("groupedPropertyAssignments") << QStringLiteral("groupedPropertyAssignments.qml"); QTest::newRow("goodAttachedProperty") << QStringLiteral("goodAttachedProperty.qml"); QTest::newRow("objectBindingOnVarProperty") << QStringLiteral("objectBoundToVar.qml"); QTest::newRow("Unversioned change signal without arguments") << QStringLiteral("unversionChangedSignalSansArguments.qml"); QTest::newRow("deprecatedFunctionOverride") << QStringLiteral("deprecatedFunctionOverride.qml"); QTest::newRow("multilineStringEscaped") << QStringLiteral("multilineStringEscaped.qml"); QTest::newRow("propertyOverride") << QStringLiteral("propertyOverride.qml"); QTest::newRow("propertyBindingValue") << QStringLiteral("propertyBindingValue.qml"); QTest::newRow("customParser") << QStringLiteral("customParser.qml"); QTest::newRow("customParser.recursive") << QStringLiteral("customParser.recursive.qml"); QTest::newRow("2Behavior") << QStringLiteral("2behavior.qml"); QTest::newRow("interceptor") << QStringLiteral("interceptor.qml"); QTest::newRow("valueSource") << QStringLiteral("valueSource.qml"); QTest::newRow("interceptor+valueSource") << QStringLiteral("interceptor_valueSource.qml"); QTest::newRow("groupedProperty (valueSource+interceptor)") << QStringLiteral("groupedProperty_valueSource_interceptor.qml"); QTest::newRow("QtQuick.Window 2.1") << QStringLiteral("qtquickWindow21.qml"); QTest::newRow("attachedTypeIndirect") << QStringLiteral("attachedTypeIndirect.qml"); QTest::newRow("objectArray") << QStringLiteral("objectArray.qml"); QTest::newRow("aliasToList") << QStringLiteral("aliasToList.qml"); QTest::newRow("QVariant") << QStringLiteral("qvariant.qml"); QTest::newRow("Accessible") << QStringLiteral("accessible.qml"); QTest::newRow("qjsroot") << QStringLiteral("qjsroot.qml"); QTest::newRow("InlineComponent") << QStringLiteral("inlineComponent.qml"); QTest::newRow("InlineComponentWithComponents") << QStringLiteral("inlineComponentWithComponents.qml"); QTest::newRow("InlineComponentsChained") << QStringLiteral("inlineComponentsChained.qml"); QTest::newRow("ignoreWarnings") << QStringLiteral("ignoreWarnings.qml"); QTest::newRow("BindingBeforeDeclaration") << QStringLiteral("bindingBeforeDeclaration.qml"); QTest::newRow("CustomParserUnqualifiedAccess") << QStringLiteral("customParserUnqualifiedAccess.qml"); QTest::newRow("ImportQMLModule") << QStringLiteral("importQMLModule.qml"); QTest::newRow("ImportDirectoryQmldir") << QStringLiteral("Things/LintDirectly.qml"); QTest::newRow("BindingsOnGroupAndAttachedProperties") << QStringLiteral("goodBindingsOnGroupAndAttached.qml"); QTest::newRow("QQmlEasingEnums::Type") << QStringLiteral("animationEasing.qml"); QTest::newRow("ValidLiterals") << QStringLiteral("validLiterals.qml"); QTest::newRow("GoodModulePrefix") << QStringLiteral("goodModulePrefix.qml"); QTest::newRow("required property in Component") << QStringLiteral("requiredPropertyInComponent.qml"); QTest::newRow("bytearray") << QStringLiteral("bytearray.qml"); QTest::newRow("initReadonly") << QStringLiteral("initReadonly.qml"); QTest::newRow("connectionNoParent") << QStringLiteral("connectionNoParent.qml"); // QTBUG-97600 QTest::newRow("goodGeneralizedGroup") << QStringLiteral("goodGeneralizedGroup.qml"); QTest::newRow("on binding in grouped property") << QStringLiteral("onBindingInGroupedProperty.qml"); QTest::newRow("declared property of JS object") << QStringLiteral("bareQt.qml"); QTest::newRow("ID overrides property") << QStringLiteral("accessibleId.qml"); QTest::newRow("matchByName") << QStringLiteral("matchByName.qml"); QTest::newRow("QObject.hasOwnProperty") << QStringLiteral("qobjectHasOwnProperty.qml"); QTest::newRow("cppPropertyChangeHandlers") << QStringLiteral("goodCppPropertyChangeHandlers.qml"); QTest::newRow("unexportedCppBase") << QStringLiteral("unexportedCppBase.qml"); QTest::newRow("requiredWithRootLevelAlias") << QStringLiteral("RequiredWithRootLevelAlias.qml"); QTest::newRow("jsVarDeclarations") << QStringLiteral("jsVarDeclarations.qml"); QTest::newRow("qmodelIndex") << QStringLiteral("qmodelIndex.qml"); } void TestQmllint::cleanQmlCode() { QFETCH(QString, filename); QJsonArray warnings; runTest(filename, Result::clean()); } void TestQmllint::compilerWarnings_data() { QTest::addColumn("filename"); QTest::addColumn("result"); QTest::addColumn("enableCompilerWarnings"); QTest::newRow("listIndices") << QStringLiteral("listIndices.qml") << Result::clean() << true; QTest::newRow("lazyAndDirect") << QStringLiteral("LazyAndDirect/Lazy.qml") << Result::clean() << true; QTest::newRow("qQmlV4Function") << QStringLiteral("varargs.qml") << Result::clean() << true; QTest::newRow("multiGrouped") << QStringLiteral("multiGrouped.qml") << Result::clean() << true; QTest::newRow("shadowable") << QStringLiteral("shadowable.qml") << Result { { Message { QStringLiteral( "with type NotSoSimple can be shadowed") } } } << true; QTest::newRow("tooFewParameters") << QStringLiteral("tooFewParams.qml") << Result { { Message { QStringLiteral("No matching override found") } } } << true; QTest::newRow("javascriptVariableArgs") << QStringLiteral("javascriptVariableArgs.qml") << Result { { Message { QStringLiteral("Function expects 0 arguments, but 2 were provided") } } } << true; QTest::newRow("unknownTypeInRegister") << QStringLiteral("unknownTypeInRegister.qml") << Result { { Message { QStringLiteral("Functions without type annotations won't be compiled") } } } << true; QTest::newRow("pragmaStrict") << QStringLiteral("pragmaStrict.qml") << Result { { { QStringLiteral( "Functions without type annotations won't be compiled") } } } << true; } void TestQmllint::compilerWarnings() { QFETCH(QString, filename); QFETCH(Result, result); QFETCH(bool, enableCompilerWarnings); QJsonArray warnings; auto options = QQmlJSLogger::options(); if (enableCompilerWarnings) options[u"compiler"_qs].setLevel(u"warning"_qs); runTest(filename, result, {}, {}, {}, UseDefaultImports, &options); } void TestQmllint::controlsSanity_data() { QTest::addColumn("filename"); QTest::addColumn("result"); QTest::newRow("functionDeclarations") << QStringLiteral("functionDeclarations.qml") << Result { { Message { QStringLiteral("Declared function \"add\"") } } }; QTest::newRow("signalHandlers") << QStringLiteral("signalHandlers.qml") << Result { { Message { QStringLiteral("Declared signal handler \"onCompleted\"") } } }; QTest::newRow("anchors") << QStringLiteral("anchors.qml") << Result { { Message { QStringLiteral("Using anchors here") } } }; } void TestQmllint::controlsSanity() { QFETCH(QString, filename); QFETCH(Result, result); QJsonArray warnings; auto options = QQmlJSLogger::options(); options[u"controls-sanity"_qs].setLevel(u"warning"_qs); runTest(filename, result, {}, {}, {}, UseDefaultImports, &options); } QString TestQmllint::runQmllint(const QString &fileToLint, std::function handleResult, const QStringList &extraArgs, bool ignoreSettings, bool addImportDirs, bool absolutePath) { auto qmlImportDir = QLibraryInfo::path(QLibraryInfo::QmlImportsPath); QStringList args; QString absoluteFilePath = QFileInfo(fileToLint).isAbsolute() ? fileToLint : testFile(fileToLint); args << QFileInfo(absoluteFilePath).fileName(); if (addImportDirs) { args << QStringLiteral("-I") << qmlImportDir << QStringLiteral("-I") << dataDirectory(); } if (ignoreSettings) args << QStringLiteral("--ignore-settings"); if (absolutePath) args << QStringLiteral("--absolute-path"); args << extraArgs; args << QStringLiteral("--silent"); QString errors; auto verify = [&](bool isSilent) { QProcess process; process.setWorkingDirectory(QFileInfo(absoluteFilePath).absolutePath()); process.start(m_qmllintPath, args); handleResult(process); errors = process.readAllStandardError(); QStringList lines = errors.split(u'\n', Qt::SkipEmptyParts); auto end = std::remove_if(lines.begin(), lines.end(), [](const QString &line) { return !line.startsWith("Warning: ") && !line.startsWith("Error: "); }); std::sort(lines.begin(), end); auto it = std::unique(lines.begin(), end); if (it != end) { qDebug() << "The warnings and errors were generated more than once:"; do { qDebug() << *it; } while (++it != end); QTest::qFail("Duplicate warnings and errors", __FILE__, __LINE__); } if (isSilent) { QTest::qVerify(errors.isEmpty(), "errors.isEmpty()", "Silent mode outputs messages", __FILE__, __LINE__); } if (QTest::currentTestFailed()) { qDebug().noquote() << "Command:" << process.program() << args.join(u' '); qDebug() << "Exit status:" << process.exitStatus(); qDebug() << "Exit code:" << process.exitCode(); qDebug() << "stderr:" << errors; qDebug() << "stdout:" << process.readAllStandardOutput(); } }; verify(true); args.removeLast(); verify(false); return errors; } QString TestQmllint::runQmllint(const QString &fileToLint, bool shouldSucceed, const QStringList &extraArgs, bool ignoreSettings, bool addImportDirs, bool absolutePath) { return runQmllint( fileToLint, [&](QProcess &process) { QVERIFY(process.waitForFinished()); QCOMPARE(process.exitStatus(), QProcess::NormalExit); if (shouldSucceed) QCOMPARE(process.exitCode(), 0); else QVERIFY(process.exitCode() != 0); }, extraArgs, ignoreSettings, addImportDirs, absolutePath); } void TestQmllint::callQmllint(const QString &fileToLint, bool shouldSucceed, QJsonArray *warnings, QStringList importPaths, QStringList qmldirFiles, QStringList resources, DefaultImportOption defaultImports, QMap *options) { QJsonArray jsonOutput; bool success = m_linter.lintFile( QFileInfo(fileToLint).isAbsolute() ? fileToLint : testFile(fileToLint), nullptr, true, warnings ? &jsonOutput : nullptr, defaultImports == UseDefaultImports ? m_defaultImportPaths + importPaths : importPaths, qmldirFiles, resources, options != nullptr ? *options : QQmlJSLogger::options()); QVERIFY2(success == shouldSucceed, QJsonDocument(jsonOutput).toJson()); if (warnings) { QVERIFY2(jsonOutput.size() == 1, QJsonDocument(jsonOutput).toJson()); *warnings = jsonOutput.at(0)[u"warnings"_qs].toArray(); } QCOMPARE(success, shouldSucceed); } void TestQmllint::runTest(const QString &testFile, const Result &result, QStringList importDirs, QStringList qmltypesFiles, QStringList resources, DefaultImportOption defaultImports, QMap *options) { QJsonArray warnings; callQmllint(testFile, result.flags.testFlag(Result::Flag::ExitsNormally), &warnings, importDirs, qmltypesFiles, resources, defaultImports, options); checkResult(warnings, result); } template void TestQmllint::checkResult(const QJsonArray &warnings, const Result &result, ExpectedMessageFailureHandler onExpectedMessageFailures, BadMessageFailureHandler onBadMessageFailures) { if (result.flags.testFlag(Result::Flag::NoMessages)) QVERIFY2(warnings.isEmpty(), qPrintable(QJsonDocument(warnings).toJson())); for (const Message &msg : result.expectedMessages) { // output.contains() expect fails: onExpectedMessageFailures(); searchWarnings(warnings, msg.text, msg.severity, msg.line, msg.column); } for (const Message &msg : result.badMessages) { // !output.contains() expect fails: onBadMessageFailures(); searchWarnings(warnings, msg.text, msg.severity, msg.line, msg.column, StringNotContained); } for (const Message &replacement : result.expectedReplacements) { searchWarnings(warnings, replacement.text, replacement.severity, replacement.line, replacement.column, StringContained, DoReplacementSearch); } } void TestQmllint::searchWarnings(const QJsonArray &warnings, const QString &substring, QtMsgType type, quint32 line, quint32 column, ContainOption shouldContain, ReplacementOption searchReplacements) { bool contains = false; auto typeStringToMsgType = [](const QString &type) -> QtMsgType { if (type == u"debug") return QtDebugMsg; if (type == u"info") return QtInfoMsg; if (type == u"warning") return QtWarningMsg; if (type == u"critical") return QtCriticalMsg; if (type == u"fatal") return QtFatalMsg; Q_UNREACHABLE(); }; for (const QJsonValue &warning : warnings) { QString warningMessage = warning[u"message"].toString(); quint32 warningLine = warning[u"line"].toInt(); quint32 warningColumn = warning[u"column"].toInt(); QtMsgType warningType = typeStringToMsgType(warning[u"type"].toString()); if (warningMessage.contains(substring)) { if (warningType != type) { continue; } if (line != 0 || column != 0) { if (warningLine != line || warningColumn != column) { continue; } } contains = true; break; } for (const QJsonValue &fix : warning[u"suggestions"].toArray()) { const QString fixMessage = fix[u"message"].toString(); if (fixMessage.contains(substring)) { contains = true; break; } if (searchReplacements == DoReplacementSearch) { QString replacement = fix[u"replacement"].toString(); #ifdef Q_OS_WIN // Replacements can contain native line endings // but we need them to be uniform in order for them to conform to our test data replacement = replacement.replace(u"\r\n"_qs, u"\n"_qs); #endif if (replacement.contains(substring)) { quint32 fixLine = fix[u"line"].toInt(); quint32 fixColumn = fix[u"column"].toInt(); if (line != 0 || column != 0) { if (fixLine != line || fixColumn != column) { continue; } } contains = true; break; } } } } const auto toDescription = [](const QJsonArray &warnings, const QString &substring, bool must = true) { // Note: this actually produces a very poorly formatted multi-line // description, but this is how we also do it in cleanQmlCode test case, // so this should suffice. in any case this mainly aids the debugging // and CI stays (or should stay) clean. return QStringLiteral("qmllint output '%1' %2 contain '%3'") .arg(QString::fromUtf8(QJsonDocument(warnings).toJson(QJsonDocument::Compact)), must ? u"must" : u"must NOT", substring); }; if (shouldContain == StringContained) QVERIFY2(contains, qPrintable(toDescription(warnings, substring))); else QVERIFY2(!contains, qPrintable(toDescription(warnings, substring, false))); } void TestQmllint::requiredProperty() { runTest("requiredProperty.qml", Result::clean()); runTest("requiredMissingProperty.qml", Result { { Message { QStringLiteral( "Property \"foo\" was marked as required but does not exist.") } } }); runTest("requiredPropertyBindings.qml", Result::clean()); runTest("requiredPropertyBindingsNow.qml", Result { { Message { QStringLiteral("Component is missing required property " "required_now_string from Base") }, Message { QStringLiteral("Component is missing required property " "required_defined_here_string from here") } } }); runTest("requiredPropertyBindingsLater.qml", Result { { Message { QStringLiteral("Component is missing required property " "required_later_string from " "Base") }, Message { QStringLiteral("Property marked as required in Derived") }, Message { QStringLiteral("Component is missing required property " "required_even_later_string " "from Base (marked as required by here)") } } }); } void TestQmllint::settingsFile() { QVERIFY(runQmllint("settings/unqualifiedSilent/unqualified.qml", true, QStringList(), false) .isEmpty()); QVERIFY(runQmllint("settings/unusedImportWarning/unused.qml", false, QStringList(), false) .contains(QStringLiteral("Warning: %1:2:1: Unused import at %1:2:1") .arg(testFile("settings/unusedImportWarning/unused.qml")))); QVERIFY(runQmllint("settings/bare/bare.qml", false, {}, false, false) .contains(QStringLiteral("Failed to find the following builtins: " "builtins.qmltypes, jsroot.qmltypes"))); QVERIFY(runQmllint("settings/qmltypes/qmltypes.qml", false, QStringList(), false) .contains(QStringLiteral("not a qmldir file. Assuming qmltypes."))); QVERIFY(runQmllint("settings/qmlimports/qmlimports.qml", true, QStringList(), false).isEmpty()); } void TestQmllint::additionalImplicitImport() { runTest("additionalImplicitImport.qml", Result::clean(), {}, {}, { testFile("implicitImportResource.qrc") }); } void TestQmllint::attachedPropertyReuse() { auto options = QQmlJSLogger::options(); options[u"multiple-attached-objects"_qs].setLevel(u"warning"_qs); runTest("attachedPropNotReused.qml", Result { { Message { QStringLiteral("Using attached type QQuickKeyNavigationAttached " "already initialized in a parent " "scope") } } }, {}, {}, {}, UseDefaultImports, &options); runTest("attachedPropEnum.qml", Result::clean(), {}, {}, {}, UseDefaultImports, &options); } void TestQmllint::missingBuiltinsNoCrash() { // We cannot use the normal linter here since the other tests might have cached the builtins // alread QQmlJSLinter linter(m_defaultImportPaths); QJsonArray jsonOutput; QJsonArray warnings; bool success = linter.lintFile(testFile("missingBuiltinsNoCrash.qml"), nullptr, true, &jsonOutput, {}, {}, {}, {}); QVERIFY2(!success, QJsonDocument(jsonOutput).toJson()); QVERIFY2(jsonOutput.size() == 1, QJsonDocument(jsonOutput).toJson()); warnings = jsonOutput.at(0)[u"warnings"_qs].toArray(); checkResult(warnings, Result { { Message { QStringLiteral("Failed to find the following builtins: " "builtins.qmltypes, jsroot.qmltypes") } } }); } void TestQmllint::absolutePath() { QString absPathOutput = runQmllint("memberNotFound.qml", false, {}, true, true, true); QString relPathOutput = runQmllint("memberNotFound.qml", false, {}, true, true, false); const QString absolutePath = QFileInfo(testFile("memberNotFound.qml")).absoluteFilePath(); QVERIFY(absPathOutput.contains(absolutePath)); QVERIFY(!relPathOutput.contains(absolutePath)); } void TestQmllint::importMultipartUri() { runTest("here.qml", Result::clean(), {}, { testFile("Elsewhere/qmldir") }); } QTEST_MAIN(TestQmllint) #include "tst_qmllint.moc"