| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
| 3 | |
| 4 | #include "qmltccommandlineutils.h" |
| 5 | #include "qmltcvisitor.h" |
| 6 | #include "qmltctyperesolver.h" |
| 7 | |
| 8 | #include "qmltccompiler.h" |
| 9 | |
| 10 | #include <private/qqmljscompiler_p.h> |
| 11 | #include <private/qqmljsresourcefilemapper_p.h> |
| 12 | #include <private/qqmljsutils_p.h> |
| 13 | |
| 14 | #include <QtCore/qcoreapplication.h> |
| 15 | #include <QtCore/qurl.h> |
| 16 | #include <QtCore/qhashfunctions.h> |
| 17 | #include <QtCore/qfileinfo.h> |
| 18 | #include <QtCore/qlibraryinfo.h> |
| 19 | #include <QtCore/qcommandlineparser.h> |
| 20 | #include <QtCore/qregularexpression.h> |
| 21 | |
| 22 | #include <QtQml/private/qqmljslexer_p.h> |
| 23 | #include <QtQml/private/qqmljsparser_p.h> |
| 24 | #include <QtQml/private/qqmljsengine_p.h> |
| 25 | #include <QtQml/private/qqmljsastvisitor_p.h> |
| 26 | #include <QtQml/private/qqmljsast_p.h> |
| 27 | #include <QtQml/private/qqmljsdiagnosticmessage_p.h> |
| 28 | #include <QtQmlCompiler/qqmlsa.h> |
| 29 | #include <QtQmlCompiler/private/qqmljsliteralbindingcheck_p.h> |
| 30 | |
| 31 | #include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE |
| 32 | |
| 33 | using namespace Qt::StringLiterals; |
| 34 | |
| 35 | void setupLogger(QQmlJSLogger &logger) // prepare logger to work with compiler |
| 36 | { |
| 37 | for (const QQmlJS::LoggerCategory &category : logger.categories()) { |
| 38 | if (category.id() == qmlUnusedImports) |
| 39 | continue; |
| 40 | logger.setCategoryLevel(id: category.id(), level: QtCriticalMsg); |
| 41 | logger.setCategoryIgnored(id: category.id(), error: false); |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | int main(int argc, char **argv) |
| 46 | { |
| 47 | // Produce reliably the same output for the same input by disabling QHash's |
| 48 | // random seeding. |
| 49 | QHashSeed::setDeterministicGlobalSeed(); |
| 50 | QCoreApplication app(argc, argv); |
| 51 | QCoreApplication::setApplicationName(u"qmltc"_s ); |
| 52 | QCoreApplication::setApplicationVersion(QStringLiteral(QT_VERSION_STR)); |
| 53 | |
| 54 | // command-line parsing: |
| 55 | QCommandLineParser parser; |
| 56 | parser.addHelpOption(); |
| 57 | parser.addVersionOption(); |
| 58 | |
| 59 | QCommandLineOption bareOption { |
| 60 | u"bare"_s , |
| 61 | QCoreApplication::translate( |
| 62 | context: "main" , key: "Do not include default import directories. This may be used to run " |
| 63 | "qmltc on a project using a different Qt version." ) |
| 64 | }; |
| 65 | parser.addOption(commandLineOption: bareOption); |
| 66 | |
| 67 | QCommandLineOption importPathOption { |
| 68 | u"I"_s , QCoreApplication::translate(context: "main" , key: "Look for QML modules in specified directory" ), |
| 69 | QCoreApplication::translate(context: "main" , key: "import directory" ) |
| 70 | }; |
| 71 | parser.addOption(commandLineOption: importPathOption); |
| 72 | QCommandLineOption qmldirOption { |
| 73 | u"i"_s , QCoreApplication::translate(context: "main" , key: "Include extra qmldir files" ), |
| 74 | QCoreApplication::translate(context: "main" , key: "qmldir file" ) |
| 75 | }; |
| 76 | parser.addOption(commandLineOption: qmldirOption); |
| 77 | QCommandLineOption outputCppOption { |
| 78 | u"impl"_s , QCoreApplication::translate(context: "main" , key: "Generated C++ source file path" ), |
| 79 | QCoreApplication::translate(context: "main" , key: "cpp path" ) |
| 80 | }; |
| 81 | parser.addOption(commandLineOption: outputCppOption); |
| 82 | QCommandLineOption outputHOption { |
| 83 | u"header"_s , QCoreApplication::translate(context: "main" , key: "Generated C++ header file path" ), |
| 84 | QCoreApplication::translate(context: "main" , key: "h path" ) |
| 85 | }; |
| 86 | parser.addOption(commandLineOption: outputHOption); |
| 87 | QCommandLineOption resourceOption { |
| 88 | u"resource"_s , |
| 89 | QCoreApplication::translate( |
| 90 | context: "main" , key: "Qt resource file that might later contain one of the compiled files" ), |
| 91 | QCoreApplication::translate(context: "main" , key: "resource file name" ) |
| 92 | }; |
| 93 | parser.addOption(commandLineOption: resourceOption); |
| 94 | QCommandLineOption metaResourceOption { |
| 95 | u"meta-resource"_s , |
| 96 | QCoreApplication::translate(context: "main" , key: "Qt meta information file (in .qrc format)" ), |
| 97 | QCoreApplication::translate(context: "main" , key: "meta file name" ) |
| 98 | }; |
| 99 | parser.addOption(commandLineOption: metaResourceOption); |
| 100 | QCommandLineOption namespaceOption { |
| 101 | u"namespace"_s , QCoreApplication::translate(context: "main" , key: "Namespace of the generated C++ code" ), |
| 102 | QCoreApplication::translate(context: "main" , key: "namespace" ) |
| 103 | }; |
| 104 | parser.addOption(commandLineOption: namespaceOption); |
| 105 | QCommandLineOption moduleOption{ |
| 106 | u"module"_s , |
| 107 | QCoreApplication::translate(context: "main" , |
| 108 | key: "Name of the QML module that this QML code belongs to." ), |
| 109 | QCoreApplication::translate(context: "main" , key: "module" ) |
| 110 | }; |
| 111 | parser.addOption(commandLineOption: moduleOption); |
| 112 | QCommandLineOption exportOption{ u"export"_s , |
| 113 | QCoreApplication::translate( |
| 114 | context: "main" , key: "Export macro used in the generated C++ code" ), |
| 115 | QCoreApplication::translate(context: "main" , key: "export" ) }; |
| 116 | parser.addOption(commandLineOption: exportOption); |
| 117 | QCommandLineOption exportIncludeOption{ |
| 118 | u"exportInclude"_s , |
| 119 | QCoreApplication::translate( |
| 120 | context: "main" , key: "Header defining the export macro to be used in the generated C++ code" ), |
| 121 | QCoreApplication::translate(context: "main" , key: "exportInclude" ) |
| 122 | }; |
| 123 | parser.addOption(commandLineOption: exportIncludeOption); |
| 124 | |
| 125 | parser.process(app); |
| 126 | |
| 127 | const QStringList sources = parser.positionalArguments(); |
| 128 | if (sources.size() != 1) { |
| 129 | if (sources.isEmpty()) { |
| 130 | parser.showHelp(); |
| 131 | } else { |
| 132 | fprintf(stderr, format: "%s\n" , |
| 133 | qPrintable(u"Too many input files specified: '"_s + sources.join(u"' '"_s ) |
| 134 | + u'\'')); |
| 135 | } |
| 136 | return EXIT_FAILURE; |
| 137 | } |
| 138 | const QString inputFile = sources.first(); |
| 139 | |
| 140 | QString url = parseUrlArgument(arg: inputFile); |
| 141 | if (url.isNull()) |
| 142 | return EXIT_FAILURE; |
| 143 | if (!url.endsWith(s: u".qml" )) { |
| 144 | fprintf(stderr, format: "Non-QML file passed as input\n" ); |
| 145 | return EXIT_FAILURE; |
| 146 | } |
| 147 | |
| 148 | static QRegularExpression nameChecker(u"^[a-zA-Z_][a-zA-Z0-9_]*\\.qml$"_s ); |
| 149 | if (auto match = nameChecker.match(subject: QUrl(url).fileName()); !match.hasMatch()) { |
| 150 | fprintf(stderr, |
| 151 | format: "The given QML filename is unsuited for type compilation: the name must consist of " |
| 152 | "letters, digits and underscores, starting with " |
| 153 | "a letter or an underscore and ending in '.qml'!\n" ); |
| 154 | return EXIT_FAILURE; |
| 155 | } |
| 156 | |
| 157 | QString sourceCode = loadUrl(url); |
| 158 | if (sourceCode.isEmpty()) |
| 159 | return EXIT_FAILURE; |
| 160 | |
| 161 | QString implicitImportDirectory = getImplicitImportDirectory(url); |
| 162 | if (implicitImportDirectory.isEmpty()) |
| 163 | return EXIT_FAILURE; |
| 164 | |
| 165 | QStringList importPaths; |
| 166 | |
| 167 | if (parser.isSet(option: resourceOption)) { |
| 168 | importPaths.append(t: QLatin1String(":/qt-project.org/imports" )); |
| 169 | importPaths.append(t: QLatin1String(":/qt/qml" )); |
| 170 | }; |
| 171 | |
| 172 | if (parser.isSet(option: importPathOption)) |
| 173 | importPaths.append(other: parser.values(option: importPathOption)); |
| 174 | |
| 175 | if (!parser.isSet(option: bareOption)) |
| 176 | importPaths.append(t: QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath)); |
| 177 | |
| 178 | QStringList qmldirFiles = QQmlJSUtils::cleanPaths(paths: parser.values(option: qmldirOption)); |
| 179 | |
| 180 | QString outputCppFile; |
| 181 | if (!parser.isSet(option: outputCppOption)) { |
| 182 | outputCppFile = url.first(n: url.size() - 3) + u"cpp"_s ; |
| 183 | } else { |
| 184 | outputCppFile = parser.value(option: outputCppOption); |
| 185 | } |
| 186 | |
| 187 | QString outputHFile; |
| 188 | if (!parser.isSet(option: outputHOption)) { |
| 189 | outputHFile = url.first(n: url.size() - 3) + u"h"_s ; |
| 190 | } else { |
| 191 | outputHFile = parser.value(option: outputHOption); |
| 192 | } |
| 193 | |
| 194 | if (!parser.isSet(option: resourceOption)) { |
| 195 | fprintf(stderr, format: "No resource paths for file: %s\n" , qPrintable(inputFile)); |
| 196 | return EXIT_FAILURE; |
| 197 | } |
| 198 | |
| 199 | // main logic: |
| 200 | QQmlJS::Engine engine; |
| 201 | QQmlJS::Lexer lexer(&engine); |
| 202 | lexer.setCode(code: sourceCode, /*lineno = */ 1); |
| 203 | QQmlJS::Parser qmlParser(&engine); |
| 204 | if (!qmlParser.parse()) { |
| 205 | const auto diagnosticMessages = qmlParser.diagnosticMessages(); |
| 206 | for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) { |
| 207 | fprintf(stderr, "%s\n" , |
| 208 | qPrintable(QStringLiteral("%1:%2:%3: %4" ) |
| 209 | .arg(inputFile) |
| 210 | .arg(m.loc.startLine) |
| 211 | .arg(m.loc.startColumn) |
| 212 | .arg(m.message))); |
| 213 | } |
| 214 | return EXIT_FAILURE; |
| 215 | } |
| 216 | |
| 217 | const QStringList resourceFiles = parser.values(option: resourceOption); |
| 218 | QQmlJSResourceFileMapper mapper(resourceFiles); |
| 219 | const QStringList metaResourceFiles = parser.values(option: metaResourceOption); |
| 220 | QQmlJSResourceFileMapper metaDataMapper(metaResourceFiles); |
| 221 | |
| 222 | const auto firstQml = [](const QStringList &paths) { |
| 223 | auto it = std::find_if(first: paths.cbegin(), last: paths.cend(), |
| 224 | pred: [](const QString &x) { return x.endsWith(s: u".qml"_s ); }); |
| 225 | if (it == paths.cend()) |
| 226 | return QString(); |
| 227 | return *it; |
| 228 | }; |
| 229 | // verify that we can map current file to qrc (then use the qrc path later) |
| 230 | const QStringList paths = mapper.resourcePaths(filter: QQmlJSResourceFileMapper::localFileFilter(file: url)); |
| 231 | if (paths.isEmpty()) { |
| 232 | fprintf(stderr, format: "Failed to find a resource path for file: %s\n" , qPrintable(inputFile)); |
| 233 | return EXIT_FAILURE; |
| 234 | } else if (paths.size() > 1) { |
| 235 | bool good = !firstQml(paths).isEmpty(); |
| 236 | good &= std::any_of(first: paths.cbegin(), last: paths.cend(), |
| 237 | pred: [](const QString &x) { return x.endsWith(s: u".h"_s ); }); |
| 238 | if (!good || paths.size() > 2) { |
| 239 | fprintf(stderr, format: "Unexpected resource paths for file: %s\n" , qPrintable(inputFile)); |
| 240 | return EXIT_FAILURE; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | QmltcCompilerInfo info; |
| 245 | info.outputCppFile = parser.value(option: outputCppOption); |
| 246 | info.outputHFile = parser.value(option: outputHOption); |
| 247 | info.resourcePath = firstQml(paths); |
| 248 | info.outputNamespace = parser.value(option: namespaceOption); |
| 249 | info.exportMacro = parser.value(option: exportOption); |
| 250 | info.exportInclude = parser.value(option: exportIncludeOption); |
| 251 | |
| 252 | if (info.outputCppFile.isEmpty()) { |
| 253 | fprintf(stderr, format: "An output C++ file is required. Pass one using --impl" ); |
| 254 | return EXIT_FAILURE; |
| 255 | } |
| 256 | if (info.outputHFile.isEmpty()) { |
| 257 | fprintf(stderr, format: "An output C++ header file is required. Pass one using --header" ); |
| 258 | return EXIT_FAILURE; |
| 259 | } |
| 260 | |
| 261 | QQmlJSImporter importer { importPaths, &mapper }; |
| 262 | importer.setMetaDataMapper(&metaDataMapper); |
| 263 | auto qmltcVisitor = [](QQmlJS::AST::Node *rootNode, QQmlJSImporter *self, |
| 264 | const QQmlJSImporter::ImportVisitorPrerequisites &p) { |
| 265 | QmltcVisitor v(p.m_target, self, p.m_logger, p.m_implicitImportDirectory, p.m_qmldirFiles); |
| 266 | QQmlJS::AST::Node::accept(node: rootNode, visitor: &v); |
| 267 | }; |
| 268 | importer.setImportVisitor(qmltcVisitor); |
| 269 | |
| 270 | QQmlJSLogger logger; |
| 271 | logger.setFilePath(url); |
| 272 | logger.setCode(sourceCode); |
| 273 | setupLogger(logger); |
| 274 | |
| 275 | auto currentScope = QQmlJSScope::create(); |
| 276 | if (parser.isSet(option: moduleOption)) |
| 277 | currentScope->setOwnModuleName(parser.value(option: moduleOption)); |
| 278 | |
| 279 | QmltcVisitor visitor(currentScope, &importer, &logger, |
| 280 | QQmlJSImportVisitor::implicitImportDirectory(localFile: url, mapper: &mapper), qmldirFiles); |
| 281 | visitor.setMode(QmltcVisitor::Compile); |
| 282 | QmltcTypeResolver typeResolver { &importer }; |
| 283 | typeResolver.init(visitor: &visitor, program: qmlParser.rootNode()); |
| 284 | |
| 285 | using PassManagerPtr = |
| 286 | std::unique_ptr<QQmlSA::PassManager, |
| 287 | decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>; |
| 288 | PassManagerPtr passMan(QQmlSA::PassManagerPrivate::createPassManager(visitor: &visitor, resolver: &typeResolver), |
| 289 | &QQmlSA::PassManagerPrivate::deletePassManager); |
| 290 | passMan->registerPropertyPass(pass: std::make_unique<QQmlJSLiteralBindingCheck>(args: passMan.get()), |
| 291 | moduleName: QString(), typeName: QString(), propertyName: QString()); |
| 292 | passMan->analyze(root: QQmlJSScope::createQQmlSAElement(visitor.result())); |
| 293 | |
| 294 | if (logger.hasErrors()) |
| 295 | return EXIT_FAILURE; |
| 296 | |
| 297 | QList<QQmlJS::DiagnosticMessage> warnings = importer.takeGlobalWarnings(); |
| 298 | if (!warnings.isEmpty()) { |
| 299 | logger.log(QStringLiteral("Type warnings occurred while compiling file:" ), id: qmlImport, |
| 300 | srcLocation: QQmlJS::SourceLocation()); |
| 301 | logger.processMessages(messages: warnings, id: qmlImport); |
| 302 | // Log_Import is critical for the compiler |
| 303 | return EXIT_FAILURE; |
| 304 | } |
| 305 | |
| 306 | QmltcCompiler compiler(url, &typeResolver, &visitor, &logger); |
| 307 | compiler.compile(info); |
| 308 | |
| 309 | if (logger.hasErrors()) |
| 310 | return EXIT_FAILURE; |
| 311 | |
| 312 | return EXIT_SUCCESS; |
| 313 | } |
| 314 | |