diff options
Diffstat (limited to 'tests/auto/qml/qmllint/tst_qmllint.cpp')
-rw-r--r-- | tests/auto/qml/qmllint/tst_qmllint.cpp | 2169 |
1 files changed, 1960 insertions, 209 deletions
diff --git a/tests/auto/qml/qmllint/tst_qmllint.cpp b/tests/auto/qml/qmllint/tst_qmllint.cpp index 25776c5448..4e69fc2e9e 100644 --- a/tests/auto/qml/qmllint/tst_qmllint.cpp +++ b/tests/auto/qml/qmllint/tst_qmllint.cpp @@ -1,41 +1,53 @@ -/**************************************************************************** -** -** Copyright (C) 2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Sergio Martins <sergio.martins@kdab.com> -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the plugins 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$ -** -****************************************************************************/ +// Copyright (C) 2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Sergio Martins <sergio.martins@kdab.com> +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtTest/QtTest> #include <QProcess> #include <QString> +#include <QtQuickTestUtils/private/qmlutils_p.h> +#include <QtQmlCompiler/private/qqmljslinter_p.h> +#include <QtQmlCompiler/private/qqmlsa_p.h> +#include <QtCore/qplugin.h> -#include <util.h> +Q_IMPORT_PLUGIN(LintPlugin) + +using namespace Qt::StringLiterals; 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, AutoFixable = 0x4 }; + + Q_DECLARE_FLAGS(Flags, Flag) + + static Result clean() { return Result { {}, {}, {}, { NoMessages, ExitsNormally } }; } + + QList<Message> expectedMessages = {}; + QList<Message> badMessages = {}; + QList<Message> expectedReplacements = {}; + + Flags flags = {}; + }; + + struct Environment : public QList<QPair<QString, QString>> + { + using QList<QPair<QString, QString>>::QList; + }; + private Q_SLOTS: void initTestCase() override; @@ -48,6 +60,9 @@ private Q_SLOTS: void dirtyQmlCode_data(); void dirtyQmlCode(); + void compilerWarnings_data(); + void compilerWarnings(); + void testUnknownCausesFail(); void directoryPassedAsQmlTypesFile(); @@ -56,102 +71,255 @@ private Q_SLOTS: void qmltypes_data(); void qmltypes(); +#ifdef QT_QMLJSROOTGEN_PRESENT + void verifyJsRoot(); +#endif + void autoqmltypes(); void resources(); + void multiDirectory(); + void requiredProperty(); + void settingsFile(); + + void additionalImplicitImport(); + + void qrcUrlImport(); + + void incorrectImportFromHost_data(); + void incorrectImportFromHost(); + + void attachedPropertyReuse(); + + void missingBuiltinsNoCrash(); + void absolutePath(); + + void importMultipartUri(); + + void lintModule_data(); + void lintModule(); + + void testLineEndings(); + void valueTypesFromString(); + + void ignoreSettingsNotCommandLineOptions(); + + void environment_data(); + void environment(); + +#if QT_CONFIG(library) + void testPlugin(); + void quickPlugin(); +#endif private: - QString runQmllint(const QString &fileToLint, - std::function<void(QProcess &)> handleResult, - const QStringList &extraArgs = QStringList()); + enum DefaultImportOption { NoDefaultImports, UseDefaultImports }; + enum ContainOption { StringNotContained, StringContained }; + enum ReplacementOption { + NoReplacementSearch, + DoReplacementSearch, + }; + + enum LintType { LintFile, LintModule }; + + QString runQmllint(const QString &fileToLint, std::function<void(QProcess &)> handleResult, + const QStringList &extraArgs = QStringList(), bool ignoreSettings = true, + bool addImportDirs = true, bool absolutePath = true, + const Environment &env = {}); QString runQmllint(const QString &fileToLint, bool shouldSucceed, - const QStringList &extraArgs = QStringList()); + const QStringList &extraArgs = QStringList(), bool ignoreSettings = true, + bool addImportDirs = true, bool absolutePath = true, + const Environment &env = {}); + void callQmllint(const QString &fileToLint, bool shouldSucceed, QJsonArray *warnings = nullptr, + QStringList importDirs = {}, QStringList qmltypesFiles = {}, + QStringList resources = {}, + DefaultImportOption defaultImports = UseDefaultImports, + QList<QQmlJS::LoggerCategory> *categories = nullptr, bool autoFixable = false, + LintType type = LintFile); + + void searchWarnings(const QJsonArray &warnings, const QString &string, + QtMsgType type = QtWarningMsg, quint32 line = 0, quint32 column = 0, + ContainOption shouldContain = StringContained, + ReplacementOption searchReplacements = NoReplacementSearch); + + template<typename ExpectedMessageFailureHandler, typename BadMessageFailureHandler> + 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, + QList<QQmlJS::LoggerCategory> *categories = 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); + QFETCH(Result, result); - const QString output = runQmllint(filename, false); - QVERIFY(output.contains(QString::asprintf("Warning: unqualified access at %s:%d:%d", testFile(filename).toUtf8().constData(), warningLine, warningColumn))); - QVERIFY(output.contains(warningMessage)); + runTest(filename, result); } void TestQmllint::testUnqualified_data() { QTest::addColumn<QString>("filename"); - QTest::addColumn<QString>("warningMessage"); - QTest::addColumn<int>("warningLine"); - QTest::addColumn<int>("warningColumn"); + QTest::addColumn<Result>("result"); - // check for false positive due to and warning about with statement - QTest::newRow("WithStatement") << QStringLiteral("WithStatement.qml") << QStringLiteral("with statements are strongly discouraged") << 10 << 25; // 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; + QTest::newRow("IdFromOuterSpace") + << QStringLiteral("IdFromOuterSpace.qml") + << Result { { Message { QStringLiteral("Unqualified access"), 4, 8 }, + Message { QStringLiteral("Unqualified access"), 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 + QTest::newRow("FromRootDirect") + << QStringLiteral("FromRoot.qml") + << Result { + { + Message { QStringLiteral("Unqualified access"), 9, 16 }, // new property + Message { QStringLiteral("Unqualified access"), 13, + 33 } // builtin property + }, + {}, + { { Message { u"root."_s, 9, 16 } }, { Message { u"root."_s, 13, 33 } } } + }; // 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; + QTest::newRow("SignalHandler") + << QStringLiteral("SignalHandler.qml") + << Result { { + Message { QStringLiteral("Unqualified access"), 5, 21 }, + Message { QStringLiteral("Unqualified access"), 10, 21 }, + Message { QStringLiteral("Unqualified access"), 8, 29 }, + Message { QStringLiteral("Unqualified access"), 12, 34 }, + }, + {}, + { + Message { QStringLiteral("function(mouse)"), 4, 22 }, + Message { QStringLiteral("function(mouse)"), 9, 24 }, + Message { QStringLiteral("(mouse) => "), 8, 16 }, + Message { QStringLiteral("(mouse) => "), 12, 21 }, + } }; // 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: <id>.parent.x") << 6 << 25; + QTest::newRow("CatchStatement") + << QStringLiteral("CatchStatement.qml") + << Result { { Message { QStringLiteral("Unqualified access"), 6, 21 } } }; + QTest::newRow("NonSpuriousParent") + << QStringLiteral("nonSpuriousParentWarning.qml") + << Result { { + Message { QStringLiteral("Unqualified access"), 6, 25 }, + }, + {}, + { { Message { u"<id>."_s, 6, 25 } } } }; QTest::newRow("crashConnections") - << QStringLiteral("crashConnections.qml") - << QStringLiteral("target: FirstRunDialog") << 4 << 13; + << QStringLiteral("crashConnections.qml") + << Result { { Message { QStringLiteral("Unqualified access"), 4, 13 } } }; + + QTest::newRow("delegateContextProperties") + << QStringLiteral("delegateContextProperties.qml") + << Result { { Message { QStringLiteral("Unqualified access"), 6, 14 }, + Message { QStringLiteral("Unqualified access"), 7, 15 }, + Message { QStringLiteral("model is implicitly injected into this " + "delegate. Add a required property instead.") }, + Message { + QStringLiteral("index is implicitly injected into this delegate. " + "Add a required property instead.") } } }; + QTest::newRow("storeSloppy") + << QStringLiteral("UnqualifiedInStoreSloppy.qml") + << Result{ { Message{ QStringLiteral("Unqualified access"), 9, 26} } }; + QTest::newRow("storeStrict") + << QStringLiteral("UnqualifiedInStoreStrict.qml") + << Result{ { Message{ QStringLiteral("Unqualified access"), 9, 52} } }; } void TestQmllint::testUnknownCausesFail() { - const QString unknownNotFound = runQmllint("unknownElement.qml", false); - QVERIFY(unknownNotFound.contains( - QStringLiteral("Warning: Unknown was not found. Did you add all import paths?"))); + 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() { - const QStringList iArg = QStringList() << QStringLiteral("-i") << dataDirectory(); - const QString errorMessages = runQmllint("unknownElement.qml", false, iArg); - const QString expectedError = QStringLiteral("Warning: QML types file cannot be a directory: ") + dataDirectory(); - QVERIFY2(errorMessages.contains(expectedError), qPrintable(QString::fromLatin1( - "Expected error to contain \"%1\", but it didn't: %2").arg(expectedError, errorMessages))); + runTest("unknownElement.qml", + Result { { Message { QStringLiteral("QML types file cannot be a directory: ") + + dataDirectory() } } }, + {}, { dataDirectory() }); } void TestQmllint::oldQmltypes() { - const QString errors = runQmllint("oldQmltypes.qml", true); - QVERIFY(errors.contains(QStringLiteral("Warning: typeinfo not declared in qmldir file"))); - QVERIFY(!errors.contains(QStringLiteral("Warning: QQuickItem was not found. Did you add all import paths?"))); - QVERIFY(errors.contains(QStringLiteral("Warning: Found deprecated dependency specifications"))); - - // Checking for both lines separately so that we don't have to mess with the line endings.b - QVERIFY(errors.contains(QStringLiteral("Meta object revision and export version differ, ignoring the revision."))); - QVERIFY(errors.contains(QStringLiteral("Revision 0 corresponds to version 0.0; it should be 1.0."))); + 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() @@ -162,14 +330,80 @@ void TestQmllint::qmltypes_data() QDirIterator it(importsPath, { "*.qmltypes" }, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) - QTest::addRow("%s", qPrintable(it.next().mid(importsPath.length()))) << it.filePath(); + QTest::addRow("%s", qPrintable(it.next().mid(importsPath.size()))) << it.filePath(); } void TestQmllint::qmltypes() { QFETCH(QString, file); - runQmllint(file, true); + // pass the warnings in, so that callQmllint() would show errors if any + QJsonArray warnings; + callQmllint(file, true, &warnings); +} + +#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 | QIODevice::Text)); + currentJsRootContent = QString::fromUtf8(currentJsRoot.readAll()); + currentJsRoot.close(); + + QFile generatedJsRoot(dir.path() + QDir::separator() + "jsroot.qmltypes"); + QVERIFY(generatedJsRoot.open(QFile::ReadOnly | QIODevice::Text)); + 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.size(), generatedLines.size()); + + for (qsizetype i = 0; i < currentLines.size(); i++) { + QCOMPARE(currentLines[i], generatedLines[i]); + } } +#endif void TestQmllint::autoqmltypes() { @@ -180,176 +414,785 @@ void TestQmllint::autoqmltypes() process.waitForFinished(); QCOMPARE(process.exitStatus(), QProcess::NormalExit); - QCOMPARE(process.exitCode(), 0); + QVERIFY(process.exitCode() != 0); - QVERIFY(process.readAllStandardError().isEmpty()); + QVERIFY(process.readAllStandardError() + .contains("is not a qmldir file. Assuming qmltypes")); QVERIFY(process.readAllStandardOutput().isEmpty()); + + { + QProcess bare; + bare.setWorkingDirectory(testFile("autoqmltypes")); + bare.start(m_qmllintPath, { QStringLiteral("--bare"), QStringLiteral("test.qml") }); + bare.waitForFinished(); + + const QByteArray errors = bare.readAllStandardError(); + QVERIFY(!errors.contains("is not a qmldir file. Assuming qmltypes")); + QVERIFY(errors.contains("Failed to import TestTest.")); + QVERIFY(bare.readAllStandardOutput().isEmpty()); + + QCOMPARE(bare.exitStatus(), QProcess::NormalExit); + QVERIFY(bare.exitCode() != 0); + } } void TestQmllint::resources() { - runQmllint(testFile("resource.qml"), true, - {QStringLiteral("--resource"), testFile("resource.qrc")}); - runQmllint(testFile("badResource.qml"), false, - {QStringLiteral("--resource"), testFile("resource.qrc")}); - runQmllint(testFile("resource.qml"), false, {}); - runQmllint(testFile("badResource.qml"), true, {}); + { + // We need to clear the import cache before we add a qrc file with different + // contents for the same paths. + const auto guard = qScopeGuard([this]() { m_linter.clearCache(); }); + + 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); + + { + const auto guard = qScopeGuard([this]() { m_linter.clearCache(); }); + callQmllint(testFile("T/b.qml"), true, nullptr, {}, {}, { testFile("T/a.qrc") }); + } + + { + const auto guard = qScopeGuard([this]() { m_linter.clearCache(); }); + callQmllint(testFile("relPathQrc/Foo/Thing.qml"), true, nullptr, {}, {}, + { testFile("relPathQrc/resources.qrc") }); + } +} + +void TestQmllint::multiDirectory() +{ + callQmllint( + testFile("MultiDirectory/qml/Inner.qml"), true, nullptr, + {}, {}, { testFile("MultiDirectory/multi.qrc") }); + + callQmllint( + testFile("MultiDirectory/qml/pages/Page.qml"), true, nullptr, + {}, {}, { testFile("MultiDirectory/multi.qrc") }); } void TestQmllint::dirtyQmlCode_data() { QTest::addColumn<QString>("filename"); - QTest::addColumn<QString>("warningMessage"); - QTest::addColumn<QString>("notContained"); + QTest::addColumn<Result>("result"); QTest::newRow("Invalid_syntax_QML") << QStringLiteral("failure1.qml") - << QStringLiteral("%1:4 : Expected token `:'") - << QString(); - QTest::newRow("Invalid_syntax_JS") - << QStringLiteral("failure1.js") - << QStringLiteral("%1:4 : Expected token `;'") - << QString(); + << 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") - << QString("Warning: unqualified access at %1:12:36") - << QStringLiteral("no matching signal found"); + << 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") - << QString("Warning: Property \"foo\" not found on type \"QtObject\" at %1:6:31") - << QString(); + << Result { { Message { + QStringLiteral("Member \"foo\" not found on type \"QtObject\""), 6, + 31 } } }; QTest::newRow("UnknownJavascriptMethd") << QStringLiteral("unknownJavascriptMethod.qml") - << QString("Warning: Property \"foo2\" not found on type \"Methods\" at %1:5:25") - << QString(); + << Result { { Message { + QStringLiteral("Member \"foo2\" not found on type \"Methods\""), 5, + 25 } } }; QTest::newRow("badAlias") << QStringLiteral("badAlias.qml") - << QString("Warning: unqualified access at %1:4:27") - << QString(); - QTest::newRow("badAliasProperty") + << Result { { Message { QStringLiteral("Cannot resolve alias \"wrong\""), 3, 1 } } }; + QTest::newRow("badAliasProperty1") << QStringLiteral("badAliasProperty.qml") - << QString("Warning: Property \"nowhere\" not found on type \"QtObject\" at %1:5:32") - << QString(); + << 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("badAliasNotAnExpression") + << QStringLiteral("badAliasNotAnExpression.qml") + << Result { { Message { + QStringLiteral("Invalid alias expression. Only IDs and field member " + "expressions can be aliased"), + 4, 30 } } }; + 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("invalidAliasTarget1") << QStringLiteral("invalidAliasTarget.qml") + << Result { { Message { + QStringLiteral("Invalid alias expression – an initalizer is needed."), + 6, 18 } } }; + QTest::newRow("invalidAliasTarget2") << QStringLiteral("invalidAliasTarget.qml") + << Result { { Message { + QStringLiteral("Invalid alias expression. Only IDs and field member expressions can be aliased"), + 7, 30 } } }; + QTest::newRow("invalidAliasTarget3") << QStringLiteral("invalidAliasTarget.qml") + << Result { { Message { + QStringLiteral("Invalid alias expression. Only IDs and field member expressions can be aliased"), + 9, 34 } } }; QTest::newRow("badParent") << QStringLiteral("badParent.qml") - << QString("Warning: Property \"rrr\" not found on type \"Item\" at %1:5:34") - << QString(); + << Result { { Message { QStringLiteral("Member \"rrr\" not found on type \"Item\""), + 5, 34 } } }; QTest::newRow("parentIsComponent") << QStringLiteral("parentIsComponent.qml") - << QString("Warning: Property \"progress\" not found on type \"QQuickItem\" at %1:7:39") - << QString(); + << Result { { Message { + QStringLiteral("Member \"progress\" not found on type \"QQuickItem\""), 7, + 39 } } }; QTest::newRow("badTypeAssertion") << QStringLiteral("badTypeAssertion.qml") - << QString("Warning: Property \"rrr\" not found on type \"Item\" at %1:5:39") - << QString(); + << Result { { Message { + QStringLiteral("Member \"rrr\" not found on type \"QQuickItem\""), 5, + 39 } } }; QTest::newRow("incompleteQmltypes") << QStringLiteral("incompleteQmltypes.qml") - << QString("Warning: Type \"QPalette\" of base \"palette\" not found when accessing member \"weDontKnowIt\" at %1:5:34") - << QString(); - QTest::newRow("inheritanceCylce") + << Result { { Message { + QStringLiteral("Type \"QPalette\" of property \"palette\" not found"), 5, + 26 } } }; + QTest::newRow("incompleteQmltypes2") + << QStringLiteral("incompleteQmltypes2.qml") + << Result { { Message { QStringLiteral("Member \"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") - << QString("Warning: Cycle2 is part of an inheritance cycle: Cycle2 -> Cycle3 -> Cycle1 -> Cycle2") - << QString(); + << Result { { Message { + QStringLiteral("Cycle1 is part of an inheritance cycle: Cycle2 -> Cycle3 " + "-> Cycle1 -> Cycle2"), + 2, 1 } } }; QTest::newRow("badQmldirImportAndDepend") << QStringLiteral("qmldirImportAndDepend/bad.qml") - << QString("Warning: Item was not found. Did you add all import paths?") - << QString(); + << Result { { Message { + QStringLiteral("Item was not found. Did you add all import paths?"), 3, + 1 } } }; QTest::newRow("javascriptMethodsInModule") << QStringLiteral("javascriptMethodsInModuleBad.qml") - << QString("Warning: Property \"unknownFunc\" not found on type \"Foo\"") - << QString(); + << Result { { Message { + QStringLiteral("Member \"unknownFunc\" not found on type \"Foo\""), 5, + 21 } } }; QTest::newRow("badEnumFromQtQml") << QStringLiteral("badEnumFromQtQml.qml") - << QString("Warning: Property \"Linear123\" not found on type \"QQmlEasingEnums\"") - << QString(); + << Result { { Message { QStringLiteral("Member \"Linear123\" not " + "found on type \"QQmlEasingEnums\""), + 4, 30 } } }; QTest::newRow("anchors3") << QStringLiteral("anchors3.qml") - << QString() - << QString(); - QTest::newRow("nanchors1") - << QStringLiteral("nanchors1.qml") - << QString() - << QString(); - QTest::newRow("nanchors2") - << QStringLiteral("nanchors2.qml") - << QString("unknown grouped property scope nanchors.") - << QString(); - QTest::newRow("nanchors3") - << QStringLiteral("nanchors3.qml") - << QString("unknown grouped property scope nanchors.") - << QString(); + << 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") - << QString("Warning: Property \"wrongwrongwrong\" not found on type \"QtObject\"") - << QString(); - QTest::newRow("badScript") - << QStringLiteral("badScript.qml") - << QString("Warning: Property \"stuff\" not found on type \"Empty\"") - << QString(); + << Result { { Message { QStringLiteral("Member \"wrongwrongwrong\" not " + "found on type \"QtObject\""), + 8, 40 } } }; + QTest::newRow("badScript") << QStringLiteral("badScript.qml") + << Result { { Message { + QStringLiteral( + "Member \"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") - << QString("Warning: type not found in namespace at %1:4:17") - << QString(); - // TODO: This fails but currently for the wrong reasons, make sure to add a warning message requirement - // once it does fail properly in order to avoid regressions. + << Result { { Message { QStringLiteral("Type not found in namespace"), 4, 19 } } }; QTest::newRow("segFault (bad)") << QStringLiteral("SegFault.bad.qml") - << QString() - << QString(); + << Result { { Message { QStringLiteral( + "Member \"foobar\" not found on type \"QQuickScreenAttached\"") } } }; QTest::newRow("VariableUsedBeforeDeclaration") << QStringLiteral("useBeforeDeclaration.qml") - << QStringLiteral("Variable \"argq\" is used before its declaration at 5:9. " - "The declaration is at 6:13.") - << QString(); + << 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") - << QStringLiteral("Parameter 1 to signal handler for \"onSig\" is called \"argarg\". " - "The signal has a parameter of the same name in position 2.") - << QStringLiteral("onSig2"); + << 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") - << QStringLiteral("Signal handler for \"onSig\" has more formal parameters " - "than the signal it handles.") - << QString(); - QTest::newRow("OnAssignment") - << QStringLiteral("onAssignment.qml") - << QStringLiteral("Property \"loops\" not found on type \"bool\"") - << QString(); - QTest::newRow("BadAttached") - << QStringLiteral("badAttached.qml") - << QStringLiteral("unknown attached property scope WrongAttached.") - << QString(); - QTest::newRow("BadBinding") - << QStringLiteral("badBinding.qml") - << QStringLiteral("Binding assigned to \"doesNotExist\", but no property " - "\"doesNotExist\" exists in the current element.") - << QString(); + << 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( + "Member \"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 literal of type string to int") } } }; + QTest::newRow("bad constant number to string") + << QStringLiteral("numberToStringProperty.qml") + << Result { { Message { QStringLiteral( + "Cannot assign literal of type double to QString") } } }; + QTest::newRow("bad unary minus to string") + << QStringLiteral("unaryMinusToStringProperty.qml") + << Result { { Message { QStringLiteral( + "Cannot assign literal of type double to QString") } } }; + 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 literal 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") - << QStringLiteral("No type found for property \"bad\". This may be due to a missing " - "import statement or incomplete qmltypes files.") - << QString(); + << 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("DefaultPropertyLookupInUnknownType") + << QStringLiteral("unknownParentDefaultPropertyCheck.qml") + << Result { { Message { QStringLiteral( + "Alien was not found. Did you add all import paths?") } } }; + 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"), 1, 1, QtInfoMsg } }, + {}, + {}, + Result::ExitsNormally }; + QTest::newRow("Unused Import (prefix)") + << QStringLiteral("unused_prefix.qml") + << Result { { Message { QStringLiteral("Unused import"), 1, 1, QtInfoMsg } }, + {}, + {}, + Result::ExitsNormally }; + QTest::newRow("TypePropertAccess") << QStringLiteral("typePropertyAccess.qml") << Result {}; + QTest::newRow("badAttachedProperty") + << QStringLiteral("badAttachedProperty.qml") + << Result { { Message { + QStringLiteral("Member \"progress\" not found on type \"TestType\"") } } }; + QTest::newRow("badAttachedPropertyNested") + << QStringLiteral("badAttachedPropertyNested.qml") + << Result { { Message { QStringLiteral( + "Member \"progress\" not found on type \"QObject\""), + 12, 41 } }, + { Message { QString("Member \"progress\" not found on type \"QObject\""), + 6, 37 } } }; + QTest::newRow("badAttachedPropertyTypeString") + << QStringLiteral("badAttachedPropertyTypeString.qml") + << Result { { Message { + QStringLiteral("Cannot assign literal 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 "), 0, 0, + QtCriticalMsg } } }; + + 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("Invalid_id_blockstatement") + << QStringLiteral("invalidId2.qml") + << Result { { Message { QStringLiteral("id must be followed by an identifier") } } }; + QTest::newRow("multilineString") + << QStringLiteral("multilineString.qml") + << Result { { Message { QStringLiteral("String contains unescaped line terminator " + "which is deprecated."), + 0, 0, QtInfoMsg } }, + {}, + { Message { "`Foo\nmultiline\\`\nstring`", 4, 32 }, + Message { "`another\\`\npart\nof it`", 6, 11 }, + Message { R"(` +quote: " \\" \\\\" +ticks: \` \` \\\` \\\` +singleTicks: ' \' \\' \\\' +expression: \${expr} \${expr} \\\${expr} \\\${expr}`)", + 10, 28 }, + Message { + R"(` +quote: " \" \\" \\\" +ticks: \` \` \\\` \\\` +singleTicks: ' \\' \\\\' +expression: \${expr} \${expr} \\\${expr} \\\${expr}`)", + 16, 27 } }, + { Result::ExitsNormally, Result::AutoFixable } }; + QTest::addRow("multifix") + << QStringLiteral("multifix.qml") + << Result { { + Message { QStringLiteral("Unqualified access"), 7, 19, QtWarningMsg}, + Message { QStringLiteral("Unqualified access"), 11, 19, QtWarningMsg}, + }, {}, { + Message { QStringLiteral("pragma ComponentBehavior: Bound\n"), 1, 1 } + }, { Result::AutoFixable }}; + 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\" does not exist") } } }; + QTest::newRow("QtQuick.Window 2.0") + << QStringLiteral("qtquickWindow20.qml") + << Result { { Message { QStringLiteral( + "Member \"window\" not found on type \"QQuickWindow\"") } } }; + QTest::newRow("unresolvedAttachedType") + << QStringLiteral("unresolvedAttachedType.qml") + << Result { { Message { QStringLiteral( + "unknown attached property scope UnresolvedAttachedType.") } }, + { Message { QStringLiteral("Property \"property\" does not exist") } } }; + QTest::newRow("nestedInlineComponents") + << QStringLiteral("nestedInlineComponents.qml") + << Result { { Message { + QStringLiteral("Nested inline components are not supported") } } }; + QTest::newRow("inlineComponentNoComponent") + << QStringLiteral("inlineComponentNoComponent.qml") + << Result { { Message { + QStringLiteral("Inline component declaration must be followed by a typename"), + 3, 2 } } }; + QTest::newRow("WithStatement") << QStringLiteral("WithStatement.qml") + << Result { { Message { QStringLiteral( + "with statements are strongly discouraged") } } }; + QTest::newRow("BadLiteralBinding") + << QStringLiteral("badLiteralBinding.qml") + << Result { { Message { + QStringLiteral("Cannot assign literal 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 access singleton as a property of an object") } } }; + QTest::newRow("BadModulePrefix2") + << QStringLiteral("badModulePrefix2.qml") + << Result { { Message { QStringLiteral( + "Cannot use a non-QObject type QRectF to access prefixed import") } } }; + 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("cachedDependency") + << QStringLiteral("cachedDependency.qml") + << Result { { Message { QStringLiteral("Unused import"), 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("Member \"red\" not found on type \"QtObject\"") } } }; + QTest::newRow("inaccessibleId") + << QStringLiteral("inaccessibleId.qml") + << Result { { Message { + QStringLiteral("Member \"objectName\" not found on type \"int\"") } } }; + QTest::newRow("inaccessibleId2") + << QStringLiteral("inaccessibleId2.qml") + << Result { { Message { + QStringLiteral("Member \"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( + "Member \"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( + "Member \"hoight\" not found on type \"Rectangle\"") }, + {}, + { Message { QStringLiteral("height") } } } }; + QTest::newRow("didYouMean(propertyCall)") + << QStringLiteral("didYouMeanPropertyCall.qml") + << Result { + { Message { QStringLiteral("Member \"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("didYouMean(enum)") + << QStringLiteral("didYouMeanEnum.qml") + << Result { { Message { QStringLiteral( + "Member \"Readx\" not found on type \"QQuickImage\"") }, + {}, + { Message { QStringLiteral("Ready") } } } }; + QTest::newRow("nullBinding") << QStringLiteral("nullBinding.qml") + << Result{ { Message{ QStringLiteral( + "Cannot assign literal of type null 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 \"gradient\" is a QJSValue property. It may or may not be " + "a method. Use a regular Q_INVOKABLE instead.") } } }; + QTest::newRow("assignNonExistingTypeToVarProp") + << QStringLiteral("assignNonExistingTypeToVarProp.qml") + << Result { { Message { QStringLiteral( + "NonExistingType was not found. Did you add all import paths?") } } }; + QTest::newRow("unboundComponents") + << QStringLiteral("unboundComponents.qml") + << Result { { + Message { QStringLiteral("Unqualified access"), 10, 25 }, + Message { QStringLiteral("Unqualified access"), 14, 33 } + } }; + QTest::newRow("badlyBoundComponents") + << QStringLiteral("badlyBoundComponents.qml") + << Result{ { Message{ QStringLiteral("Unqualified access"), 18, 36 } } }; + QTest::newRow("NotScopedEnumCpp") + << QStringLiteral("NotScopedEnumCpp.qml") + << Result{ { Message{ + QStringLiteral("You cannot access unscoped enum \"TheEnum\" from here."), 5, + 49 } } }; + + QTest::newRow("unresolvedArrayBinding") + << QStringLiteral("unresolvedArrayBinding.qml") + << Result{ { Message{ QStringLiteral(u"Declaring an object which is not an Qml object" + " as a list member.") } } }; + QTest::newRow("duplicatedPropertyName") + << QStringLiteral("duplicatedPropertyName.qml") + << Result{ { Message{ QStringLiteral("Duplicated property name \"cat\"."), 5, 5 } } }; + QTest::newRow("duplicatedSignalName") + << QStringLiteral("duplicatedPropertyName.qml") + << Result{ { Message{ QStringLiteral("Duplicated signal name \"clicked\"."), 8, 5 } } }; + QTest::newRow("missingComponentBehaviorBound") + << QStringLiteral("missingComponentBehaviorBound.qml") + << Result { + { Message{ QStringLiteral("Unqualified access"), 8, 31 } }, + {}, + { Message{ QStringLiteral("Set \"pragma ComponentBehavior: Bound\" in " + "order to use IDs from outer components " + "in nested components."), 0, 0, QtInfoMsg } }, + Result::AutoFixable + }; + QTest::newRow("IsNotAnEntryOfEnum") + << QStringLiteral("IsNotAnEntryOfEnum.qml") + << Result{ { + Message { + QStringLiteral("Member \"Mode\" not found on type \"Item\""), 12, + 29, QtWarningMsg }, + Message{ + QStringLiteral("\"Hour\" is not an entry of enum \"Mode\"."), 13, + 62, QtInfoMsg } + }, + {}, + { Message{ QStringLiteral("Hours") } } + }; + + QTest::newRow("StoreNameMethod") + << QStringLiteral("storeNameMethod.qml") + << Result { { Message { QStringLiteral("Cannot assign to method foo") } } }; + + QTest::newRow("CoerceToVoid") + << QStringLiteral("coercetovoid.qml") + << Result { { Message { + QStringLiteral("Function without return type annotation returns double") + } } }; + + QTest::newRow("lowerCaseQualifiedImport") + << QStringLiteral("lowerCaseQualifiedImport.qml") + << Result{ { + Message{ u"Import qualifier 'test' must start with a capital letter."_s }, + Message{ + u"Namespace 'test' of 'test.Rectangle' must start with an upper case letter."_s }, + } }; + QTest::newRow("lowerCaseQualifiedImport2") + << QStringLiteral("lowerCaseQualifiedImport2.qml") + << Result{ { + Message{ u"Import qualifier 'test' must start with a capital letter."_s }, + Message{ + u"Namespace 'test' of 'test.Item' must start with an upper case letter."_s }, + Message{ + u"Namespace 'test' of 'test.Rectangle' must start with an upper case letter."_s }, + Message{ + u"Namespace 'test' of 'test.color' must start with an upper case letter."_s }, + Message{ + u"Namespace 'test' of 'test.Grid' must start with an upper case letter."_s }, + } }; + QTest::newRow("notQmlRootMethods") + << QStringLiteral("notQmlRootMethods.qml") + << Result{ { + Message{ u"Member \"deleteLater\" not found on type \"QtObject\""_s }, + Message{ u"Member \"destroyed\" not found on type \"QtObject\""_s }, + } }; } void TestQmllint::dirtyQmlCode() { QFETCH(QString, filename); - QFETCH(QString, warningMessage); - QFETCH(QString, notContained); - if (warningMessage.contains(QLatin1String("%1"))) - warningMessage = warningMessage.arg(testFile(filename)); - - const QString output = runQmllint(filename, [&](QProcess &process) { - QVERIFY(process.waitForFinished()); - QCOMPARE(process.exitStatus(), QProcess::NormalExit); - QEXPECT_FAIL("anchors3", "We don't see that QQuickItem cannot be assigned to QQuickAnchorLine", Abort); - QEXPECT_FAIL("nanchors1", "Invalid grouped properties are not always detected", Abort); - QVERIFY(process.exitCode() != 0); - }); + QFETCH(Result, result); - QVERIFY(output.contains(warningMessage)); - if (!notContained.isEmpty()) - QVERIFY(!output.contains(notContained)); + 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, {}, {}, {}, + UseDefaultImports, nullptr, result.flags.testFlag(Result::Flag::AutoFixable)); + + 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() @@ -385,7 +1228,7 @@ void TestQmllint::cleanQmlCode_data() QTest::newRow("enumFromQtQml") << QStringLiteral("enumFromQtQml.qml"); QTest::newRow("anchors1") << QStringLiteral("anchors1.qml"); QTest::newRow("anchors2") << QStringLiteral("anchors2.qml"); - QTest::newRow("optionalImport") << QStringLiteral("optionalImport.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"); @@ -398,40 +1241,254 @@ void TestQmllint::cleanQmlCode_data() 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("qmlRootMethods") << QStringLiteral("qmlRootMethods.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"); + QTest::newRow("boundComponents") << QStringLiteral("boundComponents.qml"); + QTest::newRow("prefixedAttachedProperty") << QStringLiteral("prefixedAttachedProperty.qml"); + QTest::newRow("callLater") << QStringLiteral("callLater.qml"); + QTest::newRow("listPropertyMethods") << QStringLiteral("listPropertyMethods.qml"); + QTest::newRow("v4SequenceMethods") << QStringLiteral("v4SequenceMethods.qml"); + QTest::newRow("stringToByteArray") << QStringLiteral("stringToByteArray.qml"); + QTest::newRow("jsLibrary") << QStringLiteral("jsLibrary.qml"); + QTest::newRow("nullBindingFunction") << QStringLiteral("nullBindingFunction.qml"); + QTest::newRow("BindingTypeMismatchFunction") << QStringLiteral("bindingTypeMismatchFunction.qml"); + QTest::newRow("BindingTypeMismatch") << QStringLiteral("bindingTypeMismatch.qml"); + QTest::newRow("template literal (substitution)") << QStringLiteral("templateStringSubstitution.qml"); + QTest::newRow("enumsOfScrollBar") << QStringLiteral("enumsOfScrollBar.qml"); + QTest::newRow("optionalChainingCall") << QStringLiteral("optionalChainingCall.qml"); + QTest::newRow("EnumAccessCpp") << QStringLiteral("EnumAccessCpp.qml"); + QTest::newRow("qtquickdialog") << QStringLiteral("qtquickdialog.qml"); + QTest::newRow("callBase") << QStringLiteral("callBase.qml"); + QTest::newRow("propertyWithOn") << QStringLiteral("switcher.qml"); + QTest::newRow("constructorProperty") << QStringLiteral("constructorProperty.qml"); + QTest::newRow("onlyMajorVersion") << QStringLiteral("onlyMajorVersion.qml"); + QTest::newRow("attachedImportUse") << QStringLiteral("attachedImportUse.qml"); + QTest::newRow("VariantMapGetPropertyLookup") << QStringLiteral("variantMapLookup.qml"); + QTest::newRow("StringToDateTime") << QStringLiteral("stringToDateTime.qml"); + QTest::newRow("ScriptInTemplate") << QStringLiteral("scriptInTemplate.qml"); + QTest::newRow("AddressableValue") << QStringLiteral("addressableValue.qml"); + QTest::newRow("WriteListProperty") << QStringLiteral("writeListProperty.qml"); + QTest::newRow("dontConfuseMemberPrintWithGlobalPrint") << QStringLiteral("findMemberPrint.qml"); + QTest::newRow("groupedAttachedLayout") << QStringLiteral("groupedAttachedLayout.qml"); + QTest::newRow("QQmlScriptString") << QStringLiteral("scriptstring.qml"); + QTest::newRow("QEventPoint") << QStringLiteral("qEventPoint.qml"); + QTest::newRow("locale") << QStringLiteral("locale.qml"); + QTest::newRow("constInvokable") << QStringLiteral("useConstInvokable.qml"); + QTest::newRow("dontCheckJSTypes") << QStringLiteral("dontCheckJSTypes.qml"); } void TestQmllint::cleanQmlCode() { QFETCH(QString, filename); - const QString warnings = runQmllint(filename, true); - QEXPECT_FAIL("segFault", "This property exists and should not produce a warning", Abort); - QVERIFY2(warnings.isEmpty(), qPrintable(warnings)); + + QJsonArray warnings; + + runTest(filename, Result::clean()); +} + +void TestQmllint::compilerWarnings_data() +{ + QTest::addColumn<QString>("filename"); + QTest::addColumn<Result>("result"); + QTest::addColumn<bool>("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; + QTest::newRow("generalizedGroupHint") + << QStringLiteral("generalizedGroupHint.qml") + << Result { { { QStringLiteral( + "Cannot resolve property type for binding on myColor. " + "You may want use ID-based grouped properties here.") } } } + << true; + QTest::newRow("invalidIdLookup") + << QStringLiteral("invalidIdLookup.qml") + << Result { { { + QStringLiteral("Cannot retrieve a non-object type by ID: stateMachine") + } } } + << true; +} + +void TestQmllint::compilerWarnings() +{ + QFETCH(QString, filename); + QFETCH(Result, result); + QFETCH(bool, enableCompilerWarnings); + + QJsonArray warnings; + + auto categories = QQmlJSLogger::defaultCategories(); + + auto category = std::find_if(categories.begin(), categories.end(), [](const QQmlJS::LoggerCategory& category) { + return category.id() == qmlCompiler; + }); + Q_ASSERT(category != categories.end()); + + if (enableCompilerWarnings) { + category->setLevel(QtWarningMsg); + category->setIgnored(false); + } + + runTest(filename, result, {}, {}, {}, UseDefaultImports, &categories); } QString TestQmllint::runQmllint(const QString &fileToLint, std::function<void(QProcess &)> handleResult, - const QStringList &extraArgs) + const QStringList &extraArgs, bool ignoreSettings, + bool addImportDirs, bool absolutePath, const Environment &env) { auto qmlImportDir = QLibraryInfo::path(QLibraryInfo::QmlImportsPath); QStringList args; - args << (QFileInfo(fileToLint).isAbsolute() ? fileToLint : testFile(fileToLint)) - << QStringLiteral("-I") << qmlImportDir - << QStringLiteral("-I") << dataDirectory(); + 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; + QProcessEnvironment processEnv = QProcessEnvironment::systemEnvironment(); + for (const auto &entry : env) + processEnv.insert(entry.first, entry.second); + + process.setProcessEnvironment(processEnv); + process.setWorkingDirectory(QFileInfo(absoluteFilePath).absolutePath()); process.start(m_qmllintPath, args); handleResult(process); errors = process.readAllStandardError(); - if (isSilent) - QVERIFY(errors.isEmpty()); + 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() << "Command:" << process.program() << args.join(u' '); + qDebug().noquote() << "Command:" << process.program() << args.join(u' '); qDebug() << "Exit status:" << process.exitStatus(); qDebug() << "Exit code:" << process.exitCode(); qDebug() << "stderr:" << errors; @@ -444,28 +1501,722 @@ QString TestQmllint::runQmllint(const QString &fileToLint, return errors; } -QString TestQmllint::runQmllint(const QString &fileToLint, bool shouldSucceed, const QStringList &extraArgs) +QString TestQmllint::runQmllint(const QString &fileToLint, bool shouldSucceed, + const QStringList &extraArgs, bool ignoreSettings, + bool addImportDirs, bool absolutePath, const Environment &env) { - return runQmllint(fileToLint, [&](QProcess &process) { - QVERIFY(process.waitForFinished()); - QCOMPARE(process.exitStatus(), QProcess::NormalExit); + return runQmllint( + fileToLint, + [&](QProcess &process) { + QVERIFY(process.waitForFinished()); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); - QEXPECT_FAIL("segFault", "This property exists and should not produce a warning", Abort); - if (shouldSucceed) - QCOMPARE(process.exitCode(), 0); - else - QVERIFY(process.exitCode() != 0); - }, extraArgs); + if (shouldSucceed) + QCOMPARE(process.exitCode(), 0); + else + QVERIFY(process.exitCode() != 0); + }, + extraArgs, ignoreSettings, addImportDirs, absolutePath, env); +} + +void TestQmllint::callQmllint(const QString &fileToLint, bool shouldSucceed, QJsonArray *warnings, + QStringList importPaths, QStringList qmldirFiles, + QStringList resources, DefaultImportOption defaultImports, + QList<QQmlJS::LoggerCategory> *categories, bool autoFixable, + LintType type) +{ + QJsonArray jsonOutput; + + const QFileInfo info = QFileInfo(fileToLint); + const QString lintedFile = info.isAbsolute() ? fileToLint : testFile(fileToLint); + + QQmlJSLinter::LintResult lintResult; + + const QStringList resolvedImportPaths = defaultImports == UseDefaultImports + ? m_defaultImportPaths + importPaths + : importPaths; + if (type == LintFile) { + const QList<QQmlJS::LoggerCategory> resolvedCategories = + categories != nullptr ? *categories : QQmlJSLogger::defaultCategories(); + lintResult = m_linter.lintFile( + lintedFile, nullptr, true, &jsonOutput, resolvedImportPaths, qmldirFiles, + resources, resolvedCategories); + } else { + lintResult = + m_linter.lintModule(fileToLint, true, &jsonOutput, resolvedImportPaths, resources); + } + + bool success = lintResult == QQmlJSLinter::LintSuccess; + QEXPECT_FAIL("qtquickdialog", "Will fail until QTBUG-104091 is implemented", Abort); + QVERIFY2(success == shouldSucceed, QJsonDocument(jsonOutput).toJson()); + + if (warnings) { + QVERIFY2(jsonOutput.size() == 1, QJsonDocument(jsonOutput).toJson()); + *warnings = jsonOutput.at(0)[u"warnings"_s].toArray(); + } + + QCOMPARE(success, shouldSucceed); + + if (lintResult == QQmlJSLinter::LintSuccess || lintResult == QQmlJSLinter::HasWarnings) { + QString fixedCode; + QQmlJSLinter::FixResult fixResult = m_linter.applyFixes(&fixedCode, true); + + if (autoFixable) { + QCOMPARE(fixResult, QQmlJSLinter::FixSuccess); + // Check that the fixed version of the file actually passes qmllint now + QTemporaryDir dir; + QVERIFY(dir.isValid()); + QFile file(dir.filePath("Fixed.qml")); + QVERIFY2(file.open(QIODevice::WriteOnly), qPrintable(file.errorString())); + file.write(fixedCode.toUtf8()); + file.flush(); + file.close(); + + callQmllint(QFileInfo(file).absoluteFilePath(), true, nullptr, importPaths, qmldirFiles, + resources, defaultImports, categories, false); + + const QString fixedPath = testFile(info.baseName() + u".fixed.qml"_s); + + if (QFileInfo(fixedPath).exists()) { + QFile fixedFile(fixedPath); + QVERIFY(fixedFile.open(QFile::ReadOnly)); + QString fixedFileContents = QString::fromUtf8(fixedFile.readAll()); +#ifdef Q_OS_WIN + fixedCode = fixedCode.replace(u"\r\n"_s, u"\n"_s); + fixedFileContents = fixedFileContents.replace(u"\r\n"_s, u"\n"_s); +#endif + + QCOMPARE(fixedCode, fixedFileContents); + } + } else { + if (shouldSucceed) + QCOMPARE(fixResult, QQmlJSLinter::NothingToFix); + else + QVERIFY(fixResult == QQmlJSLinter::FixSuccess + || fixResult == QQmlJSLinter::NothingToFix); + } + } +} + +void TestQmllint::runTest(const QString &testFile, const Result &result, QStringList importDirs, + QStringList qmltypesFiles, QStringList resources, + DefaultImportOption defaultImports, + QList<QQmlJS::LoggerCategory> *categories) +{ + QJsonArray warnings; + callQmllint(testFile, result.flags.testFlag(Result::Flag::ExitsNormally), &warnings, importDirs, + qmltypesFiles, resources, defaultImports, categories, + result.flags.testFlag(Result::Flag::AutoFixable)); + checkResult(warnings, result); +} + +template<typename ExpectedMessageFailureHandler, typename BadMessageFailureHandler> +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 QJsonValueConstRef 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 QJsonValueConstRef 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"_s, u"\n"_s); +#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, + quint32 line, quint32 column, bool must = true) { + QString msg = QStringLiteral("qmllint output:\n%1\nIt %2 contain '%3'") + .arg(QString::fromUtf8( + QJsonDocument(warnings).toJson(QJsonDocument::Indented)), + must ? u"must" : u"must NOT", substring); + if (line != 0 || column != 0) + msg += u" (%1:%2)"_s.arg(line).arg(column); + + return msg; + }; + + if (shouldContain == StringContained) { + if (!contains) + qWarning().noquote() << toDescription(warnings, substring, line, column); + QVERIFY(contains); + } else { + if (contains) + qWarning().noquote() << toDescription(warnings, substring, line, column, false); + QVERIFY(!contains); + } } void TestQmllint::requiredProperty() { - QVERIFY(runQmllint("requiredProperty.qml", true).isEmpty()); + 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") + .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() +{ + // We're polluting the resource file system here, so let's clean up afterwards. + const auto guard = qScopeGuard([this]() {m_linter.clearCache(); }); + runTest("additionalImplicitImport.qml", Result::clean(), {}, {}, + { testFile("implicitImportResource.qrc") }); +} + +void TestQmllint::qrcUrlImport() +{ + const auto guard = qScopeGuard([this]() { m_linter.clearCache(); }); + + QJsonArray warnings; + callQmllint(testFile("untitled/main.qml"), true, &warnings, {}, {}, + { testFile("untitled/qrcUrlImport.qrc") }); + checkResult(warnings, Result::clean()); +} + +void TestQmllint::incorrectImportFromHost_data() +{ + QTest::addColumn<QString>("filename"); + QTest::addColumn<Result>("result"); + + QTest::newRow("NonexistentFile") + << QStringLiteral("importNonexistentFile.qml") + << Result{ { Message{ + QStringLiteral("File or directory you are trying to import does not exist"), + 1, 1 } } }; +#ifndef Q_OS_WIN + // there is no /dev/null device on Win + QTest::newRow("NullDevice") + << QStringLiteral("importNullDevice.qml") + << Result{ { Message{ QStringLiteral("is neither a file nor a directory. Are sure the " + "import path is correct?"), + 1, 1 } } }; +#endif +} + +void TestQmllint::incorrectImportFromHost() +{ + QFETCH(QString, filename); + QFETCH(Result, result); - const QString errors = runQmllint("requiredMissingProperty.qml", true); - QVERIFY(errors.contains(QStringLiteral("Property \"foo\" was marked as required but does not exist."))); + runTest(filename, result); } +void TestQmllint::attachedPropertyReuse() +{ + auto categories = QQmlJSLogger::defaultCategories(); + auto category = std::find_if(categories.begin(), categories.end(), [](const QQmlJS::LoggerCategory& category) { + return category.id() == qmlAttachedPropertyReuse; + }); + Q_ASSERT(category != categories.end()); + + category->setLevel(QtWarningMsg); + category->setIgnored(false); + runTest("attachedPropNotReused.qml", + Result { { Message { QStringLiteral("Using attached type QQuickKeyNavigationAttached " + "already initialized in a parent " + "scope") } } }, + {}, {}, {}, UseDefaultImports, &categories); + + runTest("attachedPropEnum.qml", Result::clean(), {}, {}, {}, UseDefaultImports, &categories); + runTest("MyStyle/ToolBar.qml", Result { + { + Message { + "Using attached type MyStyle already initialized in a parent scope"_L1, + 10, + 16 + } + }, + {}, + { + Message { + "Reference it by id instead"_L1, + 10, + 16 + } + }, + Result::AutoFixable + }); +} + +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, {}, {}, {}, {}) + == QQmlJSLinter::LintSuccess; + QVERIFY2(!success, QJsonDocument(jsonOutput).toJson()); + + QVERIFY2(jsonOutput.size() == 1, QJsonDocument(jsonOutput).toJson()); + warnings = jsonOutput.at(0)[u"warnings"_s].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") }); +} + +void TestQmllint::lintModule_data() +{ + QTest::addColumn<QString>("module"); + QTest::addColumn<QStringList>("importPaths"); + QTest::addColumn<QStringList>("resources"); + QTest::addColumn<Result>("result"); + + QTest::addRow("Things") + << u"Things"_s + << QStringList() + << QStringList() + << Result { + { Message { + u"Type \"QPalette\" not found. Used in SomethingEntirelyStrange.palette"_s, + }, + Message { + u"Type \"CustomPalette\" is not fully resolved. Used in SomethingEntirelyStrange.palette2"_s } } + }; + QTest::addRow("missingQmltypes") + << u"Fake5Compat.GraphicalEffects.private"_s + << QStringList() + << QStringList() + << Result { { Message { u"QML types file does not exist"_s } } }; + + QTest::addRow("moduleWithQrc") + << u"moduleWithQrc"_s + << QStringList({ testFile("hidden") }) + << QStringList({ + testFile("hidden/qmake_moduleWithQrc.qrc"), + testFile("hidden/moduleWithQrc_raw_qml_0.qrc") + }) + << Result::clean(); +} + +void TestQmllint::lintModule() +{ + QFETCH(QString, module); + QFETCH(QStringList, importPaths); + QFETCH(QStringList, resources); + QFETCH(Result, result); + + QJsonArray warnings; + callQmllint(module, result.flags & Result::ExitsNormally, &warnings, importPaths, {}, resources, + UseDefaultImports, nullptr, false, LintModule); + checkResult(warnings, result); +} + +void TestQmllint::testLineEndings() +{ + { + const auto textWithLF = QString::fromUtf16(u"import QtQuick 2.0\nimport QtTest 2.0 // qmllint disable unused-imports\n" + "import QtTest 2.0 // qmllint disable\n\nItem {\n @Deprecated {}\n property string deprecated\n\n " + "property string a: root.a // qmllint disable unqualifi77777777777777777777777777777777777777777777777777777" + "777777777777777777777777777777777777ed\n property string b: root.a // qmllint di000000000000000000000000" + "000000000000000000inyyyyyyyyg c: root.a\n property string d: root.a\n // qmllint enable unqualified\n\n " + "//qmllint d 4isable\n property string e: root.a\n Component.onCompleted: {\n console.log" + "(deprecated);\n }\n // qmllint enable\n\n}\n"); + + const auto lintResult = m_linter.lintFile( {}, &textWithLF, true, nullptr, {}, {}, {}, {}); + + QCOMPARE(lintResult, QQmlJSLinter::LintResult::HasWarnings); + } + { + const auto textWithCRLF = QString::fromUtf16(u"import QtQuick 2.0\nimport QtTest 2.0 // qmllint disable unused-imports\n" + "import QtTest 2.0 // qmllint disable\n\nItem {\n @Deprecated {}\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r" + "\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n property string deprecated\n\n property string a: root.a " + "// qmllint disable unqualifi77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777ed\n " + "property string b: root.a // qmllint di000000000000000000000000000000000000000000inyyyyyyyyg c: root.a\n property string d: " + "root.a\n // qmllint enable unqualified\n\n //qmllint d 4isable\n property string e: root.a\n Component.onCompleted: " + "{\n console.log(deprecated);\n }\n // qmllint enable\n\n}\n"); + + const auto lintResult = m_linter.lintFile( {}, &textWithCRLF, true, nullptr, {}, {}, {}, {}); + + QCOMPARE(lintResult, QQmlJSLinter::LintResult::HasWarnings); + } +} + +void TestQmllint::valueTypesFromString() +{ + runTest("valueTypesFromString.qml", + Result{ { + Message{ + u"Binding is not supported: Type QSizeF should be constructed using QML_STRUCTURED_VALUE's construction mechanism, instead of a string."_s }, + Message{ + u"Binding is not supported: Type QRectF should be constructed using QML_STRUCTURED_VALUE's construction mechanism, instead of a string."_s }, + Message{ + u"Binding is not supported: Type QPointF should be constructed using QML_STRUCTURED_VALUE's construction mechanism, instead of a string."_s }, + }, + { /*bad messages */ }, + { + Message{ u"({ width: 30, height: 50 })"_s }, + Message{ u"({ x: 10, y: 20, width: 30, height: 50 })"_s }, + Message{ u"({ x: 30, y: 50 })"_s }, + } }); +} + +#if QT_CONFIG(library) +void TestQmllint::testPlugin() +{ + bool pluginFound = false; + for (const QQmlJSLinter::Plugin &plugin : m_linter.plugins()) { + if (plugin.name() == "testPlugin") { + pluginFound = true; + QCOMPARE(plugin.author(), u"Qt"_s); + QCOMPARE(plugin.description(), u"A test plugin for tst_qmllint"_s); + QCOMPARE(plugin.version(), u"1.0"_s); + break; + } + } + QVERIFY(pluginFound); + + runTest("elementpass_pluginTest.qml", Result { { Message { u"ElementTest OK"_s, 4, 5 } } }); + runTest("propertypass_pluginTest.qml", + Result { + { // Specific binding for specific property + Message { + u"Saw binding on Text property text with value NULL (and type 3) in scope Text"_s }, + + // Property on any type + Message { u"Saw read on Text property x in scope Text"_s }, + Message { + u"Saw binding on Text property x with value NULL (and type 2) in scope Text"_s }, + Message { u"Saw read on Text property x in scope Item"_s }, + Message { u"Saw write on Text property x with value int in scope Item"_s }, + Message { + u"Saw binding on Item property x with value NULL (and type 2) in scope Item"_s }, + // ListModel + Message { + u"Saw binding on ListView property model with value ListModel (and type 8) in scope ListView"_s }, + Message { + u"Saw binding on ListView property height with value NULL (and type 2) in scope ListView"_s } } }); + runTest("controlsWithQuick_pluginTest.qml", + Result { { Message { u"QtQuick.Controls, QtQuick and QtQuick.Window present"_s } } }); + runTest("controlsWithoutQuick_pluginTest.qml", + Result { { Message { u"QtQuick.Controls and NO QtQuick present"_s } } }); + // Verify that none of the passes do anything when they're not supposed to + runTest("nothing_pluginTest.qml", Result::clean()); + + QVERIFY(runQmllint("settings/plugin/elemenpass_pluginSettingTest.qml", true, QStringList(), false) + .isEmpty()); +} + +// TODO: Eventually tests for (real) plugins need to be moved into a separate file +void TestQmllint::quickPlugin() +{ + const auto &plugins = m_linter.plugins(); + + const bool pluginFound = + std::find_if(plugins.cbegin(), plugins.cend(), + [](const auto &plugin) { return plugin.name() == "Quick"; }) + != plugins.cend(); + QVERIFY(pluginFound); + + runTest("pluginQuick_anchors.qml", + Result{ { Message{ + u"Cannot specify left, right, and horizontalCenter anchors at the same time."_s }, + Message { + u"Cannot specify top, bottom, and verticalCenter anchors at the same time."_s }, + Message{ + u"Baseline anchor cannot be used in conjunction with top, bottom, or verticalCenter anchors."_s }, + Message { u"Cannot assign literal of type null to QQuickAnchorLine"_s, 5, + 35 }, + Message { u"Cannot assign literal of type null to QQuickAnchorLine"_s, 6, + 33 } } }); + runTest("pluginQuick_anchorsUndefined.qml", Result::clean()); + runTest("pluginQuick_layoutChildren.qml", + Result { + { Message { + u"Detected anchors on an item that is managed by a layout. This is undefined behavior; use Layout.alignment instead."_s }, + Message { + u"Detected x on an item that is managed by a layout. This is undefined behavior; use Layout.leftMargin or Layout.rightMargin instead."_s }, + Message { + u"Detected y on an item that is managed by a layout. This is undefined behavior; use Layout.topMargin or Layout.bottomMargin instead."_s }, + Message { + u"Detected height on an item that is managed by a layout. This is undefined behavior; use implictHeight or Layout.preferredHeight instead."_s }, + Message { + u"Detected width on an item that is managed by a layout. This is undefined behavior; use implicitWidth or Layout.preferredWidth instead."_s }, + Message { + u"Cannot specify anchors for items inside Grid. Grid will not function."_s }, + Message { + u"Cannot specify x for items inside Grid. Grid will not function."_s }, + Message { + u"Cannot specify y for items inside Grid. Grid will not function."_s }, + Message { + u"Cannot specify anchors for items inside Flow. Flow will not function."_s }, + Message { + u"Cannot specify x for items inside Flow. Flow will not function."_s }, + Message { + u"Cannot specify y for items inside Flow. Flow will not function."_s } } }); + runTest("pluginQuick_attached.qml", + Result { + { Message { u"ToolTip must be attached to an Item"_s }, + Message { u"SplitView attached property only works with Items"_s }, + Message { u"ScrollIndicator must be attached to a Flickable"_s }, + Message { u"ScrollBar must be attached to a Flickable or ScrollView"_s }, + Message { u"Accessible must be attached to an Item"_s }, + Message { u"EnterKey attached property only works with Items"_s }, + Message { + u"LayoutDirection attached property only works with Items and Windows"_s }, + Message { u"Layout must be attached to Item elements"_s }, + Message { u"StackView attached property only works with Items"_s }, + Message { u"TextArea must be attached to a Flickable"_s }, + Message { u"StackLayout must be attached to an Item"_s }, + Message { + u"Tumbler: attached properties of Tumbler must be accessed through a delegate item"_s }, + Message { + u"Attached properties of SwipeDelegate must be accessed through an Item"_s }, + Message { u"SwipeView must be attached to an Item"_s } } }); + + runTest("pluginQuick_swipeDelegate.qml", + Result { { + Message { + u"SwipeDelegate: Cannot use horizontal anchors with contentItem; unable to layout the item."_s, + 6, 43 }, + Message { + u"SwipeDelegate: Cannot use horizontal anchors with background; unable to layout the item."_s, + 7, 43 }, + Message { u"SwipeDelegate: Cannot set both behind and left/right properties"_s, + 9, 9 }, + Message { + u"SwipeDelegate: Cannot use horizontal anchors with contentItem; unable to layout the item."_s, + 13, 47 }, + Message { + u"SwipeDelegate: Cannot use horizontal anchors with background; unable to layout the item."_s, + 14, 42 }, + Message { u"SwipeDelegate: Cannot set both behind and left/right properties"_s, + 16, 9 }, + } }); + + runTest("pluginQuick_varProp.qml", + Result { + { Message { + u"Unexpected type for property \"contentItem\" expected QQuickPathView, QQuickListView got QQuickItem"_s }, + Message { + u"Unexpected type for property \"columnWidthProvider\" expected function got null"_s }, + Message { + u"Unexpected type for property \"textFromValue\" expected function got null"_s }, + Message { + u"Unexpected type for property \"valueFromText\" expected function got int"_s }, + Message { + u"Unexpected type for property \"rowHeightProvider\" expected function got int"_s } } }); + runTest("pluginQuick_varPropClean.qml", Result::clean()); + runTest("pluginQuick_attachedClean.qml", Result::clean()); + runTest("pluginQuick_attachedIgnore.qml", Result::clean()); + runTest("pluginQuick_noCrashOnUneresolved.qml", Result {}); // we don't care about the specific warnings + + runTest("pluginQuick_propertyChangesParsed.qml", + Result { { + Message { + u"Property \"myColor\" is custom-parsed in PropertyChanges. " + "You should phrase this binding as \"foo.myColor: Qt.rgba(0.5, ...\""_s, + 12, 30 + }, + Message { + u"You should remove any bindings on the \"target\" property and avoid " + "custom-parsed bindings in PropertyChanges."_s, + 11, 29 + }, + Message { + u"Unknown property \"notThere\" in PropertyChanges."_s, + 13, 31 + } + } }); + runTest("pluginQuick_propertyChangesInvalidTarget.qml", Result {}); // we don't care about the specific warnings +} + +void TestQmllint::environment_data() +{ + QTest::addColumn<QString>("file"); + QTest::addColumn<bool>("shouldSucceed"); + QTest::addColumn<QStringList>("extraArgs"); + QTest::addColumn<Environment>("env"); + QTest::addColumn<QString>("expectedWarning"); + + const QString fileThatNeedsImportPath = testFile(u"NeedImportPath.qml"_s); + const QString importPath = testFile(u"ImportPath"_s); + const QString invalidImportPath = testFile(u"ImportPathThatDoesNotExist"_s); + const QString noWarningExpected; + + QTest::addRow("missing-import-dir") + << fileThatNeedsImportPath << false << QStringList{} + << Environment{ { u"QML_IMPORT_PATH"_s, importPath } } << noWarningExpected; + + QTest::addRow("import-dir-via-arg") + << fileThatNeedsImportPath << true << QStringList{ u"-I"_s, importPath } + << Environment{ { u"QML_IMPORT_PATH"_s, invalidImportPath } } << noWarningExpected; + + QTest::addRow("import-dir-via-env") + << fileThatNeedsImportPath << true << QStringList{ u"-E"_s } + << Environment{ { u"QML_IMPORT_PATH"_s, importPath } } + << u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s + .arg(importPath); + + QTest::addRow("import-dir-via-env2") + << fileThatNeedsImportPath << true << QStringList{ u"-E"_s } + << Environment{ { u"QML2_IMPORT_PATH"_s, importPath } } + << u"Using import directories passed from the deprecated environment variable \"QML2_IMPORT_PATH\": \"%1\"."_s + .arg(importPath); +} + +void TestQmllint::environment() +{ + QFETCH(QString, file); + QFETCH(bool, shouldSucceed); + QFETCH(QStringList, extraArgs); + QFETCH(Environment, env); + QFETCH(QString, expectedWarning); + + const QString output = runQmllint(file, shouldSucceed, extraArgs, false, true, false, env); + if (!expectedWarning.isEmpty()) { + QVERIFY(output.contains(expectedWarning)); + } +} + +#endif + +void TestQmllint::ignoreSettingsNotCommandLineOptions() +{ + const QString importPath = testFile(u"ImportPath"_s); + // makes sure that ignore settings only ignores settings and not command line options like + // "-I". + const QString output = runQmllint(testFile(u"NeedImportPath.qml"_s), true, + QStringList{ u"-I"_s, importPath }, true); + // should not complain about not finding the module that is in importPath + QCOMPARE(output, QString()); +} -QTEST_MAIN(TestQmllint) +QTEST_GUILESS_MAIN(TestQmllint) #include "tst_qmllint.moc" |