Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmljsimporter.cpp
Go to the documentation of this file.
1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3// Qt-Security score:significant
4
10#include "qqmljsutils_p.h"
11
12#include <QtQml/private/qqmlimportresolver_p.h>
13
14#include <QtCore/qfileinfo.h>
15#include <QtCore/qdiriterator.h>
16
18
19using namespace Qt::StringLiterals;
20
21static const QLatin1String SlashQmldir = QLatin1String("/qmldir");
22static const QLatin1String PluginsDotQmltypes = QLatin1String("plugins.qmltypes");
23static const QLatin1String JsrootDotQmltypes = QLatin1String("jsroot.qmltypes");
24
25
26QQmlJS::Import::Import(QString prefix, QString name, QTypeRevision version, bool isFile,
27 bool isDependency) :
28 m_prefix(std::move(prefix)),
29 m_name(std::move(name)),
30 m_version(version),
31 m_isFile(isFile),
32 m_isDependency(isDependency)
33{
34}
35
36bool QQmlJS::Import::isValid() const
37{
38 return !m_name.isEmpty();
39}
40
41static const QString prefixedName(const QString &prefix, const QString &name)
42{
43 Q_ASSERT(!prefix.endsWith(u'.'));
44 return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name);
45}
46
47static QString ensureSlashQmldir(const QString &path)
48{
49 if (path.endsWith(SlashQmldir))
50 return path;
51 if (path.endsWith(u'/'))
52 return path + "qmldir"_L1;
53 return path + SlashQmldir;
54}
55
56QQmlDirParser QQmlJSImporter::createQmldirParserForFile(const QString &filename, Import *import)
57{
58 Q_ASSERT(import);
59 QFile f(filename);
60 QQmlDirParser parser;
61 if (f.open(QFile::ReadOnly)) {
62 parser.parse(QString::fromUtf8(f.readAll()));
63 import->warnings.append(parser.errors(filename));
64 } else {
65 import->warnings.append({
66 QStringLiteral("Could not open qmldir file: ") + filename,
67 QtWarningMsg,
68 QQmlJS::SourceLocation()
69 });
70 }
71
72 return parser;
73}
74
75void QQmlJSImporter::readQmltypes(const QString &filename, Import *result)
76{
77 const QFileInfo fileInfo(filename);
78 if (!fileInfo.exists()) {
79 result->warnings.append({
80 QStringLiteral("QML types file does not exist: ") + filename,
81 QtWarningMsg,
82 QQmlJS::SourceLocation()
83 });
84 return;
85 }
86
87 if (fileInfo.isDir()) {
88 result->warnings.append({
89 QStringLiteral("QML types file cannot be a directory: ") + filename,
90 QtWarningMsg,
91 QQmlJS::SourceLocation()
92 });
93 return;
94 }
95
96 QFile file(filename);
97 if (!file.open(QFile::ReadOnly)) {
98 result->warnings.append({
99 QStringLiteral("QML types file cannot be opened: ") + filename,
100 QtWarningMsg,
101 QQmlJS::SourceLocation()
102 });
103 return;
104 }
105
106 QQmlJSTypeDescriptionReader reader { filename, QString::fromUtf8(file.readAll()) };
107 QStringList dependencyStrings;
108 auto succ = reader(&result->objects, &dependencyStrings);
109 if (!succ)
110 result->warnings.append({ reader.errorMessage(), QtCriticalMsg, QQmlJS::SourceLocation() });
111
112 const QString warningMessage = reader.warningMessage();
113 if (!warningMessage.isEmpty())
114 result->warnings.append({ warningMessage, QtWarningMsg, QQmlJS::SourceLocation() });
115
116 if (dependencyStrings.isEmpty())
117 return;
118
119 result->warnings.append({
120 QStringLiteral("Found deprecated dependency specifications in %1."
121 "Specify dependencies in qmldir and use qmltyperegistrar "
122 "to generate qmltypes files without dependencies.")
123 .arg(filename),
124 QtWarningMsg,
125 QQmlJS::SourceLocation()
126 });
127
128 for (const QString &dependency : std::as_const(dependencyStrings)) {
129 const auto blank = dependency.indexOf(u' ');
130 if (blank < 0) {
131 result->dependencies.append(
132 QQmlDirParser::Import(dependency, {}, QQmlDirParser::Import::Default));
133 continue;
134 }
135
136 const QString module = dependency.left(blank);
137 const QString versionString = dependency.mid(blank + 1).trimmed();
138 if (versionString == QStringLiteral("auto")) {
139 result->dependencies.append(
140 QQmlDirParser::Import(module, {}, QQmlDirParser::Import::Auto));
141 continue;
142 }
143
144 const auto dot = versionString.indexOf(u'.');
145
146 const QTypeRevision version = dot < 0
147 ? QTypeRevision::fromMajorVersion(versionString.toUShort())
148 : QTypeRevision::fromVersion(versionString.left(dot).toUShort(),
149 versionString.mid(dot + 1).toUShort());
150
151 result->dependencies.append(
152 QQmlDirParser::Import(module, version, QQmlDirParser::Import::Default));
153 }
154}
155
156static QString internalName(const QQmlJSScope::ConstPtr &scope)
157{
158 if (const auto *factory = scope.factory())
159 return factory->internalName();
160 return scope->internalName();
161}
162
163static QString filePath(const QQmlJSScope::ConstPtr &scope)
164{
165 if (const auto *factory = scope.factory())
166 return factory->filePath();
167 return scope->filePath();
168}
169
170static QString moduleName(const QQmlJSScope::ConstPtr &scope)
171{
172 if (const auto *factory = scope.factory())
173 return factory->moduleName();
174 return scope->moduleName();
175}
176
177static bool isComposite(const QQmlJSScope::ConstPtr &scope)
178{
179 // The only thing the factory can do is load a composite type.
180 return scope.factory() || scope->isComposite();
181}
182
183static QStringList aliases(const QQmlJSScope::ConstPtr &scope)
184{
185 return isComposite(scope)
186 ? QStringList()
187 : scope->aliases();
188}
189
190/*!
191 \class QQmlJSImporter
192 \internal
193*/
194QQmlJSImporter::QQmlJSImporter(const QStringList &importPaths, QQmlJSResourceFileMapper *mapper,
195 QQmlJSImporterFlags flags)
196 : m_importPaths(importPaths),
197 m_mapper(mapper),
198 m_flags(flags),
199 m_importVisitor([](QQmlJS::AST::Node *rootNode, QQmlJSImporter *self,
200 const ImportVisitorPrerequisites &p) {
201 auto visitor = std::unique_ptr<QQmlJS::AST::BaseVisitor>(new QQmlJSImportVisitor(
202 self, p.m_logger, p.m_implicitImportDirectory, p.m_qmldirFiles));
203 QQmlJS::AST::Node::accept(rootNode, visitor.get());
204 })
205{
206}
207
208static QString resolvePreferredPath(const QString &qmldirPath, const QString &prefer,
209 const QQmlJSResourceFileMapper *mapper)
210{
211 if (prefer.isEmpty())
212 return qmldirPath;
213
214 if (!prefer.endsWith(u'/')) {
215 qWarning() << "Ignoring invalid prefer path" << prefer << "(has to end with slash)";
216 return qmldirPath;
217 }
218
219 if (prefer.startsWith(u':')) {
220 // Resource path: Resolve via resource file mapper if possible.
221 if (!mapper)
222 return qmldirPath;
223
224 Q_ASSERT(prefer.endsWith(u'/'));
225 const auto entry = mapper->entry(
226 QQmlJSResourceFileMapper::resourceFileFilter(ensureSlashQmldir(prefer.mid(1))));
227
228 // This can be empty if the .qrc files does not belong to this module.
229 // In that case we trust the given qmldir file.
230 return entry.filePath.endsWith(SlashQmldir)
231 ? entry.filePath
232 : qmldirPath;
233 }
234
235 // Host file system path. This should be rare. We don't generate it.
236 const QFileInfo f(ensureSlashQmldir(prefer));
237 const QString canonical = f.canonicalFilePath();
238 if (canonical.isEmpty())
239 return qmldirPath;
240 return canonical;
241}
242
243QQmlJSImporter::Import QQmlJSImporter::readQmldir(const QString &modulePath)
244{
245 Import result;
246 const QString moduleQmldirPath = ensureSlashQmldir(modulePath);
247 auto reader = createQmldirParserForFile(moduleQmldirPath, &result);
248
249 const QString resolvedQmldirPath
250 = resolvePreferredPath(moduleQmldirPath, reader.preferredPath(), m_mapper);
251 if (resolvedQmldirPath != moduleQmldirPath)
252 reader = createQmldirParserForFile(resolvedQmldirPath, &result);
253
254 // Leave the trailing slash
255 Q_ASSERT(resolvedQmldirPath.endsWith(SlashQmldir));
256 QStringView resolvedPath = QStringView(resolvedQmldirPath).chopped(SlashQmldir.size() - 1);
257
258 result.name = reader.typeNamespace();
259
260 result.isStaticModule = reader.isStaticModule();
261 result.isSystemModule = reader.isSystemModule();
262 result.type = Import::Qmldir;
263 result.imports.append(reader.imports());
264 result.dependencies.append(reader.dependencies());
265
266 if (result.isSystemModule) {
267 // Import jsroot first, so that the builtins override it.
268 const QString jsrootPath = resolvedPath + JsrootDotQmltypes;
269 if (QFile::exists(jsrootPath)) {
270 readQmltypes(jsrootPath, &result);
271 } else {
272 result.warnings.append({
273 QStringLiteral("System module at %1 does not contain jsroot.qmltypes")
274 .arg(resolvedPath),
275 QtWarningMsg,
276 QQmlJS::SourceLocation()
277 });
278 }
279 }
280
281 const auto typeInfos = reader.typeInfos();
282 for (const auto &typeInfo : typeInfos) {
283 const QString typeInfoPath = QFileInfo(typeInfo).isRelative()
284 ? resolvedPath + typeInfo
285 : typeInfo;
286 readQmltypes(typeInfoPath, &result);
287 }
288
289 if (typeInfos.isEmpty() && !reader.plugins().isEmpty()) {
290 const QString defaultTypeInfoPath = resolvedPath + PluginsDotQmltypes;
291 if (QFile::exists(defaultTypeInfoPath)) {
292 result.warnings.append({
293 QStringLiteral("typeinfo not declared in qmldir file: ")
294 + defaultTypeInfoPath,
295 QtWarningMsg,
296 QQmlJS::SourceLocation()
297 });
298 readQmltypes(defaultTypeInfoPath, &result);
299 }
300 }
301
302 QHash<QString, QQmlJSExportedScope> qmlComponents;
303 const auto components = reader.components();
304 for (auto it = components.begin(), end = components.end(); it != end; ++it) {
305 const QString filePath = resolvedPath + it->fileName;
306 if (!QFile::exists(filePath)) {
307 result.warnings.append({
308 it->fileName + QStringLiteral(" is listed as component in ")
309 + resolvedQmldirPath
310 + QStringLiteral(" but does not exist.\n"),
311 QtWarningMsg,
312 QQmlJS::SourceLocation()
313 });
314 continue;
315 }
316
317 auto mo = qmlComponents.find(it->fileName);
318 if (mo == qmlComponents.end()) {
319 QQmlJSScope::Ptr imported = localFile2QQmlJSScope(filePath);
320 if (auto factory = imported.factory())
321 factory->setIsSingleton(it->singleton);
322 else
323 imported->setIsSingleton(it->singleton);
324 mo = qmlComponents.insert(it->fileName, {imported, QList<QQmlJSScope::Export>() });
325 }
326
327 mo->exports.append(QQmlJSScope::Export(
328 reader.typeNamespace(), it.key(), it->version, QTypeRevision()));
329 }
330 for (auto it = qmlComponents.begin(), end = qmlComponents.end(); it != end; ++it)
331 result.objects.append(it.value());
332
333 const auto scripts = reader.scripts();
334 for (const auto &script : scripts) {
335 const QString filePath = resolvedPath + script.fileName;
336 auto mo = result.scripts.find(script.fileName);
337 if (mo == result.scripts.end())
338 mo = result.scripts.insert(script.fileName, { localFile2QQmlJSScope(filePath), {} });
339
340 mo->exports.append(QQmlJSScope::Export(
341 reader.typeNamespace(), script.nameSpace,
342 script.version, QTypeRevision()));
343 }
344 return result;
345}
346
347static QList<QQmlJS::Export> createExports(const QString &name)
348{
349 QList<QQmlJS::Export> exports;
350 const bool hasExport = name.front().isUpper();
351 if (hasExport)
352 exports << QQmlJSScope::Export(QString(), name, QTypeRevision(), QTypeRevision());
353 return exports;
354}
355
356QQmlJSImporter::Import QQmlJSImporter::readDirectory(const QString &directory)
357{
358 Import import;
359 import.type = Import::Directory;
360 if (directory.startsWith(u':')) {
361 if (m_mapper) {
362 const auto resources = m_mapper->filter(
363 QQmlJSResourceFileMapper::resourceQmlDirectoryFilter(directory.mid(1)));
364 for (const auto &entry : resources) {
365 const QString name = QFileInfo(entry.resourcePath).baseName();
366 import.objects.append(
367 { localFile2QQmlJSScope(entry.filePath), createExports(name) });
368 }
369 } else {
370 qWarning() << "Cannot read files from resource directory" << directory
371 << "because no resource file mapper was provided";
372 }
373
374 return import;
375 }
376
377 // The engine doesn't really care whether a file is hidden, so we need to include them
378 QDirListing::IteratorFlags listingFlags = QDirListing::IteratorFlag::Default
379 | QDirListing::IteratorFlag::IncludeHidden
380 | QDirListing::IteratorFlag::FilesOnly;
381 QDirListing dirListing(
382 directory,
383 QStringList() << QLatin1String("*.qml"),
384 listingFlags
385 );
386 for (const QDirListing::DirEntry &entry: dirListing) {
387 QString name = entry.completeBaseName();
388
389 // .ui.qml is fine
390 if (name.endsWith(u".ui"))
391 name = name.chopped(3);
392
393 // Names with dots in them cannot be imported either.
394 if (name.contains(u'.'))
395 continue;
396
397 import.objects.append({ localFile2QQmlJSScope(entry.filePath()), createExports(name) });
398 }
399 return import;
400}
401
402void QQmlJSImporter::importDependencies(const QQmlJSImporter::Import &import, quint8 precedence,
403 QQmlJSImporter::AvailableTypes *types,
404 const QString &prefix, QTypeRevision version,
405 bool isDependency)
406{
407 // Import the dependencies with an invalid prefix. The prefix will never be matched by actual
408 // QML code but the C++ types will be visible.
409 for (auto const &dependency : std::as_const(import.dependencies))
410 importHelper(dependency.module, types, precedence + 1, QString(), dependency.version, true);
411
412 bool hasOptionalImports = false;
413 for (auto const &import : std::as_const(import.imports)) {
414 if (import.flags & QQmlDirParser::Import::Optional) {
415 hasOptionalImports = true;
416 if (!useOptionalImports()) {
417 continue;
418 }
419
420 if (!(import.flags & QQmlDirParser::Import::OptionalDefault))
421 continue;
422 }
423
424 importHelper(import.module, types, precedence + 1, isDependency ? QString() : prefix,
425 (import.flags & QQmlDirParser::Import::Auto) ? version : import.version,
426 isDependency);
427 }
428
429 if (hasOptionalImports && !useOptionalImports()) {
430 types->warnings.append({
431 u"%1 uses optional imports which are not supported. Some types might not be found."_s
432 .arg(import.name),
433 QtCriticalMsg, QQmlJS::SourceLocation()
434 });
435 }
436}
437
438static bool isVersionAllowed(const QQmlJSScope::Export &exportEntry,
439 const QQmlJS::Import &importDescription)
440{
441 const QTypeRevision importVersion = importDescription.version();
442 const QTypeRevision exportVersion = exportEntry.version();
443 if (!importVersion.hasMajorVersion())
444 return true;
445 if (importVersion.majorVersion() != exportVersion.majorVersion())
446 return false;
447 return !importVersion.hasMinorVersion()
448 || exportVersion.minorVersion() <= importVersion.minorVersion();
449}
450
451void QQmlJSImporter::insertAliases(const QQmlJS::ContextualType &type,
452 QQmlJSImporter::AvailableTypes *types)
453{
454 const QStringList cppAliases = aliases(type.scope);
455 for (const QString &alias : cppAliases)
456 types->cppNames.setType(alias, type);
457};
458
459void QQmlJSImporter::insertExport(const QQmlJS::ContextualType &type,
460 const QQmlJS::Export &valExport, const QString &qmlName,
461 QHash<QString, QList<QQmlJSScope::Export>> *seenExports,
462 QQmlJSImporter::AvailableTypes *types) const
463{
464 if (m_flags.testFlag(TolerateFileSelectors)) {
465 if (const QString fileSelector = QQmlJSUtils::fileSelectorFor(type.scope);
466 !fileSelector.isEmpty()) {
467 types->qmlNames.setFileSelectedType(fileSelector, qmlName, type);
468 // If no other variant exists (with either a different or no file selector),
469 // insert the current one.
470 // A later non-selected variant will replace this entry.
471 if (!types->qmlNames.hasType(qmlName)) {
472 types->qmlNames.setType(qmlName, type);
473 (*seenExports)[qmlName].append(valExport);
474 }
475 return;
476 }
477 }
478 types->qmlNames.setType(qmlName, type);
479 (*seenExports)[qmlName].append(valExport);
480}
481
482QQmlJSScope::Export
483QQmlJSImporter::resolveConflictingExports(const QQmlJS::Import &importDescription,
484 const QQmlJSExportedScope &val, quint8 precedence,
485 QHash<QString, QList<QQmlJSScope::Export>> *seenExports,
486 QQmlJSImporter::AvailableTypes *types)
487{
488 QQmlJSScope::Export bestExport;
489
490 for (const auto &valExport : val.exports) {
491 const QString qmlName = prefixedName(importDescription.prefix(), valExport.type());
492 if (!isVersionAllowed(valExport, importDescription))
493 continue;
494
495 // Even if the QML name is overridden by some other type, we still want
496 // to insert the C++ type, with the highest revision available.
497 if (!bestExport.isValid() || valExport.version() > bestExport.version())
498 bestExport = valExport;
499
500 auto insertExportWithConflict = [&](const QQmlJSImportedScope &conflicting) {
501 // The same set of exports can declare the same name multiple times for different
502 // versions. That's the common thing and we would just continue here when we hit
503 // it again after having inserted successfully once.
504 // However, it can also declare *different* names. Then we need to do the whole
505 // thing again.
506 if (conflicting.scope == val.scope && conflicting.revision == valExport.version())
507 return;
508
509 const SeenVersion seenVersion = computeSeenVersion(
510 importDescription, seenExports->value(qmlName), valExport.version());
511
512 insertExportWithConflictingVersion(val, precedence, qmlName, valExport,
513 conflicting.scope, seenExports, types, seenVersion);
514 };
515
516 if (const auto it = types->qmlNames.types().find(qmlName);
517 it != types->qmlNames.types().end()) {
518 insertExportWithConflict(*it);
519 continue;
520 }
521
522 const auto [it, end] = types->qmlNames.fileSelectionsEqualRange(qmlName);
523 if (it == end) {
524 insertExport({ val.scope, valExport.version(), precedence }, valExport, qmlName,
525 seenExports, types);
526 continue;
527 }
528 insertExportWithConflict(it->type);
529 }
530 return bestExport;
531}
532
533QQmlJSImporter::SeenVersion
534QQmlJSImporter::computeSeenVersion(const QQmlJS::Import &importDescription,
535 const QList<QQmlJS::Export> &existingExports,
536 QTypeRevision valExportVersion) const
537{
538 QQmlJSImporter::SeenVersion seenVersion = LowerVersion;
539 for (const QQmlJSScope::Export &entry : existingExports) {
540 if (!isVersionAllowed(entry, importDescription))
541 continue;
542
543 if (valExportVersion < entry.version()) {
544 seenVersion = HigherVersion;
545 break;
546 }
547
548 if (seenVersion == LowerVersion && valExportVersion == entry.version())
549 seenVersion = SameVersion;
550 }
551 return seenVersion;
552}
553
554void QQmlJSImporter::insertExportWithConflictingVersion(
555 const QQmlJSExportedScope &val, quint8 precedence, const QString &qmlName,
556 const QQmlJSScope::Export &valExport, const QQmlJSScope::ConstPtr &scope,
557 QHash<QString, QList<QQmlJSScope::Export>> *seenExports,
558 QQmlJSImporter::AvailableTypes *types, SeenVersion seenVersion) const
559{
560 auto onDuplicateImport = [&]() {
561 types->warnings.append({ QStringLiteral("Ambiguous type detected. "
562 "%1 %2.%3 is defined multiple times.")
563 .arg(qmlName)
564 .arg(valExport.version().majorVersion())
565 .arg(valExport.version().minorVersion()),
566 QtCriticalMsg, QQmlJS::SourceLocation() });
567
568 // Invalidate the type. We don't know which one to use.
569 types->qmlNames.clearType(qmlName);
570 };
571 switch (seenVersion) {
572 case LowerVersion:
573 insertExport({ val.scope, valExport.version(), precedence }, valExport, qmlName,
574 seenExports, types);
575 return;
576 case SameVersion: {
577 if (!m_flags.testAnyFlag(QQmlJSImporterFlag::TolerateFileSelectors) || !scope) {
578 onDuplicateImport();
579 return;
580 }
581 if (const QString fileSelector = QQmlJSUtils::fileSelectorFor(val.scope);
582 !fileSelector.isEmpty()) {
583 types->qmlNames.setFileSelectedType(fileSelector, qmlName,
584 { val.scope, valExport.version(), precedence });
585 // Ensure type is found if there are no non-file selected versions
586 if (!types->qmlNames.hasType(qmlName)) {
587 types->qmlNames.setType(qmlName,
588 { val.scope, valExport.version(), precedence });
589 (*seenExports)[qmlName].append(valExport);
590 }
591 return;
592 }
593 if (!QQmlJSUtils::fileSelectorFor(scope).isEmpty()) {
594 // the first scope we saw is file selected, update to the new one without file selector
595 types->qmlNames.setType(qmlName, { val.scope, valExport.version(), precedence });
596 (*seenExports)[qmlName].append(valExport);
597 return;
598 }
599 onDuplicateImport();
600 return;
601 }
602 case HigherVersion:
603 return;
604 }
605}
606
607void QQmlJSImporter::insertExports(const QQmlJS::Import &importDescription,
608 const QQmlJSExportedScope &val, const QString &cppName,
609 quint8 precedence,
610 QHash<QString, QList<QQmlJSScope::Export>> *seenExports,
611 QQmlJSImporter::AvailableTypes *types)
612{
613 const QQmlJSScope::Export bestExport =
614 resolveConflictingExports(importDescription, val, precedence, seenExports, types);
615 const QTypeRevision bestRevision =
616 bestExport.isValid() ? bestExport.revision() : QTypeRevision::zero();
617 const QQmlJS::ContextualType contextualType{ val.scope, bestRevision, precedence };
618 types->cppNames.setType(cppName, contextualType);
619
620 insertAliases(contextualType, types);
621
622 const QTypeRevision bestVersion =
623 bestExport.isValid() ? bestExport.version() : QTypeRevision::zero();
624 types->qmlNames.setType(prefixedName(internalPrefix, cppName),
625 { val.scope, bestVersion, precedence });
626};
627
628void QQmlJSImporter::processImport(const QQmlJS::Import &importDescription,
629 const QQmlJSImporter::Import &import, quint8 precedence,
630 QQmlJSImporter::AvailableTypes *types)
631{
632 QHash<QString, QList<QQmlJSScope::Export>> seenExports;
633
634 // Empty type means "this is the prefix"
635 if (!importDescription.prefix().isEmpty())
636 types->qmlNames.setType(importDescription.prefix(), { {}, precedence });
637
638 if (!importDescription.isDependency()) {
639 // Add a marker to show that this module has been imported
640 types->qmlNames.setType(prefixedName(modulePrefix, importDescription.name()),
641 { {}, precedence });
642
643 if (import.isStaticModule)
644 types->staticModules << import.name;
645
646 if (import.isSystemModule)
647 types->hasSystemModule = true;
648
649 types->warnings.append(import.warnings);
650 }
651
652 for (auto it = import.scripts.begin(); it != import.scripts.end(); ++it) {
653 // You cannot have a script without an export
654 Q_ASSERT(!it->exports.isEmpty());
655 insertExports(importDescription, *it, prefixedName(anonPrefix, internalName(it->scope)),
656 precedence, &seenExports, types);
657 }
658
659 // Like the engine, prefer C++ types over QML types of the same name in the same module.
660 QSet<QString> cppExportNames;
661 for (const auto &val : import.objects) {
662 if (isComposite(val.scope))
663 continue;
664 for (const auto &valExport : val.exports) {
665 if (isVersionAllowed(valExport, importDescription))
666 cppExportNames.insert(valExport.type());
667 }
668 }
669
670 // add objects
671 for (const auto &object : import.objects) {
672 const bool composite = isComposite(object.scope);
673 const QString cppName = composite
674 ? prefixedName(anonPrefix, internalName(object.scope))
675 : internalName(object.scope);
676
677 // Remove all exports of composite types that clash with exports of
678 // non-composite types. These exports of composite types are invisible.
679 // TODO: We should probably warn about this, but first we need to fix
680 // QtQuick.Dialogs not to do this anymore (see QTBUG-151326).
681 QQmlJSExportedScope val = object;
682 if (composite) {
683 val.exports.removeIf([&](const QQmlJSScope::Export &valExport) {
684 return cppExportNames.contains(valExport.type());
685 });
686 }
687
688 if (val.exports.isEmpty()) {
689 const QQmlJS::ContextualType unresolvableDummyName{ val.scope, QTypeRevision(),
690 precedence };
691 types->qmlNames.setType(prefixedName(internalPrefix, cppName), unresolvableDummyName);
692 types->cppNames.setType(cppName, unresolvableDummyName);
693 insertAliases(unresolvableDummyName, types);
694 } else {
695 insertExports(importDescription, val, cppName, precedence, &seenExports, types);
696 }
697 }
698
699 /* We need to create a temporary AvailableTypes instance here to make builtins available as
700 QQmlJSScope::resolveTypes relies on them being available. They cannot be part of the regular
701 types as they would keep overwriting existing types when loaded from cache.
702 This is only a problem with builtin types as only builtin types can be overridden by any
703 sibling import. Consider the following qmldir:
704
705 module Things
706 import QtQml 2.0
707 import QtQuick.LocalStorage auto
708
709 The module "Things" sees QtQml's definition of Qt, not the builtins', even though
710 QtQuick.LocalStorage does not depend on QtQml and is imported afterwards. Conversely:
711
712 module Stuff
713 import ModuleOverridingQObject
714 import QtQuick
715
716 The module "Stuff" sees QtQml's definition of QObject (via QtQuick), even if
717 ModuleOverridingQObject has overridden it.
718 */
719
720 QQmlJSImporter::AvailableTypes tempTypes(builtinImportHelper().cppNames);
721 tempTypes.cppNames.addTypes(types->cppNames);
722
723 // At present, there are corner cases that couldn't be resolved in a single
724 // pass of resolveTypes() (e.g. QQmlEasingEnums::Type). However, such cases
725 // only happen when enumerations are involved, thus the strategy is to
726 // resolve enumerations (which can potentially create new child scopes)
727 // before resolving the type fully
728 const QQmlJSScope::ConstPtr arrayType = tempTypes.cppNames.type(u"Array"_s).scope;
729 for (auto it = import.objects.begin(); it != import.objects.end(); ++it) {
730 if (!it->scope.factory()) {
731 QQmlJSScope::resolveEnums(it->scope, tempTypes.cppNames);
732 QQmlJSScope::resolveList(it->scope, arrayType);
733 }
734 }
735
736 for (const auto &val : std::as_const(import.objects)) {
737 // Otherwise we have already done it in localFile2ScopeTree()
738 if (!val.scope.factory() && val.scope->baseType().isNull()) {
739
740 // ignore the scope currently analyzed by QQmlJSImportVisitor, as its only populated
741 // after importing the implicit directory.
742 if (val.scope->baseTypeName() == s_inProcessMarker)
743 continue;
744
745 // Composite types use QML names, and we should have resolved those already.
746 // ... except that old qmltypes files might specify composite types with C++ names.
747 // Warn about those.
748 if (val.scope->isComposite()) {
749 types->warnings.append({
750 QStringLiteral("Found incomplete composite type %1. Do not use qmlplugindump.")
751 .arg(val.scope->internalName()),
752 QtWarningMsg,
753 QQmlJS::SourceLocation()
754 });
755 }
756
757 QQmlJSScope::resolveNonEnumTypes(val.scope, tempTypes.cppNames);
758 }
759 }
760}
761
762/*!
763 * \internal
764 * Imports builtins, but only the subset hardcoded into the parser.
765 */
766QQmlJSImporter::ImportedTypes QQmlJSImporter::importHardCodedBuiltins()
767{
768 const auto builtins = builtinImportHelper();
769
770 QQmlJS::ContextualTypes result(
771 QQmlJS::ContextualTypes::QML, {}, {}, builtins.cppNames.arrayType());
772 for (const QString hardcoded : {
773 "void"_L1, "int"_L1, "bool"_L1, "double"_L1, "real"_L1, "string"_L1, "url"_L1,
774 "date"_L1, "regexp"_L1, "rect"_L1, "point"_L1, "size"_L1, "variant"_L1, "var"_L1,
775 }) {
776
777 const auto type = builtins.qmlNames.type(hardcoded);
778 Q_ASSERT(type.scope);
779 result.setType(hardcoded, { type, quint8(QQmlJS::PrecedenceValues::Default) });
780 }
781
782 return ImportedTypes(std::move(result), {});
783}
784
785
786QQmlJSImporter::AvailableTypes QQmlJSImporter::builtinImportHelper()
787{
788 if (m_builtins)
789 return *m_builtins;
790
791 AvailableTypes builtins(QQmlJS::ContextualTypes(QQmlJS::ContextualTypes::INTERNAL, {}, {}, {}));
792
793 importHelper(u"QML"_s, &builtins, quint8(QQmlJS::PrecedenceValues::Default), QString(),
794 QTypeRevision::fromVersion(1, 0));
795
796 QQmlJSScope::ConstPtr arrayType = builtins.cppNames.type(u"Array"_s).scope;
797 Q_ASSERT(arrayType);
798
799 m_builtins = AvailableTypes(QQmlJS::ContextualTypes(
800 QQmlJS::ContextualTypes::INTERNAL, builtins.cppNames.types(), builtins.cppNames.names(),
801 arrayType));
802 m_builtins->qmlNames = QQmlJS::ContextualTypes(
803 QQmlJS::ContextualTypes::QML, builtins.qmlNames.types(), builtins.qmlNames.names(),
804 arrayType);
805 m_builtins->staticModules = std::move(builtins.staticModules);
806 m_builtins->warnings = std::move(builtins.warnings);
807 m_builtins->hasSystemModule = builtins.hasSystemModule;
808
809 return *m_builtins;
810}
811
812/*!
813 * Imports types from the specified \a qmltypesFiles.
814 */
815QList<QQmlJS::DiagnosticMessage> QQmlJSImporter::importQmldirs(const QStringList &qmldirFiles)
816{
817 QList<QQmlJS::DiagnosticMessage> warnings;
818 for (const auto &file : qmldirFiles) {
819 Import result;
820 QString qmldirName;
821 if (file.endsWith(SlashQmldir)) {
822 result = readQmldir(file.chopped(SlashQmldir.size()));
823 setQualifiedNamesOn(result);
824 qmldirName = file;
825 } else {
826 warnings.append({
827 QStringLiteral("Argument %1 to -i option is not a qmldir file. Assuming qmltypes.")
828 .arg(file),
829 QtWarningMsg,
830 QQmlJS::SourceLocation()
831 });
832
833 readQmltypes(file, &result);
834
835 // Append _FAKE_QMLDIR to our made up qmldir name so that if it ever gets used somewhere
836 // else except for cache lookups, it will blow up due to a missing file instead of
837 // producing weird results.
838 qmldirName = file + QStringLiteral("_FAKE_QMLDIR");
839 }
840
841 warnings.append(result.warnings);
842 m_seenQmldirFilesAndDirectories.insert(qmldirName, result);
843
844 for (const auto &object : std::as_const(result.objects)) {
845 for (const auto &ex : object.exports) {
846 m_seenImports.insert({ex.package(), ex.version()}, qmldirName);
847 // We also have to handle the case that no version is provided
848 m_seenImports.insert({ex.package(), QTypeRevision()}, qmldirName);
849 }
850 }
851 }
852
853 return warnings;
854}
855
856QQmlJSImporter::ImportedTypes QQmlJSImporter::importModule(const QString &module, quint8 precedence,
857 const QString &prefix,
858 QTypeRevision version,
859 QStringList *staticModuleList)
860{
861 const AvailableTypes builtins = builtinImportHelper();
862 AvailableTypes result(builtins.cppNames);
863 if (!importHelper(module, &result, precedence, prefix, version)) {
864 result.warnings.append({
865 QStringLiteral("Failed to import %1. Are your import paths set up properly?")
866 .arg(module),
867 QtWarningMsg,
868 QQmlJS::SourceLocation()
869 });
870 }
871
872 if (staticModuleList)
873 *staticModuleList << result.staticModules;
874
875 return ImportedTypes(std::move(result.qmlNames), std::move(result.warnings),
876 std::move(result.moduleImports));
877}
878
879QQmlJSImporter::ImportedTypes QQmlJSImporter::builtinInternalNames()
880{
881 auto builtins = builtinImportHelper();
882 return ImportedTypes(std::move(builtins.cppNames), std::move(builtins.warnings));
883}
884
885bool QQmlJSImporter::importHelper(const QString &module, AvailableTypes *types, quint8 precedence,
886 const QString &prefix, QTypeRevision version, bool isDependency,
887 bool isFile)
888{
889 // QtQuick/Controls and QtQuick.Controls are the same module
890 const QString moduleCacheName = QString(module).replace(u'/', u'.');
891
892 if (isDependency)
893 Q_ASSERT(prefix.isEmpty());
894
895 const QQmlJS::Import cacheKey(prefix, moduleCacheName, version, isFile, isDependency);
896
897 // Insert a prefix even if the module was not registered.
898 if (!prefix.isEmpty())
899 types->qmlNames.setType(prefix, { { }, precedence });
900
901 auto getTypesFromCache = [&]() -> bool {
902 Q_ASSERT(m_cachedImportTypes.contains(cacheKey));
903
904 const auto &cacheEntry = m_cachedImportTypes[cacheKey];
905
906 types->cppNames.addTypes(cacheEntry->cppNames);
907 types->staticModules << cacheEntry->staticModules;
908 types->hasSystemModule |= cacheEntry->hasSystemModule;
909
910 // No need to import qml names for dependencies
911 if (!isDependency) {
912 types->warnings.append(cacheEntry->warnings);
913 types->qmlNames.addTypes(cacheEntry->qmlNames);
914 types->moduleImports.append(cacheEntry->moduleImports);
915 }
916
917 return cacheEntry->wasFound;
918 };
919
920 if (m_cachedImportTypes.contains(cacheKey))
921 return getTypesFromCache();
922
923 auto cacheTypes = QSharedPointer<QQmlJSImporter::AvailableTypes>(
924 new QQmlJSImporter::AvailableTypes(QQmlJS::ContextualTypes(
925 QQmlJS::ContextualTypes::INTERNAL, {}, {}, types->cppNames.arrayType())));
926 m_cachedImportTypes[cacheKey] = cacheTypes;
927 if (!isDependency)
928 cacheTypes->moduleImports.append(cacheKey);
929
930 const std::pair<QString, QTypeRevision> importId { module, version };
931 const auto it = m_seenImports.constFind(importId);
932
933 if (it != m_seenImports.constEnd()) {
934 if (it->isEmpty()) {
935 cacheTypes->wasFound = false;
936 return false;
937 }
938
939 Q_ASSERT(m_seenQmldirFilesAndDirectories.contains(*it));
940 const QQmlJSImporter::Import import = m_seenQmldirFilesAndDirectories.value(*it);
941
942 importDependencies(import, precedence, cacheTypes.get(), prefix, version, isDependency);
943 processImport(cacheKey, import, precedence, cacheTypes.get());
944
945 return getTypesFromCache();
946 }
947
948 QStringList modulePaths;
949 if (isFile) {
950 const auto import = readDirectory(module);
951 m_seenQmldirFilesAndDirectories.insert(module, import);
952 m_seenImports.insert(importId, module);
953 importDependencies(import, precedence, cacheTypes.get(), prefix, version, isDependency);
954 processImport(cacheKey, import, precedence, cacheTypes.get());
955
956 // Try to load a qmldir below, on top of the directory import.
957 modulePaths.append(module);
958 } else if (module == "QML"_L1) {
959 // Always load builtins from the resource file system. We don't want any nasty surprises
960 // that could surface when loading them from an actual import path. We rely on the builtins
961 // to contain a number of types later on. It's better to always use our own copy.
962 modulePaths = { ":/qt-project.org/imports/QML"_L1 };
963 } else {
964 modulePaths = qQmlResolveImportPaths(module, m_importPaths, version);
965 }
966
967 for (auto const &modulePath : std::as_const(modulePaths)) {
968 QString qmldirPath = ensureSlashQmldir(modulePath);
969 if (modulePath.startsWith(u':')) {
970 if (module == "QML"_L1) {
971 // Do not try to map the builtins' resource path.
972 } else if (m_mapper) {
973 const QString resourcePath = ensureSlashQmldir(modulePath.mid(1));
974 const auto entry = m_mapper->entry(
975 QQmlJSResourceFileMapper::resourceFileFilter(resourcePath));
976 qmldirPath = entry.filePath;
977 } else {
978 // The builtins are loaded from the resource file system. Don't warn about them.
979 qWarning() << "Cannot read files from resource directory" << modulePath
980 << "because no resource file mapper was provided";
981 }
982 }
983
984 const auto setupImport = [&](const QQmlJSImporter::Import &import) {
985 m_seenImports.insert(importId, qmldirPath);
986 importDependencies(import, precedence, cacheTypes.get(), prefix, version, isDependency);
987 // Potentially merges with the result of readDirectory() above.
988 processImport(cacheKey, import, precedence, cacheTypes.get());
989 return getTypesFromCache();
990 };
991
992 // Check whether we need to import again below, reading the qmldir this time.
993 const auto it = m_seenQmldirFilesAndDirectories.constFind(qmldirPath);
994 if (it != m_seenQmldirFilesAndDirectories.constEnd() && it->type == Import::Qmldir)
995 return setupImport(*it);
996
997 const QFileInfo file(qmldirPath);
998 if (file.exists()) {
999 const auto import = readQmldir(file.canonicalPath());
1000 setQualifiedNamesOn(import);
1001 m_seenQmldirFilesAndDirectories.insert(qmldirPath, import);
1002 return setupImport(import);
1003 }
1004
1005 if (it != m_seenQmldirFilesAndDirectories.constEnd())
1006 return setupImport(*it);
1007 }
1008
1009 if (isFile) {
1010 // We've loaded the directory above
1011 return getTypesFromCache();
1012 }
1013
1014 m_seenImports.insert(importId, QString());
1015 cacheTypes->wasFound = false;
1016 return false;
1017}
1018
1019QQmlJSScope::Ptr QQmlJSImporter::localFile2QQmlJSScope(const QString &filePath)
1020{
1021 // Use the build folder path as the canonical location for a type to prevent scope duplication.
1022 const QString buildFolderFile = QQmlJSUtils::qmlBuildPathFromSourcePath(m_mapper, filePath);
1023 const QString sourceFolderFile = preferQmlFilesFromSourceFolder()
1024 ? QQmlJSUtils::qmlSourcePathFromBuildPath(m_mapper, filePath)
1025 : filePath;
1026
1027 const auto seen = m_importedFiles.find(buildFolderFile);
1028 if (seen != m_importedFiles.end())
1029 return *seen;
1030
1031 return *m_importedFiles.insert(
1032 buildFolderFile,
1033 { QQmlJSScope::create(),
1034 QSharedPointer<QDeferredFactory<QQmlJSScope>>(new QDeferredFactory<QQmlJSScope>(
1035 this, {}, sourceFolderFile, QString(), false)) });
1036}
1037
1038QQmlJSScope::Ptr QQmlJSImporter::importFile(const QString &file)
1039{
1040 return localFile2QQmlJSScope(file);
1041}
1042
1043QQmlJSImporter::ImportedTypes
1044QQmlJSImporter::importDirectory(const QString &directory, quint8 precedence, const QString &prefix)
1045{
1046 const AvailableTypes builtins = builtinImportHelper();
1047 QQmlJSImporter::AvailableTypes types(QQmlJS::ContextualTypes(
1048 QQmlJS::ContextualTypes::INTERNAL, {}, {}, builtins.cppNames.arrayType()));
1049 importHelper(directory, &types, precedence, prefix, QTypeRevision(), false, true);
1050 return ImportedTypes(std::move(types.qmlNames), std::move(types.warnings),
1051 std::move(types.moduleImports));
1052}
1053
1054/*!
1055 \internal
1056 Records through which module imports the types in \a imported are visible, so that
1057 they can later be found again by name at run time. Only call this for the imports a document
1058 actually has. Directory imports are recorded with the module of the qmldir file in the
1059 directory and an invalid version, like the QML engine does.
1060 */
1061void QQmlJSImporter::registerVersionedImports(const ImportedTypes &imported)
1062{
1063 const QList<QQmlJS::Import> &moduleImports = imported.moduleImports();
1064 const auto findImport = [&](const QString &module) -> const QQmlJS::Import * {
1065 const auto byName =
1066 std::find_if(moduleImports.cbegin(), moduleImports.cend(),
1067 [&](const QQmlJS::Import &import) { return import.name() == module; });
1068 if (byName != moduleImports.cend())
1069 return &*byName;
1070 const auto byDirectory =
1071 std::find_if(moduleImports.cbegin(), moduleImports.cend(),
1072 [](const QQmlJS::Import &import) { return import.isFile(); });
1073 return byDirectory != moduleImports.cend() ? &*byDirectory : nullptr;
1074 };
1075
1076 const QHash<QString, QQmlJS::ContextualType> &types = imported.contextualTypes().types();
1077 for (auto it = types.cbegin(), end = types.cend(); it != end; ++it) {
1078 const QQmlJSScope::ConstPtr &scope = it->scope;
1079 if (!scope)
1080 continue;
1081
1082 const QString module = moduleName(scope);
1083 if (module.isEmpty())
1084 continue;
1085
1086 const QQmlJS::Import *import = findImport(module);
1087 if (!import)
1088 continue;
1089
1090 QString name = it.key();
1091 if (!import->prefix().isEmpty()) {
1092 const QString qualifier = import->prefix() + u'.';
1093 if (!name.startsWith(qualifier))
1094 continue;
1095 name = name.mid(qualifier.size());
1096 }
1097
1098 // Internal names, anonymous types, and module markers
1099 if (name.startsWith(u'$'))
1100 continue;
1101
1102 const VersionedImport versionedImport {
1103 module, name, import->isFile() ? QTypeRevision() : import->version()
1104 };
1105 QList<VersionedImport> &versionedImports = m_versionedImports[filePath(scope)];
1106 if (!versionedImports.contains(versionedImport))
1107 versionedImports.append(versionedImport);
1108 }
1109}
1110
1111void QQmlJSImporter::setImportPaths(const QStringList &importPaths)
1112{
1113 if (m_importPaths == importPaths)
1114 return;
1115
1116 m_importPaths = importPaths;
1117
1118 // We have to get rid off all cache elements directly referencing modules, since changing
1119 // importPaths might change which module is found first
1120 m_seenImports.clear();
1121 m_cachedImportTypes.clear();
1122 // Luckily this doesn't apply to m_seenQmldirFiles
1123}
1124
1125void QQmlJSImporter::clearCache()
1126{
1127 m_seenImports.clear();
1128 m_cachedImportTypes.clear();
1129 m_seenQmldirFilesAndDirectories.clear();
1130 m_importedFiles.clear();
1131 m_versionedImports.clear();
1132 m_builtins.reset();
1133}
1134
1135QQmlJSScope::ConstPtr QQmlJSImporter::jsGlobalObject()
1136{
1137 return builtinImportHelper().cppNames.type(u"GlobalObject"_s).scope;
1138}
1139
1140void QQmlJSImporter::setQualifiedNamesOn(const Import &import)
1141{
1142 for (auto &object : import.objects) {
1143 if (object.exports.isEmpty())
1144 continue;
1145 if (auto *factory = object.scope.factory()) {
1146 factory->setModuleName(import.name);
1147 } else {
1148 object.scope->setOwnModuleName(import.name);
1149 }
1150 }
1151}
1152
1153void QQmlJSImporter::runImportVisitor(QQmlJS::AST::Node *rootNode,
1154 const ImportVisitorPrerequisites &p)
1155{
1156 m_importVisitor(rootNode, this, p);
1157}
1158
1159QT_END_NAMESPACE
bool isValid() const
\inmodule QtCore
Combined button and popup list for selecting options.
static QStringList aliases(const QQmlJSScope::ConstPtr &scope)
static QString filePath(const QQmlJSScope::ConstPtr &scope)
static const QLatin1String JsrootDotQmltypes
static bool isVersionAllowed(const QQmlJSScope::Export &exportEntry, const QQmlJS::Import &importDescription)
static bool isComposite(const QQmlJSScope::ConstPtr &scope)
static const QString prefixedName(const QString &prefix, const QString &name)
static QString ensureSlashQmldir(const QString &path)
static QString resolvePreferredPath(const QString &qmldirPath, const QString &prefer, const QQmlJSResourceFileMapper *mapper)
static const QLatin1String SlashQmldir
static QString internalName(const QQmlJSScope::ConstPtr &scope)
static QList< QQmlJS::Export > createExports(const QString &name)
static QString moduleName(const QQmlJSScope::ConstPtr &scope)
static const QLatin1String PluginsDotQmltypes