diff options
Diffstat (limited to 'src/lib/corelib/language/itemreaderastvisitor.cpp')
-rw-r--r-- | src/lib/corelib/language/itemreaderastvisitor.cpp | 643 |
1 files changed, 643 insertions, 0 deletions
diff --git a/src/lib/corelib/language/itemreaderastvisitor.cpp b/src/lib/corelib/language/itemreaderastvisitor.cpp new file mode 100644 index 000000000..a08a73648 --- /dev/null +++ b/src/lib/corelib/language/itemreaderastvisitor.cpp @@ -0,0 +1,643 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Build Suite. +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "itemreaderastvisitor.h" +#include "asttools.h" +#include "builtindeclarations.h" +#include "identifiersearch.h" +#include "itemreader.h" +#include <jsextensions/jsextensions.h> +#include <parser/qmljsast_p.h> +#include <tools/error.h> +#include <tools/fileinfo.h> +#include <tools/qbsassert.h> +#include <tools/qttools.h> +#include <logging/translator.h> + +#include <QDirIterator> +#include <QFileInfo> +#include <QStringList> + +using namespace QbsQmlJS; + +namespace qbs { +namespace Internal { + +ItemReaderASTVisitor::ItemReaderASTVisitor(ItemReader *reader, ItemReaderResult *result) + : m_reader(reader) + , m_readerResult(result) + , m_languageVersion(ImportVersion::fromString(reader->builtins()->languageVersion())) + , m_item(0) + , m_sourceValue(0) +{ +} + +ItemReaderASTVisitor::~ItemReaderASTVisitor() +{ +} + +bool ItemReaderASTVisitor::visit(AST::UiProgram *ast) +{ + Q_UNUSED(ast); + m_sourceValue.clear(); + m_file = FileContext::create(); + m_file->m_filePath = m_filePath; + + if (Q_UNLIKELY(!ast->members->member)) + throw ErrorInfo(Tr::tr("No root item found in %1.").arg(m_filePath)); + + return true; +} + +bool ItemReaderASTVisitor::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.append(as); + prototypeName.append(componentName); + m_typeNameToFile.insert(prototypeName, filePath); + return true; +} + +void ItemReaderASTVisitor::collectPrototypes(const QString &path, const QString &as) +{ + QStringList fileNames; // Yes, file *names*. + if (m_reader->findDirectoryEntries(path, &fileNames)) { + foreach (const QString &fileName, fileNames) + addPrototype(fileName, path + QLatin1Char('/') + fileName, as, false); + return; + } + + QDirIterator dirIter(path, QStringList("*.qbs")); + while (dirIter.hasNext()) { + const QString filePath = dirIter.next(); + const QString fileName = dirIter.fileName(); + if (addPrototype(fileName, filePath, as, true)) + fileNames << fileName; + } + m_reader->cacheDirectoryEntries(path, fileNames); +} + +bool ItemReaderASTVisitor::visit(AST::UiImportList *uiImportList) +{ + foreach (const QString &searchPath, m_reader->searchPaths()) + collectPrototypes(searchPath + QLatin1String("/imports"), QString()); + + const QString path = FileInfo::path(m_filePath); + + // files in the same directory are available as prototypes + collectPrototypes(path, QString()); + + QSet<QString> importAsNames; + QHash<QString, JsImport> jsImports; + + for (const AST::UiImportList *it = uiImportList; it; it = it->next) { + const AST::UiImport *const import = it->import; + + QStringList importUri; + bool isBase = false; + if (import->importUri) { + importUri = toStringList(import->importUri); + isBase = (importUri.size() == 1 && importUri.first() == QLatin1String("qbs")) + || (importUri.size() == 2 && importUri.first() == QLatin1String("qbs") + && importUri.last() == QLatin1String("base")); + if (isBase) + checkImportVersion(import->versionToken); + else if (import->versionToken.length) + m_reader->logger().printWarning(ErrorInfo(Tr::tr("Superfluous version specification."), + toCodeLocation(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(import->importIdToken)); + } + } else { + if (importUri.count() == 2 && importUri.first() == QLatin1String("qbs")) { + 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)); + } + if (Q_UNLIKELY(m_file->m_jsExtensions.contains(extensionName))) { + m_reader->logger().printWarning(Tr::tr("Built-in extension '%1' already " + "imported.").arg(extensionName)); + } else { + m_file->m_jsExtensions << extensionName; + } + continue; + } + } + + if (import->importId.isNull()) { + if (!import->fileName.isNull()) { + throw ErrorInfo(Tr::tr("File imports require 'as <Name>'"), + toCodeLocation(import->importToken)); + } + if (importUri.isEmpty()) { + throw ErrorInfo(Tr::tr("Invalid import URI."), + toCodeLocation(import->importToken)); + } + as = importUri.last(); + } else { + as = import->importId.toString(); + } + + if (Q_UNLIKELY(importAsNames.contains(as))) { + throw ErrorInfo(Tr::tr("Can't import into the same name more than once."), + toCodeLocation(import->importIdToken)); + } + if (Q_UNLIKELY(JsExtensions::hasExtension(as))) { + throw ErrorInfo(Tr::tr("Cannot reuse the name of built-in extension '%1'.") + .arg(as)); + } + importAsNames.insert(as); + } + + if (!import->fileName.isNull()) { + QString name = FileInfo::resolvePath(path, import->fileName.toString()); + + QFileInfo fi(name); + if (Q_UNLIKELY(!fi.exists())) + throw ErrorInfo(Tr::tr("Can't find imported file %0.").arg(name), + CodeLocation(m_filePath, import->fileNameToken.startLine, + import->fileNameToken.startColumn)); + name = fi.canonicalFilePath(); + if (fi.isDir()) { + collectPrototypes(name, as); + } else { + if (name.endsWith(".js", Qt::CaseInsensitive)) { + JsImport &jsImport = jsImports[as]; + jsImport.scopeName = as; + jsImport.fileNames.append(name); + jsImport.location = toCodeLocation(import->firstSourceLocation()); + } else if (name.endsWith(".qbs", Qt::CaseInsensitive)) { + m_typeNameToFile.insert(QStringList(as), name); + } else { + throw ErrorInfo(Tr::tr("Can only import .qbs and .js files"), + CodeLocation(m_filePath, import->fileNameToken.startLine, + import->fileNameToken.startColumn)); + } + } + } else if (!importUri.isEmpty()) { + const QString importPath = isBase + ? QLatin1String("qbs/base") : importUri.join(QDir::separator()); + bool found = m_typeNameToFile.contains(importUri); + if (!found) { + foreach (const QString &searchPath, m_reader->searchPaths()) { + const QFileInfo fi(FileInfo::resolvePath( + FileInfo::resolvePath(searchPath, "imports"), importPath)); + if (fi.isDir()) { + // ### versioning, qbsdir file, etc. + const QString &resultPath = fi.absoluteFilePath(); + collectPrototypes(resultPath, as); + + QDirIterator dirIter(resultPath, QStringList("*.js")); + while (dirIter.hasNext()) { + dirIter.next(); + JsImport &jsImport = jsImports[as]; + if (jsImport.scopeName.isNull()) { + jsImport.scopeName = as; + jsImport.location = toCodeLocation(import->firstSourceLocation()); + } + jsImport.fileNames.append(dirIter.filePath()); + } + found = true; + break; + } + } + } + if (Q_UNLIKELY(!found)) { + throw ErrorInfo(Tr::tr("import %1 not found").arg(importUri.join(".")), + toCodeLocation(import->fileNameToken)); + } + } + } + + for (QHash<QString, JsImport>::const_iterator it = jsImports.constBegin(); + it != jsImports.constEnd(); ++it) + { + m_file->m_jsImports += it.value(); + } + + return false; +} + +bool ItemReaderASTVisitor::visit(AST::UiObjectDefinition *ast) +{ + const QString typeName = ast->qualifiedTypeNameId->name.toString(); + + Item *item = Item::create(m_reader->m_pool); + item->m_file = m_file; + item->m_parent = m_item; + item->m_typeName = typeName; + item->m_location = ::qbs::Internal::toCodeLocation(m_file->filePath(), + ast->qualifiedTypeNameId->identifierToken); + + if (m_item) { + // Add this item to the children of the parent item. + m_item->m_children += item; + } else { + // This is the root item. + m_item = item; + m_readerResult->rootItem = item; + } + + if (ast->initializer) { + qSwap(m_item, item); + ast->initializer->accept(this); + qSwap(m_item, item); + } + + m_reader->m_builtins->setupItemForBuiltinType(item); + + if (item->typeName() != QLatin1String("Properties") + && item->typeName() != QLatin1String("SubProject")) { + setupAlternatives(item); + } + + // resolve inheritance + const QStringList fullTypeName = toStringList(ast->qualifiedTypeNameId); + const QString baseTypeFileName = m_typeNameToFile.value(fullTypeName); + if (!baseTypeFileName.isEmpty()) { + const ItemReaderResult baseFile = m_reader->internalReadFile(baseTypeFileName); + mergeItem(item, baseFile.rootItem, baseFile); + if (baseFile.rootItem->m_file->m_idScope) { + // Make ids from the derived file visible in the base file. + // ### Do we want to turn off this feature? It's QMLish but kind of strange. + ensureIdScope(item->m_file); + baseFile.rootItem->m_file->m_idScope->setPrototype(item->m_file->m_idScope); + } + } + + return false; +} + +void ItemReaderASTVisitor::checkDuplicateBinding(Item *item, const QStringList &bindingName, + const AST::SourceLocation &sourceLocation) +{ + if (Q_UNLIKELY(item->properties().contains(bindingName.last()))) { + QString msg = Tr::tr("Duplicate binding for '%1'"); + throw ErrorInfo(msg.arg(bindingName.join(".")), + qbs::Internal::toCodeLocation(m_file->filePath(), sourceLocation)); + } +} + +bool ItemReaderASTVisitor::visit(AST::UiPublicMember *ast) +{ + PropertyDeclaration p; + if (Q_UNLIKELY(ast->name.isEmpty())) + throw ErrorInfo(Tr::tr("public member without name")); + if (Q_UNLIKELY(ast->memberType.isEmpty())) + throw ErrorInfo(Tr::tr("public member without type")); + if (Q_UNLIKELY(ast->type == AST::UiPublicMember::Signal)) + throw ErrorInfo(Tr::tr("public member with signal type not supported")); + p.name = ast->name.toString(); + p.type = PropertyDeclaration::propertyTypeFromString(ast->memberType.toString()); + if (p.type == PropertyDeclaration::UnknownType) + throw ErrorInfo(Tr::tr("Unknown type '%1' in property declaration.") + .arg(ast->memberType.toString()), toCodeLocation(ast->typeToken)); + if (ast->typeModifier.compare(QLatin1String("list"))) + p.flags |= PropertyDeclaration::ListProperty; + else if (Q_UNLIKELY(!ast->typeModifier.isEmpty())) + throw ErrorInfo(Tr::tr("public member with type modifier '%1' not supported").arg( + ast->typeModifier.toString())); + + m_item->m_propertyDeclarations.insert(p.name, p); + + JSSourceValuePtr value = JSSourceValue::create(); + value->setFile(m_file); + if (ast->statement) { + m_sourceValue.swap(value); + visitStatement(ast->statement); + m_sourceValue.swap(value); + const QStringList bindingName(p.name); + checkDuplicateBinding(m_item, bindingName, ast->colonToken); + } + + m_item->m_properties.insert(p.name, value); + return false; +} + +bool ItemReaderASTVisitor::visit(AST::UiScriptBinding *ast) +{ + QBS_CHECK(ast->qualifiedId); + QBS_CHECK(!ast->qualifiedId->name.isEmpty()); + + const QStringList bindingName = toStringList(ast->qualifiedId); + + if (bindingName.length() == 1 && bindingName.first() == QLatin1String("id")) { + AST::ExpressionStatement *expStmt = + AST::cast<AST::ExpressionStatement *>(ast->statement); + if (Q_UNLIKELY(!expStmt)) + throw ErrorInfo(Tr::tr("id: must be followed by identifier")); + AST::IdentifierExpression *idExp = + AST::cast<AST::IdentifierExpression *>(expStmt->expression); + if (Q_UNLIKELY(!idExp || idExp->name.isEmpty())) + throw ErrorInfo(Tr::tr("id: must be followed by identifier")); + m_item->m_id = idExp->name.toString(); + ensureIdScope(m_file); + m_file->m_idScope->m_properties[m_item->m_id] = ItemValue::create(m_item); + return false; + } + + JSSourceValuePtr value = JSSourceValue::create(); + value->setFile(m_file); + m_sourceValue.swap(value); + visitStatement(ast->statement); + m_sourceValue.swap(value); + + Item *targetItem = targetItemForBinding(m_item, bindingName, value->location()); + checkDuplicateBinding(targetItem, bindingName, ast->qualifiedId->identifierToken); + targetItem->m_properties.insert(bindingName.last(), value); + return false; +} + +bool ItemReaderASTVisitor::visit(AST::FunctionDeclaration *ast) +{ + FunctionDeclaration f; + if (Q_UNLIKELY(ast->name.isNull())) + throw ErrorInfo(Tr::tr("function decl without name")); + f.setName(ast->name.toString()); + + // remove the name + QString funcNoName = textOf(m_sourceCode, ast); + funcNoName.replace(QRegExp("^(\\s*function\\s*)\\w*"), "(\\1"); + funcNoName.append(")"); + f.setSourceCode(funcNoName); + + f.setLocation(toCodeLocation(ast->firstSourceLocation())); + m_item->m_functions += f; + return false; +} + +bool ItemReaderASTVisitor::visitStatement(AST::Statement *statement) +{ + QBS_CHECK(statement); + QBS_CHECK(m_sourceValue); + + QString sourceCode = textOf(m_sourceCode, statement); + if (AST::cast<AST::Block *>(statement)) { + // rewrite blocks to be able to use return statements in property assignments + sourceCode.prepend("(function()"); + sourceCode.append(")()"); + m_sourceValue->m_hasFunctionForm = true; + } + + m_sourceValue->setSourceCode(sourceCode); + m_sourceValue->setLocation(toCodeLocation(statement->firstSourceLocation())); + + IdentifierSearch idsearch; + idsearch.add(QLatin1String("base"), &m_sourceValue->m_sourceUsesBase); + idsearch.add(QLatin1String("outer"), &m_sourceValue->m_sourceUsesOuter); + idsearch.start(statement); + return false; +} + +CodeLocation ItemReaderASTVisitor::toCodeLocation(AST::SourceLocation location) const +{ + return CodeLocation(m_filePath, location.startLine, location.startColumn); +} + +Item *ItemReaderASTVisitor::targetItemForBinding(Item *item, + const QStringList &bindingName, + const CodeLocation &bindingLocation) +{ + Item *targetItem = item; + const int c = bindingName.count() - 1; + for (int i = 0; i < c; ++i) { + ValuePtr v = targetItem->m_properties.value(bindingName.at(i)); + if (!v) { + Item *newItem = Item::create(m_reader->m_pool); + v = ItemValue::create(newItem); + targetItem->m_properties.insert(bindingName.at(i), v); + } + if (Q_UNLIKELY(v->type() != Value::ItemValueType)) { + QString msg = Tr::tr("Binding to non-item property."); + throw ErrorInfo(msg, bindingLocation); + } + ItemValuePtr jsv = v.staticCast<ItemValue>(); + targetItem = jsv->item(); + } + return targetItem; +} + +void ItemReaderASTVisitor::checkImportVersion(const AST::SourceLocation &versionToken) const +{ + if (!versionToken.length) + return; + const QString importVersionString = m_sourceCode.mid(versionToken.offset, versionToken.length); + const ImportVersion importVersion + = ImportVersion::fromString(importVersionString, toCodeLocation(versionToken)); + if (Q_UNLIKELY(importVersion != m_languageVersion)) + throw ErrorInfo(Tr::tr("Incompatible qbs version %1. This is qbs %2.").arg( + importVersionString, m_reader->builtins()->languageVersion()), + toCodeLocation(versionToken)); +} + +void ItemReaderASTVisitor::mergeItem(Item *dst, const Item *src, + const ItemReaderResult &baseFile) +{ + if (!src->typeName().isEmpty()) + dst->setTypeName(src->typeName()); + + int insertPos = 0; + for (int i = 0; i < src->m_children.count(); ++i) { + Item *child = src->m_children.at(i); + dst->m_children.insert(insertPos++, child); + child->m_parent = dst; + } + + for (QMap<QString, ValuePtr>::const_iterator it = src->m_properties.constBegin(); + it != src->m_properties.constEnd(); ++it) + { + ValuePtr &v = dst->m_properties[it.key()]; + if (v) { + if (v->type() == it.value()->type()) { + if (v->type() == Value::JSSourceValueType) { + JSSourceValuePtr sv = v.staticCast<JSSourceValue>(); + while (sv->baseValue()) + sv = sv->baseValue(); + const JSSourceValuePtr baseValue = it.value().staticCast<JSSourceValue>(); + sv->setBaseValue(baseValue); + for (QList<JSSourceValue::Alternative>::iterator it + = sv->m_alternatives.begin(); it != sv->m_alternatives.end(); ++it) { + JSSourceValue::Alternative &alternative = *it; + alternative.value->setBaseValue(baseValue); + } + } else if (v->type() == Value::ItemValueType) { + QBS_CHECK(v.staticCast<ItemValue>()->item()); + QBS_CHECK(it.value().staticCast<const ItemValue>()->item()); + mergeItem(v.staticCast<ItemValue>()->item(), + it.value().staticCast<const ItemValue>()->item(), + baseFile); + } else { + QBS_CHECK(!"unexpected value type"); + } + } + } else { + v = it.value(); + } + } + + for (QMap<QString, PropertyDeclaration>::const_iterator it + = src->m_propertyDeclarations.constBegin(); + it != src->m_propertyDeclarations.constEnd(); ++it) { + dst->m_propertyDeclarations[it.key()] = it.value(); + } + foreach (const JSSourceValuePtr &valueWithAlternatives, + baseFile.conditionalValuesPerScopeItem.value(src)) { + replaceConditionScopes(valueWithAlternatives, dst); + } +} + +void ItemReaderASTVisitor::ensureIdScope(const FileContextPtr &file) +{ + if (!file->m_idScope) { + file->m_idScope = Item::create(m_reader->m_pool); + file->m_idScope->m_typeName = QLatin1String("IdScope"); + } +} + +void ItemReaderASTVisitor::setupAlternatives(Item *item) +{ + QList<Item *>::iterator it = item->m_children.begin(); + while (it != item->m_children.end()) { + Item *child = *it; + if (child->typeName() == QLatin1String("Properties")) { + handlePropertiesBlock(item, child); + it = item->m_children.erase(it); + } else { + ++it; + } + } +} + +void ItemReaderASTVisitor::replaceConditionScopes(const JSSourceValuePtr &value, + Item *newScope) +{ + for (QList<JSSourceValue::Alternative>::iterator it + = value->m_alternatives.begin(); it != value->m_alternatives.end(); ++it) + it->conditionScopeItem = newScope; +} + +class PropertiesBlockConverter +{ +public: + PropertiesBlockConverter(const QString &condition, Item *propertiesBlockContainer, + const Item *propertiesBlock, + QSet<JSSourceValuePtr> *valuesWithAlternatives) + : m_propertiesBlockContainer(propertiesBlockContainer) + , m_propertiesBlock(propertiesBlock) + , m_valuesWithAlternatives(valuesWithAlternatives) + { + m_alternative.condition = condition; + m_alternative.conditionScopeItem = propertiesBlockContainer; + } + + void operator()() + { + apply(m_propertiesBlockContainer, m_propertiesBlock); + } + +private: + JSSourceValue::Alternative m_alternative; + Item *m_propertiesBlockContainer; + const Item *m_propertiesBlock; + QSet<JSSourceValuePtr> *m_valuesWithAlternatives; + + void apply(Item *a, const Item *b) + { + for (QMap<QString, ValuePtr>::const_iterator it = b->properties().constBegin(); + it != b->properties().constEnd(); ++it) { + if (b == m_propertiesBlock && it.key() == QLatin1String("condition")) + continue; + if (it.value()->type() == Value::ItemValueType) { + apply(a->itemProperty(it.key(), true)->item(), + it.value().staticCast<ItemValue>()->item()); + } else if (it.value()->type() == Value::JSSourceValueType) { + ValuePtr aval = a->property(it.key()); + if (Q_UNLIKELY(aval && aval->type() != Value::JSSourceValueType)) + throw ErrorInfo(Tr::tr("Incompatible value type in unconditional value at %1.").arg( + aval->location().toString())); + apply(it.key(), a, aval.staticCast<JSSourceValue>(), + it.value().staticCast<JSSourceValue>()); + } else { + QBS_CHECK(!"Unexpected value type in conditional value."); + } + } + } + + void apply(const QString &propertyName, Item *item, JSSourceValuePtr value, + const JSSourceValuePtr &conditionalValue) + { + QBS_ASSERT(!value || value->file() == conditionalValue->file(), return); + if (!value) { + value = JSSourceValue::create(); + value->setFile(conditionalValue->file()); + item->setProperty(propertyName, value); + value->setSourceCode(QLatin1String("undefined")); + } + m_alternative.value = conditionalValue; + value->addAlternative(m_alternative); + m_valuesWithAlternatives->insert(value); + } +}; + +void ItemReaderASTVisitor::handlePropertiesBlock(Item *item, const Item *block) +{ + ValuePtr value = block->property(QLatin1String("condition")); + if (Q_UNLIKELY(!value)) + throw ErrorInfo(Tr::tr("Properties.condition must be provided."), + block->location()); + if (Q_UNLIKELY(value->type() != Value::JSSourceValueType)) + throw ErrorInfo(Tr::tr("Properties.condition must be a value binding."), + block->location()); + JSSourceValuePtr srcval = value.staticCast<JSSourceValue>(); + const QString condition = srcval->sourceCode(); + PropertiesBlockConverter convertBlock(condition, item, block, + &m_readerResult->conditionalValuesPerScopeItem[item]); + convertBlock(); +} + +} // namespace Internal +} // namespace qbs |