/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** 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. ** ****************************************************************************/ #include "qmljslink.h" #include "parser/qmljsast_p.h" #include "qmljsdocument.h" #include "qmljsbind.h" #include "qmljsutils.h" #include "qmljsmodelmanagerinterface.h" #include "qmljsconstants.h" #include #include using namespace LanguageUtils; using namespace QmlJS::AST; namespace QmlJS { namespace { class ImportCacheKey { public: explicit ImportCacheKey(const ImportInfo &info) : type(info.type()) , path(info.path()) , majorVersion(info.version().majorVersion()) , minorVersion(info.version().minorVersion()) {} int type; QString path; int majorVersion; int minorVersion; }; uint qHash(const ImportCacheKey &info) { return ::qHash(info.type) ^ ::qHash(info.path) ^ ::qHash(info.majorVersion) ^ ::qHash(info.minorVersion); } bool operator==(const ImportCacheKey &i1, const ImportCacheKey &i2) { return i1.type == i2.type && i1.path == i2.path && i1.majorVersion == i2.majorVersion && i1.minorVersion == i2.minorVersion; } } class LinkPrivate { public: Snapshot snapshot; ValueOwner *valueOwner; QStringList importPaths; LibraryInfo builtins; ViewerContext vContext; QHash importCache; QHash > importableModuleApis; Document::Ptr document; QList *diagnosticMessages; QHash > *allDiagnosticMessages; Context::ImportsPerDocument linkImports(); void populateImportedTypes(Imports *imports, Document::Ptr doc); Import importFileOrDirectory( Document::Ptr doc, const ImportInfo &importInfo); Import importNonFile( Document::Ptr doc, const ImportInfo &importInfo); void importObject(Bind *bind, const QString &name, ObjectValue *object, NameId *targetNamespace); bool importLibrary(Document::Ptr doc, const QString &libraryPath, Import *import, const QString &importPath = QString()); void loadQmldirComponents(ObjectValue *import, LanguageUtils::ComponentVersion version, const LibraryInfo &libraryInfo, const QString &libraryPath); void loadImplicitDirectoryImports(Imports *imports, Document::Ptr doc); void loadImplicitDefaultImports(Imports *imports); void error(const Document::Ptr &doc, const AST::SourceLocation &loc, const QString &message); void warning(const Document::Ptr &doc, const AST::SourceLocation &loc, const QString &message); void appendDiagnostic(const Document::Ptr &doc, const DiagnosticMessage &message); }; /*! \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->valueOwner = new ValueOwner; d->snapshot = snapshot; d->importPaths = vContext.paths; d->builtins = builtins; d->vContext = vContext; d->diagnosticMessages = nullptr; d->allDiagnosticMessages = nullptr; ModelManagerInterface *modelManager = ModelManagerInterface::instance(); 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->valueOwner->cppQmlTypes().load(it.key(), it.value().exportedTypes); } // build an object with the context properties from C++ ObjectValue *cppContextProperties = d->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->valueOwner->cppQmlTypes().objectByCppName(cppTypeName); if (!value) value = d->valueOwner->unknownValue(); cppContextProperties->setMember(it.key(), value); } } d->valueOwner->cppQmlTypes().setCppContextProperties(cppContextProperties); } } ContextPtr Link::operator()(QHash > *messages) { d->allDiagnosticMessages = messages; return Context::create(d->snapshot, d->valueOwner, d->linkImports(), d->vContext); } ContextPtr Link::operator()(const Document::Ptr &doc, QList *messages) { d->document = doc; d->diagnosticMessages = messages; return Context::create(d->snapshot, d->valueOwner, d->linkImports(), d->vContext); } Link::~Link() { delete d; } Context::ImportsPerDocument LinkPrivate::linkImports() { Context::ImportsPerDocument importsPerDocument; // load builtin objects if (builtins.pluginTypeInfoStatus() == LibraryInfo::DumpDone || builtins.pluginTypeInfoStatus() == LibraryInfo::TypeInfoFileDone) { valueOwner->cppQmlTypes().load(QLatin1String(""), builtins.metaObjects()); } else { valueOwner->cppQmlTypes().load(QLatin1String(""), CppQmlTypesLoader::defaultQtObjects); } // load library objects shipped with Creator valueOwner->cppQmlTypes().load(QLatin1String(""), CppQmlTypesLoader::defaultLibraryObjects); if (document) { // do it on document first, to make sure import errors are shown Imports *imports = new Imports(valueOwner); // Add custom imports for the opened document for (const auto &provider : CustomImportsProvider::allProviders()) foreach (const auto &import, provider->imports(valueOwner, document.data())) importCache.insert(ImportCacheKey(import.info), import); populateImportedTypes(imports, document); importsPerDocument.insert(document.data(), QSharedPointer(imports)); } foreach (Document::Ptr doc, snapshot) { if (doc == document) continue; Imports *imports = new Imports(valueOwner); populateImportedTypes(imports, doc); importsPerDocument.insert(doc.data(), QSharedPointer(imports)); } return importsPerDocument; } void LinkPrivate::populateImportedTypes(Imports *imports, Document::Ptr doc) { importableModuleApis.clear(); // implicit imports: the package is always available loadImplicitDefaultImports(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 foreach (const ImportInfo &info, doc->bind()->imports()) { 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, Link::tr("File or directory not found.")); } break; default: break; } if (import.object) importCache.insert(ImportCacheKey(info), import); } if (import.object) imports->append(import); } } /* 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(Document::Ptr doc, const ImportInfo &importInfo) { Import import; import.info = importInfo; import.object = nullptr; import.valid = true; QString path = importInfo.path(); if (importInfo.type() == ImportType::Directory || importInfo.type() == ImportType::ImplicitDirectory) { import.object = new ObjectValue(valueOwner); importLibrary(doc, path, &import); const QList &documentsInDirectory = snapshot.documentsInDirectory(path); foreach (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) { Document::Ptr importedDoc = snapshot.document(path); if (importedDoc) import.object = importedDoc->bind()->rootObjectValue(); } else if (importInfo.type() == ImportType::QrcFile) { QLocale locale; QStringList filePaths = ModelManagerInterface::instance() ->filesAtQrcPath(path, &locale, nullptr, ModelManagerInterface::ActiveQrcResources); if (filePaths.isEmpty()) filePaths = ModelManagerInterface::instance()->filesAtQrcPath(path); if (!filePaths.isEmpty()) { Document::Ptr importedDoc = snapshot.document(filePaths.at(0)); if (importedDoc) import.object = importedDoc->bind()->rootObjectValue(); } } else if (importInfo.type() == ImportType::QrcDirectory){ import.object = new ObjectValue(valueOwner); importLibrary(doc, path, &import); const QMap paths = ModelManagerInterface::instance()->filesInQrcPath(path); for (auto iter = paths.cbegin(), end = paths.cend(); iter != end; ++iter) { if (ModelManagerInterface::guessLanguageOfFile(iter.key()).isQmlLikeLanguage()) { Document::Ptr importedDoc = snapshot.document(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; foreach (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(Document::Ptr doc, const ImportInfo &importInfo) { Import import; import.info = importInfo; import.object = new ObjectValue(valueOwner); import.valid = true; const QString packageName = importInfo.name(); const ComponentVersion version = importInfo.version(); QString libraryPath = modulePath(packageName, version.toString(), importPaths); bool importFound = !libraryPath.isEmpty() && importLibrary(doc, libraryPath, &import); // if there are cpp-based types for this package, use them too if (valueOwner->cppQmlTypes().hasModule(packageName)) { importFound = true; foreach (const CppComponentValue *object, valueOwner->cppQmlTypes().createObjectsForImport(packageName, version)) { 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(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()), Link::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").arg( importInfo.name(), importPaths.join(QLatin1Char('\n')))); } return import; } bool LinkPrivate::importLibrary(Document::Ptr doc, const QString &libraryPath_, Import *import, const QString &importPath) { const ImportInfo &importInfo = import->info; QString libraryPath = libraryPath_; LibraryInfo libraryInfo = 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()); 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 (errorLoc.isValid()) { appendDiagnostic(doc, DiagnosticMessage(Severity::ReadingTypeInfoWarning, errorLoc, Link::tr("QML module contains C++ plugins, currently reading type information..."))); 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 (errorLoc.isValid() && (packageName.isEmpty() || !valueOwner->cppQmlTypes().hasModule(packageName)) && !packageName.endsWith(QLatin1String("private"), Qt::CaseInsensitive)) { error(doc, errorLoc, libraryInfo.pluginTypeInfoError()); import->valid = false; } } else { const QString packageName = importInfo.name(); valueOwner->cppQmlTypes().load(libraryPath, libraryInfo.metaObjects(), packageName); foreach (const CppComponentValue *object, valueOwner->cppQmlTypes().createObjectsForImport(packageName, version)) { import->object->setMember(object->className(), object); } // all but no-uri module apis become available for import QList noUriModuleApis; foreach (const ModuleApiInfo &moduleApi, libraryInfo.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()) import->object->setPrototype(valueOwner->cppQmlTypes().objectByCppName(sameUriModuleApi.cppName)); } } loadQmldirComponents(import->object, version, libraryInfo, libraryPath); return true; } void LinkPrivate::error(const Document::Ptr &doc, const AST::SourceLocation &loc, const QString &message) { appendDiagnostic(doc, DiagnosticMessage(Severity::Error, loc, message)); } void LinkPrivate::warning(const Document::Ptr &doc, const AST::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 QString &libraryPath) { // if the version isn't valid, import the latest if (!version.isValid()) version = ComponentVersion(ComponentVersion::MaxVersion, ComponentVersion::MaxVersion); QSet importedTypes; foreach (const QmlDirParser::Component &component, libraryInfo.components()) { if (importedTypes.contains(component.typeName)) continue; ComponentVersion componentVersion(component.majorVersion, component.minorVersion); if (version < componentVersion) continue; importedTypes.insert(component.typeName); if (Document::Ptr importedDoc = snapshot.document( libraryPath + QLatin1Char('/') + component.fileName)) { if (ObjectValue *v = importedDoc->bind()->rootObjectValue()) import->setMember(component.typeName, v); } } } void LinkPrivate::loadImplicitDirectoryImports(Imports *imports, 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())); foreach (const QString &path, ModelManagerInterface::instance()->qrcPathsForFile(doc->fileName())) { processImport(ImportInfo::qrcDirectoryImport( Utils::QrcParser::qrcDirectoryPathForQrcFilePath(path))); } } void LinkPrivate::loadImplicitDefaultImports(Imports *imports) { const QString defaultPackage = CppQmlTypes::defaultPackage; if (valueOwner->cppQmlTypes().hasModule(defaultPackage)) { const ComponentVersion maxVersion(ComponentVersion::MaxVersion, ComponentVersion::MaxVersion); const ImportInfo info = ImportInfo::moduleImport(defaultPackage, maxVersion, QString()); Import import = importCache.value(ImportCacheKey(info)); if (!import.object) { import.valid = true; import.info = info; import.object = new ObjectValue(valueOwner, QLatin1String("")); foreach (const CppComponentValue *object, valueOwner->cppQmlTypes().createObjectsForImport( defaultPackage, maxVersion)) { import.object->setMember(object->className(), object); } importCache.insert(ImportCacheKey(info), import); } imports->append(import); } } } // namespace QmlJS