qtdeclarative/tools/qmltc/main.cpp

313 lines
12 KiB
C++
Raw Normal View History

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "qmltccommandlineutils.h"
#include "qmltcvisitor.h"
#include "qmltctyperesolver.h"
#include "qmltccompiler.h"
#include <private/qqmljscompiler_p.h>
#include <private/qqmljsresourcefilemapper_p.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qurl.h>
#include <QtCore/qhashfunctions.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qcommandlineparser.h>
#include <QtCore/qregularexpression.h>
#include <QtQml/private/qqmljslexer_p.h>
#include <QtQml/private/qqmljsparser_p.h>
#include <QtQml/private/qqmljsengine_p.h>
#include <QtQml/private/qqmljsastvisitor_p.h>
#include <QtQml/private/qqmljsast_p.h>
#include <QtQml/private/qqmljsdiagnosticmessage_p.h>
#include <QtQmlCompiler/qqmlsa.h>
#include <QtQmlCompiler/private/qqmljsliteralbindingcheck_p.h>
#include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE
using namespace Qt::StringLiterals;
void setupLogger(QQmlJSLogger &logger) // prepare logger to work with compiler
{
for (const QQmlJS::LoggerCategory &category : logger.categories()) {
if (category.id() == qmlUnusedImports)
continue;
logger.setCategoryLevel(category.id(), QtCriticalMsg);
logger.setCategoryIgnored(category.id(), false);
}
}
int main(int argc, char **argv)
{
// Produce reliably the same output for the same input by disabling QHash's
// random seeding.
QHashSeed::setDeterministicGlobalSeed();
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName(u"qmltc"_s);
QCoreApplication::setApplicationVersion(QStringLiteral(QT_VERSION_STR));
// command-line parsing:
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption bareOption {
u"bare"_s,
QCoreApplication::translate(
"main", "Do not include default import directories. This may be used to run "
"qmltc on a project using a different Qt version.")
};
parser.addOption(bareOption);
QCommandLineOption importPathOption {
u"I"_s, QCoreApplication::translate("main", "Look for QML modules in specified directory"),
QCoreApplication::translate("main", "import directory")
};
parser.addOption(importPathOption);
QCommandLineOption qmldirOption {
u"i"_s, QCoreApplication::translate("main", "Include extra qmldir files"),
QCoreApplication::translate("main", "qmldir file")
};
parser.addOption(qmldirOption);
QCommandLineOption outputCppOption {
u"impl"_s, QCoreApplication::translate("main", "Generated C++ source file path"),
QCoreApplication::translate("main", "cpp path")
};
parser.addOption(outputCppOption);
QCommandLineOption outputHOption {
u"header"_s, QCoreApplication::translate("main", "Generated C++ header file path"),
QCoreApplication::translate("main", "h path")
};
parser.addOption(outputHOption);
QCommandLineOption resourceOption {
u"resource"_s,
QCoreApplication::translate(
"main", "Qt resource file that might later contain one of the compiled files"),
QCoreApplication::translate("main", "resource file name")
};
parser.addOption(resourceOption);
QCommandLineOption metaResourceOption {
u"meta-resource"_s,
QCoreApplication::translate("main", "Qt meta information file (in .qrc format)"),
QCoreApplication::translate("main", "meta file name")
};
parser.addOption(metaResourceOption);
QCommandLineOption namespaceOption {
u"namespace"_s, QCoreApplication::translate("main", "Namespace of the generated C++ code"),
QCoreApplication::translate("main", "namespace")
};
parser.addOption(namespaceOption);
QCommandLineOption moduleOption{
u"module"_s,
QCoreApplication::translate("main",
"Name of the QML module that this QML code belongs to."),
QCoreApplication::translate("main", "module")
};
parser.addOption(moduleOption);
QCommandLineOption exportOption{ u"export"_s,
QCoreApplication::translate(
"main", "Export macro used in the generated C++ code"),
QCoreApplication::translate("main", "export") };
parser.addOption(exportOption);
QCommandLineOption exportIncludeOption{
u"exportInclude"_s,
QCoreApplication::translate(
"main", "Header defining the export macro to be used in the generated C++ code"),
QCoreApplication::translate("main", "exportInclude")
};
parser.addOption(exportIncludeOption);
parser.process(app);
const QStringList sources = parser.positionalArguments();
if (sources.size() != 1) {
if (sources.isEmpty()) {
parser.showHelp();
} else {
fprintf(stderr, "%s\n",
qPrintable(u"Too many input files specified: '"_s + sources.join(u"' '"_s)
+ u'\''));
}
return EXIT_FAILURE;
}
const QString inputFile = sources.first();
QString url = parseUrlArgument(inputFile);
if (url.isNull())
return EXIT_FAILURE;
if (!url.endsWith(u".qml")) {
fprintf(stderr, "Non-QML file passed as input\n");
return EXIT_FAILURE;
}
static QRegularExpression nameChecker(u"^[a-zA-Z_][a-zA-Z0-9_]*\\.qml$"_s);
if (auto match = nameChecker.match(QUrl(url).fileName()); !match.hasMatch()) {
fprintf(stderr,
"The given QML filename is unsuited for type compilation: the name must consist of "
"letters, digits and underscores, starting with "
"a letter or an underscore and ending in '.qml'!\n");
return EXIT_FAILURE;
}
QString sourceCode = loadUrl(url);
if (sourceCode.isEmpty())
return EXIT_FAILURE;
QString implicitImportDirectory = getImplicitImportDirectory(url);
if (implicitImportDirectory.isEmpty())
return EXIT_FAILURE;
QStringList importPaths;
if (parser.isSet(resourceOption)) {
importPaths.append(QLatin1String(":/qt-project.org/imports"));
importPaths.append(QLatin1String(":/qt/qml"));
};
if (parser.isSet(importPathOption))
importPaths.append(parser.values(importPathOption));
if (!parser.isSet(bareOption))
importPaths.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath));
QStringList qmldirFiles = parser.values(qmldirOption);
QString outputCppFile;
if (!parser.isSet(outputCppOption)) {
outputCppFile = url.first(url.size() - 3) + u"cpp"_s;
} else {
outputCppFile = parser.value(outputCppOption);
}
QString outputHFile;
if (!parser.isSet(outputHOption)) {
outputHFile = url.first(url.size() - 3) + u"h"_s;
} else {
outputHFile = parser.value(outputHOption);
}
if (!parser.isSet(resourceOption)) {
fprintf(stderr, "No resource paths for file: %s\n", qPrintable(inputFile));
return EXIT_FAILURE;
}
// main logic:
QQmlJS::Engine engine;
QQmlJS::Lexer lexer(&engine);
lexer.setCode(sourceCode, /*lineno = */ 1);
QQmlJS::Parser qmlParser(&engine);
if (!qmlParser.parse()) {
const auto diagnosticMessages = qmlParser.diagnosticMessages();
for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
fprintf(stderr, "%s\n",
qPrintable(QStringLiteral("%1:%2:%3: %4")
.arg(inputFile)
.arg(m.loc.startLine)
.arg(m.loc.startColumn)
.arg(m.message)));
}
return EXIT_FAILURE;
}
const QStringList resourceFiles = parser.values(resourceOption);
QQmlJSResourceFileMapper mapper(resourceFiles);
const QStringList metaResourceFiles = parser.values(metaResourceOption);
QQmlJSResourceFileMapper metaDataMapper(metaResourceFiles);
const auto firstQml = [](const QStringList &paths) {
auto it = std::find_if(paths.cbegin(), paths.cend(),
[](const QString &x) { return x.endsWith(u".qml"_s); });
if (it == paths.cend())
return QString();
return *it;
};
// verify that we can map current file to qrc (then use the qrc path later)
const QStringList paths = mapper.resourcePaths(QQmlJSResourceFileMapper::localFileFilter(url));
if (paths.isEmpty()) {
fprintf(stderr, "Failed to find a resource path for file: %s\n", qPrintable(inputFile));
return EXIT_FAILURE;
} else if (paths.size() > 1) {
bool good = !firstQml(paths).isEmpty();
good &= std::any_of(paths.cbegin(), paths.cend(),
[](const QString &x) { return x.endsWith(u".h"_s); });
if (!good || paths.size() > 2) {
fprintf(stderr, "Unexpected resource paths for file: %s\n", qPrintable(inputFile));
return EXIT_FAILURE;
}
}
QmltcCompilerInfo info;
info.outputCppFile = parser.value(outputCppOption);
info.outputHFile = parser.value(outputHOption);
info.resourcePath = firstQml(paths);
info.outputNamespace = parser.value(namespaceOption);
info.exportMacro = parser.value(exportOption);
info.exportInclude = parser.value(exportIncludeOption);
if (info.outputCppFile.isEmpty()) {
fprintf(stderr, "An output C++ file is required. Pass one using --impl");
return EXIT_FAILURE;
}
if (info.outputHFile.isEmpty()) {
fprintf(stderr, "An output C++ header file is required. Pass one using --header");
return EXIT_FAILURE;
}
QQmlJSImporter importer { importPaths, &mapper };
importer.setMetaDataMapper(&metaDataMapper);
dom: enable populateQmlFile as visitor in importer Change qqmljsimporter to accept a custom importvisitor instead of a custom importvisitorcreator. This gives more flexibility, for example to allow to use populateQmlFile() in combination with qqmljsimporter. Extend DomEnvironment::ensureImporter() to set up a custom visitor in the importer that calls populateQmlFile() to construct the Dom lazily. Adapt the qmltc custom visitor creator to be a custom visitor. Add a basic test to see if the qqmljsscope created on two files with the populateQmlFile() custom visitor share the same address. This is a requirement for the find usages functionality. Also run the importvisitor even when parsing fails: The importvisitor for the Dom will parse the code again with parser recovery, and therefore the importvisitor has to always be run to get the parser recovery working in the tests. Do not try to load dependencies in LoadInfo::doAddDependencies, because it will trigger QmlFile lazyloading. This happens because dependencies in the form of import statements can only be processed if qqmldomastcreator already collected all the import statements. Instead, load the dependencies in qqmldomastcreator (that is, during the lazy loading) when encountering import statements. Like this, a lazy QML file will only load its imported modules when its getting populated. Also make sure to not load anything if NoDependencies is enabled in the DomEnvironment options. Add a state enableLoadFileLazily in qqmldomastcreator to know if loadPendingDependencies() needs to be called or not. When the file is being lazily loaded, then loadPendingDependencies() needs to be called to get the imports correctly loaded. When the file is not (because WithSemanticAnalysis is disabled in the DomEnvironment), then the imports are already being processed in loadPendingDependencies() and calling it again recursively breaks loadPendingDependencies(). Allow the qqmldomastcreator to commit lazily loaded dependencies to the validEnv by saving a pointer in domEnvironment of the latest used domEnvironment. And do a commitToBase after loading dependencies from imported modules. Task-number: QTBUG-122645 Change-Id: I3ca8bc011dcb5920fffab786cdb9e748667be48e Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
2024-02-07 16:30:33 +00:00
auto qmltcVisitor = [](QQmlJS::AST::Node *rootNode, QQmlJSImporter *self,
const QQmlJSImporter::ImportVisitorPrerequisites &p) {
QmltcVisitor v(p.m_target, self, p.m_logger, p.m_implicitImportDirectory, p.m_qmldirFiles);
QQmlJS::AST::Node::accept(rootNode, &v);
};
dom: enable populateQmlFile as visitor in importer Change qqmljsimporter to accept a custom importvisitor instead of a custom importvisitorcreator. This gives more flexibility, for example to allow to use populateQmlFile() in combination with qqmljsimporter. Extend DomEnvironment::ensureImporter() to set up a custom visitor in the importer that calls populateQmlFile() to construct the Dom lazily. Adapt the qmltc custom visitor creator to be a custom visitor. Add a basic test to see if the qqmljsscope created on two files with the populateQmlFile() custom visitor share the same address. This is a requirement for the find usages functionality. Also run the importvisitor even when parsing fails: The importvisitor for the Dom will parse the code again with parser recovery, and therefore the importvisitor has to always be run to get the parser recovery working in the tests. Do not try to load dependencies in LoadInfo::doAddDependencies, because it will trigger QmlFile lazyloading. This happens because dependencies in the form of import statements can only be processed if qqmldomastcreator already collected all the import statements. Instead, load the dependencies in qqmldomastcreator (that is, during the lazy loading) when encountering import statements. Like this, a lazy QML file will only load its imported modules when its getting populated. Also make sure to not load anything if NoDependencies is enabled in the DomEnvironment options. Add a state enableLoadFileLazily in qqmldomastcreator to know if loadPendingDependencies() needs to be called or not. When the file is being lazily loaded, then loadPendingDependencies() needs to be called to get the imports correctly loaded. When the file is not (because WithSemanticAnalysis is disabled in the DomEnvironment), then the imports are already being processed in loadPendingDependencies() and calling it again recursively breaks loadPendingDependencies(). Allow the qqmldomastcreator to commit lazily loaded dependencies to the validEnv by saving a pointer in domEnvironment of the latest used domEnvironment. And do a commitToBase after loading dependencies from imported modules. Task-number: QTBUG-122645 Change-Id: I3ca8bc011dcb5920fffab786cdb9e748667be48e Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
2024-02-07 16:30:33 +00:00
importer.setImportVisitor(qmltcVisitor);
QQmlJSLogger logger;
logger.setFileName(url);
logger.setCode(sourceCode);
setupLogger(logger);
auto currentScope = QQmlJSScope::create();
if (parser.isSet(moduleOption))
currentScope->setModuleName(parser.value(moduleOption));
QmltcVisitor visitor(currentScope, &importer, &logger,
QQmlJSImportVisitor::implicitImportDirectory(url, &mapper), qmldirFiles);
visitor.setMode(QmltcVisitor::Compile);
QmltcTypeResolver typeResolver { &importer };
typeResolver.init(&visitor, qmlParser.rootNode());
using PassManagerPtr =
std::unique_ptr<QQmlSA::PassManager,
decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>;
PassManagerPtr passMan(QQmlSA::PassManagerPrivate::createPassManager(&visitor, &typeResolver),
&QQmlSA::PassManagerPrivate::deletePassManager);
passMan->registerPropertyPass(std::make_unique<QQmlJSLiteralBindingCheck>(passMan.get()),
QString(), QString(), QString());
passMan->analyze(QQmlJSScope::createQQmlSAElement(visitor.result()));
if (logger.hasErrors())
return EXIT_FAILURE;
QList<QQmlJS::DiagnosticMessage> warnings = importer.takeGlobalWarnings();
if (!warnings.isEmpty()) {
logger.log(QStringLiteral("Type warnings occurred while compiling file:"), qmlImport,
Redesign QQmlJSLogger internals High-level goal: be able to reuse existing infrastructure "as is" to configure semantic analysis categories in tools (qmllint, qmltc, qmlsc, etc.) To achieve that, simplify the logging to always "log" something, without explicitly specifying the severity. The severity is now baked into the category (and we can extend those to cover different cases) One slight deviation is the cache generation which likes to do its own thing at present. Provide a "forced logging" option where we can specify which severify we want. The hope is that this gets removed at some point Particular list of (noteworthy) changes: * No more "thresholding" by the level (this is rarely needed and is actually questionable). Instead, we can ignore a particular category explicitly * Category levels are repurposed as category severities (at least from the high-level picture that always should've been this way) * log{Warning,Info,Critical} removed. We use category severity instead * "category error" makes zero sense so removed: if our severity is: - QtWarningMsg (qmllint), it is already an "error" - QtCriticalMsg (compilers), it is already an "error" * Align m_output and m_{infos,warnings,errors} stored information * Accept the fact that we don't support QtDebugMsg and QtFatalMsg * Additional categories added to cover for places where the same category would be both an error and not an error Task-number: QTBUG-100052 Change-Id: I3cd5d17d58be204f48428877bed053f756ac40a8 Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2022-02-03 12:45:18 +00:00
QQmlJS::SourceLocation());
logger.processMessages(warnings, qmlImport);
// Log_Import is critical for the compiler
return EXIT_FAILURE;
}
QmltcCompiler compiler(url, &typeResolver, &visitor, &logger);
compiler.compile(info);
if (logger.hasErrors())
return EXIT_FAILURE;
return EXIT_SUCCESS;
}