/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** Commercial Usage ** ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** 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. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://qt.nokia.com/contact. ** **************************************************************************/ #include "qmljsdelta.h" #include "qmljsclientproxy.h" #include #include #include #include using namespace QmlJS; using namespace QmlJS::AST; using namespace QmlJSInspector::Internal; namespace { /*! Build a hash of the parents */ struct BuildParentHash : public Visitor { virtual void postVisit(Node* ); virtual bool preVisit(Node* ); QHash parent; private: QList stack; }; bool BuildParentHash::preVisit(Node* ast) { if (ast->uiObjectMemberCast()) { stack.append(ast->uiObjectMemberCast()); } return true; } void BuildParentHash::postVisit(Node* ast) { if (ast->uiObjectMemberCast()) { stack.removeLast(); if (!stack.isEmpty()) { parent.insert(ast->uiObjectMemberCast(), stack.last()); } } } static QString label(UiQualifiedId *id) { QString str; for (; id ; id = id->next) { if (!id->name) return QString(); if (!str.isEmpty()) str += QLatin1Char('.'); str += id->name->asString(); } return str; } static QString label(UiObjectMember *member, Document::Ptr doc) { QString str; if(!member) return str; if (UiObjectDefinition* foo = cast(member)) { str = label(foo->qualifiedTypeNameId); } else if(UiObjectBinding *foo = cast(member)) { str = label(foo->qualifiedId) + QLatin1Char(' ') + label(foo->qualifiedTypeNameId); } else if(UiArrayBinding *foo = cast(member)) { str = label(foo->qualifiedId) + QLatin1String("[]"); } else if(UiScriptBinding *foo = cast(member)) { Q_UNUSED(foo) } else { quint32 start = member->firstSourceLocation().begin(); quint32 end = member->lastSourceLocation().end(); str = doc->source().mid(start, end-start); } return str; } struct FindObjectMemberWithLabel : public Visitor { virtual void endVisit(UiObjectDefinition *ast) ; virtual void endVisit(UiObjectBinding *ast) ; QList found; QString cmp; Document::Ptr doc; }; void FindObjectMemberWithLabel::endVisit(UiObjectDefinition* ast) { if (label(ast, doc) == cmp) found.append(ast); } void FindObjectMemberWithLabel::endVisit(UiObjectBinding* ast) { if (label(ast, doc) == cmp) found.append(ast); } struct Map { typedef UiObjectMember*T; QHash way1; QHash way2; void insert(T t1, T t2) { way1.insert(t1,t2); way2.insert(t2,t1); } int count() { return way1.count(); } void operator+=(const Map &other) { way1.unite(other.way1); way2.unite(other.way2); } bool contains(T t1, T t2) { return way1.value(t1) == t2; } }; static QList children(UiObjectMember *ast) { QList ret; if (UiObjectDefinition* foo = cast(ast)) { UiObjectMemberList* list = foo->initializer->members; while (list) { ret.append(list->member); list = list->next; } } else if(UiObjectBinding *foo = cast(ast)) { UiObjectMemberList* list = foo->initializer->members; while (list) { ret.append(list->member); list = list->next; } } else if(UiArrayBinding *foo = cast(ast)) { UiArrayMemberList* list = foo->members; while (list) { ret.append(list->member); list = list->next; } } return ret; } Map MatchFragment(UiObjectMember *x, UiObjectMember *y, const Map &M, Document::Ptr doc1, Document::Ptr doc2) { Map M2; if (M.way1.contains(x)) return M2; if (M.way2.contains(y)) return M2; if(label(x, doc1) != label(y, doc2)) return M2; M2.insert(x, y); QList list1 = children(x); QList list2 = children(y); for (int i = 0; i < list1.count(); i++) { QString l = label(list1[i], doc1); for (int j = 0; j < list2.count(); j++) { if (l != label(list2[j], doc2)) continue; M2 += MatchFragment(list1[i], list2[j], M, doc1, doc2); list2.removeAt(j); break; } } return M2; } Map Mapping(Document::Ptr doc1, Document::Ptr doc2) { Map M; QList todo; todo.append(doc1->qmlProgram()->members->member); while(!todo.isEmpty()) { UiObjectMember *x = todo.takeFirst(); todo += children(x); if (M.way1.contains(x)) continue; //If this is too slow, we could use some sort of indexing FindObjectMemberWithLabel v3; v3.cmp = label(x, doc1); v3.doc = doc2; doc2->qmlProgram()->accept(&v3); Map M2; foreach (UiObjectMember *y, v3.found) { if (M.way2.contains(y)) continue; Map M3 = MatchFragment(x, y, M, doc1, doc2); if (M3.count() > M2.count()) M2 = M3; } M += M2; } return M; } static QString _scriptCode(UiScriptBinding *script, Document::Ptr doc) { if (script) { const int begin = script->statement->firstSourceLocation().begin(); const int end = script->statement->lastSourceLocation().end(); return doc->source().mid(begin, end - begin); } return QString(); } static QString _methodCode(UiSourceElement *source, Document::Ptr doc) { if (source) { if (FunctionDeclaration *declaration = cast(source->sourceElement)) { const int begin = declaration->lbraceToken.begin() + 1; const int end = declaration->rbraceToken.end() - 1; return doc->source().mid(begin, end - begin); } } return QString(); } static QString _propertyName(UiQualifiedId *id) { QString s; for (; id; id = id->next) { if (! id->name) return QString(); s += id->name->asString(); if (id->next) s += QLatin1Char('.'); } return s; } static QString _methodName(UiSourceElement *source) { if (source) { if (FunctionDeclaration *declaration = cast(source->sourceElement)) { return declaration->name->asString(); } } return QString(); } } void Delta::insert(UiObjectMember *member, UiObjectMember *parentMember, const QList &debugReferences, const Document::Ptr &doc) { if (doNotSendChanges) return; if (!member || !parentMember) return; // create new objects if (UiObjectDefinition* uiObjectDef = cast(member)) { unsigned begin = uiObjectDef->firstSourceLocation().begin(); unsigned end = uiObjectDef->lastSourceLocation().end(); QString qmlText = QString(uiObjectDef->firstSourceLocation().startColumn - 1, QLatin1Char(' ')); qmlText += doc->source().midRef(begin, end - begin); QStringList importList; for (UiImportList *it = doc->qmlProgram()->imports; it; it = it->next) { if (!it->import) continue; unsigned importBegin = it->import->firstSourceLocation().begin(); unsigned importEnd = it->import->lastSourceLocation().end(); importList << doc->source().mid(importBegin, importEnd - importBegin); } QString filename = doc->fileName() + QLatin1Char('_') + QString::number(doc->editorRevision()) + QLatin1Char(':') + QString::number(uiObjectDef->firstSourceLocation().startLine-importList.count()); foreach(const QDeclarativeDebugObjectReference &ref, debugReferences) { if (ref.debugId() != -1) { _referenceRefreshRequired = true; ClientProxy::instance()->createQmlObject(qmlText, ref, importList, filename); } } newObjects += member; } } void Delta::update(UiObjectDefinition* oldObject, const QmlJS::Document::Ptr& oldDoc, UiObjectDefinition* newObject, const QmlJS::Document::Ptr& newDoc, const QList< QDeclarativeDebugObjectReference >& debugReferences) { if (doNotSendChanges) return; Q_ASSERT (oldObject && newObject); QSet presentBinding; for (UiObjectMemberList *objectMemberIt = objectMembers(newObject); objectMemberIt; objectMemberIt = objectMemberIt->next) { if (UiScriptBinding *script = cast(objectMemberIt->member)) { bool found = false; const QString property = _propertyName(script->qualifiedId); presentBinding.insert(property); for (UiObjectMemberList *previousObjectMemberIt = Delta::objectMembers(oldObject); previousObjectMemberIt; previousObjectMemberIt = previousObjectMemberIt->next) { if (UiScriptBinding *previousScript = cast(previousObjectMemberIt->member)) { if (compare(script->qualifiedId, previousScript->qualifiedId)) { found = true; const QString scriptCode = _scriptCode(script, newDoc); const QString previousScriptCode = _scriptCode(previousScript, oldDoc); if (scriptCode != previousScriptCode) { foreach (const QDeclarativeDebugObjectReference &ref, debugReferences) { if (ref.debugId() != -1) updateScriptBinding(ref, script, property, scriptCode); } } } } } if (!found) { const QString scriptCode = _scriptCode(script, newDoc); foreach (const QDeclarativeDebugObjectReference &ref, debugReferences) { if (ref.debugId() != -1) updateScriptBinding(ref, script, property, scriptCode); } } } else if (UiSourceElement *uiSource = cast(objectMemberIt->member)) { bool found = false; const QString methodName = _methodName(uiSource); for (UiObjectMemberList *previousObjectMemberIt = objectMembers(oldObject); previousObjectMemberIt; previousObjectMemberIt = previousObjectMemberIt->next) { if (UiSourceElement *previousSource = cast(previousObjectMemberIt->member)) { if (compare(uiSource, previousSource)) { found = true; const QString methodCode = _methodCode(uiSource, newDoc); const QString previousMethodCode = _methodCode(previousSource, oldDoc); if (methodCode != previousMethodCode) { foreach (const QDeclarativeDebugObjectReference &ref, debugReferences) { if (ref.debugId() != -1) updateMethodBody(ref, script, methodName, methodCode); } } } } } if (!found) { const QString methodCode = _methodCode(uiSource, newDoc); foreach (const QDeclarativeDebugObjectReference &ref, debugReferences) { if (ref.debugId() != -1) updateMethodBody(ref, script, methodName, methodCode); } } } } if (doNotSendChanges) return; //reset property that are not present in the new object. for (UiObjectMemberList *previousObjectMemberIt = Delta::objectMembers(oldObject); previousObjectMemberIt; previousObjectMemberIt = previousObjectMemberIt->next) { if (UiScriptBinding *previousScript = cast(previousObjectMemberIt->member)) { const QString property = _propertyName(previousScript->qualifiedId); if (!presentBinding.contains(property)) { foreach (const QDeclarativeDebugObjectReference &ref, debugReferences) { if (ref.debugId() != -1) ClientProxy::instance()->resetBindingForObject(ref.debugId(), property); // ### remove } } } } } void Delta::remove(const QList< QDeclarativeDebugObjectReference >& debugReferences) { foreach (const QDeclarativeDebugObjectReference &ref, debugReferences) { if (ref.debugId() != -1) ClientProxy::instance()->destroyQmlObject(ref.debugId()); // ### remove } } Delta::DebugIdMap Delta::operator()(const Document::Ptr &doc1, const Document::Ptr &doc2, const DebugIdMap &debugIds) { Q_ASSERT(doc1->qmlProgram()); Q_ASSERT(doc2->qmlProgram()); QHash< UiObjectMember*, QList > newDebuggIds; Map M = Mapping(doc1, doc2); _referenceRefreshRequired = false; BuildParentHash parents2; doc2->qmlProgram()->accept(&parents2); BuildParentHash parents1; doc1->qmlProgram()->accept(&parents1); QList todo; todo.append(doc2->qmlProgram()->members->member); //UiObjectMemberList *list = 0; while(!todo.isEmpty()) { UiObjectMember *y = todo.takeFirst(); todo += children(y); if (!cast(y)) continue; if (!M.way2.contains(y)) { UiObjectMember* parent = parents2.parent.value(y); if (!M.way2.contains(parent)) continue; qDebug () << "Delta::operator(): insert " << label(y, doc2) << " to " << label(parent, doc2); insert(y, parent, newDebuggIds.value(parent), doc2); continue; } UiObjectMember *x = M.way2[y]; Q_ASSERT(cast(x)); if (debugIds.contains(x)) { QList< QDeclarativeDebugObjectReference > ids = debugIds[x]; newDebuggIds[y] = ids; update(cast(x), doc1, cast(y), doc2, ids); } //qDebug() << "Delta::operator(): match "<< label(x, doc1) << "with parent " << label(parents1.parent.value(x), doc1) // << " to "<< label(y, doc2) << "with parent " << label(parents2.parent.value(y), doc2); if (!M.contains(parents1.parent.value(x),parents2.parent.value(y))) { qDebug () << "Delta::operator(): move " << label(y, doc2) << " from " << label(parents1.parent.value(x), doc1) << " to " << label(parents2.parent.value(y), doc2) << " ### TODO"; continue; } } todo.append(doc1->qmlProgram()->members->member); while(!todo.isEmpty()) { UiObjectMember *x = todo.takeFirst(); todo += children(x); if (!cast(x)) continue; if (!M.way1.contains(x)) { qDebug () << "Delta::operator(): remove " << label(x, doc1); QList< QDeclarativeDebugObjectReference > ids = debugIds.value(x); if (!ids.isEmpty()) remove(ids); continue; } } return newDebuggIds; } static bool isLiteralValue(ExpressionNode *expr) { if (cast(expr)) return true; else if (cast(expr)) return true; else if (UnaryPlusExpression *plusExpr = cast(expr)) return isLiteralValue(plusExpr->expression); else if (UnaryMinusExpression *minusExpr = cast(expr)) return isLiteralValue(minusExpr->expression); else if (cast(expr)) return true; else if (cast(expr)) return true; else return false; } static inline bool isLiteralValue(UiScriptBinding *script) { if (!script || !script->statement) return false; ExpressionStatement *exprStmt = cast(script->statement); if (exprStmt) return isLiteralValue(exprStmt->expression); else return false; } static inline QString stripQuotes(const QString &str) { if ((str.startsWith(QLatin1Char('"')) && str.endsWith(QLatin1Char('"'))) || (str.startsWith(QLatin1Char('\'')) && str.endsWith(QLatin1Char('\'')))) return str.mid(1, str.length() - 2); return str; } static inline QString deEscape(const QString &value) { QString result = value; result.replace(QLatin1String("\\\\"), QLatin1String("\\")); result.replace(QLatin1String("\\\""), QLatin1String("\"")); result.replace(QLatin1String("\\\t"), QLatin1String("\t")); result.replace(QLatin1String("\\\r"), QLatin1String("\\\r")); result.replace(QLatin1String("\\\n"), QLatin1String("\n")); return result; } static QString cleanExpression(const QString &expression, UiScriptBinding *scriptBinding) { QString trimmedExpression = expression.trimmed(); if (ExpressionStatement *expStatement = cast(scriptBinding->statement)) { if (expStatement->semicolonToken.isValid()) trimmedExpression.chop(1); } return deEscape(stripQuotes(trimmedExpression)); } static QVariant castToLiteral(const QString &expression, UiScriptBinding *scriptBinding) { const QString cleanedValue = cleanExpression(expression, scriptBinding); QVariant castedExpression; ExpressionStatement *expStatement = cast(scriptBinding->statement); switch(expStatement->expression->kind) { case Node::Kind_NumericLiteral: case Node::Kind_UnaryPlusExpression: case Node::Kind_UnaryMinusExpression: castedExpression = QVariant(cleanedValue).toReal(); break; case Node::Kind_StringLiteral: castedExpression = QVariant(cleanedValue).toString(); break; case Node::Kind_TrueLiteral: case Node::Kind_FalseLiteral: castedExpression = QVariant(cleanedValue).toBool(); break; default: castedExpression = cleanedValue; break; } return castedExpression; } void Delta::updateMethodBody(const QDeclarativeDebugObjectReference &objectReference, UiScriptBinding *scriptBinding, const QString &methodName, const QString &methodBody) { Change change; change.script = scriptBinding; change.ref = objectReference; change.isLiteral = false; _changes.append(change); ClientProxy::instance()->setMethodBodyForObject(objectReference.debugId(), methodName, methodBody); // ### remove } void Delta::updateScriptBinding(const QDeclarativeDebugObjectReference &objectReference, UiScriptBinding *scriptBinding, const QString &propertyName, const QString &scriptCode) { if (doNotSendChanges) return; QVariant expr = scriptCode; const bool isLiteral = isLiteralValue(scriptBinding); if (isLiteral) expr = castToLiteral(scriptCode, scriptBinding); Change change; change.script = scriptBinding; change.ref = objectReference; change.isLiteral = isLiteral; _changes.append(change); ClientProxy::instance()->setBindingForObject(objectReference.debugId(), propertyName, expr, isLiteral); // ### remove } bool Delta::compare(UiQualifiedId *id, UiQualifiedId *other) { if (id == other) return true; else if (id && other) { if (id->name && other->name) { if (id->name->asString() == other->name->asString()) return compare(id->next, other->next); } } return false; } bool Delta::compare(UiSourceElement *source, UiSourceElement *other) { if (source == other) return true; else if (source && other) { if (source->sourceElement && other->sourceElement) { FunctionDeclaration *decl = cast(source->sourceElement); FunctionDeclaration *otherDecl = cast(other->sourceElement); if (decl && otherDecl && decl->name && otherDecl->name && decl->name->asString() == otherDecl->name->asString()) { return true; } } } return false; } UiObjectMemberList *Delta::objectMembers(UiObjectMember *object) { if (UiObjectDefinition *def = cast(object)) return def->initializer->members; else if (UiObjectBinding *binding = cast(object)) return binding->initializer->members; return 0; } Document::Ptr Delta::document() const { return _doc; } Document::Ptr Delta::previousDocument() const { return _previousDoc; } QList Delta::changes() const { return _changes; } bool Delta::referenceRefreshRequired() const { return _referenceRefreshRequired; }