From a98747ab6c153e1d361ec64ea616764a71465fa4 Mon Sep 17 00:00:00 2001 From: Maximilian Goldstein Date: Wed, 17 Nov 2021 11:25:36 +0100 Subject: [PATCH] qmllint: Move most code into a separate library This is necessary step for both making qmllint viable for use in controls' tst_sanity, as well as for integration with the language server. As an additional upside it allows us to run our tests up to 10x faster. Eventually we want to integrate all of this into qmlcompiler but due to the state of some of the code we will keep qmllint in a separate library as to keep qmlcompiler tidier. Change-Id: Ic057ef0cd4424d28aa05e517d74280a442ec9c5a Reviewed-by: Andrei Golubev --- src/CMakeLists.txt | 1 + src/qmllint/CMakeLists.txt | 13 ++ {tools => src}/qmllint/codegen.cpp | 49 ++-- .../codegen.h => src/qmllint/codegen_p.h | 11 +- .../qmllint/codegenwarninginterface.cpp | 6 +- .../qmllint/codegenwarninginterface_p.h | 21 +- {tools => src}/qmllint/findwarnings.cpp | 6 +- .../qmllint/findwarnings_p.h | 12 +- src/qmllint/qqmllinter.cpp | 220 ++++++++++++++++++ src/qmllint/qqmllinter_p.h | 68 ++++++ sync.profile | 1 + tests/auto/qml/qmllint/CMakeLists.txt | 1 + tests/auto/qml/qmllint/tst_qmllint.cpp | 34 ++- tools/qmllint/CMakeLists.txt | 4 +- tools/qmllint/main.cpp | 183 +-------------- 15 files changed, 407 insertions(+), 223 deletions(-) create mode 100644 src/qmllint/CMakeLists.txt rename {tools => src}/qmllint/codegen.cpp (89%) rename tools/qmllint/codegen.h => src/qmllint/codegen_p.h (95%) rename {tools => src}/qmllint/codegenwarninginterface.cpp (96%) rename tools/qmllint/codegenwarninginterface.h => src/qmllint/codegenwarninginterface_p.h (81%) rename {tools => src}/qmllint/findwarnings.cpp (99%) rename tools/qmllint/findwarnings.h => src/qmllint/findwarnings_p.h (95%) create mode 100644 src/qmllint/qqmllinter.cpp create mode 100644 src/qmllint/qqmllinter_p.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 05c7298b04..5470a08259 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -72,6 +72,7 @@ add_subdirectory(plugins) if(QT_FEATURE_qml_devtools) add_subdirectory(qmlcompiler) + add_subdirectory(qmllint) add_subdirectory(qmldom) # Build qmlcachegen now, so that we can use it in src/imports. diff --git a/src/qmllint/CMakeLists.txt b/src/qmllint/CMakeLists.txt new file mode 100644 index 0000000000..d27e01333d --- /dev/null +++ b/src/qmllint/CMakeLists.txt @@ -0,0 +1,13 @@ +qt_internal_add_module(QmlLintPrivate + STATIC + INTERNAL_MODULE + SOURCES + codegen_p.h codegen.cpp + codegenwarninginterface_p.h codegenwarninginterface.cpp + findwarnings_p.h findwarnings.cpp + qqmllinter_p.h qqmllinter.cpp + PUBLIC_LIBRARIES + Qt::CorePrivate + Qt::QmlPrivate + Qt::QmlCompilerPrivate +) diff --git a/tools/qmllint/codegen.cpp b/src/qmllint/codegen.cpp similarity index 89% rename from tools/qmllint/codegen.cpp rename to src/qmllint/codegen.cpp index 7404ce220d..9bc8cbbe0d 100644 --- a/tools/qmllint/codegen.cpp +++ b/src/qmllint/codegen.cpp @@ -26,7 +26,7 @@ ** ****************************************************************************/ -#include "codegen.h" +#include "codegen_p.h" #include #include @@ -34,6 +34,8 @@ #include +QT_BEGIN_NAMESPACE + Codegen::Codegen(QQmlJSImporter *importer, const QString &fileName, const QStringList &qmltypesFiles, QQmlJSLogger *logger, QQmlJSTypeInfo *typeInfo, const QString &code) @@ -119,9 +121,9 @@ Codegen::compileBinding(const QV4::Compiler::Context *context, const QmlIR::Bind return "nothing"; }; - return diagnose( - QStringLiteral("Binding is not a script binding, but %1.").arg(bindingName()), - QtDebugMsg, bindingLocation); + return diagnose(QStringLiteral("Binding is not a script binding, but %1.") + .arg(QString::fromUtf8(bindingName())), + QtDebugMsg, bindingLocation); } Function function; @@ -193,7 +195,7 @@ Codegen::compileBinding(const QV4::Compiler::Context *context, const QmlIR::Bind auto body = new (m_pool) QQmlJS::AST::StatementList(stmt); body = body->finish(); - QString name = "binding for "; // #### + QString name = u"binding for "_qs; // #### ast = new (m_pool) QQmlJS::AST::FunctionDeclaration(m_pool->newString(name), /*formals*/ nullptr, body); ast->lbraceToken = astNode->firstSourceLocation(); @@ -205,10 +207,10 @@ Codegen::compileBinding(const QV4::Compiler::Context *context, const QmlIR::Bind if (!generateFunction(QV4::Compiler::ContextType::Binding, context, ast, &function, &error)) { // If it's a signal and the function just returns a closure, it's harmless. // Otherwise promote the message to warning level. - return diagnose( - QStringLiteral("Could not compile binding for %1: %2") - .arg(propertyName, error.message), - (isSignal && error.type == QtDebugMsg) ? QtDebugMsg : QtWarningMsg, error.loc); + return diagnose(QStringLiteral("Could not compile binding for %1: %2") + .arg(propertyName, error.message), + (isSignal && error.type == QtDebugMsg) ? QtDebugMsg : QtWarningMsg, + error.loc); } return QQmlJSAotFunction {}; @@ -265,12 +267,10 @@ QQmlJS::DiagnosticMessage Codegen::diagnose(const QString &message, QtMsgType ty return QQmlJS::DiagnosticMessage { message, type, location }; } -bool Codegen::generateFunction( - QV4::Compiler::ContextType contextType, - const QV4::Compiler::Context *context, - QQmlJS::AST::FunctionExpression *ast, - Function *function, - QQmlJS::DiagnosticMessage *error) const +bool Codegen::generateFunction(QV4::Compiler::ContextType contextType, + const QV4::Compiler::Context *context, + QQmlJS::AST::FunctionExpression *ast, Function *function, + QQmlJS::DiagnosticMessage *error) const { const auto fail = [&](const QString &message) { error->loc = ast->firstSourceLocation(); @@ -286,17 +286,16 @@ bool Codegen::generateFunction( for (const QQmlJS::AST::BoundName &argument : qAsConst(arguments)) { if (argument.typeAnnotation) { const auto rawType = m_typeResolver->typeFromAST(argument.typeAnnotation->type); - if (m_typeResolver->storedType( - rawType, QQmlJSTypeResolver::ComponentIsGeneric::Yes)) { + if (m_typeResolver->storedType(rawType, + QQmlJSTypeResolver::ComponentIsGeneric::Yes)) { function->argumentTypes.append(rawType); continue; } else { return fail(QStringLiteral("Cannot store the argument type %1.") - .arg(rawType ? rawType->internalName() : "")); + .arg(rawType ? rawType->internalName() : u""_qs)); } } else { - return fail( - QStringLiteral("Functions without type annotations won't be compiled")); + return fail(QStringLiteral("Functions without type annotations won't be compiled")); return false; } } @@ -313,15 +312,15 @@ bool Codegen::generateFunction( } if (function->returnType) { - if (!m_typeResolver->storedType( - function->returnType, QQmlJSTypeResolver::ComponentIsGeneric::Yes)) { + if (!m_typeResolver->storedType(function->returnType, + QQmlJSTypeResolver::ComponentIsGeneric::Yes)) { return fail(QStringLiteral("Cannot store the return type %1.") .arg(function->returnType->internalName())); } } - function->isSignalHandler = !function->returnType - && contextType == QV4::Compiler::ContextType::Binding; + function->isSignalHandler = + !function->returnType && contextType == QV4::Compiler::ContextType::Binding; function->addressableScopes = m_typeResolver->objectsById(); function->code = context->code; function->sourceLocations = context->sourceLocationTable.get(); @@ -338,3 +337,5 @@ bool Codegen::generateFunction( return true; } + +QT_END_NAMESPACE diff --git a/tools/qmllint/codegen.h b/src/qmllint/codegen_p.h similarity index 95% rename from tools/qmllint/codegen.h rename to src/qmllint/codegen_p.h index 1a9dff3ed8..481f8e2341 100644 --- a/tools/qmllint/codegen.h +++ b/src/qmllint/codegen_p.h @@ -26,8 +26,8 @@ ** ****************************************************************************/ -#ifndef CODEGEN_H -#define CODEGEN_H +#ifndef CODEGEN_P_H +#define CODEGEN_P_H // // W A R N I N G @@ -54,6 +54,8 @@ #include #include +QT_BEGIN_NAMESPACE + class Codegen : public QQmlJSAotCompiler { public: @@ -94,9 +96,10 @@ private: const QQmlJS::SourceLocation &location); bool generateFunction(QV4::Compiler::ContextType contextType, const QV4::Compiler::Context *context, - QQmlJS::AST::FunctionExpression *ast, - Function *function, + QQmlJS::AST::FunctionExpression *ast, Function *function, QQmlJS::DiagnosticMessage *error) const; }; +QT_END_NAMESPACE + #endif diff --git a/tools/qmllint/codegenwarninginterface.cpp b/src/qmllint/codegenwarninginterface.cpp similarity index 96% rename from tools/qmllint/codegenwarninginterface.cpp rename to src/qmllint/codegenwarninginterface.cpp index 11e39abbb0..c06571d1bf 100644 --- a/tools/qmllint/codegenwarninginterface.cpp +++ b/src/qmllint/codegenwarninginterface.cpp @@ -26,10 +26,12 @@ ** ****************************************************************************/ -#include "codegenwarninginterface.h" +#include "codegenwarninginterface_p.h" #include +QT_BEGIN_NAMESPACE + void CodegenWarningInterface::reportVarUsedBeforeDeclaration( const QString &name, const QString &fileName, QQmlJS::SourceLocation declarationLocation, QQmlJS::SourceLocation accessLocation) @@ -42,3 +44,5 @@ void CodegenWarningInterface::reportVarUsedBeforeDeclaration( .arg(declarationLocation.startColumn), Log_Type, accessLocation); } + +QT_END_NAMESPACE diff --git a/tools/qmllint/codegenwarninginterface.h b/src/qmllint/codegenwarninginterface_p.h similarity index 81% rename from tools/qmllint/codegenwarninginterface.h rename to src/qmllint/codegenwarninginterface_p.h index e0e7078629..fb9c5ca862 100644 --- a/tools/qmllint/codegenwarninginterface.h +++ b/src/qmllint/codegenwarninginterface_p.h @@ -26,13 +26,24 @@ ** ****************************************************************************/ -#ifndef CODEGENWARNINGINTERFACE_H -#define CODEGENWARNINGINTERFACE_H +#ifndef CODEGENWARNINGINTERFACE_P_H +#define CODEGENWARNINGINTERFACE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. #include -QT_FORWARD_DECLARE_CLASS(QQmlJSLogger) +QT_BEGIN_NAMESPACE +class QQmlJSLogger; class CodegenWarningInterface final : public QV4::Compiler::CodegenWarningInterface { public: @@ -46,4 +57,6 @@ private: QQmlJSLogger *m_logger; }; -#endif // CODEGENWARNINGINTERFACE_H +QT_END_NAMESPACE + +#endif // CODEGENWARNINGINTERFACE_P_H diff --git a/tools/qmllint/findwarnings.cpp b/src/qmllint/findwarnings.cpp similarity index 99% rename from tools/qmllint/findwarnings.cpp rename to src/qmllint/findwarnings.cpp index 4943593236..d06dc59c5c 100644 --- a/tools/qmllint/findwarnings.cpp +++ b/src/qmllint/findwarnings.cpp @@ -26,7 +26,7 @@ ** ****************************************************************************/ -#include "findwarnings.h" +#include "findwarnings_p.h" #include #include @@ -41,6 +41,8 @@ #include #include +QT_BEGIN_NAMESPACE + bool FindWarningVisitor::visit(QQmlJS::AST::UiObjectDefinition *uiod) { QQmlJSImportVisitor::visit(uiod); @@ -239,3 +241,5 @@ bool FindWarningVisitor::check() return !m_logger->hasWarnings() && !m_logger->hasErrors(); } + +QT_END_NAMESPACE diff --git a/tools/qmllint/findwarnings.h b/src/qmllint/findwarnings_p.h similarity index 95% rename from tools/qmllint/findwarnings.h rename to src/qmllint/findwarnings_p.h index 8d9475f467..599eaf2dc8 100644 --- a/tools/qmllint/findwarnings.h +++ b/src/qmllint/findwarnings_p.h @@ -26,8 +26,8 @@ ** ****************************************************************************/ -#ifndef FINDUNQUALIFIED_H -#define FINDUNQUALIFIED_H +#ifndef FINDUNQUALIFIED_P_H +#define FINDUNQUALIFIED_P_H // // W A R N I N G @@ -51,6 +51,8 @@ #include +QT_BEGIN_NAMESPACE + class FindWarningVisitor : public QQmlJSImportVisitor { Q_DISABLE_COPY_MOVE(FindWarningVisitor) @@ -64,11 +66,13 @@ private: void parseComments(const QList &comments); // work around compiler error in clang11 - using QQmlJSImportVisitor::visit; using QQmlJSImportVisitor::endVisit; + using QQmlJSImportVisitor::visit; bool visit(QQmlJS::AST::UiObjectDefinition *uiod) override; void endVisit(QQmlJS::AST::UiObjectDefinition *uiod) override; }; -#endif // FINDUNQUALIFIED_H +QT_END_NAMESPACE + +#endif // FINDUNQUALIFIED_P_H diff --git a/src/qmllint/qqmllinter.cpp b/src/qmllint/qqmllinter.cpp new file mode 100644 index 0000000000..db225edde4 --- /dev/null +++ b/src/qmllint/qqmllinter.cpp @@ -0,0 +1,220 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmllinter_p.h" + +#include "codegen_p.h" +#include "codegenwarninginterface_p.h" +#include "findwarnings_p.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +QQmlLinter::QQmlLinter(const QStringList &importPaths, bool useAbsolutePath) + : m_useAbsolutePath(useAbsolutePath), m_importer(importPaths, nullptr) +{ +} + +bool QQmlLinter::lintFile(const QString &filename, const bool silent, QJsonArray *json, + const QStringList &qmlImportPaths, const QStringList &qmltypesFiles, + const QStringList &resourceFiles, + const QMap &options) +{ + QJsonArray warnings; + QJsonObject result; + + bool success = true; + + QScopeGuard jsonOutput([&] { + if (!json) + return; + + result[u"filename"_qs] = QFileInfo(filename).absoluteFilePath(); + result[u"warnings"] = warnings; + result[u"success"] = success; + + json->append(result); + }); + + auto addJsonWarning = [&](const QQmlJS::DiagnosticMessage &message) { + QJsonObject jsonMessage; + + QString type; + switch (message.type) { + case QtDebugMsg: + type = u"debug"_qs; + break; + case QtWarningMsg: + type = u"warning"_qs; + break; + case QtCriticalMsg: + type = u"critical"_qs; + break; + case QtFatalMsg: + type = u"fatal"_qs; + break; + case QtInfoMsg: + type = u"info"_qs; + break; + default: + type = u"unknown"_qs; + break; + } + + jsonMessage[u"type"_qs] = type; + + if (message.loc.isValid()) { + jsonMessage[u"line"_qs] = static_cast(message.loc.startLine); + jsonMessage[u"column"_qs] = static_cast(message.loc.startColumn); + jsonMessage[u"charOffset"_qs] = static_cast(message.loc.offset); + jsonMessage[u"length"_qs] = static_cast(message.loc.length); + } + + jsonMessage[u"message"_qs] = message.message; + + warnings << jsonMessage; + }; + + QFile file(filename); + if (!file.open(QFile::ReadOnly)) { + if (json) { + result[u"openFailed"] = true; + success = false; + } else if (!silent) { + qWarning() << "Failed to open file" << filename << file.error(); + } + return false; + } + + QString code = QString::fromUtf8(file.readAll()); + file.close(); + + QQmlJS::Engine engine; + QQmlJS::Lexer lexer(&engine); + + QFileInfo info(filename); + const QString lowerSuffix = info.suffix().toLower(); + const bool isESModule = lowerSuffix == QLatin1String("mjs"); + const bool isJavaScript = isESModule || lowerSuffix == QLatin1String("js"); + + lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/!isJavaScript); + QQmlJS::Parser parser(&engine); + + success = isJavaScript ? (isESModule ? parser.parseModule() : parser.parseProgram()) + : parser.parse(); + + if (!success && !silent) { + const auto diagnosticMessages = parser.diagnosticMessages(); + for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) { + if (json) { + addJsonWarning(m); + } else { + qWarning().noquote() << QString::fromLatin1("%1:%2 : %3") + .arg(filename) + .arg(m.loc.startLine) + .arg(m.message); + } + } + } + + if (success && !isJavaScript) { + const auto check = [&](QQmlJSResourceFileMapper *mapper) { + if (m_importer.importPaths() != qmlImportPaths) + m_importer.setImportPaths(qmlImportPaths); + + m_importer.setResourceFileMapper(mapper); + + QQmlJSLogger logger(m_useAbsolutePath ? info.absoluteFilePath() : filename, code, + silent || json); + FindWarningVisitor v { + &m_importer, + &logger, + qmltypesFiles, + engine.comments(), + }; + + for (auto it = options.cbegin(); it != options.cend(); ++it) { + logger.setCategoryError(it.value().m_category, it.value().m_error); + logger.setCategoryLevel(it.value().m_category, it.value().m_level); + } + + parser.rootNode()->accept(&v); + success = v.check(); + + if (logger.hasErrors()) + return; + + QQmlJSTypeInfo typeInfo; + Codegen codegen { &m_importer, filename, qmltypesFiles, &logger, &typeInfo, code }; + QQmlJSSaveFunction saveFunction = [](const QV4::CompiledData::SaveableUnitPointer &, + const QQmlJSAotFunctionMap &, + QString *) { return true; }; + + QQmlJSCompileError error; + + QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_qs); + + CodegenWarningInterface interface(&logger); + qCompileQmlFile(filename, saveFunction, &codegen, &error, true, &interface); + + success &= !logger.hasWarnings() && !logger.hasErrors(); + + if (json) { + for (const auto &error : logger.errors()) + addJsonWarning(error); + for (const auto &warning : logger.warnings()) + addJsonWarning(warning); + for (const auto &info : logger.infos()) + addJsonWarning(info); + } + }; + + if (resourceFiles.isEmpty()) { + check(nullptr); + } else { + QQmlJSResourceFileMapper mapper(resourceFiles); + check(&mapper); + } + } + + return success; +} + +QT_END_NAMESPACE diff --git a/src/qmllint/qqmllinter_p.h b/src/qmllint/qqmllinter_p.h new file mode 100644 index 0000000000..3d420d9d59 --- /dev/null +++ b/src/qmllint/qqmllinter_p.h @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications 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$ +** +****************************************************************************/ + +#ifndef QMLLINT_P_H +#define QMLLINT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QQmlLinter +{ +public: + QQmlLinter(const QStringList &importPaths, bool useAbsolutePath = false); + + bool lintFile(const QString &filename, const bool silent, QJsonArray *json, + const QStringList &qmlImportPaths, const QStringList &qmltypesFiles, + const QStringList &resourceFiles, + const QMap &options); + +private: + bool m_useAbsolutePath; + QQmlJSImporter m_importer; +}; + +QT_END_NAMESPACE + +#endif // QMLLINT_P_H diff --git a/sync.profile b/sync.profile index 362332c63d..06ff8e2f00 100644 --- a/sync.profile +++ b/sync.profile @@ -12,6 +12,7 @@ "QtQmlModels" => "$basedir/src/qmlmodels", "QtQmlWorkerScript" => "$basedir/src/qmlworkerscript", "QtQmlCompiler" => "$basedir/src/qmlcompiler", + "QtQmlLint" => "$basedir/src/qmllint", "QtQmlDom" => "$basedir/src/qmldom", "QtQuickLayouts" => "$basedir/src/quicklayouts", "QtQmlLocalStorage" => "$basedir/src/qmllocalstorage", diff --git a/tests/auto/qml/qmllint/CMakeLists.txt b/tests/auto/qml/qmllint/CMakeLists.txt index 15549a93b7..d7f1c6e833 100644 --- a/tests/auto/qml/qmllint/CMakeLists.txt +++ b/tests/auto/qml/qmllint/CMakeLists.txt @@ -16,6 +16,7 @@ qt_internal_add_test(tst_qmllint PUBLIC_LIBRARIES Qt::Gui Qt::QuickTestUtilsPrivate + Qt::QmlLintPrivate TESTDATA ${test_data} ) diff --git a/tests/auto/qml/qmllint/tst_qmllint.cpp b/tests/auto/qml/qmllint/tst_qmllint.cpp index 0cd74d8fd4..148f0a01cb 100644 --- a/tests/auto/qml/qmllint/tst_qmllint.cpp +++ b/tests/auto/qml/qmllint/tst_qmllint.cpp @@ -31,6 +31,7 @@ #include #include #include +#include class TestQmllint: public QQmlDataTest { @@ -95,14 +96,21 @@ private: QString runQmllint(const QString &fileToLint, bool shouldSucceed, const QStringList &extraArgs = QStringList(), bool ignoreSettings = true, bool addIncludeDirs = true); + void callQmllint(const QString &fileToLint, bool shouldSucceed, QJsonArray *warnings = nullptr); QString m_qmllintPath; QString m_qmljsrootgenPath; QString m_qmltyperegistrarPath; + + QStringList m_defaultImportPaths; + QQmlLinter m_linter; }; TestQmllint::TestQmllint() - : QQmlDataTest(QT_QMLTEST_DATADIR) + : QQmlDataTest(QT_QMLTEST_DATADIR), + m_defaultImportPaths({ QLibraryInfo::path(QLibraryInfo::QmlImportsPath), dataDirectory() }), + m_linter(m_defaultImportPaths) + { } @@ -754,7 +762,6 @@ void TestQmllint::dirtyQmlCode() 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); @@ -908,8 +915,11 @@ void TestQmllint::cleanQmlCode_data() void TestQmllint::cleanQmlCode() { QFETCH(QString, filename); - const QString warnings = runQmllint(filename, true); - QVERIFY2(warnings.isEmpty(), qPrintable(warnings)); + + QJsonArray warnings; + + callQmllint(filename, true, &warnings); + QVERIFY2(warnings.isEmpty(), qPrintable(QJsonDocument(warnings).toJson())); } QString TestQmllint::runQmllint(const QString &fileToLint, @@ -974,6 +984,22 @@ QString TestQmllint::runQmllint(const QString &fileToLint, bool shouldSucceed, extraArgs, ignoreSettings, addIncludeDirs); } +void TestQmllint::callQmllint(const QString &fileToLint, bool shouldSucceed, QJsonArray *warnings) +{ + QJsonArray jsonOutput; + + bool success = m_linter.lintFile(QFileInfo(fileToLint).isAbsolute() ? fileToLint + : testFile(fileToLint), + true, warnings ? &jsonOutput : nullptr, m_defaultImportPaths, + QStringList(), QStringList(), {}); + QCOMPARE(success, shouldSucceed); + + if (warnings) { + Q_ASSERT(jsonOutput.size() == 1); + *warnings = jsonOutput.at(0)[u"warnings"_qs].toArray(); + } +} + void TestQmllint::requiredProperty() { QVERIFY(runQmllint("requiredProperty.qml", true).isEmpty()); diff --git a/tools/qmllint/CMakeLists.txt b/tools/qmllint/CMakeLists.txt index 4107e10f74..f9127acaa3 100644 --- a/tools/qmllint/CMakeLists.txt +++ b/tools/qmllint/CMakeLists.txt @@ -9,15 +9,13 @@ qt_internal_add_tool(${target_name} TARGET_DESCRIPTION "QML Syntax Verifier" TOOLS_TARGET Qml # special case SOURCES - findwarnings.cpp findwarnings.h - codegen.cpp codegen.h - codegenwarninginterface.cpp codegenwarninginterface.h main.cpp ../shared/qqmltoolingsettings.h ../shared/qqmltoolingsettings.cpp PUBLIC_LIBRARIES Qt::CorePrivate Qt::QmlCompilerPrivate + Qt::QmlLintPrivate ) qt_internal_return_unless_building_tools() diff --git a/tools/qmllint/main.cpp b/tools/qmllint/main.cpp index 3df510ed79..b7b64f0b2b 100644 --- a/tools/qmllint/main.cpp +++ b/tools/qmllint/main.cpp @@ -26,20 +26,13 @@ ** ****************************************************************************/ -#include "findwarnings.h" -#include "codegen.h" -#include "codegenwarninginterface.h" #include "../shared/qqmltoolingsettings.h" +#include + #include #include -#include -#include -#include -#include -#include - #include #include #include @@ -48,7 +41,6 @@ #include #include #include -#include #include #if QT_CONFIG(commandlineparser) @@ -61,171 +53,6 @@ constexpr int JSON_LOGGING_FORMAT_REVISION = 1; -static bool lint_file(const QString &filename, const bool silent, const bool useAbsolutePath, - QJsonArray *json, const QStringList &qmlImportPaths, - const QStringList &qmltypesFiles, const QStringList &resourceFiles, - const QMap &options, QQmlJSImporter &importer) -{ - QJsonArray warnings; - QJsonObject result; - - bool success = true; - - QScopeGuard jsonOutput([&] { - if (!json) - return; - - result[u"filename"_qs] = QFileInfo(filename).absoluteFilePath(); - result[u"warnings"] = warnings; - result[u"success"] = success; - - json->append(result); - }); - - auto addJsonWarning = [&](const QQmlJS::DiagnosticMessage &message) { - QJsonObject jsonMessage; - - QString type; - switch (message.type) { - case QtDebugMsg: - type = "debug"; - break; - case QtWarningMsg: - type = "warning"; - break; - case QtCriticalMsg: - type = "critical"; - break; - case QtFatalMsg: - type = "fatal"; - break; - case QtInfoMsg: - type = "info"; - break; - default: - type = "unknown"; - break; - } - - jsonMessage[u"type"_qs] = type; - - if (message.loc.isValid()) { - jsonMessage[u"line"_qs] = static_cast(message.loc.startLine); - jsonMessage[u"column"_qs] = static_cast(message.loc.startColumn); - jsonMessage[u"charOffset"_qs] = static_cast(message.loc.offset); - jsonMessage[u"length"_qs] = static_cast(message.loc.length); - } - - jsonMessage[u"message"_qs] = message.message; - - warnings << jsonMessage; - }; - - QFile file(filename); - if (!file.open(QFile::ReadOnly)) { - if (json) { - result[u"openFailed"] = true; - success = false; - } else if (!silent) { - qWarning() << "Failed to open file" << filename << file.error(); - } - return false; - } - - QString code = QString::fromUtf8(file.readAll()); - file.close(); - - QQmlJS::Engine engine; - QQmlJS::Lexer lexer(&engine); - - QFileInfo info(filename); - const QString lowerSuffix = info.suffix().toLower(); - const bool isESModule = lowerSuffix == QLatin1String("mjs"); - const bool isJavaScript = isESModule || lowerSuffix == QLatin1String("js"); - - lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/ !isJavaScript); - QQmlJS::Parser parser(&engine); - - success = isJavaScript ? (isESModule ? parser.parseModule() : parser.parseProgram()) - : parser.parse(); - - if (!success && !silent) { - const auto diagnosticMessages = parser.diagnosticMessages(); - for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) { - if (json) { - addJsonWarning(m); - } else { - qWarning().noquote() << QString::fromLatin1("%1:%2 : %3") - .arg(filename) - .arg(m.loc.startLine) - .arg(m.message); - } - } - } - - if (success && !isJavaScript) { - const auto check = [&](QQmlJSResourceFileMapper *mapper) { - if (importer.importPaths() != qmlImportPaths) - importer.setImportPaths(qmlImportPaths); - - importer.setResourceFileMapper(mapper); - - QQmlJSLogger logger(useAbsolutePath ? info.absoluteFilePath() : filename, code, - silent || json); - FindWarningVisitor v { - &importer, - &logger, - qmltypesFiles, - engine.comments(), - }; - - for (auto it = options.cbegin(); it != options.cend(); ++it) { - logger.setCategoryError(it.value().m_category, it.value().m_error); - logger.setCategoryLevel(it.value().m_category, it.value().m_level); - } - - parser.rootNode()->accept(&v); - success = v.check(); - - if (logger.hasErrors()) - return; - - QQmlJSTypeInfo typeInfo; - Codegen codegen { &importer, filename, qmltypesFiles, &logger, &typeInfo, code }; - QQmlJSSaveFunction saveFunction = [](const QV4::CompiledData::SaveableUnitPointer &, - const QQmlJSAotFunctionMap &, - QString *) { return true; }; - - QQmlJSCompileError error; - - QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_qs); - - CodegenWarningInterface interface(&logger); - qCompileQmlFile(filename, saveFunction, &codegen, &error, true, &interface); - - success &= !logger.hasWarnings() && !logger.hasErrors(); - - if (json) { - for (const auto &error : logger.errors()) - addJsonWarning(error); - for (const auto &warning : logger.warnings()) - addJsonWarning(warning); - for (const auto &info : logger.infos()) - addJsonWarning(info); - } - }; - - if (resourceFiles.isEmpty()) { - check(nullptr); - } else { - QQmlJSResourceFileMapper mapper(resourceFiles); - check(&mapper); - } - } - - return success; -} - int main(int argv, char *argc[]) { qSetGlobalQHashSeed(0); @@ -394,7 +221,7 @@ All warnings can be set to three levels: QStringList resourceFiles {}; #endif bool success = true; - QQmlJSImporter importer(qmlImportPaths, nullptr); + QQmlLinter linter(qmlImportPaths, useAbsolutePath); QJsonArray jsonFiles; @@ -439,8 +266,8 @@ All warnings can be set to three levels: const auto arguments = app.arguments(); for (const QString &filename : arguments) { #endif - success &= lint_file(filename, silent, useAbsolutePath, useJson ? &jsonFiles : nullptr, - qmlImportPaths, qmltypesFiles, resourceFiles, options, importer); + success &= linter.lintFile(filename, silent, useJson ? &jsonFiles : nullptr, qmlImportPaths, + qmltypesFiles, resourceFiles, options); } if (useJson) {