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 findImportsInAst(QQmlJS::AST::UiHeaderItemList *headerItemList, 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 *headerItemIt = 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 |
Definitions
- lcImportScanner
- lcImportScannerFiles
- g_qmlImportPaths
- typeLiteral
- versionLiteral
- nameLiteral
- relativePathLiteral
- pluginsLiteral
- pluginIsOptionalLiteral
- pathLiteral
- classnamesLiteral
- dependenciesLiteral
- moduleLiteral
- javascriptLiteral
- directoryLiteral
- linkTargetLiteral
- componentsLiteral
- scriptsLiteral
- preferLiteral
- printUsage
- findImportsInAst
- versionSuffix
- pluginsForModulePath
- resolveImportPath
- ImportVariantHasher
- operator()
- getImportDetails
- dependencyStringToImport
- getGetDetailedModuleImportsIncludingDependencies
- getGetDetailedModuleImportsIncludingDependencies
- findQmlImportsInQmlCode
- findQmlImportsInQmlFile
- ImportCollector
- importFile
- importModule
- findQmlImportsInJavascriptFile
- findQmlImportsInFileWithoutDeps
- findQmlImportsInFile
- mergeImports
- isMetainfo
- operator()
- pathStartsWith
- pathStartsWith
- operator()
- excludedDirectories
- isExcluded
- findQmlImportsInDirectory
- findQmlImportsRecursively
- generateCmakeIncludeFileContent
- argumentsFromCommandLineAndFile
Learn Advanced QML with KDAB
Find out more