// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "qmljslink.h" #include "parser/qmljsast_p.h" #include "qmljsbind.h" #include "qmljsconstants.h" #include "qmljsdocument.h" #include "qmljsmodelmanagerinterface.h" #include "qmljstr.h" #include "qmljsutils.h" #include #include #include #include #include using namespace LanguageUtils; using namespace QmlJS::AST; namespace QmlJS { namespace { class ImportCacheKey { public: explicit ImportCacheKey(const ImportInfo &info) : m_type(info.type()) , m_path(info.path()) , m_majorVersion(info.version().majorVersion()) , m_minorVersion(info.version().minorVersion()) {} private: friend size_t qHash(const ImportCacheKey &); friend bool operator==(const ImportCacheKey &, const ImportCacheKey &); int m_type; QString m_path; int m_majorVersion; int m_minorVersion; }; size_t qHash(const ImportCacheKey &info) { return ::qHash(info.m_type) ^ ::qHash(info.m_path) ^ ::qHash(info.m_majorVersion) ^ ::qHash(info.m_minorVersion); } bool operator==(const ImportCacheKey &i1, const ImportCacheKey &i2) { return i1.m_type == i2.m_type && i1.m_path == i2.m_path && i1.m_majorVersion == i2.m_majorVersion && i1.m_minorVersion == i2.m_minorVersion; } } class LinkPrivate { public: Context::ImportsPerDocument linkImports(); void populateImportedTypes(Imports *imports, const Document::Ptr &doc); Import importFileOrDirectory( const Document::Ptr &doc, const ImportInfo &importInfo); Import importNonFile( const Document::Ptr &doc, const ImportInfo &importInfo); bool importLibrary(const Document::Ptr &doc, const Utils::FilePath &libraryPath, Import *import, ObjectValue *targetObject, const Utils::FilePath &importPath = Utils::FilePath(), bool optional = false); void loadQmldirComponents(ObjectValue *import, LanguageUtils::ComponentVersion version, const LibraryInfo &libraryInfo, const Utils::FilePath &libraryPath); void loadImplicitDirectoryImports(Imports *imports, const Document::Ptr &doc); void loadImplicitDefaultImports(Imports *imports); void loadImplicitBuiltinsImports(Imports *imports); void error(const Document::Ptr &doc, const SourceLocation &loc, const QString &message); void warning(const Document::Ptr &doc, const SourceLocation &loc, const QString &message); void appendDiagnostic(const Document::Ptr &doc, const DiagnosticMessage &message); private: friend class Link; void loadImplicitImports(Imports *imports, const QString &packageName, const QString &moduleName); Snapshot m_snapshot; ValueOwner *m_valueOwner = nullptr; QList m_importPaths; QList m_applicationDirectories; LibraryInfo m_builtins; ViewerContext m_vContext; QHash importCache; QHash> importableModuleApis; Document::Ptr document; QList *diagnosticMessages = nullptr; QHash> *allDiagnosticMessages = nullptr; }; /*! \class QmlJS::Link \brief The Link class creates a Context for a Snapshot. \sa Context Snapshot Initializes a context by resolving imports. This is an expensive operation. Instead of making a fresh context, consider reusing the one maintained in the \l{QmlJSEditor::SemanticInfo} of a \l{QmlJSEditor::QmlJSEditorDocument}. */ Link::Link(const Snapshot &snapshot, const ViewerContext &vContext, const LibraryInfo &builtins) : d(new LinkPrivate) { d->m_valueOwner = new ValueOwner; d->m_snapshot = snapshot; const QList list(vContext.paths.begin(), vContext.paths.end()); d->m_importPaths = list; d->m_applicationDirectories = vContext.applicationDirectories; d->m_builtins = builtins; d->m_vContext = vContext; d->diagnosticMessages = nullptr; d->allDiagnosticMessages = nullptr; QFutureInterface fi; fi.reportStarted(); ModelManagerInterface *modelManager = ModelManagerInterface::instanceForFuture(fi.future()); if (modelManager) { const ModelManagerInterface::CppDataHash cppDataHash = modelManager->cppData(); { // populate engine with types from C++ for (auto it = cppDataHash.cbegin(), end = cppDataHash.cend(); it != end; ++it) d->m_valueOwner->cppQmlTypes().load(it.key(), it.value().exportedTypes); } // build an object with the context properties from C++ ObjectValue *cppContextProperties = d->m_valueOwner->newObject(/* prototype = */ nullptr); for (const ModelManagerInterface::CppData &cppData : cppDataHash) { for (auto it = cppData.contextProperties.cbegin(), end = cppData.contextProperties.cend(); it != end; ++it) { const Value *value = nullptr; const QString &cppTypeName = it.value(); if (!cppTypeName.isEmpty()) value = d->m_valueOwner->cppQmlTypes().objectByCppName(cppTypeName); if (!value) value = d->m_valueOwner->unknownValue(); cppContextProperties->setMember(it.key(), value); } } d->m_valueOwner->cppQmlTypes().setCppContextProperties(cppContextProperties); } fi.reportFinished(); } ContextPtr Link::operator()(QHash> *messages) { d->allDiagnosticMessages = messages; return Context::create(d->m_snapshot, d->m_valueOwner, d->linkImports(), d->m_vContext); } ContextPtr Link::operator()(const Document::Ptr &doc, QList *messages) { d->document = doc; d->diagnosticMessages = messages; return Context::create(d->m_snapshot, d->m_valueOwner, d->linkImports(), d->m_vContext); } Link::~Link() { delete d; } Context::ImportsPerDocument LinkPrivate::linkImports() { Context::ImportsPerDocument importsPerDocument; // load builtin objects if (m_builtins.pluginTypeInfoStatus() == LibraryInfo::DumpDone || m_builtins.pluginTypeInfoStatus() == LibraryInfo::TypeInfoFileDone) { m_valueOwner->cppQmlTypes().load(QLatin1String(""), m_builtins.metaObjects()); } else { m_valueOwner->cppQmlTypes().load(QLatin1String(""), CppQmlTypesLoader::defaultQtObjects()); } // load library objects shipped with Creator m_valueOwner->cppQmlTypes().load(QLatin1String(""), CppQmlTypesLoader::defaultLibraryObjects()); if (document) { // do it on document first, to make sure import errors are shown auto *imports = new Imports(m_valueOwner); // Add custom imports for the opened document for (const auto &provider : CustomImportsProvider::allProviders()) { const auto providerImports = provider->imports(m_valueOwner, document.data(), &m_snapshot); for (const auto &import : providerImports) importCache.insert(ImportCacheKey(import.info), import); provider->loadBuiltins(&importsPerDocument, imports, document.data(), m_valueOwner, &m_snapshot); m_importPaths = provider->prioritizeImportPaths(document.data(), m_importPaths); } populateImportedTypes(imports, document); importsPerDocument.insert(document.data(), QSharedPointer(imports)); } for (const Document::Ptr &doc : std::as_const(m_snapshot)) { if (doc == document) continue; auto *imports = new Imports(m_valueOwner); populateImportedTypes(imports, doc); importsPerDocument.insert(doc.data(), QSharedPointer(imports)); } return importsPerDocument; } /** * A workaround for prototype issues with QEasingCurve in Qt 5.15. * * In this Qt version, QEasingCurve is declared in builtins.qmltypes, but its * prototype, QQmlEasingValueType, is only contained in the QtQml module. * * This code attempts to resolve QEasingCurve's prototype if it hasn't been set * already. It's intended to be called after all CppQmlTypes have been loaded. */ static void workaroundQEasingCurve(CppQmlTypes &cppTypes) { const CppComponentValue *easingCurve = cppTypes.objectByCppName("QEasingCurve"); if (!easingCurve || easingCurve->prototype()) return; const QString superclassName = easingCurve->metaObject()->superclassName(); if (superclassName.isEmpty()) return; const CppComponentValue *prototype = cppTypes.objectByCppName(superclassName); if (!prototype) return; const_cast(easingCurve)->setPrototype(prototype); } void LinkPrivate::populateImportedTypes(Imports *imports, const Document::Ptr &doc) { importableModuleApis.clear(); // implicit imports: the package is always available (except when the QML package was loaded). // In the latter case, still try to load the 's module . loadImplicitDefaultImports(imports); // Load the import, if the QML package was loaded. loadImplicitBuiltinsImports(imports); // implicit imports: // qml files in the same directory are available without explicit imports if (doc->isQmlDocument()) loadImplicitDirectoryImports(imports, doc); // explicit imports, whether directories, files or libraries const auto docImports = doc->bind()->imports(); for (const ImportInfo &info : docImports) { Import import = importCache.value(ImportCacheKey(info)); // ensure usage of the right ImportInfo, the cached import // can have a different 'as' clause... import.info = info; if (!import.object) { switch (info.type()) { case ImportType::File: case ImportType::Directory: case ImportType::QrcFile: case ImportType::QrcDirectory: import = importFileOrDirectory(doc, info); break; case ImportType::Library: import = importNonFile(doc, info); break; case ImportType::UnknownFile: imports->setImportFailed(); if (info.ast()) { error(doc, info.ast()->fileNameToken, Tr::tr("File or directory not found.")); } break; default: break; } if (import.object) importCache.insert(ImportCacheKey(info), import); } if (import.object) imports->append(import); } workaroundQEasingCurve(m_valueOwner->cppQmlTypes()); } /* import "content" import "content" as Xxx import "content" 4.6 import "content" 4.6 as Xxx import "http://www.ovi.com/" as Ovi import "file.js" as Foo */ Import LinkPrivate::importFileOrDirectory(const Document::Ptr &doc, const ImportInfo &importInfo) { Import import; import.info = importInfo; import.object = nullptr; import.valid = true; QString pathStr = importInfo.path(); Utils::FilePath path = Utils::FilePath::fromString(pathStr); if (importInfo.type() == ImportType::Directory || importInfo.type() == ImportType::ImplicitDirectory) { import.object = new ObjectValue(m_valueOwner); importLibrary(doc, path, &import, import.object); const QList documentsInDirectory = m_snapshot.documentsInDirectory(path); for (const Document::Ptr &importedDoc : documentsInDirectory) { if (importedDoc->bind()->rootObjectValue()) { const QString targetName = importedDoc->componentName(); import.object->setMember(targetName, importedDoc->bind()->rootObjectValue()); } } } else if (importInfo.type() == ImportType::File) { if (Document::Ptr importedDoc = m_snapshot.document(path)) import.object = importedDoc->bind()->rootObjectValue(); } else if (importInfo.type() == ImportType::QrcFile) { QLocale locale; QStringList filePaths = ModelManagerInterface::instance() ->filesAtQrcPath(pathStr, &locale, nullptr, ModelManagerInterface::ActiveQrcResources); if (filePaths.isEmpty()) filePaths = ModelManagerInterface::instance()->filesAtQrcPath(pathStr); if (!filePaths.isEmpty()) { if (Document::Ptr importedDoc = m_snapshot.document( Utils::FilePath::fromString(filePaths.at(0)))) import.object = importedDoc->bind()->rootObjectValue(); } } else if (importInfo.type() == ImportType::QrcDirectory){ import.object = new ObjectValue(m_valueOwner); importLibrary(doc, path, &import, import.object); const QMap paths = ModelManagerInterface::instance()->filesInQrcPath( pathStr); for (auto iter = paths.cbegin(), end = paths.cend(); iter != end; ++iter) { if (ModelManagerInterface::guessLanguageOfFile(Utils::FilePath::fromString(iter.key())) .isQmlLikeLanguage()) { Document::Ptr importedDoc = m_snapshot.document( Utils::FilePath::fromString(iter.value().at(0))); if (importedDoc && importedDoc->bind()->rootObjectValue()) { const QString targetName = QFileInfo(iter.key()).baseName(); import.object->setMember(targetName, importedDoc->bind()->rootObjectValue()); } } } } return import; } static ModuleApiInfo findBestModuleApi(const QList &apis, const ComponentVersion &version) { ModuleApiInfo best; for (const ModuleApiInfo &moduleApi : apis) { if (moduleApi.version <= version && (!best.version.isValid() || best.version < moduleApi.version)) { best = moduleApi; } } return best; } /* import Qt 4.6 import Qt 4.6 as Xxx */ Import LinkPrivate::importNonFile(const Document::Ptr &doc, const ImportInfo &importInfo) { Import import; import.info = importInfo; import.object = new ObjectValue(m_valueOwner); import.valid = true; const QString packageName = importInfo.name(); const ComponentVersion version = importInfo.version(); QList libraryPaths = modulePaths(packageName, version.toString(), m_importPaths + m_applicationDirectories); bool importFound = false; for (const Utils::FilePath &libPath : libraryPaths) { importFound = !libPath.isEmpty() && importLibrary(doc, libPath, &import, import.object); if (importFound) break; } if (!importFound) { for (const Utils::FilePath &dir : std::as_const(m_applicationDirectories)) { auto qmltypes = dir.dirEntries( Utils::FileFilter(QStringList{"*.qmltypes"}, QDir::Files)); // This adds the types to the C++ types, to be found below if applicable. if (!qmltypes.isEmpty()) importLibrary(doc, dir, &import, import.object); } } // if there are cpp-based types for this package, use them too if (m_valueOwner->cppQmlTypes().hasModule(packageName)) { importFound = true; const auto objects = m_valueOwner->cppQmlTypes().createObjectsForImport(packageName, version); for (const CppComponentValue *object : objects) import.object->setMember(object->className(), object); } // check module apis that previous imports may have enabled ModuleApiInfo moduleApi = findBestModuleApi(importableModuleApis.value(packageName), version); if (moduleApi.version.isValid()) { importFound = true; import.object->setPrototype(m_valueOwner->cppQmlTypes().objectByCppName(moduleApi.cppName)); } // TODO: at the moment there is not any types information on Qbs imports. if (doc->language() == Dialect::QmlQbs) importFound = true; if (!importFound && importInfo.ast()) { import.valid = false; error(doc, locationFromRange(importInfo.ast()->firstSourceLocation(), importInfo.ast()->lastSourceLocation()), Tr::tr( "QML module not found (%1).\n\n" "Import paths:\n" "%2\n\n" "For qmake projects, use the QML_IMPORT_PATH variable to add import paths.\n" "For Qbs projects, declare and set a qmlImportPaths property in your product " "to add import paths.\n" "For qmlproject projects, use the importPaths property to add import paths.\n" "For CMake projects, make sure QML_IMPORT_PATH variable is in CMakeCache.txt.\n" "For qmlRegister... calls, make sure that you define the Module URI as a string literal.\n") .arg(importInfo.name(), Utils::transform(m_importPaths, [](const Utils::FilePath &p) { return p.toString(); }).join("\n"))); } return import; } bool LinkPrivate::importLibrary(const Document::Ptr &doc, const Utils::FilePath &libraryPath, Import *import, ObjectValue *targetObject, const Utils::FilePath &importPath, bool optional) { const ImportInfo &importInfo = import->info; LibraryInfo libraryInfo = m_snapshot.libraryInfo(libraryPath); if (!libraryInfo.isValid()) return false; import->libraryPath = libraryPath; const ComponentVersion version = importInfo.version(); SourceLocation errorLoc; if (const UiImport *ast = importInfo.ast()) errorLoc = locationFromRange(ast->firstSourceLocation(), ast->lastSourceLocation()); // Load imports that are mentioned by the "import" command in a qmldir // file into the same targetObject, using the same version and "as". // // Note: Since this works on the same targetObject, the ModuleApi setPrototype() // logic will not work. But ModuleApi isn't used in Qt versions that use import // commands in qmldir files, and is pending removal in Qt 6. for (const auto &toImport : libraryInfo.imports()) { QString importName = toImport.module; ComponentVersion vNow = toImport.version; /* There was a period in which no version == auto * Required for QtQuick imports less than 2.15 */ if ((toImport.flags & QmlDirParser::Import::Auto) || !vNow.isValid()) vNow = version; Import subImport; subImport.valid = true; subImport.info = ImportInfo::moduleImport(importName, vNow, importInfo.as(), importInfo.ast()); const QList libraryPaths = modulePaths(importName, vNow.toString(), m_importPaths); subImport.libraryPath = libraryPaths.value(0); // first is the best match bool subImportFound = importLibrary(doc, subImport.libraryPath, &subImport, targetObject, importPath, true); if (!subImportFound && errorLoc.isValid()) { if (!(optional || (toImport.flags & QmlDirParser::Import::Optional))) { import->valid = false; error(doc, errorLoc, Tr::tr("Implicit import '%1' of QML module '%2' not found.\n\n" "Import paths:\n" "%3\n\n" "For qmake projects, use the QML_IMPORT_PATH variable to add import " "paths.\n" "For Qbs projects, declare and set a qmlImportPaths property in " "your product " "to add import paths.\n" "For qmlproject projects, use the importPaths property to add " "import paths.\n" "For CMake projects, make sure QML_IMPORT_PATH variable is in " "CMakeCache.txt.\n") .arg(importName, importInfo.name(), Utils::transform(m_importPaths, [](const Utils::FilePath &p) { return p.toString(); }).join(QLatin1Char('\n')))); } } else if (!subImport.valid) { import->valid = false; } } // Load types from qmltypes or plugins if (!libraryInfo.plugins().isEmpty() || !libraryInfo.typeInfos().isEmpty()) { if (libraryInfo.pluginTypeInfoStatus() == LibraryInfo::NoTypeInfo) { ModelManagerInterface *modelManager = ModelManagerInterface::instance(); if (modelManager) { if (importInfo.type() == ImportType::Library) { if (version.isValid()) { const QString uri = importInfo.name(); modelManager->loadPluginTypes( libraryPath, importPath, uri, version.toString()); } } else { modelManager->loadPluginTypes( libraryPath, libraryPath, QString(), version.toString()); } } if (!optional && errorLoc.isValid()) { appendDiagnostic(doc, DiagnosticMessage( Severity::ReadingTypeInfoWarning, errorLoc, Tr::tr("QML module contains C++ plugins, " "currently reading type information... %1") .arg(import->info.name()))); import->valid = false; } } else if (libraryInfo.pluginTypeInfoStatus() == LibraryInfo::DumpError || libraryInfo.pluginTypeInfoStatus() == LibraryInfo::TypeInfoFileError) { // Only underline import if package isn't described in .qmltypes anyway // and is not a private package QString packageName = importInfo.name(); if (!optional && errorLoc.isValid() && (packageName.isEmpty() || !m_valueOwner->cppQmlTypes().hasModule(packageName)) && !packageName.endsWith(QLatin1String("private"), Qt::CaseInsensitive)) { error(doc, errorLoc, libraryInfo.pluginTypeInfoError()); import->valid = false; } } else { const QString packageName = importInfo.name(); m_valueOwner->cppQmlTypes().load(libraryPath.toString(), libraryInfo.metaObjects(), packageName); const auto objects = m_valueOwner->cppQmlTypes().createObjectsForImport(packageName, version); for (const CppComponentValue *object : objects) targetObject->setMember(object->className(), object); // all but no-uri module apis become available for import QList noUriModuleApis; const auto moduleApis = libraryInfo.moduleApis(); for (const ModuleApiInfo &moduleApi : moduleApis) { if (moduleApi.uri.isEmpty()) noUriModuleApis += moduleApi; else importableModuleApis[moduleApi.uri] += moduleApi; } // if a module api has no uri, it shares the same name ModuleApiInfo sameUriModuleApi = findBestModuleApi(noUriModuleApis, version); if (sameUriModuleApi.version.isValid()) { targetObject->setPrototype(m_valueOwner->cppQmlTypes() .objectByCppName(sameUriModuleApi.cppName)); } } } // Load types that are mentioned explicitly in the qmldir loadQmldirComponents(targetObject, version, libraryInfo, libraryPath); return true; } void LinkPrivate::error(const Document::Ptr &doc, const SourceLocation &loc, const QString &message) { appendDiagnostic(doc, DiagnosticMessage(Severity::Error, loc, message)); } void LinkPrivate::warning(const Document::Ptr &doc, const SourceLocation &loc, const QString &message) { appendDiagnostic(doc, DiagnosticMessage(Severity::Warning, loc, message)); } void LinkPrivate::appendDiagnostic(const Document::Ptr &doc, const DiagnosticMessage &message) { if (diagnosticMessages && doc->fileName() == document->fileName()) diagnosticMessages->append(message); if (allDiagnosticMessages) (*allDiagnosticMessages)[doc->fileName()].append(message); } void LinkPrivate::loadQmldirComponents(ObjectValue *import, ComponentVersion version, const LibraryInfo &libraryInfo, const Utils::FilePath &libraryPath) { // if the version isn't valid, import the latest if (!version.isValid()) version = ComponentVersion(ComponentVersion::MaxVersion, ComponentVersion::MaxVersion); QSet importedTypes; const auto components = libraryInfo.components(); for (const QmlDirParser::Component &component : components) { ComponentVersion componentVersion(component.majorVersion, component.minorVersion); if (version < componentVersion) continue; if (!Utils::insert(importedTypes, component.typeName)) continue; if (Document::Ptr importedDoc = m_snapshot.document( libraryPath.pathAppended(component.fileName))) { if (ObjectValue *v = importedDoc->bind()->rootObjectValue()) import->setMember(component.typeName, v); } } } void LinkPrivate::loadImplicitDirectoryImports(Imports *imports, const Document::Ptr &doc) { auto processImport = [this, imports, doc](const ImportInfo &importInfo){ Import directoryImport = importCache.value(ImportCacheKey(importInfo)); if (!directoryImport.object) { directoryImport = importFileOrDirectory(doc, importInfo); if (directoryImport.object) importCache.insert(ImportCacheKey(importInfo), directoryImport); } if (directoryImport.object) imports->append(directoryImport); }; processImport(ImportInfo::implicitDirectoryImport(doc->path().path())); const auto qrcPaths = ModelManagerInterface::instance()->qrcPathsForFile(doc->fileName()); for (const QString &path : qrcPaths) { processImport(ImportInfo::qrcDirectoryImport( Utils::QrcParser::qrcDirectoryPathForQrcFilePath(path))); } } void LinkPrivate::loadImplicitImports(Imports *imports, const QString &packageName, const QString &moduleName) { if (m_valueOwner->cppQmlTypes().hasModule(packageName)) { const ComponentVersion maxVersion(ComponentVersion::MaxVersion, ComponentVersion::MaxVersion); const ImportInfo info = ImportInfo::moduleImport(packageName, maxVersion, QString()); Import import = importCache.value(ImportCacheKey(info)); if (!import.object) { import.valid = true; import.info = info; import.object = new ObjectValue(m_valueOwner, moduleName); const auto objects = m_valueOwner->cppQmlTypes().createObjectsForImport(packageName, maxVersion); for (const CppComponentValue *object : objects) import.object->setMember(object->className(), object); importCache.insert(ImportCacheKey(info), import); } imports->append(import); } } void LinkPrivate::loadImplicitDefaultImports(Imports *imports) { loadImplicitImports(imports, CppQmlTypes::defaultPackage, QLatin1String("")); } void LinkPrivate::loadImplicitBuiltinsImports(Imports *imports) { loadImplicitImports(imports, QLatin1String("QML"), QLatin1String("")); } } // namespace QmlJS