diff options
author | Fawzi Mohamed <fawzi.mohamed@qt.io> | 2021-04-13 22:22:01 +0200 |
---|---|---|
committer | Fawzi Mohamed <fawzi@gmx.ch> | 2021-12-07 12:47:34 +0200 |
commit | c630ee67b7de7c3e25034a5efe6d71fcd37f0dac (patch) | |
tree | 9e906ec42d56434f701216de06142873396a64c3 /examples | |
parent | 5ef3ff4b63a3662d709832c0f4f754c61c3a741c (diff) |
qmldom: load and rewrite example
load and rewrite example using the standalone build of the dom and
compiler libraries
Change-Id: Ib7823712aea2164291c21b07956dcec734bc9542
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
Diffstat (limited to 'examples')
-rw-r--r-- | examples/qml/qmldom/CMakeLists.txt | 36 | ||||
-rw-r--r-- | examples/qml/qmldom/qmldomloadeditwrite.cpp | 547 |
2 files changed, 583 insertions, 0 deletions
diff --git a/examples/qml/qmldom/CMakeLists.txt b/examples/qml/qmldom/CMakeLists.txt new file mode 100644 index 0000000000..975f3630bb --- /dev/null +++ b/examples/qml/qmldom/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.18) +project(qmldomloadeditwrite LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/qml/qmldomloadeditwrite") + +find_package(Qt6 COMPONENTS Core Gui Qml Network) + +add_compile_definitions( + QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/../../../tests/auto/qmldom/domdata" +) + +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../../src/qmldom/standalone qmldom) + +qt_add_executable(qmldomloadeditwrite + qmldomloadeditwrite.cpp +) + +target_link_libraries(qmldomloadeditwrite PUBLIC + Qt::Core + Qt::Qml + qmldomlib +) + +install(TARGETS qmldomloadeditwrite + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/qml/qmldom/qmldomloadeditwrite.cpp b/examples/qml/qmldom/qmldomloadeditwrite.cpp new file mode 100644 index 0000000000..074320d598 --- /dev/null +++ b/examples/qml/qmldom/qmldomloadeditwrite.cpp @@ -0,0 +1,547 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +// common declarations +#include "qmldom/qqmldomitem_p.h" +// comparisons of two DomItems +#include "qmldom/qqmldomcompare_p.h" +// field filters to compare only selected fields (ignore for example location changes) +#include "qmldom/qqmldomfieldfilter_p.h" +// needed to edit and cast to concrete type (PropertyDefinition, ScriptExpression,...) +#include "qmldom/qqmldomelements_p.h" +// cast of the top level items (DomEnvironments,...) +#include "qmldom/qqmldomtop_p.h" + +#include <QtTest/QtTest> +#include <QCborValue> +#include <QDebug> +#include <QLatin1String> +#include <QLatin1Char> +#include <QLibraryInfo> +#include <QDir> + +#include <memory> + +// everything is in the QQmlJS::Dom namespace +using namespace QQmlJS::Dom; + +int main() +{ + QString baseDir = QLatin1String(QT_QMLTEST_DATADIR) + QLatin1String("/reformatter"); + QStringList qmltypeDirs = + QStringList({ baseDir, QLibraryInfo::path(QLibraryInfo::Qml2ImportsPath) }); + + qDebug() << "Creating an environment loading qml from the directories" << qmltypeDirs; + qDebug() << "single threaded, no dependencies"; + DomItem env = + DomEnvironment::create(qmltypeDirs, + QQmlJS::Dom::DomEnvironment::Option::SingleThreaded + | QQmlJS::Dom::DomEnvironment::Option::NoDependencies); + + QString testFilePath = baseDir + QLatin1String("/file1.qml"); + DomItem tFile; // place where to store the loaded file + // env.loadBuiltins(); + + qDebug() << "loading the file" << testFilePath; + env.loadFile( + testFilePath, QString(), + [&tFile](Path, const DomItem &, const DomItem &newIt) { + tFile = newIt; // callback called when everything is loaded that receives the loaded + // external file pair (path, oldValue, newValue) + }, + LoadOption::DefaultLoad); + + // trigger the load + env.loadPendingDependencies(); + + // # Read only API: DomItem is a generic pointer for read only access to Dom Itmes :) + { + // ## declarative json like API + DomItem qmlFile = tFile.field(Fields::currentItem); + DomItem imports = qmlFile.field(Fields::imports); + DomItem qmlObj = qmlFile.field(Fields::components) + .key(QString()) + .index(0) + .field(Fields::objects) + .index(0); + + // ### Dump + // any DomItem can be dumped + qDebug() << "writing to QDebug dumps that element:" << imports; + // often the dump is too verbose, and one might want it to a separate file + QString dumpFilePath = + QDir(QDir::tempPath()) + .filePath(QFileInfo(testFilePath).baseName() + QLatin1String(".dump.json")); + qmlFile.dump(dumpFilePath, FieldFilter::defaultFilter()); + qDebug() << "dumped file to" << dumpFilePath; + + // ### Paths + qDebug() << "To identify a DomItem a canonical path can be used:" + << imports.canonicalPath(); + // a path can be converted to/from strings + QString pString = imports.canonicalPath().toString(); + Path importsPath = Path::fromString(pString); + // and loaded again using the .path(somePath) method + DomItem imports2 = env.path(importsPath); + Q_ASSERT(imports == imports2); + // the canonical path is absolute, but you can have relative paths + Path first = Path::Index(0); + DomItem firstImport = imports.path(first); + // an existing path can also be extended + Path firstImportPath = importsPath.index(0); + Q_ASSERT(firstImportPath == firstImport.canonicalPath()); + // the normal elements of a path are index, key, field + // Uppercase static method creates one, lowercase appends to an existing path. + Path mainComponentPath = Path::Field(Fields::components).key("").index(0); + DomItem mainComponent = qmlFile.path(mainComponentPath); + // DomItems have the same methods to access their elements + DomItem mainComponent2 = qmlFile.field(Fields::components).key("").index(0); + // two other special ements are root (root element for absolute paths) + Path topPath = Path::Root(PathRoot::Top); + Q_ASSERT(topPath == importsPath[0]); + // the current element performs an operation (tipically a lookup or iteration) at the + // current path location (not handled here) + Path lookupPath = Path::Current(PathCurrent::Lookup); + + // there are various visit methods to iterate/visit DomItems in particular visitTree + // which is quite flexible. + // They normally use callbacks that can return false to stop the iteration. + // Still often the DomKind specific for loop presentated later are clearer and more + // convenient + { + QDebug dbg = qDebug().noquote().nospace(); + imports.visitTree( + Path(), + [&dbg](Path p, const DomItem &el, bool adopted) { + dbg << QStringLiteral(u" ").repeated(p.length()) << "*" << p.last() << " " + << domKindToString(el.domKind()) << "(" << el.internalKindStr() + << ")\n"; + // returning false here stops the whole iteration + return true; + }, + VisitOption::Default, // we want a recursive visit visiting also the top and + // adopted + [&dbg](Path p, const DomItem &, bool canonicalChild) { + // returning false here skips that branch + if (!canonicalChild) { + dbg << QStringLiteral(u" ").repeated(p.length()) << "+" << p.last() + << " (adopted, will not recurse)\n"; + } else if (p && p.headIndex(0) % 2 == 1) { + dbg << QStringLiteral(u" ").repeated(p.length()) << "-" << p.last() + << " *recursive visit skipped*\n"; + return false; // we skip odd entries in lists; + } else { + dbg << QStringLiteral(u" ").repeated(p.length()) << "+" << p.last() + << "\n"; + } + return true; + }, + [&dbg](Path p, const DomItem &, bool) { + dbg << QStringLiteral(u" ").repeated(p.length()) << "=" << p.last() << "\n"; + return true; + }); + } + + // ### DomKind + // any DomItem belongs to 5 basic types + + // 1. Object (a C++ object) + Q_ASSERT(qmlFile.domKind() == DomKind::Object); + // The underlying type of the c++ object can be found with .internalKind() + Q_ASSERT(qmlFile.internalKind() == DomType::QmlFile); + // .initernalKindStr() is a convenience string version of it + Q_ASSERT(qmlFile.internalKindStr() == u"QmlFile"); + // the object attributes (fields) can be reached using .field(u"filedName") + // normally one should not use a string, but the Fields:: constant + DomItem qmlFile2 = tFile.field(Fields::currentItem); + // all the available fields can be listed via fields() + qDebug() << "The" << qmlObj.internalKindStr() << "at" << qmlObj.canonicalPath() + << "has the following fields:" << qmlObj.fields(); + // we can access the underlying C++ object with as<> + if (const QmlFile *qmlFilePtr = qmlFile.as<QmlFile>()) + qDebug() << "The QmlFile lives at the address" << qmlFilePtr; + // We can get the shared pointer of the owner type (which for the file is the QmlFile itself + if (std::shared_ptr<QmlFile> qmlFilePtr = qmlFile.ownerAs<QmlFile>()) + qDebug() << "QmlFile uses shared pointers as ownership method, the underlying address " + "is the same" + << qmlFilePtr.get(); + + // 2. a (Cbor-) Value, i.e a string, number,... + DomItem fPath = qmlFile.field(Fields::canonicalFilePath); + Q_ASSERT(fPath.domKind() == DomKind::Value); + // the Cbor representation of a value can be extracted with .value(), and in this case we + // can then call toString + qDebug() << "The filePath DomItem is " << fPath << " and it still 'knows' its path " + << fPath.canonicalPath() << " but can have it also as value:" << fPath.value() + << "or even better as string." << fPath.value().toString(QLatin1String("*none*")); + // a DomItem might have a valid value() even if it is not of type DomKind::Value, indeed + // CBor maps and lists are mapped to DomKind::Map and DomKind::List, and can be traversed + // thought that but also have a valid value(). + + // 3. a list + Q_ASSERT(imports.domKind() == DomKind::List); + // the number of elements can be sound with .indexes() and with .index(n) we access each + // element + qDebug() << "We have " << imports.indexes() << " imports, and the first is " + << imports.index(0); + // If we want to just loop on the elements .values() is the most convenient way + // technically values *always* works even for objects and maps, iterating on the values + for (DomItem import : imports.values()) { + if (const Import *importPtr = import.as<Import>()) { + if (importPtr->implicit) + qDebug() << importPtr->uri << importPtr->version.stringValue(); + } + } + + // 4. a map + DomItem bindings = qmlObj.field(Fields::bindings); + Q_ASSERT(bindings.domKind() == DomKind::Map); + // The keys of the map can be reached either with .keys() or .sortedKeys(), each element + // with .key(k) + qDebug() << "bindings"; + for (QString k : bindings.sortedKeys()) { + for (DomItem b : bindings.key(k).values()) { + qDebug() << k << ":" << b; + } + } + + // 5 The empty element + DomItem empty; + Q_ASSERT(empty.domKind() == DomKind::Empty); + // The empty element is the only DomItem that casted to bool returns false, so checking for + // it can be just an implicit cast to bool + Q_ASSERT(bindings && !empty); + // the empty element supports all the previus operations so that one can traverse a non + // existing path without checking at every element, but only check the result + DomItem nonExisting = qmlFile.field(u"no-existing").key(u"a").index(0); + Q_ASSERT(!nonExisting); + + // the index operator [] can be used instead of .index/.key/.field, it might be slightly + // less efficient but works + + // find type + // access type + + // ### write out + // it is possible to write out a qmlFile (actually also parts of it), which will + // automatically reformat it + QString reformattedFilePath = + QDir(QDir::tempPath()) + .filePath(QFileInfo(testFilePath).baseName() + QLatin1String(".qml")); + DomItem newFile = qmlFile.writeOut(reformattedFilePath); + qDebug() << "reformatted written at " << reformattedFilePath; + + // ## Jumping around + // ### Generic Methods + // from a DomItem you do no have just deeper in the tree, you can also go up the hierarch + // toward the root .container() just goes up one step in the canonicalPath of the object + Q_ASSERT(imports == firstImport.container()); + // .containingObject() goes up to the containing DomKind::Object, skipping over all Maps and + // Lists + Q_ASSERT(qmlFile == firstImport.containingObject()); + // .owner() returns the shared pointer based "owner" object, qmlFile and ScriptExpression + // are owningItems + Q_ASSERT(qmlFile == bindings.owner()); + // .top() goes to the top of the tree, i.e the environment (or the universe) + Q_ASSERT(env == bindings.top()); + // environment is normally the same as top, but making sure it is a actually a + // DomEnvironment + Q_ASSERT(env = bindings.environment()); + // the universe is a cache of loaded files which for each file keeps two versions: the + // latest and the latest valid it can be reached with .universe(), from the universe you + // cannot get back to the environment. + Q_ASSERT(env.universe().internalKind() == DomType::DomUniverse); + + // ## QML Oriented Methods + // The Dom model is not for generic json-like structures, so there are methods tailored for + // Qml and its structure + // The methods can succeed if there is a clearly defined unique result. + // sometime there is an obivious, but not necessarily unique choice (tipically going up the + // hierarchy), for example given a qml file the obvious choice for a component is the root + // component, but the file might contain other inline components, and for an object with + // different version exposed (C++ property versioning) the latest version is the natural + // choice, but other might be available. In these case passing GoTo::MostLikely as argument + // makes the method to this obivious choice (or possibly even only choice if no other + // versions/components are actually defined), instead of refusing any potentially ambiguous + // situation and returning the empty element. + + // .fileObject() goes to the object representing the whole file + // (from either the external object returned by load or from inside the file) + DomItem fileObject = tFile.fileObject(); + DomItem fileObject2 = imports.fileObject(); + Q_ASSERT(fileObject == fileObject2 && fileObject.internalKind() == DomType::QmlFile); + // .component() goes to the component object. + Q_ASSERT(qmlObj.component() == qmlFile.component(GoTo::MostLikely)); + // .pragmas gives access to the pragmas of the current component + Q_ASSERT(qmlFile.pragmas() == qmlFile.field(Fields::pragmas)); + + // QmlObject + // QmlObject if the main to represent the type information (methods, bindings, + // properties,...) of qml. Please note that QmlObject -> component operation is potentially + // lossy, when multiple version are exposed, so we represent a type through its root object, + // not through a component. + + // .qmlObject() goes to the current QmlObject + Q_ASSERT(qmlObj == bindings.qmlObject()); + + // Given the centrality of QmlObject several of its attributes have convenience methods + // to access them: + + // .children() makes subObjects contained inside a QmlObject accessible + // note that it is possible to add objects also by directly binding the children or data + // attribute, those children are not listed here, this accesses only those listed inside + // the QmlObject + Q_ASSERT(qmlObj.children() == qmlObj.field(Fields::children)); + DomItem subObj0 = qmlObj.children().index(0); + // .child(<i>) is a shortcut for .children.index(<i>) + Q_ASSERT(subObj0 == qmlObj.child(0)); + // rootQmlObject goes to the root qmlObject (unless one reaches an empty element) + Q_ASSERT(!subObj0 || subObj0.rootQmlObject() == qmlObj); + // .bindings() returns the bindings defined in the current object + Q_ASSERT(bindings == qmlObj.bindings()); + DomItem mCompObj = qmlObj.child(0) + .child(0) + .bindings() + .key(u"delegate") + .index(0) + .field(Fields::value) + .child(1); + // .methods() gives methods definitions and signals + DomItem methods = mCompObj.methods(); + qDebug() << "mCompObj methods:"; + for (QString methodName : methods.sortedKeys()) { + for (DomItem method : methods.key(methodName).values()) { + if (const MethodInfo *methodPtr = method.as<MethodInfo>()) { + Q_ASSERT(methodName == methodPtr->name); + qDebug() << " " << methodPtr->name << methodPtr->methodType; + } + } + } + qDebug() << "mCompObj propertyDefs:"; + // .propertyDefs() returns the properties defined in the current object + DomItem pDefs = mCompObj.propertyDefs(); + for (QString pDefName : pDefs.sortedKeys()) { + for (DomItem pDef : pDefs.key(pDefName).values()) { + if (const PropertyDefinition *pDefPtr = pDef.as<PropertyDefinition>()) { + Q_ASSERT(pDefName == pDefPtr->name); + qDebug() << " " << pDefPtr->name << pDefPtr->typeName; + } + } + } + // binding and property definitions are about the ones defined in the current object + // often one is interested also to the inherited properties. + // Here PropertyInfo helps, it list all the definitions and bindings for a given property + // in the inheritance order (local definitions, parent definitions, parent parent + // definitions,...) + // .propertyInfos() gives access in the usual way (through a DomItem) + DomItem propertyInfos = mCompObj.propertyInfos(); + // .propertyInfoWithName(<name>) directly accesses one + PropertyInfo pInfo = mCompObj.propertyInfoWithName(QStringLiteral(u"a")); + qDebug() << "bindings" << pInfo.bindings; + // .propertyInfoNames() gives the names of the properties + Q_ASSERT(propertyInfos.keys() == mCompObj.propertyInfoNames()); + + // .globalScope() goes to the globa scope object + Q_ASSERT(qmlObj.globalScope().internalKind() == DomType::GlobalScope); + // and scope to the containing scope + Q_ASSERT(bindings.scope() == qmlObj); + } + // mutate & edit + { + // DomItem handles read-only access, but if one wants to change something it cannot be used. + // MutableDomItem can be initialized with a DomItem, and provides also the methods to modify + // the item. It keeps the OwningItem and the path to the current item. + // Mutability can invalidate pointers to non owning items (and thus DomItem). + // For this reason one should not modify something that other code can have a DomItem + // pointer to, the best practice is to make shared object immutable and never change them. + // One should modify only a copy that is used only by a single thread, and + // do not shared untils all modifications are done. + // A MutableItem stays valid (or becomes Empty), but stays safe to use + // + // Assuming one guarantees that editing is ok, doing it in practice is just about using + // MutableDomItem instead of DomItem + // It is possible to simply initialize a mutable item with a DomItem + DomItem origFile = tFile.fileObject(); + MutableDomItem myFile0(origFile); + // Normally it is better to have a separate environment. Is possible to avoid re-reading + // the files already read by sharing the Universe between two environments. + // But normally it is better and just as safe to work on a copy, so that one can be sure + // that no DomItem is kept by other code gets invalidated. The .makeCopy creates a deep + // copy, and by default (DomItem::CopyOption::EnvConnected) creates an environment which to + // takes all non local elements from the current environment (its parent environment) but + // replaces the file object with the copy. When finished one can replace the file object of + // the parent with the new one using .commitToBase(). + MutableDomItem myFile = origFile.makeCopy(); + Q_ASSERT(myFile.ownerAs<QmlFile>() + && myFile.ownerAs<QmlFile>() != myFile0.ownerAs<QmlFile>()); + Q_ASSERT(myFile.environment().ownerAs<DomEnvironment>() + && myFile.environment().ownerAs<DomEnvironment>() + != myFile0.environment().ownerAs<DomEnvironment>()); + // we can check that the two files are really identical (.item() give back the DomItem of + // a MutableDomItem + Q_ASSERT(domCompareStrList(origFile, myFile, FieldFilter::compareFilter()).isEmpty()); + // MutableDomItem has the same methods as DomItem + MutableDomItem qmlObj = myFile.qmlObject(GoTo::MostLikely); + MutableDomItem qmlObj2 = myFile.field(Fields::components) + .key(QString()) + .index(0) + .field(Fields::objects) + .index(0); + Q_ASSERT(qmlObj && qmlObj == qmlObj2); + qDebug() << "mutable qmlObj has canonicalPath " << qmlObj.canonicalPath(); + // but it adds methods to add + // * new PropertyDefinitions + PropertyDefinition b; + b.name = QLatin1String("xx"); + b.typeName = QLatin1String("int"); + // if we make t true we also have to give a value... + MutableDomItem addedPDef = qmlObj.addPropertyDef(b); + qDebug() << "added property definition at:" << addedPDef.pathFromOwner(); + // * new bindings + MutableDomItem addedBinding0 = qmlObj.addBinding( + Binding("height", + std::shared_ptr<ScriptExpression>(new ScriptExpression( + QStringLiteral(u"243"), + ScriptExpression::ExpressionType::BindingExpression)))); + // by default addBinding, addPropertyDef and addMethod have the AddOption::Override + // to make it more difficult to create invalid documents, so that only the + // following binding remains (where we use the convenience constructor that constucts + // the ScriptExpression internally + MutableDomItem addedBinding = qmlObj.addBinding(Binding("height", QStringLiteral(u"242"))); + qDebug() << "added binding at:" << addedBinding.pathFromOwner(); + // * new methods + MethodInfo mInfo; + mInfo.name = QLatin1String("foo2"); + MethodParameter param; + param.name = QLatin1String("x"); + mInfo.parameters.append(param); + mInfo.setCode(QLatin1String("return 4*10+2 - x")); + // we can change the added binding + addedBinding.setCode(QLatin1String("245")); + MutableDomItem addedMethod = qmlObj.addMethod(mInfo); + qDebug() << "added method at:" << addedMethod.pathFromOwner(); + // * new QmlObjects + QmlObject subObj; + subObj.setName(QLatin1String("Item")); + MutableDomItem addedSubObj = qmlObj.addChild(subObj); + qDebug() << "added subObject at:" << addedMethod.pathFromOwner(); + // It is possible to modify the content of objects, using the mutableAs method + if (PropertyDefinition *addedPDefPtr = addedPDef.mutableAs<PropertyDefinition>()) { + addedPDefPtr->isRequired = true; + } + MutableDomItem firstChild = qmlObj.child(0); + qDebug() << "firstChild:" << firstChild; + // It is possible remove objects + if (QmlObject *qmlObjPtr = qmlObj.mutableAs<QmlObject>()) { + QList<QmlObject> children = qmlObjPtr->children(); + children.removeAt(0); + qmlObjPtr->setChildren(children); + } + // But as MutableDomItem does not keep the identity, just the same position, the addedSubObj + // becomes invalid (and firstChild changes) + qDebug() << "after removal firstChild:" << firstChild; + qDebug() << "addedSubObj becomes invalid:" << addedSubObj; + qDebug() << "But the last object is the added one:" + << qmlObj.child(qmlObj.children().indexes() - 1); + + // now origFile are different + Q_ASSERT(!domCompareStrList(origFile, myFile, FieldFilter::compareFilter()).isEmpty()); + // and we can look at the places where they differ + qDebug().noquote().nospace() + << "Edits introduced the following diffs (ignoring file locations" + << " and thus whitespace/reformatting changes):\n" + << domCompareStrList(origFile, myFile, FieldFilter::noLocationFilter(), + DomCompareStrList::AllDiffs) + .join(QString()); + + QString reformattedFilePath = + QDir(QDir::tempPath()) + .filePath(QStringLiteral(u"edited") + QFileInfo(testFilePath).baseName() + + QLatin1String(".qml")); + MutableDomItem reformattedEditedFile = myFile.writeOut(reformattedFilePath); + // the reformatted edited file might be different from the edited file + // but the differences are just in file location/formatting + Q_ASSERT(domCompareStrList(myFile, reformattedEditedFile, FieldFilter::noLocationFilter()) + .isEmpty()); + + qDebug() << "The edited file was written at " << reformattedFilePath; + QString dumpFilePath = + QDir(QDir::tempPath()) + .filePath(QStringLiteral(u"edited0") + QFileInfo(testFilePath).baseName() + + QLatin1String(".dump.json")); + myFile.dump(dumpFilePath); + qDebug() << "The non reformatted edited file was dumped at " << dumpFilePath; + QString reformattedDumpFilePath = + QDir(QDir::tempPath()) + .filePath(QStringLiteral(u"edited") + QFileInfo(testFilePath).baseName() + + QLatin1String(".dump.json")); + reformattedEditedFile.dump(reformattedDumpFilePath); + qDebug() << "The edited file was dumped at " << reformattedDumpFilePath; + // The top environment still contains the original loaded file + Q_ASSERT(origFile.ownerAs<QmlFile>() != reformattedEditedFile.ownerAs<QmlFile>()); + Q_ASSERT(tFile.fileObject().refreshed().ownerAs<QmlFile>()); + Q_ASSERT(tFile.fileObject().refreshed().ownerAs<QmlFile>() == origFile.ownerAs<QmlFile>()); + Q_ASSERT(tFile.fileObject().ownerAs<QmlFile>() == origFile.ownerAs<QmlFile>()); + Q_ASSERT(tFile.fileObject().refreshed().ownerAs<QmlFile>() + != reformattedEditedFile.ownerAs<QmlFile>()); + // we can commit the reformatted file + if (!reformattedEditedFile.commitToBase()) { + qWarning() << "No reformatted file to commit"; + } + // myFile might not be the same (If and updated check is requested, not the case here) + if (myFile.ownerAs<QmlFile>() != reformattedEditedFile.ownerAs<QmlFile>() + && !myFile.commitToBase()) { + qWarning() << "Could not commit edited file"; + } + // but refreshing it (looking up its canonical path) we always find the updated file + Q_ASSERT(myFile.refreshed().ownerAs<QmlFile>() == reformattedEditedFile.ownerAs<QmlFile>()); + Q_ASSERT(tFile.fileObject().refreshed().ownerAs<QmlFile>() + == reformattedEditedFile.ownerAs<QmlFile>()); + } +} |