/**************************************************************************** ** ** Copyright (C) 2020 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtQml module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ **/ #include "qqmldomitem_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 #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); 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 is: \code Kind kind() const override { return kindValue; } // returns the kind of the current element Path pathFromOwner(DomItem &self) const override; // returns the path from the owner to the current element Path canonicalPath(DomItem &self) const override; // returns the path from virtual bool iterateDirectSubpaths(DomItem &self, function_ref) const = 0; // iterates the *direct* subpaths, returns false if a quick end was requested \endcode But you probably want to subclass either DomElement of OwningItem for your element. DomElement stores its pathFromOwner, and computes the canonicalPath from it and its owner. OwningItem is the unit for updates to the Dom model, exposed changes always change at least one OwningItem. They have their lifetime handled with 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; } } QCborValue locationToData(SourceLocation loc, QStringView strValue) { QCborMap res({ {QStringLiteral(u"offset"), loc.offset}, {QStringLiteral(u"length"), loc.length}, {QStringLiteral(u"startLine"), loc.startLine}, {QStringLiteral(u"startColumn"), loc.startColumn} }); if (!strValue.isEmpty()) res.insert(QStringLiteral(u"strValue"), QCborValue(strValue)); return res; } QString DomBase::canonicalFilePath(DomItem &self) const { auto parent = containingObject(self); if (parent) return parent.canonicalFilePath(); return QString(); } void DomBase::writeOut(DomItem &self, OutWriter &) const { qCWarning(writeOutLog) << "Ignoring unsupported writeOut for " << domTypeToString(kind()) << ":" << self.canonicalPath(); } ConstantData::ConstantData(Path pathFromOwner, QCborValue value, Options options) : DomElement(pathFromOwner), m_value(value), m_options(options) {} bool ConstantData::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor) { static QHash knownFields; static QBasicMutex m; auto toField = [](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. */ 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() { 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() { return visitEl([this](auto &&el) { return el->containingObject(*this); }); } DomItem DomItem::qmlObject(GoTo options, FilterUpOptions filterOptions) { if (DomItem res = filterUp([](DomType k, 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) { 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) { if (DomItem res = filterUp([](DomType k, DomItem &) { return k == DomType::QmlObject; }, FilterUpOptions::ReturnInner)) return res; if (options == GoTo::MostLikely) { if (DomItem comp = component(options)) return comp.field(Fields::objects).index(0); } return DomItem(); } DomItem DomItem::container() { 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() { 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(); } DomItem DomItem::owner() { if (domTypeIsOwningItem(m_kind) || m_kind == DomType::Empty) return *this; return std::visit( [this](auto &&el) { return DomItem(this->m_top, el, this->m_ownerPath, el.get()); }, *m_owner); } DomItem DomItem::top() { if (domTypeIsTopItem(m_kind) || m_kind == DomType::Empty) return *this; return std::visit([](auto &&el) { return DomItem(el, el, Path(), el.get()); }, *m_top); } DomItem DomItem::environment() { 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() { 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... } DomItem DomItem::filterUp(function_ref filter, FilterUpOptions options) { DomItem it = *this; DomType k = it.internalKind(); switch (options) { case FilterUpOptions::ReturnOuter: case FilterUpOptions::ReturnOuterNoSelf: { bool checkTop = (options == FilterUpOptions::ReturnOuter); while (k != DomType::Empty) { if (checkTop && filter(k, it)) return it; checkTop = true; if (!domTypeIsOwningItem(k)) { DomItem el = owner(); DomItem res; k = DomType::Empty; Path pp = pathFromOwner(); for (Path p : pp.mid(0, pp.length() - 1)) { el = el.path(p); DomType k2 = el.internalKind(); if (filter(k2, el)) { k = k2; res = el; } } if (k != DomType::Empty) return res; it = it.owner(); } it = it.containingObject(); k = it.internalKind(); } } break; case FilterUpOptions::ReturnInner: while (k != DomType::Empty) { if (!domTypeIsOwningItem(k)) { DomItem el = owner(); Path pp = pathFromOwner(); for (Path p : pp) { DomItem child = el.path(p); DomType k2 = child.internalKind(); if (filter(k2, child)) return child; el = child; } it = it.owner(); } it = it.containingObject(); k = it.internalKind(); } break; } return DomItem(); } DomItem DomItem::scope(FilterUpOptions options) { DomItem res = filterUp([](DomType, DomItem &el) { return el.isScope(); }, options); return res; } DomItem DomItem::get(ErrorHandler h, QList *visitedRefs) { if (const Reference *refPtr = as()) return refPtr->get(*this, h, visitedRefs); return DomItem(); } QList DomItem::getAll(ErrorHandler h, QList *visitedRefs) { if (const Reference *refPtr = as()) return refPtr->getAll(*this, h, visitedRefs); return {}; } PropertyInfo DomItem::propertyInfoWithName(QString name) { PropertyInfo pInfo; visitPrototypeChain([&pInfo, name](DomItem &obj) { return obj.visitLocalSymbolsNamed(name, [&pInfo, name](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() { QSet res; visitPrototypeChain([&res](DomItem &obj) { res += obj.propertyDefs().keys(); res += obj.bindings().keys(); return true; }); return res; } DomItem DomItem::component(GoTo options) { if (DomItem res = filterUp( [](DomType kind, 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(Path path, DomItem::Visitor visitor, ErrorHandler errorHandler, ResolveOptions options, Path fullPath, QList *visitedRefs) { 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] = {root, 1}; } else { toDos[0] = {*this, 0}; } while (!toDos.isEmpty()) { auto toDo = toDos.last(); toDos.removeLast(); { 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](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, 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, DomItem &item, bool) { // avoid non directly attached? DomItem newItem = item[toFind]; if (newItem) toDos.append({ 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](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](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(); QStringList subpath; 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](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, 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(Path p, ErrorHandler errorHandler) { if (!p) return *this; DomItem res; resolve(p, [&res](Path, DomItem it) { res = it; return false; }, errorHandler); return res; } DomItem DomItem::path(QString p, ErrorHandler errorHandler) { return path(Path::fromString(p, errorHandler)); } DomItem DomItem::path(QStringView p, ErrorHandler errorHandler) { return path(Path::fromString(p, errorHandler)); } QList DomItem::fields() { return visitEl([this](auto &&el) { return el->fields(*this); }); } DomItem DomItem::field(QStringView name) { return visitEl([this, name](auto &&el) { return el->field(*this, name); }); } index_type DomItem::indexes() { return visitEl([this](auto &&el) { return el->indexes(*this); }); } DomItem DomItem::index(index_type i) { return visitEl([this, i](auto &&el) { return el->index(*this, i); }); } bool DomItem::visitIndexes(function_ref visitor) { // 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() { return visitEl([this](auto &&el) { return el->keys(*this); }); } QStringList DomItem::sortedKeys() { QSet ks = keys(); QStringList sortedKs(ks.begin(), ks.end()); std::sort(sortedKs.begin(), sortedKs.end()); return sortedKs; } DomItem DomItem::key(QString name) { return visitEl([this, name](auto &&el) { return el->key(*this, name); }); } bool DomItem::visitKeys(function_ref visitor) { // use iterateDirectSubpathsConst instead? for (auto k : sortedKeys()) { DomItem v = key(k); if (!visitor(k, v)) return false; } return true; } QList DomItem::values() { QList res; visitEl([this, &res](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) { if (hasAnnotations()) { DomItem anns = field(Fields::annotations); for (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) { writeOutPre(ow); visitEl([this, &ow](auto &&el) { el->writeOut(*this, ow); }); writeOutPost(ow); } void DomItem::writeOutPost(OutWriter &ow) { ow.itemEnd(*this); } DomItem DomItem::writeOutForFile(OutWriter &ow, WriteOutChecks extraChecks) { ow.indentNextlines = true; writeOut(ow); ow.eof(); DomItem fObj = fileObject(); DomItem copy = ow.updatedFile(fObj); if (extraChecks & WriteOutCheck::All) { QStringList dumped; auto maybeDump = [&ow, extraChecks, &dumped](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](Sink s) { if (dumped.isEmpty()) return; s(u"\ndump: "); for (auto dumpPath : dumped) { s(u" "); sinkEscaped(s, dumpPath); } }; auto compare = [&maybeDump, &dumpedDumper, this](DomItem &obj1, QStringView obj1Name, DomItem &obj2, QStringView obj2Name, const FieldFilter &filter) { if (!domCompareStrList(obj1, obj2, filter).isEmpty()) { maybeDump(obj1, obj1Name); maybeDump(obj2, obj2Name); qCWarning(writeOutLog).noquote().nospace() << obj2Name << " writeOut of " << this->canonicalFilePath() << " has changes:\n" << domCompareStrList(obj1, obj2, filter, DomCompareStrList::AllDiffs) .join(QString()) << dumpedDumper; return false; } return true; }; auto checkStability = [&maybeDump, &dumpedDumper, &dumped, &ow, this](QString expected, 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(fObj, u"initial", copy, u"reformatted", FieldFilter::noLocationFilter())) return DomItem(); if (extraChecks & WriteOutCheck::UpdatedDomStable) checkStability(ow.writtenStr, copy, u"reformatted"); if (extraChecks & (WriteOutCheck::Reparse | WriteOutCheck::ReparseCompare | WriteOutCheck::ReparseStable)) { DomItem newEnv = environment().makeCopy().item(); if (std::shared_ptr newEnvPtr = newEnv.ownerAs()) { std::shared_ptr newFilePtr( new QmlFile(canonicalFilePath(), ow.writtenStr)); newEnvPtr->addQmlFile(newFilePtr, AddOption::Overwrite); DomItem newFile = newEnv.copy(newFilePtr, Path()); if (newFilePtr->isValid()) { if (extraChecks & (WriteOutCheck::ReparseCompare | WriteOutCheck::ReparseStable)) { MutableDomItem newFileMutable(newFile); createDom(newFileMutable); if ((extraChecks & WriteOutCheck::ReparseCompare) && !compare(copy, u"reformatted", newFile, u"reparsed", FieldFilter::compareNoCommentsFilter())) return DomItem(); if ((extraChecks & WriteOutCheck::ReparseStable)) checkStability(ow.writtenStr, newFile, u"reparsed"); } } else { qCWarning(writeOutLog).noquote().nospace() << "writeOut of " << canonicalFilePath() << " created invalid code:\n----------\n" << ow.writtenStr << "\n----------" << [&newFile](Sink s) { newFile.iterateErrors( [s](DomItem, ErrorMessage msg) { s(u"\n "); msg.dump(s); return true; }, true); s(u"\n"); // extra empty line at the end... }; return DomItem(); } } } } return copy; } DomItem DomItem::writeOut(QString path, int nBackups, const LineWriterOptions &options, FileWriter *fw, WriteOutChecks extraChecks) { DomItem res = *this; DomItem copy; FileWriter localFw; if (!fw) fw = &localFw; switch (fw->write( path, [this, path, ©, &options, extraChecks](QTextStream &ts) { LineWriter lw([&ts](QStringView s) { ts << s; }, path, options); OutWriter ow(lw); copy = writeOutForFile(ow, extraChecks); return bool(copy); }, nBackups)) { case FileWriter::Status::ShouldWrite: case FileWriter::Status::SkippedDueToFailure: qCWarning(writeOutLog) << "failure reformatting " << path; break; case FileWriter::Status::DidWrite: case FileWriter::Status::SkippedEqual: res = copy; break; } return res; } bool DomItem::isCanonicalChild(DomItem &item) { if (item.isOwningItem()) { return canonicalPath() == item.canonicalPath().dropTail(); } else { DomItem itemOw = item.owner(); DomItem selfOw = owner(); return itemOw == selfOw && item.pathFromOwner().dropTail() == pathFromOwner(); } } bool DomItem::hasAnnotations() { 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; } bool DomItem::visitTree(Path basePath, DomItem::ChildrenVisitor visitor, VisitOptions options, DomItem::ChildrenVisitor openingVisitor, DomItem::ChildrenVisitor closingVisitor) { if (!*this) return true; if (options & VisitOption::VisitSelf && !visitor(basePath, *this, true)) return false; if (!openingVisitor(basePath, *this, true)) return true; auto atEnd = qScopeGuard( [closingVisitor, basePath, this]() { closingVisitor(basePath, *this, true); }); return visitEl([this, basePath, visitor, openingVisitor, closingVisitor, options](auto &&el) { return el->iterateDirectSubpathsConst( *this, [this, basePath, visitor, openingVisitor, closingVisitor, options](const PathEls::PathComponent &c, function_ref itemF) { Path pNow; if (!(options & VisitOption::NoPath)) { pNow = basePath; pNow = pNow.appendComponent(c); } 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); } return true; }); }); } bool DomItem::visitPrototypeChain(function_ref visitor, VisitPrototypesOptions options, ErrorHandler h, QSet *visited, QList *visitedRefs) { 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](DomItem &el) { Path elId = el.canonicalPath(); if (visitedRefs->contains(elId)) return true; else visitedRefs->append(elId); QList protos = el.getAll(h, visitedRefs); if (protos.isEmpty()) { if (std::shared_ptr envPtr = environment().ownerAs()) if (!(envPtr->options() & DomEnvironment::Option::NoDependencies)) myErrors() .warning(tr("could not resolve prototype %1 (%2)") .arg(current.canonicalPath().toString(), el.field(Fields::referredObjectPath) .value() .toString())) .withItem(*this) .handle(h); } else { if (protos.length() > 1) { QStringList protoPaths; for (DomItem &p : protos) protoPaths.append(p.canonicalPath().toString()); myErrors() .warning(tr("Multiple definitions found, using first only, " "resolving prototype %1 (%2): %3") .arg(current.canonicalPath().toString(), el.field(Fields::referredObjectPath) .value() .toString(), protoPaths.join(QLatin1String(", ")))) .withItem(*this) .handle(h); } int nProtos = 1; // change to protos.length() to us all prototypes found // (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) { toDo.append(proto); } else { myErrors() .warning(tr("Unexpected prototype type %1 (%2)") .arg(current.canonicalPath().toString(), el.field(Fields::referredObjectPath) .value() .toString())) .withItem(*this) .handle(h); } } } return true; }); } return true; } bool DomItem::visitDirectAccessibleScopes(function_ref visitor, VisitPrototypesOptions options, ErrorHandler h, QSet *visited, QList *visitedRefs) { if (internalKind() == DomType::QmlObject) return visitPrototypeChain(visitor, options, h, visited, visitedRefs); if (visited && id() != 0) { if (visited->contains(id())) return true; visited->insert(id()); } if (!(options & VisitPrototypesOption::SkipFirst)) visitor(*this); 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, ErrorHandler h, QSet *visited, QList *visitedRefs) { 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; } bool DomItem::visitScopeChain(function_ref visitor, LookupOptions options, ErrorHandler h, QSet *visited, QList *visitedRefs) { 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; 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); 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; } QSet DomItem::localSymbolNames() { QSet res; switch (internalKind()) { case DomType::QmlObject: res += propertyDefs().keys(); res += bindings().keys(); res += methods().keys(); break; case DomType::ScriptExpression: // to do break; case DomType::QmlComponent: res += ids().keys(); Q_FALLTHROUGH(); case DomType::QmlFile: // subComponents, imported types { DomItem comps = field(Fields::components); for (auto k : comps.keys()) if (!k.isEmpty()) res.insert(k); break; } case DomType::QmltypesComponent: case DomType::JsResource: case DomType::GlobalComponent: res += enumerations().keys(); break; case DomType::MethodInfo: { DomItem params = field(Fields::parameters); params.visitIndexes([&res](DomItem &p) { const MethodParameter *pPtr = p.as(); res.insert(pPtr->name); return true; }); break; } case DomType::ImportScope: { const ImportScope *currentPtr = as(); res += currentPtr->importedNames(*this); for (auto k : currentPtr->subImports().keys()) res.insert(k); break; } default: break; } return res; } bool DomItem::visitLookup1(QString symbolName, function_ref visitor, LookupOptions opts, ErrorHandler h, QSet *visited, QList *visitedRefs) { return visitScopeChain( [symbolName, visitor](DomItem &obj) { return obj.visitLocalSymbolsNamed(symbolName, [visitor](DomItem &el) { return visitor(el); }); }, opts, h, visited, visitedRefs); } class CppTypeInfo { Q_DECLARE_TR_FUNCTIONS(CppTypeInfo) public: CppTypeInfo() = default; static CppTypeInfo fromString(QStringView target, 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.match(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; }; bool DomItem::visitLookup(QString target, function_ref visitor, LookupType lookupType, LookupOptions opts, ErrorHandler errorHandler, QSet *visited, QList *visitedRefs) { 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.length() == 1) { return visitLookup1(subpath.first(), visitor, opts, errorHandler, visited, visitedRefs); } else { return visitLookup1( subpath.at(0), [&subpath, visitor, lookupType, &errorHandler, visitedRefs](DomItem &newIt) { QVector lookupToDos({ ResolveToDo { newIt, 1 } }); // invariant: always increase pathIndex to guarantee // end even with only partial visited match QList> lookupVisited(subpath.length() + 1); while (!lookupToDos.isEmpty()) { ResolveToDo tNow = lookupToDos.takeFirst(); auto vNow = qMakePair(tNow.item.id(), tNow.pathIndex); if (vNow.first != 0) { if (lookupVisited[vNow.second].contains(vNow.first)) continue; else lookupVisited[vNow.second].insert(vNow.first); } DomItem subNow = tNow.item; int iSubPath = tNow.pathIndex; Q_ASSERT(iSubPath < subpath.length()); QString subPathNow = subpath[iSubPath++]; DomItem scope = subNow.proceedToScope(); if (iSubPath < subpath.length()) { if (scope.internalKind() == DomType::QmlObject) scope.visitDirectAccessibleScopes( [&lookupToDos, subPathNow, iSubPath](DomItem &el) { return el.visitLocalSymbolsNamed( subPathNow, [&lookupToDos, iSubPath](DomItem &subEl) { lookupToDos.append({ subEl, iSubPath }); return true; }); }, VisitPrototypesOption::Normal, errorHandler, &(lookupVisited[vNow.second]), visitedRefs); } else { bool cont = scope.visitDirectAccessibleScopes( [&visitor, subPathNow, lookupType](DomItem &el) -> bool { if (lookupType == LookupType::Symbol) return el.visitLocalSymbolsNamed(subPathNow, visitor); else return el.visitLocalSymbolsNamed( subPathNow, [lookupType, &visitor](DomItem &el) -> bool { 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; }); }, VisitPrototypesOption::Normal, errorHandler, &(lookupVisited[vNow.second]), visitedRefs); if (!cont) return false; } } return true; }, 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](DomItem &els) { return els.visitIndexes([&visitor](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](QString, DomItem &els) { DomItem comps = els.field(Fields::currentItem).field(Fields::components).key(baseTarget); return comps.visitIndexes([&visitor](DomItem &el) { if (DomItem obj = el.field(Fields::objects).index(0)) return visitor(obj); return true; }); }); break; } } Q_ASSERT(false); return true; } DomItem DomItem::proceedToScope(ErrorHandler h, QList *visitedRefs) { // 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; default: return current.scope(); break; } } return DomItem(); } QList DomItem::lookup(QString symbolName, LookupType type, LookupOptions opts, ErrorHandler errorHandler) { QList res; visitLookup( symbolName, [&res](DomItem &el) { res.append(el); return true; }, type, opts, errorHandler); return res; } DomItem DomItem::lookupFirst(QString symbolName, LookupType type, LookupOptions opts, ErrorHandler errorHandler) { DomItem res; visitLookup( symbolName, [&res](DomItem &el) { res = el; return false; }, type, opts, errorHandler); return res; } quintptr DomItem::id() { return visitEl([](auto &&b) { return b->id(); }); } Path DomItem::pathFromOwner() { return visitEl([this](auto &&e) { return e->pathFromOwner(*this); }); } QString DomItem::canonicalFilePath() { return visitEl([this](auto &&e) { return e->canonicalFilePath(*this); }); } DomItem DomItem::fileLocationsTree() { if (DomItem l = field(Fields::fileLocationsTree)) return l; auto res = FileLocations::findAttachedInfo(*this, AttachedInfo::FindOption::SetFoundTreePath); if (res && res.foundTreePath.value()) { return copy(res.foundTree, res.foundTreePath.value()); } return DomItem(); } DomItem DomItem::fileLocations() { return fileLocationsTree().field(Fields::infoItem); } MutableDomItem DomItem::makeCopy(DomItem::CopyOption option) { if (m_kind == DomType::Empty) return MutableDomItem(); DomItem o = owner(); if (option == CopyOption::EnvDisconnected) { DomItem newItem = std::visit( [this, &o](auto &&el) { 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::shared_ptr( new DomEnvironment(envPtr, envPtr->loadPaths(), envPtr->options())); 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::shared_ptr(new DomEnvironment( QStringList(), DomEnvironment::Option::SingleThreaded | DomEnvironment::Option::NoDependencies, univPtr)); } else { Q_ASSERT(false); return {}; } DomItem newItem = std::visit( [this, newEnvPtr, &o](auto &&el) { 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() { DomItem env = environment(); if (std::shared_ptr envPtr = env.ownerAs()) { return envPtr->commitToBase(env); } return false; } bool DomItem::visitLocalSymbolsNamed(QString name, function_ref visitor) { 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](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.visitIndexes(visitor)) return false; break; default: Q_ASSERT(!isScope()); break; } } return true; } DomItem DomItem::operator[](const QString &cName) { if (internalKind() == DomType::Map) return key(cName); return field(cName); } DomItem DomItem::operator[](QStringView cName) { if (internalKind() == DomType::Map) return key(cName.toString()); return field(cName); } DomItem DomItem::operator[](Path p) { return path(p); } QCborValue DomItem::value() { if (internalKind() == DomType::ConstantData) return std::get(m_element).value(); return QCborValue(); } void DomItem::dumpPtr(Sink sink) { 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(Sink s, int indent, function_ref filter) { visitEl([this, s, indent, filter](auto &&e) { e->dump(*this, s, indent, filter); }); } FileWriter::Status DomItem::dump(QString path, function_ref filter, int nBackups, int indent, FileWriter *fw) { 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() { return dumperToString([this](Sink s){ dump(s); }); } int DomItem::derivedFrom() { if (m_owner) return std::visit([](auto &&ow) { return ow->derivedFrom(); }, *m_owner); return 0; } int DomItem::revision() { if (m_owner) return std::visit([](auto &&ow) { return ow->revision(); }, *m_owner); else return -1; } QDateTime DomItem::createdAt() { if (m_owner) return std::visit([](auto &&ow) { return ow->createdAt(); }, *m_owner); else return QDateTime::fromMSecsSinceEpoch(0); } QDateTime DomItem::frozenAt() { if (m_owner) return std::visit([](auto &&ow) { return ow->frozenAt(); }, *m_owner); else return QDateTime::fromMSecsSinceEpoch(0); } QDateTime DomItem::lastDataUpdateAt() { if (m_owner) return std::visit([](auto &&ow) { return ow->lastDataUpdateAt(); }, *m_owner); else return QDateTime::fromMSecsSinceEpoch(0); } void DomItem::addError(ErrorMessage msg) { if (m_owner) { DomItem myOwner = owner(); std::visit( [this, &myOwner, &msg](auto &&ow) { ow->addError(myOwner, msg.withItem(*this)); }, *m_owner); } else defaultErrorHandler(msg.withItem(*this)); } ErrorHandler DomItem::errorHandler() { DomItem self = *this; return [self](ErrorMessage m) mutable { self.addError(m); }; } void DomItem::clearErrors(ErrorGroups groups, bool iterate) { if (m_owner) { std::visit([&groups](auto &&ow) { ow->clearErrors(groups); }, *m_owner); if (iterate) iterateSubOwners([groups](DomItem i){ i.clearErrors(groups, true); return true; }); } } bool DomItem::iterateErrors(function_ref visitor, bool iterate, Path inPath) { if (m_owner) { DomItem ow = owner(); if (!std::visit([&ow, visitor, inPath](auto &&el) { return el->iterateErrors(ow, visitor, inPath); }, *m_owner)) return false; if (iterate && !iterateSubOwners([inPath, visitor](DomItem &i) { return i.iterateErrors(visitor, true, inPath); })) return false; } return true; } bool DomItem::iterateSubOwners(function_ref visitor) { if (m_owner) { DomItem ow = owner(); return std::visit([&ow, visitor](auto &&o) { return o->iterateSubOwners(ow, visitor); }, *m_owner); } return true; } bool DomItem::iterateDirectSubpaths(DirectVisitor v) { return visitMutableEl( [this, v](auto &&el) mutable { return el->iterateDirectSubpaths(*this, v); }); } shared_ptr DomItem::topPtr() { if (m_top) return std::visit([](auto &&el) -> shared_ptr { return el; }, *m_top); return {}; } shared_ptr DomItem::owningItemPtr() { if (m_owner) return std::visit([](auto &&el) -> shared_ptr { return el; }, *m_owner); return {}; } const DomBase *DomItem::base() { return visitEl([](auto &&el) { return static_cast(&(*el)); }); } DomBase *DomItem::mutableBase() { return visitMutableEl([](auto &&el) { return static_cast(&(*el)); }); } DomItem::DomItem(std::shared_ptr envPtr): DomItem(envPtr, envPtr, Path(), envPtr.get()) { } DomItem::DomItem(std::shared_ptr universePtr): DomItem(universePtr, universePtr, Path(), universePtr.get()) { } void DomItem::loadFile(QString canonicalFilePath, QString logicalPath, QString code, QDateTime codeDate, DomTop::Callback callback, LoadOptions loadOptions, std::optional fileType) { DomItem topEl = top(); if (topEl.internalKind() == DomType::DomEnvironment || topEl.internalKind() == DomType::DomUniverse) { if (auto univ = topEl.ownerAs()) univ->loadFile(*this, canonicalFilePath, logicalPath, code, codeDate, callback, loadOptions, fileType); else if (auto env = topEl.ownerAs()) { if (env->options() & DomEnvironment::Option::NoDependencies) env->loadFile(topEl, canonicalFilePath, logicalPath, code, codeDate, callback, DomTop::Callback(), DomTop::Callback(), loadOptions, fileType); else env->loadFile(topEl, canonicalFilePath, logicalPath, code, codeDate, DomTop::Callback(), DomTop::Callback(), callback, loadOptions, fileType); } else Q_ASSERT(false && "expected either DomUniverse or DomEnvironment cast to succeed"); } else { addError(myErrors().warning(tr("loadFile called without DomEnvironment or DomUniverse."))); callback(Paths::qmlFileInfoPath(canonicalFilePath), DomItem::empty, DomItem::empty); } } void DomItem::loadFile(QString filePath, QString logicalPath, DomTop::Callback callback, LoadOptions loadOptions, std::optional fileType) { DomItem topEl = top(); if (topEl.internalKind() == DomType::DomEnvironment || topEl.internalKind() == DomType::DomUniverse) { if (auto univ = topEl.ownerAs()) univ->loadFile(*this, filePath, logicalPath, callback, loadOptions); else if (auto env = topEl.ownerAs()) { if (env->options() & DomEnvironment::Option::NoDependencies) env->loadFile(topEl, filePath, logicalPath, callback, DomTop::Callback(), DomTop::Callback(), loadOptions, fileType); else env->loadFile(topEl, filePath, logicalPath, DomTop::Callback(), DomTop::Callback(), callback, loadOptions, fileType); } else Q_ASSERT(false && "expected either DomUniverse or DomEnvironment cast to succeed"); } else { addError(myErrors().warning(tr("loadFile called without DomEnvironment or DomUniverse."))); callback(Paths::qmlFileInfoPath(filePath), DomItem::empty, DomItem::empty); } } void DomItem::loadModuleDependency(QString uri, Version version, std::function callback, ErrorHandler errorHandler) { DomItem topEl = top(); if (topEl.internalKind() == DomType::DomEnvironment) { if (auto envPtr = topEl.ownerAs()) { if (envPtr->options() & DomEnvironment::Option::NoDependencies) envPtr->loadModuleDependency(topEl, uri, version, callback, nullptr, errorHandler); else envPtr->loadModuleDependency(topEl, uri, version, nullptr, callback, errorHandler); } else Q_ASSERT(false && "loadDependency expected the DomEnvironment cast to succeed"); } else { addError(myErrors().warning(tr("loadModuleDependency called without DomEnvironment."))); callback(Paths::moduleScopePath(uri, version), DomItem::empty, DomItem::empty); } } void DomItem::loadBuiltins(std::function callback, ErrorHandler h) { DomItem env = environment(); if (std::shared_ptr envPtr = env.ownerAs()) envPtr->loadBuiltins(env, callback, h); else myErrors().error(tr("Cannot load builtins without DomEnvironment")).handle(h); } void DomItem::loadPendingDependencies() { DomItem env = environment(); if (std::shared_ptr envPtr = env.ownerAs()) envPtr->loadPendingDependencies(env); else myErrors().error(tr("Called loadPendingDependencies without environment")).handle(); } /*! \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(QString code, DomType fileType) { if (code.isEmpty()) return DomItem(); DomItem env = DomEnvironment::create(QStringList(), QQmlJS::Dom::DomEnvironment::Option::SingleThreaded | QQmlJS::Dom::DomEnvironment::Option::NoDependencies); DomItem tFile; env.loadFile( QString(), QString(), code, QDateTime::currentDateTime(), [&tFile](Path, const DomItem &, const DomItem &newIt) { tFile = newIt; }, LoadOption::DefaultLoad, fileType); env.loadPendingDependencies(); return tFile; } Empty::Empty() {} Path Empty::pathFromOwner(DomItem &) const { return Path(); } Path Empty::canonicalPath(DomItem &) const { return Path(); } bool Empty::iterateDirectSubpaths(DomItem &, DirectVisitor) { return true; } DomItem Empty::containingObject(DomItem &self) const { return self; } void Empty::dump(DomItem &, Sink s, int, function_ref) const { s(u"null"); } Map::Map(Path pathFromOwner, Map::LookupFunction lookup, Keys keys, QString targetType) : DomElement(pathFromOwner), m_lookup(lookup), m_keys(keys), m_targetType(targetType) {} quintptr Map::id() const { return quintptr(0); } bool Map::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor) { QSet ksSet = keys(self); QStringList ksList = QStringList(ksSet.begin(), ksSet.end()); std::sort(ksList.begin(), ksList.end()); for (QString k : ksList) { if (!visitor(PathEls::Key(k), [&self, this, k]() { return key(self, k); })) return false; } return true; } const QSet Map::keys(DomItem &self) const { return m_keys(self); } DomItem Map::key(DomItem &self, QString name) const { return m_lookup(self, name); } void DomBase::dump( DomItem &self, 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; } 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; } }); 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(Path pathFromOwner, List::LookupFunction lookup, List::Length length, List::IteratorFunction iterator, 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( DomItem &self, Sink sink, int indent, function_ref filter) const { bool first = true; sink(u"["); const_cast(this)->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(DomItem &self, DirectVisitor visitor) { 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(DomItem &self) const { return m_length(self); } DomItem List::index(DomItem &self, index_type index) const { return m_lookup(self, index); } void List::writeOut(DomItem &self, OutWriter &ow, bool compact) const { ow.writeRegion(u"leftSquareBrace", u"["); int baseIndent = ow.increaseIndent(1); bool first = true; const_cast(this)->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(u"rightSquareBrace", u"]"); } DomElement::DomElement(Path pathFromOwner) : m_pathFromOwner(pathFromOwner) { } Path DomElement::pathFromOwner(DomItem &) const { Q_ASSERT(m_pathFromOwner && "uninitialized DomElement"); return m_pathFromOwner; } Path DomElement::canonicalPath(DomItem &self) const { Q_ASSERT(m_pathFromOwner && "uninitialized DomElement"); return self.owner().canonicalPath().path(m_pathFromOwner); } DomItem DomElement::containingObject(DomItem &self) const { Q_ASSERT(m_pathFromOwner && "uninitialized DomElement"); return DomBase::containingObject(self); } void DomElement::updatePathFromOwner(Path newPath) { //if (!domTypeCanBeInline(kind())) m_pathFromOwner = newPath; } bool Reference::shouldCache() const { for (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(Path referredObject, Path pathFromOwner, const SourceLocation &) : DomElement(pathFromOwner), referredObjectPath(referredObject) { } quintptr Reference::id() const { return quintptr(0); } bool Reference::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor) { 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(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(DomItem &) const { return QList({QString::fromUtf16(Fields::referredObjectPath), QString::fromUtf16(Fields::get)}); } DomItem Reference::index(DomItem &, index_type) const { return DomItem(); } DomItem Reference::key(DomItem &, QString) const { return DomItem(); } DomItem Reference::get(DomItem &self, 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, 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(DomItem &self, 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 (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, DomItem &el) { res.append(el); return true; }, h, ResolveOption::None, referredObjectPath, visitedRefs); if (env) { QList canonicalPaths; for (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, 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::currentDateTime()), m_lastDataUpdateAt(m_createdAt), m_frozenAt(QDateTime::fromMSecsSinceEpoch(0)) {} OwningItem::OwningItem(int derivedFrom, QDateTime lastDataUpdateAt): m_derivedFrom(derivedFrom), m_revision(nextRevision()), m_createdAt(QDateTime::currentDateTime()), m_lastDataUpdateAt(lastDataUpdateAt), m_frozenAt(QDateTime::fromMSecsSinceEpoch(0)) {} OwningItem::OwningItem(const OwningItem &o): m_derivedFrom(o.revision()), m_revision(nextRevision()), m_createdAt(QDateTime::currentDateTime()), m_lastDataUpdateAt(o.lastDataUpdateAt()), m_frozenAt(QDateTime::fromMSecsSinceEpoch(0)) { 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(DomItem &self, DirectVisitor visitor) { 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](DomItem &map, 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](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(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::currentDateTime(); 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(DomItem &, ErrorMessage msg) { addErrorLocal(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(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(DomItem &self, function_ref visitor, 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(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 &o1c, const DomItem &o2c) { DomItem &o1 = *const_cast(&o1c); DomItem &o2 = *const_cast(&o2c); if (o1.m_kind != o2.m_kind) return false; return o1.visitMutableEl([&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](ErrorMessage m) { self.addError(m); }; } MutableDomItem MutableDomItem::addPrototypePath(Path prototypePath) { if (QmlObject *el = mutableAs()) { return path(el->addPrototypePath(prototypePath)); } else { Q_ASSERT(false && "setPrototypePath on non qml scope"); return {}; } } MutableDomItem MutableDomItem::setNextScopePath(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(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(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(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()) { p->defaultValue = exp; return field(Fields::body); } 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(QString code) { DomItem it = item(); switch (it.internalKind()) { case DomType::Binding: if (Binding *b = mutableAs()) { std::shared_ptr exp(new ScriptExpression( 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::shared_ptr(new ScriptExpression( code, ScriptExpression::ExpressionType::FunctionBody, 0, pre, post)); return field(Fields::body); } break; case DomType::MethodParameter: if (MethodParameter *p = mutableAs()) { p->defaultValue = std::shared_ptr( new ScriptExpression(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(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(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, QString regionName) { index_type idx; MutableDomItem rC = field(Fields::comments); if (auto rcPtr = rC.mutableAs()) { auto &preList = rcPtr->regionComments[regionName].preComments; idx = preList.length(); preList.append(comment); MutableDomItem res = path(Path::Field(Fields::comments) .field(Fields::regionComments) .key(regionName) .field(Fields::preComments) .index(idx)); Q_ASSERT(res); return res; } return MutableDomItem(); } MutableDomItem MutableDomItem::addPostComment(const Comment &comment, QString regionName) { index_type idx; MutableDomItem rC = field(Fields::comments); if (auto rcPtr = rC.mutableAs()) { auto &postList = rcPtr->regionComments[regionName].postComments; idx = postList.length(); postList.append(comment); MutableDomItem res = path(Path::Field(Fields::comments) .field(Fields::regionComments) .key(regionName) .field(Fields::postComments) .index(idx)); Q_ASSERT(res); return res; } return MutableDomItem(); } QDebug operator<<(QDebug debug, const DomItem &c) { dumperToQDebug([&c](Sink s) { const_cast(&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(DomItem &self, DirectVisitor v) { 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(DomItem &self, OutWriter &ow, bool compact) const { ow.writeRegion(u"leftSquareBrace", u"["); 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(u"rightSquareBrace", u"]"); } } // end namespace Dom } // end namespace QQmlJS QT_END_NAMESPACE