// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // 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); 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); }; importer.setImportVisitor(qmltcVisitor); QQmlJSLogger logger; logger.setFileName(url); logger.setCode(sourceCode); setupLogger(logger); auto currentScope = QQmlJSScope::create(); if (parser.isSet(moduleOption)) currentScope->setOwnModuleName(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; PassManagerPtr passMan(QQmlSA::PassManagerPrivate::createPassManager(&visitor, &typeResolver), &QQmlSA::PassManagerPrivate::deletePassManager); passMan->registerPropertyPass(std::make_unique(passMan.get()), QString(), QString(), QString()); passMan->analyze(QQmlJSScope::createQQmlSAElement(visitor.result())); if (logger.hasErrors()) return EXIT_FAILURE; QList warnings = importer.takeGlobalWarnings(); if (!warnings.isEmpty()) { logger.log(QStringLiteral("Type warnings occurred while compiling file:"), qmlImport, 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; }