qtdeclarative/src/qmlcompiler/qqmljsimporter.cpp

420 lines
16 KiB
C++

/****************************************************************************
**
** 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$
**
****************************************************************************/
#include "qqmljsimporter_p.h"
#include "qqmljstypedescriptionreader_p.h"
#include "qqmljstypereader_p.h"
#include <QtQml/private/qqmlimportresolver_p.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qdiriterator.h>
QT_BEGIN_NAMESPACE
static const QLatin1String SlashQmldir = QLatin1String("/qmldir");
static const QLatin1String SlashPluginsDotQmltypes = QLatin1String("/plugins.qmltypes");
static const QString prefixedName(const QString &prefix, const QString &name)
{
Q_ASSERT(!prefix.endsWith(u'.'));
return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name);
}
static QQmlDirParser createQmldirParserForFile(const QString &filename)
{
QFile f(filename);
f.open(QFile::ReadOnly);
QQmlDirParser parser;
parser.parse(QString::fromUtf8(f.readAll()));
return parser;
}
void QQmlJSImporter::readQmltypes(
const QString &filename, QHash<QString, QQmlJSScope::Ptr> *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);
file.open(QFile::ReadOnly);
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 : qAsConst(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));
}
}
QQmlJSImporter::Import QQmlJSImporter::readQmldir(const QString &path)
{
Import result;
auto reader = createQmldirParserForFile(path + SlashQmldir);
result.imports.append(reader.imports());
result.dependencies.append(reader.dependencies());
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;
}
auto mo = qmlComponents.find(it.key());
if (mo == qmlComponents.end()) {
QQmlJSScope::Ptr imported = localFile2ScopeTree(filePath);
if (it->singleton)
imported->setIsSingleton(true);
mo = qmlComponents.insert(it.key(), imported);
}
(*mo)->addExport(it.key(), reader.typeNamespace(), it->version);
}
for (auto it = qmlComponents.begin(), end = qmlComponents.end(); it != end; ++it)
result.objects.insert(it.key(), it.value());
const auto typeInfos = reader.typeInfos();
for (const auto &typeInfo : typeInfos) {
const QString typeInfoPath = QFileInfo(typeInfo).isRelative()
? path + u'/' + typeInfo : typeInfo;
readQmltypes(typeInfoPath, &result.objects, &result.dependencies);
}
if (typeInfos.isEmpty() && !reader.plugins().isEmpty()) {
const QString defaultTypeInfoPath = path + SlashPluginsDotQmltypes;
if (QFile::exists(defaultTypeInfoPath)) {
m_warnings.append({
QStringLiteral("typeinfo not declared in qmldir file: ")
+ defaultTypeInfoPath,
QtWarningMsg,
QQmlJS::SourceLocation()
});
readQmltypes(defaultTypeInfoPath, &result.objects, &result.dependencies);
}
}
const auto scripts = reader.scripts();
for (const auto &script : scripts) {
const QString filePath = path + QLatin1Char('/') + script.fileName;
result.scripts.insert(script.nameSpace, localFile2ScopeTree(filePath));
}
return result;
}
void QQmlJSImporter::importDependencies(
const QQmlJSImporter::Import &import,
QQmlJSImporter::AvailableTypes *types, const QString &prefix, QTypeRevision version)
{
// 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 &import : qAsConst(import.imports)) {
importHelper(import.module, types, prefix,
(import.flags & QQmlDirParser::Import::Auto) ? version : import.version);
}
}
void QQmlJSImporter::processImport(
const QQmlJSImporter::Import &import,
QQmlJSImporter::AvailableTypes *types,
const QString &prefix)
{
const QString anonPrefix = QStringLiteral("$anonymous$");
if (!prefix.isEmpty())
types->qmlNames.insert(prefix, {}); // Empty type means "this is the prefix"
for (auto it = import.scripts.begin(); it != import.scripts.end(); ++it)
types->qmlNames.insert(prefixedName(prefix, it.key()), it.value());
// add objects
for (auto it = import.objects.begin(); it != import.objects.end(); ++it) {
const auto &val = it.value();
types->cppNames.insert(val->internalName(), val);
const auto exports = val->exports();
if (exports.isEmpty()) {
types->qmlNames.insert(
prefixedName(prefix, prefixedName(anonPrefix, val->internalName())), val);
}
for (const auto &valExport : exports)
types->qmlNames.insert(prefixedName(prefix, valExport.type()), val);
}
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);
}
}
/*!
* Imports builtins.qmltypes found in any of the import paths.
*/
QQmlJSImporter::ImportedTypes QQmlJSImporter::importBuiltins()
{
return builtinImportHelper().qmlNames;
}
QQmlJSImporter::AvailableTypes QQmlJSImporter::builtinImportHelper()
{
if (!m_builtins.qmlNames.isEmpty() || !m_builtins.cppNames.isEmpty())
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);
}
return m_builtins;
}
/*!
* Imports types from the specified \a qmltypesFiles.
*/
void QQmlJSImporter::importQmltypes(const QStringList &qmltypesFiles)
{
AvailableTypes types(builtinImportHelper().cppNames);
for (const auto &qmltypeFile : qmltypesFiles) {
Import result;
readQmltypes(qmltypeFile, &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.
const QString qmldirName = qmltypeFile + QStringLiteral("_FAKE_QMLDIR");
m_seenQmldirFiles.insert(qmldirName, result);
for (const auto &object : result.objects.values()) {
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, QQmlJS::SourceLocation location)
{
AvailableTypes result(builtinImportHelper().cppNames);
if (!importHelper(module, &result, prefix, version)) {
m_warnings.append({
QStringLiteral("Failed to import %1. Are your include paths set up properly?").arg(module),
QtWarningMsg,
location
});
}
return result.qmlNames;
}
QQmlJSImporter::ImportedTypes QQmlJSImporter::builtinInternalNames()
{
return builtinImportHelper().cppNames;
}
bool QQmlJSImporter::importHelper(const QString &module, AvailableTypes *types,
const QString &prefix, QTypeRevision version)
{
const QPair<QString, QTypeRevision> importId { module, version };
const auto it = m_seenImports.constFind(importId);
if (it != m_seenImports.constEnd()) {
if (it->isEmpty())
return false;
const auto import = m_seenQmldirFiles.constFind(*it);
Q_ASSERT(import != m_seenQmldirFiles.constEnd());
importDependencies(*import, types, prefix, version);
processImport(*import, types, prefix);
return true;
}
const auto modulePaths = qQmlResolveImportPaths(module, m_importPaths, version);
for (auto const &modulePath : modulePaths) {
const QString qmldirPath = modulePath + SlashQmldir;
const auto it = m_seenQmldirFiles.constFind(qmldirPath);
if (it != m_seenQmldirFiles.constEnd()) {
m_seenImports.insert(importId, qmldirPath);
importDependencies(*it, types, prefix, version);
processImport(*it, types, prefix);
return true;
}
const QFileInfo file(qmldirPath);
if (file.exists()) {
const auto import = readQmldir(file.canonicalPath());
m_seenQmldirFiles.insert(qmldirPath, import);
m_seenImports.insert(importId, qmldirPath);
importDependencies(import, types, prefix, version);
processImport(import, types, prefix);
return true;
}
}
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)
{
QHash<QString, QQmlJSScope::ConstPtr> 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;
}
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.
qmlNames.insert(prefixedName(prefix, QFileInfo(it.filePath()).baseName()),
localFile2ScopeTree(it.filePath()));
}
return qmlNames;
}
QT_END_NAMESPACE