diff options
Diffstat (limited to 'src/qmldom/qqmldomitem.cpp')
-rw-r--r-- | src/qmldom/qqmldomitem.cpp | 3642 |
1 files changed, 3642 insertions, 0 deletions
diff --git a/src/qmldom/qqmldomitem.cpp b/src/qmldom/qqmldomitem.cpp new file mode 100644 index 0000000000..8e4f8acd40 --- /dev/null +++ b/src/qmldom/qqmldomitem.cpp @@ -0,0 +1,3642 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qqmldomattachedinfo_p.h" +#include "qqmldomconstants_p.h" +#include "qqmldomitem_p.h" +#include "qqmldompath_p.h" +#include "qqmldomtop_p.h" +#include "qqmldomelements_p.h" +#include "qqmldomexternalitems_p.h" +#include "qqmldommock_p.h" +#include "qqmldomastdumper_p.h" +#include "qqmldomoutwriter_p.h" +#include "qqmldomfilewriter_p.h" +#include "qqmldomfieldfilter_p.h" +#include "qqmldomcompare_p.h" +#include "qqmldomastdumper_p.h" +#include "qqmldomlinewriter_p.h" +#include "qqmldom_utils_p.h" +#include "qqmldomscriptelements_p.h" + +#include <QtQml/private/qqmljslexer_p.h> +#include <QtQml/private/qqmljsparser_p.h> +#include <QtQml/private/qqmljsengine_p.h> +#include <QtQml/private/qqmljsastvisitor_p.h> +#include <QtQml/private/qqmljsast_p.h> + +#include <QtCore/QCborArray> +#include <QtCore/QCborMap> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QJsonDocument> +#include <QtCore/QJsonValue> +#include <QtCore/QMutexLocker> +#include <QtCore/QPair> +#include <QtCore/QRegularExpression> +#include <QtCore/QScopeGuard> +#include <QtCore/QtGlobal> +#include <QtCore/QTimeZone> +#include <optional> +#include <type_traits> + +QT_BEGIN_NAMESPACE + +namespace QQmlJS { +namespace Dom { + +Q_LOGGING_CATEGORY(writeOutLog, "qt.qmldom.writeOut", QtWarningMsg); +Q_STATIC_LOGGING_CATEGORY(refLog, "qt.qmldom.ref", QtWarningMsg); + +template<class... TypeList> +struct CheckDomElementT; + +template<class... Ts> +struct CheckDomElementT<std::variant<Ts...>> : std::conjunction<IsInlineDom<Ts>...> +{ +}; + +/*! + \internal + \class QQmljs::Dom::ElementT + + \brief A variant that contains all the Dom elements that an DomItem can contain. + + Types in this variant are divided in two categories: normal Dom elements and internal Dom + elements. + The first ones are inheriting directly or indirectly from DomBase, and are the usual elements + that a DomItem can wrap around, like a QmlFile or an QmlObject. They should all appear in + ElementT as pointers, e.g. QmlFile*. + The internal Dom elements are a little bit special. They appear in ElementT without pointer, do + not inherit from DomBase \b{but} should behave like a smart DomBase-pointer. That is, they should + dereference as if they were a DomBase* pointing to a normal DomElement by implementing + operator->() and operator*(). + Adding types here that are neither inheriting from DomBase nor implementing a smartpointer to + DomBase will throw compilation errors in the std::visit()-calls on this type. +*/ +static_assert(CheckDomElementT<ElementT>::value, + "Types in ElementT must either be a pointer to a class inheriting " + "from DomBase or (for internal Dom structures) implement a smart " + "pointer pointing to a class inheriting from DomBase"); + +using std::shared_ptr; +/*! +\internal +\class QQmljs::Dom::DomBase + +\brief Abstract class common to all elements of the Qml code model + +DomBase represents the base class common to all objects exposed by the Dom Model +through a DomItem. +Single inheritence is mandatory for the inline items (Empty, Map, List, ConstantData, Reference), +so that the base pointer of the object can be used a base pointer of the superclass directly. + +The subclass *must* have a +\code + constexpr static Kind kindValue +\endcode +entry with its kind to enable casting usng the DomItem::as DomItem::ownerAs templates. + +The minimal overload set to be usable consists of following methods: +\list +\li \c{kind()} returns the kind of the current element: +\code + Kind kind() const override { return kindValue; } +\endcode + +\li \c{pathFromOwner()} returns the path from the owner to the current element +\code + Path pathFromOwner(const DomItem &self) const override; +\endcode + +\li \c{canonicalPath()} returns the path +\code + Path canonicalPath(const DomItem &self) const override; +\endcode + +\li \c{iterateDirectSubpaths} iterates the *direct* subpaths/children and returns false if a quick +end was requested: +\code +bool iterateDirectSubpaths(const DomItem &self, function_ref<bool(Path, DomItem)>) const = 0; +\endcode + +\endlist + +But you probably want to subclass either \c DomElement or \c OwningItem for your element. \c +DomElement stores its \c pathFromOwner, and computes the \c canonicalPath from it and its owner. \c +OwningItem is the unit for updates to the Dom model, exposed changes always change at least one \c +OwningItem. They have their lifetime handled with \c shared_ptr and own (i.e. are responsible of +freeing) other items in them. + +\sa QQml::Dom::DomItem, QQml::Dom::DomElement, QQml::Dom::OwningItem +*/ + +QMap<DomType,QString> domTypeToStringMap() +{ + static QMap<DomType,QString> map = [](){ + QMetaEnum metaEnum = QMetaEnum::fromType<DomType>(); + QMap<DomType,QString> res; + for (int i = 0; i < metaEnum.keyCount(); ++ i) { + res[DomType(metaEnum.value(i))] = QString::fromUtf8(metaEnum.key(i)); + } + return res; + }(); + return map; +} + +QString domTypeToString(DomType k) +{ + QString res = domTypeToStringMap().value(k); + if (res.isEmpty()) + return QString::number(int(k)); + else + return res; +} + +QMap<DomKind, QString> domKindToStringMap() +{ + static QMap<DomKind, QString> map = []() { + QMetaEnum metaEnum = QMetaEnum::fromType<DomKind>(); + QMap<DomKind, QString> res; + for (int i = 0; i < metaEnum.keyCount(); ++i) { + res[DomKind(metaEnum.value(i))] = QString::fromUtf8(metaEnum.key(i)); + } + return res; + }(); + return map; +} + +QString domKindToString(DomKind k) +{ + return domKindToStringMap().value(k, QString::number(int(k))); +} + +bool domTypeIsExternalItem(DomType k) +{ + switch (k) { + case DomType::QmlDirectory: + case DomType::JsFile: + case DomType::QmlFile: + case DomType::QmltypesFile: + case DomType::GlobalScope: + return true; + default: + return false; + } +} + +bool domTypeIsTopItem(DomType k) +{ + switch (k) { + case DomType::DomEnvironment: + case DomType::DomUniverse: + return true; + default: + return false; + } + +} + +bool domTypeIsContainer(DomType k) +{ + switch (k) { + case DomType::Map: + case DomType::List: + case DomType::ListP: + return true; + default: + return false; + } +} + +bool domTypeIsScope(DomType k) +{ + switch (k) { + case DomType::QmlObject: // prop, methods,... + case DomType::ScriptExpression: // Js lexical scope + case DomType::QmlComponent: // (ids, enums -> qmlObj) + case DomType::QmlFile: // (components ->importScope) + case DomType::MethodInfo: // method arguments + case DomType::ImportScope: // (types, qualifiedImports) + case DomType::GlobalComponent: // global scope (enums -> qmlObj) + case DomType::JsResource: // js resurce (enums -> qmlObj) + case DomType::QmltypesComponent: // qmltypes component (enums -> qmlObj) + return true; + default: + return false; + } +} + +QString DomBase::canonicalFilePath(const DomItem &self) const +{ + auto parent = containingObject(self); + if (parent) + return parent.canonicalFilePath(); + return QString(); +} + +void DomBase::writeOut(const DomItem &self, OutWriter &) const +{ + qCWarning(writeOutLog) << "Ignoring unsupported writeOut for " << domTypeToString(kind()) << ":" + << self.canonicalPath(); +} + +ConstantData::ConstantData(const Path &pathFromOwner, const QCborValue &value, Options options) + : DomElement(pathFromOwner), m_value(value), m_options(options) +{} + +bool ConstantData::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const +{ + static QHash<QString, QString> knownFields; + static QBasicMutex m; + auto toField = [](const QString &f) -> QStringView { + QMutexLocker l(&m); + if (!knownFields.contains(f)) + knownFields[f] = f; + return knownFields[f]; + }; + if (m_value.isMap()) { + QCborMap map = m_value.toMap(); + auto it = map.cbegin(); + auto end = map.cend(); + while (it != end) { + QString key = it.key().toString(); + PathEls::PathComponent comp; + switch (m_options) { + case ConstantData::Options::MapIsMap: + comp = PathEls::Key(key); + break; + case ConstantData::Options::FirstMapIsFields: + comp = PathEls::Field(toField(key)); + break; + } + auto val = it.value(); + if (!self.dvValue(visitor, comp, val)) + return false; + ++it; + } + return true; + } else if (m_value.isArray()){ + QCborArray array = m_value.toArray(); + auto it = array.cbegin(); + auto end = array.cend(); + index_type i = 0; + while (it != end) { + if (!self.dvValue(visitor, PathEls::Index(i++), *it++)) + return false; + } + return true; + } else { + return true; + } +} + +quintptr ConstantData::id() const +{ + return quintptr(0); +} + +DomKind ConstantData::domKind() const +{ + if (m_value.isMap()) { + switch (m_options) { + case ConstantData::Options::MapIsMap: + return DomKind::Map; + case ConstantData::Options::FirstMapIsFields: + return DomKind::Object; + } + } + if (m_value.isArray()) + return DomKind::List; + return DomKind::Value; +} +/*! +\internal +\class QQmlJS::Dom::DomItem + +\brief A value type that references any element of the Dom. + +This class is the central element in the Dom, it is how any element can be identfied in a uniform +way, and provides the API to explore the Dom, and Path based operations. + +The DomItem (unless it is Empty) keeps a pointer to the element, and a shared pointer to its owner +and to the DomEnvironment or DomUniverse that contains them. This means that: +\list +\li A DomItem always has some context: you can get the canonicalPath(), go up along it with + containingObject() and container(). +\li the indexing operator [], or the path(), field(), key() and index() methods (along with their + fields(), keys(), and indexes() contreparts) let one visit the contents of the current element. +\li visitTree can be used to visit all subEments, if preferred on the top of it a visitor + pattern can also be used. +\li If element specific attributes are wanted the two template casting as and ownerAs allow safe +casting of the DomItem to a specific concrete type (cast to superclasses is not supported). \li +Multithreading does not create issues, because even if an update replacing an OwningItem takes place +the DomItem keeps a shared_ptr to the current owner as long as you use it \li Some elements (Empty, +List, Map, ConstantData, Reference) might be inline, meaning that they are generated on the fly, +wrapping data of the original object. \endlist + +One of the goals of the DomItem is to allow one to use real typed objects, as one is used to in C++, +and also let one use modern C++ patterns, meaning container that contain the actual object (without +pointer indirection). +Exposed OwningItems are basically immutable, but during construction, objects can be modified. +This will typically happen from a single thread, so there aren't locking issues, but pointers to +inner elements might become invalid. +In this case the use of the MutableDomItem is required. +It does not keep any pointers to internal elements, but rather the path to them, and it resolves +it every time it needs. +*/ + +FileToLoad::FileToLoad(const std::weak_ptr<DomEnvironment> &environment, + const QString &canonicalPath, const QString &logicalPath, + const std::optional<InMemoryContents> &content) + : m_environment(environment), + m_canonicalPath(canonicalPath), + m_logicalPath(logicalPath), + m_content(content) +{ +} + +FileToLoad FileToLoad::fromMemory(const std::weak_ptr<DomEnvironment> &environment, + const QString &path, const QString &code) +{ + const QString canonicalPath = QFileInfo(path).canonicalFilePath(); + return { + environment, + canonicalPath, + path, + InMemoryContents{ code }, + }; +} + +FileToLoad FileToLoad::fromFileSystem(const std::weak_ptr<DomEnvironment> &environment, + const QString &path) +{ + // make the path canonical so the file content can be loaded from it later + const QString canonicalPath = QFileInfo(path).canonicalFilePath(); + return { + environment, + canonicalPath, + path, + std::nullopt, + }; +} + +ErrorGroup DomItem::domErrorGroup = NewErrorGroup("Dom"); +DomItem DomItem::empty = DomItem(); + +ErrorGroups DomItem::myErrors() +{ + static ErrorGroups res = {{domErrorGroup}}; + return res; +} + +ErrorGroups DomItem::myResolveErrors() +{ + static ErrorGroups res = {{domErrorGroup, NewErrorGroup("Resolve")}}; + return res; +} + +Path DomItem::canonicalPath() const +{ + Path res = visitEl([this](auto &&el) { return el->canonicalPath(*this); }); + if (!(!res || res.headKind() == Path::Kind::Root)) { + qCWarning(domLog) << "non anchored canonical path:" << res.toString(); + Q_ASSERT(false); + } + return res; + +} + +DomItem DomItem::containingObject() const +{ + return visitEl([this](auto &&el) { return el->containingObject(*this); }); +} + +/*! + \internal + \brief Returns the QmlObject that this belongs to. + + qmlObject() might also return the object of a component if GoTo:MostLikely is used. + */ +DomItem DomItem::qmlObject(GoTo options, FilterUpOptions filterOptions) const +{ + if (DomItem res = filterUp([](DomType k, const DomItem &) { return k == DomType::QmlObject; }, + filterOptions)) + return res; + if (options == GoTo::MostLikely) { + if (DomItem comp = component(options)) + return comp.field(Fields::objects).index(0); + } + return DomItem(); +} + +DomItem DomItem::fileObject(GoTo options) const +{ + DomItem res = *this; + DomType k = res.internalKind(); + if (k == DomType::List || k == DomType::Map) { + res = res.containingObject(); + k = res.internalKind(); + } + if (k == DomType::ExternalItemInfo || (options == GoTo::MostLikely && k == DomType::ExternalItemPair)) + return field(Fields::currentItem); + res = owner(); + k = res.internalKind(); + while (k != DomType::Empty) { + if (k == DomType::QmlFile || k == DomType::QmldirFile || k == DomType::QmltypesFile + || k == DomType::JsFile) + break; + res = res.containingObject(); + res = res.owner(); + k = res.internalKind(); + } + return res; +} + +DomItem DomItem::rootQmlObject(GoTo options) const +{ + return qmlObject(options, FilterUpOptions::ReturnInner); +} + +DomItem DomItem::container() const +{ + Path path = pathFromOwner(); + if (!path) + path = canonicalPath(); + Source s = path.split(); + if (s.pathFromSource.length() > 1) + return containingObject().path(s.pathFromSource.dropTail()); + return containingObject(); +} + +DomItem DomItem::globalScope() const +{ + if (internalKind() == DomType::GlobalScope) + return *this; + DomItem env = environment(); + if (shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) { + return env.copy(envPtr->ensureGlobalScopeWithName(env, envPtr->globalScopeName())->current, + Path()); + } + return DomItem(); +} + +/*! + \internal + \brief The owner of an element, for an qmlObject this is the containing qml file. + */ +DomItem DomItem::owner() const +{ + if (domTypeIsOwningItem(m_kind) || m_kind == DomType::Empty) + return *this; + return std::visit([this](auto &&el) { + if constexpr (std::is_same_v<std::decay_t<decltype(el)>, std::monostate>) + return DomItem(); + else + return DomItem(this->m_top, el, this->m_ownerPath, el.get()); + }, m_owner); +} + +DomItem DomItem::top() const +{ + if (domTypeIsTopItem(m_kind) || m_kind == DomType::Empty) + return *this; + return std::visit([](auto &&el) { + if constexpr (std::is_same_v<std::decay_t<decltype(el)>, std::monostate>) + return DomItem(); + else + return DomItem(el, el, Path(), el.get()); + }, m_top); +} + +DomItem DomItem::environment() const +{ + DomItem res = top(); + if (res.internalKind() == DomType::DomEnvironment) + return res; + return DomItem(); // we are in the universe, and cannot go back to the environment... +} + +DomItem DomItem::universe() const +{ + DomItem res = top(); + if (res.internalKind() == DomType::DomUniverse) + return res; + if (res.internalKind() == DomType::DomEnvironment) + return res.field(Fields::universe); + return DomItem(); // we should be in an empty DomItem already... +} + +/*! + \internal + Shorthand to obtain the ScriptExpression DomItem, in which this DomItem is defined. + Returns an empty DomItem if the item is not defined inside a ScriptExpression. + \sa goToFile() + */ +DomItem DomItem::containingScriptExpression() const +{ + if (DomItem res = filterUp([](DomType k, const DomItem &) { return k == DomType::ScriptExpression; }, + FilterUpOptions::ReturnOuter)) + return res; + return DomItem(); +} + +/*! + \internal + Shorthand to obtain the QmlFile DomItem, in which this DomItem is defined. + Returns an empty DomItem if the item is not defined in a QML file. + \sa goToFile() + */ +DomItem DomItem::containingFile() const +{ + if (DomItem res = filterUp([](DomType k, const DomItem &) { return k == DomType::QmlFile; }, + FilterUpOptions::ReturnOuter)) + return res; + return DomItem(); +} + +/*! + \internal + Shorthand to obtain the QmlFile DomItem from a canonicalPath. + \sa containingFile() + */ +DomItem DomItem::goToFile(const QString &canonicalPath) const +{ + Q_UNUSED(canonicalPath); + DomItem file = + top().field(Fields::qmlFileWithPath).key(canonicalPath).field(Fields::currentItem); + return file; +} + +/*! + \internal + In the DomItem hierarchy, go \c n levels up. + */ +DomItem DomItem::goUp(int n) const +{ + Path path = canonicalPath(); + // first entry of path is usually top(), and you cannot go up from top(). + if (path.length() < n + 1) + return DomItem(); + + DomItem parent = top().path(path.dropTail(n)); + return parent; +} + +/*! + \internal + In the DomItem hierarchy, go 1 level up to get the direct parent. + */ +DomItem DomItem::directParent() const +{ + return goUp(1); +} + +/*! +\internal +Finds the first element in the DomItem hierarchy that satisfies filter. +Use options to set the search direction, see also \l{FilterUpOptions}. +*/ +DomItem DomItem::filterUp(function_ref<bool(DomType k, const DomItem &)> filter, FilterUpOptions options) const +{ + if (options == FilterUpOptions::ReturnOuter && filter(internalKind(), *this)) { + return *this; + } + + switch (options) { + case FilterUpOptions::ReturnOuter: + case FilterUpOptions::ReturnOuterNoSelf: { + for (DomItem current = *this, previous = DomItem(); current; + previous = current, current = current.directParent()) { + if (filter(current.internalKind(), current)) { + if (options != FilterUpOptions::ReturnOuterNoSelf || current != *this) + return current; + } + } + break; + } + case FilterUpOptions::ReturnInner: + DomItem current = top(); + for (const Path ¤tPath : canonicalPath()) { + current = current.path(currentPath); + if (filter(current.internalKind(), current)) + return current; + } + break; + } + + return DomItem(); +} + +DomItem DomItem::scope(FilterUpOptions options) const +{ + DomItem res = filterUp([](DomType, const DomItem &el) { return el.isScope(); }, options); + return res; +} + +QQmlJSScope::ConstPtr DomItem::nearestSemanticScope() const +{ + QQmlJSScope::ConstPtr scope; + visitUp([&scope](const DomItem &item) { + scope = item.semanticScope(); + return !scope; // stop when scope was true + }); + return scope; +} + +QQmlJSScope::ConstPtr DomItem::semanticScope() const +{ + QQmlJSScope::ConstPtr scope = std::visit( + [](auto &&e) -> QQmlJSScope::ConstPtr { + using T = std::remove_cv_t<std::remove_reference_t<decltype(e)>>; + if constexpr (std::is_same_v<T, const QmlObject *>) { + return e->semanticScope(); + } else if constexpr (std::is_same_v<T, const QmlComponent *>) { + return e->semanticScope(); + } else if constexpr (std::is_same_v<T, const QmltypesComponent *>) { + return e->semanticScope(); + } else if constexpr (std::is_same_v<T, SimpleObjectWrap>) { + if (const MethodInfo *mi = e->template as<MethodInfo>()) { + return mi->semanticScope(); + } + if (const auto *propertyDefinition = e->template as<PropertyDefinition>()) { + return propertyDefinition->semanticScope(); + } + } else if constexpr (std::is_same_v<T, ScriptElementDomWrapper>) { + return e.element().base()->semanticScope(); + } + return {}; + }, + m_element); + return scope; +} + +DomItem DomItem::get(const ErrorHandler &h, QList<Path> *visitedRefs) const +{ + if (const Reference *refPtr = as<Reference>()) + return refPtr->get(*this, h, visitedRefs); + return DomItem(); +} + +QList<DomItem> DomItem::getAll(const ErrorHandler &h, QList<Path> *visitedRefs) const +{ + if (const Reference *refPtr = as<Reference>()) + return refPtr->getAll(*this, h, visitedRefs); + return {}; +} + +PropertyInfo DomItem::propertyInfoWithName(const QString &name) const +{ + PropertyInfo pInfo; + visitPrototypeChain([&pInfo, name](const DomItem &obj) { + return obj.visitLocalSymbolsNamed(name, [&pInfo, name](const DomItem &el) { + switch (el.internalKind()) { + case DomType::Binding: + pInfo.bindings.append(el); + break; + case DomType::PropertyDefinition: + pInfo.propertyDefs.append(el); + break; + default: + break; + } + return true; + }); + }); + return pInfo; +} + +QSet<QString> DomItem::propertyInfoNames() const +{ + QSet<QString> res; + visitPrototypeChain([&res](const DomItem &obj) { + res += obj.propertyDefs().keys(); + res += obj.bindings().keys(); + return true; + }); + return res; +} + +DomItem DomItem::component(GoTo options) const +{ + if (DomItem res = filterUp( + [](DomType kind, const DomItem &) { + return kind == DomType::QmlComponent || kind == DomType::QmltypesComponent + || kind == DomType::GlobalComponent; + }, + FilterUpOptions::ReturnInner)) + return res; + if (options == GoTo::MostLikely) { + DomItem item = *this; + DomType kind = item.internalKind(); + if (kind == DomType::List || kind == DomType::Map) { + item = item.containingObject(); + kind = item.internalKind(); + } + switch (kind) { + case DomType::ExternalItemPair: + case DomType::ExternalItemInfo: + item = fileObject(options); + Q_FALLTHROUGH(); + case DomType::QmlFile: + return item.field(Fields::components).key(QString()).index(0); + default: + break; + } + } + return DomItem(); +} + +struct ResolveToDo { + DomItem item; + int pathIndex; +}; + +static QMap<LookupType, QString> lookupTypeToStringMap() +{ + static QMap<LookupType, QString> map = []() { + QMetaEnum metaEnum = QMetaEnum::fromType<LookupType>(); + QMap<LookupType, QString> res; + for (int i = 0; i < metaEnum.keyCount(); ++i) { + res[LookupType(metaEnum.value(i))] = QString::fromUtf8(metaEnum.key(i)); + } + return res; + }(); + return map; +} + +bool DomItem::resolve(const Path &path, DomItem::Visitor visitor, const ErrorHandler &errorHandler, + ResolveOptions options, const Path &fullPath, QList<Path> *visitedRefs) const +{ + QList<Path> vRefs; + Path fPath = fullPath; + if (fullPath.length() == 0) + fPath = path; + if (path.length()==0) + return visitor(fPath, *this); + QList<QSet<quintptr>> visited(path.length() + 1); + Path myPath = path; + QVector<ResolveToDo> toDos(1); // invariant: always increase pathIndex to guarantee end even with only partial visited match + if (path.headKind() == Path::Kind::Root) { + DomItem root = *this; + PathRoot contextId = path.headRoot(); + switch (contextId) { + case PathRoot::Modules: + root = root.environment().field(Fields::moduleIndexWithUri); + break; + case PathRoot::Cpp: + root = root.environment()[Fields::qmltypesFileWithPath]; + break; + case PathRoot::Libs: + root = root.environment()[Fields::plugins]; + break; + case PathRoot::Top: + root = root.top(); + break; + case PathRoot::Env: + root = root.environment(); + break; + case PathRoot::Universe: + root = root.environment()[u"universe"]; + break; + case PathRoot::Other: + myResolveErrors().error(tr("Root context %1 is not known").arg(path.headName())).handle(errorHandler); + return false; + } + toDos[0] = {std::move(root), 1}; + } else { + toDos[0] = {*this, 0}; + } + while (!toDos.isEmpty()) { + const ResolveToDo toDo = toDos.takeLast(); + { + auto idNow = toDo.item.id(); + if (idNow == quintptr(0) && toDo.item == *this) + idNow = quintptr(this); + if (idNow != quintptr(0) && visited[0].contains(idNow)) + continue; + } + int iPath = toDo.pathIndex; + DomItem it = toDo.item; + bool branchExhausted = false; + while (iPath < path.length() && it && !branchExhausted) { + auto idNow = it.id(); + if (idNow == quintptr() && toDo.item == *this) + idNow = quintptr(this); + if (idNow != quintptr(0)) { + auto vPair = qMakePair(idNow, iPath); + if (visited[vPair.second].contains(vPair.first)) + break; + visited[vPair.second].insert(vPair.first); + } + if (options & ResolveOption::TraceVisit && !visitor(path.mid(0,iPath), it)) + return false; + auto cNow = path[iPath++]; + switch (cNow.headKind()) { + case Path::Kind::Key: + it = it.key(cNow.headName()); + break; + case Path::Kind::Field: + if (cNow.checkHeadName(Fields::get) && it.internalKind() == DomType::Reference) { + Path toResolve = it.as<Reference>()->referredObjectPath; + Path refRef = it.canonicalPath(); + if (visitedRefs == nullptr) { + visitedRefs = &vRefs; + } + if (visitedRefs->contains(refRef)) { + myResolveErrors() + .error([visitedRefs, refRef](const Sink &sink) { + const QString msg = tr("Circular reference:") + QLatin1Char('\n'); + sink(QStringView{msg}); + for (const Path &vPath : *visitedRefs) { + sink(u" "); + vPath.dump(sink); + sink(u" >\n"); + } + refRef.dump(sink); + }) + .handle(errorHandler); + it = DomItem(); + } else { + visitedRefs->append(refRef); + DomItem resolveRes; + it.resolve( + toResolve, + [&resolveRes](Path, const DomItem &r) { + resolveRes = r; + return false; + }, + errorHandler, ResolveOption::None, toResolve, visitedRefs); + it = resolveRes; + } + } else { + it = it.field(cNow.headName()); // avoid instantiation of QString? + } + break; + case Path::Kind::Index: + it = it.index(cNow.headIndex()); + break; + case Path::Kind::Empty: + { + // immediate expansion, might use extra memory, but simplifies code (no suspended evaluation) + Path toFind; + do { + if (iPath >= path.length()) { + myResolveErrors().warning(tr("Resolve with path ending with empty path, matches nothing.")) + .handle(errorHandler); + branchExhausted = true; // allow and visit all? + break; + } + toFind = path[iPath++]; + } while (toFind.headKind() == Path::Kind::Empty); + QVector<Path::Kind> validFind({Path::Kind::Key, Path::Kind::Field, Path::Kind::Field, Path::Kind::Index}); + if (!validFind.contains(toFind.headKind())) { + myResolveErrors().error(tr("After an empty path only key, field or indexes are supported, not %1.").arg(toFind.toString())) + .handle(errorHandler); + branchExhausted = true; // allow and visit all? + return false; + } + if (!branchExhausted) + visitTree( + Path(), + [&toFind, &toDos, iPath](Path, const DomItem &item, bool) { + // avoid non directly attached? + DomItem newItem = item[toFind]; + if (newItem) + toDos.append({ std::move(newItem), iPath }); + return true; + }, + VisitOption::VisitSelf | VisitOption::Recurse + | VisitOption::VisitAdopted | VisitOption::NoPath); + branchExhausted = true; + break; + } + case Path::Kind::Root: + myResolveErrors().error(tr("Root path is supported only at the beginning, and only once, found %1 at %2 in %3") + .arg(cNow.toString()).arg(iPath -1).arg(path.toString())).handle(errorHandler); + return false; + case Path::Kind::Current: + { + PathCurrent current = cNow.headCurrent(); + switch (current) { + case PathCurrent::Other: + // todo + case PathCurrent::Obj: + if (domKind() != DomKind::Object) + it = it.containingObject(); + break; + case PathCurrent::ObjChain: { + bool cont = it.visitPrototypeChain( + [&toDos, iPath](const DomItem &subEl) { + toDos.append({ subEl, iPath }); + return true; + }, + VisitPrototypesOption::Normal, errorHandler, nullptr, + visitedRefs); // avoid passing visitedRefs? + if (!cont) + return false; + branchExhausted = true; + break; + } + case PathCurrent::ScopeChain: { + bool cont = it.visitScopeChain( + [&toDos, iPath](const DomItem &subEl) { + toDos.append({ subEl, iPath }); + return true; + }, + LookupOption::Normal, errorHandler); + if (!cont) + return false; + branchExhausted = true; + break; + } + case PathCurrent::Component: + it = it.component(); + break; + case PathCurrent::Module: + case PathCurrent::Ids: + it = it.component().ids(); + break; + case PathCurrent::Types: + it = it.component()[Fields::exports]; + break; + case PathCurrent::LookupStrict: + case PathCurrent::LookupDynamic: + case PathCurrent::Lookup: { + LookupOptions opt = LookupOption::Normal; + if (current == PathCurrent::Lookup) { + DomItem comp = it.component(); + DomItem strict = comp.field(u"~strictLookup~"); + if (!strict) { + DomItem env = it.environment(); + strict = env.field(u"defaultStrictLookup"); + } + if (strict && strict.value().toBool()) + opt = opt | LookupOption::Strict; + } else if (current == PathCurrent::LookupStrict) { + opt = opt | LookupOption::Strict; + } + if (it.internalKind() == DomType::ScriptExpression) { + myResolveErrors() + .error(tr("Javascript lookups not yet implemented")) + .handle(errorHandler); + return false; + } + // enter lookup + auto idNow = it.id(); + if (idNow == quintptr(0) && toDo.item == *this) + idNow = quintptr(this); + if (idNow != quintptr(0)) { + auto vPair = qMakePair(idNow, iPath); + if (visited[vPair.second].contains(vPair.first)) + break; + visited[vPair.second].insert(vPair.first); + } + if (options & ResolveOption::TraceVisit && !visitor(path.mid(0, iPath), it)) + return false; + if (iPath + 1 >= path.length()) { + myResolveErrors() + .error(tr("Premature end of path, expected a field specifying the " + "type, and a key specifying the name to search after a " + "lookup directive in %2") + .arg(path.toString())) + .handle(errorHandler); + return false; + } + Path cNow = path[iPath++]; + if (cNow.headKind() != Path::Kind::Field) { + myResolveErrors() + .error(tr("Expected a key path specifying the type to search after " + "a lookup directive, not %1 at component %2 of %3") + .arg(cNow.toString()) + .arg(iPath) + .arg(path.toString())) + .handle(errorHandler); + return false; + } + QString expectedType = cNow.headName(); + LookupType lookupType = LookupType::Symbol; + { + bool found = false; + auto m = lookupTypeToStringMap(); + auto it = m.begin(); + auto end = m.end(); + while (it != end) { + if (it.value().compare(expectedType, Qt::CaseInsensitive) == 0) { + lookupType = it.key(); + found = true; + } + ++it; + } + if (!found) { + QString types; + it = lookupTypeToStringMap().begin(); + while (it != end) { + if (!types.isEmpty()) + types += QLatin1String("', '"); + types += it.value(); + ++it; + } + myResolveErrors() + .error(tr("Type for lookup was expected to be one of '%1', not " + "%2") + .arg(types, expectedType)) + .handle(errorHandler); + return false; + } + } + cNow = path[iPath++]; + if (cNow.headKind() != Path::Kind::Key) { + myResolveErrors() + .error(tr("Expected a key specifying the path to search after the " + "@lookup directive and type, not %1 at component %2 of " + "%3") + .arg(cNow.toString()) + .arg(iPath) + .arg(path.toString())) + .handle(errorHandler); + return false; + } + QString target = cNow.headName(); + if (target.isEmpty()) { + myResolveErrors() + .warning(tr("Path with empty lookup at component %1 of %2 will " + "match nothing in %3.") + .arg(iPath) + .arg(path.toString()) + .arg(it.canonicalPath().toString())) + .handle(errorHandler); + return true; + } + it.visitLookup( + target, + [&toDos, iPath](const DomItem &subEl) { + toDos.append({ subEl, iPath }); + return true; + }, + lookupType, opt, errorHandler, &(visited[iPath]), visitedRefs); + branchExhausted = true; + break; + } + } + break; + } + case Path::Kind::Any: + visitTree( + Path(), + [&toDos, iPath](Path, const DomItem &item, bool) { + toDos.append({ item, iPath }); + return true; + }, + VisitOption::VisitSelf | VisitOption::Recurse | VisitOption::VisitAdopted); + branchExhausted = true; + break; + case Path::Kind::Filter: + if (cNow.headFilter() && !cNow.headFilter()(it)) + branchExhausted = true; + break; + } + } + // visit the resolved path + if (!branchExhausted && iPath == path.length() && !visitor(fPath, it)) + return false; + } + return true; +} + +DomItem DomItem::path(const Path &p, const ErrorHandler &errorHandler) const +{ + if (!p) + return *this; + DomItem res; + resolve(p, [&res](const Path &, const DomItem &it) { + res = it; + return false; + }, errorHandler); + return res; +} + +DomItem DomItem::path(const QString &p, const ErrorHandler &errorHandler) const +{ + return path(Path::fromString(p, errorHandler)); +} + +DomItem DomItem::path(QStringView p, const ErrorHandler &errorHandler) const +{ + return path(Path::fromString(p, errorHandler)); +} + +QList<QString> DomItem::fields() const +{ + return visitEl([this](auto &&el) { return el->fields(*this); }); +} + +DomItem DomItem::field(QStringView name) const +{ + return visitEl([this, name](auto &&el) { return el->field(*this, name); }); +} + +index_type DomItem::indexes() const +{ + return visitEl([this](auto &&el) { return el->indexes(*this); }); +} + +DomItem DomItem::index(index_type i) const +{ + return visitEl([this, i](auto &&el) { return el->index(*this, i); }); +} + +bool DomItem::visitIndexes(function_ref<bool(const DomItem &)> visitor) const +{ + // use iterateDirectSubpathsConst instead? + int nIndexes = indexes(); + for (int i = 0; i < nIndexes; ++i) { + DomItem v = index(i); + if (!visitor(v)) + return false; + } + return true; +} + +QSet<QString> DomItem::keys() const +{ + return visitEl([this](auto &&el) { return el->keys(*this); }); +} + +QStringList DomItem::sortedKeys() const +{ + QSet<QString> ks = keys(); + QStringList sortedKs(ks.begin(), ks.end()); + std::sort(sortedKs.begin(), sortedKs.end()); + return sortedKs; +} + +DomItem DomItem::key(const QString &name) const +{ + return visitEl([this, name](auto &&el) { return el->key(*this, name); }); +} + +bool DomItem::visitKeys(function_ref<bool(const QString &, const DomItem &)> visitor) const +{ + // use iterateDirectSubpathsConst instead? + const QStringList keys = sortedKeys(); + for (const QString &k : keys) { + DomItem v = key(k); + if (!visitor(k, v)) + return false; + } + return true; +} + +QList<DomItem> DomItem::values() const +{ + QList<DomItem> res; + visitEl([this, &res](const auto &el) { + return el->iterateDirectSubpathsConst( + *this, [&res](const PathEls::PathComponent &, function_ref<DomItem()> item) { + res.append(item()); + return true; + }); + }); + return res; +} + +void DomItem::writeOutPre(OutWriter &ow) const +{ + if (hasAnnotations()) { + DomItem anns = field(Fields::annotations); + for (const auto &ann : anns.values()) { + if (ann.annotations().indexes() == 0) { + ow.ensureNewline(); + ann.writeOut(ow); + ow.ensureNewline(); + } else { + DomItem annAnn = ann.annotations(); + Q_ASSERT_X(annAnn.indexes() == 1 && annAnn.index(0).name() == u"duplicate", + "DomItem::writeOutPre", "Unexpected annotation of annotation"); + } + } + } + ow.itemStart(*this); +} + +void DomItem::writeOut(OutWriter &ow) const +{ + writeOutPre(ow); + visitEl([this, &ow](auto &&el) { el->writeOut(*this, ow); }); + writeOutPost(ow); +} + +void DomItem::writeOutPost(OutWriter &ow) const +{ + ow.itemEnd(*this); +} + +DomItem::WriteOutCheckResult DomItem::performWriteOutChecks(const DomItem &original, const DomItem &reformatted, + OutWriter &ow, + WriteOutChecks extraChecks) const +{ + QStringList dumped; + auto maybeDump = [&ow, extraChecks, &dumped](const DomItem &obj, QStringView objName) { + QString objDumpPath; + if (extraChecks & WriteOutCheck::DumpOnFailure) { + objDumpPath = QDir(QDir::tempPath()) + .filePath(objName.toString() + + QFileInfo(ow.lineWriter.fileName()).baseName() + + QLatin1String(".dump.json")); + obj.dump(objDumpPath); + dumped.append(objDumpPath); + } + return objDumpPath; + }; + auto dumpedDumper = [&dumped](const Sink &s) { + if (dumped.isEmpty()) + return; + s(u"\ndump: "); + for (const auto &dumpPath : dumped) { + s(u" "); + sinkEscaped(s, dumpPath); + } + }; + auto compare = [&maybeDump, &dumpedDumper, this](const DomItem &obj1, QStringView obj1Name, + const DomItem &obj2, QStringView obj2Name, + const FieldFilter &filter) { + const auto diffList = domCompareStrList(obj1, obj2, filter, DomCompareStrList::AllDiffs); + if (!diffList.isEmpty()) { + maybeDump(obj1, obj1Name); + maybeDump(obj2, obj2Name); + qCWarning(writeOutLog).noquote().nospace() + << obj2Name << " writeOut of " << this->canonicalFilePath() << " has changes:\n" + << diffList.join(QString()) << dumpedDumper; + return false; + } + return true; + }; + auto checkStability = [&maybeDump, &dumpedDumper, &dumped, &ow, + this](const QString &expected, const DomItem &obj, QStringView objName) { + LineWriter lw2([](QStringView) {}, ow.lineWriter.fileName(), ow.lineWriter.options()); + OutWriter ow2(lw2); + ow2.indentNextlines = true; + obj.writeOut(ow2); + ow2.eof(); + if (ow2.writtenStr != expected) { + DomItem fObj = this->fileObject(); + maybeDump(fObj, u"initial"); + maybeDump(obj, objName); + qCWarning(writeOutLog).noquote().nospace() + << objName << " non stable writeOut of " << this->canonicalFilePath() << ":" + << lineDiff(ow2.writtenStr, expected, 2) << dumpedDumper; + dumped.clear(); + return false; + } + return true; + }; + + if ((extraChecks & WriteOutCheck::UpdatedDomCompare) + && !compare(original, u"initial", reformatted, u"reformatted", + FieldFilter::noLocationFilter())) + return WriteOutCheckResult::Failed; + + if (extraChecks & WriteOutCheck::UpdatedDomStable) { + checkStability(ow.writtenStr, reformatted, u"reformatted"); + } + + if (extraChecks + & (WriteOutCheck::Reparse | WriteOutCheck::ReparseCompare | WriteOutCheck::ReparseStable)) { + DomItem newEnv = environment().makeCopy().item(); + std::shared_ptr<DomEnvironment> newEnvPtr = newEnv.ownerAs<DomEnvironment>(); + if (!newEnvPtr) + return WriteOutCheckResult::Failed; + + auto newFilePtr = std::make_shared<QmlFile>(canonicalFilePath(), ow.writtenStr); + if (!newFilePtr) + return WriteOutCheckResult::Failed; + newEnvPtr->addQmlFile(newFilePtr, AddOption::Overwrite); + + DomItem newFile = newEnv.copy(newFilePtr, Path()); + if (newFilePtr->isValid()) { + if (extraChecks & (WriteOutCheck::ReparseCompare | WriteOutCheck::ReparseStable)) { + newEnvPtr->populateFromQmlFile(newFile); + if ((extraChecks & WriteOutCheck::ReparseCompare) + && !compare(reformatted, u"reformatted", newFile, u"reparsed", + FieldFilter::compareNoCommentsFilter())) + return WriteOutCheckResult::Failed; + if ((extraChecks & WriteOutCheck::ReparseStable)) + checkStability(ow.writtenStr, newFile, u"reparsed"); + } + } else { + const auto iterateErrors = [&newFile](const Sink &s) { + newFile.iterateErrors( + [s](const DomItem &, const ErrorMessage &msg) { + s(u"\n "); + msg.dump(s); + return true; + }, + true); + s(u"\n"); // extra empty line at the end... + }; + qCWarning(writeOutLog).noquote().nospace() + << "writeOut of " << canonicalFilePath() + << " created invalid code:\n----------\n" + << ow.writtenStr << "\n----------" << iterateErrors; + return WriteOutCheckResult::Failed; + } + } + return WriteOutCheckResult::Success; +} + +/*! + \internal + Performes WriteOut of the FileItem and verifies the consistency of the DOM structure. + + OutWriter is essentially a visitor traversing the DOM structure, starting from + the current item representing a FileItem. + While traversing it might be saving some intermediate information, used later for restoring + written out item. Restoration is needed to validate that the DOM structure of the written item + has not changed. +*/ +bool DomItem::writeOutForFile(OutWriter &ow, WriteOutChecks extraChecks) const +{ + ow.indentNextlines = true; + writeOut(ow); + ow.eof(); + + auto currentFileItem = fileObject(); + auto writtenFileItem = ow.restoreWrittenFileItem(currentFileItem); + WriteOutCheckResult result = WriteOutCheckResult::Success; + if (extraChecks & WriteOutCheck::All) + result = performWriteOutChecks(currentFileItem, writtenFileItem, ow, extraChecks); + return result == WriteOutCheckResult::Success ? bool(writtenFileItem) : false; +} + +bool DomItem::writeOut(const QString &path, int nBackups, const LineWriterOptions &options, + FileWriter *fw, WriteOutChecks extraChecks) const +{ + FileWriter localFw; + if (!fw) + fw = &localFw; + auto status = fw->write( + path, + [this, path, &options, extraChecks](QTextStream &ts) { + LineWriter lw([&ts](QStringView s) { ts << s; }, path, options); + OutWriter ow(lw); + return writeOutForFile(ow, extraChecks); + }, + nBackups); + switch (status) { + case FileWriter::Status::DidWrite: + case FileWriter::Status::SkippedEqual: + return true; + case FileWriter::Status::ShouldWrite: + case FileWriter::Status::SkippedDueToFailure: + qCWarning(writeOutLog) << "failure reformatting " << path; + return false; + default: + qCWarning(writeOutLog) << "Unknown FileWriter::Status "; + Q_ASSERT(false); + return false; + } +} + +bool DomItem::isCanonicalChild(const DomItem &item) const +{ + bool isChild = false; + if (item.isOwningItem()) { + isChild = canonicalPath() == item.canonicalPath().dropTail(); + } else { + DomItem itemOw = item.owner(); + DomItem selfOw = owner(); + isChild = itemOw == selfOw && item.pathFromOwner().dropTail() == pathFromOwner(); + } + return isChild; +} + +bool DomItem::hasAnnotations() const +{ + bool hasAnnotations = false; + DomType iKind = internalKind(); + switch (iKind) { + case DomType::Id: + if (const Id *myPtr = as<Id>()) + hasAnnotations = !myPtr->annotations.isEmpty(); + break; + case DomType::PropertyDefinition: + if (const PropertyDefinition *myPtr = as<PropertyDefinition>()) + hasAnnotations = !myPtr->annotations.isEmpty(); + break; + case DomType::MethodInfo: + if (const MethodInfo *myPtr = as<MethodInfo>()) + hasAnnotations = !myPtr->annotations.isEmpty(); + break; + case DomType::QmlObject: + if (const QmlObject *myPtr = as<QmlObject>()) + hasAnnotations = !myPtr->annotations().isEmpty(); + break; + case DomType::Binding: + if (const Binding *myPtr = as<Binding>()) + hasAnnotations = !myPtr->annotations().isEmpty(); + break; + default: + break; + } + return hasAnnotations; +} + +/*! + \internal + \brief Visits recursively all the children of this item using the given visitors. + + First, the visitor is called and can continue or exit the visit by returning true or false. + + Second, the openingVisitor is called and controls if the children of the current item needs to + be visited or not by returning true or false. In either case, the visitation of all the other + siblings is not affected. If both visitor and openingVisitor returned true, then the childrens of + the current item will be recursively visited. + + Finally, after all the children were visited by visitor and openingVisitor, the closingVisitor + is called. Its return value is currently ignored. + + Compared to the AST::Visitor*, openingVisitor and closingVisitor are called in the same order as + the visit() and endVisit()-calls. + + Filtering allows to not visit certain part of the trees, and is checked before(!) the lazy child + is instantiated via its lambda. For example, visiting propertyInfos or defaultPropertyname takes + a lot of time because it resolves and collects all properties inherited from base types, and + might not even be relevant for the visitors. + */ +bool DomItem::visitTree(const Path &basePath, DomItem::ChildrenVisitor visitor, + VisitOptions options, DomItem::ChildrenVisitor openingVisitor, + DomItem::ChildrenVisitor closingVisitor, const FieldFilter &filter) const +{ + if (!*this) + return true; + if (options & VisitOption::VisitSelf && !visitor(basePath, *this, true)) + return false; + if (options & VisitOption::VisitSelf && !openingVisitor(basePath, *this, true)) + return true; + auto atEnd = qScopeGuard([closingVisitor, basePath, this, options]() { + if (options & VisitOption::VisitSelf) { + closingVisitor(basePath, *this, true); + } + }); + return visitEl([this, basePath, visitor, openingVisitor, closingVisitor, options, + &filter](auto &&el) { + return el->iterateDirectSubpathsConst( + *this, + [this, basePath, visitor, openingVisitor, closingVisitor, options, + &filter](const PathEls::PathComponent &c, function_ref<DomItem()> itemF) { + Path pNow; + if (!(options & VisitOption::NoPath)) { + pNow = basePath; + pNow = pNow.appendComponent(c); + } + if (!filter(*this, c, DomItem{})) + return true; + DomItem item = itemF(); + bool directChild = isCanonicalChild(item); + if (!directChild && !(options & VisitOption::VisitAdopted)) + return true; + if (!directChild || !(options & VisitOption::Recurse)) { + if (!visitor(pNow, item, directChild)) + return false; + // give an option to avoid calling open/close when not recursing? + // calling it always allows close to do the reverse looping (children before + // parent) + if (!openingVisitor(pNow, item, directChild)) + return true; + closingVisitor(pNow, item, directChild); + } else { + return item.visitTree(pNow, visitor, options | VisitOption::VisitSelf, + openingVisitor, closingVisitor, filter); + } + return true; + }); + }); +} +static bool visitPrototypeIndex(QList<DomItem> &toDo, const DomItem ¤t, + const DomItem &derivedFromPrototype, const ErrorHandler &h, + QList<Path> *visitedRefs, VisitPrototypesOptions options, + const DomItem &prototype) +{ + Path elId = prototype.canonicalPath(); + if (visitedRefs->contains(elId)) + return true; + else + visitedRefs->append(elId); + QList<DomItem> protos = prototype.getAll(h, visitedRefs); + if (protos.isEmpty()) { + if (std::shared_ptr<DomEnvironment> envPtr = + derivedFromPrototype.environment().ownerAs<DomEnvironment>()) + if (!(envPtr->options() & DomEnvironment::Option::NoDependencies)) + derivedFromPrototype.myErrors() + .warning(derivedFromPrototype.tr("could not resolve prototype %1 (%2)") + .arg(current.canonicalPath().toString(), + prototype.field(Fields::referredObjectPath) + .value() + .toString())) + .withItem(derivedFromPrototype) + .handle(h); + } else { + if (protos.size() > 1) { + QStringList protoPaths; + for (const DomItem &p : protos) + protoPaths.append(p.canonicalPath().toString()); + derivedFromPrototype.myErrors() + .warning(derivedFromPrototype + .tr("Multiple definitions found, using first only, resolving " + "prototype %1 (%2): %3") + .arg(current.canonicalPath().toString(), + prototype.field(Fields::referredObjectPath) + .value() + .toString(), + protoPaths.join(QLatin1String(", ")))) + .withItem(derivedFromPrototype) + .handle(h); + } + int nProtos = 1; // change to protos.length() to use all prototypes + // (sloppier) + for (int i = nProtos; i != 0;) { + DomItem proto = protos.at(--i); + if (proto.internalKind() == DomType::Export) { + if (!(options & VisitPrototypesOption::ManualProceedToScope)) + proto = proto.proceedToScope(h, visitedRefs); + toDo.append(proto); + } else if (proto.internalKind() == DomType::QmlObject + || proto.internalKind() == DomType::QmlComponent) { + toDo.append(proto); + } else { + derivedFromPrototype.myErrors() + .warning(derivedFromPrototype.tr("Unexpected prototype type %1 (%2)") + .arg(current.canonicalPath().toString(), + prototype.field(Fields::referredObjectPath) + .value() + .toString())) + .withItem(derivedFromPrototype) + .handle(h); + } + } + } + return true; +} + +bool DomItem::visitPrototypeChain(function_ref<bool(const DomItem &)> visitor, + VisitPrototypesOptions options, const ErrorHandler &h, + QSet<quintptr> *visited, QList<Path> *visitedRefs) const +{ + QSet<quintptr> visitedLocal; + if (!visited) + visited = &visitedLocal; + QList<Path> refsLocal; + if (!visitedRefs) + visitedRefs = &refsLocal; + bool shouldVisit = !(options & VisitPrototypesOption::SkipFirst); + DomItem current = qmlObject(); + if (!current) { + myErrors().warning(tr("Prototype chain called outside object")).withItem(*this).handle(h); + return true; + } + QList<DomItem> toDo({ current }); + while (!toDo.isEmpty()) { + current = toDo.takeLast(); + current = current.proceedToScope(h, visitedRefs); + if (visited->contains(current.id())) { + // to warn about circular dependencies a purely local visited trace is required + // as common ancestors of unrelated objects are valid and should be skipped + // so we do not to warn unless requested + if (options & VisitPrototypesOption::RevisitWarn) + myErrors() + .warning(tr("Detected multiple visit of %1 visiting prototypes of %2") + .arg(current.canonicalPath().toString(), + canonicalPath().toString())) + .withItem(*this) + .handle(h); + continue; + } + visited->insert(current.id()); + if (shouldVisit && !visitor(current)) + return false; + shouldVisit = true; + current.field(Fields::prototypes) + .visitIndexes([&toDo, ¤t, this, &h, visitedRefs, options](const DomItem &el) { + return visitPrototypeIndex(toDo, current, *this, h, visitedRefs, options, el); + }); + } + return true; +} + +bool DomItem::visitDirectAccessibleScopes( + function_ref<bool(const DomItem &)> visitor, VisitPrototypesOptions options, + const ErrorHandler &h, QSet<quintptr> *visited, QList<Path> *visitedRefs) const +{ + // these are the scopes one can access with the . operator from the current location + // but currently not the attached types, which we should + DomType k = internalKind(); + if (k == DomType::QmlObject) + return visitPrototypeChain(visitor, options, h, visited, visitedRefs); + if (visited && id() != 0) { + if (visited->contains(id())) + return true; + visited->insert(id()); + } + if (k == DomType::Id || k == DomType::Reference || k == DomType::Export) { + // we go to the scope if it is clearly defined + DomItem v = proceedToScope(h, visitedRefs); + if (v.internalKind() == DomType::QmlObject) + return v.visitPrototypeChain(visitor, options, h, visited, visitedRefs); + } + if (k == DomType::Binding) { + // from a binding we can get to its value if it is a object + DomItem v = field(Fields::value); + if (v.internalKind() == DomType::QmlObject) + return v.visitPrototypeChain(visitor, options, h, visited, visitedRefs); + } + if (k == DomType::PropertyDefinition) { + // from a property definition we go to the type stored in it + DomItem t = field(Fields::type).proceedToScope(h, visitedRefs); + if (t.internalKind() == DomType::QmlObject) + return t.visitPrototypeChain(visitor, options, h, visited, visitedRefs); + } + if (!(options & VisitPrototypesOption::SkipFirst) && isScope() && !visitor(*this)) + return false; + return true; +} + +/*! + * \brief DomItem::visitStaticTypePrototypeChains + * \param visitor + * \param visitFirst + * \param visited + * \return + * + * visit the values JS reaches accessing a type directly: the values if it is a singleton or the + * attached type + */ +bool DomItem::visitStaticTypePrototypeChains( + function_ref<bool(const DomItem &)> visitor, VisitPrototypesOptions options, + const ErrorHandler &h, QSet<quintptr> *visited, QList<Path> *visitedRefs) const +{ + QSet<quintptr> visitedLocal; + if (!visited) + visited = &visitedLocal; + DomItem current = qmlObject(); + DomItem comp = current.component(); + if (comp.field(Fields::isSingleton).value().toBool(false) + && !current.visitPrototypeChain(visitor, options, h, visited, visitedRefs)) + return false; + if (DomItem attachedT = current.component().field(Fields::attachedType).field(Fields::get)) + if (!attachedT.visitPrototypeChain( + visitor, options & ~VisitPrototypesOptions(VisitPrototypesOption::SkipFirst), h, + visited, visitedRefs)) + return false; + return true; +} + +/*! + \brief Let the visitor visit the Dom Tree hierarchy of this DomItem. + */ +bool DomItem::visitUp(function_ref<bool(const DomItem &)> visitor) const +{ + Path p = canonicalPath(); + while (p.length() > 0) { + DomItem current = top().path(p); + if (!visitor(current)) + return false; + p = p.dropTail(); + } + return true; +} + +/*! + \brief Let the visitor visit the QML scope hierarchy of this DomItem. + */ +bool DomItem::visitScopeChain( + function_ref<bool(const DomItem &)> visitor, LookupOptions options, const ErrorHandler &h, + QSet<quintptr> *visited, QList<Path> *visitedRefs) const +{ + QSet<quintptr> visitedLocal; + if (!visited) + visited = &visitedLocal; + QList<Path> visitedRefsLocal; + if (!visitedRefs) + visitedRefs = &visitedRefsLocal; + DomItem current = scope(); + if (!current) { + myResolveErrors().warning(tr("Called visitScopeChain outside scopes")).handle(h); + return true; + } + QList<DomItem> toDo { current }; + bool visitFirst = !(options & LookupOption::SkipFirstScope); + bool visitCurrent = visitFirst; + bool first = true; + QSet<quintptr> alreadyAddedComponentMaps; + while (!toDo.isEmpty()) { + DomItem current = toDo.takeLast(); + if (visited->contains(current.id())) + continue; + visited->insert(current.id()); + if (visitCurrent && !visitor(current)) + return false; + visitCurrent = true; + switch (current.internalKind()) { + case DomType::QmlObject: { + if (!current.visitPrototypeChain(visitor, VisitPrototypesOption::SkipFirst, h, visited, + visitedRefs)) + return false; + DomItem root = current.rootQmlObject(); + if (root && root != current) { + first = false; + toDo.append(root); + } else if (DomItem next = current.scope( + FilterUpOptions::ReturnOuterNoSelf)) { // should be the component + toDo.append(next); + } + } break; + case DomType::ScriptExpression: // Js lexical scope + first = false; + if (DomItem next = current.scope(FilterUpOptions::ReturnOuterNoSelf)) + toDo.append(next); + break; + case DomType::QmlComponent: { // ids/attached type + if ((options & LookupOption::Strict) == 0) { + if (DomItem comp = current.field(Fields::nextComponent)) + toDo.append(comp); + } + if (first && visitFirst && (options & LookupOption::VisitTopClassType) + && *this == current) { // visit attached type if it is the top of the chain + if (DomItem attachedT = current.field(Fields::attachedType).field(Fields::get)) + toDo.append(attachedT); + } + if (DomItem next = current.scope(FilterUpOptions::ReturnOuterNoSelf)) + toDo.append(next); + + DomItem owner = current.owner(); + Path pathToComponentMap = current.pathFromOwner().dropTail(2); + DomItem componentMap = owner.path(pathToComponentMap); + if (alreadyAddedComponentMaps.contains(componentMap.id())) + break; + alreadyAddedComponentMaps.insert(componentMap.id()); + const auto keys = componentMap.keys(); + for (const QString &x : keys) { + DomItem componentList = componentMap.key(x); + for (int i = 0; i < componentList.indexes(); ++i) { + DomItem component = componentList.index(i); + if (component != current && !visited->contains(component.id())) + toDo.append(component); + } + } + first = false; + break; + } + case DomType::QmlFile: // subComponents, imported types + if (DomItem iScope = + current.field(Fields::importScope)) // treat file as a separate scope? + toDo.append(iScope); + first = false; + break; + case DomType::MethodInfo: // method arguments + first = false; + if (DomItem next = current.scope(FilterUpOptions::ReturnOuterNoSelf)) + toDo.append(next); + break; + case DomType::ImportScope: // types + first = false; + if (auto globalC = globalScope().field(Fields::rootComponent)) + toDo.append(globalC); + break; + case DomType::JsResource: + case DomType::GlobalComponent: + first = false; + if (DomItem next = current.field(Fields::objects).index(0)) + toDo.append(next); + break; + case DomType::QmltypesComponent: + first = false; + break; + default: + first = false; + myResolveErrors() + .error(tr("Unexpected non scope object %1 (%2) reached in visitScopeChain") + .arg(domTypeToString(current.internalKind()), + current.canonicalPath().toString())) + .handle(h); + Q_ASSERT(false); + break; + } + } + return true; +} + +bool DomItem::visitLookup1( + const QString &symbolName, function_ref<bool(const DomItem &)> visitor, LookupOptions opts, + const ErrorHandler &h, QSet<quintptr> *visited, QList<Path> *visitedRefs) const +{ + return visitScopeChain( + [symbolName, visitor](const DomItem &obj) { + return obj.visitLocalSymbolsNamed(symbolName, + [visitor](const DomItem &el) { return visitor(el); }); + }, + opts, h, visited, visitedRefs); +} + +class CppTypeInfo +{ + Q_DECLARE_TR_FUNCTIONS(CppTypeInfo) +public: + CppTypeInfo() = default; + + static CppTypeInfo fromString(QStringView target, const ErrorHandler &h = nullptr) + { + CppTypeInfo res; + QRegularExpression reTarget = QRegularExpression(QRegularExpression::anchoredPattern( + uR"(QList<(?<list>[a-zA-Z_0-9:]+) *(?<listPtr>\*?)>|QMap< *(?<mapKey>[a-zA-Z_0-9:]+) *, *(?<mapValue>[a-zA-Z_0-9:]+) *(?<mapPtr>\*?)>|(?<baseType>[a-zA-Z_0-9:]+) *(?<ptr>\*?))")); + + QRegularExpressionMatch m = reTarget.matchView(target); + if (!m.hasMatch()) { + DomItem::myResolveErrors() + .error(tr("Unexpected complex CppType %1").arg(target)) + .handle(h); + } + res.baseType = m.captured(u"baseType"); + res.isPointer = !m.captured(u"ptr").isEmpty(); + if (!m.captured(u"list").isEmpty()) { + res.isList = true; + res.baseType = m.captured(u"list"); + res.isPointer = !m.captured(u"listPtr").isEmpty(); + } + if (!m.captured(u"mapValue").isEmpty()) { + res.isMap = true; + if (m.captured(u"mapKey") != u"QString") { + DomItem::myResolveErrors() + .error(tr("Unexpected complex CppType %1 (map with non QString key)") + .arg(target)) + .handle(h); + } + res.baseType = m.captured(u"mapValue"); + res.isPointer = !m.captured(u"mapPtr").isEmpty(); + } + return res; + } + + QString baseType; + bool isPointer = false; + bool isMap = false; + bool isList = false; +}; + +static bool visitForLookupType(const DomItem &el, LookupType lookupType, + function_ref<bool(const DomItem &)> visitor) +{ + bool correctType = false; + DomType iType = el.internalKind(); + switch (lookupType) { + case LookupType::Binding: + correctType = (iType == DomType::Binding); + break; + case LookupType::Method: + correctType = (iType == DomType::MethodInfo); + break; + case LookupType::Property: + correctType = (iType == DomType::PropertyDefinition || iType == DomType::Binding); + break; + case LookupType::PropertyDef: + correctType = (iType == DomType::PropertyDefinition); + break; + case LookupType::Type: + correctType = (iType == DomType::Export); // accept direct QmlObject ref? + break; + default: + Q_ASSERT(false); + break; + } + if (correctType) + return visitor(el); + return true; +} + +static bool visitQualifiedNameLookup( + const DomItem &newIt, const QStringList &subpath, + function_ref<bool(const DomItem &)> visitor, LookupType lookupType, + const ErrorHandler &errorHandler, QList<Path> *visitedRefs) +{ + QVector<ResolveToDo> lookupToDos( + { ResolveToDo{ newIt, 1 } }); // invariant: always increase pathIndex to guarantee + // end even with only partial visited match + QList<QSet<quintptr>> lookupVisited(subpath.size() + 1); + while (!lookupToDos.isEmpty()) { + ResolveToDo tNow = lookupToDos.takeFirst(); + auto vNow = qMakePair(tNow.item.id(), tNow.pathIndex); + DomItem subNow = tNow.item; + int iSubPath = tNow.pathIndex; + Q_ASSERT(iSubPath < subpath.size()); + QString subPathNow = subpath[iSubPath++]; + DomItem scope = subNow.proceedToScope(); + if (iSubPath < subpath.size()) { + if (vNow.first != 0) { + if (lookupVisited[vNow.second].contains(vNow.first)) + continue; + else + lookupVisited[vNow.second].insert(vNow.first); + } + if (scope.internalKind() == DomType::QmlObject) + scope.visitDirectAccessibleScopes( + [&lookupToDos, &subPathNow, iSubPath](const DomItem &el) { + return el.visitLocalSymbolsNamed( + subPathNow, [&lookupToDos, iSubPath](const DomItem &subEl) { + lookupToDos.append({ subEl, iSubPath }); + return true; + }); + }, + VisitPrototypesOption::Normal, errorHandler, &(lookupVisited[vNow.second]), + visitedRefs); + } else { + bool cont = scope.visitDirectAccessibleScopes( + [&visitor, &subPathNow, lookupType](const DomItem &el) -> bool { + if (lookupType == LookupType::Symbol) + return el.visitLocalSymbolsNamed(subPathNow, visitor); + else + return el.visitLocalSymbolsNamed( + subPathNow, [lookupType, &visitor](const DomItem &el) -> bool { + return visitForLookupType(el, lookupType, visitor); + }); + }, + VisitPrototypesOption::Normal, errorHandler, &(lookupVisited[vNow.second]), + visitedRefs); + if (!cont) + return false; + } + } + return true; +} + +bool DomItem::visitLookup( + const QString &target, function_ref<bool(const DomItem &)> visitor, LookupType lookupType, + LookupOptions opts, const ErrorHandler &errorHandler, QSet<quintptr> *visited, + QList<Path> *visitedRefs) const +{ + if (target.isEmpty()) + return true; + switch (lookupType) { + case LookupType::Binding: + case LookupType::Method: + case LookupType::Property: + case LookupType::PropertyDef: + case LookupType::Symbol: + case LookupType::Type: { + QStringList subpath = target.split(QChar::fromLatin1('.')); + if (subpath.size() == 1) { + return visitLookup1(subpath.first(), visitor, opts, errorHandler, visited, visitedRefs); + } else { + return visitLookup1( + subpath.at(0), + [&subpath, visitor, lookupType, &errorHandler, + visitedRefs](const DomItem &newIt) -> bool { + return visitQualifiedNameLookup(newIt, subpath, visitor, lookupType, + errorHandler, visitedRefs); + }, + opts, errorHandler, visited, visitedRefs); + } + break; + } + case LookupType::CppType: { + QString baseTarget = CppTypeInfo::fromString(target, errorHandler).baseType; + DomItem localQmltypes = owner(); + while (localQmltypes && localQmltypes.internalKind() != DomType::QmltypesFile) { + localQmltypes = localQmltypes.containingObject(); + localQmltypes = localQmltypes.owner(); + } + if (localQmltypes) { + if (DomItem localTypes = localQmltypes.field(Fields::components).key(baseTarget)) { + bool cont = localTypes.visitIndexes([&visitor](const DomItem &els) { + return els.visitIndexes([&visitor](const DomItem &el) { + if (DomItem obj = el.field(Fields::objects).index(0)) + return visitor(obj); + return true; + }); + }); + if (!cont) + return false; + } + } + DomItem qmltypes = environment().field(Fields::qmltypesFileWithPath); + return qmltypes.visitKeys([baseTarget, &visitor](const QString &, const DomItem &els) { + DomItem comps = + els.field(Fields::currentItem).field(Fields::components).key(baseTarget); + return comps.visitIndexes([&visitor](const DomItem &el) { + if (DomItem obj = el.field(Fields::objects).index(0)) + return visitor(obj); + return true; + }); + }); + break; + } + } + Q_ASSERT(false); + return true; +} + +/*! + \internal + \brief Dereference DomItems pointing to other DomItems. + + Dereferences DomItems with internalKind being References, Export and Id. + Also does multiple rounds of resolving for nested DomItems. + Prefer this over \l {DomItem::get}. + */ +DomItem DomItem::proceedToScope(const ErrorHandler &h, QList<Path> *visitedRefs) const +{ + // follow references, resolve exports + DomItem current = *this; + while (current) { + switch (current.internalKind()) { + case DomType::Reference: { + Path currentPath = current.canonicalPath(); + current = current.get(h, visitedRefs); + break; + } + case DomType::Export: + current = current.field(Fields::type); + break; + case DomType::Id: + current = current.field(Fields::referredObject); + break; + default: + return current.scope(); + break; + } + } + return DomItem(); +} + +QList<DomItem> DomItem::lookup(const QString &symbolName, LookupType type, LookupOptions opts, + const ErrorHandler &errorHandler) const +{ + QList<DomItem> res; + visitLookup( + symbolName, + [&res](const DomItem &el) { + res.append(el); + return true; + }, + type, opts, errorHandler); + return res; +} + +DomItem DomItem::lookupFirst(const QString &symbolName, LookupType type, LookupOptions opts, + const ErrorHandler &errorHandler) const +{ + DomItem res; + visitLookup( + symbolName, + [&res](const DomItem &el) { + res = el; + return false; + }, + type, opts, errorHandler); + return res; +} + +quintptr DomItem::id() const +{ + return visitEl([](auto &&b) { return b->id(); }); +} + +Path DomItem::pathFromOwner() const +{ + return visitEl([this](auto &&e) { return e->pathFromOwner(*this); }); +} + +QString DomItem::canonicalFilePath() const +{ + return visitEl([this](auto &&e) { return e->canonicalFilePath(*this); }); +} + +DomItem DomItem::fileLocationsTree() const +{ + if (DomItem l = field(Fields::fileLocationsTree)) + return l; + auto res = FileLocations::findAttachedInfo(*this); + if (res && res.foundTreePath) { + return copy(res.foundTree, res.foundTreePath); + } + return DomItem(); +} + +DomItem DomItem::fileLocations() const +{ + return fileLocationsTree().field(Fields::infoItem); +} + +MutableDomItem DomItem::makeCopy(DomItem::CopyOption option) const +{ + if (m_kind == DomType::Empty) + return MutableDomItem(); + DomItem o = owner(); + if (option == CopyOption::EnvDisconnected) { + DomItem newItem = std::visit([this, &o](auto &&el) { + if constexpr (std::is_same_v<std::decay_t<decltype(el)>, std::monostate>) { + return DomItem(); + } else { + auto copyPtr = el->makeCopy(o); + return DomItem(m_top, copyPtr, m_ownerPath, copyPtr.get()); + } + }, m_owner); + return MutableDomItem(newItem.path(pathFromOwner())); + } + DomItem env = environment(); + std::shared_ptr<DomEnvironment> newEnvPtr; + if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) { + newEnvPtr = std::make_shared<DomEnvironment>(envPtr, envPtr->loadPaths(), envPtr->options(), + envPtr->domCreationOptions()); + DomBase *eBase = envPtr.get(); + if (std::holds_alternative<const DomEnvironment *>(m_element) && eBase + && std::get<const DomEnvironment *>(m_element) == eBase) + return MutableDomItem(DomItem(newEnvPtr)); + } else if (std::shared_ptr<DomUniverse> univPtr = top().ownerAs<DomUniverse>()) { + newEnvPtr = std::make_shared<DomEnvironment>( + QStringList(), + DomEnvironment::Option::SingleThreaded | DomEnvironment::Option::NoDependencies, + DomCreationOption::None, univPtr); + } else { + Q_ASSERT(false); + return {}; + } + DomItem newItem = std::visit( + [this, newEnvPtr, &o](auto &&el) { + if constexpr (std::is_same_v<std::decay_t<decltype(el)>, std::monostate>) { + return DomItem(); + } else { + auto copyPtr = el->makeCopy(o); + return DomItem(newEnvPtr, copyPtr, m_ownerPath, copyPtr.get()); + } + }, + m_owner); + + switch (o.internalKind()) { + case DomType::QmlDirectory: + newEnvPtr->addQmlDirectory(newItem.ownerAs<QmlDirectory>(), AddOption::Overwrite); + break; + case DomType::JsFile: + newEnvPtr->addJsFile(newItem.ownerAs<JsFile>(), AddOption::Overwrite); + break; + case DomType::QmlFile: + newEnvPtr->addQmlFile(newItem.ownerAs<QmlFile>(), AddOption::Overwrite); + break; + case DomType::QmltypesFile: + newEnvPtr->addQmltypesFile(newItem.ownerAs<QmltypesFile>(), AddOption::Overwrite); + break; + case DomType::GlobalScope: { + newEnvPtr->addGlobalScope(newItem.ownerAs<GlobalScope>(), AddOption::Overwrite); + } break; + case DomType::ModuleIndex: + case DomType::MockOwner: + case DomType::ScriptExpression: + case DomType::AstComments: + case DomType::LoadInfo: + case DomType::AttachedInfo: + case DomType::DomEnvironment: + case DomType::DomUniverse: + qCWarning(domLog) << "DomItem::makeCopy " << internalKindStr() + << " does not support binding to environment"; + Q_ASSERT(false); + return MutableDomItem(); + default: + qCWarning(domLog) << "DomItem::makeCopy(" << internalKindStr() + << ") is not an known OwningItem"; + Q_ASSERT(o.isOwningItem()); + return MutableDomItem(); + } + DomItem newEnv(newEnvPtr); + Q_ASSERT(newEnv.path(o.canonicalPath()).m_owner == newItem.m_owner); + return MutableDomItem(newItem.path(pathFromOwner())); +} + +bool DomItem::commitToBase(const std::shared_ptr<DomEnvironment> &validEnvPtr) const +{ + DomItem env = environment(); + if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) { + return envPtr->commitToBase(env, validEnvPtr); + } + return false; +} + +bool DomItem::visitLocalSymbolsNamed(const QString &name, function_ref<bool(const DomItem &)> visitor) const +{ + if (name.isEmpty()) // no empty symbol + return true; + // we could avoid discriminating by type and just access all the needed stuff in the correct + // sequence, making sure it is empty if not provided, but for now I find it clearer to check the + // type + DomItem f; + DomItem v; + switch (internalKind()) { + case DomType::QmlObject: + f = field(Fields::propertyDefs); + v = f.key(name); + if (!v.visitIndexes(visitor)) + return false; + f = field(Fields::bindings); + v = f.key(name); + if (!v.visitIndexes(visitor)) + return false; + f = field(Fields::methods); + v = f.key(name); + if (!v.visitIndexes(visitor)) + return false; + break; + case DomType::ScriptExpression: + // to do + break; + case DomType::QmlComponent: + f = field(Fields::ids); + v = f.key(name); + if (!v.visitIndexes(visitor)) + return false; + Q_FALLTHROUGH(); + case DomType::JsResource: + case DomType::GlobalComponent: + case DomType::QmltypesComponent: + f = field(Fields::enumerations); + v = f.key(name); + if (!v.visitIndexes(visitor)) + return false; + break; + case DomType::MethodInfo: { + DomItem params = field(Fields::parameters); + if (!params.visitIndexes([name, visitor](const DomItem &p) { + const MethodParameter *pPtr = p.as<MethodParameter>(); + if (pPtr->name == name && !visitor(p)) + return false; + return true; + })) + return false; + break; + } + case DomType::QmlFile: { + f = field(Fields::components); + v = f.key(name); + if (!v.visitIndexes(visitor)) + return false; + break; + } + case DomType::ImportScope: { + f = field(Fields::imported); + v = f.key(name); + if (!v.visitIndexes(visitor)) + return false; + f = field(Fields::qualifiedImports); + v = f.key(name); + if (v && !visitor(v)) + return false; + break; + default: + Q_ASSERT(!isScope()); + break; + } + } + return true; +} + +DomItem DomItem::operator[](const QString &cName) const +{ + if (internalKind() == DomType::Map) + return key(cName); + return field(cName); +} + +DomItem DomItem::operator[](QStringView cName) const +{ + if (internalKind() == DomType::Map) + return key(cName.toString()); + return field(cName); +} + +DomItem DomItem::operator[](const Path &p) const +{ + return path(p); +} + +QCborValue DomItem::value() const +{ + return base()->value(); +} + +void DomItem::dumpPtr(const Sink &sink) const +{ + sink(u"DomItem{ topPtr:"); + sink(QString::number((quintptr)topPtr().get(), 16)); + sink(u", ownerPtr:"); + sink(QString::number((quintptr)owningItemPtr().get(), 16)); + sink(u", ownerPath:"); + m_ownerPath.dump(sink); + sink(u", elPtr:"); + sink(QString::number((quintptr)base(),16)); + sink(u"}"); +} + +void DomItem::dump( + const Sink &s, int indent, + function_ref<bool(const DomItem &, const PathEls::PathComponent &, const DomItem &)> filter) const +{ + visitEl([this, s, indent, filter](auto &&e) { e->dump(*this, s, indent, filter); }); +} + +FileWriter::Status +DomItem::dump(const QString &path, + function_ref<bool(const DomItem &, const PathEls::PathComponent &, const DomItem &)> filter, + int nBackups, int indent, FileWriter *fw) const +{ + FileWriter localFw; + if (!fw) + fw = &localFw; + switch (fw->write( + path, + [this, indent, filter](QTextStream &ts) { + this->dump([&ts](QStringView s) { ts << s; }, indent, filter); + return true; + }, + nBackups)) { + case FileWriter::Status::ShouldWrite: + case FileWriter::Status::SkippedDueToFailure: + qWarning() << "Failure dumping " << canonicalPath() << " to " << path; + break; + case FileWriter::Status::DidWrite: + case FileWriter::Status::SkippedEqual: + break; + } + return fw->status; +} + +QString DomItem::toString() const +{ + return dumperToString([this](const Sink &s){ dump(s); }); +} + +int DomItem::derivedFrom() const +{ + return std::visit([](auto &&ow) { + if constexpr (std::is_same_v<std::decay_t<decltype(ow)>, std::monostate>) + return 0; + else + return ow->derivedFrom(); + }, m_owner); +} + +int DomItem::revision() const +{ + return std::visit([](auto &&ow) { + if constexpr (std::is_same_v<std::decay_t<decltype(ow)>, std::monostate>) + return -1; + else + return ow->revision(); + }, m_owner); +} + +QDateTime DomItem::createdAt() const +{ + return std::visit([](auto &&ow) { + if constexpr (std::is_same_v<std::decay_t<decltype(ow)>, std::monostate>) + return QDateTime::fromMSecsSinceEpoch(0, QTimeZone::UTC); + else + return ow->createdAt(); + }, m_owner); +} + +QDateTime DomItem::frozenAt() const +{ + return std::visit([](auto &&ow) { + if constexpr (std::is_same_v<std::decay_t<decltype(ow)>, std::monostate>) + return QDateTime::fromMSecsSinceEpoch(0, QTimeZone::UTC); + else + return ow->frozenAt(); + }, m_owner); +} + +QDateTime DomItem::lastDataUpdateAt() const +{ + return std::visit([](auto &&ow) { + if constexpr (std::is_same_v<std::decay_t<decltype(ow)>, std::monostate>) + return QDateTime::fromMSecsSinceEpoch(0, QTimeZone::UTC); + else + return ow->lastDataUpdateAt(); + }, m_owner); +} + +void DomItem::addError(ErrorMessage &&msg) const +{ + std::visit([this, &msg](auto &&ow) { + if constexpr (std::is_same_v<std::decay_t<decltype(ow)>, std::monostate>) + defaultErrorHandler(msg.withItem(*this)); + else + ow->addError(owner(), std::move(msg.withItem(*this))); + }, m_owner); +} + +ErrorHandler DomItem::errorHandler() const +{ + // We need a copy here. Error handlers may be called when this is gone. + return [self = *this](const ErrorMessage &m) { self.addError(ErrorMessage(m)); }; +} + +void DomItem::clearErrors(const ErrorGroups &groups, bool iterate) const +{ + std::visit([&groups](auto &&ow) { + if constexpr (!std::is_same_v<std::decay_t<decltype(ow)>, std::monostate>) + ow->clearErrors(groups); + }, m_owner); + + if (iterate) { + iterateSubOwners([groups](const DomItem &i){ + i.clearErrors(groups, true); + return true; + }); + } +} + +bool DomItem::iterateErrors( + function_ref<bool(const DomItem &, const ErrorMessage &)> visitor, bool iterate, + Path inPath) const +{ + if (!std::visit([this, visitor, inPath](auto &&el) { + if constexpr (std::is_same_v<std::decay_t<decltype(el)>, std::monostate>) + return true; + else + return el->iterateErrors(owner(), visitor, inPath); + }, m_owner)) { + return false; + } + + if (iterate && !iterateSubOwners([inPath, visitor](const DomItem &i) { + return i.iterateErrors(visitor, true, inPath); + })) { + return false; + } + + return true; +} + +bool DomItem::iterateSubOwners(function_ref<bool(const DomItem &)> visitor) const +{ + return std::visit([this, visitor](auto &&o) { + if constexpr (std::is_same_v<std::decay_t<decltype(o)>, std::monostate>) + return true; + else + return o->iterateSubOwners(owner(), visitor); + }, m_owner); +} + +bool DomItem::iterateDirectSubpaths(DirectVisitor v) const +{ + return visitEl( + [this, v](auto &&el) { return el->iterateDirectSubpaths(*this, v); }); +} + +DomItem DomItem::subReferencesItem(const PathEls::PathComponent &c, const QList<Path> &paths) const +{ + return subListItem( + List::fromQList<Path>(pathFromOwner().appendComponent(c), paths, + [](const DomItem &list, const PathEls::PathComponent &p, const Path &el) { + return list.subReferenceItem(p, el); + })); +} + +DomItem DomItem::subReferenceItem(const PathEls::PathComponent &c, const Path &referencedObject) const +{ + if (domTypeIsOwningItem(internalKind())) { + return DomItem(m_top, m_owner, m_ownerPath, Reference(referencedObject, Path(c))); + } else { + return DomItem(m_top, m_owner, m_ownerPath, + Reference(referencedObject, pathFromOwner().appendComponent(c))); + } +} + +shared_ptr<DomTop> DomItem::topPtr() const +{ + return std::visit([](auto &&el) -> shared_ptr<DomTop> { + if constexpr (std::is_same_v<std::decay_t<decltype(el)>, std::monostate>) + return {}; + else + return el; + }, m_top); +} + +shared_ptr<OwningItem> DomItem::owningItemPtr() const +{ + return std::visit([](auto &&el) -> shared_ptr<OwningItem> { + if constexpr (std::is_same_v<std::decay_t<decltype(el)>, std::monostate>) + return {}; + else + return el; + }, m_owner); +} + +/*! + \internal + Returns a pointer to the virtual base pointer to a DomBase. +*/ +const DomBase *DomItem::base() const +{ + return visitEl([](auto &&el) -> const DomBase * { return el->domBase(); }); +} + +DomItem::DomItem(const std::shared_ptr<DomEnvironment> &envPtr): + DomItem(envPtr, envPtr, Path(), envPtr.get()) +{ +} + +DomItem::DomItem(const std::shared_ptr<DomUniverse> &universePtr): + DomItem(universePtr, universePtr, Path(), universePtr.get()) +{ +} + +/*! +\brief Creates a new document with the given code + +This is mostly useful for testing or loading a single code snippet without any dependency. +The fileType should normally be QmlFile, but you might want to load a qmltypes file for +example and interpret it as qmltypes file (not plain Qml), or as JsFile. In those case +set the file type accordingly. +*/ +DomItem DomItem::fromCode(const QString &code, DomType fileType) +{ + if (code.isEmpty()) + return DomItem(); + auto env = + DomEnvironment::create(QStringList(), + QQmlJS::Dom::DomEnvironment::Option::SingleThreaded + | QQmlJS::Dom::DomEnvironment::Option::NoDependencies); + + DomItem tFile; + + env->loadFile( + FileToLoad::fromMemory(env, QString(), code), + [&tFile](Path, const DomItem &, const DomItem &newIt) { tFile = newIt; }, + std::make_optional(fileType)); + env->loadPendingDependencies(); + return tFile.fileObject(); +} + +Empty::Empty() +{} + +Path Empty::pathFromOwner(const DomItem &) const +{ + return Path(); +} + +Path Empty::canonicalPath(const DomItem &) const +{ + return Path(); +} + +bool Empty::iterateDirectSubpaths(const DomItem &, DirectVisitor) const +{ + return true; +} + +DomItem Empty::containingObject(const DomItem &self) const +{ + return self; +} + +void Empty::dump( + const DomItem &, const Sink &s, int, + function_ref<bool(const DomItem &, const PathEls::PathComponent &, const DomItem &)>) const +{ + s(u"null"); +} + +Map::Map(const Path &pathFromOwner, const Map::LookupFunction &lookup, + const Keys &keys, const QString &targetType) + : DomElement(pathFromOwner), m_lookup(lookup), m_keys(keys), m_targetType(targetType) +{} + +quintptr Map::id() const +{ + return quintptr(0); +} + +bool Map::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const +{ + QSet<QString> ksSet = keys(self); + QStringList ksList = QStringList(ksSet.begin(), ksSet.end()); + std::sort(ksList.begin(), ksList.end()); + for (const QString &k : std::as_const(ksList)) { + if (!visitor(PathEls::Key(k), [&self, this, k]() { return key(self, k); })) + return false; + } + return true; +} + +const QSet<QString> Map::keys(const DomItem &self) const +{ + return m_keys(self); +} + +DomItem Map::key(const DomItem &self, const QString &name) const +{ + return m_lookup(self, name); +} + +void DomBase::dump( + const DomItem &self, const Sink &sink, int indent, + function_ref<bool(const DomItem &, const PathEls::PathComponent &, const DomItem &)> filter) const +{ + bool comma = false; + DomKind dK = self.domKind(); + switch (dK) { + case DomKind::Object: + sink(u"{ \"~type~\":"); + sinkEscaped(sink, typeName()); + comma = true; + break; + case DomKind::Value: + { + QJsonValue v = value().toJsonValue(); + if (v.isString()) + sinkEscaped(sink, v.toString()); + else if (v.isBool()) + if (v.toBool()) + sink(u"true"); + else + sink(u"false"); + else if (v.isDouble()) + if (value().isInteger()) + sink(QString::number(value().toInteger())); + else + sink(QString::number(v.toDouble())); + else { + sink(QString::fromUtf8(QJsonDocument::fromVariant(v.toVariant()).toJson())); + } + break; + } + case DomKind::Empty: + sink(u"null"); + break; + case DomKind::List: + sink(u"["); + break; + case DomKind::Map: + sink(u"{"); + break; + case DomKind::ScriptElement: + // nothing to print + break; + } + auto closeParens = qScopeGuard( + [dK, sink, indent]{ + switch (dK) { + case DomKind::Object: + sinkNewline(sink, indent); + sink(u"}"); + break; + case DomKind::Value: + break; + case DomKind::Empty: + break; + case DomKind::List: + sinkNewline(sink, indent); + sink(u"]"); + break; + case DomKind::Map: + sinkNewline(sink, indent); + sink(u"}"); + break; + case DomKind::ScriptElement: + // nothing to print + break; + } + }); + index_type idx = 0; + self.iterateDirectSubpaths( + [&comma, &idx, dK, sink, indent, &self, filter](const PathEls::PathComponent &c, + function_ref<DomItem()> itemF) { + DomItem i = itemF(); + if (!filter(self, c, i)) + return true; + if (comma) + sink(u","); + else + comma = true; + switch (c.kind()) { + case Path::Kind::Field: + sinkNewline(sink, indent + 2); + if (dK != DomKind::Object) + sink(u"UNEXPECTED ENTRY ERROR:"); + sinkEscaped(sink, c.name()); + sink(u":"); + break; + case Path::Kind::Key: + sinkNewline(sink, indent + 2); + if (dK != DomKind::Map) + sink(u"UNEXPECTED ENTRY ERROR:"); + sinkEscaped(sink, c.name()); + sink(u":"); + break; + case Path::Kind::Index: + sinkNewline(sink, indent + 2); + if (dK != DomKind::List) + sink(u"UNEXPECTED ENTRY ERROR:"); + else if (idx++ != c.index()) + sink(u"OUT OF ORDER ARRAY:"); + break; + default: + sinkNewline(sink, indent + 2); + sink(u"UNEXPECTED PATH KIND ERROR (ignored)"); + break; + } + if (self.isCanonicalChild(i)) { + i.dump(sink, indent + 2, filter); + } else { + sink(uR"({ "~type~": "Reference", "immediate": true, "referredObjectPath":")"); + i.canonicalPath().dump([sink](QStringView s) { + sinkEscaped(sink, s, EscapeOptions::NoOuterQuotes); + }); + sink(u"\"}"); + } + return true; + }); +} + +List::List(const Path &pathFromOwner, const List::LookupFunction &lookup, + const List::Length &length, const List::IteratorFunction &iterator, + const QString &elType): + DomElement(pathFromOwner), m_lookup(lookup), m_length(length), m_iterator(iterator), + m_elType(elType) +{} + +quintptr List::id() const +{ + return quintptr(0); +} + +void List::dump( + const DomItem &self, const Sink &sink, int indent, + function_ref<bool(const DomItem &, const PathEls::PathComponent &, const DomItem &)> filter) const +{ + bool first = true; + sink(u"["); + iterateDirectSubpaths( + self, + [&self, indent, &first, sink, filter](const PathEls::PathComponent &c, + function_ref<DomItem()> itemF) { + DomItem item = itemF(); + if (!filter(self, c, item)) + return true; + if (first) + first = false; + else + sink(u","); + sinkNewline(sink, indent + 2); + item.dump(sink, indent + 2, filter); + return true; + }); + sink(u"]"); +} + +bool List::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const +{ + if (m_iterator) { + return m_iterator(self, [visitor](index_type i, function_ref<DomItem()> itemF) { + return visitor(PathEls::Index(i), itemF); + }); + } + index_type len = indexes(self); + for (index_type i = 0; i < len; ++i) { + if (!visitor(PathEls::Index(i), [this, &self, i]() { return index(self, i); })) + return false; + } + return true; +} + +index_type List::indexes(const DomItem &self) const +{ + return m_length(self); +} + +DomItem List::index(const DomItem &self, index_type index) const +{ + return m_lookup(self, index); +} + +void List::writeOut(const DomItem &self, OutWriter &ow, bool compact) const +{ + ow.writeRegion(LeftBracketRegion); + int baseIndent = ow.increaseIndent(1); + bool first = true; + iterateDirectSubpaths( + self, + [&ow, &first, compact](const PathEls::PathComponent &, function_ref<DomItem()> elF) { + if (first) + first = false; + else + ow.write(u", ", LineWriter::TextAddType::Extra); + if (!compact) + ow.ensureNewline(1); + DomItem el = elF(); + el.writeOut(ow); + return true; + }); + if (!compact && !first) + ow.newline(); + ow.decreaseIndent(1, baseIndent); + ow.writeRegion(RightBracketRegion); +} + +DomElement::DomElement(const Path &pathFromOwner) : m_pathFromOwner(pathFromOwner) { } + +Path DomElement::pathFromOwner(const DomItem &) const +{ + Q_ASSERT(m_pathFromOwner && "uninitialized DomElement"); + return m_pathFromOwner; +} + +Path DomElement::canonicalPath(const DomItem &self) const +{ + Q_ASSERT(m_pathFromOwner && "uninitialized DomElement"); + return self.owner().canonicalPath().path(m_pathFromOwner); +} + +DomItem DomElement::containingObject(const DomItem &self) const +{ + Q_ASSERT(m_pathFromOwner && "uninitialized DomElement"); + return DomBase::containingObject(self); +} + +void DomElement::updatePathFromOwner(const Path &newPath) +{ + //if (!domTypeCanBeInline(kind())) + m_pathFromOwner = newPath; +} + +bool Reference::shouldCache() const +{ + for (const Path &p : referredObjectPath) { + switch (p.headKind()) { + case Path::Kind::Current: + switch (p.headCurrent()) { + case PathCurrent::Lookup: + case PathCurrent::LookupDynamic: + case PathCurrent::LookupStrict: + case PathCurrent::ObjChain: + case PathCurrent::ScopeChain: + return true; + default: + break; + } + break; + case Path::Kind::Empty: + case Path::Kind::Any: + case Path::Kind::Filter: + return true; + default: + break; + } + } + return false; +} + +Reference::Reference(const Path &referredObject, const Path &pathFromOwner, const SourceLocation &) + : DomElement(pathFromOwner), referredObjectPath(referredObject) +{ +} + +quintptr Reference::id() const +{ + return quintptr(0); +} + +bool Reference::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const +{ + bool cont = true; + cont = cont && self.dvValueLazyField(visitor, Fields::referredObjectPath, [this]() { + return referredObjectPath.toString(); + }); + cont = cont + && self.dvItemField(visitor, Fields::get, [this, &self]() { return this->get(self); }); + return cont; +} + +DomItem Reference::field(const DomItem &self, QStringView name) const +{ + if (Fields::referredObjectPath == name) + return self.subDataItemField(Fields::referredObjectPath, referredObjectPath.toString()); + if (Fields::get == name) + return get(self); + return DomItem(); +} + +QList<QString> Reference::fields(const DomItem &) const +{ + return QList<QString>({QString::fromUtf16(Fields::referredObjectPath), QString::fromUtf16(Fields::get)}); +} + +DomItem Reference::index(const DomItem &, index_type) const +{ + return DomItem(); +} + +DomItem Reference::key(const DomItem &, const QString &) const +{ + return DomItem(); +} + +DomItem Reference::get(const DomItem &self, const ErrorHandler &h, QList<Path> *visitedRefs) const +{ + DomItem res; + if (referredObjectPath) { + DomItem env; + Path selfPath; + Path cachedPath; + if (shouldCache()) { + env = self.environment(); + if (env) { + selfPath = self.canonicalPath(); + RefCacheEntry cached = RefCacheEntry::forPath(self, selfPath); + switch (cached.cached) { + case RefCacheEntry::Cached::None: + break; + case RefCacheEntry::Cached::First: + case RefCacheEntry::Cached::All: + if (!cached.canonicalPaths.isEmpty()) + cachedPath = cached.canonicalPaths.first(); + else + return res; + break; + } + if (cachedPath) { + res = env.path(cachedPath); + if (!res) + qCWarning(refLog) << "referenceCache outdated, reference at " << selfPath + << " leads to invalid path " << cachedPath; + else + return res; + } + } + } + QList<Path> visitedRefsLocal; + self.resolve( + referredObjectPath, + [&res](Path, const DomItem &el) { + res = el; + return false; + }, + h, ResolveOption::None, referredObjectPath, + (visitedRefs ? visitedRefs : &visitedRefsLocal)); + if (env) + RefCacheEntry::addForPath( + env, selfPath, RefCacheEntry { RefCacheEntry::Cached::First, { cachedPath } }); + } + return res; +} + +QList<DomItem> Reference::getAll( + const DomItem &self, const ErrorHandler &h, QList<Path> *visitedRefs) const +{ + QList<DomItem> res; + if (referredObjectPath) { + DomItem env; + Path selfPath; + QList<Path> cachedPaths; + if (shouldCache()) { + selfPath = canonicalPath(self); + env = self.environment(); + RefCacheEntry cached = RefCacheEntry::forPath(env, selfPath); + switch (cached.cached) { + case RefCacheEntry::Cached::None: + case RefCacheEntry::Cached::First: + break; + case RefCacheEntry::Cached::All: + cachedPaths += cached.canonicalPaths; + if (cachedPaths.isEmpty()) + return res; + } + } + if (!cachedPaths.isEmpty()) { + bool outdated = false; + for (const Path &p : cachedPaths) { + DomItem newEl = env.path(p); + if (!newEl) { + outdated = true; + qCWarning(refLog) << "referenceCache outdated, reference at " << selfPath + << " leads to invalid path " << p; + break; + } else { + res.append(newEl); + } + } + if (outdated) { + res.clear(); + } else { + return res; + } + } + self.resolve( + referredObjectPath, + [&res](Path, const DomItem &el) { + res.append(el); + return true; + }, + h, ResolveOption::None, referredObjectPath, visitedRefs); + if (env) { + QList<Path> canonicalPaths; + for (const DomItem &i : res) { + if (i) + canonicalPaths.append(i.canonicalPath()); + else + qCWarning(refLog) + << "getAll of reference at " << selfPath << " visits empty items."; + } + RefCacheEntry::addForPath( + env, selfPath, + RefCacheEntry { RefCacheEntry::Cached::All, std::move(canonicalPaths) }); + } + } + return res; +} + +/*! +\internal +\class QQmlJS::Dom::OwningItem + +\brief A DomItem that owns other DomItems and is managed through a shared pointer + +This is the unit of update of the Dom model, only OwningItem can be updated after +exposed, to update a single element one has to copy its owner, change it, and expose an new one. +The non owning contents of an exposed OwiningItem can safely be considered immutable, and pointers +to them must remain valid for the whole lifetime of the OwningItem (that is managed with +shared_ptr), only the sub OwningItems *might* be change. +The OwningItem has a mutex that controls it access (mainly for the errors, observers, and sub +OwningItems), Access to the rest is *not* controlled, it should either be by a single thread +(during construction), or immutable (after pubblication). + +*/ + +OwningItem::OwningItem(int derivedFrom) + : m_derivedFrom(derivedFrom), + m_revision(nextRevision()), + m_createdAt(QDateTime::currentDateTimeUtc()), + m_lastDataUpdateAt(m_createdAt), + m_frozenAt(QDateTime::fromMSecsSinceEpoch(0, QTimeZone::UTC)) +{} + +OwningItem::OwningItem(int derivedFrom, const QDateTime &lastDataUpdateAt) + : m_derivedFrom(derivedFrom), + m_revision(nextRevision()), + m_createdAt(QDateTime::currentDateTimeUtc()), + m_lastDataUpdateAt(lastDataUpdateAt), + m_frozenAt(QDateTime::fromMSecsSinceEpoch(0, QTimeZone::UTC)) +{} + +OwningItem::OwningItem(const OwningItem &o) + : m_derivedFrom(o.revision()), + m_revision(nextRevision()), + m_createdAt(QDateTime::currentDateTimeUtc()), + m_lastDataUpdateAt(o.lastDataUpdateAt()), + m_frozenAt(QDateTime::fromMSecsSinceEpoch(0, QTimeZone::UTC)) +{ + QMultiMap<Path, ErrorMessage> my_errors; + { + QMutexLocker l1(o.mutex()); + my_errors = o.m_errors; + + } + { + QMutexLocker l2(mutex()); + m_errors = my_errors; + } +} + + +int OwningItem::nextRevision() +{ + static QAtomicInt nextRev(0); + return ++nextRev; +} + +bool OwningItem::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const +{ + bool cont = true; + cont = cont && self.dvItemField(visitor, Fields::errors, [&self, this]() { + QMultiMap<Path, ErrorMessage> myErrors = localErrors(); + return self.subMapItem(Map( + self.pathFromOwner().field(Fields::errors), + [myErrors](const DomItem &map, const QString &key) { + auto it = myErrors.find(Path::fromString(key)); + if (it != myErrors.end()) + return map.subDataItem(PathEls::Key(key), it->toCbor(), + ConstantData::Options::FirstMapIsFields); + else + return DomItem(); + }, + [myErrors](const DomItem &) { + QSet<QString> res; + auto it = myErrors.keyBegin(); + auto end = myErrors.keyEnd(); + while (it != end) + res.insert(it++->toString()); + return res; + }, + QLatin1String("ErrorMessages"))); + }); + return cont; +} + +DomItem OwningItem::containingObject(const DomItem &self) const +{ + Source s = self.canonicalPath().split(); + if (s.pathFromSource) { + if (!s.pathToSource) + return DomItem(); + return self.path(s.pathToSource); + } + return DomItem(); +} + +int OwningItem::derivedFrom() const +{ + return m_derivedFrom; +} + +int OwningItem::revision() const +{ + return m_revision; +} + +bool OwningItem::frozen() const +{ + return m_frozenAt > m_createdAt; +} + +bool OwningItem::freeze() +{ + if (!frozen()) { + m_frozenAt = QDateTime::currentDateTimeUtc(); + if (m_frozenAt <= m_createdAt) + m_frozenAt = m_createdAt.addSecs(1); + return true; + } + return false; +} + +QDateTime OwningItem::createdAt() const +{ + return m_createdAt; +} + +QDateTime OwningItem::lastDataUpdateAt() const +{ + return m_lastDataUpdateAt; +} + +QDateTime OwningItem::frozenAt() const +{ + return m_frozenAt; +} + +void OwningItem::refreshedDataAt(QDateTime tNew) +{ + if (m_lastDataUpdateAt < tNew) // remove check? + m_lastDataUpdateAt = tNew; +} + +void OwningItem::addError(const DomItem &, ErrorMessage &&msg) +{ + addErrorLocal(std::move(msg)); +} + +void OwningItem::addErrorLocal(ErrorMessage &&msg) +{ + QMutexLocker l(mutex()); + quint32 &c = m_errorsCounts[msg]; + c += 1; + if (c == 1) + m_errors.insert(msg.path, msg); +} + +void OwningItem::clearErrors(const ErrorGroups &groups) +{ + QMutexLocker l(mutex()); + auto it = m_errors.begin(); + while (it != m_errors.end()) { + if (it->errorGroups == groups) + it = m_errors.erase(it); + else + ++it; + } +} + +bool OwningItem::iterateErrors( + const DomItem &self, function_ref<bool(const DomItem &, const ErrorMessage &)> visitor, + const Path &inPath) +{ + QMultiMap<Path, ErrorMessage> myErrors; + { + QMutexLocker l(mutex()); + myErrors = m_errors; + } + auto it = myErrors.lowerBound(inPath); + auto end = myErrors.end(); + while (it != end && it.key().mid(0, inPath.length()) == inPath) { + if (!visitor(self, *it++)) + return false; + } + return true; +} + +bool OwningItem::iterateSubOwners(const DomItem &self, function_ref<bool(const DomItem &owner)> visitor) +{ + return self.iterateDirectSubpaths( + [&self, visitor](const PathEls::PathComponent &, function_ref<DomItem()> iF) { + DomItem i = iF(); + if (i.owningItemPtr() != self.owningItemPtr()) { + DomItem container = i.container(); + if (container.id() == self.id()) + return visitor(i); + } + return true; + }); +} + +bool operator==(const DomItem &o1, const DomItem &o2) +{ + if (o1.m_kind != o2.m_kind) + return false; + return o1.visitEl([&o1, &o2](auto &&el1) { + auto &&el2 = std::get<std::decay_t<decltype(el1)>>(o2.m_element); + auto id1 = el1->id(); + auto id2 = el2->id(); + if (id1 != id2) + return false; + if (id1 != quintptr(0)) + return true; + if (o1.m_owner != o2.m_owner) + return false; + Path p1 = el1->pathFromOwner(o1); + Path p2 = el2->pathFromOwner(o2); + if (p1 != p2) + return false; + return true; + }); +} + +ErrorHandler MutableDomItem::errorHandler() +{ + MutableDomItem self; + return [&self](const ErrorMessage &m) { self.addError(ErrorMessage(m)); }; +} + +MutableDomItem MutableDomItem::addPrototypePath(const Path &prototypePath) +{ + if (QmlObject *el = mutableAs<QmlObject>()) { + return path(el->addPrototypePath(prototypePath)); + } else { + Q_ASSERT(false && "setPrototypePath on non qml scope"); + return {}; + } +} + +MutableDomItem MutableDomItem::setNextScopePath(const Path &nextScopePath) +{ + if (QmlObject *el = mutableAs<QmlObject>()) { + el->setNextScopePath(nextScopePath); + return field(Fields::nextScope); + } else { + Q_ASSERT(false && "setNextScopePath on non qml scope"); + return {}; + } +} + +MutableDomItem MutableDomItem::setPropertyDefs(QMultiMap<QString, PropertyDefinition> propertyDefs) +{ + if (QmlObject *el = mutableAs<QmlObject>()) { + el->setPropertyDefs(propertyDefs); + return field(Fields::propertyDefs); + } else { + Q_ASSERT(false && "setPropertyDefs on non qml scope"); + return {}; + } +} + +MutableDomItem MutableDomItem::setBindings(QMultiMap<QString, Binding> bindings) +{ + if (QmlObject *el = mutableAs<QmlObject>()) { + el->setBindings(bindings); + return field(Fields::bindings); + } else { + Q_ASSERT(false && "setBindings on non qml scope"); + return {}; + } +} + +MutableDomItem MutableDomItem::setMethods(QMultiMap<QString, MethodInfo> functionDefs) +{ + if (QmlObject *el = mutableAs<QmlObject>()) + el->setMethods(functionDefs); + else + Q_ASSERT(false && "setMethods on non qml scope"); + return {}; +} + +MutableDomItem MutableDomItem::setChildren(const QList<QmlObject> &children) +{ + if (QmlObject *el = mutableAs<QmlObject>()) { + el->setChildren(children); + return field(Fields::children); + } else + Q_ASSERT(false && "setChildren on non qml scope"); + return {}; +} + +MutableDomItem MutableDomItem::setAnnotations(const QList<QmlObject> &annotations) +{ + if (QmlObject *el = mutableAs<QmlObject>()) + el->setAnnotations(annotations); + else if (Binding *el = mutableAs<Binding>()) { + el->setAnnotations(annotations); + el->updatePathFromOwner(pathFromOwner()); + } else if (PropertyDefinition *el = mutableAs<PropertyDefinition>()) { + el->annotations = annotations; + el->updatePathFromOwner(pathFromOwner()); + } else if (MethodInfo *el = mutableAs<MethodInfo>()) { + el->annotations = annotations; + el->updatePathFromOwner(pathFromOwner()); + } else if (EnumDecl *el = mutableAs<EnumDecl>()) { + el->setAnnotations(annotations); + el->updatePathFromOwner(pathFromOwner()); + } else if (!annotations.isEmpty()) { + Q_ASSERT(false && "setAnnotations on element not supporting them"); + } + return field(Fields::annotations); +} +MutableDomItem MutableDomItem::setScript(const std::shared_ptr<ScriptExpression> &exp) +{ + switch (internalKind()) { + case DomType::Binding: + if (Binding *b = mutableAs<Binding>()) { + b->setValue(std::make_unique<BindingValue>(exp)); + return field(Fields::value); + } + break; + case DomType::MethodInfo: + if (MethodInfo *m = mutableAs<MethodInfo>()) { + m->body = exp; + return field(Fields::body); + } + break; + case DomType::MethodParameter: + if (MethodParameter *p = mutableAs<MethodParameter>()) { + if (exp->expressionType() == ScriptExpression::ExpressionType::ArgInitializer) { + p->defaultValue = exp; + return field(Fields::defaultValue); + } + if (exp->expressionType() == ScriptExpression::ExpressionType::ArgumentStructure) { + p->value = exp; + return field(Fields::value); + } + } + break; + case DomType::ScriptExpression: + return container().setScript(exp); + default: + qCWarning(domLog) << "setScript called on" << internalKindStr(); + Q_ASSERT_X(false, "setScript", + "setScript supported only on Binding, MethodInfo, MethodParameter, and " + "ScriptExpression contained in them"); + } + return MutableDomItem(); +} + +MutableDomItem MutableDomItem::setCode(const QString &code) +{ + DomItem it = item(); + switch (it.internalKind()) { + case DomType::Binding: + if (Binding *b = mutableAs<Binding>()) { + auto exp = std::make_shared<ScriptExpression>( + code, ScriptExpression::ExpressionType::BindingExpression); + b->setValue(std::make_unique<BindingValue>(exp)); + return field(Fields::value); + } + break; + case DomType::MethodInfo: + if (MethodInfo *m = mutableAs<MethodInfo>()) { + QString pre = m->preCode(it); + QString post = m->preCode(it); + m->body = std::make_shared<ScriptExpression>( + code, ScriptExpression::ExpressionType::FunctionBody, 0, pre, post); + return field(Fields::body); + } + break; + case DomType::MethodParameter: + if (MethodParameter *p = mutableAs<MethodParameter>()) { + p->defaultValue = std::make_shared<ScriptExpression>( + code, ScriptExpression::ExpressionType::ArgInitializer); + return field(Fields::defaultValue); + } + break; + case DomType::ScriptExpression: + if (std::shared_ptr<ScriptExpression> exp = ownerAs<ScriptExpression>()) { + std::shared_ptr<ScriptExpression> newExp = exp->copyWithUpdatedCode(it, code); + return container().setScript(newExp); + } + break; + default: + qCWarning(domLog) << "setCode called on" << internalKindStr(); + Q_ASSERT_X( + false, "setCode", + "setCode supported only on Binding, MethodInfo, MethodParameter, ScriptExpression"); + } + return MutableDomItem(); +} + +MutableDomItem MutableDomItem::addPropertyDef( + const PropertyDefinition &propertyDef, AddOption option) +{ + if (QmlObject *el = mutableAs<QmlObject>()) + return el->addPropertyDef(*this, propertyDef, option); + else + Q_ASSERT(false && "addPropertyDef on non qml scope"); + return MutableDomItem(); +} + +MutableDomItem MutableDomItem::addBinding(Binding binding, AddOption option) +{ + if (QmlObject *el = mutableAs<QmlObject>()) + return el->addBinding(*this, binding, option); + else + Q_ASSERT(false && "addBinding on non qml scope"); + return MutableDomItem(); +} + +MutableDomItem MutableDomItem::addMethod(const MethodInfo &functionDef, AddOption option) +{ + if (QmlObject *el = mutableAs<QmlObject>()) + return el->addMethod(*this, functionDef, option); + else + Q_ASSERT(false && "addMethod on non qml scope"); + return MutableDomItem(); +} + +MutableDomItem MutableDomItem::addChild(QmlObject child) +{ + if (QmlObject *el = mutableAs<QmlObject>()) { + return el->addChild(*this, child); + } else if (QmlComponent *el = mutableAs<QmlComponent>()) { + Path p = el->addObject(child); + return owner().path(p); // convenience: treat component objects as children + } else { + Q_ASSERT(false && "addChild on non qml scope"); + } + return MutableDomItem(); +} + +MutableDomItem MutableDomItem::addAnnotation(QmlObject annotation) +{ + Path res; + switch (internalKind()) { + case DomType::QmlObject: { + QmlObject *el = mutableAs<QmlObject>(); + res = el->addAnnotation(annotation); + } break; + case DomType::Binding: { + Binding *el = mutableAs<Binding>(); + + res = el->addAnnotation(m_pathFromOwner, annotation); + } break; + case DomType::PropertyDefinition: { + PropertyDefinition *el = mutableAs<PropertyDefinition>(); + res = el->addAnnotation(m_pathFromOwner, annotation); + } break; + case DomType::MethodInfo: { + MethodInfo *el = mutableAs<MethodInfo>(); + res = el->addAnnotation(m_pathFromOwner, annotation); + } break; + case DomType::Id: { + Id *el = mutableAs<Id>(); + res = el->addAnnotation(m_pathFromOwner, annotation); + } break; + default: + Q_ASSERT(false && "addAnnotation on element not supporting them"); + } + return MutableDomItem(owner().item(), res); +} + +MutableDomItem MutableDomItem::addPreComment(const Comment &comment, FileLocationRegion region) +{ + index_type idx; + MutableDomItem rC = field(Fields::comments); + if (auto rcPtr = rC.mutableAs<RegionComments>()) { + auto commentedElement = rcPtr->regionComments()[region]; + idx = commentedElement.preComments().size(); + commentedElement.addComment(comment); + MutableDomItem res = path(Path::Field(Fields::comments) + .field(Fields::regionComments) + .key(fileLocationRegionName(region)) + .field(Fields::preComments) + .index(idx)); + Q_ASSERT(res); + return res; + } + return MutableDomItem(); +} + +MutableDomItem MutableDomItem::addPostComment(const Comment &comment, FileLocationRegion region) +{ + index_type idx; + MutableDomItem rC = field(Fields::comments); + if (auto rcPtr = rC.mutableAs<RegionComments>()) { + auto commentedElement = rcPtr->regionComments()[region]; + idx = commentedElement.postComments().size(); + commentedElement.addComment(comment); + MutableDomItem res = path(Path::Field(Fields::comments) + .field(Fields::regionComments) + .key(fileLocationRegionName(region)) + .field(Fields::postComments) + .index(idx)); + Q_ASSERT(res); + return res; + } + return MutableDomItem(); +} + +QDebug operator<<(QDebug debug, const DomItem &c) +{ + dumperToQDebug([&c](const Sink &s) { c.dump(s); }, debug); + return debug; +} + +QDebug operator<<(QDebug debug, const MutableDomItem &c) +{ + MutableDomItem cc(c); + return debug.noquote().nospace() << "MutableDomItem(" << domTypeToString(cc.internalKind()) + << ", " << cc.canonicalPath().toString() << ")"; +} + +bool ListPBase::iterateDirectSubpaths(const DomItem &self, DirectVisitor v) const +{ + index_type len = index_type(m_pList.size()); + for (index_type i = 0; i < len; ++i) { + if (!v(PathEls::Index(i), [this, &self, i] { return this->index(self, i); })) + return false; + } + return true; +} + +void ListPBase::writeOut(const DomItem &self, OutWriter &ow, bool compact) const +{ + ow.writeRegion(LeftBracketRegion); + int baseIndent = ow.increaseIndent(1); + bool first = true; + index_type len = index_type(m_pList.size()); + for (index_type i = 0; i < len; ++i) { + if (first) + first = false; + else + ow.write(u", ", LineWriter::TextAddType::Extra); + if (!compact) + ow.ensureNewline(1); + DomItem el = index(self, i); + el.writeOut(ow); + } + if (!compact && !first) + ow.newline(); + ow.decreaseIndent(1, baseIndent); + ow.writeRegion(RightBracketRegion); +} + +QQmlJSScope::ConstPtr ScriptElement::semanticScope() +{ + return m_scope; +} +void ScriptElement::setSemanticScope(const QQmlJSScope::ConstPtr &scope) +{ + m_scope = scope; +} + +/*! + \internal + \brief Returns a pointer to the virtual base for virtual method calls. + + A helper to call virtual methods without having to call std::visit(...). + */ +ScriptElement::PointerType<ScriptElement> ScriptElementVariant::base() const +{ + if (!m_data) + return nullptr; + + return std::visit( + [](auto &&e) { + // std::reinterpret_pointer_cast does not exist on qnx it seems... + return std::shared_ptr<ScriptElement>( + e, static_cast<ScriptElement *>(e.get())); + }, + *m_data); +} + +} // end namespace Dom +} // end namespace QQmlJS + +QT_END_NAMESPACE + +#include "moc_qqmldomitem_p.cpp" |