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