diff options
Diffstat (limited to 'src/lib/corelib/api/projectfileupdater.cpp')
-rw-r--r-- | src/lib/corelib/api/projectfileupdater.cpp | 475 |
1 files changed, 475 insertions, 0 deletions
diff --git a/src/lib/corelib/api/projectfileupdater.cpp b/src/lib/corelib/api/projectfileupdater.cpp new file mode 100644 index 000000000..e7d879b28 --- /dev/null +++ b/src/lib/corelib/api/projectfileupdater.cpp @@ -0,0 +1,475 @@ +/**************************************************************************** +** +** 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 "projectfileupdater.h" + +#include "projectdata.h" +#include "qmljsrewriter.h" + +#include <language/asttools.h> +#include <logging/translator.h> +#include <parser/qmljsast_p.h> +#include <parser/qmljsastvisitor_p.h> +#include <parser/qmljsengine_p.h> +#include <parser/qmljslexer_p.h> +#include <parser/qmljsparser_p.h> +#include <tools/qbsassert.h> + +#include <QFile> + +using namespace QbsQmlJS; +using namespace AST; + +namespace qbs { +namespace Internal { + +class ItemFinder : public Visitor +{ +public: + ItemFinder(const CodeLocation &cl) : m_cl(cl), m_item(0) { } + + UiObjectDefinition *item() const { return m_item; } + +private: + bool visit(UiObjectDefinition *ast) + { + if (toCodeLocation(m_cl.fileName(), ast->firstSourceLocation()) == m_cl) { + m_item = ast; + return false; + } + return true; + } + + const CodeLocation m_cl; + UiObjectDefinition *m_item; +}; + +class FilesBindingFinder : public Visitor +{ +public: + FilesBindingFinder(const UiObjectDefinition *startItem) + : m_startItem(startItem), m_binding(0) + { + } + + UiScriptBinding *binding() const { return m_binding; } + +private: + bool visit(UiObjectDefinition *ast) + { + // We start with the direct parent of the binding, so do not descend into any + // other item. + return ast == m_startItem; + } + + bool visit(UiScriptBinding *ast) + { + if (ast->qualifiedId->name.toString() != QLatin1String("files")) + return true; + m_binding = ast; + return false; + } + + const UiObjectDefinition * const m_startItem; + UiScriptBinding *m_binding; +}; + + +ProjectFileUpdater::ProjectFileUpdater(const QString &projectFile) : m_projectFile(projectFile) +{ +} + +void ProjectFileUpdater::apply() +{ + QFile file(m_projectFile); + if (!file.open(QFile::ReadOnly)) { + throw ErrorInfo(Tr::tr("File '%1' cannot be opened for reading: %2") + .arg(m_projectFile, file.errorString())); + } + QString content = QString::fromLocal8Bit(file.readAll()); + file.close(); + Engine engine; + Lexer lexer(&engine); + lexer.setCode(content, 1); + Parser parser(&engine); + if (!parser.parse()) { + QList<DiagnosticMessage> parserMessages = parser.diagnosticMessages(); + if (!parserMessages.isEmpty()) { + ErrorInfo errorInfo; + errorInfo.append(Tr::tr("Failure parsing project file.")); + foreach (const DiagnosticMessage &msg, parserMessages) + errorInfo.append(msg.message, toCodeLocation(file.fileName(), msg.loc)); + throw errorInfo; + } + } + + doApply(content, parser.ast()); + + if (!file.open(QFile::WriteOnly)) { + throw ErrorInfo(Tr::tr("File '%1' cannot be opened for writing: %2") + .arg(m_projectFile, file.errorString())); + } + file.resize(0); + file.write(content.toLocal8Bit()); +} + + +ProjectFileGroupInserter::ProjectFileGroupInserter(const ProductData &product, + const QString &groupName) + : ProjectFileUpdater(product.location().fileName()) + , m_product(product) + , m_groupName(groupName) +{ +} + +void ProjectFileGroupInserter::doApply(QString &fileContent, UiProgram *ast) +{ + ItemFinder itemFinder(m_product.location()); + ast->accept(&itemFinder); + if (!itemFinder.item()) { + throw ErrorInfo(Tr::tr("The project file parser failed to find the product item."), + CodeLocation(projectFile())); + } + + ChangeSet changeSet; + Rewriter rewriter(fileContent, &changeSet, QStringList()); + QString groupItemString; + const int productItemIndentation + = itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1; + const int groupItemIndentation = productItemIndentation + 4; + const QString groupItemIndentationString = QString(groupItemIndentation, QLatin1Char(' ')); + groupItemString += groupItemIndentationString + QLatin1String("Group {\n"); + groupItemString += groupItemIndentationString + groupItemIndentationString + + QLatin1String("name: \"") + m_groupName + QLatin1String("\"\n"); + groupItemString += groupItemIndentationString + groupItemIndentationString + + QLatin1String("files: []\n"); + groupItemString += groupItemIndentationString + QLatin1Char('}'); + rewriter.addObject(itemFinder.item()->initializer, groupItemString); + + int lineOffset = 3 + 1; // Our text + a leading newline that is always added by the rewriter. + const QList<ChangeSet::EditOp> &editOps = changeSet.operationList(); + QBS_CHECK(editOps.count() == 1); + const ChangeSet::EditOp &insertOp = editOps.first(); + setLineOffset(lineOffset); + + int insertionLine = fileContent.left(insertOp.pos1).count(QLatin1Char('\n')); + for (int i = 0; i < insertOp.text.count() && insertOp.text.at(i) == QLatin1Char('\n'); ++i) + ++insertionLine; // To account for newlines prepended by the rewriter. + ++insertionLine; // To account for zero-based indexing. + setItemPosition(CodeLocation(projectFile(), insertionLine, + groupItemIndentation + 1)); + changeSet.apply(&fileContent); +} + +static QString getNodeRepresentation(const QString &fileContent, const Node *node) +{ + const quint32 start = node->firstSourceLocation().offset; + const quint32 end = node->lastSourceLocation().end(); + return fileContent.mid(start, end - start); +} + +static const ChangeSet::EditOp &getEditOp(const ChangeSet &changeSet) +{ + const QList<ChangeSet::EditOp> &editOps = changeSet.operationList(); + QBS_CHECK(editOps.count() == 1); + return editOps.first(); +} + +static int getLineOffsetForChangedBinding(const ChangeSet &changeSet, const QString &oldRhs) +{ + return getEditOp(changeSet).text.count(QLatin1Char('\n')) - oldRhs.count(QLatin1Char('\n')); +} + +static int getBindingLine(const ChangeSet &changeSet, const QString &fileContent) +{ + return fileContent.left(getEditOp(changeSet).pos1 + 1).count(QLatin1Char('\n')) + 1; +} + + +ProjectFileFilesAdder::ProjectFileFilesAdder(const ProductData &product, const GroupData &group, + const QStringList &files) + : ProjectFileUpdater(product.location().fileName()) + , m_product(product) + , m_group(group) + , m_files(files) +{ +} + +void ProjectFileFilesAdder::doApply(QString &fileContent, UiProgram *ast) +{ + // Find the item containing the "files" binding. + ItemFinder itemFinder(m_group.isValid() ? m_group.location() : m_product.location()); + ast->accept(&itemFinder); + if (!itemFinder.item()) { + throw ErrorInfo(Tr::tr("The project file parser failed to find the item."), + CodeLocation(projectFile())); + } + + const int itemIndentation + = itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1; + const int bindingIndentation = itemIndentation + 4; + const int arrayElemIndentation = bindingIndentation + 4; + QString newFilesString; + foreach (const QString &relFilePath, m_files) { + newFilesString += QString(arrayElemIndentation, QLatin1Char(' ')); + newFilesString += QLatin1Char('"'); + newFilesString += relFilePath; + newFilesString += QLatin1Char('"'); + newFilesString += QLatin1String(",\n"); + } + newFilesString.chop(2); // Trailing comma and newline. + + // Now get the binding itself. + FilesBindingFinder bindingFinder(itemFinder.item()); + itemFinder.item()->accept(&bindingFinder); + + ChangeSet changeSet; + Rewriter rewriter(fileContent, &changeSet, QStringList()); + + UiScriptBinding * const filesBinding = bindingFinder.binding(); + if (filesBinding) { + if (filesBinding->statement->kind != Node::Kind_ExpressionStatement) + throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex.")); // TODO: rename, add new and concat. + const ExpressionStatement * const exprStatement + = static_cast<ExpressionStatement *>(filesBinding->statement); + switch (exprStatement->expression->kind) { + case Node::Kind_ArrayLiteral: { + QString filesString = QLatin1String("[\n"); + const ElementList *elem + = static_cast<ArrayLiteral *>(exprStatement->expression)->elements; + while (elem) { + filesString += QString(arrayElemIndentation, QLatin1Char(' ')); + filesString += getNodeRepresentation(fileContent, elem->expression); + filesString += QLatin1String(",\n"); + elem = elem->next; + } + filesString += newFilesString; + filesString += QLatin1Char('\n'); + filesString += QString(bindingIndentation, QLatin1Char(' ')); + filesString += QLatin1Char(']'); + rewriter.changeBinding(itemFinder.item()->initializer, QLatin1String("files"), + filesString, Rewriter::ScriptBinding); + break; + } + case Node::Kind_StringLiteral: { + const QString existingElement + = static_cast<StringLiteral *>(exprStatement->expression)->value.toString(); + QString filesString = QLatin1String("[\n"); + filesString += QString(arrayElemIndentation, QLatin1Char(' ')); + filesString += QLatin1Char('"') + existingElement + QLatin1Char('"'); + filesString += QLatin1String(",\n"); + filesString += newFilesString; + filesString += QLatin1Char('\n'); + filesString += QString(bindingIndentation, QLatin1Char(' ')); + filesString += QLatin1Char(']'); + rewriter.changeBinding(itemFinder.item()->initializer, QLatin1String("files"), + filesString, Rewriter::ScriptBinding); + break; + } + default: { + // Note that we can often do better than simply concatenating: For instance, + // in the case where the existing list is of the form ["a", "b"].concat(myProperty), + // we could keep on parsing until we find the array literal and then merge it with + // the new files, preventing cascading concat() calls. + // But this is not essential and can be implemented when we have some downtime. + const QString rhsRepr = getNodeRepresentation(fileContent, exprStatement->expression); + QString filesString = QLatin1String("[\n"); + filesString += newFilesString; + filesString += QLatin1Char('\n'); + filesString += QString(bindingIndentation, QLatin1Char(' ')); + + // It cannot be the other way around, since the existing right-hand side could + // have string type. + filesString += QString::fromLatin1("].concat(%1)").arg(rhsRepr); + + rewriter.changeBinding(itemFinder.item()->initializer, QLatin1String("files"), + filesString, Rewriter::ScriptBinding); + } + } + } else { // Can happen for the product itself, for which the "files" binding is not mandatory. + newFilesString.prepend(QLatin1String("[\n")); + newFilesString += QLatin1Char('\n'); + newFilesString += QString(bindingIndentation, QLatin1Char(' ')); + newFilesString += QLatin1Char(']'); + const QString bindingString = QString(bindingIndentation, QLatin1Char(' ')) + + QLatin1String("files"); + rewriter.addBinding(itemFinder.item()->initializer, bindingString, newFilesString, + Rewriter::ScriptBinding); + } + + setLineOffset(getLineOffsetForChangedBinding(changeSet, getNodeRepresentation(fileContent, + filesBinding->statement))); + const int insertionLine = getBindingLine(changeSet, fileContent) + 1; + const int insertionColumn = (filesBinding ? arrayElemIndentation : bindingIndentation) + 1; + setItemPosition(CodeLocation(projectFile(), insertionLine, insertionColumn)); + changeSet.apply(&fileContent); +} + +ProjectFileFilesRemover::ProjectFileFilesRemover(const ProductData &product, const GroupData &group, + const QStringList &files) + : ProjectFileUpdater(product.location().fileName()) + , m_product(product) + , m_group(group) + , m_files(files) +{ +} + +void ProjectFileFilesRemover::doApply(QString &fileContent, UiProgram *ast) +{ + // Find the item containing the "files" binding. + ItemFinder itemFinder(m_group.isValid() ? m_group.location() : m_product.location()); + ast->accept(&itemFinder); + if (!itemFinder.item()) { + throw ErrorInfo(Tr::tr("The project file parser failed to find the item."), + CodeLocation(projectFile())); + } + + // Now get the binding itself. + FilesBindingFinder bindingFinder(itemFinder.item()); + itemFinder.item()->accept(&bindingFinder); + if (!bindingFinder.binding()) { + throw ErrorInfo(Tr::tr("Could not find the 'files' binding in the project file."), + m_product.location()); + } + + if (bindingFinder.binding()->statement->kind != Node::Kind_ExpressionStatement) + throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex.")); + const CodeLocation bindingLocation + = toCodeLocation(projectFile(), bindingFinder.binding()->firstSourceLocation()); + + ChangeSet changeSet; + Rewriter rewriter(fileContent, &changeSet, QStringList()); + + const int itemIndentation + = itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1; + const int bindingIndentation = itemIndentation + 4; + const int arrayElemIndentation = bindingIndentation + 4; + + const ExpressionStatement * const exprStatement + = static_cast<ExpressionStatement *>(bindingFinder.binding()->statement); + switch (exprStatement->expression->kind) { + case Node::Kind_ArrayLiteral: { + QStringList filesToRemove = m_files; + QStringList newFilesList; + const ElementList *elem = static_cast<ArrayLiteral *>(exprStatement->expression)->elements; + while (elem) { + if (elem->expression->kind != Node::Kind_StringLiteral) { + throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex."), + bindingLocation); + } + const QString existingFile + = static_cast<StringLiteral *>(elem->expression)->value.toString(); + if (!filesToRemove.removeOne(existingFile)) + newFilesList << existingFile; + elem = elem->next; + } + if (!filesToRemove.isEmpty()) { + throw ErrorInfo(Tr::tr("The following files were not found in the 'files' list: %1") + .arg(filesToRemove.join(QLatin1String(", "))), bindingLocation); + } + QString filesString = QLatin1String("[\n"); + foreach (const QString &file, newFilesList) { + filesString += QString(arrayElemIndentation, QLatin1Char(' ')); + filesString += QString::fromLocal8Bit("\"%1\",\n").arg(file); + } + filesString += QString(bindingIndentation, QLatin1Char(' ')); + filesString += QLatin1Char(']'); + rewriter.changeBinding(itemFinder.item()->initializer, QLatin1String("files"), + filesString, Rewriter::ScriptBinding); + break; + } + case Node::Kind_StringLiteral: { + if (m_files.count() != 1) { + throw ErrorInfo(Tr::tr("Was requested to remove %1 files, but there is only " + "one in the list.").arg(m_files.count()), bindingLocation); + } + const QString existingFile + = static_cast<StringLiteral *>(exprStatement->expression)->value.toString(); + if (existingFile != m_files.first()) { + throw ErrorInfo(Tr::tr("File '1' could not be found in the 'files' list."), + bindingLocation); + } + rewriter.changeBinding(itemFinder.item()->initializer, QLatin1String("files"), + QLatin1String("[]"), Rewriter::ScriptBinding); + break; + } + default: + throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex."), + bindingLocation); + } + + setLineOffset(getLineOffsetForChangedBinding(changeSet, + getNodeRepresentation(fileContent, exprStatement->expression))); + const int bindingLine = getBindingLine(changeSet, fileContent); + const int bindingColumn = (bindingFinder.binding() + ? arrayElemIndentation : bindingIndentation) + 1; + setItemPosition(CodeLocation(projectFile(), bindingLine, bindingColumn)); + changeSet.apply(&fileContent); +} + + +ProjectFileGroupRemover::ProjectFileGroupRemover(const ProductData &product, const GroupData &group) + : ProjectFileUpdater(product.location().fileName()) + , m_product(product) + , m_group(group) +{ +} + +void ProjectFileGroupRemover::doApply(QString &fileContent, UiProgram *ast) +{ + ItemFinder productFinder(m_product.location()); + ast->accept(&productFinder); + if (!productFinder.item()) { + throw ErrorInfo(Tr::tr("The project file parser failed to find the product item."), + CodeLocation(projectFile())); + } + + ItemFinder groupFinder(m_group.location()); + productFinder.item()->accept(&groupFinder); + if (!groupFinder.item()) { + throw ErrorInfo(Tr::tr("The project file parser failed to find the group item."), + m_product.location()); + } + + ChangeSet changeSet; + Rewriter rewriter(fileContent, &changeSet, QStringList()); + rewriter.removeObjectMember(groupFinder.item(), productFinder.item()); + + setItemPosition(m_group.location()); + const QList<ChangeSet::EditOp> &editOps = changeSet.operationList(); + QBS_CHECK(editOps.count() == 1); + const ChangeSet::EditOp &op = editOps.first(); + const QString removedText = fileContent.mid(op.pos1, op.length1); + setLineOffset(-removedText.count(QLatin1Char('\n'))); + + changeSet.apply(&fileContent); +} + +} // namespace Internal +} // namespace qbs |