diff options
Diffstat (limited to 'src/qmlcompiler/qqmljsimporter.cpp')
-rw-r--r-- | src/qmlcompiler/qqmljsimporter.cpp | 902 |
1 files changed, 740 insertions, 162 deletions
diff --git a/src/qmlcompiler/qqmljsimporter.cpp b/src/qmlcompiler/qqmljsimporter.cpp index 45cb29a677..bcc8e98434 100644 --- a/src/qmlcompiler/qqmljsimporter.cpp +++ b/src/qmlcompiler/qqmljsimporter.cpp @@ -1,34 +1,11 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the tools applications of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// 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> @@ -37,8 +14,26 @@ QT_BEGIN_NAMESPACE -static const QLatin1String SlashQmldir = QLatin1String("/qmldir"); -static const QLatin1String SlashPluginsDotQmltypes = QLatin1String("/plugins.qmltypes"); +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) { @@ -46,17 +41,26 @@ static const QString prefixedName(const QString &prefix, const QString &name) return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name); } -static QQmlDirParser createQmldirParserForFile(const QString &filename) +QQmlDirParser QQmlJSImporter::createQmldirParserForFile(const QString &filename) { QFile f(filename); - f.open(QFile::ReadOnly); QQmlDirParser parser; - parser.parse(QString::fromUtf8(f.readAll())); + 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, QHash<QString, QQmlJSScope::Ptr> *objects, + const QString &filename, QList<QQmlJSExportedScope> *objects, QList<QQmlDirParser::Import> *dependencies) { const QFileInfo fileInfo(filename); @@ -79,7 +83,15 @@ void QQmlJSImporter::readQmltypes( } QFile file(filename); - file.open(QFile::ReadOnly); + 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); @@ -102,7 +114,7 @@ void QQmlJSImporter::readQmltypes( QQmlJS::SourceLocation() }); - for (const QString &dependency : qAsConst(dependencyStrings)) { + for (const QString &dependency : std::as_const(dependencyStrings)) { const auto blank = dependency.indexOf(u' '); if (blank < 0) { dependencies->append(QQmlDirParser::Import(dependency, {}, @@ -129,46 +141,109 @@ void QQmlJSImporter::readQmltypes( } } -QQmlJSImporter::Import QQmlJSImporter::readQmldir(const QString &path) +static QString internalName(const QQmlJSScope::ConstPtr &scope) { - Import result; - auto reader = createQmldirParserForFile(path + SlashQmldir); - result.imports.append(reader.imports()); - result.dependencies.append(reader.dependencies()); + if (const auto *factory = scope.factory()) + return factory->internalName(); + return scope->internalName(); +} - QHash<QString, QQmlJSScope::Ptr> qmlComponents; - const auto components = reader.components(); - for (auto it = components.begin(), end = components.end(); it != end; ++it) { - const QString filePath = path + QLatin1Char('/') + it->fileName; - if (!QFile::exists(filePath)) { - m_warnings.append({ - it->fileName + QStringLiteral(" is listed as component in ") - + path + SlashQmldir - + QStringLiteral(" but does not exist.\n"), - QtWarningMsg, - QQmlJS::SourceLocation() - }); - continue; - } +static bool isComposite(const QQmlJSScope::ConstPtr &scope) +{ + // The only thing the factory can do is load a composite type. + return scope.factory() || scope->isComposite(); +} - auto mo = qmlComponents.find(it.key()); - if (mo == qmlComponents.end()) - mo = qmlComponents.insert(it.key(), localFile2ScopeTree(filePath)); +static QStringList aliases(const QQmlJSScope::ConstPtr &scope) +{ + return isComposite(scope) + ? QStringList() + : scope->aliases(); +} - (*mo)->addExport(it.key(), reader.typeNamespace(), it->version); +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; } - for (auto it = qmlComponents.begin(), end = qmlComponents.end(); it != end; ++it) - result.objects.insert(it.key(), it.value()); + + 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() - ? path + u'/' + typeInfo : typeInfo; + ? resolvedPath + typeInfo + : typeInfo; readQmltypes(typeInfoPath, &result.objects, &result.dependencies); } if (typeInfos.isEmpty() && !reader.plugins().isEmpty()) { - const QString defaultTypeInfoPath = path + SlashPluginsDotQmltypes; + const QString defaultTypeInfoPath = resolvedPath + PluginsDotQmltypes; if (QFile::exists(defaultTypeInfoPath)) { m_warnings.append({ QStringLiteral("typeinfo not declared in qmldir file: ") @@ -180,67 +255,350 @@ QQmlJSImporter::Import QQmlJSImporter::readQmldir(const QString &path) } } + 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 = path + QLatin1Char('/') + script.fileName; - result.scripts.insert(script.nameSpace, localFile2ScopeTree(filePath)); + 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; } -void QQmlJSImporter::importDependencies( - const QQmlJSImporter::Import &import, - QQmlJSImporter::AvailableTypes *types, const QString &prefix, QTypeRevision version) +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. - const QString invalidPrefix = QString::fromLatin1("$dependency$"); - for (auto const &dependency : qAsConst(import.dependencies)) - importHelper(dependency.module, types, invalidPrefix, dependency.version); + 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); + } - for (auto const &import : qAsConst(import.imports)) { - importHelper(import.module, types, prefix, - (import.flags & QQmlDirParser::Import::Auto) ? version : import.version); + 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() }); } } -void QQmlJSImporter::processImport( - const QQmlJSImporter::Import &import, - QQmlJSImporter::AvailableTypes *types, - const QString &prefix) +static bool isVersionAllowed(const QQmlJSScope::Export &exportEntry, + const QQmlJS::Import &importDescription) { - const QString anonPrefix = QStringLiteral("$anonymous$"); + 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(); +} - if (!prefix.isEmpty()) - types->qmlNames.insert(prefix, {}); // Empty type means "this is the prefix" +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 }); + }; - for (auto it = import.scripts.begin(); it != import.scripts.end(); ++it) - types->qmlNames.insert(prefixedName(prefix, it.key()), it.value()); + 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; + } - // add objects - for (auto it = import.objects.begin(); it != import.objects.end(); ++it) { - const auto &val = it.value(); - types->cppNames.insert(val->internalName(), val); + 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; + } + } - const auto exports = val->exports(); - if (exports.isEmpty()) { - types->qmlNames.insert( - prefixedName(prefix, prefixedName(anonPrefix, val->internalName())), val); + types->qmlNames.setType(qmlName, { val.scope, valExport.version() }); + seenExports[qmlName].append(valExport); } - for (const auto &valExport : exports) - types->qmlNames.insert(prefixedName(prefix, valExport.type()), val); + 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) { - const auto &val = it.value(); - if (val->baseType().isNull()) // Otherwise we have already done it in localFile2ScopeTree() - QQmlJSScope::resolveTypes(val, types->cppNames); + 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 found in any of the import paths. + * Imports builtins.qmltypes and jsroot.qmltypes found in any of the import paths. */ QQmlJSImporter::ImportedTypes QQmlJSImporter::importBuiltins() { @@ -250,44 +608,142 @@ QQmlJSImporter::ImportedTypes QQmlJSImporter::importBuiltins() QQmlJSImporter::AvailableTypes QQmlJSImporter::builtinImportHelper() { - if (!m_builtins.qmlNames.isEmpty() || !m_builtins.cppNames.isEmpty()) - return m_builtins; + if (m_builtins) + return *m_builtins; - for (auto const &dir : m_importPaths) { - Import result; - QDirIterator it { dir, QStringList() << QLatin1String("builtins.qmltypes"), QDir::NoFilter, - QDirIterator::Subdirectories }; - while (it.hasNext()) - readQmltypes(it.next(), &result.objects, &result.dependencies); - importDependencies(result, &m_builtins); - processImport(result, &m_builtins); + AvailableTypes builtins(ImportedTypes(ImportedTypes::INTERNAL, {}, {})); + + Import result; + result.name = QStringLiteral("QML"); + + QStringList qmltypesFiles = { QStringLiteral("builtins.qmltypes"), + QStringLiteral("jsroot.qmltypes") }; + const auto importBuiltins = [&](const QStringList &imports) { + for (auto const &dir : imports) { + QDirIterator it { dir, qmltypesFiles, QDir::NoFilter }; + while (it.hasNext() && !qmltypesFiles.isEmpty()) { + readQmltypes(it.next(), &result.objects, &result.dependencies); + qmltypesFiles.removeOne(it.fileName()); + } + setQualifiedNamesOn(result); + importDependencies(result, &builtins); + + if (qmltypesFiles.isEmpty()) + return; + } + }; + + importBuiltins(m_importPaths); + 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() }); + importBuiltins({ u":/qt-project.org/qml/builtins"_s }); // use qrc as a "last resort" + } + Q_ASSERT(qmltypesFiles.isEmpty()); // 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; + } } - return m_builtins; + 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. */ -QQmlJSImporter::ImportedTypes QQmlJSImporter::importQmltypes(const QStringList &qmltypesFiles) +void QQmlJSImporter::importQmldirs(const QStringList &qmldirFiles) { - AvailableTypes types(builtinImportHelper().cppNames); - Import result; - - for (const auto &qmltypeFile : qmltypesFiles) - readQmltypes(qmltypeFile, &result.objects, &result.dependencies); + 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"); + } - importDependencies(result, &types); - processImport(result, &types); + m_seenQmldirFiles.insert(qmldirName, result); - return types.qmlNames; + 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) +QQmlJSImporter::ImportedTypes QQmlJSImporter::importModule(const QString &module, + const QString &prefix, + QTypeRevision version, + QStringList *staticModuleList) { - AvailableTypes result(builtinImportHelper().cppNames); - importHelper(module, &result, prefix, version); + 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; } @@ -296,31 +752,136 @@ QQmlJSImporter::ImportedTypes QQmlJSImporter::builtinInternalNames() return builtinImportHelper().cppNames; } -void QQmlJSImporter::importHelper(const QString &module, AvailableTypes *types, - const QString &prefix, QTypeRevision version) +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.find(importId); - if (it != m_seenImports.end()) { - importDependencies(*it, types, prefix, version); - processImport(*it, types, prefix); - return; + 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; } - const auto qmltypesPaths = qQmlResolveImportPaths(module, m_importPaths, version); - for (auto const &qmltypesPath : qmltypesPaths) { - const QFileInfo file(qmltypesPath + SlashQmldir); + 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()); - m_seenImports.insert(importId, import); - importDependencies(import, types, prefix, version); - processImport(import, types, prefix); - return; + 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; } } - m_seenImports.insert(importId, {}); + 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) @@ -344,38 +905,55 @@ QQmlJSScope::Ptr QQmlJSImporter::importFile(const QString &file) QQmlJSImporter::ImportedTypes QQmlJSImporter::importDirectory( const QString &directory, const QString &prefix) { - QHash<QString, QQmlJSScope::ConstPtr> qmlNames; + const AvailableTypes builtins = builtinImportHelper(); + QQmlJSImporter::AvailableTypes types( + ImportedTypes( + ImportedTypes::INTERNAL, {}, builtins.cppNames.arrayType())); + importHelper(directory, &types, prefix, QTypeRevision(), false, true); + return types.qmlNames; +} - 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()) { - qmlNames.insert(prefixedName(prefix, name), - localFile2ScopeTree(entry.filePath)); - } - } - } - return qmlNames; - } +void QQmlJSImporter::setImportPaths(const QStringList &importPaths) +{ + m_importPaths = importPaths; - QDirIterator it { - directory, - QStringList() << QLatin1String("*.qml"), - QDir::NoFilter - }; - while (it.hasNext()) { - it.next(); - if (!it.fileName().front().isUpper()) - continue; // Non-uppercase names cannot be imported anyway. + // 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; +} - qmlNames.insert(prefixedName(prefix, QFileInfo(it.filePath()).baseName()), - localFile2ScopeTree(it.filePath())); +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); + } } +} - return qmlNames; +void QQmlJSImporter::runImportVisitor(QQmlJS::AST::Node *rootNode, + const ImportVisitorPrerequisites &p) +{ + m_importVisitor(rootNode, this, p); } QT_END_NAMESPACE |