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
36QT_USE_NAMESPACE
37
38using namespace Qt::StringLiterals;
39
40Q_LOGGING_CATEGORY(lcImportScanner, "qt.qml.import.scanner");
41Q_LOGGING_CATEGORY(lcImportScannerFiles, "qt.qml.import.scanner.files");
42
43using FileImportsWithoutDepsCache = QHash<QString, QVariantList>;
44
45namespace {
46
47QStringList g_qmlImportPaths;
48bool g_addImportVersion = false;
49
50inline QString typeLiteral() { return QStringLiteral("type"); }
51inline QString versionLiteral() { return QStringLiteral("version"); }
52inline QString nameLiteral() { return QStringLiteral("name"); }
53inline QString relativePathLiteral() { return QStringLiteral("relativePath"); }
54inline QString pluginsLiteral() { return QStringLiteral("plugins"); }
55inline QString pluginIsOptionalLiteral() { return QStringLiteral("pluginIsOptional"); }
56inline QString pathLiteral() { return QStringLiteral("path"); }
57inline QString classnamesLiteral() { return QStringLiteral("classnames"); }
58inline QString dependenciesLiteral() { return QStringLiteral("dependencies"); }
59inline QString moduleLiteral() { return QStringLiteral("module"); }
60inline QString javascriptLiteral() { return QStringLiteral("javascript"); }
61inline QString directoryLiteral() { return QStringLiteral("directory"); }
62inline QString linkTargetLiteral()
63{
64 return QStringLiteral("linkTarget");
65}
66inline QString componentsLiteral() { return QStringLiteral("components"); }
67inline QString scriptsLiteral() { return QStringLiteral("scripts"); }
68inline QString preferLiteral() { return QStringLiteral("prefer"); }
69
70void 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
85QVariantList findImportsInAst(QQmlJS::AST::UiHeaderItemList *headerItemList, 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 *headerItemIt = 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
139QVariantList findQmlImportsInFileWithoutDeps(const QString &filePath,
140 FileImportsWithoutDepsCache
141 &fileImportsWithoutDepsCache);
142
143static 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.
151QVariantMap 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).
279std::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.
345struct 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
380using 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.
385ImportDetailsAndDeps
386getImportDetails(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.
451QVariantMap 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.
466QVariantList 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
507QVariantList mergeImports(const QVariantList &a, const QVariantList &b);
508
509// Returns details of given input imports and their recursive module dependencies.
510QVariantList 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
531QVariantList 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
554QVariantList 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
566struct 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
603QVariantList 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.
632QVariantList 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.
662QVariantList 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.
692QVariantList 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
704struct isMetainfo {
705 bool operator() (const QFileInfo &x) const {
706 return x.suffix() == QLatin1String("metainfo");
707 }
708};
709
710struct 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
718static 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
725static 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
737QVariantList 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.
781QVariantList 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
811QString 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
852bool 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
883int 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

source code of qtdeclarative/tools/qmlimportscanner/main.cpp