| 1 | // Copyright (C) 2016 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 <private/qqmljslexer_p.h> | 
| 5 | #include <private/qqmljsparser_p.h> | 
| 6 | #include <private/qqmljsast_p.h> | 
| 7 | #include <private/qqmljsdiagnosticmessage_p.h> | 
| 8 | #include <private/qqmldirparser_p.h> | 
| 9 | #include <private/qqmljsresourcefilemapper_p.h> | 
| 10 |  | 
| 11 | #include <QtCore/QCoreApplication> | 
| 12 | #include <QtCore/QDebug> | 
| 13 | #include <QtCore/QDateTime> | 
| 14 | #include <QtCore/QDir> | 
| 15 | #include <QtCore/QDirIterator> | 
| 16 | #include <QtCore/QFile> | 
| 17 | #include <QtCore/QFileInfo> | 
| 18 | #include <QtCore/QHash> | 
| 19 | #include <QtCore/QSet> | 
| 20 | #include <QtCore/QStringList> | 
| 21 | #include <QtCore/QMetaObject> | 
| 22 | #include <QtCore/QMetaProperty> | 
| 23 | #include <QtCore/QVariant> | 
| 24 | #include <QtCore/QVariantMap> | 
| 25 | #include <QtCore/QJsonObject> | 
| 26 | #include <QtCore/QJsonArray> | 
| 27 | #include <QtCore/QJsonDocument> | 
| 28 | #include <QtCore/QLibraryInfo> | 
| 29 | #include <QtCore/QLoggingCategory> | 
| 30 |  | 
| 31 | #include <iostream> | 
| 32 | #include <algorithm> | 
| 33 | #include <unordered_map> | 
| 34 | #include <unordered_set> | 
| 35 |  | 
| 36 | QT_USE_NAMESPACE | 
| 37 |  | 
| 38 | using namespace Qt::StringLiterals; | 
| 39 |  | 
| 40 | Q_LOGGING_CATEGORY(lcImportScanner, "qt.qml.import.scanner" ); | 
| 41 | Q_LOGGING_CATEGORY(lcImportScannerFiles, "qt.qml.import.scanner.files" ); | 
| 42 |  | 
| 43 | using FileImportsWithoutDepsCache = QHash<QString, QVariantList>; | 
| 44 |  | 
| 45 | namespace { | 
| 46 |  | 
| 47 | QStringList g_qmlImportPaths; | 
| 48 |  | 
| 49 | inline QString typeLiteral()         { return QStringLiteral("type" ); } | 
| 50 | inline QString versionLiteral()      { return QStringLiteral("version" ); } | 
| 51 | inline QString nameLiteral()         { return QStringLiteral("name" ); } | 
| 52 | inline QString relativePathLiteral() { return QStringLiteral("relativePath" ); } | 
| 53 | inline QString pluginsLiteral()      { return QStringLiteral("plugins" ); } | 
| 54 | inline QString pluginIsOptionalLiteral() { return QStringLiteral("pluginIsOptional" ); } | 
| 55 | inline QString pathLiteral()         { return QStringLiteral("path" ); } | 
| 56 | inline QString classnamesLiteral()   { return QStringLiteral("classnames" ); } | 
| 57 | inline QString dependenciesLiteral() { return QStringLiteral("dependencies" ); } | 
| 58 | inline QString moduleLiteral()       { return QStringLiteral("module" ); } | 
| 59 | inline QString javascriptLiteral()   { return QStringLiteral("javascript" ); } | 
| 60 | inline QString directoryLiteral()    { return QStringLiteral("directory" ); } | 
| 61 | inline QString linkTargetLiteral() | 
| 62 | { | 
| 63 |     return QStringLiteral("linkTarget" ); | 
| 64 | } | 
| 65 | inline QString componentsLiteral() { return QStringLiteral("components" ); } | 
| 66 | inline QString scriptsLiteral() { return QStringLiteral("scripts" ); } | 
| 67 | inline QString preferLiteral() { return QStringLiteral("prefer" ); } | 
| 68 |  | 
| 69 | void printUsage(const QString &appNameIn) | 
| 70 | { | 
| 71 |     const std::string appName = appNameIn.toStdString(); | 
| 72 |     const QString qmlPath = QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath); | 
| 73 |     std::cerr | 
| 74 |         << "Usage: "  << appName << " -rootPath path/to/app/qml/directory -importPath path/to/qt/qml/directory\n"  | 
| 75 |            "       "  << appName << " -qmlFiles file1 file2 -importPath path/to/qt/qml/directory\n"  | 
| 76 |            "       "  << appName << " -qrcFiles file1.qrc file2.qrc -importPath path/to/qt/qml/directory\n\n"  | 
| 77 |            "Example: "  << appName << " -rootPath . -importPath "  | 
| 78 |         << QDir::toNativeSeparators(pathName: qmlPath).toStdString() | 
| 79 |         << "\n\nOptions:\n"  | 
| 80 |             << "  -exclude <directory>: Exclude directory\n"  | 
| 81 |         << '\n'; | 
| 82 | } | 
| 83 |  | 
| 84 | QVariantList (QQmlJS::AST::UiHeaderItemList *, const QString &filePath) | 
| 85 | { | 
| 86 |     QVariantList imports; | 
| 87 |  | 
| 88 |     // Extract uri and version from the imports (which look like "import Foo.Bar 1.2.3") | 
| 89 |     for (QQmlJS::AST::UiHeaderItemList * = headerItemList; headerItemIt; headerItemIt = headerItemIt->next) { | 
| 90 |         QVariantMap import; | 
| 91 |         QQmlJS::AST::UiImport *importNode = QQmlJS::AST::cast<QQmlJS::AST::UiImport *>(ast: headerItemIt->headerItem); | 
| 92 |         if (!importNode) | 
| 93 |             continue; | 
| 94 |         // Handle directory imports | 
| 95 |         if (!importNode->fileName.isEmpty()) { | 
| 96 |             QString name = importNode->fileName.toString(); | 
| 97 |             import[nameLiteral()] = name; | 
| 98 |             if (name.endsWith(s: QLatin1String(".js" ))) { | 
| 99 |                 import[typeLiteral()] = javascriptLiteral(); | 
| 100 |             } else { | 
| 101 |                 import[typeLiteral()] = directoryLiteral(); | 
| 102 |             } | 
| 103 |  | 
| 104 |             import[pathLiteral()] = QDir::cleanPath( | 
| 105 |                     path: QFileInfo(filePath).path() + QLatin1Char('/') + name); | 
| 106 |         } else { | 
| 107 |             // Walk the id chain ("Foo" -> "Bar" -> etc) | 
| 108 |             QString  name; | 
| 109 |             QQmlJS::AST::UiQualifiedId *uri = importNode->importUri; | 
| 110 |             while (uri) { | 
| 111 |                 name.append(v: uri->name); | 
| 112 |                 name.append(c: QLatin1Char('.')); | 
| 113 |                 uri = uri->next; | 
| 114 |             } | 
| 115 |             name.chop(n: 1); // remove trailing "." | 
| 116 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) | 
| 117 |             if (name.startsWith(QLatin1String("QtQuick.Controls" )) && name.endsWith(QLatin1String("impl" ))) | 
| 118 |                 continue; | 
| 119 | #endif | 
| 120 |             if (!name.isEmpty()) | 
| 121 |                 import[nameLiteral()] = name; | 
| 122 |             import[typeLiteral()] = moduleLiteral(); | 
| 123 |             auto versionString = importNode->version | 
| 124 |                     ? QString::number(importNode->version->version.majorVersion()) | 
| 125 |                       + QLatin1Char('.') | 
| 126 |                       + QString::number(importNode->version->version.minorVersion()) | 
| 127 |                     : QString(); | 
| 128 |             if (!versionString.isEmpty()) | 
| 129 |                 import[versionLiteral()] = versionString; | 
| 130 |         } | 
| 131 |  | 
| 132 |         imports.append(t: import); | 
| 133 |     } | 
| 134 |  | 
| 135 |     return imports; | 
| 136 | } | 
| 137 |  | 
| 138 | QVariantList findQmlImportsInFileWithoutDeps(const QString &filePath, | 
| 139 |                                   FileImportsWithoutDepsCache | 
| 140 |                                   &fileImportsWithoutDepsCache); | 
| 141 |  | 
| 142 | static QString versionSuffix(QTypeRevision version) | 
| 143 | { | 
| 144 |     return QLatin1Char(' ') + QString::number(version.majorVersion()) + QLatin1Char('.') | 
| 145 |             + QString::number(version.minorVersion()); | 
| 146 | } | 
| 147 |  | 
| 148 | // Read the qmldir file, extract a list of plugins by | 
| 149 | // parsing the "plugin", "import", and "classname" directives. | 
| 150 | QVariantMap pluginsForModulePath(const QString &modulePath, | 
| 151 |                                  const QString &version, | 
| 152 |                                  FileImportsWithoutDepsCache | 
| 153 |                                  &fileImportsWithoutDepsCache) { | 
| 154 |     using Cache = QHash<QPair<QString, QString>, QVariantMap>; | 
| 155 |     static Cache pluginsCache; | 
| 156 |     const QPair<QString, QString> cacheKey = std::make_pair(x: modulePath, y: version); | 
| 157 |     const Cache::const_iterator it = pluginsCache.find(key: cacheKey); | 
| 158 |     if (it != pluginsCache.end()) { | 
| 159 |         return *it; | 
| 160 |     } | 
| 161 |  | 
| 162 |     QFile qmldirFile(modulePath + QLatin1String("/qmldir" )); | 
| 163 |     if (!qmldirFile.exists()) { | 
| 164 |         qWarning() << "qmldir file not found at"  << modulePath; | 
| 165 |         return QVariantMap(); | 
| 166 |     } | 
| 167 |  | 
| 168 |     if (!qmldirFile.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { | 
| 169 |         qWarning() << "qmldir file not found at"  << modulePath; | 
| 170 |         return QVariantMap(); | 
| 171 |     } | 
| 172 |  | 
| 173 |     QQmlDirParser parser; | 
| 174 |     parser.parse(source: QString::fromUtf8(ba: qmldirFile.readAll())); | 
| 175 |     if (parser.hasError()) { | 
| 176 |         qWarning() << "qmldir file malformed at"  << modulePath; | 
| 177 |         for (const auto &error : parser.errors(uri: QLatin1String("qmldir" ))) | 
| 178 |             qWarning() << error.message; | 
| 179 |         return QVariantMap(); | 
| 180 |     } | 
| 181 |  | 
| 182 |     QVariantMap pluginInfo; | 
| 183 |  | 
| 184 |     QStringList pluginNameList; | 
| 185 |     bool isOptional = false; | 
| 186 |     const auto plugins = parser.plugins(); | 
| 187 |     for (const auto &plugin : plugins) { | 
| 188 |         pluginNameList.append(t: plugin.name); | 
| 189 |         isOptional = plugin.optional; | 
| 190 |     } | 
| 191 |     pluginInfo[pluginsLiteral()] = pluginNameList.join(sep: QLatin1Char(' ')); | 
| 192 |  | 
| 193 |     if (plugins.size() > 1) { | 
| 194 |         qWarning() << QStringLiteral("Warning: \"%1\" contains multiple plugin entries. This is discouraged and does not support marking plugins as optional." ).arg(a: modulePath); | 
| 195 |         isOptional = false; | 
| 196 |     } | 
| 197 |  | 
| 198 |     if (isOptional) { | 
| 199 |         pluginInfo[pluginIsOptionalLiteral()] = true; | 
| 200 |     } | 
| 201 |  | 
| 202 |     if (!parser.linkTarget().isEmpty()) { | 
| 203 |         pluginInfo[linkTargetLiteral()] = parser.linkTarget(); | 
| 204 |     } | 
| 205 |  | 
| 206 |     pluginInfo[classnamesLiteral()] = parser.classNames().join(sep: QLatin1Char(' ')); | 
| 207 |  | 
| 208 |     QStringList importsAndDependencies; | 
| 209 |     const auto dependencies = parser.dependencies(); | 
| 210 |     for (const auto &dependency : dependencies) | 
| 211 |         importsAndDependencies.append(t: dependency.module + versionSuffix(version: dependency.version)); | 
| 212 |  | 
| 213 |     const auto imports = parser.imports(); | 
| 214 |     for (const auto &import : imports) { | 
| 215 |         if (import.flags & QQmlDirParser::Import::Auto) { | 
| 216 |             importsAndDependencies.append( | 
| 217 |                         t: import.module + QLatin1Char(' ') | 
| 218 |                         + (version.isEmpty() ? QString::fromLatin1(ba: "auto" ) : version)); | 
| 219 |         } else if (import.version.isValid()) { | 
| 220 |             importsAndDependencies.append(t: import.module + versionSuffix(version: import.version)); | 
| 221 |         } else { | 
| 222 |             importsAndDependencies.append(t: import.module); | 
| 223 |         } | 
| 224 |     } | 
| 225 |  | 
| 226 |     QVariantList importsFromFiles; | 
| 227 |     QStringList componentFiles; | 
| 228 |     QStringList scriptFiles; | 
| 229 |     const auto components = parser.components(); | 
| 230 |     for (const auto &component : components) { | 
| 231 |         const QString componentFullPath = modulePath + QLatin1Char('/') + component.fileName; | 
| 232 |         componentFiles.append(t: componentFullPath); | 
| 233 |         importsFromFiles | 
| 234 |                 += findQmlImportsInFileWithoutDeps(filePath: componentFullPath, | 
| 235 |                                                    fileImportsWithoutDepsCache); | 
| 236 |     } | 
| 237 |     const auto scripts = parser.scripts(); | 
| 238 |     for (const auto &script : scripts) { | 
| 239 |         const QString scriptFullPath = modulePath + QLatin1Char('/') + script.fileName; | 
| 240 |         scriptFiles.append(t: scriptFullPath); | 
| 241 |         importsFromFiles | 
| 242 |                 += findQmlImportsInFileWithoutDeps(filePath: scriptFullPath, | 
| 243 |                                                    fileImportsWithoutDepsCache); | 
| 244 |     } | 
| 245 |  | 
| 246 |     for (const QVariant &import : importsFromFiles) { | 
| 247 |         const QVariantMap details = qvariant_cast<QVariantMap>(v: import); | 
| 248 |         if (details.value(key: typeLiteral()) != moduleLiteral()) | 
| 249 |             continue; | 
| 250 |         const QString name = details.value(key: nameLiteral()).toString(); | 
| 251 |         const QString version = details.value(key: versionLiteral()).toString(); | 
| 252 |         importsAndDependencies.append( | 
| 253 |                     t: version.isEmpty() ? name : (name + QLatin1Char(' ') + version)); | 
| 254 |     } | 
| 255 |  | 
| 256 |     if (!importsAndDependencies.isEmpty()) { | 
| 257 |         importsAndDependencies.removeDuplicates(); | 
| 258 |         pluginInfo[dependenciesLiteral()] = importsAndDependencies; | 
| 259 |     } | 
| 260 |     if (!componentFiles.isEmpty()) { | 
| 261 |         componentFiles.sort(); | 
| 262 |         pluginInfo[componentsLiteral()] = componentFiles; | 
| 263 |     } | 
| 264 |     if (!scriptFiles.isEmpty()) { | 
| 265 |         scriptFiles.sort(); | 
| 266 |         pluginInfo[scriptsLiteral()] = scriptFiles; | 
| 267 |     } | 
| 268 |  | 
| 269 |     if (!parser.preferredPath().isEmpty()) | 
| 270 |         pluginInfo[preferLiteral()] = parser.preferredPath(); | 
| 271 |  | 
| 272 |     pluginsCache.insert(key: cacheKey, value: pluginInfo); | 
| 273 |     return pluginInfo; | 
| 274 | } | 
| 275 |  | 
| 276 | // Search for a given qml import in g_qmlImportPaths and return a pair | 
| 277 | // of absolute / relative paths (for deployment). | 
| 278 | QPair<QString, QString> resolveImportPath(const QString &uri, const QString &version) | 
| 279 | { | 
| 280 |     const QLatin1Char dot('.'); | 
| 281 |     const QLatin1Char slash('/'); | 
| 282 |     const QStringList parts = uri.split(sep: dot, behavior: Qt::SkipEmptyParts); | 
| 283 |  | 
| 284 |     QString ver = version; | 
| 285 |     QPair<QString, QString> candidate; | 
| 286 |     while (true) { | 
| 287 |         for (const QString &qmlImportPath : std::as_const(t&: g_qmlImportPaths)) { | 
| 288 |             // Search for the most specific version first, and search | 
| 289 |             // also for the version in parent modules. For example: | 
| 290 |             // - qml/QtQml/Models.2.0 | 
| 291 |             // - qml/QtQml.2.0/Models | 
| 292 |             // - qml/QtQml/Models.2 | 
| 293 |             // - qml/QtQml.2/Models | 
| 294 |             // - qml/QtQml/Models | 
| 295 |             if (ver.isEmpty()) { | 
| 296 |                 QString relativePath = parts.join(sep: slash); | 
| 297 |                 if (relativePath.endsWith(c: slash)) | 
| 298 |                     relativePath.chop(n: 1); | 
| 299 |                 const QString candidatePath = QDir::cleanPath(path: qmlImportPath + slash + relativePath); | 
| 300 |                 const QDir candidateDir(candidatePath); | 
| 301 |                 if (candidateDir.exists()) { | 
| 302 |                     const auto newCandidate = qMakePair(value1: candidatePath, value2&: relativePath); // import found | 
| 303 |                     if (candidateDir.exists(name: u"qmldir"_s )) // if it has a qmldir, we are fine | 
| 304 |                         return newCandidate; | 
| 305 |                     else if (candidate.first.isEmpty()) | 
| 306 |                         candidate = newCandidate; | 
| 307 |                     // otherwise we keep looking if we can find the module again (with a qmldir this time) | 
| 308 |                 } | 
| 309 |             } else { | 
| 310 |                 for (int index = parts.size() - 1; index >= 0; --index) { | 
| 311 |                     QString relativePath = parts.mid(pos: 0, len: index + 1).join(sep: slash) | 
| 312 |                         + dot + ver + slash + parts.mid(pos: index + 1).join(sep: slash); | 
| 313 |                     if (relativePath.endsWith(c: slash)) | 
| 314 |                         relativePath.chop(n: 1); | 
| 315 |                     const QString candidatePath = QDir::cleanPath(path: qmlImportPath + slash + relativePath); | 
| 316 |                     const QDir candidateDir(candidatePath); | 
| 317 |                     if (candidateDir.exists()) { | 
| 318 |                         const auto newCandidate = qMakePair(value1: candidatePath, value2&: relativePath); // import found | 
| 319 |                         if (candidateDir.exists(name: u"qmldir"_s )) | 
| 320 |                             return newCandidate; | 
| 321 |                         else if (candidate.first.isEmpty()) | 
| 322 |                             candidate = newCandidate; | 
| 323 |                     } | 
| 324 |                 } | 
| 325 |             } | 
| 326 |         } | 
| 327 |  | 
| 328 |         // Remove the last version digit; stop if there are none left | 
| 329 |         if (ver.isEmpty()) | 
| 330 |             break; | 
| 331 |  | 
| 332 |         int lastDot = ver.lastIndexOf(c: dot); | 
| 333 |         if (lastDot == -1) | 
| 334 |             ver.clear(); | 
| 335 |         else | 
| 336 |             ver = ver.mid(position: 0, n: lastDot); | 
| 337 |     } | 
| 338 |  | 
| 339 |     return candidate; | 
| 340 | } | 
| 341 |  | 
| 342 | // Provides a hasher for module details stored in a QVariantMap disguised as a QVariant.. | 
| 343 | // Only supports a subset of types. | 
| 344 | struct ImportVariantHasher { | 
| 345 |    std::size_t operator()(const QVariant &importVariant) const | 
| 346 |    { | 
| 347 |        size_t computedHash = 0; | 
| 348 |        QVariantMap importMap = qvariant_cast<QVariantMap>(v: importVariant); | 
| 349 |        for (auto it = importMap.constKeyValueBegin(); it != importMap.constKeyValueEnd(); ++it) { | 
| 350 |            const QString &key = it->first; | 
| 351 |            const QVariant &value = it->second; | 
| 352 |  | 
| 353 |            if (!value.isValid() || value.isNull()) { | 
| 354 |                computedHash = qHashMulti(seed: computedHash, args: key, args: 0); | 
| 355 |                continue; | 
| 356 |            } | 
| 357 |  | 
| 358 |            const auto valueTypeId = value.typeId(); | 
| 359 |            switch (valueTypeId) { | 
| 360 |            case QMetaType::QString: | 
| 361 |                computedHash = qHashMulti(seed: computedHash, args: key, args: value.toString()); | 
| 362 |                break; | 
| 363 |            case QMetaType::Bool: | 
| 364 |                computedHash = qHashMulti(seed: computedHash, args: key, args: value.toBool()); | 
| 365 |                break; | 
| 366 |            case QMetaType::QStringList: | 
| 367 |                computedHash = qHashMulti(seed: computedHash, args: key, args: value.toStringList()); | 
| 368 |                break; | 
| 369 |            default: | 
| 370 |                Q_ASSERT_X(valueTypeId, "ImportVariantHasher" , "Invalid variant type detected" ); | 
| 371 |                break; | 
| 372 |            } | 
| 373 |        } | 
| 374 |  | 
| 375 |        return computedHash; | 
| 376 |    } | 
| 377 | }; | 
| 378 |  | 
| 379 | using ImportDetailsAndDeps = QPair<QVariantMap, QStringList>; | 
| 380 |  | 
| 381 | // Returns the import information as it will be written out to the json / .cmake file. | 
| 382 | // The dependencies are not stored in the same QVariantMap because we don't currently need that | 
| 383 | // information in the output file. | 
| 384 | ImportDetailsAndDeps | 
| 385 | getImportDetails(const QVariant &inputImport, | 
| 386 |                  FileImportsWithoutDepsCache &fileImportsWithoutDepsCache) { | 
| 387 |  | 
| 388 |     using Cache = std::unordered_map<QVariant, ImportDetailsAndDeps, ImportVariantHasher>; | 
| 389 |     static Cache cache; | 
| 390 |  | 
| 391 |     const Cache::const_iterator it = cache.find(x: inputImport); | 
| 392 |     if (it != cache.end()) { | 
| 393 |         return it->second; | 
| 394 |     } | 
| 395 |  | 
| 396 |     QVariantMap import = qvariant_cast<QVariantMap>(v: inputImport); | 
| 397 |     QStringList dependencies; | 
| 398 |     if (import.value(key: typeLiteral()) == moduleLiteral()) { | 
| 399 |         const QString version = import.value(key: versionLiteral()).toString(); | 
| 400 |         const QPair<QString, QString> paths = | 
| 401 |             resolveImportPath(uri: import.value(key: nameLiteral()).toString(), version); | 
| 402 |         QVariantMap plugininfo; | 
| 403 |         if (!paths.first.isEmpty()) { | 
| 404 |             import.insert(key: pathLiteral(), value: paths.first); | 
| 405 |             import.insert(key: relativePathLiteral(), value: paths.second); | 
| 406 |             plugininfo = pluginsForModulePath(modulePath: paths.first, | 
| 407 |                                               version, | 
| 408 |                                               fileImportsWithoutDepsCache); | 
| 409 |         } | 
| 410 |         QString linkTarget = plugininfo.value(key: linkTargetLiteral()).toString(); | 
| 411 |         QString plugins = plugininfo.value(key: pluginsLiteral()).toString(); | 
| 412 |         bool isOptional = plugininfo.value(key: pluginIsOptionalLiteral(), defaultValue: QVariant(false)).toBool(); | 
| 413 |         QString classnames = plugininfo.value(key: classnamesLiteral()).toString(); | 
| 414 |         QStringList components = plugininfo.value(key: componentsLiteral()).toStringList(); | 
| 415 |         QStringList scripts = plugininfo.value(key: scriptsLiteral()).toStringList(); | 
| 416 |         QString prefer = plugininfo.value(key: preferLiteral()).toString(); | 
| 417 |         if (!linkTarget.isEmpty()) | 
| 418 |             import.insert(key: linkTargetLiteral(), value: linkTarget); | 
| 419 |         if (!plugins.isEmpty()) | 
| 420 |             import.insert(QStringLiteral("plugin" ), value: plugins); | 
| 421 |         if (isOptional) | 
| 422 |             import.insert(key: pluginIsOptionalLiteral(), value: true); | 
| 423 |         if (!classnames.isEmpty()) | 
| 424 |             import.insert(QStringLiteral("classname" ), value: classnames); | 
| 425 |         if (plugininfo.contains(key: dependenciesLiteral())) { | 
| 426 |             dependencies = plugininfo.value(key: dependenciesLiteral()).toStringList(); | 
| 427 |         } | 
| 428 |         if (!components.isEmpty()) { | 
| 429 |             components.removeDuplicates(); | 
| 430 |             import.insert(key: componentsLiteral(), value: components); | 
| 431 |         } | 
| 432 |         if (!scripts.isEmpty()) { | 
| 433 |             scripts.removeDuplicates(); | 
| 434 |             import.insert(key: scriptsLiteral(), value: scripts); | 
| 435 |         } | 
| 436 |         if (!prefer.isEmpty()) { | 
| 437 |             import.insert(key: preferLiteral(), value: prefer); | 
| 438 |         } | 
| 439 |     } | 
| 440 |     import.remove(key: versionLiteral()); | 
| 441 |  | 
| 442 |     const ImportDetailsAndDeps result = {import, dependencies}; | 
| 443 |     cache.insert(x: {inputImport, result}); | 
| 444 |     return result; | 
| 445 | } | 
| 446 |  | 
| 447 | // Parse a dependency string line into a QVariantMap, to be used as a key when processing imports | 
| 448 | // in getGetDetailedModuleImportsIncludingDependencies. | 
| 449 | QVariantMap dependencyStringToImport(const QString &line) { | 
| 450 |     const auto dep = QStringView{line}.split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts); | 
| 451 |     const QString name = dep[0].toString(); | 
| 452 |     QVariantMap depImport; | 
| 453 |     depImport[typeLiteral()] = moduleLiteral(); | 
| 454 |     depImport[nameLiteral()] = name; | 
| 455 |     if (dep.size() > 1) | 
| 456 |         depImport[versionLiteral()] = dep[1].toString(); | 
| 457 |     return depImport; | 
| 458 | } | 
| 459 |  | 
| 460 | // Returns details of given input import and its recursive module dependencies. | 
| 461 | // The details include absolute file system paths for the the module plugin, components, | 
| 462 | // etc. | 
| 463 | // An internal cache is used to prevent repeated computation for the same input module. | 
| 464 | QVariantList getGetDetailedModuleImportsIncludingDependencies( | 
| 465 |         const QVariant &inputImport, | 
| 466 |         FileImportsWithoutDepsCache &fileImportsWithoutDepsCache) | 
| 467 | { | 
| 468 |     using Cache = std::unordered_map<QVariant, QVariantList, ImportVariantHasher>; | 
| 469 |     static Cache importsCacheWithDeps; | 
| 470 |  | 
| 471 |     const Cache::const_iterator it = importsCacheWithDeps.find(x: inputImport); | 
| 472 |     if (it != importsCacheWithDeps.end()) { | 
| 473 |         return it->second; | 
| 474 |     } | 
| 475 |  | 
| 476 |     QVariantList done; | 
| 477 |     QVariantList importsToProcess; | 
| 478 |     std::unordered_set<QVariant, ImportVariantHasher> importsSeen; | 
| 479 |     importsToProcess.append(t: inputImport); | 
| 480 |  | 
| 481 |     for (int i = 0; i < importsToProcess.size(); ++i) { | 
| 482 |         const QVariant importToProcess = importsToProcess.at(i); | 
| 483 |         auto [details, deps] = getImportDetails(inputImport: importToProcess, fileImportsWithoutDepsCache); | 
| 484 |         if (details.value(key: typeLiteral()) == moduleLiteral()) { | 
| 485 |             for (const QString &line : deps) { | 
| 486 |                 const QVariantMap depImport = dependencyStringToImport(line); | 
| 487 |  | 
| 488 |                 // Skip self-dependencies. | 
| 489 |                 if (depImport == importToProcess) | 
| 490 |                     continue; | 
| 491 |  | 
| 492 |                 if (importsSeen.find(x: depImport) == importsSeen.end()) { | 
| 493 |                     importsToProcess.append(t: depImport); | 
| 494 |                     importsSeen.insert(x: depImport); | 
| 495 |                 } | 
| 496 |             } | 
| 497 |         } | 
| 498 |         done.append(t: details); | 
| 499 |     } | 
| 500 |  | 
| 501 |     importsCacheWithDeps.insert(x: {inputImport, done}); | 
| 502 |     return done; | 
| 503 | } | 
| 504 |  | 
| 505 | QVariantList mergeImports(const QVariantList &a, const QVariantList &b); | 
| 506 |  | 
| 507 | // Returns details of given input imports and their recursive module dependencies. | 
| 508 | QVariantList getGetDetailedModuleImportsIncludingDependencies( | 
| 509 |         const QVariantList &inputImports, | 
| 510 |         FileImportsWithoutDepsCache &fileImportsWithoutDepsCache) | 
| 511 | { | 
| 512 |     QVariantList result; | 
| 513 |  | 
| 514 |     // Get rid of duplicates in input module list. | 
| 515 |     QVariantList inputImportsCopy; | 
| 516 |     inputImportsCopy = mergeImports(a: inputImportsCopy, b: inputImports); | 
| 517 |  | 
| 518 |     // Collect recursive dependencies for each input module and merge into result, discarding | 
| 519 |     // duplicates. | 
| 520 |     for (auto it = inputImportsCopy.begin(); it != inputImportsCopy.end(); ++it) { | 
| 521 |         QVariantList imports = getGetDetailedModuleImportsIncludingDependencies( | 
| 522 |                     inputImport: *it, fileImportsWithoutDepsCache); | 
| 523 |         result = mergeImports(a: result, b: imports); | 
| 524 |     } | 
| 525 |     return result; | 
| 526 | } | 
| 527 |  | 
| 528 | // Scan a single qml file for import statements | 
| 529 | QVariantList findQmlImportsInQmlCode(const QString &filePath, const QString &code) | 
| 530 | { | 
| 531 |     qCDebug(lcImportScannerFiles) << "Parsing code and finding imports in"  << filePath | 
| 532 |                                   << "TS:"  << QDateTime::currentMSecsSinceEpoch(); | 
| 533 |  | 
| 534 |     QQmlJS::Engine engine; | 
| 535 |     QQmlJS::Lexer lexer(&engine); | 
| 536 |     lexer.setCode(code, /*line = */ lineno: 1); | 
| 537 |     QQmlJS::Parser parser(&engine); | 
| 538 |  | 
| 539 |     if (!parser.parse() || !parser.diagnosticMessages().isEmpty()) { | 
| 540 |         // Extract errors from the parser | 
| 541 |         const auto diagnosticMessages = parser.diagnosticMessages(); | 
| 542 |         for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) { | 
| 543 |             std::cerr << QDir::toNativeSeparators(filePath).toStdString() << ':' | 
| 544 |                       << m.loc.startLine << ':' << m.message.toStdString() << std::endl; | 
| 545 |         } | 
| 546 |         return QVariantList(); | 
| 547 |     } | 
| 548 |     return findImportsInAst(parser.ast()->headers, filePath); | 
| 549 | } | 
| 550 |  | 
| 551 | // Scan a single qml file for import statements | 
| 552 | QVariantList findQmlImportsInQmlFile(const QString &filePath) | 
| 553 | { | 
| 554 |     QFile file(filePath); | 
| 555 |     if (!file.open(flags: QIODevice::ReadOnly)) { | 
| 556 |            std::cerr << "Cannot open input file "  << QDir::toNativeSeparators(pathName: file.fileName()).toStdString() | 
| 557 |                      << ':' << file.errorString().toStdString() << std::endl; | 
| 558 |            return QVariantList(); | 
| 559 |     } | 
| 560 |     QString code = QString::fromUtf8(ba: file.readAll()); | 
| 561 |     return findQmlImportsInQmlCode(filePath, code); | 
| 562 | } | 
| 563 |  | 
| 564 | struct ImportCollector : public QQmlJS::Directives | 
| 565 | { | 
| 566 |     QVariantList imports; | 
| 567 |  | 
| 568 |     void importFile(const QString &jsfile, const QString &module, int line, int column) override | 
| 569 |     { | 
| 570 |         QVariantMap entry; | 
| 571 |         entry[typeLiteral()] = javascriptLiteral(); | 
| 572 |         entry[pathLiteral()] = jsfile; | 
| 573 |         imports << entry; | 
| 574 |  | 
| 575 |         Q_UNUSED(module); | 
| 576 |         Q_UNUSED(line); | 
| 577 |         Q_UNUSED(column); | 
| 578 |     } | 
| 579 |  | 
| 580 |     void importModule(const QString &uri, const QString &version, const QString &module, int line, int column) override | 
| 581 |     { | 
| 582 |         QVariantMap entry; | 
| 583 |         if (uri.contains(c: QLatin1Char('/'))) { | 
| 584 |             entry[typeLiteral()] = directoryLiteral(); | 
| 585 |             entry[nameLiteral()] = uri; | 
| 586 |         } else { | 
| 587 |             entry[typeLiteral()] = moduleLiteral(); | 
| 588 |             entry[nameLiteral()] = uri; | 
| 589 |             if (!version.isEmpty()) | 
| 590 |                 entry[versionLiteral()] = version; | 
| 591 |         } | 
| 592 |         imports << entry; | 
| 593 |  | 
| 594 |         Q_UNUSED(module); | 
| 595 |         Q_UNUSED(line); | 
| 596 |         Q_UNUSED(column); | 
| 597 |     } | 
| 598 | }; | 
| 599 |  | 
| 600 | // Scan a single javascrupt file for import statements | 
| 601 | QVariantList findQmlImportsInJavascriptFile(const QString &filePath) | 
| 602 | { | 
| 603 |     QFile file(filePath); | 
| 604 |     if (!file.open(flags: QIODevice::ReadOnly)) { | 
| 605 |            std::cerr << "Cannot open input file "  << QDir::toNativeSeparators(pathName: file.fileName()).toStdString() | 
| 606 |                      << ':' << file.errorString().toStdString() << std::endl; | 
| 607 |            return QVariantList(); | 
| 608 |     } | 
| 609 |  | 
| 610 |     QString sourceCode = QString::fromUtf8(ba: file.readAll()); | 
| 611 |     file.close(); | 
| 612 |  | 
| 613 |     QQmlJS::Engine ee; | 
| 614 |     ImportCollector collector; | 
| 615 |     ee.setDirectives(&collector); | 
| 616 |     QQmlJS::Lexer lexer(&ee); | 
| 617 |     lexer.setCode(code: sourceCode, /*line*/lineno: 1, /*qml mode*/qmlMode: false); | 
| 618 |     QQmlJS::Parser parser(&ee); | 
| 619 |     parser.parseProgram(); | 
| 620 |  | 
| 621 |     const auto diagnosticMessages = parser.diagnosticMessages(); | 
| 622 |     for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) | 
| 623 |         if (m.isError()) | 
| 624 |             return QVariantList(); | 
| 625 |  | 
| 626 |     return collector.imports; | 
| 627 | } | 
| 628 |  | 
| 629 | // Scan a single qml or js file for import statements without resolving dependencies. | 
| 630 | QVariantList findQmlImportsInFileWithoutDeps(const QString &filePath, | 
| 631 |                                   FileImportsWithoutDepsCache | 
| 632 |                                   &fileImportsWithoutDepsCache) | 
| 633 | { | 
| 634 |     const FileImportsWithoutDepsCache::const_iterator it = | 
| 635 |             fileImportsWithoutDepsCache.find(key: filePath); | 
| 636 |     if (it != fileImportsWithoutDepsCache.end()) { | 
| 637 |         return *it; | 
| 638 |     } | 
| 639 |  | 
| 640 |     QVariantList imports; | 
| 641 |     if (filePath == QLatin1String("-" )) { | 
| 642 |         QFile f; | 
| 643 |         if (f.open(stdin, ioFlags: QIODevice::ReadOnly)) | 
| 644 |             imports = findQmlImportsInQmlCode(filePath: QLatin1String("<stdin>" ), code: QString::fromUtf8(ba: f.readAll())); | 
| 645 |     } else if (filePath.endsWith(s: QLatin1String(".qml" ))) { | 
| 646 |         imports = findQmlImportsInQmlFile(filePath); | 
| 647 |     } else if (filePath.endsWith(s: QLatin1String(".js" ))) { | 
| 648 |         imports = findQmlImportsInJavascriptFile(filePath); | 
| 649 |     } else { | 
| 650 |         qCDebug(lcImportScanner) << "Skipping file because it's not a .qml/.js file" ; | 
| 651 |         return imports; | 
| 652 |     } | 
| 653 |  | 
| 654 |     fileImportsWithoutDepsCache.insert(key: filePath, value: imports); | 
| 655 |     return imports; | 
| 656 | } | 
| 657 |  | 
| 658 | // Scan a single qml or js file for import statements, resolve dependencies and return the full | 
| 659 | // list of modules the file depends on. | 
| 660 | QVariantList findQmlImportsInFile(const QString &filePath, | 
| 661 |                                   FileImportsWithoutDepsCache | 
| 662 |                                   &fileImportsWithoutDepsCache) { | 
| 663 |     const auto fileProcessTimeBegin = QDateTime::currentDateTime(); | 
| 664 |  | 
| 665 |     QVariantList imports = findQmlImportsInFileWithoutDeps(filePath, | 
| 666 |                                                            fileImportsWithoutDepsCache); | 
| 667 |     if (imports.empty()) | 
| 668 |         return imports; | 
| 669 |  | 
| 670 |     const auto pathsTimeBegin = QDateTime::currentDateTime(); | 
| 671 |  | 
| 672 |     qCDebug(lcImportScanner) << "Finding module paths for imported modules in"  << filePath | 
| 673 |                              << "TS:"  << pathsTimeBegin.toMSecsSinceEpoch(); | 
| 674 |     QVariantList importPaths = getGetDetailedModuleImportsIncludingDependencies( | 
| 675 |                 inputImports: imports, fileImportsWithoutDepsCache); | 
| 676 |  | 
| 677 |     const auto pathsTimeEnd = QDateTime::currentDateTime(); | 
| 678 |     const auto duration = pathsTimeBegin.msecsTo(pathsTimeEnd); | 
| 679 |     const auto fileProcessingDuration = fileProcessTimeBegin.msecsTo(pathsTimeEnd); | 
| 680 |     qCDebug(lcImportScanner) << "Found module paths:"  << importPaths.size() | 
| 681 |                              << "TS:"  << pathsTimeEnd.toMSecsSinceEpoch() | 
| 682 |                              << "Path resolution duration:"  << duration << "msecs" ; | 
| 683 |     qCDebug(lcImportScanner) << "Scan duration:"  << fileProcessingDuration << "msecs" ; | 
| 684 |     return importPaths; | 
| 685 | } | 
| 686 |  | 
| 687 | // Merge two lists of imports, discard duplicates. | 
| 688 | // Empirical tests show that for a small amount of values, the n^2 QVariantList comparison | 
| 689 | // is still faster than using an unordered_set + hashing a complex QVariantMap. | 
| 690 | QVariantList mergeImports(const QVariantList &a, const QVariantList &b) | 
| 691 | { | 
| 692 |     QVariantList merged = a; | 
| 693 |     for (const QVariant &variant : b) { | 
| 694 |         if (!merged.contains(t: variant)) | 
| 695 |             merged.append(t: variant); | 
| 696 |     } | 
| 697 |     return merged; | 
| 698 | } | 
| 699 |  | 
| 700 | // Predicates needed by findQmlImportsInDirectory. | 
| 701 |  | 
| 702 | struct isMetainfo { | 
| 703 |     bool operator() (const QFileInfo &x) const { | 
| 704 |         return x.suffix() == QLatin1String("metainfo" ); | 
| 705 |     } | 
| 706 | }; | 
| 707 |  | 
| 708 | struct pathStartsWith { | 
| 709 |     pathStartsWith(const QString &path) : _path(path) {} | 
| 710 |     bool operator() (const QString &x) const { | 
| 711 |         return _path.startsWith(s: x); | 
| 712 |     } | 
| 713 |     const QString _path; | 
| 714 | }; | 
| 715 |  | 
| 716 | static QStringList excludedDirectories = { | 
| 717 |     ".qtcreator"_L1 , ".qtc_clangd"_L1 , // Windows does not consider these hidden | 
| 718 | #ifdef Q_OS_WIN | 
| 719 |     "release"_L1 , "debug"_L1  | 
| 720 | #endif | 
| 721 | }; | 
| 722 |  | 
| 723 | static bool isExcluded(const QFileInfo &dir) | 
| 724 | { | 
| 725 |     if (excludedDirectories.contains(str: dir.fileName())) | 
| 726 |         return true; | 
| 727 |  | 
| 728 |     const QString &path = dir.absoluteFilePath(); | 
| 729 |     // Skip obvious build output directories | 
| 730 |     return path.contains(s: "Debug-iphoneos"_L1 ) || path.contains(s: "Release-iphoneos"_L1 ) | 
| 731 |         || path.contains(s: "Debug-iphonesimulator"_L1 ) || path.contains(s: "Release-iphonesimulator"_L1 ); | 
| 732 | } | 
| 733 |  | 
| 734 | // Scan all qml files in directory for import statements | 
| 735 | QVariantList findQmlImportsInDirectory(const QString &qmlDir, | 
| 736 |                                        FileImportsWithoutDepsCache | 
| 737 |                                        &fileImportsWithoutDepsCache) | 
| 738 | { | 
| 739 |     QVariantList ret; | 
| 740 |     if (qmlDir.isEmpty()) | 
| 741 |         return ret; | 
| 742 |  | 
| 743 |     QDirIterator iterator(qmlDir, QDir::AllDirs | QDir::NoDotDot, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); | 
| 744 |     QStringList blacklist; | 
| 745 |  | 
| 746 |     while (iterator.hasNext()) { | 
| 747 |         iterator.next(); | 
| 748 |         if (isExcluded(dir: iterator.fileInfo())) | 
| 749 |             continue; | 
| 750 |         const QString path = iterator.filePath(); | 
| 751 |         const QFileInfoList entries = QDir(path).entryInfoList(); | 
| 752 |  | 
| 753 |         // Skip designer related stuff | 
| 754 |         if (std::find_if(first: entries.cbegin(), last: entries.cend(), pred: isMetainfo()) != entries.cend()) { | 
| 755 |             blacklist << path; | 
| 756 |             continue; | 
| 757 |         } | 
| 758 |  | 
| 759 |         if (std::find_if(first: blacklist.cbegin(), last: blacklist.cend(), pred: pathStartsWith(path)) != blacklist.cend()) | 
| 760 |             continue; | 
| 761 |  | 
| 762 |         for (const QFileInfo &x : entries) | 
| 763 |             if (x.isFile()) { | 
| 764 |                 const auto entryAbsolutePath = x.absoluteFilePath(); | 
| 765 |                 qCDebug(lcImportScanner) << "Scanning file"  << entryAbsolutePath | 
| 766 |                                          << "TS:"  << QDateTime::currentMSecsSinceEpoch(); | 
| 767 |                 ret = mergeImports(a: ret, | 
| 768 |                                    b: findQmlImportsInFile( | 
| 769 |                                        filePath: entryAbsolutePath, | 
| 770 |                                        fileImportsWithoutDepsCache)); | 
| 771 |             } | 
| 772 |      } | 
| 773 |      return ret; | 
| 774 | } | 
| 775 |  | 
| 776 | // Find qml imports recursively from a root set of qml files. | 
| 777 | // The directories in qmlDirs are searched recursively. | 
| 778 | // The files in qmlFiles parsed directly. | 
| 779 | QVariantList findQmlImportsRecursively(const QStringList &qmlDirs, | 
| 780 |                                        const QStringList &scanFiles, | 
| 781 |                                        FileImportsWithoutDepsCache | 
| 782 |                                        &fileImportsWithoutDepsCache) | 
| 783 | { | 
| 784 |     QVariantList ret; | 
| 785 |  | 
| 786 |     qCDebug(lcImportScanner) << "Scanning"  << qmlDirs.size() << "root directories and"  | 
| 787 |                              << scanFiles.size() << "files." ; | 
| 788 |  | 
| 789 |     // Scan all app root qml directories for imports | 
| 790 |     for (const QString &qmlDir : qmlDirs) { | 
| 791 |         qCDebug(lcImportScanner) << "Scanning root"  << qmlDir | 
| 792 |                                  << "TS:"  << QDateTime::currentMSecsSinceEpoch(); | 
| 793 |         QVariantList imports = findQmlImportsInDirectory(qmlDir, fileImportsWithoutDepsCache); | 
| 794 |         ret = mergeImports(a: ret, b: imports); | 
| 795 |     } | 
| 796 |  | 
| 797 |     // Scan app qml files for imports | 
| 798 |     for (const QString &file : scanFiles) { | 
| 799 |         qCDebug(lcImportScanner) << "Scanning file"  << file | 
| 800 |                                  << "TS:"  << QDateTime::currentMSecsSinceEpoch(); | 
| 801 |         QVariantList imports = findQmlImportsInFile(filePath: file, fileImportsWithoutDepsCache); | 
| 802 |         ret = mergeImports(a: ret, b: imports); | 
| 803 |     } | 
| 804 |  | 
| 805 |     return ret; | 
| 806 | } | 
| 807 |  | 
| 808 |  | 
| 809 | QString generateCmakeIncludeFileContent(const QVariantList &importList) { | 
| 810 |     // The function assumes that "list" is a QVariantList with 0 or more QVariantMaps, where | 
| 811 |     // each map contains QString -> QVariant<QString> mappings. This matches with the structure | 
| 812 |     // that qmake parses for static qml plugin auto imporitng. | 
| 813 |     // So: [ {"a": "a","b": "b"}, {"c": "c"} ] | 
| 814 |     QString content; | 
| 815 |     QTextStream s(&content); | 
| 816 |     int importsCount = 0; | 
| 817 |     for (const QVariant &importVariant: importList) { | 
| 818 |         if (static_cast<QMetaType::Type>(importVariant.userType()) == QMetaType::QVariantMap) { | 
| 819 |             s << QStringLiteral("set(qml_import_scanner_import_" ) << importsCount | 
| 820 |               << QStringLiteral(" \"" ); | 
| 821 |  | 
| 822 |             const QMap<QString, QVariant> &importDict = importVariant.toMap(); | 
| 823 |             for (auto it = importDict.cbegin(); it != importDict.cend(); ++it) { | 
| 824 |                 s << it.key().toUpper() << QLatin1Char(';'); | 
| 825 |                 // QVariant can implicitly convert QString to the QStringList with the single | 
| 826 |                 // element, let's use this. | 
| 827 |                 QStringList args = it.value().toStringList(); | 
| 828 |                 if (args.isEmpty()) { | 
| 829 |                     // This should not happen, but if it does, the result of the | 
| 830 |                     // 'cmake_parse_arguments' call will be incorrect, so follow up semicolon | 
| 831 |                     // indicates that the single-/multiarg option is empty. | 
| 832 |                     s << QLatin1Char(';'); | 
| 833 |                 } else { | 
| 834 |                     for (auto arg : args) { | 
| 835 |                         s << arg << QLatin1Char(';'); | 
| 836 |                     } | 
| 837 |                 } | 
| 838 |             } | 
| 839 |             s << QStringLiteral("\")\n" ); | 
| 840 |             ++importsCount; | 
| 841 |         } | 
| 842 |     } | 
| 843 |     if (importsCount >= 0) { | 
| 844 |         content.prepend(s: QString(QStringLiteral("set(qml_import_scanner_imports_count %1)\n" )) | 
| 845 |                .arg(a: importsCount)); | 
| 846 |     } | 
| 847 |     return content; | 
| 848 | } | 
| 849 |  | 
| 850 | bool argumentsFromCommandLineAndFile(QStringList &allArguments, const QStringList &arguments) | 
| 851 | { | 
| 852 |     allArguments.reserve(asize: arguments.size()); | 
| 853 |     for (const QString &argument : arguments) { | 
| 854 |         // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it | 
| 855 |         if (argument.startsWith(c: QLatin1Char('@'))) { | 
| 856 |             QString optionsFile = argument; | 
| 857 |             optionsFile.remove(i: 0, len: 1); | 
| 858 |             if (optionsFile.isEmpty()) { | 
| 859 |                 fprintf(stderr, format: "The @ option requires an input file" ); | 
| 860 |                 return false; | 
| 861 |             } | 
| 862 |             QFile f(optionsFile); | 
| 863 |             if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { | 
| 864 |                 fprintf(stderr, format: "Cannot open options file specified with @" ); | 
| 865 |                 return false; | 
| 866 |             } | 
| 867 |             while (!f.atEnd()) { | 
| 868 |                 QString line = QString::fromLocal8Bit(ba: f.readLine().trimmed()); | 
| 869 |                 if (!line.isEmpty()) | 
| 870 |                     allArguments << line; | 
| 871 |             } | 
| 872 |         } else { | 
| 873 |             allArguments << argument; | 
| 874 |         } | 
| 875 |     } | 
| 876 |     return true; | 
| 877 | } | 
| 878 |  | 
| 879 | } // namespace | 
| 880 |  | 
| 881 | int main(int argc, char *argv[]) | 
| 882 | { | 
| 883 |     QCoreApplication app(argc, argv); | 
| 884 |     QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); | 
| 885 |     QStringList args; | 
| 886 |     if (!argumentsFromCommandLineAndFile(allArguments&: args, arguments: app.arguments())) | 
| 887 |         return EXIT_FAILURE; | 
| 888 |     const QString appName = QFileInfo(app.applicationFilePath()).baseName(); | 
| 889 |     if (args.size() < 2) { | 
| 890 |         printUsage(appNameIn: appName); | 
| 891 |         return 1; | 
| 892 |     } | 
| 893 |  | 
| 894 |     // QQmlDirParser returnes QMultiHashes. Ensure deterministic output. | 
| 895 |     QHashSeed::setDeterministicGlobalSeed(); | 
| 896 |  | 
| 897 |     QStringList qmlRootPaths; | 
| 898 |     QStringList scanFiles; | 
| 899 |     QStringList qmlImportPaths; | 
| 900 |     QStringList qrcFiles; | 
| 901 |     bool generateCmakeContent = false; | 
| 902 |     QString outputFile; | 
| 903 |  | 
| 904 |     int i = 1; | 
| 905 |     while (i < args.size()) { | 
| 906 |         bool checkDirExists = true; | 
| 907 |         const QString &arg = args.at(i); | 
| 908 |         ++i; | 
| 909 |         QStringList *argReceiver = nullptr; | 
| 910 |         if (!arg.startsWith(c: QLatin1Char('-')) || arg == QLatin1String("-" )) { | 
| 911 |             qmlRootPaths += arg; | 
| 912 |         } else if (arg == QLatin1String("-rootPath" )) { | 
| 913 |             if (i >= args.size()) | 
| 914 |                 std::cerr << "-rootPath requires an argument\n" ; | 
| 915 |             argReceiver = &qmlRootPaths; | 
| 916 |         } else if (arg == QLatin1String("-qmlFiles" )) { | 
| 917 |             if (i >= args.size()) | 
| 918 |                 std::cerr << "-qmlFiles requires an argument\n" ; | 
| 919 |             argReceiver = &scanFiles; | 
| 920 |         } else if (arg == QLatin1String("-jsFiles" )) { | 
| 921 |             if (i >= args.size()) | 
| 922 |                 std::cerr << "-jsFiles requires an argument\n" ; | 
| 923 |             argReceiver = &scanFiles; | 
| 924 |         } else if (arg == QLatin1String("-importPath" )) { | 
| 925 |             if (i >= args.size()) | 
| 926 |                 std::cerr << "-importPath requires an argument\n" ; | 
| 927 |             argReceiver = &qmlImportPaths; | 
| 928 |         } else if (arg == "-exclude"_L1 ) { | 
| 929 |             if (i >= args.size()) | 
| 930 |                 std::cerr << "-exclude Path requires an argument\n" ; | 
| 931 |             checkDirExists = false; | 
| 932 |             argReceiver = &excludedDirectories; | 
| 933 |         } else if (arg == QLatin1String("-cmake-output" )) { | 
| 934 |              generateCmakeContent = true; | 
| 935 |         } else if (arg == QLatin1String("-qrcFiles" )) { | 
| 936 |             argReceiver = &qrcFiles; | 
| 937 |         } else if (arg == QLatin1String("-output-file" )) { | 
| 938 |             if (i >= args.size()) { | 
| 939 |                 std::cerr << "-output-file requires an argument\n" ; | 
| 940 |                 return 1; | 
| 941 |             } | 
| 942 |             outputFile = args.at(i); | 
| 943 |             ++i; | 
| 944 |             continue; | 
| 945 |         } else { | 
| 946 |             std::cerr << qPrintable(appName) << ": Invalid argument: \""  | 
| 947 |                 << qPrintable(arg) << "\"\n" ; | 
| 948 |             return 1; | 
| 949 |         } | 
| 950 |  | 
| 951 |         while (i < args.size()) { | 
| 952 |             const QString arg = args.at(i); | 
| 953 |             if (arg.startsWith(c: QLatin1Char('-')) && arg != QLatin1String("-" )) | 
| 954 |                 break; | 
| 955 |             ++i; | 
| 956 |             if (arg != QLatin1String("-" ) && checkDirExists && !QFile::exists(fileName: arg)) { | 
| 957 |                 std::cerr << qPrintable(appName) << ": No such file or directory: \""  | 
| 958 |                     << qPrintable(arg) << "\"\n" ; | 
| 959 |                 return 1; | 
| 960 |             } else if (argReceiver) { | 
| 961 |                 *argReceiver += arg; | 
| 962 |             } else { | 
| 963 |                 std::cerr << qPrintable(appName) << ": Invalid argument: \""  | 
| 964 |                     << qPrintable(arg) << "\"\n" ; | 
| 965 |                 return 1; | 
| 966 |             } | 
| 967 |         } | 
| 968 |     } | 
| 969 |  | 
| 970 |     if (!qrcFiles.isEmpty()) { | 
| 971 |         scanFiles << QQmlJSResourceFileMapper(qrcFiles).filePaths( | 
| 972 |                          filter: QQmlJSResourceFileMapper::allQmlJSFilter()); | 
| 973 |     } | 
| 974 |  | 
| 975 |     g_qmlImportPaths = qmlImportPaths; | 
| 976 |  | 
| 977 |     FileImportsWithoutDepsCache fileImportsWithoutDepsCache; | 
| 978 |  | 
| 979 |     // Find the imports! | 
| 980 |     QVariantList imports = findQmlImportsRecursively(qmlDirs: qmlRootPaths, | 
| 981 |                                                      scanFiles, | 
| 982 |                                                      fileImportsWithoutDepsCache | 
| 983 |                                                      ); | 
| 984 |  | 
| 985 |     QByteArray content; | 
| 986 |     if (generateCmakeContent) { | 
| 987 |         // Convert to CMake code | 
| 988 |         content = generateCmakeIncludeFileContent(importList: imports).toUtf8(); | 
| 989 |     } else { | 
| 990 |         // Convert to JSON | 
| 991 |         content = QJsonDocument(QJsonArray::fromVariantList(list: imports)).toJson(); | 
| 992 |     } | 
| 993 |  | 
| 994 |     if (outputFile.isEmpty()) { | 
| 995 |         std::cout << content.constData() << std::endl; | 
| 996 |     } else { | 
| 997 |         QFile f(outputFile); | 
| 998 |         if (!f.open(flags: QIODevice::WriteOnly | QIODevice::Text)) { | 
| 999 |             std::cerr << qPrintable(appName) << ": Unable to write to output file: \""  | 
| 1000 |                 << qPrintable(outputFile) << "\"\n" ; | 
| 1001 |             return 1; | 
| 1002 |         } | 
| 1003 |         QTextStream out(&f); | 
| 1004 |         out << content << "\n" ; | 
| 1005 |     } | 
| 1006 |     return 0; | 
| 1007 | } | 
| 1008 |  |