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