Revision control
Copy as Markdown
// 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 "qqmldomfilelocations_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 <qqmldomlinewriterfactory_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/QRegularExpression>
#include <QtCore/QScopeGuard>
#include <QtCore/QtGlobal>
#include <QtCore/QTimeZone>
#include <optional>
#include <type_traits>
#include <utility>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(writeOutLog, "qt.qmldom.writeOut", QtWarningMsg);
Q_STATIC_LOGGING_CATEGORY(refLog, "qt.qmldom.ref", QtWarningMsg);
namespace QQmlJS {
namespace Dom {
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 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::JsFile:
case DomType::QmlFile:
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::GlobalComponent: // global scope (enums -> qmlObj)
case DomType::JsResource: // js resurce (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.invokeVisitorOnValue(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.invokeVisitorOnValue(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,
// we can't lazy load files from an empty path, but non-existent paths where canonicalPath
// is empty are OK
canonicalPath.isEmpty() ? path : 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::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();
}
/*!
\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, 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::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<QString, LookupType> stringToLookupTypeMap()
{
static auto map = []() {
QMetaEnum metaEnum = QMetaEnum::fromType<LookupType>();
QMap<QString, LookupType> res;
for (int i = 0; i < metaEnum.keyCount(); ++i) {
res[QString::fromUtf8(metaEnum.key(i)).toLower()] = LookupType(metaEnum.value(i));
}
return res;
}();
return map;
}
static DomItem rootFromContext(const DomItem &root, PathRoot contextId)
{
switch (contextId) {
case PathRoot::Modules:
return root.environment().field(Fields::moduleIndexWithUri);
case PathRoot::Libs:
return root.environment()[Fields::plugins];
case PathRoot::Top:
return root.top();
case PathRoot::Env:
return root.environment();
case PathRoot::Universe:
return root.environment()[u"universe"];
default:
return DomItem{};
}
}
/*!
\internal
Is used to resolve the "get" field of QQmlJS::Dom::Reference. Reference contains a link to a type,
and its "get" field does return the DomItem for the linked type. For example, a type has a link to
its base type.
*/
static DomItem resolveReference(const DomItem &it, const Path &refRef, QList<Path> *visitedRefs,
const ErrorHandler &errorHandler, const ErrorGroups &errorGroup)
{
if (visitedRefs->contains(refRef)) {
errorGroup
.error([visitedRefs, refRef](const Sink &sink) {
const QString msg = DomItem::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);
return {};
}
const Reference *ref = it.as<Reference>();
Q_ASSERT(ref);
visitedRefs->append(refRef);
DomItem resolveRes;
it.resolve(
ref->referredObjectPath,
[&resolveRes](Path, const DomItem &r) {
resolveRes = r;
return false;
},
errorHandler, ResolveOption::None, ref->referredObjectPath, visitedRefs);
return resolveRes;
}
static LookupOptions resolveLookupOptions(const PathCurrent ¤t, const DomItem &it)
{
LookupOptions opt = LookupOption::Normal;
if (current == PathCurrent::LookupStrict)
return opt | LookupOption::Strict;
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;
}
return opt;
}
static std::optional<LookupType> lookupTypeFromString(const QString &expectedType)
{
const auto &map = stringToLookupTypeMap();
auto it = map.find(expectedType.toLower());
if (it == map.end())
return {};
return *it;
}
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;
QList<ResolveToDo> toDos(1); // invariant: always increase pathIndex to guarantee end even with only partial visited match
if (path.headKind() == Path::Kind::Root) {
DomItem root = rootFromContext(*this, path.headRoot());
if (!root) {
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 = std::make_pair(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) {
it = resolveReference(it, it.canonicalPath(),
visitedRefs == nullptr ? &vRefs : visitedRefs,
errorHandler, myResolveErrors());
} 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:
myResolveErrors()
.error(tr("Empty is not supported inside paths"))
.handle(errorHandler);
return false;
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::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: {
const LookupOptions opt = resolveLookupOptions(current, it);
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 = std::make_pair(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();
auto lookupType = lookupTypeFromString(expectedType);
if (!lookupType) {
myResolveErrors()
.error(tr("Type for lookup was expected to be one of '%1', not "
"%2")
.arg(stringToLookupTypeMap().keys().join(", "_L1),
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;
},
*(std::move(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 iterateDirectSubpaths 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 iterateDirectSubpaths 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;
iterateDirectSubpaths([&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();
}
DomItem::WriteOutCheckResult DomItem::performWriteOutChecks(const DomItem &reformatted,
OutWriter &ow,
WriteOutChecks extraChecks) const
{
auto compare = [this](const DomItem &obj1, const DomItem &obj2, QStringView obj2Name,
const FieldFilter &filter) {
const auto diffList = domCompareStrList(obj1, obj2, filter, DomCompareStrList::AllDiffs);
if (!diffList.isEmpty()) {
qCWarning(writeOutLog).noquote().nospace()
<< obj2Name << " writeOut of " << this->canonicalFilePath() << " has changes:\n"
<< diffList.join(QString());
return false;
}
return true;
};
auto checkStability = [&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) {
qCWarning(writeOutLog).noquote().nospace()
<< objName << " non stable writeOut of " << this->canonicalFilePath() << ":"
<< lineDiff(expected, ow2.writtenStr, 2);
return false;
}
return true;
};
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);
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, 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;
auto currentFileItem = fileObject();
writeOut(ow);
ow.eof();
WriteOutCheckResult result = WriteOutCheckResult::Success;
if (extraChecks)
result = performWriteOutChecks(currentFileItem, ow, extraChecks);
return result == WriteOutCheckResult::Success ? bool(currentFileItem) : false;
}
bool DomItem::writeOut(const QString &path, 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) {
auto lw = createLineWriter([&ts](QStringView s) { ts << s; }, path, options);
OutWriter ow(getFileItemOwner(fileObject()), *lw);
return writeOutForFile(ow, extraChecks);
});
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 iterateDirectSubpaths(
[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.withComponent(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::JsResource:
case DomType::GlobalComponent:
first = false;
if (DomItem next = current.field(Fields::objects).index(0))
toDo.append(next);
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)
{
QList<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 = std::make_pair(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:
return true;
}
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([](auto &&e) { return e->pathFromOwner(); });
}
QString DomItem::canonicalFilePath() const
{
return visitEl([this](auto &&e) { return e->canonicalFilePath(*this); });
}
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->domCreationOption());
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::Default, 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::JsFile:
newEnvPtr->addJsFile(newItem.ownerAs<JsFile>(), AddOption::Overwrite);
break;
case DomType::QmlFile:
newEnvPtr->addQmlFile(newItem.ownerAs<QmlFile>(), AddOption::Overwrite);
break;
case DomType::MockOwner:
case DomType::ScriptExpression:
case DomType::AstComments:
case DomType::LoadInfo:
case DomType::FileLocationsNode:
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::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;
}
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 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;
})) {
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().withComponent(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().withComponent(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({}, 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));
return tFile.fileObject();
}
Empty::Empty()
{}
Path Empty::pathFromOwner() 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:
{
sink(QString::fromUtf8(value().toJsonValue().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
{
const auto fLoc = FileLocations::treeOf(self);
ow.writeRegion(fLoc, LeftBracketRegion);
bool first = true;
iterateDirectSubpaths(self,
[&ow, &first, &fLoc, compact](const PathEls::PathComponent &,
function_ref<DomItem()> elF) {
if (first)
first = false;
else
ow.writeRegion(fLoc, CommaTokenRegion).ensureSpace();
if (!compact)
ow.ensureNewline(1);
DomItem el = elF();
el.writeOut(ow);
return true;
});
if (!compact && !first)
ow.newline();
ow.writeRegion(fLoc, RightBracketRegion);
}
DomElement::DomElement(const Path &pathFromOwner) : m_pathFromOwner(pathFromOwner) { }
Path DomElement::canonicalPath(const DomItem &self) const
{
Q_ASSERT(m_pathFromOwner && "uninitialized DomElement");
return self.owner().canonicalPath().withPath(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:
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.invokeVisitorOnLazyField(visitor, Fields::referredObjectPath, [this]() {
return referredObjectPath.toString();
});
cont = cont && visitor(PathEls::Field(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.subDataItem(PathEls::Field(Fields::referredObjectPath),
referredObjectPath.toString());
}
if (Fields::get == name)
return get(self);
return DomItem();
}
QList<QString> Reference::fields(const DomItem &) const
{
return QList<QString>({Fields::referredObjectPath.toString(), Fields::get.toString()});
}
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 && visitor(PathEls::Field(Fields::errors), [&self, this]() {
QMultiMap<Path, ErrorMessage> myErrors = localErrors();
return self.subMapItem(Map(
self.pathFromOwner().withField(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();
Path p2 = el2->pathFromOwner();
if (p1 != p2)
return false;
return true;
});
}
ErrorHandler MutableDomItem::errorHandler()
{
MutableDomItem self;
return [&self](const ErrorMessage &m) { self.addError(ErrorMessage(m)); };
}
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();
}
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
{
const auto fLoc = FileLocations::treeOf(self);
ow.writeRegion(fLoc, LeftBracketRegion);
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.writeRegion(fLoc, CommaTokenRegion).ensureSpace();
if (!compact)
ow.ensureNewline(1);
DomItem el = index(self, i);
el.writeOut(ow);
}
if (!compact && !first)
ow.newline();
ow.writeRegion(fLoc, 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);
}
std::shared_ptr<ExternalOwningItem> getFileItemOwner(const DomItem &fileItem)
{
switch (fileItem.internalKind()) {
case DomType::JsFile:
return fileItem.ownerAs<JsFile>();
case DomType::QmlFile:
return fileItem.ownerAs<QmlFile>();
default:
Q_UNREACHABLE_RETURN({});
}
}
} // end namespace Dom
} // end namespace QQmlJS
QT_END_NAMESPACE
#include "moc_qqmldomitem_p.cpp"