diff options
Diffstat (limited to 'src/lib/corelib/loader/astimportshandler.cpp')
-rw-r--r-- | src/lib/corelib/loader/astimportshandler.cpp | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/src/lib/corelib/loader/astimportshandler.cpp b/src/lib/corelib/loader/astimportshandler.cpp new file mode 100644 index 000000000..c0281ca24 --- /dev/null +++ b/src/lib/corelib/loader/astimportshandler.cpp @@ -0,0 +1,293 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** 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-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include "astimportshandler.h" + +#include "itemreadervisitorstate.h" + +#include <jsextensions/jsextensions.h> +#include <language/asttools.h> +#include <language/builtindeclarations.h> +#include <language/filecontext.h> +#include <logging/logger.h> +#include <logging/translator.h> +#include <parser/qmljsast_p.h> +#include <tools/error.h> +#include <tools/fileinfo.h> +#include <tools/qttools.h> +#include <tools/stringconstants.h> +#include <tools/version.h> + +#include <QtCore/qdiriterator.h> + +namespace qbs { +namespace Internal { + +ASTImportsHandler::ASTImportsHandler(ItemReaderVisitorState &visitorState, Logger &logger, + const FileContextPtr &file) + : m_visitorState(visitorState) + , m_logger(logger) + , m_file(file) + , m_directory(FileInfo::path(m_file->filePath())) +{ +} + +void ASTImportsHandler::handleImports(const QbsQmlJS::AST::UiImportList *uiImportList) +{ + const auto searchPaths = m_file->searchPaths(); + for (const QString &searchPath : searchPaths) + collectPrototypes(searchPath + QStringLiteral("/imports"), QString()); + + // files in the same directory are available as prototypes + collectPrototypes(m_directory, QString()); + + bool baseImported = false; + for (const auto *it = uiImportList; it; it = it->next) + handleImport(it->import, &baseImported); + if (!baseImported) { + QStringRef qbsref(&StringConstants::qbsModule()); + QbsQmlJS::AST::UiQualifiedId qbsURI(qbsref); + qbsURI.finish(); + QbsQmlJS::AST::UiImport imp(&qbsURI); + handleImport(&imp, &baseImported); + } + + for (auto it = m_jsImports.constBegin(); it != m_jsImports.constEnd(); ++it) + m_file->addJsImport(it.value()); +} + +void ASTImportsHandler::handleImport(const QbsQmlJS::AST::UiImport *import, bool *baseImported) +{ + QStringList importUri; + bool isBase = false; + if (import->importUri) { + importUri = toStringList(import->importUri); + isBase = (importUri.size() == 1 && importUri.front() == StringConstants::qbsModule()) + || (importUri.size() == 2 && importUri.front() == StringConstants::qbsModule() + && importUri.last() == StringConstants::baseVar()); + if (isBase) { + *baseImported = true; + checkImportVersion(import->versionToken); + } else if (import->versionToken.length) { + m_logger.printWarning(ErrorInfo(Tr::tr("Superfluous version specification."), + toCodeLocation(m_file->filePath(), import->versionToken))); + } + } + + QString as; + if (isBase) { + if (Q_UNLIKELY(!import->importId.isNull())) { + throw ErrorInfo(Tr::tr("Import of qbs.base must have no 'as <Name>'"), + toCodeLocation(m_file->filePath(), import->importIdToken)); + } + } else { + if (importUri.size() == 2 && importUri.front() == StringConstants::qbsModule()) { + const QString extensionName = importUri.last(); + if (JsExtensions::hasExtension(extensionName)) { + if (Q_UNLIKELY(!import->importId.isNull())) { + throw ErrorInfo(Tr::tr("Import of built-in extension '%1' " + "must not have 'as' specifier.").arg(extensionName), + toCodeLocation(m_file->filePath(), import->asToken)); + } + if (Q_UNLIKELY(m_file->jsExtensions().contains(extensionName))) { + m_logger.printWarning(ErrorInfo(Tr::tr("Built-in extension '%1' already " + "imported.").arg(extensionName), + toCodeLocation(m_file->filePath(), + import->importToken))); + } else { + m_file->addJsExtension(extensionName); + } + return; + } + } + + if (import->importId.isNull()) { + if (!import->fileName.isNull()) { + throw ErrorInfo(Tr::tr("File imports require 'as <Name>'"), + toCodeLocation(m_file->filePath(), import->importToken)); + } + if (importUri.empty()) { + throw ErrorInfo(Tr::tr("Invalid import URI."), + toCodeLocation(m_file->filePath(), import->importToken)); + } + as = importUri.last(); + } else { + as = import->importId.toString(); + } + + if (Q_UNLIKELY(JsExtensions::hasExtension(as))) + throw ErrorInfo(Tr::tr("Cannot reuse the name of built-in extension '%1'.").arg(as), + toCodeLocation(m_file->filePath(), import->importIdToken)); + if (Q_UNLIKELY(!m_importAsNames.insert(as).second)) { + throw ErrorInfo(Tr::tr("Cannot import into the same name more than once."), + toCodeLocation(m_file->filePath(), import->importIdToken)); + } + } + + if (!import->fileName.isNull()) { + QString filePath = FileInfo::resolvePath(m_directory, import->fileName.toString()); + + QFileInfo fi(filePath); + if (Q_UNLIKELY(!fi.exists())) + throw ErrorInfo(Tr::tr("Cannot find imported file %0.") + .arg(QDir::toNativeSeparators(filePath)), + CodeLocation(m_file->filePath(), import->fileNameToken.startLine, + import->fileNameToken.startColumn)); + filePath = fi.canonicalFilePath(); + if (fi.isDir()) { + collectPrototypesAndJsCollections(filePath, as, + toCodeLocation(m_file->filePath(), import->fileNameToken)); + } else { + if (filePath.endsWith(QStringLiteral(".js"), Qt::CaseInsensitive)) { + JsImport &jsImport = m_jsImports[as]; + jsImport.scopeName = as; + jsImport.filePaths.push_back(filePath); + jsImport.location + = toCodeLocation(m_file->filePath(), import->firstSourceLocation()); + } else if (filePath.endsWith(QStringLiteral(".qbs"), Qt::CaseInsensitive)) { + m_typeNameToFile.insert(QStringList(as), filePath); + } else { + throw ErrorInfo(Tr::tr("Can only import .qbs and .js files"), + CodeLocation(m_file->filePath(), import->fileNameToken.startLine, + import->fileNameToken.startColumn)); + } + } + } else if (!importUri.empty()) { + const QString importPath = isBase + ? QStringLiteral("qbs/base") : importUri.join(QDir::separator()); + bool found = m_typeNameToFile.contains(importUri); + if (!found) { + const auto searchPaths = m_file->searchPaths(); + for (const QString &searchPath : searchPaths) { + const QFileInfo fi(FileInfo::resolvePath( + FileInfo::resolvePath(searchPath, + StringConstants::importsDir()), + importPath)); + if (fi.isDir()) { + // ### versioning, qbsdir file, etc. + const QString &resultPath = fi.absoluteFilePath(); + collectPrototypesAndJsCollections(resultPath, as, + toCodeLocation(m_file->filePath(), import->fileNameToken)); + found = true; + break; + } + } + } + if (Q_UNLIKELY(!found)) { + throw ErrorInfo(Tr::tr("import %1 not found") + .arg(importUri.join(QLatin1Char('.'))), + toCodeLocation(m_file->filePath(), import->fileNameToken)); + } + } +} + +Version ASTImportsHandler::readImportVersion(const QString &str, const CodeLocation &location) +{ + const Version v = Version::fromString(str); + if (Q_UNLIKELY(!v.isValid())) + throw ErrorInfo(Tr::tr("Cannot parse version number in import statement."), location); + if (Q_UNLIKELY(v.patchLevel() != 0)) { + throw ErrorInfo(Tr::tr("Version number in import statement cannot have more than " + "two components."), location); + } + return v; +} + +bool ASTImportsHandler::addPrototype(const QString &fileName, const QString &filePath, + const QString &as, bool needsCheck) +{ + if (needsCheck && fileName.size() <= 4) + return false; + + const QString componentName = fileName.left(fileName.size() - 4); + // ### validate componentName + + if (needsCheck && !componentName.at(0).isUpper()) + return false; + + QStringList prototypeName; + if (!as.isEmpty()) + prototypeName.push_back(as); + prototypeName.push_back(componentName); + if (!m_typeNameToFile.contains(prototypeName)) + m_typeNameToFile.insert(prototypeName, filePath); + return true; +} + +void ASTImportsHandler::checkImportVersion(const QbsQmlJS::AST::SourceLocation &versionToken) const +{ + if (!versionToken.length) + return; + const QString importVersionString + = m_file->content().mid(versionToken.offset, versionToken.length); + const Version importVersion = readImportVersion(importVersionString, + toCodeLocation(m_file->filePath(), versionToken)); + if (Q_UNLIKELY(importVersion != BuiltinDeclarations::instance().languageVersion())) + throw ErrorInfo(Tr::tr("Incompatible qbs language version %1. This is version %2.").arg( + importVersionString, + BuiltinDeclarations::instance().languageVersion().toString()), + toCodeLocation(m_file->filePath(), versionToken)); + +} + +void ASTImportsHandler::collectPrototypes(const QString &path, const QString &as) +{ + QStringList fileNames; // Yes, file *names*. + m_visitorState.findDirectoryEntries(path, &fileNames); + for (const QString &fileName : std::as_const(fileNames)) + addPrototype(fileName, path + QLatin1Char('/') + fileName, as, false); +} + +void ASTImportsHandler::collectPrototypesAndJsCollections(const QString &path, const QString &as, + const CodeLocation &location) +{ + collectPrototypes(path, as); + QDirIterator dirIter(path, StringConstants::jsFileWildcards()); + while (dirIter.hasNext()) { + dirIter.next(); + JsImport &jsImport = m_jsImports[as]; + if (jsImport.scopeName.isNull()) { + jsImport.scopeName = as; + jsImport.location = location; + } + jsImport.filePaths.push_back(dirIter.filePath()); + } +} + +} // namespace Internal +} // namespace qbs |