// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE namespace QQmlJS { namespace Dom { Q_LOGGING_CATEGORY(writeOutLog, "qt.qmldom.writeOut", QtWarningMsg); static Q_LOGGING_CATEGORY(refLog, "qt.qmldom.ref", QtWarningMsg); template struct CheckDomElementT; template struct CheckDomElementT> : std::conjunction...> { }; /*! \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::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) 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 domTypeToStringMap() { static QMap map = [](){ QMetaEnum metaEnum = QMetaEnum::fromType(); QMap 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 domKindToStringMap() { static QMap map = []() { QMetaEnum metaEnum = QMetaEnum::fromType(); QMap 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 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 &environment, const QString &canonicalPath, const QString &logicalPath, const std::optional &content) : m_environment(environment), m_canonicalPath(canonicalPath), m_logicalPath(logicalPath), m_content(content) { } FileToLoad FileToLoad::fromMemory(const std::weak_ptr &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 &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 envPtr = env.ownerAs()) { 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::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::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 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>; if constexpr (std::is_same_v) { return e->semanticScope(); } else if constexpr (std::is_same_v) { return e->semanticScope(); } else if constexpr (std::is_same_v) { return e->semanticScope(); } else if constexpr (std::is_same_v) { if (const MethodInfo *mi = e->template as()) { return mi->semanticScope(); } if (const auto *propertyDefinition = e->template as()) { return propertyDefinition->semanticScope(); } } else if constexpr (std::is_same_v) { return e.element().base()->semanticScope(); } return {}; }, m_element); return scope; } DomItem DomItem::get(const ErrorHandler &h, QList *visitedRefs) const { if (const Reference *refPtr = as()) return refPtr->get(*this, h, visitedRefs); return DomItem(); } QList DomItem::getAll(const ErrorHandler &h, QList *visitedRefs) const { if (const Reference *refPtr = as()) 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 DomItem::propertyInfoNames() const { QSet 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 lookupTypeToStringMap() { static QMap map = []() { QMetaEnum metaEnum = QMetaEnum::fromType(); QMap 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 *visitedRefs) const { QList vRefs; Path fPath = fullPath; if (fullPath.length() == 0) fPath = path; if (path.length()==0) return visitor(fPath, *this); QList> visited(path.length() + 1); Path myPath = path; QVector 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()->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 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 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 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 DomItem::keys() const { return visitEl([this](auto &&el) { return el->keys(*this); }); } QStringList DomItem::sortedKeys() const { QSet 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 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::values() const { QList res; visitEl([this, &res](const auto &el) { return el->iterateDirectSubpathsConst( *this, [&res](const PathEls::PathComponent &, function_ref 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 newEnvPtr = newEnv.ownerAs(); if (!newEnvPtr) return WriteOutCheckResult::Failed; auto newFilePtr = std::make_shared(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()) hasAnnotations = !myPtr->annotations.isEmpty(); break; case DomType::PropertyDefinition: if (const PropertyDefinition *myPtr = as()) hasAnnotations = !myPtr->annotations.isEmpty(); break; case DomType::MethodInfo: if (const MethodInfo *myPtr = as()) hasAnnotations = !myPtr->annotations.isEmpty(); break; case DomType::QmlObject: if (const QmlObject *myPtr = as()) hasAnnotations = !myPtr->annotations().isEmpty(); break; case DomType::Binding: if (const Binding *myPtr = as()) 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 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 &toDo, const DomItem ¤t, const DomItem &derivedFromPrototype, const ErrorHandler &h, QList *visitedRefs, VisitPrototypesOptions options, const DomItem &prototype) { Path elId = prototype.canonicalPath(); if (visitedRefs->contains(elId)) return true; else visitedRefs->append(elId); QList protos = prototype.getAll(h, visitedRefs); if (protos.isEmpty()) { if (std::shared_ptr envPtr = derivedFromPrototype.environment().ownerAs()) 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 visitor, VisitPrototypesOptions options, const ErrorHandler &h, QSet *visited, QList *visitedRefs) const { QSet visitedLocal; if (!visited) visited = &visitedLocal; QList 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 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 visitor, VisitPrototypesOptions options, const ErrorHandler &h, QSet *visited, QList *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 visitor, VisitPrototypesOptions options, const ErrorHandler &h, QSet *visited, QList *visitedRefs) const { QSet 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 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 visitor, LookupOptions options, const ErrorHandler &h, QSet *visited, QList *visitedRefs) const { QSet visitedLocal; if (!visited) visited = &visitedLocal; QList visitedRefsLocal; if (!visitedRefs) visitedRefs = &visitedRefsLocal; DomItem current = scope(); if (!current) { myResolveErrors().warning(tr("Called visitScopeChain outside scopes")).handle(h); return true; } QList toDo { current }; bool visitFirst = !(options & LookupOption::SkipFirstScope); bool visitCurrent = visitFirst; bool first = true; QSet 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 visitor, LookupOptions opts, const ErrorHandler &h, QSet *visited, QList *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<(?[a-zA-Z_0-9:]+) *(?\*?)>|QMap< *(?[a-zA-Z_0-9:]+) *, *(?[a-zA-Z_0-9:]+) *(?\*?)>|(?[a-zA-Z_0-9:]+) *(?\*?))")); 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 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 visitor, LookupType lookupType, const ErrorHandler &errorHandler, QList *visitedRefs) { QVector lookupToDos( { ResolveToDo{ newIt, 1 } }); // invariant: always increase pathIndex to guarantee // end even with only partial visited match QList> 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 visitor, LookupType lookupType, LookupOptions opts, const ErrorHandler &errorHandler, QSet *visited, QList *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 *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::lookup(const QString &symbolName, LookupType type, LookupOptions opts, const ErrorHandler &errorHandler) const { QList 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::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 newEnvPtr; if (std::shared_ptr envPtr = env.ownerAs()) { newEnvPtr = std::make_shared(envPtr, envPtr->loadPaths(), envPtr->options(), envPtr->domCreationOptions()); DomBase *eBase = envPtr.get(); if (std::holds_alternative(m_element) && eBase && std::get(m_element) == eBase) return MutableDomItem(DomItem(newEnvPtr)); } else if (std::shared_ptr univPtr = top().ownerAs()) { newEnvPtr = std::make_shared( 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::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(), AddOption::Overwrite); break; case DomType::JsFile: newEnvPtr->addJsFile(newItem.ownerAs(), AddOption::Overwrite); break; case DomType::QmlFile: newEnvPtr->addQmlFile(newItem.ownerAs(), AddOption::Overwrite); break; case DomType::QmltypesFile: newEnvPtr->addQmltypesFile(newItem.ownerAs(), AddOption::Overwrite); break; case DomType::GlobalScope: { newEnvPtr->addGlobalScope(newItem.ownerAs(), 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 &validEnvPtr) const { DomItem env = environment(); if (std::shared_ptr envPtr = env.ownerAs()) { return envPtr->commitToBase(env, validEnvPtr); } return false; } bool DomItem::visitLocalSymbolsNamed(const QString &name, function_ref 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(); 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 filter) const { visitEl([this, s, indent, filter](auto &&e) { e->dump(*this, s, indent, filter); }); } FileWriter::Status DomItem::dump(const QString &path, function_ref 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::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::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::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::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::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::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::monostate>) ow->clearErrors(groups); }, m_owner); if (iterate) { iterateSubOwners([groups](const DomItem &i){ i.clearErrors(groups, true); return true; }); } } bool DomItem::iterateErrors( function_ref visitor, bool iterate, Path inPath) const { if (!std::visit([this, visitor, inPath](auto &&el) { if constexpr (std::is_same_v, 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 visitor) const { return std::visit([this, visitor](auto &&o) { if constexpr (std::is_same_v, 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 &paths) const { return subListItem( List::fromQList(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 DomItem::topPtr() const { return std::visit([](auto &&el) -> shared_ptr { if constexpr (std::is_same_v, std::monostate>) return {}; else return el; }, m_top); } shared_ptr DomItem::owningItemPtr() const { return std::visit([](auto &&el) -> shared_ptr { if constexpr (std::is_same_v, 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 &envPtr): DomItem(envPtr, envPtr, Path(), envPtr.get()) { } DomItem::DomItem(const std::shared_ptr &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) 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 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 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 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 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 filter) const { bool first = true; sink(u"["); iterateDirectSubpaths( self, [&self, indent, &first, sink, filter](const PathEls::PathComponent &c, function_ref 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 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 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 Reference::fields(const DomItem &) const { return QList({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 *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 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 Reference::getAll( const DomItem &self, const ErrorHandler &h, QList *visitedRefs) const { QList res; if (referredObjectPath) { DomItem env; Path selfPath; QList 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 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 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 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 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 visitor, const Path &inPath) { QMultiMap 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 visitor) { return self.iterateDirectSubpaths( [&self, visitor](const PathEls::PathComponent &, function_ref 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>(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()) { 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()) { el->setNextScopePath(nextScopePath); return field(Fields::nextScope); } else { Q_ASSERT(false && "setNextScopePath on non qml scope"); return {}; } } MutableDomItem MutableDomItem::setPropertyDefs(QMultiMap propertyDefs) { if (QmlObject *el = mutableAs()) { el->setPropertyDefs(propertyDefs); return field(Fields::propertyDefs); } else { Q_ASSERT(false && "setPropertyDefs on non qml scope"); return {}; } } MutableDomItem MutableDomItem::setBindings(QMultiMap bindings) { if (QmlObject *el = mutableAs()) { el->setBindings(bindings); return field(Fields::bindings); } else { Q_ASSERT(false && "setBindings on non qml scope"); return {}; } } MutableDomItem MutableDomItem::setMethods(QMultiMap functionDefs) { if (QmlObject *el = mutableAs()) el->setMethods(functionDefs); else Q_ASSERT(false && "setMethods on non qml scope"); return {}; } MutableDomItem MutableDomItem::setChildren(const QList &children) { if (QmlObject *el = mutableAs()) { el->setChildren(children); return field(Fields::children); } else Q_ASSERT(false && "setChildren on non qml scope"); return {}; } MutableDomItem MutableDomItem::setAnnotations(const QList &annotations) { if (QmlObject *el = mutableAs()) el->setAnnotations(annotations); else if (Binding *el = mutableAs()) { el->setAnnotations(annotations); el->updatePathFromOwner(pathFromOwner()); } else if (PropertyDefinition *el = mutableAs()) { el->annotations = annotations; el->updatePathFromOwner(pathFromOwner()); } else if (MethodInfo *el = mutableAs()) { el->annotations = annotations; el->updatePathFromOwner(pathFromOwner()); } else if (EnumDecl *el = mutableAs()) { 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 &exp) { switch (internalKind()) { case DomType::Binding: if (Binding *b = mutableAs()) { b->setValue(std::make_unique(exp)); return field(Fields::value); } break; case DomType::MethodInfo: if (MethodInfo *m = mutableAs()) { m->body = exp; return field(Fields::body); } break; case DomType::MethodParameter: if (MethodParameter *p = mutableAs()) { 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()) { auto exp = std::make_shared( code, ScriptExpression::ExpressionType::BindingExpression); b->setValue(std::make_unique(exp)); return field(Fields::value); } break; case DomType::MethodInfo: if (MethodInfo *m = mutableAs()) { QString pre = m->preCode(it); QString post = m->preCode(it); m->body = std::make_shared( code, ScriptExpression::ExpressionType::FunctionBody, 0, pre, post); return field(Fields::body); } break; case DomType::MethodParameter: if (MethodParameter *p = mutableAs()) { p->defaultValue = std::make_shared( code, ScriptExpression::ExpressionType::ArgInitializer); return field(Fields::defaultValue); } break; case DomType::ScriptExpression: if (std::shared_ptr exp = ownerAs()) { std::shared_ptr 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()) 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()) 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()) 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()) { return el->addChild(*this, child); } else if (QmlComponent *el = mutableAs()) { 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(); res = el->addAnnotation(annotation); } break; case DomType::Binding: { Binding *el = mutableAs(); res = el->addAnnotation(m_pathFromOwner, annotation); } break; case DomType::PropertyDefinition: { PropertyDefinition *el = mutableAs(); res = el->addAnnotation(m_pathFromOwner, annotation); } break; case DomType::MethodInfo: { MethodInfo *el = mutableAs(); res = el->addAnnotation(m_pathFromOwner, annotation); } break; case DomType::Id: { Id *el = mutableAs(); 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()) { 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()) { 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 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( e, static_cast(e.get())); }, *m_data); } } // end namespace Dom } // end namespace QQmlJS QT_END_NAMESPACE #include "moc_qqmldomitem_p.cpp"