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;
48
49inline QString typeLiteral() { return QStringLiteral("type"); }
50inline QString versionLiteral() { return QStringLiteral("version"); }
51inline QString nameLiteral() { return QStringLiteral("name"); }
52inline QString relativePathLiteral() { return QStringLiteral("relativePath"); }
53inline QString pluginsLiteral() { return QStringLiteral("plugins"); }
54inline QString pluginIsOptionalLiteral() { return QStringLiteral("pluginIsOptional"); }
55inline QString pathLiteral() { return QStringLiteral("path"); }
56inline QString classnamesLiteral() { return QStringLiteral("classnames"); }
57inline QString dependenciesLiteral() { return QStringLiteral("dependencies"); }
58inline QString moduleLiteral() { return QStringLiteral("module"); }
59inline QString javascriptLiteral() { return QStringLiteral("javascript"); }
60inline QString directoryLiteral() { return QStringLiteral("directory"); }
61inline QString linkTargetLiteral()
62{
63 return QStringLiteral("linkTarget");
64}
65inline QString componentsLiteral() { return QStringLiteral("components"); }
66inline QString scriptsLiteral() { return QStringLiteral("scripts"); }
67inline QString preferLiteral() { return QStringLiteral("prefer"); }
68
69void 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
84QVariantList 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
138QVariantList findQmlImportsInFileWithoutDeps(const QString &filePath,
139 FileImportsWithoutDepsCache
140 &fileImportsWithoutDepsCache);
141
142static 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.
150QVariantMap 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).
278QPair<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.
344struct 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
379using 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.
384ImportDetailsAndDeps
385getImportDetails(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.
449QVariantMap 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.
464QVariantList 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
505QVariantList mergeImports(const QVariantList &a, const QVariantList &b);
506
507// Returns details of given input imports and their recursive module dependencies.
508QVariantList 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
529QVariantList 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
552QVariantList 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
564struct 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
601QVariantList 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.
630QVariantList 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.
660QVariantList 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.
690QVariantList 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
702struct isMetainfo {
703 bool operator() (const QFileInfo &x) const {
704 return x.suffix() == QLatin1String("metainfo");
705 }
706};
707
708struct 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
716static 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
723static 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
735QVariantList 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.
779QVariantList 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
809QString 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
850bool 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
881int 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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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