diff options
Diffstat (limited to 'src/qmlcompiler/qqmljsimporter.cpp')
-rw-r--r-- | src/qmlcompiler/qqmljsimporter.cpp | 969 |
1 files changed, 969 insertions, 0 deletions
diff --git a/src/qmlcompiler/qqmljsimporter.cpp b/src/qmlcompiler/qqmljsimporter.cpp new file mode 100644 index 0000000000..ae5d1654f0 --- /dev/null +++ b/src/qmlcompiler/qqmljsimporter.cpp @@ -0,0 +1,969 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qqmljsimporter_p.h" +#include "qqmljstypedescriptionreader_p.h" +#include "qqmljstypereader_p.h" +#include "qqmljsimportvisitor_p.h" +#include "qqmljslogger_p.h" + +#include <QtQml/private/qqmlimportresolver_p.h> + +#include <QtCore/qfileinfo.h> +#include <QtCore/qdiriterator.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static const QLatin1String SlashQmldir = QLatin1String("/qmldir"); +static const QLatin1String PluginsDotQmltypes = QLatin1String("plugins.qmltypes"); + + +QQmlJS::Import::Import(QString prefix, QString name, QTypeRevision version, bool isFile, + bool isDependency) : + m_prefix(std::move(prefix)), + m_name(std::move(name)), + m_version(version), + m_isFile(isFile), + m_isDependency(isDependency) +{ +} + +bool QQmlJS::Import::isValid() const +{ + return !m_name.isEmpty(); +} + +static const QString prefixedName(const QString &prefix, const QString &name) +{ + Q_ASSERT(!prefix.endsWith(u'.')); + return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name); +} + +QQmlDirParser QQmlJSImporter::createQmldirParserForFile(const QString &filename) +{ + QFile f(filename); + QQmlDirParser parser; + if (f.open(QFile::ReadOnly)) { + parser.parse(QString::fromUtf8(f.readAll())); + } else { + m_warnings.append({ + QStringLiteral("Could not open qmldir file: ") + + filename, + QtWarningMsg, + QQmlJS::SourceLocation() + }); + } + + return parser; +} + +void QQmlJSImporter::readQmltypes( + const QString &filename, QList<QQmlJSExportedScope> *objects, + QList<QQmlDirParser::Import> *dependencies) +{ + const QFileInfo fileInfo(filename); + if (!fileInfo.exists()) { + m_warnings.append({ + QStringLiteral("QML types file does not exist: ") + filename, + QtWarningMsg, + QQmlJS::SourceLocation() + }); + return; + } + + if (fileInfo.isDir()) { + m_warnings.append({ + QStringLiteral("QML types file cannot be a directory: ") + filename, + QtWarningMsg, + QQmlJS::SourceLocation() + }); + return; + } + + QFile file(filename); + if (!file.open(QFile::ReadOnly)) { + m_warnings.append({ + QStringLiteral("QML types file cannot be opened: ") + filename, + QtWarningMsg, + QQmlJS::SourceLocation() + }); + return; + } + + QQmlJSTypeDescriptionReader reader { filename, QString::fromUtf8(file.readAll()) }; + QStringList dependencyStrings; + auto succ = reader(objects, &dependencyStrings); + if (!succ) + m_warnings.append({ reader.errorMessage(), QtCriticalMsg, QQmlJS::SourceLocation() }); + + const QString warningMessage = reader.warningMessage(); + if (!warningMessage.isEmpty()) + m_warnings.append({ warningMessage, QtWarningMsg, QQmlJS::SourceLocation() }); + + if (dependencyStrings.isEmpty()) + return; + + m_warnings.append({ + QStringLiteral("Found deprecated dependency specifications in %1." + "Specify dependencies in qmldir and use qmltyperegistrar " + "to generate qmltypes files without dependencies.") + .arg(filename), + QtWarningMsg, + QQmlJS::SourceLocation() + }); + + for (const QString &dependency : std::as_const(dependencyStrings)) { + const auto blank = dependency.indexOf(u' '); + if (blank < 0) { + dependencies->append(QQmlDirParser::Import(dependency, {}, + QQmlDirParser::Import::Default)); + continue; + } + + const QString module = dependency.left(blank); + const QString versionString = dependency.mid(blank + 1).trimmed(); + if (versionString == QStringLiteral("auto")) { + dependencies->append(QQmlDirParser::Import(module, {}, QQmlDirParser::Import::Auto)); + continue; + } + + const auto dot = versionString.indexOf(u'.'); + + const QTypeRevision version = dot < 0 + ? QTypeRevision::fromMajorVersion(versionString.toUShort()) + : QTypeRevision::fromVersion(versionString.left(dot).toUShort(), + versionString.mid(dot + 1).toUShort()); + + dependencies->append(QQmlDirParser::Import(module, version, + QQmlDirParser::Import::Default)); + } +} + +static QString internalName(const QQmlJSScope::ConstPtr &scope) +{ + if (const auto *factory = scope.factory()) + return factory->internalName(); + return scope->internalName(); +} + +static bool isComposite(const QQmlJSScope::ConstPtr &scope) +{ + // The only thing the factory can do is load a composite type. + return scope.factory() || scope->isComposite(); +} + +static QStringList aliases(const QQmlJSScope::ConstPtr &scope) +{ + return isComposite(scope) + ? QStringList() + : scope->aliases(); +} + +QQmlJSImporter::QQmlJSImporter(const QStringList &importPaths, QQmlJSResourceFileMapper *mapper, + bool useOptionalImports) + : m_importPaths(importPaths), + m_mapper(mapper), + m_useOptionalImports(useOptionalImports), + m_importVisitor([](QQmlJS::AST::Node *rootNode, QQmlJSImporter *self, + const ImportVisitorPrerequisites &p) { + auto visitor = std::unique_ptr<QQmlJS::AST::BaseVisitor>(new QQmlJSImportVisitor( + p.m_target, self, p.m_logger, p.m_implicitImportDirectory, p.m_qmldirFiles)); + QQmlJS::AST::Node::accept(rootNode, visitor.get()); + }) +{ +} + +static QString resolvePreferredPath( + const QString &qmldirPath, const QString &prefer, QQmlJSResourceFileMapper *mapper) +{ + if (prefer.isEmpty()) + return qmldirPath; + + if (!prefer.endsWith(u'/')) { + qWarning() << "Ignoring invalid prefer path" << prefer << "(has to end with slash)"; + return qmldirPath; + } + + if (prefer.startsWith(u':')) { + // Resource path: Resolve via resource file mapper if possible. + if (!mapper) + return qmldirPath; + + Q_ASSERT(prefer.endsWith(u'/')); + const auto entry = mapper->entry( + QQmlJSResourceFileMapper::resourceFileFilter(prefer.mid(1) + SlashQmldir.mid(1))); + + // This can be empty if the .qrc files does not belong to this module. + // In that case we trust the given qmldir file. + return entry.filePath.endsWith(SlashQmldir) + ? entry.filePath + : qmldirPath; + } + + // Host file system path. This should be rare. We don't generate it. + const QFileInfo f(prefer + SlashQmldir); + const QString canonical = f.canonicalFilePath(); + if (canonical.isEmpty()) { + qWarning() << "No qmldir at" << prefer; + return qmldirPath; + } + return canonical; +} + +QQmlJSImporter::Import QQmlJSImporter::readQmldir(const QString &modulePath) +{ + const QString moduleQmldirPath = modulePath + SlashQmldir; + auto reader = createQmldirParserForFile(moduleQmldirPath); + + const QString resolvedQmldirPath + = resolvePreferredPath(moduleQmldirPath, reader.preferredPath(), m_mapper); + if (resolvedQmldirPath != moduleQmldirPath) + reader = createQmldirParserForFile(resolvedQmldirPath); + + // Leave the trailing slash + Q_ASSERT(resolvedQmldirPath.endsWith(SlashQmldir)); + QStringView resolvedPath = QStringView(resolvedQmldirPath).chopped(SlashQmldir.size() - 1); + + Import result; + result.name = reader.typeNamespace(); + + result.isStaticModule = reader.isStaticModule(); + result.isSystemModule = reader.isSystemModule(); + result.imports.append(reader.imports()); + result.dependencies.append(reader.dependencies()); + + const auto typeInfos = reader.typeInfos(); + for (const auto &typeInfo : typeInfos) { + const QString typeInfoPath = QFileInfo(typeInfo).isRelative() + ? resolvedPath + typeInfo + : typeInfo; + readQmltypes(typeInfoPath, &result.objects, &result.dependencies); + } + + if (typeInfos.isEmpty() && !reader.plugins().isEmpty()) { + const QString defaultTypeInfoPath = resolvedPath + PluginsDotQmltypes; + if (QFile::exists(defaultTypeInfoPath)) { + m_warnings.append({ + QStringLiteral("typeinfo not declared in qmldir file: ") + + defaultTypeInfoPath, + QtWarningMsg, + QQmlJS::SourceLocation() + }); + readQmltypes(defaultTypeInfoPath, &result.objects, &result.dependencies); + } + } + + QHash<QString, QQmlJSExportedScope> qmlComponents; + const auto components = reader.components(); + for (auto it = components.begin(), end = components.end(); it != end; ++it) { + const QString filePath = resolvedPath + it->fileName; + if (!QFile::exists(filePath)) { + m_warnings.append({ + it->fileName + QStringLiteral(" is listed as component in ") + + resolvedQmldirPath + + QStringLiteral(" but does not exist.\n"), + QtWarningMsg, + QQmlJS::SourceLocation() + }); + continue; + } + + auto mo = qmlComponents.find(it->fileName); + if (mo == qmlComponents.end()) { + QQmlJSScope::Ptr imported = localFile2ScopeTree(filePath); + if (auto *factory = imported.factory()) { + if (it->singleton) { + factory->setIsSingleton(true); + } + } + mo = qmlComponents.insert(it->fileName, {imported, QList<QQmlJSScope::Export>() }); + } + + mo->exports.append(QQmlJSScope::Export( + reader.typeNamespace(), it.key(), it->version, QTypeRevision())); + } + for (auto it = qmlComponents.begin(), end = qmlComponents.end(); it != end; ++it) + result.objects.append(it.value()); + + const auto scripts = reader.scripts(); + for (const auto &script : scripts) { + const QString filePath = resolvedPath + script.fileName; + auto mo = result.scripts.find(script.fileName); + if (mo == result.scripts.end()) + mo = result.scripts.insert(script.fileName, { localFile2ScopeTree(filePath), {} }); + + mo->exports.append(QQmlJSScope::Export( + reader.typeNamespace(), script.nameSpace, + script.version, QTypeRevision())); + } + return result; +} + +QQmlJSImporter::Import QQmlJSImporter::readDirectory(const QString &directory) +{ + Import import; + if (directory.startsWith(u':')) { + if (m_mapper) { + const auto resources = m_mapper->filter( + QQmlJSResourceFileMapper::resourceQmlDirectoryFilter(directory.mid(1))); + for (const auto &entry : resources) { + const QString name = QFileInfo(entry.resourcePath).baseName(); + if (name.front().isUpper()) { + import.objects.append({ + localFile2ScopeTree(entry.filePath), + { QQmlJSScope::Export(QString(), name, QTypeRevision(), QTypeRevision()) } + }); + } + } + } else { + qWarning() << "Cannot read files from resource directory" << directory + << "because no resource file mapper was provided"; + } + + return import; + } + + QDirIterator it { + directory, + QStringList() << QLatin1String("*.qml"), + QDir::NoFilter + }; + while (it.hasNext()) { + QString name = it.nextFileInfo().completeBaseName(); + + // Non-uppercase names cannot be imported anyway. + if (!name.front().isUpper()) + continue; + + // .ui.qml is fine + if (name.endsWith(u".ui")) + name = name.chopped(3); + + // Names with dots in them cannot be imported either. + if (name.contains(u'.')) + continue; + + import.objects.append({ + localFile2ScopeTree(it.filePath()), + { QQmlJSScope::Export(QString(), name, QTypeRevision(), QTypeRevision()) } + }); + } + return import; +} + +void QQmlJSImporter::importDependencies(const QQmlJSImporter::Import &import, + QQmlJSImporter::AvailableTypes *types, + const QString &prefix, QTypeRevision version, + bool isDependency) +{ + // Import the dependencies with an invalid prefix. The prefix will never be matched by actual + // QML code but the C++ types will be visible. + for (auto const &dependency : std::as_const(import.dependencies)) + importHelper(dependency.module, types, QString(), dependency.version, true); + + bool hasOptionalImports = false; + for (auto const &import : std::as_const(import.imports)) { + if (import.flags & QQmlDirParser::Import::Optional) { + hasOptionalImports = true; + if (!m_useOptionalImports) { + continue; + } + + if (!(import.flags & QQmlDirParser::Import::OptionalDefault)) + continue; + } + + importHelper(import.module, types, isDependency ? QString() : prefix, + (import.flags & QQmlDirParser::Import::Auto) ? version : import.version, + isDependency); + } + + if (hasOptionalImports && !m_useOptionalImports) { + m_warnings.append( + { u"%1 uses optional imports which are not supported. Some types might not be found."_s + .arg(import.name), + QtCriticalMsg, QQmlJS::SourceLocation() }); + } +} + +static bool isVersionAllowed(const QQmlJSScope::Export &exportEntry, + const QQmlJS::Import &importDescription) +{ + const QTypeRevision importVersion = importDescription.version(); + const QTypeRevision exportVersion = exportEntry.version(); + if (!importVersion.hasMajorVersion()) + return true; + if (importVersion.majorVersion() != exportVersion.majorVersion()) + return false; + return !importVersion.hasMinorVersion() + || exportVersion.minorVersion() <= importVersion.minorVersion(); +} + +void QQmlJSImporter::processImport(const QQmlJS::Import &importDescription, + const QQmlJSImporter::Import &import, + QQmlJSImporter::AvailableTypes *types) +{ + // In the list of QML types we prefix unresolvable QML names with $anonymous$, and C++ + // names with $internal$. This is to avoid clashes between them. + // In the list of C++ types we insert types that don't have a C++ name as their + // QML name prefixed with $anonymous$. + const QString anonPrefix = QStringLiteral("$anonymous$"); + const QString internalPrefix = QStringLiteral("$internal$"); + const QString modulePrefix = QStringLiteral("$module$"); + QHash<QString, QList<QQmlJSScope::Export>> seenExports; + + const auto insertAliases = [&](const QQmlJSScope::ConstPtr &scope, QTypeRevision revision) { + const QStringList cppAliases = aliases(scope); + for (const QString &alias : cppAliases) + types->cppNames.setType(alias, { scope, revision }); + }; + + const auto insertExports = [&](const QQmlJSExportedScope &val, const QString &cppName) { + QQmlJSScope::Export bestExport; + + // Resolve conflicting qmlNames within an import + for (const auto &valExport : val.exports) { + const QString qmlName = prefixedName(importDescription.prefix(), valExport.type()); + if (!isVersionAllowed(valExport, importDescription)) + continue; + + // Even if the QML name is overridden by some other type, we still want + // to insert the C++ type, with the highest revision available. + if (!bestExport.isValid() || valExport.version() > bestExport.version()) + bestExport = valExport; + + const auto it = types->qmlNames.types().find(qmlName); + if (it != types->qmlNames.types().end()) { + + // The same set of exports can declare the same name multiple times for different + // versions. That's the common thing and we would just continue here when we hit + // it again after having inserted successfully once. + // However, it can also declare *different* names. Then we need to do the whole + // thing again. + if (it->scope == val.scope && it->revision == valExport.version()) + continue; + + const auto existingExports = seenExports.value(qmlName); + enum { LowerVersion, SameVersion, HigherVersion } seenVersion = LowerVersion; + for (const QQmlJSScope::Export &entry : existingExports) { + if (!isVersionAllowed(entry, importDescription)) + continue; + + if (valExport.version() < entry.version()) { + seenVersion = HigherVersion; + break; + } + + if (seenVersion == LowerVersion && valExport.version() == entry.version()) + seenVersion = SameVersion; + } + + switch (seenVersion) { + case LowerVersion: + break; + case SameVersion: { + m_warnings.append({ + QStringLiteral("Ambiguous type detected. " + "%1 %2.%3 is defined multiple times.") + .arg(qmlName) + .arg(valExport.version().majorVersion()) + .arg(valExport.version().minorVersion()), + QtCriticalMsg, + QQmlJS::SourceLocation() + }); + + // Invalidate the type. We don't know which one to use. + types->qmlNames.clearType(qmlName); + continue; + } + case HigherVersion: + continue; + } + } + + types->qmlNames.setType(qmlName, { val.scope, valExport.version() }); + seenExports[qmlName].append(valExport); + } + + const QTypeRevision bestRevision = bestExport.isValid() + ? bestExport.revision() + : QTypeRevision::zero(); + types->cppNames.setType(cppName, { val.scope, bestRevision }); + + insertAliases(val.scope, bestRevision); + + const QTypeRevision bestVersion = bestExport.isValid() + ? bestExport.version() + : QTypeRevision::zero(); + types->qmlNames.setType(prefixedName(internalPrefix, cppName), { val.scope, bestVersion }); + }; + + // Empty type means "this is the prefix" + if (!importDescription.prefix().isEmpty()) + types->qmlNames.setType(importDescription.prefix(), {}); + + // Add a marker to show that this module has been imported + if (!importDescription.isDependency()) + types->qmlNames.setType(prefixedName(modulePrefix, importDescription.name()), {}); + + if (!importDescription.isDependency()) { + if (import.isStaticModule) + types->staticModules << import.name; + + if (import.isSystemModule) + types->hasSystemModule = true; + } + + for (auto it = import.scripts.begin(); it != import.scripts.end(); ++it) { + // You cannot have a script without an export + Q_ASSERT(!it->exports.isEmpty()); + insertExports(*it, prefixedName(anonPrefix, internalName(it->scope))); + } + + // add objects + for (const auto &val : import.objects) { + const QString cppName = isComposite(val.scope) + ? prefixedName(anonPrefix, internalName(val.scope)) + : internalName(val.scope); + + if (val.exports.isEmpty()) { + // Insert an unresolvable dummy name + types->qmlNames.setType( + prefixedName(internalPrefix, cppName), { val.scope, QTypeRevision() }); + types->cppNames.setType(cppName, { val.scope, QTypeRevision() }); + insertAliases(val.scope, QTypeRevision()); + } else { + insertExports(val, cppName); + } + } + + /* We need to create a temporary AvailableTypes instance here to make builtins available as + QQmlJSScope::resolveTypes relies on them being available. They cannot be part of the regular + types as they would keep overwriting existing types when loaded from cache. + This is only a problem with builtin types as only builtin types can be overridden by any + sibling import. Consider the following qmldir: + + module Things + import QtQml 2.0 + import QtQuick.LocalStorage auto + + The module "Things" sees QtQml's definition of Qt, not the builtins', even though + QtQuick.LocalStorage does not depend on QtQml and is imported afterwards. Conversely: + + module Stuff + import ModuleOverridingQObject + import QtQuick + + The module "Stuff" sees QtQml's definition of QObject (via QtQuick), even if + ModuleOverridingQObject has overridden it. + */ + + QQmlJSImporter::AvailableTypes tempTypes(builtinImportHelper().cppNames); + tempTypes.cppNames.addTypes(types->cppNames); + + // At present, there are corner cases that couldn't be resolved in a single + // pass of resolveTypes() (e.g. QQmlEasingEnums::Type). However, such cases + // only happen when enumerations are involved, thus the strategy is to + // resolve enumerations (which can potentially create new child scopes) + // before resolving the type fully + const QQmlJSScope::ConstPtr arrayType = tempTypes.cppNames.type(u"Array"_s).scope; + for (auto it = import.objects.begin(); it != import.objects.end(); ++it) { + if (!it->scope.factory()) { + QQmlJSScope::resolveEnums(it->scope, tempTypes.cppNames); + QQmlJSScope::resolveList(it->scope, arrayType); + } + } + + for (const auto &val : std::as_const(import.objects)) { + // Otherwise we have already done it in localFile2ScopeTree() + if (!val.scope.factory() && val.scope->baseType().isNull()) { + + // Composite types use QML names, and we should have resolved those already. + // ... except that old qmltypes files might specify composite types with C++ names. + // Warn about those. + if (val.scope->isComposite()) { + m_warnings.append({ + QStringLiteral("Found incomplete composite type %1. Do not use qmlplugindump.") + .arg(val.scope->internalName()), + QtWarningMsg, + QQmlJS::SourceLocation() + }); + } + + QQmlJSScope::resolveNonEnumTypes(val.scope, tempTypes.cppNames); + } + } +} + +/*! + * Imports builtins.qmltypes and jsroot.qmltypes found in any of the import paths. + */ +QQmlJSImporter::ImportedTypes QQmlJSImporter::importBuiltins() +{ + return builtinImportHelper().qmlNames; +} + + +QQmlJSImporter::AvailableTypes QQmlJSImporter::builtinImportHelper() +{ + if (m_builtins) + return *m_builtins; + + AvailableTypes builtins(ImportedTypes(ImportedTypes::INTERNAL, {}, {})); + + Import result; + result.name = QStringLiteral("QML"); + + const auto importBuiltins = [&](const QString &qmltypesFile, const QStringList &imports) { + for (auto const &dir : imports) { + const QDir importDir(dir); + if (!importDir.exists(qmltypesFile)) + continue; + + readQmltypes( + importDir.filePath(qmltypesFile), &result.objects, &result.dependencies); + setQualifiedNamesOn(result); + importDependencies(result, &builtins); + return true; + } + + return false; + }; + + // If the same name (such as "Qt") appears in the JS root and in the builtins, + // we want the builtins to override the JS root. Therefore, process jsroot first. + QStringList qmltypesFiles; + for (QString qmltypesFile : { "jsroot.qmltypes"_L1, "builtins.qmltypes"_L1 }) { + if (!importBuiltins(qmltypesFile, m_importPaths)) + qmltypesFiles.append(std::move(qmltypesFile)); + } + + if (!qmltypesFiles.isEmpty()) { + const QString pathsString = + m_importPaths.isEmpty() ? u"<empty>"_s : m_importPaths.join(u"\n\t"); + m_warnings.append({ QStringLiteral("Failed to find the following builtins: %1 (so will use " + "qrc). Import paths used:\n\t%2") + .arg(qmltypesFiles.join(u", "), pathsString), + QtWarningMsg, QQmlJS::SourceLocation() }); + + // use qrc as a "last resort" + for (const QString &qmltypesFile : std::as_const(qmltypesFiles)) { + const bool found = importBuiltins(qmltypesFile, { u":/qt-project.org/qml/builtins"_s }); + Q_ASSERT(found); // since qrc must cover it in all the bad cases + } + } + + // Process them together since there they have interdependencies that wouldn't get resolved + // otherwise + const QQmlJS::Import builtinImport( + QString(), QStringLiteral("QML"), QTypeRevision::fromVersion(1, 0), false, true); + + QQmlJSScope::ConstPtr intType; + QQmlJSScope::ConstPtr arrayType; + + for (const QQmlJSExportedScope &exported : result.objects) { + if (exported.scope->internalName() == u"int"_s) { + intType = exported.scope; + if (!arrayType.isNull()) + break; + } else if (exported.scope->internalName() == u"Array"_s) { + arrayType = exported.scope; + if (!intType.isNull()) + break; + } + } + + Q_ASSERT(intType); + Q_ASSERT(arrayType); + + m_builtins = AvailableTypes( + ImportedTypes(ImportedTypes::INTERNAL, builtins.cppNames.types(), arrayType)); + m_builtins->qmlNames + = ImportedTypes(ImportedTypes::QML, builtins.qmlNames.types(), arrayType); + + processImport(builtinImport, result, &(*m_builtins)); + + return *m_builtins; +} + +/*! + * Imports types from the specified \a qmltypesFiles. + */ +void QQmlJSImporter::importQmldirs(const QStringList &qmldirFiles) +{ + for (const auto &file : qmldirFiles) { + Import result; + QString qmldirName; + if (file.endsWith(SlashQmldir)) { + result = readQmldir(file.chopped(SlashQmldir.size())); + setQualifiedNamesOn(result); + qmldirName = file; + } else { + m_warnings.append({ + QStringLiteral("Argument %1 to -i option is not a qmldir file. Assuming qmltypes.") + .arg(file), + QtWarningMsg, + QQmlJS::SourceLocation() + }); + + readQmltypes(file, &result.objects, &result.dependencies); + + // Append _FAKE_QMLDIR to our made up qmldir name so that if it ever gets used somewhere + // else except for cache lookups, it will blow up due to a missing file instead of + // producing weird results. + qmldirName = file + QStringLiteral("_FAKE_QMLDIR"); + } + + m_seenQmldirFiles.insert(qmldirName, result); + + for (const auto &object : std::as_const(result.objects)) { + for (const auto &ex : object.exports) { + m_seenImports.insert({ex.package(), ex.version()}, qmldirName); + // We also have to handle the case that no version is provided + m_seenImports.insert({ex.package(), QTypeRevision()}, qmldirName); + } + } + } +} + +QQmlJSImporter::ImportedTypes QQmlJSImporter::importModule(const QString &module, + const QString &prefix, + QTypeRevision version, + QStringList *staticModuleList) +{ + const AvailableTypes builtins = builtinImportHelper(); + AvailableTypes result(builtins.cppNames); + if (!importHelper(module, &result, prefix, version)) { + m_warnings.append({ + QStringLiteral("Failed to import %1. Are your import paths set up properly?").arg(module), + QtWarningMsg, + QQmlJS::SourceLocation() + }); + } + + // If we imported a system module add all builtin QML types + if (result.hasSystemModule) { + for (auto nameIt = builtins.qmlNames.types().keyBegin(), + end = builtins.qmlNames.types().keyEnd(); + nameIt != end; ++nameIt) + result.qmlNames.setType(prefixedName(prefix, *nameIt), builtins.qmlNames.type(*nameIt)); + } + + if (staticModuleList) + *staticModuleList << result.staticModules; + + return result.qmlNames; +} + +QQmlJSImporter::ImportedTypes QQmlJSImporter::builtinInternalNames() +{ + return builtinImportHelper().cppNames; +} + +bool QQmlJSImporter::importHelper(const QString &module, AvailableTypes *types, + const QString &prefix, QTypeRevision version, bool isDependency, + bool isFile) +{ + // QtQuick/Controls and QtQuick.Controls are the same module + const QString moduleCacheName = QString(module).replace(u'/', u'.'); + + if (isDependency) + Q_ASSERT(prefix.isEmpty()); + + const QQmlJS::Import cacheKey(prefix, moduleCacheName, version, isFile, isDependency); + + auto getTypesFromCache = [&]() -> bool { + if (!m_cachedImportTypes.contains(cacheKey)) + return false; + + const auto &cacheEntry = m_cachedImportTypes[cacheKey]; + + types->cppNames.addTypes(cacheEntry->cppNames); + types->staticModules << cacheEntry->staticModules; + types->hasSystemModule |= cacheEntry->hasSystemModule; + + // No need to import qml names for dependencies + if (!isDependency) + types->qmlNames.addTypes(cacheEntry->qmlNames); + + return true; + }; + + // The QML module only contains builtins and is not registered declaratively, so ignore requests + // for importing it + if (module == u"QML"_s) + return true; + + if (getTypesFromCache()) + return true; + + auto cacheTypes = QSharedPointer<QQmlJSImporter::AvailableTypes>( + new QQmlJSImporter::AvailableTypes( + ImportedTypes(ImportedTypes::INTERNAL, {}, types->cppNames.arrayType()))); + m_cachedImportTypes[cacheKey] = cacheTypes; + + const QPair<QString, QTypeRevision> importId { module, version }; + const auto it = m_seenImports.constFind(importId); + + if (it != m_seenImports.constEnd()) { + if (it->isEmpty()) + return false; + + Q_ASSERT(m_seenQmldirFiles.contains(*it)); + const QQmlJSImporter::Import import = m_seenQmldirFiles.value(*it); + + importDependencies(import, cacheTypes.get(), prefix, version, isDependency); + processImport(cacheKey, import, cacheTypes.get()); + + const bool typesFromCache = getTypesFromCache(); + Q_ASSERT(typesFromCache); + return typesFromCache; + } + + QStringList modulePaths; + if (isFile) { + const auto import = readDirectory(module); + m_seenQmldirFiles.insert(module, import); + m_seenImports.insert(importId, module); + importDependencies(import, cacheTypes.get(), prefix, version, isDependency); + processImport(cacheKey, import, cacheTypes.get()); + + // Try to load a qmldir below, on top of the directory import. + modulePaths.append(module); + } else { + modulePaths = qQmlResolveImportPaths(module, m_importPaths, version); + } + + for (auto const &modulePath : modulePaths) { + QString qmldirPath; + if (modulePath.startsWith(u':')) { + if (m_mapper) { + const QString resourcePath = modulePath.mid( + 1, modulePath.endsWith(u'/') ? modulePath.size() - 2 : -1) + + SlashQmldir; + const auto entry = m_mapper->entry( + QQmlJSResourceFileMapper::resourceFileFilter(resourcePath)); + qmldirPath = entry.filePath; + } else { + qWarning() << "Cannot read files from resource directory" << modulePath + << "because no resource file mapper was provided"; + } + } else { + qmldirPath = modulePath + SlashQmldir; + } + + const auto it = m_seenQmldirFiles.constFind(qmldirPath); + if (it != m_seenQmldirFiles.constEnd()) { + const QQmlJSImporter::Import import = *it; + m_seenImports.insert(importId, qmldirPath); + importDependencies(import, cacheTypes.get(), prefix, version, isDependency); + processImport(cacheKey, import, cacheTypes.get()); + + const bool typesFromCache = getTypesFromCache(); + Q_ASSERT(typesFromCache); + return typesFromCache; + } + + const QFileInfo file(qmldirPath); + if (file.exists()) { + const auto import = readQmldir(file.canonicalPath()); + setQualifiedNamesOn(import); + m_seenQmldirFiles.insert(qmldirPath, import); + m_seenImports.insert(importId, qmldirPath); + importDependencies(import, cacheTypes.get(), prefix, version, isDependency); + + // Potentially merges with the result of readDirectory() above. + processImport(cacheKey, import, cacheTypes.get()); + + const bool typesFromCache = getTypesFromCache(); + Q_ASSERT(typesFromCache); + return typesFromCache; + } + } + + if (isFile) { + // We've loaded the directory above + const bool typesFromCache = getTypesFromCache(); + Q_ASSERT(typesFromCache); + return typesFromCache; + } + + m_seenImports.insert(importId, QString()); + return false; +} + +QQmlJSScope::Ptr QQmlJSImporter::localFile2ScopeTree(const QString &filePath) +{ + const auto seen = m_importedFiles.find(filePath); + if (seen != m_importedFiles.end()) + return *seen; + + return *m_importedFiles.insert(filePath, { + QQmlJSScope::create(), + QSharedPointer<QDeferredFactory<QQmlJSScope>>( + new QDeferredFactory<QQmlJSScope>(this, filePath)) + }); +} + +QQmlJSScope::Ptr QQmlJSImporter::importFile(const QString &file) +{ + return localFile2ScopeTree(file); +} + +QQmlJSImporter::ImportedTypes QQmlJSImporter::importDirectory( + const QString &directory, const QString &prefix) +{ + const AvailableTypes builtins = builtinImportHelper(); + QQmlJSImporter::AvailableTypes types( + ImportedTypes( + ImportedTypes::INTERNAL, {}, builtins.cppNames.arrayType())); + importHelper(directory, &types, prefix, QTypeRevision(), false, true); + return types.qmlNames; +} + +void QQmlJSImporter::setImportPaths(const QStringList &importPaths) +{ + m_importPaths = importPaths; + + // We have to get rid off all cache elements directly referencing modules, since changing + // importPaths might change which module is found first + m_seenImports.clear(); + m_cachedImportTypes.clear(); + // Luckily this doesn't apply to m_seenQmldirFiles +} + +void QQmlJSImporter::clearCache() +{ + m_seenImports.clear(); + m_cachedImportTypes.clear(); + m_seenQmldirFiles.clear(); + m_importedFiles.clear(); +} + +QQmlJSScope::ConstPtr QQmlJSImporter::jsGlobalObject() const +{ + return m_builtins->cppNames.type(u"GlobalObject"_s).scope; +} + +void QQmlJSImporter::setQualifiedNamesOn(const Import &import) +{ + for (auto &object : import.objects) { + if (object.exports.isEmpty()) + continue; + if (auto *factory = object.scope.factory()) { + factory->setModuleName(import.name); + } else { + object.scope->setOwnModuleName(import.name); + } + } +} + +void QQmlJSImporter::runImportVisitor(QQmlJS::AST::Node *rootNode, + const ImportVisitorPrerequisites &p) +{ + m_importVisitor(rootNode, this, p); +} + +QT_END_NAMESPACE |